diff --git a/front/apps/mobile/app/(public)/_layout.tsx b/front/apps/mobile/app/(public)/_layout.tsx index b33bbabe..b4a57b1c 100644 --- a/front/apps/mobile/app/(public)/_layout.tsx +++ b/front/apps/mobile/app/(public)/_layout.tsx @@ -18,16 +18,26 @@ * along with Kyoo. If not, see . */ -import { NavbarTitle } from "@kyoo/ui"; -import { Stack } from "expo-router"; +import { Account, ConnectionErrorContext, useAccount } from "@kyoo/models"; +import { NavbarProfile, NavbarTitle } from "@kyoo/ui"; +import { Redirect, Stack } from "expo-router"; +import { useContext, useRef } from "react"; import { useTheme } from "yoshiki/native"; export default function PublicLayout() { const theme = useTheme(); + const account = useAccount(); + const { error } = useContext(ConnectionErrorContext); + const oldAccount = useRef(null); + + if (account && !error && account != oldAccount.current) return ; + oldAccount.current = account; + return ( , + headerRight: () => , contentStyle: { backgroundColor: theme.background, }, diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx index bb140bc5..27f3bad5 100644 --- a/front/apps/mobile/app/_layout.tsx +++ b/front/apps/mobile/app/_layout.tsx @@ -22,8 +22,10 @@ import "react-native-reanimated"; import { PortalProvider } from "@gorhom/portal"; import { ThemeSelector } from "@kyoo/primitives"; -import { AccountProvider, createQueryClient } from "@kyoo/models"; -import { QueryClientProvider } from "@tanstack/react-query"; +import { DownloadProvider } from "@kyoo/ui"; +import { AccountProvider, createQueryClient, storage } from "@kyoo/models"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; import i18next from "i18next"; import { Slot } from "expo-router"; import { getLocales } from "expo-localization"; @@ -48,7 +50,29 @@ import "@formatjs/intl-displaynames/locale-data/fr"; import en from "../../../translations/en.json"; import fr from "../../../translations/fr.json"; import { useTheme } from "yoshiki/native"; -import { DownloadProvider } from "@kyoo/ui"; +import NetInfo from "@react-native-community/netinfo"; +import { onlineManager } from "@tanstack/react-query"; + +onlineManager.setEventListener((setOnline) => { + return NetInfo.addEventListener((state) => { + setOnline(!!state.isConnected); + }); +}); + +const clientStorage = { + setItem: (key, value) => { + storage.set(key, value); + }, + getItem: (key) => { + const value = storage.getString(key); + return value === undefined ? null : value; + }, + removeItem: (key) => { + storage.delete(key); + }, +} satisfies Partial; + +export const clientPersister = createSyncStoragePersister({ storage: clientStorage }); i18next.use(initReactI18next).init({ interpolation: { @@ -92,7 +116,14 @@ export default function Root() { if (!fontsLoaded) return null; return ( - + false }, + }} + > - + ); } diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json index 1b14268b..b2f758de 100644 --- a/front/apps/mobile/package.json +++ b/front/apps/mobile/package.json @@ -20,8 +20,11 @@ "@kesha-antonov/react-native-background-downloader": "^2.10.0", "@kyoo/ui": "workspace:^", "@material-symbols/svg-400": "^0.14.1", + "@react-native-community/netinfo": "^11.2.1", "@shopify/flash-list": "1.4.3", + "@tanstack/query-sync-storage-persister": "^5.14.1", "@tanstack/react-query": "^5.12.1", + "@tanstack/react-query-persist-client": "^5.14.1", "array-shuffle": "^3.0.0", "babel-plugin-transform-inline-environment-variables": "^0.4.4", "expo": "^49.0.21", diff --git a/front/packages/models/src/accounts.tsx b/front/packages/models/src/accounts.tsx index c44ccac9..ff48332c 100644 --- a/front/packages/models/src/accounts.tsx +++ b/front/packages/models/src/accounts.tsx @@ -144,7 +144,7 @@ export const AccountProvider = ({ export const useAccount = () => { const acc = useContext(AccountContext); - return acc.find((x) => x.selected); + return acc.find((x) => x.selected) || null; }; export const useAccounts = () => { diff --git a/front/packages/models/src/index.ts b/front/packages/models/src/index.ts index 2fbbad44..4c8a5eca 100644 --- a/front/packages/models/src/index.ts +++ b/front/packages/models/src/index.ts @@ -19,6 +19,7 @@ */ export * from "./accounts"; +export { storage } from "./account-internal"; export * from "./resources"; export * from "./traits"; export * from "./page"; diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index 1d7e8f66..36e57c89 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -24,6 +24,7 @@ import { QueryClient, QueryFunctionContext, useInfiniteQuery, + useMutation, useQuery, } from "@tanstack/react-query"; import { z } from "zod"; @@ -131,6 +132,13 @@ export const queryFn = async ( return parsed.data; }; +export type MutationParam = { + params?: Record; + body?: object; + path: string[]; + method: "POST" | "DELETE"; +}; + export const createQueryClient = () => new QueryClient({ defaultOptions: { @@ -141,6 +149,15 @@ export const createQueryClient = () => refetchOnReconnect: false, retry: false, }, + mutations: { + mutationFn: (({ method, path, body, params }: MutationParam) => { + return queryFn({ + method, + path: toQueryKey({ path, params }), + body, + }); + }) as any, + }, }, }); @@ -165,7 +182,10 @@ export type QueryPage = ComponentType< randomItems?: Items[]; }; -export const toQueryKey = (query: QueryIdentifier) => { +export const toQueryKey = (query: { + path: (string | undefined)[]; + params?: { [query: string]: boolean | number | string | string[] | undefined }; +}) => { if (query.params) { return [ ...query.path, diff --git a/front/packages/ui/src/downloads/state.tsx b/front/packages/ui/src/downloads/state.tsx index 3f4b11a8..71d6ba73 100644 --- a/front/packages/ui/src/downloads/state.tsx +++ b/front/packages/ui/src/downloads/state.tsx @@ -25,9 +25,12 @@ import { deleteAsync } from "expo-file-system"; import { Account, Episode, + EpisodeP, Movie, + MovieP, QueryIdentifier, WatchInfo, + WatchInfoP, queryFn, toQueryKey, } from "@kyoo/models"; @@ -38,6 +41,7 @@ import { ReactNode, useEffect } from "react"; import { Platform, ToastAndroid } from "react-native"; import { useQueryClient } from "@tanstack/react-query"; import { Router } from "expo-router/build/types"; +import { z } from "zod"; export type State = { status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED"; @@ -202,8 +206,8 @@ export const DownloadProvider = ({ children }: { children: ReactNode }) => { const t = tasks.find((x) => x.id == dl.data.id); if (t) return setupDownloadTask(dl, t, store); return { - data: dl.data, - info: dl.info, + data: z.union([EpisodeP, MovieP]).parse(dl.data), + info: WatchInfoP.parse(dl.info), path: dl.path, state: atom({ status: dl.state.status === "DONE" ? "DONE" : "FAILED", diff --git a/front/packages/ui/src/player/watch-status-observer.tsx b/front/packages/ui/src/player/watch-status-observer.tsx index a51ad6f9..804248ce 100644 --- a/front/packages/ui/src/player/watch-status-observer.tsx +++ b/front/packages/ui/src/player/watch-status-observer.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import { WatchStatusV, queryFn, useAccount } from "@kyoo/models"; +import { MutationParam, WatchStatusV, useAccount } from "@kyoo/models"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useEffect, useCallback } from "react"; import { useAtomValue } from "jotai"; @@ -34,20 +34,23 @@ export const WatchStatusObserver = ({ }) => { const account = useAccount(); const queryClient = useQueryClient(); - const { mutate } = useMutation({ - mutationFn: (seconds: number) => - queryFn({ - path: [ - type, - slug, - "watchStatus", - `?status=${WatchStatusV.Watching}&watchedTime=${Math.round(seconds)}`, - ], - method: "POST", - }), + const { mutate: _mutate } = useMutation({ + mutationKey: [type, slug, "watchStatus"], onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type === "episode" ? "show" : type, slug] }), }); + const mutate = useCallback( + (seconds: number) => + _mutate({ + method: "POST", + path: [type, slug, "watchStatus"], + params: { + status: WatchStatusV.Watching, + watchedTime: Math.round(seconds), + }, + }), + [_mutate, type, slug], + ); const readProgress = useAtomCallback( useCallback((get) => { const currCount = get(progressAtom); diff --git a/front/yarn.lock b/front/yarn.lock index 597ae40a..5d6fa31c 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -4089,6 +4089,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/netinfo@npm:^11.2.1": + version: 11.2.1 + resolution: "@react-native-community/netinfo@npm:11.2.1" + peerDependencies: + react-native: ">=0.59" + checksum: d54b75c6dc6d7a8cbb054d84ed9326e1715259137e333f26cd801c6b820996cacceb5a1cf3ee94b91866361c040b7a12eb78db3695b076e8f30c3403985f98d5 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:^0.72.0": version: 0.72.0 resolution: "@react-native/assets-registry@npm:0.72.0" @@ -4574,6 +4583,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.14.1": + version: 5.14.1 + resolution: "@tanstack/query-core@npm:5.14.1" + checksum: 412cea2a9fa675fe64e405412bc2233295229e0b39d677146fd9b671765d033a8edcefb4d2fb5365a47c3cc5b2f15a75062501d37973cb4c9deb64fd501ff9e1 + languageName: node + linkType: hard + "@tanstack/query-devtools@npm:5.12.1": version: 5.12.1 resolution: "@tanstack/query-devtools@npm:5.12.1" @@ -4581,6 +4597,25 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-persist-client-core@npm:5.14.1": + version: 5.14.1 + resolution: "@tanstack/query-persist-client-core@npm:5.14.1" + dependencies: + "@tanstack/query-core": 5.14.1 + checksum: 437f3e8e41c71e88a5770e10912a5a2c9e18dfed063240fb2a6efaa9d2cdfd04bdf1d392f9e793e030cdd8440f0ea81b3a09722ad8adcb683a1642d32ef2e6b4 + languageName: node + linkType: hard + +"@tanstack/query-sync-storage-persister@npm:^5.14.1": + version: 5.14.1 + resolution: "@tanstack/query-sync-storage-persister@npm:5.14.1" + dependencies: + "@tanstack/query-core": 5.14.1 + "@tanstack/query-persist-client-core": 5.14.1 + checksum: 60006d134dd7da7d2627509daff2c5cd20060f01083287f999292231af354962390d24e8b578c2dcca8ddb2f8e383f46008b5579db34745df3554aff70d410cc + languageName: node + linkType: hard + "@tanstack/react-query-devtools@npm:^5.12.2": version: 5.12.2 resolution: "@tanstack/react-query-devtools@npm:5.12.2" @@ -4593,6 +4628,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-query-persist-client@npm:^5.14.1": + version: 5.14.1 + resolution: "@tanstack/react-query-persist-client@npm:5.14.1" + dependencies: + "@tanstack/query-persist-client-core": 5.14.1 + peerDependencies: + "@tanstack/react-query": ^5.14.1 + react: ^18.0.0 + checksum: 8df54287bfcccbf6d636e3559cf416a5e90518fd3a287458ef85f6205c961370afc1e61cedc7a6035a2eb632c08ac93a02e25744b955b36ac37ab93fc536b6dd + languageName: node + linkType: hard + "@tanstack/react-query@npm:^5.12.1": version: 5.12.1 resolution: "@tanstack/react-query@npm:5.12.1" @@ -11310,8 +11357,11 @@ __metadata: "@kesha-antonov/react-native-background-downloader": ^2.10.0 "@kyoo/ui": "workspace:^" "@material-symbols/svg-400": ^0.14.1 + "@react-native-community/netinfo": ^11.2.1 "@shopify/flash-list": 1.4.3 + "@tanstack/query-sync-storage-persister": ^5.14.1 "@tanstack/react-query": ^5.12.1 + "@tanstack/react-query-persist-client": ^5.14.1 "@types/react": 18.2.39 array-shuffle: ^3.0.0 babel-plugin-transform-inline-environment-variables: ^0.4.4 diff --git a/transcoder/src/identify.rs b/transcoder/src/identify.rs index 4d8434e6..f0d71bc5 100644 --- a/transcoder/src/identify.rs +++ b/transcoder/src/identify.rs @@ -208,7 +208,7 @@ pub async fn identify(path: String) -> Option { extension: Path::new(&path) .extension() .map(|x| x.to_os_string().into_string().unwrap()) - .unwrap_or(String::from(".mkv")), + .unwrap_or(String::from("mkv")), container: general["Format"].as_str().unwrap().to_string(), video: { let v = output["media"]["track"]