From c4d4d3aa6f28e04a5828f3b4b4453d239746bed0 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 16 May 2025 13:49:45 +0800 Subject: [PATCH] refactor(download): handle stream saver download outside of driver implementation --- src/api/explorer.ts | 7 ++- .../Viewers/EpubViewer/EpubViewer.tsx | 23 +++---- .../Viewers/ImageViewer/ImageEditor.tsx | 2 +- .../ImageViewer/react-photo-view/Photo.tsx | 16 ++--- .../Viewers/MusicPlayer/MusicPlayer.tsx | 15 ++--- src/component/Viewers/PdfViewer.tsx | 14 ++--- src/component/Viewers/Photopea/Photopea.tsx | 62 +++++-------------- src/component/Viewers/Video/VideoViewer.tsx | 8 +-- src/redux/thunks/download.ts | 16 ++--- src/redux/thunks/file.ts | 2 +- src/redux/thunks/viewer.ts | 4 +- 11 files changed, 61 insertions(+), 108 deletions(-) diff --git a/src/api/explorer.ts b/src/api/explorer.ts index 86a6ddb..26d3991 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -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; diff --git a/src/component/Viewers/EpubViewer/EpubViewer.tsx b/src/component/Viewers/EpubViewer/EpubViewer.tsx index e1da5f1..0b098ca 100644 --- a/src/component/Viewers/EpubViewer/EpubViewer.tsx +++ b/src/component/Viewers/EpubViewer/EpubViewer.tsx @@ -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); } diff --git a/src/component/Viewers/ImageViewer/ImageEditor.tsx b/src/component/Viewers/ImageViewer/ImageEditor.tsx index 0fa5d98..d7bb689 100644 --- a/src/component/Viewers/ImageViewer/ImageEditor.tsx +++ b/src/component/Viewers/ImageViewer/ImageEditor.tsx @@ -37,7 +37,7 @@ const ImageEditor = () => { }), ) .then((res) => { - setImageSrc(res.urls[0]); + setImageSrc(res.urls[0].url); }) .catch(() => { dispatch(switchToImageViewer()); diff --git a/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx b/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx index d330802..ee5a279 100644 --- a/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx +++ b/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx @@ -23,9 +23,7 @@ export interface IPhotoProps extends React.HTMLAttributes { 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 && ( - + )} ); @@ -109,9 +103,7 @@ export default function Photo({ if (brokenElement) { return ( - {typeof brokenElement === "function" - ? brokenElement({ src: imageSrc ?? "" }) - : brokenElement} + {typeof brokenElement === "function" ? brokenElement({ src: imageSrc ?? "" }) : brokenElement} ); } diff --git a/src/component/Viewers/MusicPlayer/MusicPlayer.tsx b/src/component/Viewers/MusicPlayer/MusicPlayer.tsx index 5d26c22..1ed5e3f 100644 --- a/src/component/Viewers/MusicPlayer/MusicPlayer.tsx +++ b/src/component/Viewers/MusicPlayer/MusicPlayer.tsx @@ -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(() => { diff --git a/src/component/Viewers/PdfViewer.tsx b/src/component/Viewers/PdfViewer.tsx index 934dd20..5e6d48d 100644 --- a/src/component/Viewers/PdfViewer.tsx +++ b/src/component/Viewers/PdfViewer.tsx @@ -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()}`); diff --git a/src/component/Viewers/Photopea/Photopea.tsx b/src/component/Viewers/Photopea/Photopea.tsx index 41eb7c4..9c64798 100644 --- a/src/component/Viewers/Photopea/Photopea.tsx +++ b/src/component/Viewers/Photopea/Photopea.tsx @@ -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); @@ -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) => - (pp.current = e.currentTarget), - [], - ); + const onLoad = useCallback((e: React.SyntheticEvent) => (pp.current = e.currentTarget), []); const openMore = useCallback( (e: React.MouseEvent) => { @@ -200,11 +176,7 @@ const Photopea = () => { readOnly={!supportUpdate.current} actions={ supportUpdate.current ? ( - + diff --git a/src/component/Viewers/Video/VideoViewer.tsx b/src/component/Viewers/Video/VideoViewer.tsx index 2d43ecf..eac9306 100644 --- a/src/component/Viewers/Video/VideoViewer.tsx +++ b/src/component/Viewers/Video/VideoViewer.tsx @@ -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; diff --git a/src/redux/thunks/download.ts b/src/redux/thunks/download.ts index ec97699..7e6192a 100644 --- a/src/redux/thunks/download.ts +++ b/src/redux/thunks/download.ts @@ -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); } }; } diff --git a/src/redux/thunks/file.ts b/src/redux/thunks/file.ts index 0fa96a7..d6c7333 100644 --- a/src/redux/thunks/file.ts +++ b/src/redux/thunks/file.ts @@ -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}`); } diff --git a/src/redux/thunks/viewer.ts b/src/redux/thunks/viewer.ts index 5ebbbd3..537fdfa 100644 --- a/src/redux/thunks/viewer.ts +++ b/src/redux/thunks/viewer.ts @@ -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,