diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx
index d80b07e3..8fb8dcef 100644
--- a/app/(auth)/(tabs)/(home)/settings.tv.tsx
+++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx
@@ -1,4 +1,7 @@
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 { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -21,7 +24,13 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
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 {
AudioTranscodeMode,
InactivityTimeout,
@@ -30,11 +39,13 @@ import {
TVTypographyScale,
useSettings,
} from "@/utils/atoms/settings";
+import { storage } from "@/utils/mmkv";
import {
getPreviousServers,
type SavedServer,
type SavedServerAccount,
} from "@/utils/secureCredentials";
+import { clearTopShelfCacheSafely } from "@/utils/topshelf/cache";
export default function SettingsTV() {
const { t } = useTranslation();
@@ -43,9 +54,11 @@ export default function SettingsTV() {
const { logout, loginWithSavedCredential, loginWithPassword } = useJellyfin();
const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom);
+ const [, setCacheVersion] = useAtom(cacheVersionAtom);
const { showOptions } = useTVOptionModal();
const { showUserSwitchModal } = useTVUserSwitchModal();
const typography = useScaledTVTypography();
+ const queryClient = useQueryClient();
// Local state for OpenSubtitles API key (only commit on blur)
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 =
settings.audioTranscodeMode || AudioTranscodeMode.Auto;
const currentSubtitleMode =
@@ -790,6 +883,15 @@ export default function SettingsTV() {
onToggle={(value) => updateSettings({ tvThemeMusicEnabled: value })}
/>
+ {/* Storage Section */}
+
+
+
{/* User Section */}
{
const { t } = useTranslation();
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
+ const cacheVersion = useAtomValue(cacheVersionAtom);
const insets = useSafeAreaInsets();
const { settings } = useSettings();
const scrollRef = useRef(null);
@@ -669,7 +674,7 @@ export const Home = () => {
);
return (
-
+
{/* Dynamic backdrop with crossfade - only shown when hero is disabled */}
{!showHero && settings.showHomeBackdrop && (
= ({
style={{
flexDirection: "row",
gap: ITEM_GAP,
- paddingHorizontal: SCALE_PADDING,
- paddingVertical: SCALE_PADDING,
+ paddingLeft: sizes.padding.horizontal,
+ paddingRight: sizes.padding.horizontal,
+ paddingVertical: sizes.gaps.small,
}}
>
{[1, 2, 3, 4, 5].map((i) => (
@@ -287,12 +288,13 @@ export const InfiniteScrollingCollectionList: React.FC = ({
backgroundColor: "#262626",
width: itemWidth,
aspectRatio: orientation === "horizontal" ? 16 / 9 : 10 / 15,
- borderRadius: scaleSize(12),
- marginBottom: scaleSize(8),
+ borderRadius: scaleSize(24),
}}
/>
= ({
style={{
flexDirection: "row",
gap: ITEM_GAP,
- paddingHorizontal: SCALE_PADDING,
+ paddingLeft: sizes.padding.horizontal,
+ paddingRight: sizes.padding.horizontal,
paddingVertical: SCALE_PADDING,
}}
>
@@ -169,10 +170,31 @@ const WatchlistSection: React.FC = ({
backgroundColor: "#262626",
width: posterSizes.poster,
aspectRatio: 10 / 15,
- borderRadius: scaleSize(12),
- marginBottom: scaleSize(8),
+ borderRadius: scaleSize(24),
}}
/>
+
+
+ Placeholder text here
+
+
))}
@@ -217,6 +239,7 @@ export const StreamystatsPromotedWatchlists: React.FC<
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const { settings } = useSettings();
+ const typography = useScaledTVTypography();
const streamyStatsEnabled = useMemo(() => {
return Boolean(settings?.streamyStatsServerUrl);
@@ -291,15 +314,16 @@ export const StreamystatsPromotedWatchlists: React.FC<
width: scaleSize(128),
backgroundColor: "#262626",
borderRadius: scaleSize(4),
- marginLeft: SCALE_PADDING,
- marginBottom: scaleSize(16),
+ marginLeft: sizes.padding.horizontal,
+ marginBottom: scaleSize(20),
}}
/>
@@ -310,10 +334,31 @@ export const StreamystatsPromotedWatchlists: React.FC<
backgroundColor: "#262626",
width: posterSizes.poster,
aspectRatio: 10 / 15,
- borderRadius: scaleSize(12),
- marginBottom: scaleSize(8),
+ borderRadius: scaleSize(24),
}}
/>
+
+
+ Placeholder text here
+
+
))}
diff --git a/components/home/StreamystatsRecommendations.tv.tsx b/components/home/StreamystatsRecommendations.tv.tsx
index 151d37a2..ad9e388f 100644
--- a/components/home/StreamystatsRecommendations.tv.tsx
+++ b/components/home/StreamystatsRecommendations.tv.tsx
@@ -210,7 +210,8 @@ export const StreamystatsRecommendations: React.FC = ({
style={{
flexDirection: "row",
gap: sizes.gaps.item,
- paddingHorizontal: sizes.padding.scale,
+ paddingLeft: sizes.padding.horizontal,
+ paddingRight: sizes.padding.horizontal,
paddingVertical: sizes.padding.scale,
}}
>
@@ -221,10 +222,31 @@ export const StreamystatsRecommendations: React.FC = ({
backgroundColor: "#262626",
width: sizes.posters.poster,
aspectRatio: 10 / 15,
- borderRadius: scaleSize(12),
- marginBottom: scaleSize(8),
+ borderRadius: scaleSize(24),
}}
/>
+
+
+ Placeholder text here
+
+
))}
diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx
index bd4d0f50..da58ef76 100644
--- a/providers/JellyfinProvider.tsx
+++ b/providers/JellyfinProvider.tsx
@@ -19,7 +19,7 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
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 useRouter from "@/hooks/useAppRouter";
import { useInterval } from "@/hooks/useInterval";
@@ -45,9 +45,44 @@ interface Server {
address: string;
}
-export const apiAtom = atom(null);
-export const userAtom = atom(null);
+const initialApi = (() => {
+ 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(initialApi);
+export const userAtom = atom(initialUser);
export const wsAtom = atom(null);
+export const cacheVersionAtom = atom(0);
interface LoginOptions {
saveAccount?: boolean;
@@ -88,29 +123,32 @@ const JellyfinContext = createContext(
export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
- const [jellyfin, setJellyfin] = useState(undefined);
- const [deviceId, setDeviceId] = useState(undefined);
+ const [jellyfin] = useState(() => {
+ 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(() => {
+ try {
+ return getOrSetDeviceId();
+ } catch {
+ return undefined;
+ }
+ });
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 [user, setUser] = useAtom(userAtom);
const [isPolling, setIsPolling] = useState(false);
diff --git a/providers/NetworkStatusProvider.tsx b/providers/NetworkStatusProvider.tsx
index 25b4fd62..b71aaeab 100644
--- a/providers/NetworkStatusProvider.tsx
+++ b/providers/NetworkStatusProvider.tsx
@@ -35,8 +35,8 @@ async function checkApiReachable(basePath?: string): Promise {
}
export function NetworkStatusProvider({ children }: { children: ReactNode }) {
- const [isConnected, setIsConnected] = useState(false);
- const [serverConnected, setServerConnected] = useState(null);
+ const [isConnected, setIsConnected] = useState(true);
+ const [serverConnected, setServerConnected] = useState(true);
const [loading, setLoading] = useState(false);
const [api] = useAtom(apiAtom);
const queryClient = useQueryClient();
diff --git a/translations/en.json b/translations/en.json
index 00f1ab11..a11f2a75 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -463,7 +463,13 @@
"music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
"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": {
"title": "Intro",