From f1cc396bfc66f0155218b0236d9d9768c902a57e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 16 Dec 2023 01:28:31 +0100 Subject: [PATCH] Store and restore download lists --- front/apps/mobile/package.json | 1 + front/packages/ui/package.json | 4 +- front/packages/ui/src/downloads/download.tsx | 129 +++++++++++++----- .../ui/src/downloads/download.web.tsx | 0 front/yarn.lock | 19 ++- 5 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 front/packages/ui/src/downloads/download.web.tsx diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json index 6085325b..1b14268b 100644 --- a/front/apps/mobile/package.json +++ b/front/apps/mobile/package.json @@ -28,6 +28,7 @@ "expo-build-properties": "~0.8.3", "expo-constants": "~14.4.2", "expo-dev-client": "~2.4.12", + "expo-file-system": "~15.4.5", "expo-font": "~11.4.0", "expo-linear-gradient": "~12.3.0", "expo-linking": "~5.0.2", diff --git a/front/packages/ui/package.json b/front/packages/ui/package.json index 282d6479..63a747ce 100644 --- a/front/packages/ui/package.json +++ b/front/packages/ui/package.json @@ -18,6 +18,7 @@ "@material-symbols/svg-400": "*", "@shopify/flash-list": "^1.3.1", "@tanstack/react-query": "*", + "expo-file-system": "*", "expo-linear-gradient": "*", "i18next": "*", "moti": "*", @@ -29,7 +30,8 @@ "yoshiki": "*" }, "optionalDependencies": { - "@kesha-antonov/react-native-background-downloader": "^2.10.0" + "@kesha-antonov/react-native-background-downloader": "^2.10.0", + "expo-file-system": "^15.6.0" }, "peerDependenciesMeta": { "@kesha-antonov/react-native-background-downloader": { diff --git a/front/packages/ui/src/downloads/download.tsx b/front/packages/ui/src/downloads/download.tsx index 7e266f88..1b24e2ad 100644 --- a/front/packages/ui/src/downloads/download.tsx +++ b/front/packages/ui/src/downloads/download.tsx @@ -26,6 +26,7 @@ import { checkForExistingDownloads, ensureDownloadsAreRunning, } from "@kesha-antonov/react-native-background-downloader"; +import { deleteAsync } from "expo-file-system"; import { Account, Episode, @@ -46,9 +47,9 @@ type State = { size: number; availableSize: number; error?: string; - pause: () => void; - resume: () => void; - stop: () => void; + pause: (() => void) | null; + resume: (() => void) | null; + remove: () => void; play: () => void; }; @@ -74,11 +75,42 @@ const query = (query: QueryIdentifier, info: Account): Promise => info.token.access_token, ); -const listenToTask = (task: DownloadTask, update: (f: (old: State) => State) => void) => { +const setupDownloadTask = ( + state: { data: Episode | Movie; info: WatchInfo; path: string }, + task: DownloadTask, + store: ReturnType, +) => { + const stateAtom = atom({ + status: task.state, + progress: task.percent * 100, + size: task.totalBytes, + availableSize: task.bytesWritten, + pause: () => { + task.pause(); + store.set(stateAtom, (x) => ({ ...x, state: "PAUSED" })); + }, + resume: () => { + task.resume(); + store.set(stateAtom, (x) => ({ ...x, state: "DOWNLOADING" })); + }, + remove: () => { + task.stop(); + store.set(downloadAtom, (x) => x.filter((y) => y.data.id !== task.id)); + }, + play: () => { + // TODO: set useQuery cache + // TODO: move to the play page. + }, + } as State); + + // we use the store instead of the onMount because we want to update the state to cache it even if it was not + // used during this launch of the app. + const update = updater(store, stateAtom); + task .begin(({ expectedBytes }) => update((x) => ({ ...x, size: expectedBytes }))) .progress((percent, availableSize, size) => - update((x) => ({ ...x, percent, size, availableSize })), + update((x) => ({ ...x, percent, size, availableSize, status: "DOWNLOADING" })), ) .done(() => { update((x) => ({ ...x, percent: 100, status: "DONE" })); @@ -87,11 +119,36 @@ const listenToTask = (task: DownloadTask, update: (f: (old: State) => State) => completeHandler(task.id); }) .error((error) => update((x) => ({ ...x, status: "FAILED", error }))); + + return { data: state.data, info: state.info, path: state.path, state: stateAtom }; +}; + +const updater = ( + store: ReturnType, + atom: PrimitiveAtom, +): ((f: (old: State) => State) => void) => { + return (f) => { + // if it lags, we could only store progress info on status change and not on every change. + store.set(atom, f); + + const downloads = store.get(downloadAtom); + storage.set( + "downloads", + JSON.stringify( + downloads.map((d) => ({ + data: d.data, + info: d.info, + path: d.path, + state: store.get(d.state), + })), + ), + ); + }; }; export const useDownloader = () => { const setDownloads = useSetAtom(downloadAtom); - const atomStore = useStore(); + const store = useStore(); return async (type: "episode" | "movie", slug: string) => { const account = getCurrentAccount()!; @@ -114,27 +171,7 @@ export const useDownloader = () => { // network: Network.ALL, }); - const state = atom({ - status: task.state, - progress: task.percent * 100, - size: task.totalBytes, - availableSize: task.bytesWritten, - pause: () => task.pause(), - resume: () => task.resume(), - stop: () => { - task.stop(); - setDownloads((x) => x.filter((y) => y.data.id !== task.id)); - }, - play: () => { - // TODO: set useQuery cache - // TODO: move to the play page. - }, - }); - - // we use the store instead of the onMount because we want to update the state to cache it even if it was not - // used during this launch of the app. - listenToTask(task, (f) => atomStore.set(state, f)); - setDownloads((x) => [...x, { data, info, path, state }]); + setDownloads((x) => [...x, setupDownloadTask({ data, info, path }, task, store)]); }; }; @@ -143,15 +180,39 @@ export const DownloadProvider = () => { useEffect(() => { async function run() { + if (store.get(downloadAtom).length) return; + const tasks = await checkForExistingDownloads(); - const downloads = store.get(downloadAtom); + const dls: { data: Episode | Movie; info: WatchInfo; path: string; state: State }[] = + JSON.parse(storage.getString("downloads") ?? "[]"); + const downloads = dls.map((dl) => { + const t = tasks.find((x) => x.id == dl.data.id); + if (t) return setupDownloadTask(dl, t, store); + return { + data: dl.data, + info: dl.info, + path: dl.path, + state: atom({ + status: dl.state.status === "DONE" ? "DONE" : "FAILED", + progress: dl.state.progress, + size: dl.state.size, + availableSize: dl.state.availableSize, + pause: null, + resume: null, + play: () => { + // TODO: setup this + }, + remove: () => { + deleteAsync(dl.path); + store.set(downloadAtom, (x) => x.filter((y) => y.data.id !== dl.data.id)); + }, + } as State), + }; + }); + store.set(downloadAtom, downloads); + for (const t of tasks) { - const d = downloads.find((x) => x.data.id === t.id); - if (!d) { - t.stop(); - continue; - } - listenToTask(t, (f) => store.set(d.state, f)); + if (!downloads.find((x) => x.data.id === t.id)) t.stop(); } ensureDownloadsAreRunning(); } diff --git a/front/packages/ui/src/downloads/download.web.tsx b/front/packages/ui/src/downloads/download.web.tsx new file mode 100644 index 00000000..e69de29b diff --git a/front/yarn.lock b/front/yarn.lock index 906d0378..ac017f68 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2878,9 +2878,6 @@ __metadata: "@tanstack/react-query": "*" react: "*" react-native: "*" - dependenciesMeta: - "@kesha-antonov/react-native-background-downloader": - optional: true peerDependenciesMeta: react-native-web: optional: true @@ -2952,6 +2949,7 @@ __metadata: "@kyoo/primitives": "workspace:^" "@shopify/flash-list": ^1.6.3 "@types/react": 18.2.39 + expo-file-system: ^15.6.0 react-native-uuid: ^2.0.1 typescript: ^5.3.2 peerDependencies: @@ -2959,6 +2957,7 @@ __metadata: "@material-symbols/svg-400": "*" "@shopify/flash-list": ^1.3.1 "@tanstack/react-query": "*" + expo-file-system: "*" expo-linear-gradient: "*" i18next: "*" moti: "*" @@ -2971,6 +2970,8 @@ __metadata: dependenciesMeta: "@kesha-antonov/react-native-background-downloader": optional: true + expo-file-system: + optional: true peerDependenciesMeta: "@kesha-antonov/react-native-background-downloader": optional: true @@ -7890,6 +7891,17 @@ __metadata: languageName: node linkType: hard +"expo-file-system@npm:^15.6.0": + version: 15.9.0 + resolution: "expo-file-system@npm:15.9.0" + dependencies: + uuid: ^3.4.0 + peerDependencies: + expo: "*" + checksum: 9e10089cd28e7384fdb942c5a0515a5d7e5cd43637ebe42ab5cbc1fb5a06cf466d4ea195eea3479a4080fc30215c2e2df7048e83a6bae3413fff95b9487a5144 + languageName: node + linkType: hard + "expo-file-system@npm:~15.4.0, expo-file-system@npm:~15.4.2": version: 15.4.2 resolution: "expo-file-system@npm:15.4.2" @@ -11301,6 +11313,7 @@ __metadata: expo-build-properties: ~0.8.3 expo-constants: ~14.4.2 expo-dev-client: ~2.4.12 + expo-file-system: ~15.4.5 expo-font: ~11.4.0 expo-linear-gradient: ~12.3.0 expo-linking: ~5.0.2