mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-23 16:58:30 +00:00
refactor preview.tsx to make it more manageable (#2351)
no functionality changes, but splits the giant preview.tsx to a model file and the sub-views for more manageable code.
This commit is contained in:
parent
88747742c9
commit
3b9aab207f
7 changed files with 1068 additions and 1018 deletions
|
|
@ -10,7 +10,7 @@ import {
|
|||
SubBlockProps,
|
||||
} from "@/app/block/blocktypes";
|
||||
import { LauncherViewModel } from "@/app/view/launcher/launcher";
|
||||
import { PreviewModel } from "@/app/view/preview/preview";
|
||||
import { PreviewModel } from "@/app/view/preview/preview-model";
|
||||
import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
|
||||
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { ContextMenuModel } from "@/app/store/contextmenu";
|
|||
import { atoms, getApi, globalStore } from "@/app/store/global";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { type PreviewModel } from "@/app/view/preview/preview";
|
||||
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
|
||||
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
|
||||
import { addOpenMenuItems } from "@/util/previewutil";
|
||||
|
|
@ -34,6 +33,7 @@ import { useDrag, useDrop } from "react-dnd";
|
|||
import { quote as shellQuote } from "shell-quote";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import "./directorypreview.scss";
|
||||
import { type PreviewModel } from "./preview-model";
|
||||
|
||||
const PageJumpSize = 20;
|
||||
|
||||
|
|
@ -527,12 +527,12 @@ function TableBody({
|
|||
if (focusIndex === null || !bodyRef.current || !osRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const rowElement = bodyRef.current.querySelector(`[data-rowindex="${focusIndex}"]`) as HTMLDivElement;
|
||||
if (!rowElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const viewport = osRef.osInstance().elements().viewport;
|
||||
const viewportHeight = viewport.offsetHeight;
|
||||
const rowRect = rowElement.getBoundingClientRect();
|
||||
|
|
@ -540,7 +540,7 @@ function TableBody({
|
|||
const viewportScrollTop = viewport.scrollTop;
|
||||
const rowTopRelativeToViewport = rowRect.top - parentRect.top + viewport.scrollTop;
|
||||
const rowBottomRelativeToViewport = rowRect.bottom - parentRect.top + viewport.scrollTop;
|
||||
|
||||
|
||||
if (rowTopRelativeToViewport - 30 < viewportScrollTop) {
|
||||
// Row is above the visible area
|
||||
let topVal = rowTopRelativeToViewport - 30;
|
||||
|
|
@ -675,7 +675,7 @@ function TableBody({
|
|||
setSearch={setSearch}
|
||||
idx={idx}
|
||||
handleFileContextMenu={handleFileContextMenu}
|
||||
key={idx}
|
||||
key={"top-" + idx}
|
||||
/>
|
||||
))}
|
||||
{table.getCenterRows().map((row, idx) => (
|
||||
|
|
@ -687,7 +687,7 @@ function TableBody({
|
|||
setSearch={setSearch}
|
||||
idx={idx + table.getTopRows().length}
|
||||
handleFileContextMenu={handleFileContextMenu}
|
||||
key={idx}
|
||||
key={"center" + idx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -732,9 +732,12 @@ const TableRow = React.forwardRef(function ({
|
|||
[dragItem]
|
||||
);
|
||||
|
||||
const dragRef = useCallback((node: HTMLDivElement | null) => {
|
||||
drag(node);
|
||||
}, [drag]);
|
||||
const dragRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
drag(node);
|
||||
},
|
||||
[drag]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
79
frontend/app/view/preview/preview-edit.tsx
Normal file
79
frontend/app/view/preview/preview-edit.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { tryReinjectKey } from "@/app/store/keymodel";
|
||||
import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
|
||||
import { globalStore } from "@/store/global";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
import { fireAndForget } from "@/util/util";
|
||||
import { Monaco } from "@monaco-editor/react";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import { useEffect } from "react";
|
||||
import type { SpecializedViewProps } from "./preview";
|
||||
|
||||
function CodeEditPreview({ model }: SpecializedViewProps) {
|
||||
const fileContent = useAtomValue(model.fileContent);
|
||||
const setNewFileContent = useSetAtom(model.newFileContent);
|
||||
const fileInfo = useAtomValue(model.statFile);
|
||||
const fileName = fileInfo?.name;
|
||||
const blockMeta = useAtomValue(model.blockAtom)?.meta;
|
||||
|
||||
function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean {
|
||||
if (checkKeyPressed(e, "Cmd:e")) {
|
||||
fireAndForget(() => model.setEditMode(false));
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:s") || checkKeyPressed(e, "Ctrl:s")) {
|
||||
fireAndForget(model.handleFileSave.bind(model));
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:r")) {
|
||||
fireAndForget(model.handleFileRevert.bind(model));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
model.codeEditKeyDownHandler = codeEditKeyDownHandler;
|
||||
return () => {
|
||||
model.codeEditKeyDownHandler = null;
|
||||
model.monacoRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
function onMount(editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco): () => void {
|
||||
model.monacoRef.current = editor;
|
||||
|
||||
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
|
||||
const waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
|
||||
const handled = tryReinjectKey(waveEvent);
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const isFocused = globalStore.get(model.nodeModel.isFocused);
|
||||
if (isFocused) {
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
blockId={model.blockId}
|
||||
text={fileContent}
|
||||
filename={fileName}
|
||||
fileinfo={fileInfo}
|
||||
meta={blockMeta}
|
||||
onChange={(text) => setNewFileContent(text)}
|
||||
onMount={onMount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { CodeEditPreview };
|
||||
34
frontend/app/view/preview/preview-markdown.tsx
Normal file
34
frontend/app/view/preview/preview-markdown.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/element/markdown";
|
||||
import { getOverrideConfigAtom } from "@/store/global";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import type { SpecializedViewProps } from "./preview";
|
||||
|
||||
function MarkdownPreview({ model }: SpecializedViewProps) {
|
||||
const connName = useAtomValue(model.connection);
|
||||
const fileInfo = useAtomValue(model.statFile);
|
||||
const fontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fontsize"));
|
||||
const fixedFontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fixedfontsize"));
|
||||
const resolveOpts: MarkdownResolveOpts = useMemo<MarkdownResolveOpts>(() => {
|
||||
return {
|
||||
connName: connName,
|
||||
baseDir: fileInfo.dir,
|
||||
};
|
||||
}, [connName, fileInfo.dir]);
|
||||
return (
|
||||
<div className="view-preview view-preview-markdown">
|
||||
<Markdown
|
||||
textAtom={model.fileContent}
|
||||
showTocAtom={model.markdownShowToc}
|
||||
resolveOpts={resolveOpts}
|
||||
fontSizeOverride={fontSizeOverride}
|
||||
fixedFontSizeOverride={fixedFontSizeOverride}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MarkdownPreview };
|
||||
842
frontend/app/view/preview/preview-model.tsx
Normal file
842
frontend/app/view/preview/preview-model.tsx
Normal file
|
|
@ -0,0 +1,842 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { BlockNodeModel } from "@/app/block/blocktypes";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { getConnStatusAtom, getOverrideConfigAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
|
||||
import { checkKeyPressed } from "@/util/keyutil";
|
||||
import { addOpenMenuItems } from "@/util/previewutil";
|
||||
import { base64ToString, fireAndForget, isBlank, jotaiLoadableValue, stringToBase64 } from "@/util/util";
|
||||
import { formatRemoteUri } from "@/util/waveutil";
|
||||
import clsx from "clsx";
|
||||
import { Atom, atom, Getter, PrimitiveAtom, WritableAtom } from "jotai";
|
||||
import { loadable } from "jotai/utils";
|
||||
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import { createRef } from "react";
|
||||
import { PreviewView } from "./preview";
|
||||
|
||||
// TODO drive this using config
|
||||
const BOOKMARKS: { label: string; path: string }[] = [
|
||||
{ label: "Home", path: "~" },
|
||||
{ label: "Desktop", path: "~/Desktop" },
|
||||
{ label: "Downloads", path: "~/Downloads" },
|
||||
{ label: "Documents", path: "~/Documents" },
|
||||
{ label: "Root", path: "/" },
|
||||
];
|
||||
|
||||
const MaxFileSize = 1024 * 1024 * 10; // 10MB
|
||||
const MaxCSVSize = 1024 * 1024 * 1; // 1MB
|
||||
|
||||
const textApplicationMimetypes = [
|
||||
"application/sql",
|
||||
"application/x-php",
|
||||
"application/x-pem-file",
|
||||
"application/x-httpd-php",
|
||||
"application/liquid",
|
||||
"application/graphql",
|
||||
"application/javascript",
|
||||
"application/typescript",
|
||||
"application/x-javascript",
|
||||
"application/x-typescript",
|
||||
"application/dart",
|
||||
"application/vnd.dart",
|
||||
"application/x-ruby",
|
||||
"application/sql",
|
||||
"application/wasm",
|
||||
"application/x-latex",
|
||||
"application/x-sh",
|
||||
"application/x-python",
|
||||
"application/x-awk",
|
||||
];
|
||||
|
||||
function isTextFile(mimeType: string): boolean {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
mimeType.startsWith("text/") ||
|
||||
textApplicationMimetypes.includes(mimeType) ||
|
||||
(mimeType.startsWith("application/") &&
|
||||
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml"))) ||
|
||||
mimeType.includes("xml")
|
||||
);
|
||||
}
|
||||
|
||||
function isStreamingType(mimeType: string): boolean {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
mimeType.startsWith("application/pdf") ||
|
||||
mimeType.startsWith("video/") ||
|
||||
mimeType.startsWith("audio/") ||
|
||||
mimeType.startsWith("image/")
|
||||
);
|
||||
}
|
||||
|
||||
function iconForFile(mimeType: string): string {
|
||||
if (mimeType == null) {
|
||||
mimeType = "unknown";
|
||||
}
|
||||
if (mimeType == "application/pdf") {
|
||||
return "file-pdf";
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
return "image";
|
||||
} else if (mimeType.startsWith("video/")) {
|
||||
return "film";
|
||||
} else if (mimeType.startsWith("audio/")) {
|
||||
return "headphones";
|
||||
} else if (mimeType.startsWith("text/markdown")) {
|
||||
return "file-lines";
|
||||
} else if (mimeType == "text/csv") {
|
||||
return "file-csv";
|
||||
} else if (
|
||||
mimeType.startsWith("text/") ||
|
||||
mimeType == "application/sql" ||
|
||||
(mimeType.startsWith("application/") &&
|
||||
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
||||
) {
|
||||
return "file-code";
|
||||
} else {
|
||||
return "file";
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviewModel implements ViewModel {
|
||||
viewType: string;
|
||||
blockId: string;
|
||||
nodeModel: BlockNodeModel;
|
||||
noPadding?: Atom<boolean>;
|
||||
blockAtom: Atom<Block>;
|
||||
viewIcon: Atom<string | IconButtonDecl>;
|
||||
viewName: Atom<string>;
|
||||
viewText: Atom<HeaderElem[]>;
|
||||
preIconButton: Atom<IconButtonDecl>;
|
||||
endIconButtons: Atom<IconButtonDecl[]>;
|
||||
previewTextRef: React.RefObject<HTMLDivElement>;
|
||||
editMode: Atom<boolean>;
|
||||
canPreview: PrimitiveAtom<boolean>;
|
||||
specializedView: Atom<Promise<{ specializedView?: string; errorStr?: string }>>;
|
||||
loadableSpecializedView: Atom<Loadable<{ specializedView?: string; errorStr?: string }>>;
|
||||
manageConnection: Atom<boolean>;
|
||||
connStatus: Atom<ConnStatus>;
|
||||
filterOutNowsh?: Atom<boolean>;
|
||||
|
||||
metaFilePath: Atom<string>;
|
||||
statFilePath: Atom<Promise<string>>;
|
||||
loadableFileInfo: Atom<Loadable<FileInfo>>;
|
||||
connection: Atom<Promise<string>>;
|
||||
connectionImmediate: Atom<string>;
|
||||
statFile: Atom<Promise<FileInfo>>;
|
||||
fullFile: Atom<Promise<FileData>>;
|
||||
fileMimeType: Atom<Promise<string>>;
|
||||
fileMimeTypeLoadable: Atom<Loadable<string>>;
|
||||
fileContentSaved: PrimitiveAtom<string | null>;
|
||||
fileContent: WritableAtom<Promise<string>, [string], void>;
|
||||
newFileContent: PrimitiveAtom<string | null>;
|
||||
connectionError: PrimitiveAtom<string>;
|
||||
errorMsgAtom: PrimitiveAtom<ErrorMsg>;
|
||||
|
||||
openFileModal: PrimitiveAtom<boolean>;
|
||||
openFileModalDelay: PrimitiveAtom<boolean>;
|
||||
openFileError: PrimitiveAtom<string>;
|
||||
openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>;
|
||||
|
||||
markdownShowToc: PrimitiveAtom<boolean>;
|
||||
|
||||
monacoRef: React.MutableRefObject<MonacoTypes.editor.IStandaloneCodeEditor>;
|
||||
|
||||
showHiddenFiles: PrimitiveAtom<boolean>;
|
||||
refreshVersion: PrimitiveAtom<number>;
|
||||
refreshCallback: () => void;
|
||||
directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
|
||||
codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
|
||||
|
||||
showS3 = atom(true);
|
||||
|
||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||
this.viewType = "preview";
|
||||
this.blockId = blockId;
|
||||
this.nodeModel = nodeModel;
|
||||
let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true;
|
||||
this.showHiddenFiles = atom<boolean>(showHiddenFiles);
|
||||
this.refreshVersion = atom(0);
|
||||
this.previewTextRef = createRef();
|
||||
this.openFileModal = atom(false);
|
||||
this.openFileModalDelay = atom(false);
|
||||
this.openFileError = atom(null) as PrimitiveAtom<string>;
|
||||
this.openFileModalGiveFocusRef = createRef();
|
||||
this.manageConnection = atom(true);
|
||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||
this.markdownShowToc = atom(false);
|
||||
this.filterOutNowsh = atom(true);
|
||||
this.monacoRef = createRef();
|
||||
this.connectionError = atom("");
|
||||
this.errorMsgAtom = atom(null) as PrimitiveAtom<ErrorMsg | null>;
|
||||
this.viewIcon = atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
if (blockData?.meta?.icon) {
|
||||
return blockData.meta.icon;
|
||||
}
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const mimeTypeLoadable = get(this.fileMimeTypeLoadable);
|
||||
const mimeType = jotaiLoadableValue(mimeTypeLoadable, "");
|
||||
if (mimeType == "directory") {
|
||||
return {
|
||||
elemtype: "iconbutton",
|
||||
icon: "folder-open",
|
||||
longClick: (e: React.MouseEvent<any>) => {
|
||||
const menuItems: ContextMenuItem[] = BOOKMARKS.map((bookmark) => ({
|
||||
label: `Go to ${bookmark.label} (${bookmark.path})`,
|
||||
click: () => this.goHistory(bookmark.path),
|
||||
}));
|
||||
ContextMenuModel.showContextMenu(menuItems, e);
|
||||
},
|
||||
};
|
||||
}
|
||||
return iconForFile(mimeType);
|
||||
});
|
||||
this.editMode = atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
return blockData?.meta?.edit ?? false;
|
||||
});
|
||||
this.viewName = atom("Preview");
|
||||
this.viewText = atom((get) => {
|
||||
let headerPath = get(this.metaFilePath);
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return [
|
||||
{
|
||||
elemtype: "text",
|
||||
text: headerPath,
|
||||
className: "preview-filename",
|
||||
},
|
||||
];
|
||||
}
|
||||
const loadableSV = get(this.loadableSpecializedView);
|
||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||
const loadableFileInfo = get(this.loadableFileInfo);
|
||||
if (loadableFileInfo.state == "hasData") {
|
||||
headerPath = loadableFileInfo.data?.path;
|
||||
if (headerPath == "~") {
|
||||
headerPath = `~ (${loadableFileInfo.data?.dir + "/" + loadableFileInfo.data?.name})`;
|
||||
}
|
||||
}
|
||||
if (!isBlank(headerPath) && headerPath != "/" && headerPath.endsWith("/")) {
|
||||
headerPath = headerPath.slice(0, -1);
|
||||
}
|
||||
const viewTextChildren: HeaderElem[] = [
|
||||
{
|
||||
elemtype: "text",
|
||||
text: headerPath,
|
||||
ref: this.previewTextRef,
|
||||
className: "preview-filename",
|
||||
onClick: () => this.toggleOpenFileModal(),
|
||||
},
|
||||
];
|
||||
let saveClassName = "grey";
|
||||
if (get(this.newFileContent) !== null) {
|
||||
saveClassName = "green";
|
||||
}
|
||||
if (isCeView) {
|
||||
const fileInfo = globalStore.get(this.loadableFileInfo);
|
||||
if (fileInfo.state != "hasData") {
|
||||
viewTextChildren.push({
|
||||
elemtype: "textbutton",
|
||||
text: "Loading ...",
|
||||
className: clsx(
|
||||
`grey warning border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500`
|
||||
),
|
||||
onClick: () => {},
|
||||
});
|
||||
} else if (fileInfo.data.readonly) {
|
||||
viewTextChildren.push({
|
||||
elemtype: "textbutton",
|
||||
text: "Read Only",
|
||||
className: clsx(
|
||||
`yellow warning border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500`
|
||||
),
|
||||
onClick: () => {},
|
||||
});
|
||||
} else {
|
||||
viewTextChildren.push({
|
||||
elemtype: "textbutton",
|
||||
text: "Save",
|
||||
className: clsx(
|
||||
`${saveClassName} warning border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500`
|
||||
),
|
||||
onClick: () => fireAndForget(this.handleFileSave.bind(this)),
|
||||
});
|
||||
}
|
||||
if (get(this.canPreview)) {
|
||||
viewTextChildren.push({
|
||||
elemtype: "textbutton",
|
||||
text: "Preview",
|
||||
className:
|
||||
"grey border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500",
|
||||
onClick: () => fireAndForget(() => this.setEditMode(false)),
|
||||
});
|
||||
}
|
||||
} else if (get(this.canPreview)) {
|
||||
viewTextChildren.push({
|
||||
elemtype: "textbutton",
|
||||
text: "Edit",
|
||||
className:
|
||||
"grey border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500",
|
||||
onClick: () => fireAndForget(() => this.setEditMode(true)),
|
||||
});
|
||||
}
|
||||
return [
|
||||
{
|
||||
elemtype: "div",
|
||||
children: viewTextChildren,
|
||||
},
|
||||
] as HeaderElem[];
|
||||
});
|
||||
this.preIconButton = atom((get) => {
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const mimeType = jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
const metaPath = get(this.metaFilePath);
|
||||
if (mimeType == "directory" && metaPath == "/") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
elemtype: "iconbutton",
|
||||
icon: "chevron-left",
|
||||
click: this.goParentDirectory.bind(this),
|
||||
};
|
||||
});
|
||||
this.endIconButtons = atom((get) => {
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const mimeType = jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
const loadableSV = get(this.loadableSpecializedView);
|
||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||
if (mimeType == "directory") {
|
||||
const 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?.(),
|
||||
},
|
||||
] as IconButtonDecl[];
|
||||
} else if (!isCeView && mimeType?.startsWith("text/markdown")) {
|
||||
return [
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
icon: "book",
|
||||
title: "Table of Contents",
|
||||
click: () => this.markdownShowTocToggle(),
|
||||
},
|
||||
] as IconButtonDecl[];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
this.metaFilePath = atom<string>((get) => {
|
||||
const file = get(this.blockAtom)?.meta?.file;
|
||||
if (isBlank(file)) {
|
||||
return "~";
|
||||
}
|
||||
return file;
|
||||
});
|
||||
this.statFilePath = atom<Promise<string>>(async (get) => {
|
||||
const fileInfo = await get(this.statFile);
|
||||
return fileInfo?.path;
|
||||
});
|
||||
this.connection = atom<Promise<string>>(async (get) => {
|
||||
const connName = get(this.blockAtom)?.meta?.connection;
|
||||
try {
|
||||
await RpcApi.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 });
|
||||
globalStore.set(this.connectionError, "");
|
||||
} catch (e) {
|
||||
globalStore.set(this.connectionError, e as string);
|
||||
}
|
||||
return connName;
|
||||
});
|
||||
this.connectionImmediate = atom<string>((get) => {
|
||||
return get(this.blockAtom)?.meta?.connection;
|
||||
});
|
||||
this.statFile = atom<Promise<FileInfo>>(async (get) => {
|
||||
const fileName = get(this.metaFilePath);
|
||||
const path = await this.formatRemoteUri(fileName, get);
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const statFile = await RpcApi.FileInfoCommand(TabRpcClient, {
|
||||
info: {
|
||||
path,
|
||||
},
|
||||
});
|
||||
return statFile;
|
||||
} catch (e) {
|
||||
const errorStatus: ErrorMsg = {
|
||||
status: "File Read Failed",
|
||||
text: `${e}`,
|
||||
};
|
||||
globalStore.set(this.errorMsgAtom, errorStatus);
|
||||
}
|
||||
});
|
||||
this.fileMimeType = atom<Promise<string>>(async (get) => {
|
||||
const fileInfo = await get(this.statFile);
|
||||
return fileInfo?.mimetype;
|
||||
});
|
||||
this.fileMimeTypeLoadable = loadable(this.fileMimeType);
|
||||
this.newFileContent = atom(null) as PrimitiveAtom<string | null>;
|
||||
this.goParentDirectory = this.goParentDirectory.bind(this);
|
||||
|
||||
const fullFileAtom = atom<Promise<FileData>>(async (get) => {
|
||||
const fileName = get(this.metaFilePath);
|
||||
const path = await this.formatRemoteUri(fileName, get);
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const file = await RpcApi.FileReadCommand(TabRpcClient, {
|
||||
info: {
|
||||
path,
|
||||
},
|
||||
});
|
||||
return file;
|
||||
} catch (e) {
|
||||
const errorStatus: ErrorMsg = {
|
||||
status: "File Read Failed",
|
||||
text: `${e}`,
|
||||
};
|
||||
globalStore.set(this.errorMsgAtom, errorStatus);
|
||||
}
|
||||
});
|
||||
|
||||
this.fileContentSaved = atom(null) as PrimitiveAtom<string | null>;
|
||||
const fileContentAtom = atom(
|
||||
async (get) => {
|
||||
const newContent = get(this.newFileContent);
|
||||
if (newContent != null) {
|
||||
return newContent;
|
||||
}
|
||||
const savedContent = get(this.fileContentSaved);
|
||||
if (savedContent != null) {
|
||||
return savedContent;
|
||||
}
|
||||
const fullFile = await get(fullFileAtom);
|
||||
return base64ToString(fullFile?.data64);
|
||||
},
|
||||
(_, set, update: string) => {
|
||||
set(this.fileContentSaved, update);
|
||||
}
|
||||
);
|
||||
|
||||
this.fullFile = fullFileAtom;
|
||||
this.fileContent = fileContentAtom;
|
||||
|
||||
this.specializedView = atom<Promise<{ specializedView?: string; errorStr?: string }>>(async (get) => {
|
||||
return this.getSpecializedView(get);
|
||||
});
|
||||
this.loadableSpecializedView = loadable(this.specializedView);
|
||||
this.canPreview = atom(false);
|
||||
this.loadableFileInfo = loadable(this.statFile);
|
||||
this.connStatus = atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
const connName = blockData?.meta?.connection;
|
||||
const connAtom = getConnStatusAtom(connName);
|
||||
return get(connAtom);
|
||||
});
|
||||
|
||||
this.noPadding = atom(true);
|
||||
}
|
||||
|
||||
markdownShowTocToggle() {
|
||||
globalStore.set(this.markdownShowToc, !globalStore.get(this.markdownShowToc));
|
||||
}
|
||||
|
||||
get viewComponent(): ViewComponent {
|
||||
return PreviewView;
|
||||
}
|
||||
|
||||
async getSpecializedView(getFn: Getter): Promise<{ specializedView?: string; errorStr?: string }> {
|
||||
const mimeType = await getFn(this.fileMimeType);
|
||||
const fileInfo = await getFn(this.statFile);
|
||||
const fileName = fileInfo?.name;
|
||||
const connErr = getFn(this.connectionError);
|
||||
const editMode = getFn(this.editMode);
|
||||
const genErr = getFn(this.errorMsgAtom);
|
||||
|
||||
if (!fileInfo) {
|
||||
return { errorStr: `Load Error: ${genErr?.text}` };
|
||||
}
|
||||
if (connErr != "") {
|
||||
return { errorStr: `Connection Error: ${connErr}` };
|
||||
}
|
||||
if (fileInfo?.notfound) {
|
||||
return { specializedView: "codeedit" };
|
||||
}
|
||||
if (mimeType == null) {
|
||||
return { errorStr: `Unable to determine mimetype for: ${fileInfo.path}` };
|
||||
}
|
||||
if (isStreamingType(mimeType)) {
|
||||
return { specializedView: "streaming" };
|
||||
}
|
||||
if (!fileInfo) {
|
||||
const fileNameStr = fileName ? " " + JSON.stringify(fileName) : "";
|
||||
return { errorStr: "File Not Found" + fileNameStr };
|
||||
}
|
||||
if (fileInfo.size > MaxFileSize) {
|
||||
return { errorStr: "File Too Large to Preview (10 MB Max)" };
|
||||
}
|
||||
if (mimeType == "text/csv" && fileInfo.size > MaxCSVSize) {
|
||||
return { errorStr: "CSV File Too Large to Preview (1 MB Max)" };
|
||||
}
|
||||
if (mimeType == "directory") {
|
||||
return { specializedView: "directory" };
|
||||
}
|
||||
if (mimeType == "text/csv") {
|
||||
if (editMode) {
|
||||
return { specializedView: "codeedit" };
|
||||
}
|
||||
return { specializedView: "csv" };
|
||||
}
|
||||
if (mimeType.startsWith("text/markdown")) {
|
||||
if (editMode) {
|
||||
return { specializedView: "codeedit" };
|
||||
}
|
||||
return { specializedView: "markdown" };
|
||||
}
|
||||
if (isTextFile(mimeType) || fileInfo.size == 0) {
|
||||
return { specializedView: "codeedit" };
|
||||
}
|
||||
return { errorStr: `Preview (${mimeType})` };
|
||||
}
|
||||
|
||||
updateOpenFileModalAndError(isOpen, errorMsg = null) {
|
||||
globalStore.set(this.openFileModal, isOpen);
|
||||
globalStore.set(this.openFileError, errorMsg);
|
||||
if (isOpen) {
|
||||
globalStore.set(this.openFileModalDelay, true);
|
||||
} else {
|
||||
const delayVal = globalStore.get(this.openFileModalDelay);
|
||||
if (delayVal) {
|
||||
setTimeout(() => {
|
||||
globalStore.set(this.openFileModalDelay, false);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleOpenFileModal() {
|
||||
const modalOpen = globalStore.get(this.openFileModal);
|
||||
const delayVal = globalStore.get(this.openFileModalDelay);
|
||||
if (!modalOpen && delayVal) {
|
||||
return;
|
||||
}
|
||||
this.updateOpenFileModalAndError(!modalOpen);
|
||||
}
|
||||
|
||||
async goHistory(newPath: string) {
|
||||
let fileName = globalStore.get(this.metaFilePath);
|
||||
if (fileName == null) {
|
||||
fileName = "";
|
||||
}
|
||||
const blockMeta = globalStore.get(this.blockAtom)?.meta;
|
||||
const updateMeta = goHistory("file", fileName, newPath, blockMeta);
|
||||
if (updateMeta == null) {
|
||||
return;
|
||||
}
|
||||
const blockOref = WOS.makeORef("block", this.blockId);
|
||||
await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
|
||||
|
||||
// Clear the saved file buffers
|
||||
globalStore.set(this.fileContentSaved, null);
|
||||
globalStore.set(this.newFileContent, null);
|
||||
}
|
||||
|
||||
async goParentDirectory({ fileInfo = null }: { fileInfo?: FileInfo | null }) {
|
||||
// optional parameter needed for recursive case
|
||||
const defaultFileInfo = await globalStore.get(this.statFile);
|
||||
if (fileInfo === null) {
|
||||
fileInfo = defaultFileInfo;
|
||||
}
|
||||
if (fileInfo == null) {
|
||||
this.updateOpenFileModalAndError(false);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
this.updateOpenFileModalAndError(false);
|
||||
await this.goHistory(fileInfo.dir);
|
||||
refocusNode(this.blockId);
|
||||
} catch (e) {
|
||||
globalStore.set(this.openFileError, e.message);
|
||||
console.error("Error opening file", fileInfo.dir, e);
|
||||
}
|
||||
}
|
||||
|
||||
async goHistoryBack() {
|
||||
const blockMeta = globalStore.get(this.blockAtom)?.meta;
|
||||
const curPath = globalStore.get(this.metaFilePath);
|
||||
const updateMeta = goHistoryBack("file", curPath, blockMeta, true);
|
||||
if (updateMeta == null) {
|
||||
return;
|
||||
}
|
||||
updateMeta.edit = false;
|
||||
const blockOref = WOS.makeORef("block", this.blockId);
|
||||
await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
|
||||
}
|
||||
|
||||
async goHistoryForward() {
|
||||
const blockMeta = globalStore.get(this.blockAtom)?.meta;
|
||||
const curPath = globalStore.get(this.metaFilePath);
|
||||
const updateMeta = goHistoryForward("file", curPath, blockMeta);
|
||||
if (updateMeta == null) {
|
||||
return;
|
||||
}
|
||||
updateMeta.edit = false;
|
||||
const blockOref = WOS.makeORef("block", this.blockId);
|
||||
await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
|
||||
}
|
||||
|
||||
async setEditMode(edit: boolean) {
|
||||
const blockMeta = globalStore.get(this.blockAtom)?.meta;
|
||||
const blockOref = WOS.makeORef("block", this.blockId);
|
||||
await services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit });
|
||||
}
|
||||
|
||||
async handleFileSave() {
|
||||
const filePath = await globalStore.get(this.statFilePath);
|
||||
if (filePath == null) {
|
||||
return;
|
||||
}
|
||||
const newFileContent = globalStore.get(this.newFileContent);
|
||||
if (newFileContent == null) {
|
||||
console.log("not saving file, newFileContent is null");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await RpcApi.FileWriteCommand(TabRpcClient, {
|
||||
info: {
|
||||
path: await this.formatRemoteUri(filePath, globalStore.get),
|
||||
},
|
||||
data64: stringToBase64(newFileContent),
|
||||
});
|
||||
globalStore.set(this.fileContent, newFileContent);
|
||||
globalStore.set(this.newFileContent, null);
|
||||
console.log("saved file", filePath);
|
||||
} catch (e) {
|
||||
const errorStatus: ErrorMsg = {
|
||||
status: "Save Failed",
|
||||
text: `${e}`,
|
||||
};
|
||||
globalStore.set(this.errorMsgAtom, errorStatus);
|
||||
}
|
||||
}
|
||||
|
||||
async handleFileRevert() {
|
||||
const fileContent = await globalStore.get(this.fileContent);
|
||||
this.monacoRef.current?.setValue(fileContent);
|
||||
globalStore.set(this.newFileContent, null);
|
||||
}
|
||||
|
||||
async handleOpenFile(filePath: string) {
|
||||
const conn = globalStore.get(this.connectionImmediate);
|
||||
if (!isBlank(conn) && conn.startsWith("aws:")) {
|
||||
if (!isBlank(filePath) && filePath != "/" && filePath.startsWith("/")) {
|
||||
filePath = filePath.substring(1);
|
||||
}
|
||||
}
|
||||
const fileInfo = await globalStore.get(this.statFile);
|
||||
this.updateOpenFileModalAndError(false);
|
||||
if (fileInfo == null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
this.goHistory(filePath);
|
||||
refocusNode(this.blockId);
|
||||
} catch (e) {
|
||||
globalStore.set(this.openFileError, e.message);
|
||||
console.error("Error opening file", filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
isSpecializedView(sv: string): boolean {
|
||||
const loadableSV = globalStore.get(this.loadableSpecializedView);
|
||||
return loadableSV.state == "hasData" && loadableSV.data.specializedView == sv;
|
||||
}
|
||||
|
||||
getSettingsMenuItems(): ContextMenuItem[] {
|
||||
const defaultFontSize = globalStore.get(getSettingsKeyAtom("editor:fontsize")) ?? 12;
|
||||
const blockData = globalStore.get(this.blockAtom);
|
||||
const overrideFontSize = blockData?.meta?.["editor:fontsize"];
|
||||
const menuItems: ContextMenuItem[] = [];
|
||||
menuItems.push({
|
||||
label: "Copy Full Path",
|
||||
click: () =>
|
||||
fireAndForget(async () => {
|
||||
const filePath = await globalStore.get(this.statFilePath);
|
||||
if (filePath == null) {
|
||||
return;
|
||||
}
|
||||
const conn = await globalStore.get(this.connection);
|
||||
if (conn) {
|
||||
// remote path
|
||||
await navigator.clipboard.writeText(formatRemoteUri(filePath, conn));
|
||||
} else {
|
||||
// local path
|
||||
await navigator.clipboard.writeText(filePath);
|
||||
}
|
||||
}),
|
||||
});
|
||||
menuItems.push({
|
||||
label: "Copy File Name",
|
||||
click: () =>
|
||||
fireAndForget(async () => {
|
||||
const fileInfo = await globalStore.get(this.statFile);
|
||||
if (fileInfo == null || fileInfo.name == null) {
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(fileInfo.name);
|
||||
}),
|
||||
});
|
||||
menuItems.push({ type: "separator" });
|
||||
const fontSizeSubMenu: ContextMenuItem[] = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map(
|
||||
(fontSize: number) => {
|
||||
return {
|
||||
label: fontSize.toString() + "px",
|
||||
type: "checkbox",
|
||||
checked: overrideFontSize == fontSize,
|
||||
click: () => {
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "editor:fontsize": fontSize },
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
fontSizeSubMenu.unshift({
|
||||
label: "Default (" + defaultFontSize + "px)",
|
||||
type: "checkbox",
|
||||
checked: overrideFontSize == null,
|
||||
click: () => {
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "editor:fontsize": null },
|
||||
});
|
||||
},
|
||||
});
|
||||
menuItems.push({
|
||||
label: "Editor Font Size",
|
||||
submenu: fontSizeSubMenu,
|
||||
});
|
||||
const finfo = jotaiLoadableValue(globalStore.get(this.loadableFileInfo), null);
|
||||
addOpenMenuItems(menuItems, globalStore.get(this.connectionImmediate), finfo);
|
||||
const loadableSV = globalStore.get(this.loadableSpecializedView);
|
||||
const wordWrapAtom = getOverrideConfigAtom(this.blockId, "editor:wordwrap");
|
||||
const wordWrap = globalStore.get(wordWrapAtom) ?? false;
|
||||
if (loadableSV.state == "hasData") {
|
||||
if (loadableSV.data.specializedView == "codeedit") {
|
||||
if (globalStore.get(this.newFileContent) != null) {
|
||||
menuItems.push({ type: "separator" });
|
||||
menuItems.push({
|
||||
label: "Save File",
|
||||
click: () => fireAndForget(this.handleFileSave.bind(this)),
|
||||
});
|
||||
menuItems.push({
|
||||
label: "Revert File",
|
||||
click: () => fireAndForget(this.handleFileRevert.bind(this)),
|
||||
});
|
||||
}
|
||||
menuItems.push({ type: "separator" });
|
||||
menuItems.push({
|
||||
label: "Word Wrap",
|
||||
type: "checkbox",
|
||||
checked: wordWrap,
|
||||
click: () =>
|
||||
fireAndForget(async () => {
|
||||
const blockOref = WOS.makeORef("block", this.blockId);
|
||||
await services.ObjectService.UpdateObjectMeta(blockOref, {
|
||||
"editor:wordwrap": !wordWrap,
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
giveFocus(): boolean {
|
||||
const openModalOpen = globalStore.get(this.openFileModal);
|
||||
if (openModalOpen) {
|
||||
this.openFileModalGiveFocusRef.current?.();
|
||||
return true;
|
||||
}
|
||||
if (this.monacoRef.current) {
|
||||
this.monacoRef.current.focus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
keyDownHandler(e: WaveKeyboardEvent): boolean {
|
||||
if (checkKeyPressed(e, "Cmd:ArrowLeft")) {
|
||||
fireAndForget(this.goHistoryBack.bind(this));
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:ArrowRight")) {
|
||||
fireAndForget(this.goHistoryForward.bind(this));
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:ArrowUp")) {
|
||||
// handle up directory
|
||||
fireAndForget(() => this.goParentDirectory({}));
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:o")) {
|
||||
this.toggleOpenFileModal();
|
||||
return true;
|
||||
}
|
||||
const canPreview = globalStore.get(this.canPreview);
|
||||
if (canPreview) {
|
||||
if (checkKeyPressed(e, "Cmd:e")) {
|
||||
const editMode = globalStore.get(this.editMode);
|
||||
fireAndForget(() => this.setEditMode(!editMode));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.directoryKeyDownHandler) {
|
||||
const handled = this.directoryKeyDownHandler(e);
|
||||
if (handled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.codeEditKeyDownHandler) {
|
||||
const handled = this.codeEditKeyDownHandler(e);
|
||||
if (handled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async formatRemoteUri(path: string, get: Getter): Promise<string> {
|
||||
return formatRemoteUri(path, await get(this.connection));
|
||||
}
|
||||
}
|
||||
89
frontend/app/view/preview/preview-streaming.tsx
Normal file
89
frontend/app/view/preview/preview-streaming.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Button } from "@/app/element/button";
|
||||
import { CenteredDiv } from "@/app/element/quickelems";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import { formatRemoteUri } from "@/util/waveutil";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { TransformComponent, TransformWrapper, useControls } from "react-zoom-pan-pinch";
|
||||
import type { SpecializedViewProps } from "./preview";
|
||||
|
||||
function ImageZoomControls() {
|
||||
const { zoomIn, zoomOut, resetTransform } = useControls();
|
||||
|
||||
return (
|
||||
<div className="tools">
|
||||
<Button onClick={() => zoomIn()} title="Zoom In">
|
||||
<i className="fa-sharp fa-plus" />
|
||||
</Button>
|
||||
<Button onClick={() => zoomOut()} title="Zoom Out">
|
||||
<i className="fa-sharp fa-minus" />
|
||||
</Button>
|
||||
<Button onClick={() => resetTransform()} title="Reset Zoom">
|
||||
<i className="fa-sharp fa-rotate-left" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamingImagePreview({ url }: { url: string }) {
|
||||
return (
|
||||
<div className="view-preview view-preview-image">
|
||||
<TransformWrapper initialScale={1} centerOnInit pinch={{ step: 10 }}>
|
||||
{({ zoomIn, zoomOut, resetTransform, ...rest }) => (
|
||||
<>
|
||||
<ImageZoomControls />
|
||||
<TransformComponent>
|
||||
<img src={url} />
|
||||
</TransformComponent>
|
||||
</>
|
||||
)}
|
||||
</TransformWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamingPreview({ model }: SpecializedViewProps) {
|
||||
const conn = useAtomValue(model.connection);
|
||||
const fileInfo = useAtomValue(model.statFile);
|
||||
const filePath = fileInfo.path;
|
||||
const remotePath = formatRemoteUri(filePath, conn);
|
||||
const usp = new URLSearchParams();
|
||||
usp.set("path", remotePath);
|
||||
if (conn != null) {
|
||||
usp.set("connection", conn);
|
||||
}
|
||||
const streamingUrl = `${getWebServerEndpoint()}/wave/stream-file?${usp.toString()}`;
|
||||
if (fileInfo.mimetype === "application/pdf") {
|
||||
return (
|
||||
<div className="view-preview view-preview-pdf">
|
||||
<iframe src={streamingUrl} width="100%" height="100%" name="pdfview" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (fileInfo.mimetype.startsWith("video/")) {
|
||||
return (
|
||||
<div className="view-preview view-preview-video">
|
||||
<video controls>
|
||||
<source src={streamingUrl} />
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (fileInfo.mimetype.startsWith("audio/")) {
|
||||
return (
|
||||
<div className="view-preview view-preview-audio">
|
||||
<audio controls>
|
||||
<source src={streamingUrl} />
|
||||
</audio>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (fileInfo.mimetype.startsWith("image/")) {
|
||||
return <StreamingImagePreview url={streamingUrl} />;
|
||||
}
|
||||
return <CenteredDiv>Preview Not Supported</CenteredDiv>;
|
||||
}
|
||||
|
||||
export { StreamingPreview };
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue