From 146bade6f1705c134691feae44ffede9e221dc60 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 May 2024 22:48:23 -0700 Subject: [PATCH] streaming images, audio, and video --- frontend/app/block/block.tsx | 4 +- frontend/app/element/quickelems.less | 2 +- frontend/app/element/quickelems.tsx | 8 +-- frontend/app/view/preview.tsx | 70 +++++++++++++++++++++++--- frontend/app/view/view.less | 12 +++++ frontend/app/workspace/workspace.tsx | 3 ++ main.go | 26 +++++++++- pkg/service/fileservice/fileservice.go | 5 ++ 8 files changed, 115 insertions(+), 15 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 3a99550e7..fe7af24b2 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -8,7 +8,7 @@ import { atoms, blockDataMap, removeBlockFromTab } from "@/store/global"; import { TerminalView } from "@/app/view/term"; import { PreviewView } from "@/app/view/preview"; import { PlotView } from "@/app/view/plotview"; -import { CenteredLoadingDiv } from "@/element/quickelems"; +import { CenteredDiv } from "@/element/quickelems"; import "./block.less"; @@ -53,7 +53,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => {
- }>{blockElem} + Loading...}>{blockElem}
); diff --git a/frontend/app/element/quickelems.less b/frontend/app/element/quickelems.less index c443f31c8..b4f845323 100644 --- a/frontend/app/element/quickelems.less +++ b/frontend/app/element/quickelems.less @@ -1,7 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -.centered-loading { +.centered-div { display: flex; flex-direction: row; align-items: center; diff --git a/frontend/app/element/quickelems.tsx b/frontend/app/element/quickelems.tsx index 9717ee434..5de52a253 100644 --- a/frontend/app/element/quickelems.tsx +++ b/frontend/app/element/quickelems.tsx @@ -3,12 +3,12 @@ import "./quickelems.less"; -function CenteredLoadingDiv() { +function CenteredDiv({ children }: { children: React.ReactNode }) { return ( -
-
Loading...
+
+
{children}
); } -export { CenteredLoadingDiv }; +export { CenteredDiv as CenteredDiv }; diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index bb8c1c72e..6533fafc1 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -7,25 +7,69 @@ import { atoms, blockDataMap, useBlockAtom } from "@/store/global"; import { Markdown } from "@/element/markdown"; import * as FileService from "@/bindings/pkg/service/fileservice/FileService"; import * as util from "@/util/util"; -import { loadable } from "jotai/utils"; +import { CenteredDiv } from "../element/quickelems"; import "./view.less"; -const MarkdownPreview = ({ contentAtom }: { contentAtom: jotai.Atom> }) => { +const MaxFileSize = 1024 * 1024 * 10; // 10MB + +function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom> }) { const readmeText = jotai.useAtomValue(contentAtom); return (
); -}; -const PreviewView = ({ blockId }: { blockId: string }) => { +} + +function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) { + const filePath = fileInfo.path; + const streamingUrl = "/wave/stream-file?path=" + encodeURIComponent(filePath); + if (fileInfo.mimetype.startsWith("video/")) { + return ( +
+ +
+ ); + } + if (fileInfo.mimetype.startsWith("audio/")) { + return ( +
+ +
+ ); + } + if (fileInfo.mimetype.startsWith("image/")) { + return ( +
+ +
+ ); + } + return Preview Not Supported; +} + +function PreviewView({ blockId }: { blockId: string }) { const blockDataAtom: jotai.Atom = blockDataMap.get(blockId); const fileNameAtom = useBlockAtom(blockId, "preview:filename", () => jotai.atom((get) => { return get(blockDataAtom)?.meta?.file; }) ); + const statFileAtom = useBlockAtom(blockId, "preview:statfile", () => + jotai.atom>(async (get) => { + const fileName = get(fileNameAtom); + if (fileName == null) { + return null; + } + const statFile = await FileService.StatFile(fileName); + return statFile; + }) + ); const fullFileAtom = useBlockAtom(blockId, "preview:fullfile", () => jotai.atom>(async (get) => { const fileName = get(fileNameAtom); @@ -38,8 +82,8 @@ const PreviewView = ({ blockId }: { blockId: string }) => { ); const fileMimeTypeAtom = useBlockAtom(blockId, "preview:mimetype", () => jotai.atom>(async (get) => { - const fullFile = await get(fullFileAtom); - return fullFile?.info?.mimetype; + const fileInfo = await get(statFileAtom); + return fileInfo?.mimetype; }) ); const fileContentAtom = useBlockAtom(blockId, "preview:filecontent", () => @@ -52,6 +96,18 @@ const PreviewView = ({ blockId }: { blockId: string }) => { if (mimeType == null) { mimeType = ""; } + const fileInfo = jotai.useAtomValue(statFileAtom); + + // handle streaming files here + if (mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType.startsWith("image/")) { + return ; + } + if (fileInfo == null) { + return File Not Found; + } + if (fileInfo.size > MaxFileSize) { + return File Too Large to Preview; + } if (mimeType === "text/markdown") { return ; } @@ -67,6 +123,6 @@ const PreviewView = ({ blockId }: { blockId: string }) => {
Preview ({mimeType})
); -}; +} export { PreviewView }; diff --git a/frontend/app/view/view.less b/frontend/app/view/view.less index abb45a3d6..3700d3475 100644 --- a/frontend/app/view/view.less +++ b/frontend/app/view/view.less @@ -51,4 +51,16 @@ font: var(--fixed-font); } } + + &.view-preview-image, + &.view-preview-video, + &.view-preview-audio { + video, + audio, + img { + width: 100%; + padding: 10px; + object-fit: contain; + } + } } diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index cbc15b8f4..2d03b7942 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -89,6 +89,9 @@ function Widgets() {
clickPreview("go.mod")}>
+
clickPreview("build/appicon.png")}> + +
clickPlot()}>
diff --git a/main.go b/main.go index 0ad8df88e..18678152e 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,8 @@ package main import ( "embed" "log" + "net/http" + "strings" "github.com/wavetermdev/thenextwave/pkg/blockstore" "github.com/wavetermdev/thenextwave/pkg/eventbus" @@ -61,6 +63,28 @@ func createWindow(app *application.App) { }) } +type waveAssetHandler struct { + AssetHandler http.Handler +} + +func serveWaveUrls(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/wave/stream-file" { + fileName := r.URL.Query().Get("path") + fileName = wavebase.ExpandHomeDir(fileName) + http.ServeFile(w, r, fileName) + return + } + http.NotFound(w, r) +} + +func (wah waveAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.HasPrefix(r.URL.Path, "/wave/") { + serveWaveUrls(w, r) + return + } + wah.AssetHandler.ServeHTTP(w, r) +} + func main() { err := wavebase.EnsureWaveHomeDir() if err != nil { @@ -83,7 +107,7 @@ func main() { }, Icon: appIcon, Assets: application.AssetOptions{ - Handler: application.AssetFileServerFS(assets), + Handler: waveAssetHandler{AssetHandler: application.AssetFileServerFS(assets)}, }, Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: true, diff --git a/pkg/service/fileservice/fileservice.go b/pkg/service/fileservice/fileservice.go index 306d2465d..a1c486f16 100644 --- a/pkg/service/fileservice/fileservice.go +++ b/pkg/service/fileservice/fileservice.go @@ -13,6 +13,8 @@ import ( "github.com/wavetermdev/thenextwave/pkg/wavebase" ) +const MaxFileSize = 10 * 1024 * 1024 // 10M + type FileService struct{} type FileInfo struct { @@ -58,6 +60,9 @@ func (fs *FileService) ReadFile(path string) (*FullFile, error) { if finfo.NotFound { return &FullFile{Info: finfo}, nil } + if finfo.Size > MaxFileSize { + return nil, fmt.Errorf("file %q is too large to read, use /wave/stream-file", path) + } cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path)) barr, err := os.ReadFile(cleanedPath) if err != nil {