forked from Ninjalama/streamyfin_mirror
wip
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
39
components/Chromecast.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
18
utils/log.ts
18
utils/log.ts
@@ -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;
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user