This commit is contained in:
Fredrik Burmester
2024-08-18 13:30:12 +02:00
parent d8201aa1fc
commit ca7fd382f2
9 changed files with 70 additions and 153 deletions

View File

@@ -5,7 +5,6 @@ import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionLi
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { MediaListSection } from "@/components/medialists/MediaListSection"; import { MediaListSection } from "@/components/medialists/MediaListSection";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { import {
@@ -30,7 +29,6 @@ export default function index() {
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [settings, _] = useSettings();
const [isConnected, setIsConnected] = useState<boolean | null>(null); const [isConnected, setIsConnected] = useState<boolean | null>(null);
@@ -171,11 +169,7 @@ export default function index() {
}); });
const { data: mediaListCollections } = useQuery({ const { data: mediaListCollections } = useQuery({
queryKey: [ queryKey: ["mediaListCollections-home", user?.Id],
"mediaListCollections-home",
user?.Id,
settings?.mediaListCollectionIds,
],
queryFn: async () => { queryFn: async () => {
if (!api || !user?.Id) return []; if (!api || !user?.Id) return [];
@@ -187,16 +181,9 @@ export default function index() {
includeItemTypes: ["BoxSet"], includeItemTypes: ["BoxSet"],
}); });
const ids = return [];
response.data.Items?.filter(
(c) =>
c.Name !== "cf_carousel" &&
settings?.mediaListCollectionIds?.includes(c.Id!)
) ?? [];
return ids;
}, },
enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true, enabled: !!api && !!user?.Id && false,
staleTime: 0, staleTime: 0,
}); });
@@ -263,10 +250,6 @@ export default function index() {
orientation="horizontal" orientation="horizontal"
/> />
{mediaListCollections?.map((ml) => (
<MediaListSection key={ml.Id} collection={ml} />
))}
<ScrollingCollectionList <ScrollingCollectionList
title="Recently Added in Movies" title="Recently Added in Movies"
data={recentlyAddedInMovies} data={recentlyAddedInMovies}

View File

@@ -6,7 +6,6 @@ import { clearLogs, readFromLog } from "@/utils/log";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import { SettingToggles } from "@/components/settings/SettingToggles";
export default function settings() { export default function settings() {
const { logout } = useJellyfin(); const { logout } = useJellyfin();
@@ -30,8 +29,6 @@ export default function settings() {
<ListItem title="Server" subTitle={api?.basePath} /> <ListItem title="Server" subTitle={api?.basePath} />
</View> </View>
<SettingToggles />
<View className="flex flex-col space-y-2"> <View className="flex flex-col space-y-2">
<Button color="black" onPress={logout}> <Button color="black" onPress={logout}>
Log out Log out

View File

@@ -45,8 +45,6 @@ export default function RootLayout() {
} }
function Layout() { function Layout() {
const [settings, updateSettings] = useSettings();
useKeepAwake(); useKeepAwake();
const queryClientRef = useRef<QueryClient>( const queryClientRef = useRef<QueryClient>(

39
components/Chromecast.tsx Normal file
View File

@@ -0,0 +1,39 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import React, { useEffect } from "react";
import { View } from "react-native";
import {
CastButton,
useCastDevice,
useDevices,
useRemoteMediaClient,
} from "react-native-google-cast";
import GoogleCast from "react-native-google-cast";
type Props = {
width?: number;
height?: number;
};
export const Chromecast: React.FC<Props> = ({ width = 48, height = 48 }) => {
const client = useRemoteMediaClient();
const castDevice = useCastDevice();
const devices = useDevices();
const sessionManager = GoogleCast.getSessionManager();
const discoveryManager = GoogleCast.getDiscoveryManager();
useEffect(() => {
(async () => {
if (!discoveryManager) {
return;
}
await discoveryManager.startDiscovery();
})();
}, [client, devices, castDevice, sessionManager, discoveryManager]);
return (
<View className="rounded h-10 aspect-square flex items-center justify-center">
<CastButton style={{ tintColor: "white", height, width }} />
</View>
);
};

View File

@@ -20,7 +20,6 @@
"@gorhom/bottom-sheet": "^4", "@gorhom/bottom-sheet": "^4",
"@jellyfin/sdk": "^0.10.0", "@jellyfin/sdk": "^0.10.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.0", "@kesha-antonov/react-native-background-downloader": "^3.2.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/netinfo": "11.3.1", "@react-native-community/netinfo": "11.3.1",
"@react-native-menu/menu": "^1.1.2", "@react-native-menu/menu": "^1.1.2",
"@react-native-tvos/config-tv": "^0.0.10", "@react-native-tvos/config-tv": "^0.0.10",

View File

@@ -1,8 +1,6 @@
import { Api, Jellyfin } from "@jellyfin/sdk"; import { Api, Jellyfin } from "@jellyfin/sdk";
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
import AsyncStorage from "@react-native-async-storage/async-storage"; import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { isLoaded } from "expo-font";
import { router, useSegments } from "expo-router"; import { router, useSegments } from "expo-router";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import React, { import React, {
@@ -31,15 +29,14 @@ interface JellyfinContextValue {
} }
const JellyfinContext = createContext<JellyfinContextValue | undefined>( const JellyfinContext = createContext<JellyfinContextValue | undefined>(
undefined, undefined
); );
const getOrSetDeviceId = async () => { const getOrSetDeviceId = async () => {
let deviceId = await AsyncStorage.getItem("deviceId"); let deviceId = null;
if (!deviceId) { if (!deviceId) {
deviceId = uuid.v4() as string; deviceId = uuid.v4() as string;
await AsyncStorage.setItem("deviceId", deviceId);
} }
return deviceId; return deviceId;
@@ -58,7 +55,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
new Jellyfin({ new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.6.1" }, clientInfo: { name: "Streamyfin", version: "0.6.1" },
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id }, deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
}), })
); );
})(); })();
}, []); }, []);
@@ -67,8 +64,9 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const [user, setUser] = useAtom(userAtom); const [user, setUser] = useAtom(userAtom);
const discoverServers = async (url: string): Promise<Server[]> => { const discoverServers = async (url: string): Promise<Server[]> => {
const servers = const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
await jellyfin?.discovery.getRecommendedServerCandidates(url); url
);
return servers?.map((server) => ({ address: server.address })) || []; return servers?.map((server) => ({ address: server.address })) || [];
}; };
@@ -79,7 +77,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (!apiInstance?.basePath) throw new Error("Failed to connect"); if (!apiInstance?.basePath) throw new Error("Failed to connect");
setApi(apiInstance); setApi(apiInstance);
await AsyncStorage.setItem("serverUrl", server.address);
}, },
onError: (error) => { onError: (error) => {
console.error("Failed to set server:", error); console.error("Failed to set server:", error);
@@ -88,7 +85,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const removeServerMutation = useMutation({ const removeServerMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await AsyncStorage.removeItem("serverUrl");
setApi(null); setApi(null);
}, },
onError: (error) => { onError: (error) => {
@@ -110,9 +106,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (auth.data.AccessToken && auth.data.User) { if (auth.data.AccessToken && auth.data.User) {
setUser(auth.data.User); setUser(auth.data.User);
await AsyncStorage.setItem("user", JSON.stringify(auth.data.User));
setApi(jellyfin.createApi(api?.basePath, auth.data?.AccessToken)); setApi(jellyfin.createApi(api?.basePath, auth.data?.AccessToken));
await AsyncStorage.setItem("token", auth.data?.AccessToken);
} else { } else {
throw new Error("Invalid username or password"); throw new Error("Invalid username or password");
} }
@@ -124,7 +118,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const logoutMutation = useMutation({ const logoutMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await AsyncStorage.removeItem("token");
setUser(null); setUser(null);
}, },
onError: (error) => { onError: (error) => {
@@ -132,36 +125,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
}, },
}); });
const { isLoading, isFetching } = useQuery({
queryKey: [
"initializeJellyfin",
user?.Id,
api?.basePath,
jellyfin?.clientInfo,
],
queryFn: async () => {
try {
const token = await AsyncStorage.getItem("token");
const serverUrl = await AsyncStorage.getItem("serverUrl");
const user = JSON.parse(
(await AsyncStorage.getItem("user")) as string,
) as UserDto;
if (serverUrl && token && user.Id && jellyfin) {
const apiInstance = jellyfin.createApi(serverUrl, token);
setApi(apiInstance);
setUser(user);
}
return true;
} catch (e) {
console.error(e);
}
},
staleTime: 0,
enabled: !user?.Id || !api || !jellyfin,
});
const contextValue: JellyfinContextValue = { const contextValue: JellyfinContextValue = {
discoverServers, discoverServers,
setServer: (server) => setServerMutation.mutateAsync(server), setServer: (server) => setServerMutation.mutateAsync(server),
@@ -171,7 +134,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
logout: () => logoutMutation.mutateAsync(), logout: () => logoutMutation.mutateAsync(),
}; };
useProtectedRoute(user, isLoading || isFetching); useProtectedRoute(user);
return ( return (
<JellyfinContext.Provider value={contextValue}> <JellyfinContext.Provider value={contextValue}>

View File

@@ -1,6 +1,4 @@
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect } from "react";
type Settings = { type Settings = {
autoRotate?: boolean; autoRotate?: boolean;
@@ -12,55 +10,27 @@ type Settings = {
mediaListCollectionIds?: string[]; mediaListCollectionIds?: string[];
}; };
/** // Default settings
* const defaultSettings: Settings = {
* The settings atom is a Jotai atom that stores the user's settings. autoRotate: true,
* It is initialized with a default value of null, which indicates that the settings have not been loaded yet. forceLandscapeInVideoPlayer: false,
* The settings are loaded from AsyncStorage when the atom is read for the first time. openFullScreenVideoPlayerByDefault: true,
* usePopularPlugin: false,
*/ deviceProfile: "Expo",
forceDirectPlay: false,
// Utility function to load settings from AsyncStorage mediaListCollectionIds: [],
const loadSettings = async (): Promise<Settings> => {
const jsonValue = await AsyncStorage.getItem("settings");
return jsonValue != null
? JSON.parse(jsonValue)
: {
autoRotate: true,
forceLandscapeInVideoPlayer: false,
openFullScreenVideoPlayerByDefault: false,
usePopularPlugin: false,
deviceProfile: "Expo",
forceDirectPlay: false,
mediaListCollectionIds: [],
};
}; };
// Utility function to save settings to AsyncStorage // Create an atom to store the settings in memory, initialized with default settings
const saveSettings = async (settings: Settings) => { const settingsAtom = atom<Settings>(defaultSettings);
const jsonValue = JSON.stringify(settings);
await AsyncStorage.setItem("settings", jsonValue);
};
// Create an atom to store the settings in memory // A hook to manage settings, providing a way to update them
const settingsAtom = atom<Settings | null>(null);
// A hook to manage settings, loading them on initial mount and providing a way to update them
export const useSettings = () => { export const useSettings = () => {
const [settings, setSettings] = useAtom(settingsAtom); const [settings, setSettings] = useAtom(settingsAtom);
useEffect(() => { const updateSettings = (update: Partial<Settings>) => {
if (settings === null) { const newSettings = { ...settings, ...update };
loadSettings().then(setSettings); setSettings(newSettings);
}
}, [settings, setSettings]);
const updateSettings = async (update: Partial<Settings>) => {
if (settings) {
const newSettings = { ...settings, ...update };
setSettings(newSettings);
await saveSettings(newSettings);
}
}; };
return [settings, updateSettings] as const; return [settings, updateSettings] as const;

View File

@@ -1,4 +1,4 @@
import AsyncStorage from "@react-native-async-storage/async-storage"; import { atom } from "jotai";
import { atomWithStorage, createJSONStorage } from "jotai/utils"; import { atomWithStorage, createJSONStorage } from "jotai/utils";
type LogLevel = "INFO" | "WARN" | "ERROR"; type LogLevel = "INFO" | "WARN" | "ERROR";
@@ -10,8 +10,7 @@ interface LogEntry {
data?: any; data?: any;
} }
const asyncStorage = createJSONStorage(() => AsyncStorage); const logsAtom = atom([]);
const logsAtom = atomWithStorage("logs", [], asyncStorage);
export const writeToLog = async ( export const writeToLog = async (
level: LogLevel, level: LogLevel,
@@ -25,23 +24,16 @@ export const writeToLog = async (
data: data, data: data,
}; };
const currentLogs = await AsyncStorage.getItem("logs"); const logs: LogEntry[] = [];
const logs: LogEntry[] = currentLogs ? JSON.parse(currentLogs) : [];
logs.push(newEntry); logs.push(newEntry);
const maxLogs = 100; const maxLogs = 100;
const recentLogs = logs.slice(Math.max(logs.length - maxLogs, 0));
await AsyncStorage.setItem("logs", JSON.stringify(recentLogs));
}; };
export const readFromLog = async (): Promise<LogEntry[]> => { export const readFromLog = async (): Promise<LogEntry[]> => {
const logs = await AsyncStorage.getItem("logs"); return [];
return logs ? JSON.parse(logs) : [];
}; };
export const clearLogs = async () => { export const clearLogs = async () => {};
await AsyncStorage.removeItem("logs");
};
export default logsAtom; export default logsAtom;

View File

@@ -1603,13 +1603,6 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0" "@radix-ui/react-compose-refs" "1.0.0"
"@react-native-async-storage/async-storage@1.23.1":
version "1.23.1"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz#cad3cd4fab7dacfe9838dce6ecb352f79150c883"
integrity sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==
dependencies:
merge-options "^3.0.4"
"@react-native-community/cli-clean@13.6.9": "@react-native-community/cli-clean@13.6.9":
version "13.6.9" version "13.6.9"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-13.6.9.tgz#b6754f39c2b877c9d730feb848945150e1d52209" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-13.6.9.tgz#b6754f39c2b877c9d730feb848945150e1d52209"
@@ -5131,11 +5124,6 @@ is-path-inside@^3.0.2:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@^2.0.4: is-plain-object@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -6155,13 +6143,6 @@ memory-cache@~0.2.0:
resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a" resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a"
integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA== integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==
merge-options@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
dependencies:
is-plain-obj "^2.1.0"
merge-stream@^2.0.0: merge-stream@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -7315,11 +7296,6 @@ react-native-get-random-values@^1.11.0:
dependencies: dependencies:
fast-base64-decode "^1.0.0" fast-base64-decode "^1.0.0"
react-native-google-cast@^4.8.2:
version "4.8.2"
resolved "https://registry.yarnpkg.com/react-native-google-cast/-/react-native-google-cast-4.8.2.tgz#584fea0f8038e817d075857a537bdbfff435d764"
integrity sha512-dmVjfjneit0IguqrjmmunrcjvqNcQQ+EZL4zwCxrrEI3dcfAwBZ1eIDxHaQtvMWP6BsHb2WIFJopPDULJXsvBw==
react-native-helmet-async@2.0.4: react-native-helmet-async@2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz#93f53a1ff22d6898039688a541653a2d6b6866bb" resolved "https://registry.yarnpkg.com/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz#93f53a1ff22d6898039688a541653a2d6b6866bb"