Store and restore download lists

This commit is contained in:
Zoe Roux 2023-12-16 01:28:31 +01:00
parent a483955763
commit f1cc396bfc
5 changed files with 115 additions and 38 deletions

View File

@ -28,6 +28,7 @@
"expo-build-properties": "~0.8.3", "expo-build-properties": "~0.8.3",
"expo-constants": "~14.4.2", "expo-constants": "~14.4.2",
"expo-dev-client": "~2.4.12", "expo-dev-client": "~2.4.12",
"expo-file-system": "~15.4.5",
"expo-font": "~11.4.0", "expo-font": "~11.4.0",
"expo-linear-gradient": "~12.3.0", "expo-linear-gradient": "~12.3.0",
"expo-linking": "~5.0.2", "expo-linking": "~5.0.2",

View File

@ -18,6 +18,7 @@
"@material-symbols/svg-400": "*", "@material-symbols/svg-400": "*",
"@shopify/flash-list": "^1.3.1", "@shopify/flash-list": "^1.3.1",
"@tanstack/react-query": "*", "@tanstack/react-query": "*",
"expo-file-system": "*",
"expo-linear-gradient": "*", "expo-linear-gradient": "*",
"i18next": "*", "i18next": "*",
"moti": "*", "moti": "*",
@ -29,7 +30,8 @@
"yoshiki": "*" "yoshiki": "*"
}, },
"optionalDependencies": { "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": { "peerDependenciesMeta": {
"@kesha-antonov/react-native-background-downloader": { "@kesha-antonov/react-native-background-downloader": {

View File

@ -26,6 +26,7 @@ import {
checkForExistingDownloads, checkForExistingDownloads,
ensureDownloadsAreRunning, ensureDownloadsAreRunning,
} from "@kesha-antonov/react-native-background-downloader"; } from "@kesha-antonov/react-native-background-downloader";
import { deleteAsync } from "expo-file-system";
import { import {
Account, Account,
Episode, Episode,
@ -46,9 +47,9 @@ type State = {
size: number; size: number;
availableSize: number; availableSize: number;
error?: string; error?: string;
pause: () => void; pause: (() => void) | null;
resume: () => void; resume: (() => void) | null;
stop: () => void; remove: () => void;
play: () => void; play: () => void;
}; };
@ -74,11 +75,42 @@ const query = <T,>(query: QueryIdentifier<T>, info: Account): Promise<T> =>
info.token.access_token, 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<typeof useStore>,
) => {
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 task
.begin(({ expectedBytes }) => update((x) => ({ ...x, size: expectedBytes }))) .begin(({ expectedBytes }) => update((x) => ({ ...x, size: expectedBytes })))
.progress((percent, availableSize, size) => .progress((percent, availableSize, size) =>
update((x) => ({ ...x, percent, size, availableSize })), update((x) => ({ ...x, percent, size, availableSize, status: "DOWNLOADING" })),
) )
.done(() => { .done(() => {
update((x) => ({ ...x, percent: 100, status: "DONE" })); update((x) => ({ ...x, percent: 100, status: "DONE" }));
@ -87,11 +119,36 @@ const listenToTask = (task: DownloadTask, update: (f: (old: State) => State) =>
completeHandler(task.id); completeHandler(task.id);
}) })
.error((error) => update((x) => ({ ...x, status: "FAILED", error }))); .error((error) => update((x) => ({ ...x, status: "FAILED", error })));
return { data: state.data, info: state.info, path: state.path, state: stateAtom };
};
const updater = (
store: ReturnType<typeof useStore>,
atom: PrimitiveAtom<State>,
): ((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 = () => { export const useDownloader = () => {
const setDownloads = useSetAtom(downloadAtom); const setDownloads = useSetAtom(downloadAtom);
const atomStore = useStore(); const store = useStore();
return async (type: "episode" | "movie", slug: string) => { return async (type: "episode" | "movie", slug: string) => {
const account = getCurrentAccount()!; const account = getCurrentAccount()!;
@ -114,27 +171,7 @@ export const useDownloader = () => {
// network: Network.ALL, // network: Network.ALL,
}); });
const state = atom({ setDownloads((x) => [...x, setupDownloadTask({ data, info, path }, task, store)]);
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 }]);
}; };
}; };
@ -143,15 +180,39 @@ export const DownloadProvider = () => {
useEffect(() => { useEffect(() => {
async function run() { async function run() {
if (store.get(downloadAtom).length) return;
const tasks = await checkForExistingDownloads(); 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) { for (const t of tasks) {
const d = downloads.find((x) => x.data.id === t.id); if (!downloads.find((x) => x.data.id === t.id)) t.stop();
if (!d) {
t.stop();
continue;
}
listenToTask(t, (f) => store.set(d.state, f));
} }
ensureDownloadsAreRunning(); ensureDownloadsAreRunning();
} }

View File

@ -2878,9 +2878,6 @@ __metadata:
"@tanstack/react-query": "*" "@tanstack/react-query": "*"
react: "*" react: "*"
react-native: "*" react-native: "*"
dependenciesMeta:
"@kesha-antonov/react-native-background-downloader":
optional: true
peerDependenciesMeta: peerDependenciesMeta:
react-native-web: react-native-web:
optional: true optional: true
@ -2952,6 +2949,7 @@ __metadata:
"@kyoo/primitives": "workspace:^" "@kyoo/primitives": "workspace:^"
"@shopify/flash-list": ^1.6.3 "@shopify/flash-list": ^1.6.3
"@types/react": 18.2.39 "@types/react": 18.2.39
expo-file-system: ^15.6.0
react-native-uuid: ^2.0.1 react-native-uuid: ^2.0.1
typescript: ^5.3.2 typescript: ^5.3.2
peerDependencies: peerDependencies:
@ -2959,6 +2957,7 @@ __metadata:
"@material-symbols/svg-400": "*" "@material-symbols/svg-400": "*"
"@shopify/flash-list": ^1.3.1 "@shopify/flash-list": ^1.3.1
"@tanstack/react-query": "*" "@tanstack/react-query": "*"
expo-file-system: "*"
expo-linear-gradient: "*" expo-linear-gradient: "*"
i18next: "*" i18next: "*"
moti: "*" moti: "*"
@ -2971,6 +2970,8 @@ __metadata:
dependenciesMeta: dependenciesMeta:
"@kesha-antonov/react-native-background-downloader": "@kesha-antonov/react-native-background-downloader":
optional: true optional: true
expo-file-system:
optional: true
peerDependenciesMeta: peerDependenciesMeta:
"@kesha-antonov/react-native-background-downloader": "@kesha-antonov/react-native-background-downloader":
optional: true optional: true
@ -7890,6 +7891,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "expo-file-system@npm:~15.4.0, expo-file-system@npm:~15.4.2":
version: 15.4.2 version: 15.4.2
resolution: "expo-file-system@npm:15.4.2" resolution: "expo-file-system@npm:15.4.2"
@ -11301,6 +11313,7 @@ __metadata:
expo-build-properties: ~0.8.3 expo-build-properties: ~0.8.3
expo-constants: ~14.4.2 expo-constants: ~14.4.2
expo-dev-client: ~2.4.12 expo-dev-client: ~2.4.12
expo-file-system: ~15.4.5
expo-font: ~11.4.0 expo-font: ~11.4.0
expo-linear-gradient: ~12.3.0 expo-linear-gradient: ~12.3.0
expo-linking: ~5.0.2 expo-linking: ~5.0.2