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

View File

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

View File

@@ -45,8 +45,6 @@ export default function RootLayout() {
}
function Layout() {
const [settings, updateSettings] = useSettings();
useKeepAwake();
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",
"@jellyfin/sdk": "^0.10.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-menu/menu": "^1.1.2",
"@react-native-tvos/config-tv": "^0.0.10",

View File

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

View File

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

View File

@@ -1603,13 +1603,6 @@
"@babel/runtime" "^7.13.10"
"@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":
version "13.6.9"
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"
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:
version "2.0.4"
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"
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:
version "2.0.0"
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:
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:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz#93f53a1ff22d6898039688a541653a2d6b6866bb"