modifying viewmodel to be more flexible -- preicon button and endiconbuttons

This commit is contained in:
sawka 2024-07-08 16:36:30 -07:00
parent 3b01234914
commit b97802b1ac
5 changed files with 125 additions and 56 deletions

View File

@ -84,8 +84,13 @@
gap: 8px; gap: 8px;
color: var(--main-text-color); color: var(--main-text-color);
.block-frame-back-button { .block-frame-preicon-button {
opacity: 0.7;
cursor: pointer; cursor: pointer;
&:hover {
opacity: 1;
}
} }
.block-frame-view-icon { .block-frame-view-icon {
@ -117,6 +122,16 @@
display: flex; display: flex;
align-items: center; align-items: center;
.block-frame-endicon-button {
opacity: 0.7;
cursor: pointer;
padding: 4px 6px;
&:hover {
opacity: 1;
}
}
.block-frame-settings { .block-frame-settings {
display: flex; display: flex;
width: 24px; width: 24px;

View File

@ -230,8 +230,8 @@ const BlockFrame_Default_Component = ({
let isFocused = jotai.useAtomValue(isFocusedAtom); let isFocused = jotai.useAtomValue(isFocusedAtom);
const viewIcon = jotai.useAtomValue(viewModel.viewIcon); const viewIcon = jotai.useAtomValue(viewModel.viewIcon);
const viewText = jotai.useAtomValue(viewModel.viewText); const viewText = jotai.useAtomValue(viewModel.viewText);
const hasBackButton = jotai.useAtomValue(viewModel.hasBackButton); const preIconButton = jotai.useAtomValue(viewModel.preIconButton);
const hasForwardButton = jotai.useAtomValue(viewModel.hasForwardButton); const endIconButtons = jotai.useAtomValue(viewModel.endIconButtons);
if (preview) { if (preview) {
isFocused = true; isFocused = true;
} }
@ -242,6 +242,43 @@ const BlockFrame_Default_Component = ({
if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) { if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
style.borderColor = blockData.meta["frame:bordercolor:focused"]; style.borderColor = blockData.meta["frame:bordercolor:focused"];
} }
let preIconButtonElem: JSX.Element = null;
if (preIconButton) {
preIconButtonElem = (
<div className="block-frame-preicon-button" title={preIconButton.title} onClick={preIconButton.click}>
<i className={util.makeIconClass(preIconButton.icon, true)} />
</div>
);
}
let endIconsElem: JSX.Element[] = [];
if (endIconButtons && endIconButtons.length > 0) {
for (let idx = 0; idx < endIconButtons.length; idx++) {
const button = endIconButtons[idx];
endIconsElem.push(
<div key={idx} className="block-frame-endicon-button" title={button.title} onClick={button.click}>
<i className={util.makeIconClass(button.icon, true)} />
</div>
);
}
}
endIconsElem.push(
<div
key="settings"
className="block-frame-endicon-button block-frame-settings"
onClick={(e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose)}
>
<i className="fa fa-solid fa-cog fa-fw" />
</div>
);
endIconsElem.push(
<div
key="close"
className={clsx("block-frame-endicon-button block-frame-default-close")}
onClick={layoutModel?.onClose}
>
<i className="fa fa-solid fa-xmark-large fa-fw" />
</div>
);
return ( return (
<div <div
className={clsx( className={clsx(
@ -262,11 +299,7 @@ const BlockFrame_Default_Component = ({
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose)} onContextMenu={(e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose)}
> >
<div className="block-frame-default-header-iconview"> <div className="block-frame-default-header-iconview">
{hasBackButton && !hasForwardButton && ( {preIconButtonElem}
<div className="block-frame-back-button" onClick={viewModel.onBack}>
<i className="fa fa-solid fa-chevron-left fa-fw" />
</div>
)}
<div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div> <div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>
<div className="block-frame-view-type">{blockViewToName(blockData?.view)}</div> <div className="block-frame-view-type">{blockViewToName(blockData?.view)}</div>
{settingsConfig?.blockheader?.showblockids && ( {settingsConfig?.blockheader?.showblockids && (
@ -275,19 +308,8 @@ const BlockFrame_Default_Component = ({
</div> </div>
{util.isBlank(viewText) ? null : <div className="block-frame-text">{viewText}</div>} {util.isBlank(viewText) ? null : <div className="block-frame-text">{viewText}</div>}
<div className="flex-spacer"></div> <div className="flex-spacer"></div>
<div className="block-frame-end-icons"> <div className="block-frame-end-icons">{endIconsElem}</div>
<div
className="block-frame-settings"
onClick={(e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose)}
>
<i className="fa fa-solid fa-cog fa-fw" />
</div>
<div className={clsx("block-frame-default-close")} onClick={layoutModel?.onClose}>
<i className="fa fa-solid fa-xmark-large fa-fw" />
</div>
</div>
</div> </div>
{preview ? <div className="block-frame-preview" /> : children} {preview ? <div className="block-frame-preview" /> : children}
</div> </div>
); );
@ -412,8 +434,8 @@ function makeDefaultViewModel(blockId: string): ViewModel {
const blockData = get(blockDataAtom); const blockData = get(blockDataAtom);
return blockData?.meta?.title; return blockData?.meta?.title;
}), }),
hasBackButton: jotai.atom(false), preIconButton: jotai.atom(null),
hasForwardButton: jotai.atom(false), endIconButtons: jotai.atom(null),
hasSearch: jotai.atom(false), hasSearch: jotai.atom(false),
}; };
return viewModel; return viewModel;

View File

@ -4,6 +4,7 @@
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil"; import * as keyutil from "@/util/keyutil";
import * as util from "@/util/util"; import * as util from "@/util/util";
import type { PreviewModel } from "@/view/preview";
import { import {
Row, Row,
Table, Table,
@ -30,7 +31,7 @@ interface DirectoryTableProps {
setFileName: (_: string) => void; setFileName: (_: string) => void;
setSearch: (_: string) => void; setSearch: (_: string) => void;
setSelectedPath: (_: string) => void; setSelectedPath: (_: string) => void;
setRefresh: React.Dispatch<React.SetStateAction<boolean>>; setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
} }
const columnHelper = createColumnHelper<FileInfo>(); const columnHelper = createColumnHelper<FileInfo>();
@ -143,7 +144,7 @@ function DirectoryTable({
setFileName, setFileName,
setSearch, setSearch,
setSelectedPath, setSelectedPath,
setRefresh, setRefreshVersion,
}: DirectoryTableProps) { }: DirectoryTableProps) {
let settings = jotai.useAtomValue(atoms.settingsConfigAtom); let settings = jotai.useAtomValue(atoms.settingsConfigAtom);
const getIconFromMimeType = React.useCallback( const getIconFromMimeType = React.useCallback(
@ -299,7 +300,7 @@ function DirectoryTable({
setFocusIndex={setFocusIndex} setFocusIndex={setFocusIndex}
setSearch={setSearch} setSearch={setSearch}
setSelectedPath={setSelectedPath} setSelectedPath={setSelectedPath}
setRefresh={setRefresh} setRefreshVersion={setRefreshVersion}
/> />
) : ( ) : (
<TableBody <TableBody
@ -311,7 +312,7 @@ function DirectoryTable({
setFocusIndex={setFocusIndex} setFocusIndex={setFocusIndex}
setSearch={setSearch} setSearch={setSearch}
setSelectedPath={setSelectedPath} setSelectedPath={setSelectedPath}
setRefresh={setRefresh} setRefreshVersion={setRefreshVersion}
/> />
)} )}
</div> </div>
@ -327,7 +328,7 @@ interface TableBodyProps {
setFileName: (_: string) => void; setFileName: (_: string) => void;
setSearch: (_: string) => void; setSearch: (_: string) => void;
setSelectedPath: (_: string) => void; setSelectedPath: (_: string) => void;
setRefresh: React.Dispatch<React.SetStateAction<boolean>>; setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
} }
function TableBody({ function TableBody({
@ -339,7 +340,7 @@ function TableBody({
setFileName, setFileName,
setSearch, setSearch,
setSelectedPath, setSelectedPath,
setRefresh, setRefreshVersion,
}: TableBodyProps) { }: TableBodyProps) {
const dummyLineRef = React.useRef<HTMLDivElement>(null); const dummyLineRef = React.useRef<HTMLDivElement>(null);
const parentRef = React.useRef<HTMLDivElement>(null); const parentRef = React.useRef<HTMLDivElement>(null);
@ -391,7 +392,7 @@ function TableBody({
label: "Delete File", label: "Delete File",
click: async () => { click: async () => {
await services.FileService.DeleteFile(path).catch((e) => console.log(e)); //todo these errors need a popup await services.FileService.DeleteFile(path).catch((e) => console.log(e)); //todo these errors need a popup
setRefresh((current) => !current); setRefreshVersion((current) => current + 1);
}, },
}); });
menu.push({ menu.push({
@ -402,7 +403,7 @@ function TableBody({
}); });
ContextMenuModel.showContextMenu(menu, e); ContextMenuModel.showContextMenu(menu, e);
}, },
[setRefresh] [setRefreshVersion]
); );
const displayRow = React.useCallback( const displayRow = React.useCallback(
@ -463,17 +464,26 @@ const MemoizedTableBody = React.memo(
interface DirectoryPreviewProps { interface DirectoryPreviewProps {
fileNameAtom: jotai.WritableAtom<string, [string], void>; fileNameAtom: jotai.WritableAtom<string, [string], void>;
model: PreviewModel;
} }
function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) { function DirectoryPreview({ fileNameAtom, model }: DirectoryPreviewProps) {
console.log("DirectoryPreview render");
const [searchText, setSearchText] = React.useState(""); const [searchText, setSearchText] = React.useState("");
const [focusIndex, setFocusIndex] = React.useState(0); const [focusIndex, setFocusIndex] = React.useState(0);
const [content, setContent] = React.useState<FileInfo[]>([]); const [content, setContent] = React.useState<FileInfo[]>([]);
const [fileName, setFileName] = jotai.useAtom(fileNameAtom); const [fileName, setFileName] = jotai.useAtom(fileNameAtom);
const [hideHiddenFiles, setHideHiddenFiles] = React.useState(true); const [hideHiddenFiles, setHideHiddenFiles] = jotai.useAtom(model.showHiddenFiles);
const [selectedPath, setSelectedPath] = React.useState(""); const [selectedPath, setSelectedPath] = React.useState("");
const [refresh, setRefresh] = React.useState(false); const [refreshVersion, setRefreshVersion] = React.useState(0);
React.useEffect(() => {
model.refreshCallback = () => {
setRefreshVersion((refreshVersion) => refreshVersion + 1);
};
return () => {
model.refreshCallback = null;
};
}, [setRefreshVersion]);
React.useEffect(() => { React.useEffect(() => {
const getContent = async () => { const getContent = async () => {
@ -489,7 +499,7 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
setContent(filtered); setContent(filtered);
}; };
getContent(); getContent();
}, [fileName, searchText, hideHiddenFiles, refresh]); }, [fileName, searchText, hideHiddenFiles, refreshVersion]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(waveEvent: WaveKeyboardEvent): boolean => { (waveEvent: WaveKeyboardEvent): boolean => {
@ -533,15 +543,6 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
autoFocus={true} autoFocus={true}
value={searchText} value={searchText}
/> />
<div onClick={() => setHideHiddenFiles((current) => !current)} className="dir-table-button">
{!hideHiddenFiles && <i className={"fa-sharp fa-solid fa-eye-slash"} />}
{hideHiddenFiles && <i className={"fa-sharp fa-solid fa-eye"} />}
<input type="text" value={searchText} onChange={() => {}}></input>
</div>
<div onClick={() => setRefresh((current) => !current)} className="dir-table-button">
<i className="fa-solid fa-arrows-rotate" />
<input type="text" value={searchText} onChange={() => {}}></input>
</div>
</div> </div>
<DirectoryTable <DirectoryTable
data={content} data={content}
@ -551,7 +552,7 @@ function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
setFocusIndex={setFocusIndex} setFocusIndex={setFocusIndex}
setSearch={setSearchText} setSearch={setSearchText}
setSelectedPath={setSelectedPath} setSelectedPath={setSelectedPath}
setRefresh={setRefresh} setRefreshVersion={setRefreshVersion}
/> />
</div> </div>
); );

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/element/markdown"; import { Markdown } from "@/element/markdown";
import { getBackendHostPort, getObjectId, globalStore, useBlockAtom } from "@/store/global"; import { getBackendHostPort, globalStore, useBlockAtom } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import * as util from "@/util/util"; import * as util from "@/util/util";
@ -26,8 +26,8 @@ export class PreviewModel implements ViewModel {
viewIcon: jotai.Atom<string>; viewIcon: jotai.Atom<string>;
viewName: jotai.Atom<string>; viewName: jotai.Atom<string>;
viewText: jotai.Atom<string>; viewText: jotai.Atom<string>;
hasBackButton: jotai.Atom<boolean>; preIconButton: jotai.Atom<IconButtonDecl>;
hasForwardButton: jotai.Atom<boolean>; endIconButtons: jotai.Atom<IconButtonDecl[]>;
hasSearch: jotai.Atom<boolean>; hasSearch: jotai.Atom<boolean>;
fileName: jotai.WritableAtom<string, [string], void>; fileName: jotai.WritableAtom<string, [string], void>;
@ -37,12 +37,16 @@ export class PreviewModel implements ViewModel {
fileMimeTypeLoadable: jotai.Atom<Loadable<string>>; fileMimeTypeLoadable: jotai.Atom<Loadable<string>>;
fileContent: jotai.Atom<Promise<string>>; fileContent: jotai.Atom<Promise<string>>;
showHiddenFiles: jotai.PrimitiveAtom<boolean>;
refreshCallback: () => void;
setPreviewFileName(fileName: string) { setPreviewFileName(fileName: string) {
services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName }); services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName });
} }
constructor(blockId: string) { constructor(blockId: string) {
this.blockId = blockId; this.blockId = blockId;
this.showHiddenFiles = jotai.atom(true);
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`); this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.viewIcon = jotai.atom((get) => { this.viewIcon = jotai.atom((get) => {
let blockData = get(this.blockAtom); let blockData = get(this.blockAtom);
@ -57,13 +61,34 @@ export class PreviewModel implements ViewModel {
this.viewText = jotai.atom((get) => { this.viewText = jotai.atom((get) => {
return get(this.fileName); return get(this.fileName);
}); });
this.hasBackButton = jotai.atom(true); this.preIconButton = jotai.atom((get) => {
this.hasForwardButton = jotai.atom((get) => {
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") { if (mimeType == "directory") {
return true; return null;
} }
return false; return {
icon: "chevron-left",
click: this.onBack.bind(this),
};
});
this.endIconButtons = jotai.atom((get) => {
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") {
let showHiddenFiles = get(this.showHiddenFiles);
return [
{
icon: showHiddenFiles ? "eye" : "eye-slash",
click: () => {
globalStore.set(this.showHiddenFiles, (prev) => !prev);
},
},
{
icon: "arrows-rotate",
click: () => this.refreshCallback?.(),
},
];
}
return null;
}); });
this.hasSearch = jotai.atom(false); this.hasSearch = jotai.atom(false);
@ -310,7 +335,6 @@ function iconForFile(mimeType: string, fileName: string): string {
} }
function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }) { function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }) {
console.log("render previewview", getObjectId(model));
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`); const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
const fileNameAtom = model.fileName; const fileNameAtom = model.fileName;
@ -359,7 +383,7 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel
) { ) {
specializedView = <CodeEditPreview readonly={true} contentAtom={fileContentAtom} filename={fileName} />; specializedView = <CodeEditPreview readonly={true} contentAtom={fileContentAtom} filename={fileName} />;
} else if (mimeType === "directory") { } else if (mimeType === "directory") {
specializedView = <DirectoryPreview fileNameAtom={fileNameAtom} />; specializedView = <DirectoryPreview fileNameAtom={fileNameAtom} model={model} />;
} else { } else {
specializedView = ( specializedView = (
<div className="view-preview"> <div className="view-preview">

View File

@ -116,12 +116,19 @@ declare global {
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void }; type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
type IconButtonDecl = {
icon: string;
title?: string;
click: () => void;
};
interface ViewModel { interface ViewModel {
viewIcon: jotai.Atom<string>; viewIcon: jotai.Atom<string>;
viewName: jotai.Atom<string>; viewName: jotai.Atom<string>;
viewText: jotai.Atom<string>; viewText: jotai.Atom<string>;
hasBackButton: jotai.Atom<boolean>; preIconButton: jotai.Atom<IconButtonDecl>;
hasForwardButton: jotai.Atom<boolean>; endIconButtons: jotai.Atom<IconButtonDecl[]>;
hasSearch: jotai.Atom<boolean>; hasSearch: jotai.Atom<boolean>;
onBack?: () => void; onBack?: () => void;