Create a basic download list

This commit is contained in:
Zoe Roux 2023-12-18 18:45:32 +01:00
parent 2e0a0e5eb0
commit c0cf11ee79
6 changed files with 202 additions and 9 deletions

View File

@ -82,7 +82,6 @@ const Menu = <AsProps,>({
<> <>
<Trigger <Trigger
onPress={() => { onPress={() => {
if ("onPress" in props && typeof props.onPress === "function") props.onPress();
setOpen(true); setOpen(true);
}} }}
{...(props as any)} {...(props as any)}

View File

@ -18,21 +18,192 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useAtomValue } from "jotai"; import { State, downloadAtom } from "./state";
import { downloadAtom } from "./state";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { View } from "react-native"; import { ImageStyle, View } from "react-native";
import { P } from "@kyoo/primitives"; 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<State>;
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 (
<PressableFeedback
onPress={() => play?.()}
{...css(
{
alignItems: "center",
flexDirection: "row",
fover: {
self: focusReset,
title: {
textDecorationLine: "underline",
},
},
},
props,
)}
>
<ImageBackground
src={image}
quality="low"
alt=""
gradient={false}
hideLoad={false}
layout={{
width: percent(18),
aspectRatio: 16 / 9,
}}
{...(css({ flexShrink: 0, m: ts(1) }) as { style: ImageStyle })}
>
{/* {(watchedPercent || watchedStatus === WatchStatusV.Completed) && ( */}
{/* <> */}
{/* <View */}
{/* {...css({ */}
{/* backgroundColor: (theme) => theme.overlay0, */}
{/* width: percent(100), */}
{/* height: ts(0.5), */}
{/* position: "absolute", */}
{/* bottom: 0, */}
{/* })} */}
{/* /> */}
{/* <View */}
{/* {...css({ */}
{/* backgroundColor: (theme) => theme.accent, */}
{/* width: percent(watchedPercent ?? 100), */}
{/* height: ts(0.5), */}
{/* position: "absolute", */}
{/* bottom: 0, */}
{/* })} */}
{/* /> */}
{/* </> */}
{/* )} */}
</ImageBackground>
<View
{...css({
flexGrow: 1,
flexShrink: 1,
flexDirection: "row",
justifyContent: "space-between",
})}
>
<View {...css({ flexGrow: 1, flexShrink: 1 })}>
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
{name ?? t("show.episodeNoMetadata")}
</H6>
{status === "FAILED" && <P>{t("downloads.error", { error: error ?? "Unknow error" })}</P>}
{runtime && status !== "FAILED" && <SubP>{displayRuntime(runtime)}</SubP>}
</View>
<Menu Trigger={IconButton} icon={downloadIcon(status)}>
{status === "FAILED" && <Menu.Item label={t("downloads.retry")} onSelect={() => {}} />}
{status === "DOWNLOADING" && (
<Menu.Item label={t("downloads.pause")} onSelect={() => pause?.()} />
)}
{status === "PAUSED" && (
<Menu.Item label={t("downloads.resume")} onSelect={() => resume?.()} />
)}
<Menu.Item
label={t("downloads.delete")}
onSelect={() => {
Alert.alert(
t("downloads.delete"),
t("downloads.deleteMessage"),
[
{ text: t("misc.cancel"), style: "cancel" },
{ text: t("misc.delete"), onPress: remove, style: "destructive" },
],
{
icon: "error",
},
);
}}
/>
</Menu>
</View>
</PressableFeedback>
);
};
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 = () => { export const DownloadPage = () => {
const pageStyle = usePageStyle();
const downloads = useAtomValue(downloadAtom); const downloads = useAtomValue(downloadAtom);
const { t } = useTranslation();
if (downloads.length === 0) return <EmptyView message={t("downloads.empty")} />;
return ( return (
<FlashList <FlashList
data={downloads} data={downloads}
renderItem={({ item }) => <P>{item.data.name}</P>} getItemType={(item) => item.data.kind}
renderItem={({ item }) => (
<DownloadedItem
name={
item.data.kind === "episode"
? `${episodeDisplayNumber(item.data)!}: ${item.data.name}`
: item.data.name
}
statusAtom={item.state}
runtime={item.data.runtime}
kind={item.data.kind}
image={item.data.kind === "episode" ? item.data.thumbnail : item.data.poster}
/>
)}
estimatedItemSize={EpisodeLine.layout.size}
keyExtractor={(x) => x.data.id} keyExtractor={(x) => x.data.id}
numColumns={1} numColumns={1}
contentContainerStyle={pageStyle}
/> />
); );
}; };

View File

@ -37,7 +37,7 @@ import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { Platform, ToastAndroid } from "react-native"; import { Platform, ToastAndroid } from "react-native";
type State = { export type State = {
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED"; status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
progress: number; progress: number;
size: number; size: number;
@ -158,6 +158,11 @@ export const useDownloader = () => {
query(Player.infoQuery(type, slug), account), 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 // TODO: support custom paths
const path = `${RNBackgroundDownloader.directories.documents}/${slug}-${data.id}.${info.extension}`; const path = `${RNBackgroundDownloader.directories.documents}/${slug}-${data.id}.${info.extension}`;
const task = RNBackgroundDownloader.download({ const task = RNBackgroundDownloader.download({

View File

@ -55,7 +55,7 @@ export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"]
}; };
const SearchBar = forwardRef<TextInput, Stylable>(function SearchBar(props, ref) { const SearchBar = forwardRef<TextInput, Stylable>(function SearchBar(props, ref) {
const { css, theme } = useYoshiki(); const { theme } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const { push, replace, back } = useRouter(); const { push, replace, back } = useRouter();
const hasChanged = useRef<boolean>(false); const hasChanged = useRef<boolean>(false);
@ -135,12 +135,12 @@ export const NavbarProfile = () => {
t("login.delete"), t("login.delete"),
t("login.delete-confirmation"), t("login.delete-confirmation"),
[ [
{ text: t("misc.cancel"), style: "cancel" },
{ {
text: t("misc.delete"), text: t("misc.delete"),
onPress: deleteAccount, onPress: deleteAccount,
style: "destructive", style: "destructive",
}, },
{ text: t("misc.cancel"), style: "cancel" },
], ],
{ {
cancelable: true, cancelable: true,

View File

@ -107,6 +107,15 @@
"delete": "Delete your account", "delete": "Delete your account",
"delete-confirmation": "This action can't be reverted. Are you sure?" "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": { "errors": {
"connection": "Could not connect to the kyoo's server", "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?", "connection-tips": "Troublshotting tips:\n - Are you connected to internet?\n - Is your kyoo's server online?\n - Have your account been banned?",

View File

@ -107,6 +107,15 @@
"delete": "Supprimer votre compte", "delete": "Supprimer votre compte",
"delete-confirmation": "Cette action ne peut pas être annulée. Êtes-vous sur?" "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": { "errors": {
"connection": "Impossible de se connecter au serveur de kyoo.", "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 ?", "connection-tips": "Possible causes:\n - Etes-vous connecté a internet ?\n - Votre serveur kyoo est-il allumé ?\n - Votre compte est-il bannis ?",