Hidden Files (#86)

Adds the following changes
- rename "Permissions" to "Perm"
- use a "-" if the mimetype is unknown
- add a button to hide and show hidden files
- fix the datetime to format based on how far in the past the date is
This commit is contained in:
Sylvie Crowe 2024-06-27 18:13:42 -07:00 committed by GitHub
parent 4d4e026749
commit ecd2464bbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 100 additions and 152 deletions

View File

@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Button } from "@/element/button";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as util from "@/util/util"; import * as util from "@/util/util";
import { import {
@ -12,6 +13,7 @@ import {
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import clsx from "clsx"; import clsx from "clsx";
import dayjs from "dayjs";
import * as jotai from "jotai"; import * as jotai from "jotai";
import React from "react"; import React from "react";
import { ContextMenuModel } from "../store/contextmenu"; import { ContextMenuModel } from "../store/contextmenu";
@ -21,12 +23,12 @@ import "./directorypreview.less";
interface DirectoryTableProps { interface DirectoryTableProps {
data: FileInfo[]; data: FileInfo[];
cwd: string;
focusIndex: number; focusIndex: number;
enter: boolean;
setFocusIndex: (_: number) => void; setFocusIndex: (_: number) => void;
setFileName: (_: string) => void; setFileName: (_: string) => void;
setSearch: (_: string) => void; setSearch: (_: string) => void;
setSelectedPath: (_: string) => void;
setRefresh: React.Dispatch<React.SetStateAction<boolean>>;
} }
const columnHelper = createColumnHelper<FileInfo>(); const columnHelper = createColumnHelper<FileInfo>();
@ -83,15 +85,17 @@ function getSpecificUnit(bytes: number, suffix: string): string {
return `${bytes / divisor} ${displaySuffixes[suffix]}`; return `${bytes / divisor} ${displaySuffixes[suffix]}`;
} }
function getLastModifiedTime( function getLastModifiedTime(unixMillis: number): string {
unixMillis: number, let fileDatetime = dayjs(new Date(unixMillis));
locale: Intl.LocalesArgument, let nowDatetime = dayjs(new Date());
options: DateTimeFormatConfigType
): string { if (nowDatetime.year() != fileDatetime.year()) {
if (locale === "C") { return dayjs(fileDatetime).format("M/D/YY");
locale = "lookup"; } else if (nowDatetime.month() != fileDatetime.month()) {
return dayjs(fileDatetime).format("MMM D");
} else {
return dayjs(fileDatetime).format("h:mm A");
} }
return new Date(unixMillis).toLocaleString(locale, options); //todo use config
} }
const iconRegex = /^[a-z0-9- ]+$/; const iconRegex = /^[a-z0-9- ]+$/;
@ -121,41 +125,23 @@ function getSortIcon(sortType: string | boolean): React.ReactNode {
} }
} }
function handleFileContextMenu(e: React.MouseEvent<HTMLDivElement>, path: string) {
e.preventDefault();
e.stopPropagation();
let menu: ContextMenuItem[] = [];
menu.push({
label: "Open in New Block",
click: async () => {
const blockDef = {
view: "preview",
meta: { file: path },
};
await createBlock(blockDef);
},
});
menu.push({
label: "Delete File",
click: async () => {
await services.FileService.DeleteFile(path).catch((e) => console.log(e)); //todo these errors need a popup
},
});
menu.push({
label: "Download File",
click: async () => {
getApi().downloadFile(path);
},
});
ContextMenuModel.showContextMenu(menu, e);
}
function cleanMimetype(input: string): string { function cleanMimetype(input: string): string {
if (input == "") {
return "-";
}
const truncated = input.split(";")[0]; const truncated = input.split(";")[0];
return truncated.trim(); return truncated.trim();
} }
function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileName, setSearch }: DirectoryTableProps) { function DirectoryTable({
data,
focusIndex,
setFocusIndex,
setFileName,
setSearch,
setSelectedPath,
setRefresh,
}: DirectoryTableProps) {
let settings = jotai.useAtomValue(atoms.settingsConfigAtom); let settings = jotai.useAtomValue(atoms.settingsConfigAtom);
const getIconFromMimeType = React.useCallback( const getIconFromMimeType = React.useCallback(
(mimeType: string): string => { (mimeType: string): string => {
@ -198,16 +184,12 @@ function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileNa
}), }),
columnHelper.accessor("modestr", { columnHelper.accessor("modestr", {
cell: (info) => <span className="dir-table-modestr">{info.getValue()}</span>, cell: (info) => <span className="dir-table-modestr">{info.getValue()}</span>,
header: () => <span>Permissions</span>, header: () => <span>Perm</span>,
size: 91, size: 91,
sortingFn: "alphanumeric", sortingFn: "alphanumeric",
}), }),
columnHelper.accessor("modtime", { columnHelper.accessor("modtime", {
cell: (info) => ( cell: (info) => <span className="dir-table-lastmod">{getLastModifiedTime(info.getValue())}</span>,
<span className="dir-table-lastmod">
{getLastModifiedTime(info.getValue(), settings.datetime.locale, settings.datetime.format)}
</span>
),
header: () => <span>Last Modified</span>, header: () => <span>Last Modified</span>,
size: 185, size: 185,
sortingFn: "datetime", sortingFn: "datetime",
@ -290,22 +272,22 @@ function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileNa
{table.getState().columnSizingInfo.isResizingColumn ? ( {table.getState().columnSizingInfo.isResizingColumn ? (
<MemoizedTableBody <MemoizedTableBody
table={table} table={table}
cwd={cwd}
focusIndex={focusIndex} focusIndex={focusIndex}
enter={enter}
setFileName={setFileName} setFileName={setFileName}
setFocusIndex={setFocusIndex} setFocusIndex={setFocusIndex}
setSearch={setSearch} setSearch={setSearch}
setSelectedPath={setSelectedPath}
setRefresh={setRefresh}
/> />
) : ( ) : (
<TableBody <TableBody
table={table} table={table}
cwd={cwd}
focusIndex={focusIndex} focusIndex={focusIndex}
enter={enter}
setFileName={setFileName} setFileName={setFileName}
setFocusIndex={setFocusIndex} setFocusIndex={setFocusIndex}
setSearch={setSearch} setSearch={setSearch}
setSelectedPath={setSelectedPath}
setRefresh={setRefresh}
/> />
)} )}
</div> </div>
@ -314,26 +296,60 @@ function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileNa
interface TableBodyProps { interface TableBodyProps {
table: Table<FileInfo>; table: Table<FileInfo>;
cwd: string;
focusIndex: number; focusIndex: number;
enter: boolean;
setFocusIndex: (_: number) => void; setFocusIndex: (_: number) => void;
setFileName: (_: string) => void; setFileName: (_: string) => void;
setSearch: (_: string) => void; setSearch: (_: string) => void;
setSelectedPath: (_: string) => void;
setRefresh: React.Dispatch<React.SetStateAction<boolean>>;
} }
function TableBody({ table, cwd, focusIndex, enter, setFocusIndex, setFileName, setSearch }: TableBodyProps) { function TableBody({
let [refresh, setRefresh] = React.useState(false); table,
focusIndex,
setFocusIndex,
setFileName,
setSearch,
setSelectedPath,
setRefresh,
}: TableBodyProps) {
React.useEffect(() => { React.useEffect(() => {
const selected = (table.getSortedRowModel()?.flatRows[focusIndex]?.getValue("path") as string) ?? null; setSelectedPath((table.getSortedRowModel()?.flatRows[focusIndex]?.getValue("path") as string) ?? null);
if (selected != null) { }, [table, focusIndex]);
setFileName(selected);
setSearch(""); const handleFileContextMenu = React.useCallback(
} (e: React.MouseEvent<HTMLDivElement>, path: string) => {
}, [enter]); e.preventDefault();
e.stopPropagation();
let menu: ContextMenuItem[] = [];
menu.push({
label: "Open in New Block",
click: async () => {
const blockDef = {
view: "preview",
meta: { file: path },
};
await createBlock(blockDef);
},
});
menu.push({
label: "Delete File",
click: async () => {
await services.FileService.DeleteFile(path).catch((e) => console.log(e)); //todo these errors need a popup
setRefresh((current) => !current);
},
});
menu.push({
label: "Download File",
click: async () => {
getApi().downloadFile(path);
},
});
ContextMenuModel.showContextMenu(menu, e);
},
[setRefresh]
);
table.getRow;
return ( return (
<div className="dir-table-body"> <div className="dir-table-body">
{table.getRowModel().rows.map((row, idx) => ( {table.getRowModel().rows.map((row, idx) => (
@ -376,10 +392,12 @@ interface DirectoryPreviewProps {
function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) { function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
const [searchText, setSearchText] = React.useState(""); const [searchText, setSearchText] = React.useState("");
let [focusIndex, setFocusIndex] = React.useState(0); const [focusIndex, setFocusIndex] = React.useState(0);
const [content, setContent] = React.useState<FileInfo[]>([]); const [content, setContent] = React.useState<FileInfo[]>([]);
let [fileName, setFileName] = jotai.useAtom(fileNameAtom); const [fileName, setFileName] = jotai.useAtom(fileNameAtom);
const [enter, setEnter] = React.useState(false); const [hideHiddenFiles, setHideHiddenFiles] = React.useState(true);
const [selectedPath, setSelectedPath] = React.useState("");
const [refresh, setRefresh] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
const getContent = async () => { const getContent = async () => {
@ -387,12 +405,15 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
const serializedContent = util.base64ToString(file?.data64); const serializedContent = util.base64ToString(file?.data64);
let content: FileInfo[] = JSON.parse(serializedContent); let content: FileInfo[] = JSON.parse(serializedContent);
let filtered = content.filter((fileInfo) => { let filtered = content.filter((fileInfo) => {
if (hideHiddenFiles && fileInfo.name.startsWith(".")) {
return false;
}
return fileInfo.name.toLowerCase().includes(searchText); return fileInfo.name.toLowerCase().includes(searchText);
}); });
setContent(filtered); setContent(filtered);
}; };
getContent(); getContent();
}, [fileName, searchText]); }, [fileName, searchText, hideHiddenFiles, refresh]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(e) => { (e) => {
@ -410,16 +431,13 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
break; break;
case "Enter": case "Enter":
e.preventDefault(); e.preventDefault();
setEnter((current) => !current); setFileName(selectedPath);
/* setSearchText("");
const fullPath = fileName.concat("/", newFileName);
setFileName(fullPath);
*/
break; break;
default: default:
} }
}, },
[content, focusIndex, setEnter] [content, focusIndex, selectedPath]
); );
React.useEffect(() => { React.useEffect(() => {
@ -443,15 +461,23 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
autoFocus={true} autoFocus={true}
value={searchText} value={searchText}
/> />
<Button onClick={() => setHideHiddenFiles((current) => !current)}>
Hidden Files:&nbsp;
{!hideHiddenFiles && <i className={"fa-sharp fa-solid fa-eye-slash"} />}
{hideHiddenFiles && <i className={"fa-sharp fa-solid fa-eye"} />}
</Button>
<Button onClick={() => setRefresh((current) => !current)}>
<i className="fa-solid fa-arrows-rotate" />
</Button>
</div> </div>
<DirectoryTable <DirectoryTable
data={content} data={content}
cwd={fileName}
focusIndex={focusIndex} focusIndex={focusIndex}
enter={enter}
setFileName={setFileName} setFileName={setFileName}
setFocusIndex={setFocusIndex} setFocusIndex={setFocusIndex}
setSearch={setSearchText} setSearch={setSearchText}
setSelectedPath={setSelectedPath}
setRefresh={setRefresh}
/> />
</> </>
); );

View File

@ -1,59 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wconfig
import (
"encoding/json"
"fmt"
)
type DateTimeStyle uint8
const (
DateTimeStyleFull = iota + 1
DateTimeStyleLong
DateTimeStyleMedium
DateTimeStyleShort
)
var dateTimeStyleToString = map[uint8]string{
1: "full",
2: "long",
3: "medium",
4: "short",
}
var stringToDateTimeStyle = map[string]uint8{
"full": 1,
"long": 2,
"medium": 3,
"short": 4,
}
func (dts DateTimeStyle) String() string {
return dateTimeStyleToString[uint8(dts)]
}
func parseDateTimeStyle(input string) (DateTimeStyle, error) {
value, ok := stringToDateTimeStyle[input]
if !ok {
return DateTimeStyle(0), fmt.Errorf("%q is not a valid date-time style", input)
}
return DateTimeStyle(value), nil
}
func (dts DateTimeStyle) MarshalJSON() ([]byte, error) {
return json.Marshal(dts.String())
}
func (dts *DateTimeStyle) UnmarshalJSON(data []byte) (err error) {
var buffer string
if err := json.Unmarshal(data, &buffer); err != nil {
return err
}
if *dts, err = parseDateTimeStyle(buffer); err != nil {
return err
}
return nil
}

View File

@ -27,17 +27,6 @@ type TerminalConfigType struct {
FontFamily string `json:"fontfamily,omitempty"` FontFamily string `json:"fontfamily,omitempty"`
} }
type DateTimeConfigType struct {
Locale string `json:"locale"`
Format DateTimeFormatConfigType `json:"format"`
}
type DateTimeFormatConfigType struct {
DateStyle DateTimeStyle `json:"dateStyle"`
TimeStyle DateTimeStyle `json:"timeStyle"`
//TimeZone string `json:"timeZone"` TODO: need a universal way to obtain this before adding it
}
type MimeTypeConfigType struct { type MimeTypeConfigType struct {
Icon string `json:"icon"` Icon string `json:"icon"`
Color string `json:"color"` Color string `json:"color"`
@ -49,7 +38,6 @@ type BlockHeaderOpts struct {
type SettingsConfigType struct { type SettingsConfigType struct {
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
DateTime DateTimeConfigType `json:"datetime"`
Term TerminalConfigType `json:"term"` Term TerminalConfigType `json:"term"`
Widgets []WidgetsConfigType `json:"widgets"` Widgets []WidgetsConfigType `json:"widgets"`
BlockHeader BlockHeaderOpts `json:"blockheader"` BlockHeader BlockHeaderOpts `json:"blockheader"`
@ -57,13 +45,6 @@ type SettingsConfigType struct {
func getSettingsConfigDefaults() SettingsConfigType { func getSettingsConfigDefaults() SettingsConfigType {
return SettingsConfigType{ return SettingsConfigType{
DateTime: DateTimeConfigType{
Locale: wavebase.DetermineLocale(),
Format: DateTimeFormatConfigType{
DateStyle: DateTimeStyleMedium,
TimeStyle: DateTimeStyleMedium,
},
},
MimeTypes: map[string]MimeTypeConfigType{ MimeTypes: map[string]MimeTypeConfigType{
"audio": {Icon: "file-audio"}, "audio": {Icon: "file-audio"},
"application/pdf": {Icon: "file-pdf"}, "application/pdf": {Icon: "file-pdf"},