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.
|
// 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:
|
||||||
|
{!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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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"`
|
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"},
|
||||||
|
Loading…
Reference in New Issue
Block a user