mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-23 08:48:28 +00:00
streaming images, audio, and video
This commit is contained in:
parent
d34ccfd7ab
commit
146bade6f1
8 changed files with 115 additions and 15 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
26
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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue