// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { ContextMenuModel } from "@/app/store/contextmenu"; import { Markdown } from "@/element/markdown"; import { createBlock, globalStore, useBlockAtom } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import { getWebServerEndpoint } from "@/util/endpoints"; import * as util from "@/util/util"; import clsx from "clsx"; import * as jotai from "jotai"; import { loadable } from "jotai/utils"; import { useEffect, useRef } from "react"; import { CenteredDiv } from "../../element/quickelems"; import { CodeEditor } from "../codeeditor/codeeditor"; import { CSVView } from "./csvview"; import { DirectoryPreview } from "./directorypreview"; import "./preview.less"; const MaxFileSize = 1024 * 1024 * 10; // 10MB const MaxCSVSize = 1024 * 1024 * 1; // 1MB function isTextFile(mimeType: string): boolean { return ( mimeType.startsWith("text/") || mimeType == "application/sql" || (mimeType.startsWith("application/") && (mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml"))) || mimeType == "application/pem-certificate-chain" ); } export class PreviewModel implements ViewModel { blockId: string; blockAtom: jotai.Atom; viewIcon: jotai.Atom; viewName: jotai.Atom; viewText: jotai.Atom; preIconButton: jotai.Atom; endIconButtons: jotai.Atom; ceReadOnly: jotai.PrimitiveAtom; isCeView: jotai.PrimitiveAtom; fileName: jotai.WritableAtom; connection: jotai.Atom; statFile: jotai.Atom>; fullFile: jotai.Atom>; fileMimeType: jotai.Atom>; fileMimeTypeLoadable: jotai.Atom>; fileContent: jotai.Atom>; newFileContent: jotai.PrimitiveAtom; showHiddenFiles: jotai.PrimitiveAtom; refreshVersion: jotai.PrimitiveAtom; refreshCallback: () => void; directoryInputElem: HTMLInputElement; setPreviewFileName(fileName: string) { services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName }); } constructor(blockId: string) { this.blockId = blockId; this.showHiddenFiles = jotai.atom(true); this.refreshVersion = jotai.atom(0); this.ceReadOnly = jotai.atom(true); this.isCeView = jotai.atom(false); this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); this.viewIcon = jotai.atom((get) => { let blockData = get(this.blockAtom); if (blockData?.meta?.icon) { return blockData.meta.icon; } const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { return { elemtype: "iconbutton", icon: "folder-open", longClick: (e: React.MouseEvent) => { let menuItems: ContextMenuItem[] = []; menuItems.push({ label: "Go to Home", click: () => globalStore.set(this.fileName, "~") }); menuItems.push({ label: "Go to Desktop", click: () => globalStore.set(this.fileName, "~/Desktop"), }); menuItems.push({ label: "Go to Downloads", click: () => globalStore.set(this.fileName, "~/Downloads"), }); menuItems.push({ label: "Go to Documents", click: () => globalStore.set(this.fileName, "~/Documents"), }); menuItems.push({ label: "Go to Root", click: () => globalStore.set(this.fileName, "/") }); ContextMenuModel.showContextMenu(menuItems, e); }, }; } const fileName = get(this.fileName); return iconForFile(mimeType, fileName); }); this.viewName = jotai.atom("Preview"); this.viewText = jotai.atom((get) => { if (get(this.isCeView)) { const viewTextChildren: HeaderElem[] = [ { elemtype: "input", value: get(this.fileName), isDisabled: true, }, ]; if (get(this.ceReadOnly) == false) { let saveClassName = "secondary"; if (get(this.newFileContent) !== null) { saveClassName = "primary"; } viewTextChildren.push( { elemtype: "textbutton", text: "Save", className: clsx( `${saveClassName} warning border-radius-4 vertical-padding-2 horizontal-padding-10` ), onClick: this.handleFileSave.bind(this), }, { elemtype: "textbutton", text: "Cancel", className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10", onClick: () => this.toggleCodeEditorReadOnly(true), } ); } else { viewTextChildren.push({ elemtype: "textbutton", text: "Edit", className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10", onClick: () => this.toggleCodeEditorReadOnly(false), }); } return [ { elemtype: "div", children: viewTextChildren, }, ] as HeaderElem[]; } else { return [ { elemtype: "text", text: get(this.fileName), }, ]; } }); this.preIconButton = jotai.atom((get) => { const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { return null; } return { elemtype: "iconbutton", icon: "chevron-left", click: this.handleBack.bind(this), }; }); this.endIconButtons = jotai.atom((get) => { const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { let showHiddenFiles = get(this.showHiddenFiles); return [ { elemtype: "iconbutton", icon: showHiddenFiles ? "eye" : "eye-slash", click: () => { globalStore.set(this.showHiddenFiles, (prev) => !prev); }, }, { elemtype: "iconbutton", icon: "arrows-rotate", click: () => this.refreshCallback?.(), }, ]; } return null; }); this.fileName = jotai.atom( (get) => { return get(this.blockAtom)?.meta?.file; }, (get, set, update) => { services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update }); } ); this.connection = jotai.atom((get) => { return get(this.blockAtom)?.meta?.connection; }); this.statFile = jotai.atom>(async (get) => { const fileName = get(this.fileName); if (fileName == null) { return null; } const conn = get(this.connection) ?? ""; const statFile = await services.FileService.StatFile(conn, fileName); return statFile; }); this.fullFile = jotai.atom>(async (get) => { const fileName = get(this.fileName); if (fileName == null) { return null; } const conn = get(this.connection) ?? ""; const file = await services.FileService.ReadFile(conn, fileName); return file; }); this.fileMimeType = jotai.atom>(async (get) => { const fileInfo = await get(this.statFile); return fileInfo?.mimetype; }); this.fileMimeTypeLoadable = loadable(this.fileMimeType); this.fileContent = jotai.atom>(async (get) => { const fullFile = await get(this.fullFile); return util.base64ToString(fullFile?.data64); }); this.newFileContent = jotai.atom(null) as jotai.PrimitiveAtom; this.handleBack = this.handleBack.bind(this); } handleBack() { const fileName = globalStore.get(this.fileName); if (fileName == null) { return; } const splitPath = fileName.split("/"); console.log("splitPath-1", splitPath); splitPath.pop(); console.log("splitPath-2", splitPath); const newPath = splitPath.join("/"); globalStore.set(this.fileName, newPath); } toggleCodeEditorReadOnly(readOnly: boolean) { globalStore.set(this.ceReadOnly, readOnly); } async handleFileSave() { const fileName = globalStore.get(this.fileName); const newFileContent = globalStore.get(this.newFileContent); const conn = globalStore.get(this.connection) ?? ""; try { services.FileService.SaveFile(conn, fileName, util.stringToBase64(newFileContent)); globalStore.set(this.newFileContent, null); } catch (error) { console.error("Error saving file:", error); } } getSettingsMenuItems(): ContextMenuItem[] { const menuItems: ContextMenuItem[] = []; menuItems.push({ label: "Copy Full Path", click: () => { const fileName = globalStore.get(this.fileName); if (fileName == null) { return; } navigator.clipboard.writeText(fileName); }, }); menuItems.push({ label: "Copy File Name", click: () => { let fileName = globalStore.get(this.fileName); if (fileName == null) { return; } if (fileName.endsWith("/")) { fileName = fileName.substring(0, fileName.length - 1); } const splitPath = fileName.split("/"); const baseName = splitPath[splitPath.length - 1]; navigator.clipboard.writeText(baseName); }, }); const mimeType = util.jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { menuItems.push({ label: "Open Terminal in New Block", click: async () => { const termBlockDef: BlockDef = { meta: { view: "term", controller: "shell", "cmd:cwd": globalStore.get(this.fileName), }, }; await createBlock(termBlockDef); }, }); } return menuItems; } giveFocus(): boolean { if (this.directoryInputElem) { this.directoryInputElem.focus({ preventScroll: true }); return true; } return false; } } function makePreviewModel(blockId: string): PreviewModel { const previewModel = new PreviewModel(blockId); return previewModel; } function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom }) { const [cwd, setCwd] = jotai.useAtom(cwdAtom); if (cwd == null || cwd == "") { return null; } let splitNav = [cwd]; let remaining = cwd; let idx = remaining.lastIndexOf("/"); while (idx !== -1) { remaining = remaining.substring(0, idx); splitNav.unshift(remaining); idx = remaining.lastIndexOf("/"); } if (splitNav.length === 0) { splitNav = [cwd]; } return (
{splitNav.map((item, idx) => { let splitPath = item.split("/"); if (splitPath.length === 0) { splitPath = [item]; } const isLast = idx == splitNav.length - 1; let baseName = splitPath[splitPath.length - 1]; if (!isLast) { baseName += "/"; } return (
setCwd(item)} > {baseName}
); })}
); } function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom> }) { const readmeText = jotai.useAtomValue(contentAtom); return (
); } function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) { const filePath = fileInfo.path; const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(filePath); if (fileInfo.mimetype == "application/pdf") { return (