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"]