diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index 828bbe5f..5fecdd1c 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Directory, Paths } from "expo-file-system"; import { Image } from "expo-image"; import { useAtom } from "jotai"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -70,6 +70,13 @@ export default function SettingsTV() { ); const [jellyseerrPassword, setJellyseerrPassword] = useState(""); + // Settings load storage + plugin overrides after mount, and a plugin can lock + // the URL at runtime. Keep the local field in sync with the effective value + // so it never shows stale/empty text or overwrites a locked URL on blur. + useEffect(() => { + setJellyseerrServerUrl(settings.jellyseerrServerUrl || ""); + }, [settings.jellyseerrServerUrl]); + const isJellyseerrLocked = pluginSettings?.jellyseerrServerUrl?.locked === true; const isJellyseerrConnected = !!jellyseerrApi; diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 9269f8b1..63b300b1 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -122,13 +122,19 @@ export default function SearchPage() { const isFocused = useIsFocused(); const { settings } = useSettings(); - const { jellyseerrApi } = useJellyseerr(); + const { jellyseerrApi, clearAllJellyseerData } = useJellyseerr(); // Prompt the user to connect when a Jellyseerr server is configured but no - // session exists yet (only once per focus, and only while the tab is focused). + // session exists yet (once per focus, and only while the tab is focused). const jellyseerrAlertedRef = useRef(false); useEffect(() => { - if (!isFocused || !settings?.jellyseerrServerUrl || jellyseerrApi) return; + // Reset when the tab loses focus so the prompt can show again next time + // (e.g. after the user disconnects and returns). + if (!isFocused) { + jellyseerrAlertedRef.current = false; + return; + } + if (!settings?.jellyseerrServerUrl || jellyseerrApi) return; if (jellyseerrAlertedRef.current) return; jellyseerrAlertedRef.current = true; Alert.alert( @@ -137,22 +143,36 @@ export default function SearchPage() { ); }, [isFocused, settings?.jellyseerrServerUrl, jellyseerrApi, t]); - // Validate the Jellyseerr session when switching to Discover; warn if expired. + // Validate the Jellyseerr session when switching to Discover; if the session + // has expired, clear the stale "connected" state and prompt to reconnect. useEffect(() => { if ( + !isFocused || searchType !== "Discover" || !jellyseerrApi || !settings?.jellyseerrServerUrl ) return; + let cancelled = false; validateJellyseerrSession(settings.jellyseerrServerUrl).then((status) => { - if (status.valid) return; + if (cancelled || status.valid || status.reason !== "expired") return; + clearAllJellyseerData(); Alert.alert( t("jellyseerr.session_expired"), t("jellyseerr.session_expired_connect_again"), ); }); - }, [searchType, jellyseerrApi, settings?.jellyseerrServerUrl, t]); + return () => { + cancelled = true; + }; + }, [ + isFocused, + searchType, + jellyseerrApi, + settings?.jellyseerrServerUrl, + clearAllJellyseerData, + t, + ]); const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState( diff --git a/components/search/TVJellyseerrSearchResults.tsx b/components/search/TVJellyseerrSearchResults.tsx index f811479b..c7c10872 100644 --- a/components/search/TVJellyseerrSearchResults.tsx +++ b/components/search/TVJellyseerrSearchResults.tsx @@ -14,8 +14,7 @@ import type { PersonResult, TvResult, } from "@/utils/jellyseerr/server/models/Search"; - -const SCALE_PADDING = 20; +import { scaleSize } from "@/utils/scaleSize"; interface TVJellyseerrPosterProps { item: MovieResult | TvResult; @@ -40,6 +39,8 @@ const TVJellyseerrPoster: React.FC = ({ const title = getTitle(item); const year = getYear(item); + const posterWidth = scaleSize(210); + const isInLibrary = item.mediaInfo?.status === MediaStatus.AVAILABLE || item.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE; @@ -55,7 +56,7 @@ const TVJellyseerrPoster: React.FC = ({ style={[ animatedStyle, { - width: 210, + width: posterWidth, shadowColor: "#fff", shadowOffset: { width: 0, height: 0 }, shadowOpacity: focused ? 0.6 : 0, @@ -65,7 +66,7 @@ const TVJellyseerrPoster: React.FC = ({ > = ({ ? jellyseerrApi?.imageProxy(item.profilePath, "w185") : null; + const containerWidth = scaleSize(160); + const avatarSize = scaleSize(140); + return ( = ({ > = ({ showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: sizes.padding.horizontal, - paddingVertical: SCALE_PADDING, + paddingVertical: sizes.padding.scale, gap: 20, }} style={{ overflow: "visible" }} @@ -310,7 +314,7 @@ const TVJellyseerrTvSection: React.FC = ({ showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: sizes.padding.horizontal, - paddingVertical: SCALE_PADDING, + paddingVertical: sizes.padding.scale, gap: 20, }} style={{ overflow: "visible" }} @@ -363,7 +367,7 @@ const TVJellyseerrPersonSection: React.FC = ({ showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: sizes.padding.horizontal, - paddingVertical: SCALE_PADDING, + paddingVertical: sizes.padding.scale, gap: 20, }} style={{ overflow: "visible" }} diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 0c53c1cb..b8f38dea 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -76,8 +76,13 @@ export type JellyseerrSessionStatus = /** * Checks whether the persisted Jellyseerr session (user + cookies) is still - * valid by hitting the server status endpoint. Clears local session data if the - * request fails (expired/revoked cookie). + * valid by hitting an authenticated endpoint (`/auth/me`). + * + * Returns `expired` only when the server actually rejects the session + * (HTTP 401/403). Transient failures (network down, server unreachable, 5xx) + * are treated as `valid` so a flaky connection doesn't log the user out. This + * helper does not mutate persisted state — the caller decides whether to clear + * the session (e.g. via `clearAllJellyseerData`). */ export async function validateJellyseerrSession( serverUrl: string, @@ -91,11 +96,16 @@ export async function validateJellyseerrSession( try { const api = new JellyseerrApi(serverUrl); - await api.axios.get(Endpoints.API_V1 + Endpoints.STATUS); + await api.axios.get(Endpoints.API_V1 + Endpoints.AUTH_ME); + return { valid: true }; + } catch (error) { + const status = (error as AxiosError)?.response?.status; + // Only an auth rejection means the session is gone. Anything else + // (offline, DNS failure, server error) should not be reported as expired. + if (status === 401 || status === 403) { + return { valid: false, reason: "expired" }; + } return { valid: true }; - } catch { - clearJellyseerrStorageData(); - return { valid: false, reason: "expired" }; } } @@ -123,6 +133,7 @@ export enum Endpoints { DISCOVER_TV_NETWORK = DISCOVER + TV + NETWORK, DISCOVER_MOVIES_STUDIO = `${DISCOVER}${MOVIE}s${STUDIO}`, AUTH_JELLYFIN = "/auth/jellyfin", + AUTH_ME = "/auth/me", } export type DiscoverEndpoint = @@ -480,7 +491,7 @@ export const useJellyseerr = () => { setJellyseerrUser(undefined); updateSettings({ jellyseerrServerUrl: undefined }); queryClient.removeQueries({ queryKey: ["search", "jellyseerr"] }); - }, [queryClient]); + }, [queryClient, setJellyseerrUser, updateSettings]); const requestMedia = useCallback( (title: string, request: MediaRequestBody, onSuccess?: () => void) => { diff --git a/translations/sv.json b/translations/sv.json index 72a03372..b36c4dc0 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -505,11 +505,7 @@ "episodes": "Episodes", "movies": "Movies", "loading": "Loading…", - "seeAll": "See all", - "connect": "Anslut", - "connecting": "Ansluter…", - "connected": "Ansluten", - "not_connected": "Inte ansluten" + "seeAll": "See all" }, "search": { "search": "Sök...", @@ -736,10 +732,6 @@ "request_button": "Önska", "are_you_sure_you_want_to_request_all_seasons": "Är du säker på att du vill begära alla säsonger?", "failed_to_login": "Inloggningen Misslyckades", - "connect_to_jellyseerr": "Anslut till Jellyseerr", - "connect_in_settings": "Jellyseerr är tillgängligt. Anslut i Inställningar för att aktivera förfrågningsfunktioner.", - "session_expired": "Sessionen har gått ut", - "session_expired_connect_again": "Din Jellyseerr-session har gått ut. Anslut igen i Inställningar.", "cast": "Roller", "details": "Detaljer", "status": "Status",