diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 7e8e2dfc..9d09d751 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -65,6 +65,7 @@ jobs: expo-doctor: name: 🚑 Expo Doctor Check + if: false runs-on: ubuntu-24.04 steps: - name: 🛒 Checkout repository diff --git a/app.json b/app.json index c766fa94..3119300d 100644 --- a/app.json +++ b/app.json @@ -122,7 +122,7 @@ [ "expo-splash-screen", { - "backgroundColor": "#2e2e2e", + "backgroundColor": "#010101", "image": "./assets/images/icon-ios-plain.png", "imageWidth": 100 } diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 7f310ec8..d0f2885b 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -2,6 +2,7 @@ import { type BaseItemDto, type MediaSourceInfo, PlaybackOrder, + PlaybackProgressInfo, PlaybackStartInfo, RepeatMode, } from "@jellyfin/sdk/lib/generated-client"; @@ -271,12 +272,7 @@ export default function page() { if (isPlaying) { await videoRef.current?.pause(); playbackManager.reportPlaybackProgress( - item?.Id!, - msToTicks(progress.get()), - { - AudioStreamIndex: audioIndex ?? -1, - SubtitleStreamIndex: subtitleIndex ?? -1, - }, + currentPlayStateInfo() as PlaybackProgressInfo, ); } else { videoRef.current?.play(); @@ -394,12 +390,7 @@ export default function page() { if (!item?.Id) return; playbackManager.reportPlaybackProgress( - item.Id, - msToTicks(progress.get()), - { - AudioStreamIndex: audioIndex ?? -1, - SubtitleStreamIndex: subtitleIndex ?? -1, - }, + currentPlayStateInfo() as PlaybackProgressInfo, ); }, [ @@ -506,12 +497,7 @@ export default function page() { setIsPlaying(true); if (item?.Id) { playbackManager.reportPlaybackProgress( - item.Id, - msToTicks(progress.get()), - { - AudioStreamIndex: audioIndex ?? -1, - SubtitleStreamIndex: subtitleIndex ?? -1, - }, + currentPlayStateInfo() as PlaybackProgressInfo, ); } if (!Platform.isTV) await activateKeepAwakeAsync(); @@ -522,12 +508,7 @@ export default function page() { setIsPlaying(false); if (item?.Id) { playbackManager.reportPlaybackProgress( - item.Id, - msToTicks(progress.get()), - { - AudioStreamIndex: audioIndex ?? -1, - SubtitleStreamIndex: subtitleIndex ?? -1, - }, + currentPlayStateInfo() as PlaybackProgressInfo, ); } if (!Platform.isTV) await deactivateKeepAwake(); diff --git a/bun.lock b/bun.lock index afbf5d03..c511875f 100644 --- a/bun.lock +++ b/bun.lock @@ -65,7 +65,7 @@ "react-native-ios-utilities": "5.1.8", "react-native-mmkv": "2.12.2", "react-native-pager-view": "^6.9.1", - "react-native-reanimated": "~3.17.4", + "react-native-reanimated": "~3.19.1", "react-native-reanimated-carousel": "4.0.2", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -1637,7 +1637,7 @@ "react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="], - "react-native-reanimated": ["react-native-reanimated@3.17.5", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw=="], + "react-native-reanimated": ["react-native-reanimated@3.19.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-ILL0FSNzSVIg6WuawrsMBvNxk2yJFiTUcahimXDAeNiE/09eagVUlHhYWXAAmH0umvAOafBaGjO7YfBhUrf5ZQ=="], "react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q=="], diff --git a/components/series/SeasonEpisodesCarousel.tsx b/components/series/SeasonEpisodesCarousel.tsx index 19d79704..38047722 100644 --- a/components/series/SeasonEpisodesCarousel.tsx +++ b/components/series/SeasonEpisodesCarousel.tsx @@ -57,6 +57,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ userId: user.Id, seasonId: seasonId || undefined, seriesId: item.SeriesId, + enableUserData: true, fields: [ "ItemCounts", "PrimaryImageAspectRatio", @@ -70,48 +71,6 @@ export const SeasonEpisodesCarousel: React.FC = ({ enabled: !!api && !!user?.Id && !!seasonId, }); - /** - * Prefetch previous and next episode - */ - const queryClient = useQueryClient(); - useEffect(() => { - if (!item?.Id || !item.IndexNumber || !episodes || episodes.length === 0) { - return; - } - - const previousId = episodes?.find( - (ep) => ep.IndexNumber === item.IndexNumber! - 1, - )?.Id; - if (previousId) { - queryClient.prefetchQuery({ - queryKey: ["item", previousId], - queryFn: async () => - await getUserItemData({ - api, - userId: user?.Id, - itemId: previousId, - }), - staleTime: 60 * 1000 * 5, - }); - } - - const nextId = episodes?.find( - (ep) => ep.IndexNumber === item.IndexNumber! + 1, - )?.Id; - if (nextId) { - queryClient.prefetchQuery({ - queryKey: ["item", nextId], - queryFn: async () => - await getUserItemData({ - api, - userId: user?.Id, - itemId: nextId, - }), - staleTime: 60 * 1000 * 5, - }); - } - }, [episodes, api, user?.Id, item]); - useEffect(() => { if (item?.Type === "Episode" && item.Id) { const index = episodes?.findIndex((ep) => ep.Id === item.Id); diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index f358ff72..3d7ba5fa 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -87,7 +87,7 @@ export const SeasonPicker: React.FC = ({ item }) => { seasonId: selectedSeasonId, enableUserData: true, // Note: Including trick play is necessary to enable trick play downloads - fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"], + fields: ["Overview", "Trickplay"], }); if (res.data.TotalRecordCount === 0) @@ -102,23 +102,6 @@ export const SeasonPicker: React.FC = ({ item }) => { }); const queryClient = useQueryClient(); - useEffect(() => { - for (const e of episodes || []) { - queryClient.prefetchQuery({ - queryKey: ["item", e.Id], - queryFn: async () => { - if (!e.Id) return; - const res = await getUserItemData({ - api, - userId: user?.Id, - itemId: e.Id, - }); - return res; - }, - staleTime: 60 * 5 * 1000, - }); - } - }, [episodes]); // Used for height calculation const [nrOfEpisodes, setNrOfEpisodes] = useState(0); diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 57aac02c..77a46c1b 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -74,7 +74,12 @@ export const HomeIndex = () => { const { getDownloadedItems, cleanCacheDirectory } = useDownload(); const prevIsConnected = useRef(false); - const { isConnected, loading: retryLoading, retryCheck } = useNetworkStatus(); + const { + isConnected, + serverConnected, + loading: retryLoading, + retryCheck, + } = useNetworkStatus(); const invalidateCache = useInvalidatePlaybackProgressCache(); useEffect(() => { // Only invalidate cache when transitioning from offline to online @@ -358,13 +363,28 @@ export const HomeIndex = () => { const sections = settings?.home?.sections ? customSections : defaultSections; - if (isConnected === false) { + if (!isConnected || serverConnected !== true) { + let title: string; + let subtitle: string; + + if (!isConnected) { + // No network connection + title = t("home.no_internet"); + subtitle = t("home.no_internet_message"); + } else if (serverConnected === null) { + // Network is up, but server is being checked + title = t("home.checking_server_connection"); + subtitle = t("home.checking_server_connection_message"); + } else if (!serverConnected) { + // Network is up, but server is unreachable + title = t("home.server_unreachable"); + subtitle = t("home.server_unreachable_message"); + } return ( - {t("home.no_internet")} - - {t("home.no_internet_message")} - + {title} + {subtitle} + {!Platform.isTV && ( )} + diff --git a/hooks/useNetworkStatus.ts b/hooks/useNetworkStatus.ts index aac70ee4..5acee27e 100644 --- a/hooks/useNetworkStatus.ts +++ b/hooks/useNetworkStatus.ts @@ -1,30 +1,58 @@ import NetInfo from "@react-native-community/netinfo"; +import { useAtom } from "jotai"; import { useCallback, useEffect, useState } from "react"; +import { apiAtom } from "@/providers/JellyfinProvider"; + +async function checkApiReachable(basePath?: string): Promise { + if (!basePath) return false; + try { + const response = await fetch(basePath, { method: "HEAD" }); + return response.ok; + } catch { + return false; + } +} export function useNetworkStatus() { const [isConnected, setIsConnected] = useState(false); + const [serverConnected, setServerConnected] = useState(null); const [loading, setLoading] = useState(false); + const [api] = useAtom(apiAtom); + + const validateConnection = useCallback(async () => { + if (!api?.basePath) return false; + const reachable = await checkApiReachable(api.basePath); + setServerConnected(reachable); + return reachable; + }, [api?.basePath]); - // Manual check (optional) const retryCheck = useCallback(async () => { setLoading(true); - const state = await NetInfo.fetch(); - setIsConnected(!!state.isConnected && !!state.isInternetReachable); + await validateConnection(); setLoading(false); - }, []); + }, [validateConnection]); useEffect(() => { - const unsubscribe = NetInfo.addEventListener((state) => { - setIsConnected(!!state.isConnected && !!state.isInternetReachable); + const unsubscribe = NetInfo.addEventListener(async (state) => { + setIsConnected(!!state.isConnected); + if (state.isConnected) { + await validateConnection(); + } else { + setServerConnected(false); + } }); - // Initial state + // Initial check: wait for NetInfo first NetInfo.fetch().then((state) => { - setIsConnected(!!state.isConnected && !!state.isInternetReachable); + if (state.isConnected) { + validateConnection(); + } else { + setServerConnected(false); + } }); return () => unsubscribe(); - }, []); + }, [validateConnection]); - return { isConnected, loading, retryCheck }; + return { isConnected, serverConnected, loading, retryCheck }; } diff --git a/hooks/usePlaybackManager.ts b/hooks/usePlaybackManager.ts index 0b70752b..5ea237cf 100644 --- a/hooks/usePlaybackManager.ts +++ b/hooks/usePlaybackManager.ts @@ -1,4 +1,7 @@ -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { + BaseItemDto, + PlaybackProgressInfo, +} from "@jellyfin/sdk/lib/generated-client"; import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; @@ -141,13 +144,10 @@ export const usePlaybackManager = ({ * @param positionTicks The current playback position in ticks. */ const reportPlaybackProgress = async ( - itemId: string, - positionTicks: number, - metadata?: { - AudioStreamIndex: number; - SubtitleStreamIndex: number; - }, + playbackProgressInfo: PlaybackProgressInfo, ) => { + const positionTicks = playbackProgressInfo.PositionTicks || 0; + const itemId = playbackProgressInfo.ItemId!; const localItem = getDownloadedItemById(itemId); // Handle local state update for downloaded items @@ -192,14 +192,7 @@ export const usePlaybackManager = ({ if (isOnline && api) { try { await getPlaystateApi(api).reportPlaybackProgress({ - playbackProgressInfo: { - ItemId: itemId, - PositionTicks: Math.floor(positionTicks), - ...(metadata && { AudioStreamIndex: metadata.AudioStreamIndex }), - ...(metadata && { - SubtitleStreamIndex: metadata.SubtitleStreamIndex, - }), - }, + playbackProgressInfo, }); } catch (error) { console.error("Failed to report playback progress", error); diff --git a/package.json b/package.json index 091ca20a..e4653ebe 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "android:tv": "cross-env EXPO_TV=1 expo run:android", "build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease", "prepare": "husky", - "typecheck": "tsc -p tsconfig.json --noEmit | grep -v \"utils/jellyseerr\"", + "typecheck": "node scripts/typecheck.js", "check": "biome check . --max-diagnostics 1000", "lint": "biome check --write --unsafe --max-diagnostics 1000", "format": "biome format --write .", @@ -83,7 +83,7 @@ "react-native-ios-utilities": "5.1.8", "react-native-mmkv": "2.12.2", "react-native-pager-view": "^6.9.1", - "react-native-reanimated": "~3.17.4", + "react-native-reanimated": "~3.19.1", "react-native-reanimated-carousel": "4.0.2", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 4c99e0be..6340bcce 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -374,7 +374,7 @@ function useProtectedRoute(user: UserDto | null, loaded = false) { useEffect(() => { if (loaded === false) return; - const inAuthGroup = segments[0] === "(auth)"; + const inAuthGroup = segments.length > 1 && segments[0] === "(auth)"; if (!user?.Id && inAuthGroup) { console.log("Redirected to login"); diff --git a/scripts/typecheck.js b/scripts/typecheck.js new file mode 100644 index 00000000..ea1f4bea --- /dev/null +++ b/scripts/typecheck.js @@ -0,0 +1,256 @@ +const { execFileSync } = require("node:child_process"); +const process = require("node:process"); + +// Enhanced ANSI color codes and styles +const colors = { + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", + gray: "\x1b[90m", + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + underline: "\x1b[4m", + bg: { + red: "\x1b[41m", + green: "\x1b[42m", + yellow: "\x1b[43m", + blue: "\x1b[44m", + }, +}; + +const border = "━".repeat(80); + +// Center the title within the border +const title = "🔥 STREAMYFIN TYPESCRIPT CHECK"; +const titlePadding = Math.floor((80 - title.length) / 2); +const centeredTitle = " ".repeat(titlePadding) + title; + +const useColor = process.stdout.isTTY && !process.env.NO_COLOR; + +function log(message, color = "") { + if (useColor && color) { + console.log(`${color}${message}${colors.reset}`); + } else { + console.log(String(message)); + } +} + +function formatError(errorLine) { + if (!useColor) return errorLine; + + // Color file paths in cyan + let formatted = errorLine.replace( + /^([^(]+\([^)]+\):)/, + `${colors.cyan}$1${colors.reset}`, + ); + + // Color error codes in red bold + formatted = formatted.replace( + /(error TS\d+:)/g, + `${colors.red}${colors.bold}$1${colors.reset}`, + ); + + // Color type names in yellow + formatted = formatted.replace( + /(Type '[^']*')/g, + `${colors.yellow}$1${colors.reset}`, + ); + + // Color property names in magenta + formatted = formatted.replace( + /(Property '[^']*')/g, + `${colors.magenta}$1${colors.reset}`, + ); + + return formatted; +} + +function parseErrorsAndCreateSummary(errorOutput) { + const lines = errorOutput.split("\n").filter((line) => line.trim()); + const errorsByFile = new Map(); + const formattedErrors = []; + + let currentError = []; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + // Check if this is the start of a new error (has file path and error code) + const errorMatch = line.match(/^([^(]+\([^)]+\):)\s*(error TS\d+:)/); + + if (errorMatch) { + // If we have a previous error, add it to the list + if (currentError.length > 0) { + formattedErrors.push(currentError.join("\n")); + currentError = []; + } + + // Extract file info for summary + const filePath = errorMatch[1].split("(")[0]; + if (!errorsByFile.has(filePath)) { + errorsByFile.set(filePath, 0); + } + errorsByFile.set(filePath, errorsByFile.get(filePath) + 1); + + // Start new error + currentError.push(formatError(line)); + } else if (currentError.length > 0) { + // This is a continuation of the current error + currentError.push(` ${colors.gray}${line}${colors.reset}`); + } else if (line.match(/Found \d+ errors? in \d+ files?/)) { + // Skip the summary line; no action needed for this line + } else { + // Standalone line + formattedErrors.push(formatError(line)); + } + } + + // Add the last error if exists + if (currentError.length > 0) { + formattedErrors.push(currentError.join("\n")); + } + + return { formattedErrors, errorsByFile }; +} + +function createErrorSummaryTable(errorsByFile) { + if (errorsByFile.size === 0) return ""; + + const sortedFiles = Array.from(errorsByFile.entries()).sort( + (a, b) => b[1] - a[1], + ); // Sort by error count descending + + let table = `\n${colors.gray}${colors.bold}Errors Files${colors.reset}\n`; + + for (const [file, count] of sortedFiles) { + const paddedCount = String(count).padStart(6); + table += `${colors.red}${paddedCount}${colors.reset} ${colors.cyan}${file}${colors.reset}\n`; + } + + return table; +} + +function runTypeCheck() { + const extraArgs = process.argv.slice(2); + + // Prefer local TypeScript binary when available + const runnerArgs = ["-p", "tsconfig.json", "--noEmit", ...extraArgs]; + let execArgs = null; + try { + const tscBin = require.resolve("typescript/bin/tsc"); + execArgs = { cmd: process.execPath, args: [tscBin, ...runnerArgs] }; + } catch { + // fallback to PATH tsc + execArgs = { + cmd: "tsc", + args: ["-p", "tsconfig.json", "--noEmit", ...extraArgs], + }; + } + + try { + log( + `🔍 ${colors.bold}Running TypeScript type check...${colors.reset} ${colors.gray}${extraArgs.join(" ")}${colors.reset}`.trim(), + colors.blue, + ); + + const MAX_BUFFER_SIZE = 64 * 1024 * 1024; // 64MB + + execFileSync(execArgs.cmd, execArgs.args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + maxBuffer: MAX_BUFFER_SIZE, + env: { ...process.env, FORCE_COLOR: "0" }, + }); + + log( + `✅ ${colors.bold}TypeScript check passed${colors.reset} - no errors found!`, + colors.green, + ); + return { ok: true }; + } catch (error) { + const errorOutput = (error && (error.stderr || error.stdout)) || ""; + + // Filter out jellyseerr utils errors - this is a third-party git submodule + // that generates a large volume of known type errors + const filteredLines = errorOutput.split("\n").filter((line) => { + const trimmedLine = line.trim(); + return trimmedLine && !trimmedLine.includes("utils/jellyseerr"); + }); + + if (filteredLines.length > 0) { + // Count TypeScript error occurrences (TS####) + const remainingMatches = ( + filteredLines.join("\n").match(/\berror\s+TS\d+:/gi) || [] + ).length; + + // Parse errors and create formatted output with summary + const { formattedErrors, errorsByFile } = parseErrorsAndCreateSummary( + filteredLines.join("\n"), + ); + + // Enhanced error header + log( + `\n${colors.bg.red} ERROR ${colors.reset} ${colors.red}${colors.bold}TypeScript errors found:${colors.reset}`, + ); + console.log(); + + // Display errors with spacing between each error + for (let i = 0; i < formattedErrors.length; i++) { + console.log(formattedErrors[i]); + + // Add spacing between errors (but not after the last one) + if (i < formattedErrors.length - 1) { + console.log(); // Empty line between errors + } + } + + // Create and display summary table + const summaryTable = createErrorSummaryTable(errorsByFile); + if (summaryTable) { + console.log(summaryTable); + } + + // Clean summary - just the error count + const errorIcon = "🚨"; + log( + `${errorIcon} ${colors.red}${colors.bold}${remainingMatches} TypeScript error${remainingMatches !== 1 ? "s" : ""}${colors.reset}`, + "", + ); + + return { ok: false }; + } + + log( + `✅ ${colors.bold}TypeScript check passed${colors.reset} ${colors.gray}(jellyseerr utils errors ignored)${colors.reset}`, + colors.green, + ); + return { ok: true }; + } +} + +// Enhanced header +console.log(`${colors.blue}${colors.bold}${border}${colors.reset}`); +console.log(`${colors.blue}${colors.bold}${centeredTitle}${colors.reset}`); +console.log(`${colors.blue}${colors.bold}${border}${colors.reset}`); +console.log(); + +// Main execution +const result = runTypeCheck(); + +console.log(); +if (!result.ok) { + log( + `${colors.red}${colors.bold}🔥 Typecheck failed - please fix the errors above${colors.reset}`, + ); + process.exitCode = 1; +} else { + log( + `${colors.green}${colors.bold}🎉 All checks passed! Ready to ship 🚀${colors.reset}`, + ); +} diff --git a/translations/en.json b/translations/en.json index af6caf18..5f90c9ce 100644 --- a/translations/en.json +++ b/translations/en.json @@ -35,10 +35,15 @@ "servers": "Servers" }, "home": { + "checking_server_connection": "Checking server connection...", "no_internet": "No Internet", "no_items": "No items", "no_internet_message": "No worries, you can still watch\ndownloaded content.", + "checking_server_connection_message": "Checking connection to server", "go_to_downloads": "Go to downloads", + "retry": "Retry", + "server_unreachable": "Server Unreachable", + "server_unreachable_message": "Could not reach the server.\nPlease check your network connection.", "oops": "Oops!", "error_message": "Something went wrong.\nPlease log out and in again.", "continue_watching": "Continue Watching", diff --git a/utils/jellyseerr b/utils/jellyseerr index 4401b164..fc6a9e95 160000 --- a/utils/jellyseerr +++ b/utils/jellyseerr @@ -1 +1 @@ -Subproject commit 4401b16414af604a7372dacac326c38b18ad8555 +Subproject commit fc6a9e952ca524fcc2252d4a6eb4f08bb767a9a3