streaming images, audio, and video

This commit is contained in:
sawka 2024-05-16 22:48:23 -07:00
parent d34ccfd7ab
commit 146bade6f1
8 changed files with 115 additions and 15 deletions

View file

@ -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 }) => {
</div>
</div>
<div key="content" className="block-content">
<React.Suspense fallback={<CenteredLoadingDiv />}>{blockElem}</React.Suspense>
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
</div>
</div>
);

View file

@ -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;

View file

@ -3,12 +3,12 @@
import "./quickelems.less";
function CenteredLoadingDiv() {
function CenteredDiv({ children }: { children: React.ReactNode }) {
return (
<div className="centered-loading">
<div>Loading...</div>
<div className="centered-div">
<div>{children}</div>
</div>
);
}
export { CenteredLoadingDiv };
export { CenteredDiv as CenteredDiv };

View file

@ -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<Promise<string>> }) => {
const MaxFileSize = 1024 * 1024 * 10; // 10MB
function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<string>> }) {
const readmeText = jotai.useAtomValue(contentAtom);
return (
<div className="view-preview view-preview-markdown">
<Markdown text={readmeText} />
</div>
);
};
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 (
<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 (
<div className="view-preview view-preview-image">
<img src={streamingUrl} />
</div>
);
}
return <CenteredDiv>Preview Not Supported</CenteredDiv>;
}
function PreviewView({ blockId }: { blockId: string }) {
const blockDataAtom: jotai.Atom<BlockData> = blockDataMap.get(blockId);
const fileNameAtom = useBlockAtom(blockId, "preview:filename", () =>
jotai.atom<string>((get) => {
return get(blockDataAtom)?.meta?.file;
})
);
const statFileAtom = useBlockAtom(blockId, "preview:statfile", () =>
jotai.atom<Promise<FileInfo>>(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<Promise<FullFile>>(async (get) => {
const fileName = get(fileNameAtom);
@ -38,8 +82,8 @@ const PreviewView = ({ blockId }: { blockId: string }) => {
);
const fileMimeTypeAtom = useBlockAtom(blockId, "preview:mimetype", () =>
jotai.atom<Promise<string>>(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 <StreamingPreview fileInfo={fileInfo} />;
}
if (fileInfo == null) {
return <CenteredDiv>File Not Found</CenteredDiv>;
}
if (fileInfo.size > MaxFileSize) {
return <CenteredDiv>File Too Large to Preview</CenteredDiv>;
}
if (mimeType === "text/markdown") {
return <MarkdownPreview contentAtom={fileContentAtom} />;
}
@ -67,6 +123,6 @@ const PreviewView = ({ blockId }: { blockId: string }) => {
<div>Preview ({mimeType})</div>
</div>
);
};
}
export { PreviewView };

View file

@ -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;
}
}
}

View file

@ -89,6 +89,9 @@ function Widgets() {
<div className="widget" onClick={() => clickPreview("go.mod")}>
<i className="fa fa-solid fa-files fa-fw" />
</div>
<div className="widget" onClick={() => clickPreview("build/appicon.png")}>
<i className="fa fa-solid fa-files fa-fw" />
</div>
<div className="widget" onClick={() => clickPlot()}>
<i className="fa fa-solid fa-chart-simple fa-fw" />
</div>

26
main.go
View file

@ -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,

View file

@ -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 {