mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-12 15:32:54 +01:00
feat(tv): fix home page loading skeletons and initialize auth/network status synchronously
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Directory, Paths } from "expo-file-system";
|
||||||
|
import { Image } from "expo-image";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -21,7 +24,13 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
|
|||||||
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
||||||
import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
|
import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
|
||||||
import { APP_LANGUAGES } from "@/i18n";
|
import { APP_LANGUAGES } from "@/i18n";
|
||||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
import { clearCache as clearAudioCache } from "@/providers/AudioStorage";
|
||||||
|
import {
|
||||||
|
apiAtom,
|
||||||
|
cacheVersionAtom,
|
||||||
|
useJellyfin,
|
||||||
|
userAtom,
|
||||||
|
} from "@/providers/JellyfinProvider";
|
||||||
import {
|
import {
|
||||||
AudioTranscodeMode,
|
AudioTranscodeMode,
|
||||||
InactivityTimeout,
|
InactivityTimeout,
|
||||||
@@ -30,11 +39,13 @@ import {
|
|||||||
TVTypographyScale,
|
TVTypographyScale,
|
||||||
useSettings,
|
useSettings,
|
||||||
} from "@/utils/atoms/settings";
|
} from "@/utils/atoms/settings";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
import {
|
import {
|
||||||
getPreviousServers,
|
getPreviousServers,
|
||||||
type SavedServer,
|
type SavedServer,
|
||||||
type SavedServerAccount,
|
type SavedServerAccount,
|
||||||
} from "@/utils/secureCredentials";
|
} from "@/utils/secureCredentials";
|
||||||
|
import { clearTopShelfCacheSafely } from "@/utils/topshelf/cache";
|
||||||
|
|
||||||
export default function SettingsTV() {
|
export default function SettingsTV() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -43,9 +54,11 @@ export default function SettingsTV() {
|
|||||||
const { logout, loginWithSavedCredential, loginWithPassword } = useJellyfin();
|
const { logout, loginWithSavedCredential, loginWithPassword } = useJellyfin();
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
const [, setCacheVersion] = useAtom(cacheVersionAtom);
|
||||||
const { showOptions } = useTVOptionModal();
|
const { showOptions } = useTVOptionModal();
|
||||||
const { showUserSwitchModal } = useTVUserSwitchModal();
|
const { showUserSwitchModal } = useTVUserSwitchModal();
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Local state for OpenSubtitles API key (only commit on blur)
|
// Local state for OpenSubtitles API key (only commit on blur)
|
||||||
const [openSubtitlesApiKey, setOpenSubtitlesApiKey] = useState(
|
const [openSubtitlesApiKey, setOpenSubtitlesApiKey] = useState(
|
||||||
@@ -163,6 +176,86 @@ export default function SettingsTV() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle clearing all cache in the entire app
|
||||||
|
const handleClearCache = async () => {
|
||||||
|
Alert.alert(
|
||||||
|
t("home.settings.storage.clear_all_cache_confirm", "Clear All Cache?"),
|
||||||
|
t(
|
||||||
|
"home.settings.storage.clear_all_cache_confirm_desc",
|
||||||
|
"Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: t("common.cancel", "Cancel"),
|
||||||
|
style: "cancel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("common.ok", "OK"),
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
// 1. Clear React Query Cache (memory & MMKV)
|
||||||
|
storage.remove("REACT_QUERY_OFFLINE_CACHE");
|
||||||
|
await queryClient.resetQueries();
|
||||||
|
|
||||||
|
// 2. Clear expo-image cache (memory & disk)
|
||||||
|
await Image.clearDiskCache();
|
||||||
|
Image.clearMemoryCache();
|
||||||
|
|
||||||
|
// 3. Clear AudioStorage (music) cache
|
||||||
|
await clearAudioCache();
|
||||||
|
|
||||||
|
// 4. Clear TopShelf cache
|
||||||
|
clearTopShelfCacheSafely();
|
||||||
|
|
||||||
|
// 5. Clear Subtitle Cache
|
||||||
|
storage.remove("downloadedSubtitles.json");
|
||||||
|
const subtitlesDir = new Directory(
|
||||||
|
Paths.cache,
|
||||||
|
"streamyfin-subtitles",
|
||||||
|
);
|
||||||
|
if (subtitlesDir.exists) {
|
||||||
|
await subtitlesDir.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Clear MMKV caches like extracted image colors and other non-essential storage keys
|
||||||
|
const keysToKeep = [
|
||||||
|
"settings",
|
||||||
|
"serverUrl",
|
||||||
|
"token",
|
||||||
|
"user",
|
||||||
|
"deviceId",
|
||||||
|
"previousServers",
|
||||||
|
"hasAskedForNotificationPermission",
|
||||||
|
"hasShownIntro",
|
||||||
|
"multiAccountMigrated",
|
||||||
|
"selectedTVServer",
|
||||||
|
"downloads.v2.json",
|
||||||
|
];
|
||||||
|
const allKeys = storage.getAllKeys();
|
||||||
|
for (const key of allKeys) {
|
||||||
|
if (!keysToKeep.includes(key)) {
|
||||||
|
storage.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Increment cache version to force remount of components
|
||||||
|
setCacheVersion((v) => v + 1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to clear cache:", error);
|
||||||
|
Alert.alert(
|
||||||
|
t("home.settings.toasts.error_deleting_files", "Error"),
|
||||||
|
t(
|
||||||
|
"home.settings.storage.clear_all_cache_error_desc",
|
||||||
|
"An error occurred while clearing the cache.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const currentAudioTranscode =
|
const currentAudioTranscode =
|
||||||
settings.audioTranscodeMode || AudioTranscodeMode.Auto;
|
settings.audioTranscodeMode || AudioTranscodeMode.Auto;
|
||||||
const currentSubtitleMode =
|
const currentSubtitleMode =
|
||||||
@@ -790,6 +883,15 @@ export default function SettingsTV() {
|
|||||||
onToggle={(value) => updateSettings({ tvThemeMusicEnabled: value })}
|
onToggle={(value) => updateSettings({ tvThemeMusicEnabled: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Storage Section */}
|
||||||
|
<TVSectionHeader title={t("home.settings.storage.storage_title")} />
|
||||||
|
<TVSettingsOptionButton
|
||||||
|
label={t("home.settings.storage.clear_all_cache")}
|
||||||
|
value=''
|
||||||
|
onPress={handleClearCache}
|
||||||
|
isFirst
|
||||||
|
/>
|
||||||
|
|
||||||
{/* User Section */}
|
{/* User Section */}
|
||||||
<TVSectionHeader
|
<TVSectionHeader
|
||||||
title={t("home.settings.user_info.user_info_title")}
|
title={t("home.settings.user_info.user_info_title")}
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ import useRouter from "@/hooks/useAppRouter";
|
|||||||
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import {
|
||||||
|
apiAtom,
|
||||||
|
cacheVersionAtom,
|
||||||
|
userAtom,
|
||||||
|
} from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
import { scaleSize } from "@/utils/scaleSize";
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
@@ -69,6 +73,7 @@ export const Home = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
|
const cacheVersion = useAtomValue(cacheVersionAtom);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const scrollRef = useRef<ScrollView>(null);
|
const scrollRef = useRef<ScrollView>(null);
|
||||||
@@ -669,7 +674,7 @@ export const Home = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: "#000000" }}>
|
<View key={cacheVersion} style={{ flex: 1, backgroundColor: "#000000" }}>
|
||||||
{/* Dynamic backdrop with crossfade - only shown when hero is disabled */}
|
{/* Dynamic backdrop with crossfade - only shown when hero is disabled */}
|
||||||
{!showHero && settings.showHomeBackdrop && (
|
{!showHero && settings.showHomeBackdrop && (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
|||||||
import { scaleSize } from "@/utils/scaleSize";
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
||||||
const SCALE_PADDING = scaleSize(20);
|
const _SCALE_PADDING = scaleSize(20);
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@@ -276,8 +276,9 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: ITEM_GAP,
|
gap: ITEM_GAP,
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingLeft: sizes.padding.horizontal,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingRight: sizes.padding.horizontal,
|
||||||
|
paddingVertical: sizes.gaps.small,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
@@ -287,12 +288,13 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: itemWidth,
|
width: itemWidth,
|
||||||
aspectRatio: orientation === "horizontal" ? 16 / 9 : 10 / 15,
|
aspectRatio: orientation === "horizontal" ? 16 / 9 : 10 / 15,
|
||||||
borderRadius: scaleSize(12),
|
borderRadius: scaleSize(24),
|
||||||
marginBottom: scaleSize(8),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
marginTop: scaleSize(12),
|
||||||
|
paddingHorizontal: scaleSize(4),
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: ITEM_GAP,
|
gap: ITEM_GAP,
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingLeft: sizes.padding.horizontal,
|
||||||
|
paddingRight: sizes.padding.horizontal,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: SCALE_PADDING,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -169,10 +170,31 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: posterSizes.poster,
|
width: posterSizes.poster,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: scaleSize(12),
|
borderRadius: scaleSize(24),
|
||||||
marginBottom: scaleSize(8),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: scaleSize(12),
|
||||||
|
paddingHorizontal: scaleSize(4),
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: "hidden",
|
||||||
|
marginBottom: 4,
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#262626",
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: typography.callout,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
Placeholder text here
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -217,6 +239,7 @@ export const StreamystatsPromotedWatchlists: React.FC<
|
|||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
const typography = useScaledTVTypography();
|
||||||
|
|
||||||
const streamyStatsEnabled = useMemo(() => {
|
const streamyStatsEnabled = useMemo(() => {
|
||||||
return Boolean(settings?.streamyStatsServerUrl);
|
return Boolean(settings?.streamyStatsServerUrl);
|
||||||
@@ -291,15 +314,16 @@ export const StreamystatsPromotedWatchlists: React.FC<
|
|||||||
width: scaleSize(128),
|
width: scaleSize(128),
|
||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
borderRadius: scaleSize(4),
|
borderRadius: scaleSize(4),
|
||||||
marginLeft: SCALE_PADDING,
|
marginLeft: sizes.padding.horizontal,
|
||||||
marginBottom: scaleSize(16),
|
marginBottom: scaleSize(20),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: ITEM_GAP,
|
gap: ITEM_GAP,
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingLeft: sizes.padding.horizontal,
|
||||||
|
paddingRight: sizes.padding.horizontal,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: SCALE_PADDING,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -310,10 +334,31 @@ export const StreamystatsPromotedWatchlists: React.FC<
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: posterSizes.poster,
|
width: posterSizes.poster,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: scaleSize(12),
|
borderRadius: scaleSize(24),
|
||||||
marginBottom: scaleSize(8),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: scaleSize(12),
|
||||||
|
paddingHorizontal: scaleSize(4),
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: "hidden",
|
||||||
|
marginBottom: 4,
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#262626",
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: typography.callout,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
Placeholder text here
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -210,7 +210,8 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: sizes.gaps.item,
|
gap: sizes.gaps.item,
|
||||||
paddingHorizontal: sizes.padding.scale,
|
paddingLeft: sizes.padding.horizontal,
|
||||||
|
paddingRight: sizes.padding.horizontal,
|
||||||
paddingVertical: sizes.padding.scale,
|
paddingVertical: sizes.padding.scale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -221,10 +222,31 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: sizes.posters.poster,
|
width: sizes.posters.poster,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: scaleSize(12),
|
borderRadius: scaleSize(24),
|
||||||
marginBottom: scaleSize(8),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: scaleSize(12),
|
||||||
|
paddingHorizontal: scaleSize(4),
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: "hidden",
|
||||||
|
marginBottom: 4,
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#262626",
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: typography.callout,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
Placeholder text here
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AppState, Platform } from "react-native";
|
import { AppState, Platform } from "react-native";
|
||||||
import { getDeviceName } from "react-native-device-info";
|
import { getDeviceNameSync } from "react-native-device-info";
|
||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useInterval } from "@/hooks/useInterval";
|
import { useInterval } from "@/hooks/useInterval";
|
||||||
@@ -45,9 +45,44 @@ interface Server {
|
|||||||
address: string;
|
address: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiAtom = atom<Api | null>(null);
|
const initialApi = (() => {
|
||||||
export const userAtom = atom<UserDto | null>(null);
|
try {
|
||||||
|
const token = storage.getString("token") || null;
|
||||||
|
const serverUrl = storage.getString("serverUrl") || null;
|
||||||
|
if (serverUrl && token) {
|
||||||
|
const id = getOrSetDeviceId();
|
||||||
|
const deviceName = getDeviceNameSync();
|
||||||
|
const jellyfinInstance = new Jellyfin({
|
||||||
|
clientInfo: { name: "Streamyfin", version: "0.52.0" },
|
||||||
|
deviceInfo: {
|
||||||
|
name: deviceName,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return jellyfinInstance.createApi(serverUrl, token);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to initialize API synchronously:", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const initialUser = (() => {
|
||||||
|
try {
|
||||||
|
const userStr = storage.getString("user");
|
||||||
|
if (userStr) {
|
||||||
|
return JSON.parse(userStr) as UserDto;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse initial user synchronously:", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const apiAtom = atom<Api | null>(initialApi);
|
||||||
|
export const userAtom = atom<UserDto | null>(initialUser);
|
||||||
export const wsAtom = atom<WebSocket | null>(null);
|
export const wsAtom = atom<WebSocket | null>(null);
|
||||||
|
export const cacheVersionAtom = atom<number>(0);
|
||||||
|
|
||||||
interface LoginOptions {
|
interface LoginOptions {
|
||||||
saveAccount?: boolean;
|
saveAccount?: boolean;
|
||||||
@@ -88,29 +123,32 @@ const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
|||||||
export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [jellyfin, setJellyfin] = useState<Jellyfin | undefined>(undefined);
|
const [jellyfin] = useState<Jellyfin | undefined>(() => {
|
||||||
const [deviceId, setDeviceId] = useState<string | undefined>(undefined);
|
try {
|
||||||
|
const id = getOrSetDeviceId();
|
||||||
|
const deviceName = getDeviceNameSync();
|
||||||
|
return new Jellyfin({
|
||||||
|
clientInfo: { name: "Streamyfin", version: "0.52.0" },
|
||||||
|
deviceInfo: {
|
||||||
|
name: deviceName,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to initialize Jellyfin synchronously in state:", e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [deviceId] = useState<string | undefined>(() => {
|
||||||
|
try {
|
||||||
|
return getOrSetDeviceId();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const id = getOrSetDeviceId();
|
|
||||||
const deviceName = await getDeviceName();
|
|
||||||
setJellyfin(
|
|
||||||
() =>
|
|
||||||
new Jellyfin({
|
|
||||||
clientInfo: { name: "Streamyfin", version: "0.52.0" },
|
|
||||||
deviceInfo: {
|
|
||||||
name: deviceName,
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setDeviceId(id);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [api, setApi] = useAtom(apiAtom);
|
const [api, setApi] = useAtom(apiAtom);
|
||||||
const [user, setUser] = useAtom(userAtom);
|
const [user, setUser] = useAtom(userAtom);
|
||||||
const [isPolling, setIsPolling] = useState<boolean>(false);
|
const [isPolling, setIsPolling] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ async function checkApiReachable(basePath?: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkStatusProvider({ children }: { children: ReactNode }) {
|
export function NetworkStatusProvider({ children }: { children: ReactNode }) {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(true);
|
||||||
const [serverConnected, setServerConnected] = useState<boolean | null>(null);
|
const [serverConnected, setServerConnected] = useState<boolean | null>(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -463,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
|
|||||||
Reference in New Issue
Block a user