diff --git a/front/packages/primitives/src/menu.tsx b/front/packages/primitives/src/menu.tsx index 483ab6e8..55627b95 100644 --- a/front/packages/primitives/src/menu.tsx +++ b/front/packages/primitives/src/menu.tsx @@ -82,7 +82,6 @@ const Menu = ({ <> { - if ("onPress" in props && typeof props.onPress === "function") props.onPress(); setOpen(true); }} {...(props as any)} diff --git a/front/packages/ui/src/downloads/page.tsx b/front/packages/ui/src/downloads/page.tsx index 47c0edc8..9eda0808 100644 --- a/front/packages/ui/src/downloads/page.tsx +++ b/front/packages/ui/src/downloads/page.tsx @@ -18,21 +18,192 @@ * along with Kyoo. If not, see . */ -import { useAtomValue } from "jotai"; -import { downloadAtom } from "./state"; +import { State, downloadAtom } from "./state"; import { FlashList } from "@shopify/flash-list"; -import { View } from "react-native"; -import { P } from "@kyoo/primitives"; +import { ImageStyle, View } from "react-native"; +import { + Alert, + H6, + IconButton, + ImageBackground, + Link, + Menu, + P, + PressableFeedback, + SubP, + focusReset, + ts, + usePageStyle, +} from "@kyoo/primitives"; +import { EpisodeLine, displayRuntime, episodeDisplayNumber } from "../details/episode"; +import { useTranslation } from "react-i18next"; +import { EmptyView } from "../fetch"; +import { percent, useYoshiki } from "yoshiki/native"; +import { KyooImage } from "@kyoo/models"; +import { Atom, useAtomValue } from "jotai"; +import DownloadForOffline from "@material-symbols/svg-400/rounded/download_for_offline.svg"; +import Downloading from "@material-symbols/svg-400/rounded/downloading.svg"; +import Error from "@material-symbols/svg-400/rounded/error.svg"; +import NotStarted from "@material-symbols/svg-400/rounded/not_started.svg"; + +const DownloadedItem = ({ + name, + statusAtom, + runtime, + kind, + image, + ...props +}: { + name: string; + statusAtom: Atom; + runtime: number | null; + kind: "episode" | "movie"; + image: KyooImage | null; +}) => { + const { css } = useYoshiki(); + const { t } = useTranslation(); + const { error, status, pause, resume, remove, play } = useAtomValue(statusAtom); + + return ( + play?.()} + {...css( + { + alignItems: "center", + flexDirection: "row", + fover: { + self: focusReset, + title: { + textDecorationLine: "underline", + }, + }, + }, + props, + )} + > + + {/* {(watchedPercent || watchedStatus === WatchStatusV.Completed) && ( */} + {/* <> */} + {/* theme.overlay0, */} + {/* width: percent(100), */} + {/* height: ts(0.5), */} + {/* position: "absolute", */} + {/* bottom: 0, */} + {/* })} */} + {/* /> */} + {/* theme.accent, */} + {/* width: percent(watchedPercent ?? 100), */} + {/* height: ts(0.5), */} + {/* position: "absolute", */} + {/* bottom: 0, */} + {/* })} */} + {/* /> */} + {/* */} + {/* )} */} + + + +
+ {name ?? t("show.episodeNoMetadata")} +
+ {status === "FAILED" &&

{t("downloads.error", { error: error ?? "Unknow error" })}

} + {runtime && status !== "FAILED" && {displayRuntime(runtime)}} +
+ + {status === "FAILED" && {}} />} + {status === "DOWNLOADING" && ( + pause?.()} /> + )} + {status === "PAUSED" && ( + resume?.()} /> + )} + { + Alert.alert( + t("downloads.delete"), + t("downloads.deleteMessage"), + [ + { text: t("misc.cancel"), style: "cancel" }, + { text: t("misc.delete"), onPress: remove, style: "destructive" }, + ], + { + icon: "error", + }, + ); + }} + /> + +
+
+ ); +}; + +const downloadIcon = (status: State["status"]) => { + switch (status) { + case "DONE": + return DownloadForOffline; + case "DOWNLOADING": + return Downloading; + case "FAILED": + return Error; + case "PAUSED": + case "STOPPED": + default: + return NotStarted; + } +}; export const DownloadPage = () => { + const pageStyle = usePageStyle(); const downloads = useAtomValue(downloadAtom); + const { t } = useTranslation(); + + if (downloads.length === 0) return ; return (

{item.data.name}

} + getItemType={(item) => item.data.kind} + renderItem={({ item }) => ( + + )} + estimatedItemSize={EpisodeLine.layout.size} keyExtractor={(x) => x.data.id} numColumns={1} + contentContainerStyle={pageStyle} /> ); }; diff --git a/front/packages/ui/src/downloads/state.tsx b/front/packages/ui/src/downloads/state.tsx index 3aa6c837..715d7bc1 100644 --- a/front/packages/ui/src/downloads/state.tsx +++ b/front/packages/ui/src/downloads/state.tsx @@ -37,7 +37,7 @@ import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal"; import { ReactNode, useEffect } from "react"; import { Platform, ToastAndroid } from "react-native"; -type State = { +export type State = { status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED"; progress: number; size: number; @@ -158,6 +158,11 @@ export const useDownloader = () => { query(Player.infoQuery(type, slug), account), ]); + if (store.get(downloadAtom).find((x) => x.data.id === data.id)) { + ToastAndroid.show(`${slug} is already downloaded, skipping`, ToastAndroid.LONG); + return; + } + // TODO: support custom paths const path = `${RNBackgroundDownloader.directories.documents}/${slug}-${data.id}.${info.extension}`; const task = RNBackgroundDownloader.download({ diff --git a/front/packages/ui/src/navbar/index.tsx b/front/packages/ui/src/navbar/index.tsx index 900152df..999bd3ed 100644 --- a/front/packages/ui/src/navbar/index.tsx +++ b/front/packages/ui/src/navbar/index.tsx @@ -55,7 +55,7 @@ export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"] }; const SearchBar = forwardRef(function SearchBar(props, ref) { - const { css, theme } = useYoshiki(); + const { theme } = useYoshiki(); const { t } = useTranslation(); const { push, replace, back } = useRouter(); const hasChanged = useRef(false); @@ -135,12 +135,12 @@ export const NavbarProfile = () => { t("login.delete"), t("login.delete-confirmation"), [ + { text: t("misc.cancel"), style: "cancel" }, { text: t("misc.delete"), onPress: deleteAccount, style: "destructive", }, - { text: t("misc.cancel"), style: "cancel" }, ], { cancelable: true, diff --git a/front/translations/en.json b/front/translations/en.json index fe51ebe6..134d2080 100644 --- a/front/translations/en.json +++ b/front/translations/en.json @@ -107,6 +107,15 @@ "delete": "Delete your account", "delete-confirmation": "This action can't be reverted. Are you sure?" }, + "downloads": { + "empty": "Nothing downloaded yet, start browsing for something you like", + "error": "Error: {{error}}", + "delete": "Delete item", + "deleteMessage": "Do you want to remove this item from your local storage?", + "pause": "Pause", + "resume": "Resume", + "retry": "Retry" + }, "errors": { "connection": "Could not connect to the kyoo's server", "connection-tips": "Troublshotting tips:\n - Are you connected to internet?\n - Is your kyoo's server online?\n - Have your account been banned?", diff --git a/front/translations/fr.json b/front/translations/fr.json index 615ea641..15a87852 100644 --- a/front/translations/fr.json +++ b/front/translations/fr.json @@ -107,6 +107,15 @@ "delete": "Supprimer votre compte", "delete-confirmation": "Cette action ne peut pas être annulée. Êtes-vous sur?" }, + "downloads": { + "empty": "Rien de téléchargé pour l'instant, commencez à rechercher quelque chose que vous aimez", + "error": "Erreur: {{error}}", + "delete": "Supprimer un item télechargé", + "deleteMessage": "Voulez-vous vraiment supprimer un item télechgargé ?", + "pause": "Pause", + "resume": "Reprendre", + "retry": "Réessayer" + }, "errors": { "connection": "Impossible de se connecter au serveur de kyoo.", "connection-tips": "Possible causes:\n - Etes-vous connecté a internet ?\n - Votre serveur kyoo est-il allumé ?\n - Votre compte est-il bannis ?",