refactor(download): handle stream saver download outside of driver implementation

This commit is contained in:
Aaron Liu 2025-05-16 13:49:45 +08:00
parent 3e8b1eabfc
commit c4d4d3aa6f
11 changed files with 61 additions and 108 deletions

View File

@ -284,10 +284,15 @@ export interface FileURLService extends MultipleUriService {
}
export interface FileURLResponse {
urls: string[];
urls: EntityURLResponse[];
expires: string;
}
export interface EntityURLResponse {
url: string;
stream_saver_display_name?: string;
}
export interface GetFileInfoService {
uri: string;
extended?: boolean;

View File

@ -1,12 +1,12 @@
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
import React, { Suspense, useCallback, useEffect, useState } from "react";
import { closeEpubViewer } from "../../../redux/globalStateSlice.ts";
import { Box, useTheme } from "@mui/material";
import React, { Suspense, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getFileEntityUrl } from "../../../api/api.ts";
import { getFileLinkedUri } from "../../../util";
import { closeEpubViewer } from "../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import SessionManager, { UserSettings } from "../../../session";
import { getFileLinkedUri } from "../../../util";
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
const Epub = React.lazy(() => import("./Epub.tsx"));
@ -23,10 +23,7 @@ const EpubViewer = () => {
(epubcifi: string) => {
setLocation(epubcifi);
if (viewerState?.file) {
SessionManager.set(
`${UserSettings.BookLocationPrefix}_${viewerState.file.id}`,
epubcifi,
);
SessionManager.set(`${UserSettings.BookLocationPrefix}_${viewerState.file.id}`, epubcifi);
}
},
[viewerState?.file],
@ -46,10 +43,8 @@ const EpubViewer = () => {
}),
)
.then((res) => {
setSrc(res.urls[0]);
const location = SessionManager.get(
`${UserSettings.BookLocationPrefix}_${viewerState.file.id}`,
);
setSrc(res.urls[0].url);
const location = SessionManager.get(`${UserSettings.BookLocationPrefix}_${viewerState.file.id}`);
if (location) {
setLocation(location);
}

View File

@ -37,7 +37,7 @@ const ImageEditor = () => {
}),
)
.then((res) => {
setImageSrc(res.urls[0]);
setImageSrc(res.urls[0].url);
})
.catch(() => {
dispatch(switchToImageViewer());

View File

@ -23,9 +23,7 @@ export interface IPhotoProps extends React.HTMLAttributes<HTMLElement> {
broken: boolean;
onPhotoLoad: (params: IPhotoLoadedParams) => void;
loadingElement?: JSX.Element;
brokenElement?:
| JSX.Element
| ((photoProps: BrokenElementParams) => JSX.Element);
brokenElement?: JSX.Element | ((photoProps: BrokenElementParams) => JSX.Element);
}
export default function Photo({
@ -51,7 +49,7 @@ export default function Photo({
}),
)
.then((res) => {
setImageSrc(res.urls[0]);
setImageSrc(res.urls[0].url);
})
.catch((e) => {
if (mountedRef.current) {
@ -96,11 +94,7 @@ export default function Photo({
/>
)}
{!loaded && (
<FacebookCircularProgress
sx={{ position: "absolute", top: 0 }}
fgColor={"#fff"}
bgColor={grey[800]}
/>
<FacebookCircularProgress sx={{ position: "absolute", top: 0 }} fgColor={"#fff"} bgColor={grey[800]} />
)}
</>
);
@ -109,9 +103,7 @@ export default function Photo({
if (brokenElement) {
return (
<span className="PhotoView__icon">
{typeof brokenElement === "function"
? brokenElement({ src: imageSrc ?? "" })
: brokenElement}
{typeof brokenElement === "function" ? brokenElement({ src: imageSrc ?? "" }) : brokenElement}
</span>
);
}

View File

@ -58,7 +58,7 @@ const MusicPlayer = () => {
entity: playerState.version,
}),
);
audio.current.src = res.urls[0];
audio.current.src = res.urls[0].url;
audio.current.currentTime = 0;
audio.current.play();
audio.current.volume = latestVolume ?? volume;
@ -83,10 +83,7 @@ const MusicPlayer = () => {
if (isNext) {
playIndex(((index ?? 0) + 1) % playerState?.files.length);
} else {
playIndex(
((index ?? 0) - 1 + playerState?.files.length) %
playerState?.files.length,
);
playIndex(((index ?? 0) - 1 + playerState?.files.length) % playerState?.files.length);
}
break;
case LoopMode.single_repeat:
@ -94,9 +91,7 @@ const MusicPlayer = () => {
break;
case LoopMode.shuffle:
if (isNext) {
const nextIndex = Math.floor(
Math.random() * playerState?.files.length,
);
const nextIndex = Math.floor(Math.random() * playerState?.files.length);
playIndex(nextIndex);
} else {
playHistory.current.pop();
@ -124,9 +119,7 @@ const MusicPlayer = () => {
}, []);
const playingTooltip = playerState
? `[${(index ?? 0) + 1}/${playerState.files.length}] ${playerState?.files[
index ?? 0
]?.name}`
? `[${(index ?? 0) + 1}/${playerState.files.length}] ${playerState?.files[index ?? 0]?.name}`
: "";
const onPlayerPopoverClose = useCallback(() => {

View File

@ -1,11 +1,11 @@
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
import ViewerDialog, { ViewerLoading } from "./ViewerDialog.tsx";
import React, { useCallback, useEffect, useState } from "react";
import { closePdfViewer } from "../../redux/globalStateSlice.ts";
import { Box, useTheme } from "@mui/material";
import { getFileEntityUrl } from "../../api/api.ts";
import { getFileLinkedUri } from "../../util";
import i18next from "i18next";
import { useCallback, useEffect, useState } from "react";
import { getFileEntityUrl } from "../../api/api.ts";
import { closePdfViewer } from "../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
import { getFileLinkedUri } from "../../util";
import ViewerDialog, { ViewerLoading } from "./ViewerDialog.tsx";
const viewerBase = "/pdfviewer.html";
@ -31,7 +31,7 @@ const PdfViewer = () => {
)
.then((res) => {
const search = new URLSearchParams();
search.set("file", res.urls[0]);
search.set("file", res.urls[0].url);
search.set("lng", i18next.language);
search.set("darkMode", theme.palette.mode == "dark" ? "2" : "1");
setSrc(`${viewerBase}?${search.toString()}`);

View File

@ -1,20 +1,15 @@
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
closePhotopeaViewer,
GeneralViewerState,
} from "../../../redux/globalStateSlice.ts";
import { Box, Button, ButtonGroup, ListItemText, Menu } from "@mui/material";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { getFileEntityUrl } from "../../../api/api.ts";
import { fileExtension, getFileLinkedUri } from "../../../util";
import { closePhotopeaViewer, GeneralViewerState } from "../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import { savePhotopea } from "../../../redux/thunks/viewer.ts";
import useActionDisplayOpt, {
canUpdate,
} from "../../FileManager/ContextMenu/useActionDisplayOpt.ts";
import CaretDown from "../../Icons/CaretDown.tsx";
import { fileExtension, getFileLinkedUri } from "../../../util";
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
import useActionDisplayOpt, { canUpdate } from "../../FileManager/ContextMenu/useActionDisplayOpt.ts";
import CaretDown from "../../Icons/CaretDown.tsx";
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
import SaveAsNewFormat from "./SaveAsNewFormat.tsx";
const photopeiaOrigin = "https://www.photopea.com";
@ -23,10 +18,7 @@ const photopeiaUrl =
const saveCommand = "SAVE";
const savePSDCommand = "SAVEPSD";
const appendBuffer = function (
buffer1: ArrayBuffer,
buffer2: ArrayBuffer,
): ArrayBuffer {
const appendBuffer = function (buffer1: ArrayBuffer, buffer2: ArrayBuffer): ArrayBuffer {
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
@ -41,13 +33,9 @@ const saveOpt = {
const Photopea = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const viewerState = useAppSelector(
(state) => state.globalState.photopeaViewer,
);
const viewerState = useAppSelector((state) => state.globalState.photopeaViewer);
const displayOpt = useActionDisplayOpt(
viewerState?.file ? [viewerState?.file] : [],
);
const displayOpt = useActionDisplayOpt(viewerState?.file ? [viewerState?.file] : []);
const [loading, setLoading] = useState(false);
const [loaded, setLoaded] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@ -88,7 +76,7 @@ const Photopea = () => {
}),
)
.then((res) => {
entityUrl.current = res.urls[0];
entityUrl.current = res.urls[0].url;
setSrc(photopeiaUrl);
})
.catch(() => {
@ -110,10 +98,7 @@ const Photopea = () => {
}
}
pp.current.contentWindow?.postMessage(
`app.activeDocument.saveToOE("${ext}")`,
"*",
);
pp.current.contentWindow?.postMessage(`app.activeDocument.saveToOE("${ext}")`, "*");
saveStarted.current = newFile ? saveOpt.saveAs : saveOpt.started;
};
@ -124,15 +109,10 @@ const Photopea = () => {
console.log(e);
if (e.data == "done") {
if (doneCount.current == 0) {
pp.current?.contentWindow?.postMessage(
`app.open("${entityUrl.current}","",false)`,
"*",
);
pp.current?.contentWindow?.postMessage(`app.open("${entityUrl.current}","",false)`, "*");
} else if (doneCount.current == 2) {
pp.current?.contentWindow?.postMessage(
`app.activeDocument.name="${
currentState.current?.file.name.replace(/"/g, '\\"') ?? ""
}"`,
`app.activeDocument.name="${currentState.current?.file.name.replace(/"/g, '\\"') ?? ""}"`,
"*",
);
setLoaded(true);
@ -161,11 +141,7 @@ const Photopea = () => {
dispatch(closePhotopeaViewer());
}, [dispatch]);
const onLoad = useCallback(
(e: React.SyntheticEvent<HTMLIFrameElement>) =>
(pp.current = e.currentTarget),
[],
);
const onLoad = useCallback((e: React.SyntheticEvent<HTMLIFrameElement>) => (pp.current = e.currentTarget), []);
const openMore = useCallback(
(e: React.MouseEvent<any>) => {
@ -200,11 +176,7 @@ const Photopea = () => {
readOnly={!supportUpdate.current}
actions={
supportUpdate.current ? (
<ButtonGroup
disabled={loading || !loaded}
disableElevation
variant="contained"
>
<ButtonGroup disabled={loading || !loaded} disableElevation variant="contained">
<Button onClick={() => save()} variant={"contained"}>
{t("fileManager.save")}
</Button>

View File

@ -80,7 +80,7 @@ const VideoViewer = () => {
}),
);
art.subtitle.switch(subtitleUrl.urls[0], {
art.subtitle.switch(subtitleUrl.urls[0].url, {
type: fileExtension(subtitle.name) ?? "",
});
art.subtitle.show = true;
@ -129,7 +129,7 @@ const VideoViewer = () => {
)
.then((res) => {
const current = art.currentTime;
currentUrl.current = res.urls[0];
currentUrl.current = res.urls[0].url;
let timeOut = dayjs(res.expires).diff(dayjs(), "millisecond") - srcRefreshMargin;
if (timeOut < 0) {
@ -137,7 +137,7 @@ const VideoViewer = () => {
}
currentExpire.current = setTimeout(refreshSrc, timeOut);
art.switchUrl(res.urls[0]).then(() => {
art.switchUrl(res.urls[0].url).then(() => {
art.currentTime = current;
});
@ -336,7 +336,7 @@ const VideoViewer = () => {
const realUrl = base.join(...url.split("/"));
try {
const res = await dispatch(getFileEntityUrl({ uris: [realUrl.toString()] }));
return res.urls[0];
return res.urls[0].url;
} catch (e) {
console.error(e);
return url;

View File

@ -22,8 +22,6 @@ import { AppThunk } from "../store.ts";
import { promiseId, selectOption } from "./dialog.ts";
import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks, walk, walkAll } from "./file.ts";
const streamSaverParam = "stream_saver";
enum MultipleDownloadOption {
Browser,
StreamSaver,
@ -99,7 +97,7 @@ export function backendBatchDownload(files: FileResponse[]): AppThunk {
"application:fileManager.preparingBathDownload",
);
window.location.assign(downloadUrl.urls[0]);
window.location.assign(downloadUrl.urls[0].url);
};
}
@ -248,7 +246,7 @@ function startBrowserBatchDownloadTo(
appendLog(i18next.t("modals.directoryDownloadStarted", { name }));
try {
const res = await fetch(entityUrls.urls[i], {
const res = await fetch(entityUrls.urls[i].url, {
signal: cancelSignals[downloadId].signal,
});
await saveFileToFileSystemDirectory(handle, await res.blob(), name);
@ -333,7 +331,7 @@ export function streamSaverDownload(files: FileResponse[]): AppThunk {
if (!url) {
continue;
}
const res = await fetch(url);
const res = await fetch(url.url);
const stream = () => res.body;
ctrl.enqueue({ name: batch[i].relativePath, stream });
}
@ -371,13 +369,11 @@ export function downloadSingleFile(file: FileResponse, preferredEntity?: string)
"application:fileManager.preparingDownload",
);
const downloadUrl = new URL(urlRes.urls[0]);
const streamSaverName = downloadUrl.searchParams.get(streamSaverParam);
const streamSaverName = urlRes.urls[0].stream_saver_display_name;
if (streamSaverName) {
// remove streamSaverParam from query
downloadUrl.searchParams.delete(streamSaverParam);
const fileStream = streamSaver.createWriteStream(streamSaverName);
const res = await fetch(downloadUrl.toString());
const res = await fetch(urlRes.urls[0].url);
const readableStream = res.body;
if (!readableStream) {
return;
@ -395,7 +391,7 @@ export function downloadSingleFile(file: FileResponse, preferredEntity?: string)
return readableStream.pipeTo(fileStream).finally(() => closeSnackbar(downloadingSnackbar));
}
} else {
window.location.assign(urlRes.urls[0]);
window.location.assign(urlRes.urls[0].url);
}
};
}

View File

@ -967,7 +967,7 @@ export function getEntityContent(file: FileResponse, version?: string): AppThunk
}),
);
try {
const data = await fetch(urls.urls[0]);
const data = await fetch(urls.urls[0].url);
if (!data.ok) {
throw new Error(`Failed to load file, response code ${data.status}`);
}

View File

@ -263,8 +263,8 @@ export function openCustomViewer(file: FileResponse, viewer: Viewer, preferredVe
const currentUser = SessionManager.currentUser();
const vars: { [key: string]: string } = {
src: encodeURIComponent(entityUrl.urls[0]),
src_raw: entityUrl.urls[0],
src: encodeURIComponent(entityUrl.urls[0].url),
src_raw: entityUrl.urls[0].url,
name: encodeURIComponent(file.name),
version: preferredVersion ? preferredVersion : "",
id: file.id,