mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
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:
parent
4d4e026749
commit
ecd2464bbf
@ -1,6 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Button } from "@/element/button";
|
||||
import * as services from "@/store/services";
|
||||
import * as util from "@/util/util";
|
||||
import {
|
||||
@ -12,6 +13,7 @@ import {
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import clsx from "clsx";
|
||||
import dayjs from "dayjs";
|
||||
import * as jotai from "jotai";
|
||||
import React from "react";
|
||||
import { ContextMenuModel } from "../store/contextmenu";
|
||||
@ -21,12 +23,12 @@ import "./directorypreview.less";
|
||||
|
||||
interface DirectoryTableProps {
|
||||
data: FileInfo[];
|
||||
cwd: string;
|
||||
focusIndex: number;
|
||||
enter: boolean;
|
||||
setFocusIndex: (_: number) => void;
|
||||
setFileName: (_: string) => void;
|
||||
setSearch: (_: string) => void;
|
||||
setSelectedPath: (_: string) => void;
|
||||
setRefresh: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<FileInfo>();
|
||||
@ -83,15 +85,17 @@ function getSpecificUnit(bytes: number, suffix: string): string {
|
||||
return `${bytes / divisor} ${displaySuffixes[suffix]}`;
|
||||
}
|
||||
|
||||
function getLastModifiedTime(
|
||||
unixMillis: number,
|
||||
locale: Intl.LocalesArgument,
|
||||
options: DateTimeFormatConfigType
|
||||
): string {
|
||||
if (locale === "C") {
|
||||
locale = "lookup";
|
||||
function getLastModifiedTime(unixMillis: number): string {
|
||||
let fileDatetime = dayjs(new Date(unixMillis));
|
||||
let nowDatetime = dayjs(new Date());
|
||||
|
||||
if (nowDatetime.year() != fileDatetime.year()) {
|
||||
return dayjs(fileDatetime).format("M/D/YY");
|
||||
} 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- ]+$/;
|
||||
@ -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 {
|
||||
if (input == "") {
|
||||
return "-";
|
||||
}
|
||||
const truncated = input.split(";")[0];
|
||||
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);
|
||||
const getIconFromMimeType = React.useCallback(
|
||||
(mimeType: string): string => {
|
||||
@ -198,16 +184,12 @@ function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileNa
|
||||
}),
|
||||
columnHelper.accessor("modestr", {
|
||||
cell: (info) => <span className="dir-table-modestr">{info.getValue()}</span>,
|
||||
header: () => <span>Permissions</span>,
|
||||
header: () => <span>Perm</span>,
|
||||
size: 91,
|
||||
sortingFn: "alphanumeric",
|
||||
}),
|
||||
columnHelper.accessor("modtime", {
|
||||
cell: (info) => (
|
||||
<span className="dir-table-lastmod">
|
||||
{getLastModifiedTime(info.getValue(), settings.datetime.locale, settings.datetime.format)}
|
||||
</span>
|
||||
),
|
||||
cell: (info) => <span className="dir-table-lastmod">{getLastModifiedTime(info.getValue())}</span>,
|
||||
header: () => <span>Last Modified</span>,
|
||||
size: 185,
|
||||
sortingFn: "datetime",
|
||||
@ -290,22 +272,22 @@ function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileNa
|
||||
{table.getState().columnSizingInfo.isResizingColumn ? (
|
||||
<MemoizedTableBody
|
||||
table={table}
|
||||
cwd={cwd}
|
||||
focusIndex={focusIndex}
|
||||
enter={enter}
|
||||
setFileName={setFileName}
|
||||
setFocusIndex={setFocusIndex}
|
||||
setSearch={setSearch}
|
||||
setSelectedPath={setSelectedPath}
|
||||
setRefresh={setRefresh}
|
||||
/>
|
||||
) : (
|
||||
<TableBody
|
||||
table={table}
|
||||
cwd={cwd}
|
||||
focusIndex={focusIndex}
|
||||
enter={enter}
|
||||
setFileName={setFileName}
|
||||
setFocusIndex={setFocusIndex}
|
||||
setSearch={setSearch}
|
||||
setSelectedPath={setSelectedPath}
|
||||
setRefresh={setRefresh}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -314,26 +296,60 @@ function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileNa
|
||||
|
||||
interface TableBodyProps {
|
||||
table: Table<FileInfo>;
|
||||
cwd: string;
|
||||
focusIndex: number;
|
||||
enter: boolean;
|
||||
setFocusIndex: (_: number) => void;
|
||||
setFileName: (_: string) => void;
|
||||
setSearch: (_: string) => void;
|
||||
setSelectedPath: (_: string) => void;
|
||||
setRefresh: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
function TableBody({ table, cwd, focusIndex, enter, setFocusIndex, setFileName, setSearch }: TableBodyProps) {
|
||||
let [refresh, setRefresh] = React.useState(false);
|
||||
|
||||
function TableBody({
|
||||
table,
|
||||
focusIndex,
|
||||
setFocusIndex,
|
||||
setFileName,
|
||||
setSearch,
|
||||
setSelectedPath,
|
||||
setRefresh,
|
||||
}: TableBodyProps) {
|
||||
React.useEffect(() => {
|
||||
const selected = (table.getSortedRowModel()?.flatRows[focusIndex]?.getValue("path") as string) ?? null;
|
||||
if (selected != null) {
|
||||
setFileName(selected);
|
||||
setSearch("");
|
||||
}
|
||||
}, [enter]);
|
||||
setSelectedPath((table.getSortedRowModel()?.flatRows[focusIndex]?.getValue("path") as string) ?? null);
|
||||
}, [table, focusIndex]);
|
||||
|
||||
const handleFileContextMenu = React.useCallback(
|
||||
(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
|
||||
setRefresh((current) => !current);
|
||||
},
|
||||
});
|
||||
menu.push({
|
||||
label: "Download File",
|
||||
click: async () => {
|
||||
getApi().downloadFile(path);
|
||||
},
|
||||
});
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
},
|
||||
[setRefresh]
|
||||
);
|
||||
|
||||
table.getRow;
|
||||
return (
|
||||
<div className="dir-table-body">
|
||||
{table.getRowModel().rows.map((row, idx) => (
|
||||
@ -376,10 +392,12 @@ interface DirectoryPreviewProps {
|
||||
|
||||
function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
||||
const [searchText, setSearchText] = React.useState("");
|
||||
let [focusIndex, setFocusIndex] = React.useState(0);
|
||||
const [focusIndex, setFocusIndex] = React.useState(0);
|
||||
const [content, setContent] = React.useState<FileInfo[]>([]);
|
||||
let [fileName, setFileName] = jotai.useAtom(fileNameAtom);
|
||||
const [enter, setEnter] = React.useState(false);
|
||||
const [fileName, setFileName] = jotai.useAtom(fileNameAtom);
|
||||
const [hideHiddenFiles, setHideHiddenFiles] = React.useState(true);
|
||||
const [selectedPath, setSelectedPath] = React.useState("");
|
||||
const [refresh, setRefresh] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const getContent = async () => {
|
||||
@ -387,12 +405,15 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
||||
const serializedContent = util.base64ToString(file?.data64);
|
||||
let content: FileInfo[] = JSON.parse(serializedContent);
|
||||
let filtered = content.filter((fileInfo) => {
|
||||
if (hideHiddenFiles && fileInfo.name.startsWith(".")) {
|
||||
return false;
|
||||
}
|
||||
return fileInfo.name.toLowerCase().includes(searchText);
|
||||
});
|
||||
setContent(filtered);
|
||||
};
|
||||
getContent();
|
||||
}, [fileName, searchText]);
|
||||
}, [fileName, searchText, hideHiddenFiles, refresh]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(e) => {
|
||||
@ -410,16 +431,13 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
setEnter((current) => !current);
|
||||
/*
|
||||
const fullPath = fileName.concat("/", newFileName);
|
||||
setFileName(fullPath);
|
||||
*/
|
||||
setFileName(selectedPath);
|
||||
setSearchText("");
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[content, focusIndex, setEnter]
|
||||
[content, focusIndex, selectedPath]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -443,15 +461,23 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
||||
autoFocus={true}
|
||||
value={searchText}
|
||||
/>
|
||||
<Button onClick={() => setHideHiddenFiles((current) => !current)}>
|
||||
Hidden Files:
|
||||
{!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>
|
||||
<DirectoryTable
|
||||
data={content}
|
||||
cwd={fileName}
|
||||
focusIndex={focusIndex}
|
||||
enter={enter}
|
||||
setFileName={setFileName}
|
||||
setFocusIndex={setFocusIndex}
|
||||
setSearch={setSearchText}
|
||||
setSelectedPath={setSelectedPath}
|
||||
setRefresh={setRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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
|
||||
}
|
@ -27,17 +27,6 @@ type TerminalConfigType struct {
|
||||
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 {
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
@ -49,7 +38,6 @@ type BlockHeaderOpts struct {
|
||||
|
||||
type SettingsConfigType struct {
|
||||
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
|
||||
DateTime DateTimeConfigType `json:"datetime"`
|
||||
Term TerminalConfigType `json:"term"`
|
||||
Widgets []WidgetsConfigType `json:"widgets"`
|
||||
BlockHeader BlockHeaderOpts `json:"blockheader"`
|
||||
@ -57,13 +45,6 @@ type SettingsConfigType struct {
|
||||
|
||||
func getSettingsConfigDefaults() SettingsConfigType {
|
||||
return SettingsConfigType{
|
||||
DateTime: DateTimeConfigType{
|
||||
Locale: wavebase.DetermineLocale(),
|
||||
Format: DateTimeFormatConfigType{
|
||||
DateStyle: DateTimeStyleMedium,
|
||||
TimeStyle: DateTimeStyleMedium,
|
||||
},
|
||||
},
|
||||
MimeTypes: map[string]MimeTypeConfigType{
|
||||
"audio": {Icon: "file-audio"},
|
||||
"application/pdf": {Icon: "file-pdf"},
|
||||
|
Loading…
Reference in New Issue
Block a user