From c778956a52fbdb04fa27c739f93dc9ed0127d299 Mon Sep 17 00:00:00 2001 From: stenlan <14372001+stenlan@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:41:35 +0200 Subject: [PATCH 01/33] fix: Jellyseerr discovery crash (#1032) --- .../(home)/settings/hide-libraries/page.tsx | 2 +- .../(home)/settings/jellyseerr/page.tsx | 2 +- .../(home)/settings/marlin-search/page.tsx | 2 +- .../jellyseerr/company/[companyId].tsx | 11 +++--- .../jellyseerr/genre/[genreId].tsx | 17 ++++----- .../jellyseerr/person/[personId].tsx | 8 +--- app/(auth)/(tabs)/(libraries)/_layout.tsx | 2 +- app/(auth)/(tabs)/(libraries)/index.tsx | 2 +- app/(auth)/(tabs)/(search)/index.tsx | 2 +- app/(auth)/(tabs)/_layout.tsx | 2 +- app/(auth)/player/direct-player.tsx | 3 +- app/_layout.tsx | 2 +- components/DownloadItem.tsx | 2 +- components/ItemContent.tsx | 2 +- components/PlayButton.tsx | 2 +- components/PlayButton.tv.tsx | 2 +- components/common/JellyseerrItemRouter.tsx | 3 +- components/home/LargeMovieCarousel.tsx | 2 +- .../jellyseerr/discover/MovieTvSlide.tsx | 17 +++------ components/library/LibraryItemCard.tsx | 2 +- components/posters/JellyseerrPoster.tsx | 3 +- components/settings/AppLanguageSelector.tsx | 2 +- components/settings/AudioToggles.tsx | 2 +- components/settings/ChromecastSettings.tsx | 2 +- components/settings/Dashboard.tsx | 2 +- components/settings/DownloadSettings.tsx | 2 +- components/settings/GestureControls.tsx | 2 +- components/settings/HomeIndex.tsx | 8 +--- components/settings/Jellyseerr.tsx | 2 +- components/settings/MediaContext.tsx | 2 +- components/settings/MediaToggles.tsx | 2 +- components/settings/OtherSettings.tsx | 2 +- components/settings/PluginSettings.tsx | 2 +- components/settings/SubtitleToggles.tsx | 2 +- .../video-player/controls/BottomControls.tsx | 2 +- .../video-player/controls/CenterControls.tsx | 2 +- .../controls/ContinueWatchingOverlay.tsx | 2 +- components/video-player/controls/Controls.tsx | 2 +- .../video-player/controls/GestureOverlay.tsx | 2 +- .../video-player/controls/HeaderControls.tsx | 2 +- .../controls/hooks/useVideoNavigation.ts | 2 +- hooks/useHaptic.ts | 2 +- hooks/useJellyseerr.ts | 37 +++++++++---------- providers/DownloadProvider.tsx | 2 +- providers/JellyfinProvider.tsx | 13 +------ utils/_jellyseerr/useJellyseerrCanRequest.ts | 3 +- utils/atoms/queue.ts | 2 +- utils/atoms/settings.ts | 15 ++++---- 48 files changed, 90 insertions(+), 120 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index da4dfef5..e1c8b56b 100644 --- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx @@ -12,7 +12,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; export default function page() { - const [settings, updateSettings, pluginSettings] = useSettings(null); + const { settings, updateSettings, pluginSettings } = useSettings(); const user = useAtomValue(userAtom); const api = useAtomValue(apiAtom); diff --git a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx index 3e4e410e..86222f93 100644 --- a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx @@ -3,7 +3,7 @@ import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { useSettings } from "@/utils/atoms/settings"; export default function page() { - const [_settings, _updateSettings, pluginSettings] = useSettings(null); + const { pluginSettings } = useSettings(); return ( (settings?.marlinServerUrl || ""); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx index 4e7ba94b..cd9cc3cc 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx @@ -15,7 +15,7 @@ import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Dis export default function page() { const local = useLocalSearchParams(); - const { jellyseerrApi } = useJellyseerr(); + const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { companyId, image, type } = local as unknown as { companyId: string; @@ -53,7 +53,10 @@ export default function page() { uniqBy( data?.pages ?.filter((p) => p?.results.length) - .flatMap((p) => p?.results ?? []), + .flatMap( + (p) => + p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [], + ), "id", ) ?? [], [data], @@ -98,9 +101,7 @@ export default function page() { }} /> } - renderItem={(item, _index) => ( - - )} + renderItem={(item, _index) => } /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx index 5482c45d..7ea00808 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx @@ -8,14 +8,10 @@ import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; -import { - type MovieResult, - type TvResult, -} from "@/utils/jellyseerr/server/models/Search"; export default function page() { const local = useLocalSearchParams(); - const { jellyseerrApi } = useJellyseerr(); + const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { genreId, name, type } = local as unknown as { genreId: string; @@ -51,7 +47,10 @@ export default function page() { uniqBy( data?.pages ?.filter((p) => p?.results.length) - .flatMap((p) => p?.results ?? []), + .flatMap( + (p) => + p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [], + ), "id", ) ?? [], [data], @@ -62,7 +61,7 @@ export default function page() { jellyseerrApi ? flatData.map((r) => jellyseerrApi.imageProxy( - (r as TvResult | MovieResult).backdropPath, + r.backdropPath, "w1920_and_h800_multi_faces", ), ) @@ -92,9 +91,7 @@ export default function page() { {name} } - renderItem={(item, _index) => ( - - )} + renderItem={(item, _index) => } /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx index 9c03aff4..7bef1d7a 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx @@ -10,10 +10,6 @@ import { OverviewText } from "@/components/OverviewText"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; -import type { - MovieResult, - TvResult, -} from "@/utils/jellyseerr/server/models/Search"; export default function page() { const local = useLocalSearchParams(); @@ -106,9 +102,7 @@ export default function page() { MainContent={() => ( )} - renderItem={(item, _index) => ( - - )} + renderItem={(item, _index) => } /> ); } diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index 6d032a7a..e450e3ec 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -9,7 +9,7 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; import { useTranslation } from "react-i18next"; export default function IndexLayout() { - const [settings, updateSettings, pluginSettings] = useSettings(null); + const { settings, updateSettings, pluginSettings } = useSettings(); const { t } = useTranslation(); diff --git a/app/(auth)/(tabs)/(libraries)/index.tsx b/app/(auth)/(tabs)/(libraries)/index.tsx index 75b4ef29..c638e5d2 100644 --- a/app/(auth)/(tabs)/(libraries)/index.tsx +++ b/app/(auth)/(tabs)/(libraries)/index.tsx @@ -19,7 +19,7 @@ export default function index() { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const queryClient = useQueryClient(); - const [settings] = useSettings(null); + const { settings } = useSettings(); const { t } = useTranslation(); diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 8bab9bf1..5a95ad48 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -71,7 +71,7 @@ export default function search() { const [api] = useAtom(apiAtom); - const [settings] = useSettings(null); + const { settings } = useSettings(); const { jellyseerrApi } = useJellyseerr(); const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState( diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 622542b4..41a76eb6 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -27,7 +27,7 @@ export const NativeTabs = withLayoutContext< >(Navigator); export default function TabLayout() { - const [settings] = useSettings(null); + const { settings } = useSettings(); const { t } = useTranslation(); const router = useRouter(); diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index a8de9cca..ca906a78 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -97,7 +97,7 @@ export default function page() { /** Playback position in ticks. */ playbackPosition?: string; }>(); - const [_settings] = useSettings(null); + useSettings(); const offline = offlineStr === "true"; const playbackManager = usePlaybackManager(); @@ -748,7 +748,6 @@ export default function page() { setAspectRatio={setAspectRatio} setScaleFactor={setScaleFactor} isVlc - api={api} downloadedFiles={downloadedFiles} /> )} diff --git a/app/_layout.tsx b/app/_layout.tsx index 5b77604d..29f2c89b 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -230,7 +230,7 @@ const queryClient = new QueryClient({ }); function Layout() { - const [settings] = useSettings(null); + const { settings } = useSettings(); const [user] = useAtom(userAtom); const [api] = useAtom(apiAtom); const appState = useRef(AppState.currentState); diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index eb1710a3..60a248c7 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -61,7 +61,7 @@ export const DownloadItems: React.FC = ({ const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const [queue, _setQueue] = useAtom(queueAtom); - const [settings] = useSettings(null); + const { settings } = useSettings(); const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false); const { processes, startBackgroundDownload, getDownloadedItems } = diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index c49049f8..6c91a530 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -54,7 +54,7 @@ interface ItemContentProps { export const ItemContent: React.FC = React.memo( ({ item, isOffline }) => { const [api] = useAtom(apiAtom); - const [settings] = useSettings(null); + const { settings } = useSettings(); const { orientation } = useOrientation(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index 932cf51d..3f4ca141 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -67,7 +67,7 @@ export const PlayButton: React.FC = ({ const startColor = useSharedValue(colorAtom); const widthProgress = useSharedValue(0); const colorChangeProgress = useSharedValue(0); - const [settings, updateSettings] = useSettings(null); + const { settings, updateSettings } = useSettings(); const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index c9443cc6..b4fa45a9 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -44,7 +44,7 @@ export const PlayButton: React.FC = ({ const startColor = useSharedValue(colorAtom); const widthProgress = useSharedValue(0); const colorChangeProgress = useSharedValue(0); - const [settings] = useSettings(null); + const { settings } = useSettings(); const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index d9a039eb..89b7d2f2 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -10,6 +10,7 @@ import { Permission, } from "@/utils/jellyseerr/server/lib/permissions"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; import type { MovieResult, TvResult, @@ -17,7 +18,7 @@ import type { import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; interface Props extends TouchableOpacityProps { - result?: MovieResult | TvResult | MovieDetails | TvDetails; + result?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast; mediaTitle: string; releaseYear: number; canRequest: boolean; diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index de1d7de2..375059d0 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -26,7 +26,7 @@ import { itemRouter } from "../common/TouchableItemRouter"; interface Props extends ViewProps {} export const LargeMovieCarousel: React.FC = ({ ...props }) => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const ref = React.useRef(null); const progress = useSharedValue(0); diff --git a/components/jellyseerr/discover/MovieTvSlide.tsx b/components/jellyseerr/discover/MovieTvSlide.tsx index 38974b5f..a82f48a3 100644 --- a/components/jellyseerr/discover/MovieTvSlide.tsx +++ b/components/jellyseerr/discover/MovieTvSlide.tsx @@ -11,16 +11,12 @@ import { useJellyseerr, } from "@/hooks/useJellyseerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; -import type { - MovieResult, - TvResult, -} from "@/utils/jellyseerr/server/models/Search"; const MovieTvSlide: React.FC = ({ slide, ...props }) => { - const { jellyseerrApi } = useJellyseerr(); + const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ["jellyseerr", "discover", slide.id], @@ -69,7 +65,9 @@ const MovieTvSlide: React.FC = ({ uniqBy( data?.pages ?.filter((p) => p?.results.length) - .flatMap((p) => p?.results), + .flatMap((p) => + p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)), + ), "id", ), [data], @@ -86,12 +84,7 @@ const MovieTvSlide: React.FC = ({ onEndReached={() => { if (hasNextPage) fetchNextPage(); }} - renderItem={(item) => ( - - )} + renderItem={(item) => } /> ) ); diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index f7d3fb9b..efb29013 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -41,7 +41,7 @@ const icons: Record = { export const LibraryItemCard: React.FC = ({ library, ...props }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const [settings] = useSettings(null); + const { settings } = useSettings(); const { t } = useTranslation(); diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 49605078..d7b1fcdb 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -20,6 +20,7 @@ import { MediaStatus } from "@/utils/jellyseerr/server/constants/media"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; import type { MovieResult, TvResult, @@ -27,7 +28,7 @@ import type { import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; interface Props extends ViewProps { - item?: MovieResult | TvResult | MovieDetails | TvDetails; + item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast; horizontal?: boolean; showDownloadInfo?: boolean; mediaRequest?: MediaRequest; diff --git a/components/settings/AppLanguageSelector.tsx b/components/settings/AppLanguageSelector.tsx index 42d68929..f77bc2c8 100644 --- a/components/settings/AppLanguageSelector.tsx +++ b/components/settings/AppLanguageSelector.tsx @@ -12,7 +12,7 @@ interface Props extends ViewProps {} export const AppLanguageSelector: React.FC = () => { const isTv = Platform.isTV; - const [settings, updateSettings] = useSettings(null); + const { settings, updateSettings } = useSettings(); const { t } = useTranslation(); if (isTv) return null; diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx index 29f851c9..82ff1157 100644 --- a/components/settings/AudioToggles.tsx +++ b/components/settings/AudioToggles.tsx @@ -17,7 +17,7 @@ export const AudioToggles: React.FC = ({ ...props }) => { const isTv = Platform.isTV; const media = useMedia(); - const [_, __, pluginSettings] = useSettings(null); + const { pluginSettings } = useSettings(); const { settings, updateSettings } = media; const cultures = media.cultures; const { t } = useTranslation(); diff --git a/components/settings/ChromecastSettings.tsx b/components/settings/ChromecastSettings.tsx index 096da489..4da06332 100644 --- a/components/settings/ChromecastSettings.tsx +++ b/components/settings/ChromecastSettings.tsx @@ -4,7 +4,7 @@ import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; export const ChromecastSettings: React.FC = ({ ...props }) => { - const [settings, updateSettings] = useSettings(null); + const { settings, updateSettings } = useSettings(); return ( diff --git a/components/settings/Dashboard.tsx b/components/settings/Dashboard.tsx index 0404f535..768c6e0b 100644 --- a/components/settings/Dashboard.tsx +++ b/components/settings/Dashboard.tsx @@ -7,7 +7,7 @@ import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; export const Dashboard = () => { - const [settings, _updateSettings] = useSettings(null); + const { settings } = useSettings(); const { sessions = [] } = useSessions({} as useSessionsProps); const router = useRouter(); diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx index 3d609610..483d895b 100644 --- a/components/settings/DownloadSettings.tsx +++ b/components/settings/DownloadSettings.tsx @@ -7,7 +7,7 @@ import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; export default function DownloadSettings({ ...props }) { - const [settings, updateSettings, pluginSettings] = useSettings(null); + const { settings, updateSettings, pluginSettings } = useSettings(); const { t } = useTranslation(); const allDisabled = useMemo( diff --git a/components/settings/GestureControls.tsx b/components/settings/GestureControls.tsx index bb77af99..55aadaa1 100644 --- a/components/settings/GestureControls.tsx +++ b/components/settings/GestureControls.tsx @@ -13,7 +13,7 @@ interface Props extends ViewProps {} export const GestureControls: React.FC = ({ ...props }) => { const { t } = useTranslation(); - const [settings, updateSettings, pluginSettings] = useSettings(null); + const { settings, updateSettings, pluginSettings } = useSettings(); const disabled = useMemo( () => diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 5278c17f..75215461 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -64,13 +64,7 @@ export const HomeIndex = () => { const user = useAtomValue(userAtom); const [loading, setLoading] = useState(false); - const [ - settings, - _updateSettings, - _pluginSettings, - _setPluginSettings, - refreshStreamyfinPluginSettings, - ] = useSettings(null); + const { settings, refreshStreamyfinPluginSettings } = useSettings(); const navigation = useNavigation(); diff --git a/components/settings/Jellyseerr.tsx b/components/settings/Jellyseerr.tsx index 87a99964..470d40a2 100644 --- a/components/settings/Jellyseerr.tsx +++ b/components/settings/Jellyseerr.tsx @@ -20,7 +20,7 @@ export const JellyseerrSettings = () => { const { t } = useTranslation(); const [user] = useAtom(userAtom); - const [settings, updateSettings, _pluginSettings] = useSettings(null); + const { settings, updateSettings } = useSettings(); const [jellyseerrPassword, setJellyseerrPassword] = useState< string | undefined diff --git a/components/settings/MediaContext.tsx b/components/settings/MediaContext.tsx index 4f363694..c6c7856e 100644 --- a/components/settings/MediaContext.tsx +++ b/components/settings/MediaContext.tsx @@ -28,7 +28,7 @@ export const useMedia = () => { }; export const MediaProvider = ({ children }: { children: ReactNode }) => { - const [settings, updateSettings] = useSettings(null); + const { settings, updateSettings } = useSettings(); const api = useAtomValue(apiAtom); const queryClient = useQueryClient(); diff --git a/components/settings/MediaToggles.tsx b/components/settings/MediaToggles.tsx index 1715446a..c01a6d13 100644 --- a/components/settings/MediaToggles.tsx +++ b/components/settings/MediaToggles.tsx @@ -13,7 +13,7 @@ interface Props extends ViewProps {} export const MediaToggles: React.FC = ({ ...props }) => { const { t } = useTranslation(); - const [settings, updateSettings, pluginSettings] = useSettings(null); + const { settings, updateSettings, pluginSettings } = useSettings(); const disabled = useMemo( () => diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index cfeb10f7..e0ca1147 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -23,7 +23,7 @@ import { ListItem } from "../list/ListItem"; export const OtherSettings: React.FC = () => { const router = useRouter(); - const [settings, updateSettings, pluginSettings] = useSettings(null); + const { settings, updateSettings, pluginSettings } = useSettings(); const { t } = useTranslation(); diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx index c830f9e3..9d8c64dd 100644 --- a/components/settings/PluginSettings.tsx +++ b/components/settings/PluginSettings.tsx @@ -6,7 +6,7 @@ import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; export const PluginSettings = () => { - const [settings, _updateSettings] = useSettings(null); + const { settings } = useSettings(); const router = useRouter(); diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 60cd96e7..59ec1570 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -20,7 +20,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { const isTv = Platform.isTV; const media = useMedia(); - const [_, __, pluginSettings] = useSettings(null); + const { pluginSettings } = useSettings(); const { settings, updateSettings } = media; const cultures = media.cultures; const { t } = useTranslation(); diff --git a/components/video-player/controls/BottomControls.tsx b/components/video-player/controls/BottomControls.tsx index 3ff5987b..59da6f7b 100644 --- a/components/video-player/controls/BottomControls.tsx +++ b/components/video-player/controls/BottomControls.tsx @@ -88,7 +88,7 @@ export const BottomControls: FC = ({ trickplayInfo, time, }) => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const insets = useSafeAreaInsets(); return ( diff --git a/components/video-player/controls/CenterControls.tsx b/components/video-player/controls/CenterControls.tsx index b00d9333..47659746 100644 --- a/components/video-player/controls/CenterControls.tsx +++ b/components/video-player/controls/CenterControls.tsx @@ -30,7 +30,7 @@ export const CenterControls: FC = ({ handleSkipBackward, handleSkipForward, }) => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const insets = useSafeAreaInsets(); return ( diff --git a/components/video-player/controls/ContinueWatchingOverlay.tsx b/components/video-player/controls/ContinueWatchingOverlay.tsx index 829c4d83..4c353dee 100644 --- a/components/video-player/controls/ContinueWatchingOverlay.tsx +++ b/components/video-player/controls/ContinueWatchingOverlay.tsx @@ -16,7 +16,7 @@ export interface ContinueWatchingOverlayProps { const ContinueWatchingOverlay: React.FC = ({ goToNextItem, }) => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const router = useRouter(); return settings.autoPlayEpisodeCount >= diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 7ca768af..fee1a7de 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -116,7 +116,7 @@ export const Controls: FC = ({ api = null, downloadedFiles = undefined, }) => { - const [settings, updateSettings] = useSettings(api); + const { settings, updateSettings } = useSettings(); const router = useRouter(); const lightHapticFeedback = useHaptic("light"); diff --git a/components/video-player/controls/GestureOverlay.tsx b/components/video-player/controls/GestureOverlay.tsx index 1f6fe0ad..e4ca20e6 100644 --- a/components/video-player/controls/GestureOverlay.tsx +++ b/components/video-player/controls/GestureOverlay.tsx @@ -31,7 +31,7 @@ export const GestureOverlay = ({ onSkipForward, onSkipBackward, }: Props) => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const lightHaptic = useHaptic("light"); const [feedback, setFeedback] = useState({ diff --git a/components/video-player/controls/HeaderControls.tsx b/components/video-player/controls/HeaderControls.tsx index 534e366d..07232014 100644 --- a/components/video-player/controls/HeaderControls.tsx +++ b/components/video-player/controls/HeaderControls.tsx @@ -70,7 +70,7 @@ export const HeaderControls: FC = ({ setVideoAspectRatio, setVideoScaleFactor, }) => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); const { width: screenWidth } = useWindowDimensions(); diff --git a/components/video-player/controls/hooks/useVideoNavigation.ts b/components/video-player/controls/hooks/useVideoNavigation.ts index 524bdd7f..0573d6e4 100644 --- a/components/video-player/controls/hooks/useVideoNavigation.ts +++ b/components/video-player/controls/hooks/useVideoNavigation.ts @@ -20,7 +20,7 @@ export function useVideoNavigation({ seek, play, }: UseVideoNavigationProps) { - const [settings] = useSettings(null); + const { settings } = useSettings(); const lightHapticFeedback = useHaptic("light"); const wasPlayingRef = useRef(false); diff --git a/hooks/useHaptic.ts b/hooks/useHaptic.ts index 8afeffcf..42ae274e 100644 --- a/hooks/useHaptic.ts +++ b/hooks/useHaptic.ts @@ -14,7 +14,7 @@ export type HapticFeedbackType = | "error"; export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { - const [settings] = useSettings(null); + const { settings } = useSettings(); const isTv = Platform.isTV; const isDisabled = isTv || diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index cb1ee376..6add563e 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -14,7 +14,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { t } from "i18next"; import { useCallback, useMemo } from "react"; import { toast } from "sonner-native"; -import { defaultValues, Settings } from "@/utils/atoms/settings"; +import { useSettings } from "@/utils/atoms/settings"; import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; import { IssueStatus, @@ -40,6 +40,7 @@ import type { UserResultsResponse } from "@/utils/jellyseerr/server/interfaces/a import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { CombinedCredit, + PersonCreditCast, PersonDetails, } from "@/utils/jellyseerr/server/models/Person"; import type { @@ -416,10 +417,8 @@ export class JellyseerrApi { const jellyseerrUserAtom = atom(storage.get(JELLYSEERR_USER)); -export const useJellyseerr = ( - settings: Settings = defaultValues, - updateSettings: (update: Partial) => void = () => {}, -) => { +export const useJellyseerr = () => { + const { settings, updateSettings } = useSettings(); const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); const queryClient = useQueryClient(); @@ -468,49 +467,47 @@ export const useJellyseerr = ( [jellyseerrApi], ); - const isJellyseerrResult = ( + const isJellyseerrMovieOrTvResult = ( items: any | null | undefined, - ): items is Results => { + ): items is MovieResult | TvResult => { return ( items && Object.hasOwn(items, "mediaType") && - Object.values(MediaType).includes(items.mediaType as MediaType) + (items.mediaType === MediaType.MOVIE || items.mediaType === MediaType.TV) ); }; const getTitle = ( - item?: TvResult | TvDetails | MovieResult | MovieDetails, + item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, ) => { - return isJellyseerrResult(item) + return isJellyseerrMovieOrTvResult(item) ? item.mediaType === MediaType.MOVIE ? item?.title : item?.name - : item?.mediaInfo.mediaType === MediaType.MOVIE + : item?.mediaInfo?.mediaType === MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name; }; const getYear = ( - item?: TvResult | TvDetails | MovieResult | MovieDetails, + item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, ) => { return new Date( - (isJellyseerrResult(item) + (isJellyseerrMovieOrTvResult(item) ? item.mediaType === MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate - : item?.mediaInfo.mediaType === MediaType.MOVIE + : item?.mediaInfo?.mediaType === MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate) || "", )?.getFullYear?.(); }; const getMediaType = ( - item?: TvResult | TvDetails | MovieResult | MovieDetails, + item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, ): MediaType => { - return isJellyseerrResult(item) - ? item.mediaType === "movie" - ? MediaType.MOVIE - : MediaType.TV + return isJellyseerrMovieOrTvResult(item) + ? (item.mediaType as MediaType) : item?.mediaInfo?.mediaType; }; @@ -528,7 +525,7 @@ export const useJellyseerr = ( jellyseerrUser, setJellyseerrUser, clearAllJellyseerData, - isJellyseerrResult, + isJellyseerrMovieOrTvResult, getTitle, getYear, getMediaType, diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 7b76d934..7a8fe7d1 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -87,7 +87,7 @@ function useDownloadProvider() { const { saveSeriesPrimaryImage } = useDownloadHelper(); const { saveImage } = useImageStorage(); const [processes, setProcesses] = useAtom(processesAtom); - const [settings] = useSettings(null); + const { settings } = useSettings(); const successHapticFeedback = useHaptic("success"); /// Cant use the background downloader callback. As its not triggered if size is unknown. diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 8b4d0728..56f64abd 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -79,17 +79,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const [user, setUser] = useAtom(userAtom); const [isPolling, setIsPolling] = useState(false); const [secret, setSecret] = useState(null); - const [ - _settings, - _updateSettings, - _pluginSettings, - setPluginSettings, - refreshStreamyfinPluginSettings, - ] = useSettings(api); - const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr( - _settings || {}, - _updateSettings, - ); + const { setPluginSettings, refreshStreamyfinPluginSettings } = useSettings(); + const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(); const headers = useMemo(() => { if (!deviceId) return {}; diff --git a/utils/_jellyseerr/useJellyseerrCanRequest.ts b/utils/_jellyseerr/useJellyseerrCanRequest.ts index c8872769..2cd67a33 100644 --- a/utils/_jellyseerr/useJellyseerrCanRequest.ts +++ b/utils/_jellyseerr/useJellyseerrCanRequest.ts @@ -9,6 +9,7 @@ import { hasPermission, Permission, } from "@/utils/jellyseerr/server/lib/permissions"; +import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; import type { MovieResult, TvResult, @@ -18,7 +19,7 @@ import type { MovieDetails } from "../jellyseerr/server/models/Movie"; import type { TvDetails } from "../jellyseerr/server/models/Tv"; export const useJellyseerrCanRequest = ( - item?: MovieResult | TvResult | MovieDetails | TvDetails, + item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast, ) => { const { jellyseerrUser } = useJellyseerr(); diff --git a/utils/atoms/queue.ts b/utils/atoms/queue.ts index 516b951f..3b787e22 100644 --- a/utils/atoms/queue.ts +++ b/utils/atoms/queue.ts @@ -56,7 +56,7 @@ export const useJobProcessor = () => { const [queue, setQueue] = useAtom(queueAtom); const [running, setRunning] = useAtom(runningAtom); const [processes] = useAtom(processesAtom); - const [settings] = useSettings(null); + const { settings } = useSettings(); useEffect(() => { if ( diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 40e4b022..d24114c5 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,4 +1,3 @@ -import type { Api } from "@jellyfin/sdk"; import { type BaseItemKind, type CultureDto, @@ -7,11 +6,12 @@ import { type SortOrder, SubtitlePlaybackMode, } from "@jellyfin/sdk/lib/generated-client"; -import { atom, useAtom } from "jotai"; +import { atom, useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; import { Platform } from "react-native"; import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom } from "@/providers/JellyfinProvider"; import { writeInfoLog } from "@/utils/log"; import { storage } from "../mmkv"; @@ -278,7 +278,8 @@ export const pluginSettingsAtom = atom( loadPluginSettings(), ); -export const useSettings = (api: Api | null) => { +export const useSettings = () => { + const api = useAtomValue(apiAtom); const [_settings, setSettings] = useAtom(settingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); @@ -302,11 +303,11 @@ export const useSettings = (api: Api | null) => { return; } const settings = await api.getStreamyfinPluginConfig().then( - ({ data }: any) => { + ({ data }) => { writeInfoLog("Got plugin settings", data?.settings); return data?.settings; }, - (_err: any) => undefined, + (_err) => undefined, ); setPluginSettings(settings); return settings; @@ -367,11 +368,11 @@ export const useSettings = (api: Api | null) => { }; }, [_settings, pluginSettings]); - return [ + return { settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings, - ] as const; + }; }; From 93a63f6b4895bdba9e82f81ca230680f608c0495 Mon Sep 17 00:00:00 2001 From: stenlan <14372001+stenlan@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:42:03 +0200 Subject: [PATCH 02/33] fix: #1007 Season episodes temporarily disappearing when marking one as watched (#1031) Co-authored-by: Fredrik Burmester --- .../actors/[actorId].tsx | 2 +- components/MoreMoviesWithActor.tsx | 2 +- components/SimilarItems.tsx | 2 +- .../{HorrizontalScroll.tsx => HorizontalScroll.tsx} | 0 ...rizontalScroll.tsx => InfiniteHorizontalScroll.tsx} | 0 components/medialists/MediaListSection.tsx | 2 +- components/series/CastAndCrew.tsx | 2 +- components/series/CurrentSeries.tsx | 2 +- components/series/JellyseerrSeasons.tsx | 2 +- components/series/SeasonEpisodesCarousel.tsx | 10 +++------- components/series/SeasonPicker.tsx | 4 ++-- components/video-player/controls/EpisodeList.tsx | 2 +- 12 files changed, 13 insertions(+), 17 deletions(-) rename components/common/{HorrizontalScroll.tsx => HorizontalScroll.tsx} (100%) rename components/common/{InfiniteHorrizontalScroll.tsx => InfiniteHorizontalScroll.tsx} (100%) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx index 4b80911f..d95488fa 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx @@ -7,7 +7,7 @@ import { useAtom } from "jotai"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; -import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll"; +import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorizontalScroll"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { ItemCardText } from "@/components/ItemCardText"; diff --git a/components/MoreMoviesWithActor.tsx b/components/MoreMoviesWithActor.tsx index d95a04fb..35d80d93 100644 --- a/components/MoreMoviesWithActor.tsx +++ b/components/MoreMoviesWithActor.tsx @@ -5,7 +5,7 @@ import { useAtom } from "jotai"; import type React from "react"; import { useTranslation } from "react-i18next"; import { View, type ViewProps } from "react-native"; -import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import { HorizontalScroll } from "@/components/common/HorizontalScroll"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { ItemCardText } from "@/components/ItemCardText"; diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index 4288f1e7..b73e5518 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { View, type ViewProps } from "react-native"; import MoviePoster from "@/components/posters/MoviePoster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { HorizontalScroll } from "./common/HorrizontalScroll"; +import { HorizontalScroll } from "./common/HorizontalScroll"; import { Text } from "./common/Text"; import { TouchableItemRouter } from "./common/TouchableItemRouter"; import { ItemCardText } from "./ItemCardText"; diff --git a/components/common/HorrizontalScroll.tsx b/components/common/HorizontalScroll.tsx similarity index 100% rename from components/common/HorrizontalScroll.tsx rename to components/common/HorizontalScroll.tsx diff --git a/components/common/InfiniteHorrizontalScroll.tsx b/components/common/InfiniteHorizontalScroll.tsx similarity index 100% rename from components/common/InfiniteHorrizontalScroll.tsx rename to components/common/InfiniteHorizontalScroll.tsx diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx index da5ed8bf..7eb750fa 100644 --- a/components/medialists/MediaListSection.tsx +++ b/components/medialists/MediaListSection.tsx @@ -12,7 +12,7 @@ import { useAtom } from "jotai"; import { useCallback } from "react"; import { View, type ViewProps } from "react-native"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll"; +import { InfiniteHorizontalScroll } from "../common/InfiniteHorizontalScroll"; import { Text } from "../common/Text"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; import { ItemCardText } from "../ItemCardText"; diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index 4263cd37..18365635 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next"; import { TouchableOpacity, View, type ViewProps } from "react-native"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { HorizontalScroll } from "../common/HorrizontalScroll"; +import { HorizontalScroll } from "../common/HorizontalScroll"; import { Text } from "../common/Text"; import { itemRouter } from "../common/TouchableItemRouter"; import Poster from "../posters/Poster"; diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx index c1a4dede..c3d0e0d9 100644 --- a/components/series/CurrentSeries.tsx +++ b/components/series/CurrentSeries.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; import { TouchableOpacity, View, type ViewProps } from "react-native"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; -import { HorizontalScroll } from "../common/HorrizontalScroll"; +import { HorizontalScroll } from "../common/HorizontalScroll"; import { Text } from "../common/Text"; import Poster from "../posters/Poster"; diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index 25cece2f..e339e84b 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -11,7 +11,7 @@ import { orderBy } from "lodash"; import type React from "react"; import { useCallback, useMemo, useState } from "react"; import { Alert, TouchableOpacity, View } from "react-native"; -import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import { HorizontalScroll } from "@/components/common/HorizontalScroll"; import { Text } from "@/components/common/Text"; import { Tags } from "@/components/GenreTags"; import { dateOpts } from "@/components/jellyseerr/DetailFacts"; diff --git a/components/series/SeasonEpisodesCarousel.tsx b/components/series/SeasonEpisodesCarousel.tsx index 5a3304ad..19d79704 100644 --- a/components/series/SeasonEpisodesCarousel.tsx +++ b/components/series/SeasonEpisodesCarousel.tsx @@ -12,7 +12,7 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { HorizontalScroll, type HorizontalScrollRef, -} from "../common/HorrizontalScroll"; +} from "../common/HorizontalScroll"; import { ItemCardText } from "../ItemCardText"; interface Props extends ViewProps { @@ -42,11 +42,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ return item?.SeasonId; }, [item]); - const { - data: episodes, - isLoading, - isFetching, - } = useQuery({ + const { data: episodes, isPending } = useQuery({ queryKey: ["episodes", seasonId, isOffline], queryFn: async () => { if (isOffline) { @@ -132,7 +128,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ ref={scrollRef} data={episodes} extraData={item} - loading={loading || isLoading || isFetching} + loading={loading || isPending} renderItem={(_item, _idx) => ( = ({ item }) => { return season.Id!; }, [seasons, seasonIndex]); - const { data: episodes, isFetching } = useQuery({ + const { data: episodes, isPending } = useQuery({ queryKey: ["episodes", item.Id, selectedSeasonId], queryFn: async () => { if (!api || !user?.Id || !item.Id || !selectedSeasonId) { @@ -165,7 +165,7 @@ export const SeasonPicker: React.FC = ({ item }) => { ) : null} - {isFetching ? ( + {isPending ? ( Date: Wed, 3 Sep 2025 21:45:15 +0200 Subject: [PATCH 03/33] fix: BaseItemDto/BaseItemPerson routing bug (#1030) --- .../[actorId].tsx => persons/[personId].tsx} | 14 +++++++------- components/common/TouchableItemRouter.tsx | 14 ++++---------- components/series/CastAndCrew.tsx | 18 +++++++++++++----- components/stacks/NestedTabPageStack.tsx | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) rename app/(auth)/(tabs)/(home,libraries,search,favorites)/{actors/[actorId].tsx => persons/[personId].tsx} (93%) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/persons/[personId].tsx similarity index 93% rename from app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/persons/[personId].tsx index d95488fa..f2f8dcaf 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/persons/[personId].tsx @@ -22,21 +22,21 @@ import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; const page: React.FC = () => { const local = useLocalSearchParams(); - const { actorId } = local as { actorId: string }; + const { personId } = local as { personId: string }; const { t } = useTranslation(); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const { data: item, isLoading: l1 } = useQuery({ - queryKey: ["item", actorId], + queryKey: ["item", personId], queryFn: async () => await getUserItemData({ api, userId: user?.Id, - itemId: actorId, + itemId: personId, }), - enabled: !!actorId && !!api, + enabled: !!personId && !!api, staleTime: 60, }); @@ -50,7 +50,7 @@ const page: React.FC = () => { const response = await getItemsApi(api).getItems({ userId: user.Id, - personIds: [actorId], + personIds: [personId], startIndex: pageParam, limit: 16, sortOrder: ["Descending", "Descending", "Ascending"], @@ -68,7 +68,7 @@ const page: React.FC = () => { return response.data; }, - [api, user?.Id, actorId], + [api, user?.Id, personId], ); const backdropUrl = useMemo( @@ -131,7 +131,7 @@ const page: React.FC = () => { )} queryFn={fetchItems} - queryKey={["actor", "movies", actorId]} + queryKey={["actor", "movies", personId]} /> diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 614e5bb7..b84dfae1 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,8 +1,5 @@ import { useActionSheet } from "@expo/react-native-action-sheet"; -import type { - BaseItemDto, - BaseItemPerson, -} from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useRouter, useSegments } from "expo-router"; import { type PropsWithChildren, useCallback } from "react"; import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; @@ -14,10 +11,7 @@ interface Props extends TouchableOpacityProps { isOffline?: boolean; } -export const itemRouter = ( - item: BaseItemDto | BaseItemPerson, - from: string, -) => { +export const itemRouter = (item: BaseItemDto, from: string) => { if ("CollectionType" in item && item.CollectionType === "livetv") { return `/(auth)/(tabs)/${from}/livetv`; } @@ -26,8 +20,8 @@ export const itemRouter = ( return `/(auth)/(tabs)/${from}/series/${item.Id}`; } - if (item.Type === "Person" || item.Type === "Actor") { - return `/(auth)/(tabs)/${from}/actors/${item.Id}`; + if (item.Type === "Person") { + return `/(auth)/(tabs)/${from}/persons/${item.Id}`; } if (item.Type === "BoxSet") { diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index 18365635..15568451 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -27,16 +27,18 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => { const from = segments[2]; const destinctPeople = useMemo(() => { - const people: BaseItemPerson[] = []; + const people: Record = {}; item?.People?.forEach((person) => { - const existingPerson = people.find((p) => p.Id === person.Id); + if (!person.Id) return; + + const existingPerson = people[person.Id]; if (existingPerson) { existingPerson.Role = `${existingPerson.Role}, ${person.Role}`; } else { - people.push(person); + people[person.Id] = person; } }); - return people; + return Object.values(people); }, [item?.People]); if (!from) return null; @@ -54,7 +56,13 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => { renderItem={(i) => ( { - const url = itemRouter(i, from); + const url = itemRouter( + { + Id: i.Id, + Type: "Person", + }, + from, + ); // @ts-expect-error router.push(url); }} diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx index 8e66e0bf..6c151b0f 100644 --- a/components/stacks/NestedTabPageStack.tsx +++ b/components/stacks/NestedTabPageStack.tsx @@ -17,7 +17,7 @@ export const commonScreenOptions: ICommonScreenOptions = { headerLeft: () => , }; -const routes = ["actors/[actorId]", "items/page", "series/[id]"]; +const routes = ["persons/[personId]", "items/page", "series/[id]"]; export const nestedTabPageScreenOptions: Record = Object.fromEntries(routes.map((route) => [route, commonScreenOptions])); From b4014c922eaae11186334109417cf6a40b9e494c Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 3 Sep 2025 21:50:25 +0200 Subject: [PATCH 04/33] feat: native download notifications (#1006) --- app.json | 4 +- app/_layout.tsx | 9 +++ eas.json | 19 +++---- providers/DownloadProvider.tsx | 100 ++++++++++++++++++++++++++++++++- providers/JellyfinProvider.tsx | 4 +- 5 files changed, 120 insertions(+), 16 deletions(-) diff --git a/app.json b/app.json index c9382a30..59a58f31 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.35.0", + "version": "0.35.1", "orientation": "default", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -37,7 +37,7 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 66, + "versionCode": 67, "adaptiveIcon": { "foregroundImage": "./assets/images/icon-android-plain.png", "monochromeImage": "./assets/images/icon-android-themed.png", diff --git a/app/_layout.tsx b/app/_layout.tsx index 29f2c89b..e1f3149a 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -269,6 +269,15 @@ function Layout() { await Notifications?.setNotificationChannelAsync("default", { name: "default", }); + + // Create dedicated channel for download notifications + console.log("Setting android notification channel 'downloads'"); + await Notifications?.setNotificationChannelAsync("downloads", { + name: "Downloads", + importance: Notifications.AndroidImportance.DEFAULT, + vibrationPattern: [0, 250, 250, 250], + lightColor: "#FF231F7C", + }); } const granted = await checkAndRequestPermissions(); diff --git a/eas.json b/eas.json index a9aef4c6..17ff8edd 100644 --- a/eas.json +++ b/eas.json @@ -26,13 +26,6 @@ "EXPO_PUBLIC_WRITE_DEBUG": "1" } }, - "preview": { - "environment": "development", - "distribution": "internal", - "env": { - "EXPO_PUBLIC_WRITE_DEBUG": "1" - } - }, "development-simulator": { "environment": "development", "developmentClient": true, @@ -44,16 +37,22 @@ "EXPO_PUBLIC_WRITE_DEBUG": "1" } }, + "preview": { + "distribution": "internal", + "env": { + "EXPO_PUBLIC_WRITE_DEBUG": "1" + } + }, "production": { "environment": "production", - "channel": "0.35.0", + "channel": "0.35.1", "android": { "image": "latest" } }, "production-apk": { "environment": "production", - "channel": "0.35.0", + "channel": "0.35.1", "android": { "buildType": "apk", "image": "latest" @@ -61,7 +60,7 @@ }, "production-apk-tv": { "environment": "production", - "channel": "0.35.0", + "channel": "0.35.1", "android": { "buildType": "apk", "image": "latest" diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 7a8fe7d1..c3180c0a 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -4,6 +4,7 @@ import type { } from "@jellyfin/sdk/lib/generated-client/models"; import * as Application from "expo-application"; import * as FileSystem from "expo-file-system"; +import * as Notifications from "expo-notifications"; import { router } from "expo-router"; import { atom, useAtom } from "jotai"; import { throttle } from "lodash"; @@ -90,6 +91,69 @@ function useDownloadProvider() { const { settings } = useSettings(); const successHapticFeedback = useHaptic("success"); + // Generate notification content based on item type + const getNotificationContent = useCallback( + (item: BaseItemDto, isSuccess: boolean) => { + if (item.Type === "Episode") { + const season = item.ParentIndexNumber + ? String(item.ParentIndexNumber).padStart(2, "0") + : "??"; + const episode = item.IndexNumber + ? String(item.IndexNumber).padStart(2, "0") + : "??"; + const subtitle = `${item.Name} - [S${season}E${episode}] (${item.SeriesName})`; + + return { + title: isSuccess ? "Download complete" : "Download failed", + body: subtitle, + }; + } else if (item.Type === "Movie") { + const year = item.ProductionYear ? ` (${item.ProductionYear})` : ""; + const subtitle = `${item.Name}${year}`; + + return { + title: isSuccess ? "Download complete" : "Download failed", + body: subtitle, + }; + } else { + // Fallback for other types + return { + title: isSuccess + ? t("home.downloads.toasts.download_completed_for_item", { + item: item.Name, + }) + : t("home.downloads.toasts.download_failed_for_item", { + item: item.Name, + }), + body: item.Name || "Unknown item", + }; + } + }, + [t], + ); + + // Send local notification for download events + const sendDownloadNotification = useCallback( + async (title: string, body: string, data?: Record) => { + if (Platform.isTV) return; + + try { + await Notifications.scheduleNotificationAsync({ + content: { + title, + body, + data, + ...(Platform.OS === "android" && { channelId: "downloads" }), + }, + trigger: null, // Show immediately + }); + } catch (error) { + console.error("Failed to send notification:", error); + } + }, + [], + ); + /// Cant use the background downloader callback. As its not triggered if size is unknown. const updateProgress = async () => { const tasks = await BackGroundDownloader.checkForExistingDownloads(); @@ -418,6 +482,21 @@ function useDownloadProvider() { } await saveDownloadsDatabase(db); + // Send native notification for successful download + const successNotification = getNotificationContent( + process.item, + true, + ); + await sendDownloadNotification( + successNotification.title, + successNotification.body, + { + itemId: process.item.Id, + itemName: process.item.Name, + type: "download_completed", + }, + ); + toast.success( t("home.downloads.toasts.download_completed_for_item", { item: process.item.Name, @@ -425,8 +504,25 @@ function useDownloadProvider() { ); removeProcess(process.id); }) - .error((error: any) => { + .error(async (error: any) => { console.error("Download error:", error); + + // Send native notification for failed download + const failureNotification = getNotificationContent( + process.item, + false, + ); + await sendDownloadNotification( + failureNotification.title, + failureNotification.body, + { + itemId: process.item.Id, + itemName: process.item.Name, + type: "download_failed", + error: error?.message || "Unknown error", + }, + ); + toast.error( t("home.downloads.toasts.download_failed_for_item", { item: process.item.Name, @@ -435,7 +531,7 @@ function useDownloadProvider() { removeProcess(process.id); }); }, - [authHeader], + [authHeader, sendDownloadNotification, getNotificationContent], ); const manageDownloadQueue = useCallback(() => { diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 56f64abd..1ca64814 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ setJellyfin( () => new Jellyfin({ - clientInfo: { name: "Streamyfin", version: "0.35.0" }, + clientInfo: { name: "Streamyfin", version: "0.35.1" }, deviceInfo: { name: deviceName, id, @@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ return { authorization: `MediaBrowser Client="Streamyfin", Device=${ Platform.OS === "android" ? "Android" : "iOS" - }, DeviceId="${deviceId}", Version="0.35.0"`, + }, DeviceId="${deviceId}", Version="0.35.1"`, }; }, [deviceId]); From 0b9bbb63eb8e704891cc4623c55e9f906219e4c9 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 3 Sep 2025 22:26:57 +0200 Subject: [PATCH 05/33] fix: download card design and percentage negative number fix --- components/downloads/ActiveDownloads.tsx | 182 +-------------------- components/downloads/DownloadCard.tsx | 198 +++++++++++++++++++++++ 2 files changed, 200 insertions(+), 180 deletions(-) create mode 100644 components/downloads/DownloadCard.tsx diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 178296b1..97b197c4 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,27 +1,9 @@ -import { Ionicons } from "@expo/vector-icons"; -import { useQueryClient } from "@tanstack/react-query"; -import { Image } from "expo-image"; -import { useRouter } from "expo-router"; import { t } from "i18next"; -import { useMemo } from "react"; -import { - ActivityIndicator, - TouchableOpacity, - type TouchableOpacityProps, - View, - type ViewProps, -} from "react-native"; -import { toast } from "sonner-native"; +import { View, type ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { JobStatus } from "@/providers/Downloads/types"; -import { storage } from "@/utils/mmkv"; -import { formatTimeString } from "@/utils/time"; -import { Button } from "../Button"; - -const bytesToMB = (bytes: number) => { - return bytes / 1024 / 1024; -}; +import { DownloadCard } from "./DownloadCard"; interface ActiveDownloadsProps extends ViewProps {} @@ -52,163 +34,3 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { ); } - -interface DownloadCardProps extends TouchableOpacityProps { - process: JobStatus; -} - -const DownloadCard = ({ process, ...props }: DownloadCardProps) => { - const { startDownload, pauseDownload, resumeDownload, removeProcess } = - useDownload(); - const router = useRouter(); - const queryClient = useQueryClient(); - - const handlePause = async (id: string) => { - try { - await pauseDownload(id); - toast.success(t("home.downloads.toasts.download_paused")); - } catch (error) { - console.error("Error pausing download:", error); - toast.error(t("home.downloads.toasts.could_not_pause_download")); - } - }; - - const handleResume = async (id: string) => { - try { - await resumeDownload(id); - toast.success(t("home.downloads.toasts.download_resumed")); - } catch (error) { - console.error("Error resuming download:", error); - toast.error(t("home.downloads.toasts.could_not_resume_download")); - } - }; - - const handleDelete = async (id: string) => { - try { - await removeProcess(id); - toast.success(t("home.downloads.toasts.download_deleted")); - queryClient.invalidateQueries({ queryKey: ["downloads"] }); - } catch (error) { - console.error("Error deleting download:", error); - toast.error(t("home.downloads.toasts.could_not_delete_download")); - } - }; - - const eta = (p: JobStatus) => { - if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null; - - const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0); - if (bytesRemaining <= 0) return null; - - const secondsRemaining = bytesRemaining / p.speed; - - return formatTimeString(secondsRemaining, "s"); - }; - - const base64Image = useMemo(() => { - return storage.getString(process.item.Id!); - }, []); - - return ( - router.push(`/(auth)/items/page?id=${process.item.Id}`)} - className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden' - {...props} - > - {process.status === "downloading" && ( - - )} - - - {base64Image && ( - - - - )} - - {process.item.Type} - {process.item.Name} - - {process.item.ProductionYear} - - - {process.progress === 0 ? ( - - ) : ( - {process.progress.toFixed(0)}% - )} - {process.speed && process.speed > 0 && ( - - {bytesToMB(process.speed).toFixed(2)} MB/s - - )} - {eta(process) && ( - - {t("home.downloads.eta", { eta: eta(process) })} - - )} - - - - {process.status} - - - - {process.status === "downloading" && ( - handlePause(process.id)} - className='p-2 rounded-full bg-yellow-600' - > - - - )} - {process.status === "paused" && ( - handleResume(process.id)} - className='p-2 rounded-full bg-green-600' - > - - - )} - handleDelete(process.id)} - className='p-2 rounded-full bg-red-600' - > - - - - - {process.status === "completed" && ( - - - - )} - - - ); -}; diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx new file mode 100644 index 00000000..5ffce752 --- /dev/null +++ b/components/downloads/DownloadCard.tsx @@ -0,0 +1,198 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useQueryClient } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useRouter } from "expo-router"; +import { t } from "i18next"; +import { useMemo } from "react"; +import { + ActivityIndicator, + TouchableOpacity, + type TouchableOpacityProps, + View, +} from "react-native"; +import { toast } from "sonner-native"; +import { Text } from "@/components/common/Text"; +import { useDownload } from "@/providers/DownloadProvider"; +import { JobStatus } from "@/providers/Downloads/types"; +import { storage } from "@/utils/mmkv"; +import { formatTimeString } from "@/utils/time"; +import { Button } from "../Button"; + +const bytesToMB = (bytes: number) => { + return bytes / 1024 / 1024; +}; + +interface DownloadCardProps extends TouchableOpacityProps { + process: JobStatus; +} + +export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { + const { startDownload, pauseDownload, resumeDownload, removeProcess } = + useDownload(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const handlePause = async (id: string) => { + try { + await pauseDownload(id); + toast.success(t("home.downloads.toasts.download_paused")); + } catch (error) { + console.error("Error pausing download:", error); + toast.error(t("home.downloads.toasts.could_not_pause_download")); + } + }; + + const handleResume = async (id: string) => { + try { + await resumeDownload(id); + toast.success(t("home.downloads.toasts.download_resumed")); + } catch (error) { + console.error("Error resuming download:", error); + toast.error(t("home.downloads.toasts.could_not_resume_download")); + } + }; + + const handleDelete = async (id: string) => { + try { + await removeProcess(id); + toast.success(t("home.downloads.toasts.download_deleted")); + queryClient.invalidateQueries({ queryKey: ["downloads"] }); + } catch (error) { + console.error("Error deleting download:", error); + toast.error(t("home.downloads.toasts.could_not_delete_download")); + } + }; + + const eta = (p: JobStatus) => { + if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null; + + const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0); + if (bytesRemaining <= 0) return null; + + const secondsRemaining = bytesRemaining / p.speed; + + return formatTimeString(secondsRemaining, "s"); + }; + + const base64Image = useMemo(() => { + return storage.getString(process.item.Id!); + }, []); + + // Sanitize progress to ensure it's within valid bounds + const sanitizedProgress = useMemo(() => { + if ( + typeof process.progress !== "number" || + Number.isNaN(process.progress) + ) { + return 0; + } + return Math.max(0, Math.min(100, process.progress)); + }, [process.progress]); + + return ( + router.push(`/(auth)/items/page?id=${process.item.Id}`)} + className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden' + {...props} + > + {process.status === "downloading" && ( + 0 + ? `${Math.max(5, sanitizedProgress)}%` + : "5%", + }} + /> + )} + + {/* Action buttons in top right corner */} + + {process.status === "downloading" && ( + handlePause(process.id)} + className='p-1' + > + + + )} + {process.status === "paused" && ( + handleResume(process.id)} + className='p-1' + > + + + )} + handleDelete(process.id)} + className='p-1' + > + + + + + + + {base64Image && ( + + + + )} + + {process.item.Type} + {process.item.Name} + + {process.item.ProductionYear} + + + {sanitizedProgress === 0 ? ( + + ) : ( + {sanitizedProgress.toFixed(0)}% + )} + {process.speed && process.speed > 0 && ( + + {bytesToMB(process.speed).toFixed(2)} MB/s + + )} + {eta(process) && ( + + {t("home.downloads.eta", { eta: eta(process) })} + + )} + + + + {process.status} + + + + {process.status === "completed" && ( + + + + )} + + + ); +}; From e47c863aa447f0c9aa84823112aca0802e303948 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Thu, 4 Sep 2025 07:22:15 +0200 Subject: [PATCH 06/33] chore: version --- app.json | 4 ++-- eas.json | 6 +++--- providers/JellyfinProvider.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app.json b/app.json index 59a58f31..b387cfeb 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.35.1", + "version": "0.36.0", "orientation": "default", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -37,7 +37,7 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 67, + "versionCode": 69, "adaptiveIcon": { "foregroundImage": "./assets/images/icon-android-plain.png", "monochromeImage": "./assets/images/icon-android-themed.png", diff --git a/eas.json b/eas.json index 17ff8edd..9173f4ea 100644 --- a/eas.json +++ b/eas.json @@ -45,14 +45,14 @@ }, "production": { "environment": "production", - "channel": "0.35.1", + "channel": "0.36.0", "android": { "image": "latest" } }, "production-apk": { "environment": "production", - "channel": "0.35.1", + "channel": "0.36.0", "android": { "buildType": "apk", "image": "latest" @@ -60,7 +60,7 @@ }, "production-apk-tv": { "environment": "production", - "channel": "0.35.1", + "channel": "0.36.0", "android": { "buildType": "apk", "image": "latest" diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 1ca64814..4c99e0be 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ setJellyfin( () => new Jellyfin({ - clientInfo: { name: "Streamyfin", version: "0.35.1" }, + clientInfo: { name: "Streamyfin", version: "0.36.0" }, deviceInfo: { name: deviceName, id, @@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ return { authorization: `MediaBrowser Client="Streamyfin", Device=${ Platform.OS === "android" ? "Android" : "iOS" - }, DeviceId="${deviceId}", Version="0.35.1"`, + }, DeviceId="${deviceId}", Version="0.36.0"`, }; }, [deviceId]); From 0298fb00aa44542e9e4528dd657dfeb1f189564c Mon Sep 17 00:00:00 2001 From: lostb1t Date: Thu, 4 Sep 2025 17:31:56 +0200 Subject: [PATCH 07/33] fix: section titles (#1036) fixes a bug where the section titles were just "section-[index]" --- components/settings/HomeIndex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 75215461..57aac02c 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -308,7 +308,7 @@ export const HomeIndex = () => { if (!api || !user?.Id || !settings?.home?.sections) return []; const ss: Section[] = []; for (const [index, section] of settings.home.sections.entries()) { - const id = section.items?.title || `section-${index}`; + const id = section.title || `section-${index}`; ss.push({ title: t(`${id}`), queryKey: ["home", id], From 42922cc92b62147fea1b77113a4d643f3697f939 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:23:23 +0200 Subject: [PATCH 08/33] chore(deps): Update actions/stale action to v10 (#1039) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 2e52296f..936e110d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,7 +15,7 @@ jobs: steps: - name: ๐Ÿ”„ Mark/Close Stale Issues - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: # Global settings repo-token: ${{ secrets.GITHUB_TOKEN }} From b5917821a0c02bc338601ddbbad17b49ce917787 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:55:24 +0200 Subject: [PATCH 09/33] chore(deps): Update actions/setup-node action to v5 (#1038) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> --- .github/workflows/linting.yml | 2 +- .github/workflows/update-issue-form.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index aab34a05..7e8e2dfc 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -106,7 +106,7 @@ jobs: fetch-depth: 0 - name: "๐ŸŸข Setup Node.js" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: '24.x' diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 8b4f42ff..34680b2e 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: "๐ŸŸข Setup Node.js" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: '24.x' cache: 'npm' From 32094fbc9f8fa85970ebb4447f9cb062e5091adf Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:10:16 +0200 Subject: [PATCH 10/33] docs: add copilot-instructions.md for context-aware AI suggestions (#1035) Co-authored-by: lostb1t --- .github/copilot-instructions.md | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..c15d5500 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,60 @@ +# Copilot Instructions for Streamyfin + +## Project Overview + +Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). +It supports mobile (iOS/Android) and TV platforms, and integrates with Jellyfin and Jellyseerr APIs. + +## Main Technologies + +- React Native (Expo) +- TypeScript +- React Query +- Jotai (state management) +- Jellyfin SDK (TypeScript) +- BiomeJS (code formatting/linting) +- EAS (Expo Application Services) +- Shell scripting (for automation) +- GitHub Actions (CI/CD) + +## Code Structure + +- `app/` โ€“ Main application code (screens, navigation, etc.) +- `components/` โ€“ Reusable UI components +- `providers/` โ€“ Context and API providers (e.g., JellyfinProvider.tsx) +- `utils/` โ€“ Utility functions and atoms +- `assets/` โ€“ Images and static assets +- `scripts/` โ€“ Automation scripts (Node.js, Bash) +- `plugins/` โ€“ Expo/Metro plugins +- `README.md` โ€“ Project documentation + +## Coding Conventions + +- Use TypeScript for all new code. +- Prefer functional React components. +- Use hooks for state and side effects. +- Use Jotai for global state. +- Use React Query for data fetching/caching. +- Use BiomeJS for formatting and linting. +- Follow the established folder structure for screens/components. + +## API Usage + +- Use the Jellyfin SDK for all server interactions. +- Use the `apiAtom` and `userAtom` from `JellyfinProvider` for authenticated API calls. +- For navigation, use `expo-router`. + +## Commit Messages + +- Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat:`, `fix:`, `chore:`). +- Example: `feat(player): add Chromecast support` + +## Special Instructions + +- When suggesting code, prefer using existing atoms, hooks, and utility functions. +- When adding new features, ensure they are accessible via both mobile and TV navigation if relevant. +- When updating dependencies or scripts, check for compatibility with Expo and EAS. + +--- + +**Copilot: Please use these instructions to provide context-aware suggestions and code completions for this repository.** \ No newline at end of file From 2b77d0fefb294f69932ece6c0a3abe133ad06c2e Mon Sep 17 00:00:00 2001 From: sarendsen Date: Mon, 8 Sep 2025 13:25:51 +0200 Subject: [PATCH 11/33] fix: media source sheet was using the wrong prop --- components/MediaSourceSheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/MediaSourceSheet.tsx b/components/MediaSourceSheet.tsx index 76d336f2..f21243a3 100644 --- a/components/MediaSourceSheet.tsx +++ b/components/MediaSourceSheet.tsx @@ -36,7 +36,7 @@ export const MediaSourceSheet: React.FC = ({ return getDisplayName(selected); }, [selected, getDisplayName]); - if (isTv || (item.MediaStreams && item.MediaStreams.length <= 1)) return null; + if (isTv || (item.MediaSources && item.MediaSources.length <= 1)) return null; return ( From ceafdbf9eead4a8fd326ef9dcda129f66c61032c Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:02:33 +0200 Subject: [PATCH 12/33] fix: fixed permission request for activity (#1046) Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app.json | 1 + bun.lock | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index b387cfeb..c766fa94 100644 --- a/app.json +++ b/app.json @@ -49,6 +49,7 @@ "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK", "android.permission.WRITE_SETTINGS" ], + "blockedPermissions": ["android.permission.ACTIVITY_RECOGNITION"], "googleServicesFile": "./google-services.json" }, "plugins": [ diff --git a/bun.lock b/bun.lock index 912b358c..b0a6c931 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "@jellyfin/sdk": "^0.11.0", "@kesha-antonov/react-native-background-downloader": "^3.2.6", "@react-native-community/netinfo": "^11.4.1", - "@react-native-menu/menu": "^1.2.3", + "@react-native-menu/menu": "1.2.3", "@react-navigation/material-top-tabs": "^7.2.14", "@react-navigation/native": "^7.0.14", "@shopify/flash-list": "^1.8.3", @@ -537,7 +537,7 @@ "@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="], - "@react-native-menu/menu": ["@react-native-menu/menu@1.2.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-63NU9NHd/KcBa7lhhV69xQmY3ORUxjMRmlzTY/qrY56GHnuKX+16dvUpKAuRSPK5kebBuRw5X4MFe2L9JHUlWA=="], + "@react-native-menu/menu": ["@react-native-menu/menu@1.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-sEfiVIivsa0lSelFm9Wbm/RAi+XoEHc75GGhjwvSrj9KSCVvNNXwr9F8l42e1t/lzYvVYzmkYxLG6VKxrDYJiw=="], "@react-native-tvos/config-tv": ["@react-native-tvos/config-tv@0.1.3", "", { "dependencies": { "getenv": "^1.0.0" }, "peerDependencies": { "expo": ">=52.0.0" } }, "sha512-9N3scEwrNtgbXRr9Pmg5H7mlsNKdbS4Jovt92aaWX6ggj3bx5zmHZHTtbqFdiNWC551LGcr79i/S/xDKE6CqNg=="], diff --git a/package.json b/package.json index 2df415b6..2b987dd0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@jellyfin/sdk": "^0.11.0", "@kesha-antonov/react-native-background-downloader": "^3.2.6", "@react-native-community/netinfo": "^11.4.1", - "@react-native-menu/menu": "^1.2.3", + "@react-native-menu/menu": "1.2.3", "@react-navigation/material-top-tabs": "^7.2.14", "@react-navigation/native": "^7.0.14", "@shopify/flash-list": "^1.8.3", From 0b0592a699277005b2540fd2462f35cb6d6d7db6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:15:23 +0200 Subject: [PATCH 13/33] fix: library sorting persistence by adding missing useEffect dependencies (#1047) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lostb1t <168401+lostb1t@users.noreply.github.com> Co-authored-by: lance chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/(tabs)/(libraries)/[libraryId].tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index 2f58d74c..d795fb91 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -77,7 +77,13 @@ const Page = () => { } else { _setSortBy([SortByOption.SortName]); } - }, []); + }, [ + libraryId, + sortOrderPreference, + sortByPreference, + _setSortOrder, + _setSortBy, + ]); const setSortBy = useCallback( (sortBy: SortByOption[]) => { @@ -87,7 +93,7 @@ const Page = () => { } _setSortBy(sortBy); }, - [libraryId, sortByPreference], + [libraryId, sortByPreference, setSortByPreference, _setSortBy], ); const setSortOrder = useCallback( @@ -101,7 +107,7 @@ const Page = () => { } _setSortOrder(sortOrder); }, - [libraryId, sortOrderPreference], + [libraryId, sortOrderPreference, setOderByPreference, _setSortOrder], ); const nrOfCols = useMemo(() => { From b02f49fbd2d013edbfb5ab4547caf2a0286b4649 Mon Sep 17 00:00:00 2001 From: Drew Bowman Date: Mon, 8 Sep 2025 09:33:43 -0500 Subject: [PATCH 14/33] feat: improve quick connect bottom sheet (#1000) Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> Co-authored-by: lostb1t --- components/inputs/PinInput.tsx | 123 +++++++++++++++++++++++++++ components/settings/QuickConnect.tsx | 31 ++++--- 2 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 components/inputs/PinInput.tsx diff --git a/components/inputs/PinInput.tsx b/components/inputs/PinInput.tsx new file mode 100644 index 00000000..a4885ceb --- /dev/null +++ b/components/inputs/PinInput.tsx @@ -0,0 +1,123 @@ +import { BottomSheetTextInput } from "@gorhom/bottom-sheet"; +import React, { useCallback, useImperativeHandle, useRef } from "react"; +import { StyleSheet, Text, type TextInputProps, View } from "react-native"; + +interface PinInputProps extends Omit { + value: string; + onChangeText: (text: string) => void; + length?: number; + autoFocus?: boolean; +} + +export interface PinInputRef { + focus: () => void; +} + +const PinInputComponent = React.forwardRef( + (props, ref) => { + const { + value, + onChangeText, + length = 6, + style, + autoFocus, + ...rest + } = props; + + const inputRef = useRef(null); + const activeIndex = value.length; + + const handlePress = useCallback(() => { + inputRef.current?.focus(); + }, []); + + useImperativeHandle( + ref, + () => ({ + focus: () => inputRef.current?.focus(), + }), + [], + ); + + return ( + + + + {Array(length) + .fill(0) + .map((_, i) => ( + + {value[i]} + {i === activeIndex && } + + ))} + + + ); + }, +); + +PinInputComponent.displayName = "PinInput"; + +export const PinInput = PinInputComponent; + +const styles = StyleSheet.create({ + container: { + width: "100%", + }, + hiddenInput: { + position: "absolute", + width: 1, + height: 1, + opacity: 0, + }, + cells: { + flexDirection: "row", + justifyContent: "space-between", + width: "100%", + }, + cell: { + width: 40, + height: 48, + borderWidth: 1, + borderColor: "#374151", + borderRadius: 8, + alignItems: "center", + justifyContent: "center", + backgroundColor: "#1F2937", + }, + activeCell: { + borderColor: "#6366F1", + }, + filledCell: { + borderColor: "#4B5563", + }, + digit: { + fontSize: 24, + color: "white", + fontWeight: "500", + }, + cursor: { + position: "absolute", + width: 2, + height: 24, + backgroundColor: "#6366F1", + animation: "blink 1s infinite", + }, +}); diff --git a/components/settings/QuickConnect.tsx b/components/settings/QuickConnect.tsx index b61500ce..d0cbf85e 100644 --- a/components/settings/QuickConnect.tsx +++ b/components/settings/QuickConnect.tsx @@ -2,7 +2,6 @@ import { BottomSheetBackdrop, type BottomSheetBackdropProps, BottomSheetModal, - BottomSheetTextInput, BottomSheetView, } from "@gorhom/bottom-sheet"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; @@ -10,17 +9,19 @@ import { useAtom } from "jotai"; import type React from "react"; import { useCallback, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Alert, View, type ViewProps } from "react-native"; +import { Alert, Platform, View, type ViewProps } from "react-native"; import { useHaptic } from "@/hooks/useHaptic"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { Button } from "../Button"; import { Text } from "../common/Text"; +import { PinInput } from "../inputs/PinInput"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; interface Props extends ViewProps {} export const QuickConnect: React.FC = ({ ...props }) => { + const isTv = Platform.isTV; const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const [quickConnectCode, setQuickConnectCode] = useState(); @@ -73,11 +74,17 @@ export const QuickConnect: React.FC = ({ ...props }) => { } }, [api, user, quickConnectCode]); + if (isTv) return null; + return ( bottomSheetModalRef?.current?.present()} + onPress={() => { + // Reset the code when opening the sheet + setQuickConnectCode(""); + bottomSheetModalRef?.current?.present(); + }} title={t("home.settings.quick_connect.authorize_button")} textColor='blue' /> @@ -93,6 +100,9 @@ export const QuickConnect: React.FC = ({ ...props }) => { backgroundColor: "#171717", }} backdropComponent={renderBackdrop} + keyboardBehavior='interactive' + keyboardBlurBehavior='restore' + android_keyboardInputMode='adjustResize' > @@ -102,16 +112,17 @@ export const QuickConnect: React.FC = ({ ...props }) => { - - + + {t( "home.settings.quick_connect.enter_the_quick_connect_code", )} - placeholderTextColor='#9CA3AF' - value={quickConnectCode} + + From 2a49b766e724306e8970bcb40792640bd578fcf6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:47:34 +0200 Subject: [PATCH 15/33] chore(deps): Update github/codeql-action action to v3.30.1 (#1045) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index 763728ce..0934f953 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -31,13 +31,13 @@ jobs: fetch-depth: 0 - name: ๐Ÿ Initialize CodeQL - uses: github/codeql-action/init@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 + uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - name: ๐Ÿ› ๏ธ Autobuild - uses: github/codeql-action/autobuild@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 + uses: github/codeql-action/autobuild@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 - name: ๐Ÿงช Perform CodeQL Analysis - uses: github/codeql-action/analyze@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 + uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 From bdc0962d6092d1416704ac65adc1015e63d34dbd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:51:01 +0200 Subject: [PATCH 16/33] chore(deps): Update dependency @biomejs/biome to v2.2.3 (#1044) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index ac6b28ea..884f247f 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", "files": { "includes": [ "**/*", From 0fd2d766a347f9f1bc8e82ee9a4489cd3061dcf5 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:51:41 +0200 Subject: [PATCH 17/33] chore(deps): Update dependency @biomejs/biome to v2.2.3 (#1048) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 20 ++++++++++---------- package.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index b0a6c931..20890bcb 100644 --- a/bun.lock +++ b/bun.lock @@ -84,7 +84,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@biomejs/biome": "^2.2.2", + "@biomejs/biome": "^2.2.3", "@react-native-community/cli": "^20.0.0", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.12", @@ -297,23 +297,23 @@ "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - "@biomejs/biome": ["@biomejs/biome@2.2.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.2", "@biomejs/cli-darwin-x64": "2.2.2", "@biomejs/cli-linux-arm64": "2.2.2", "@biomejs/cli-linux-arm64-musl": "2.2.2", "@biomejs/cli-linux-x64": "2.2.2", "@biomejs/cli-linux-x64-musl": "2.2.2", "@biomejs/cli-win32-arm64": "2.2.2", "@biomejs/cli-win32-x64": "2.2.2" }, "bin": { "biome": "bin/biome" } }, "sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w=="], + "@biomejs/biome": ["@biomejs/biome@2.2.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.3", "@biomejs/cli-darwin-x64": "2.2.3", "@biomejs/cli-linux-arm64": "2.2.3", "@biomejs/cli-linux-arm64-musl": "2.2.3", "@biomejs/cli-linux-x64": "2.2.3", "@biomejs/cli-linux-x64-musl": "2.2.3", "@biomejs/cli-win32-arm64": "2.2.3", "@biomejs/cli-win32-x64": "2.2.3" }, "bin": { "biome": "bin/biome" } }, "sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ=="], "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.9.2", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-IZZKllcaqCGsKIgeXmYFGU95IXxbBpXtwKws4Lg2GJw/qqAYYsPFEl0JBvnymSD7G1zkHYEilg5UHuTd0NmX7A=="], diff --git a/package.json b/package.json index 2b987dd0..dff98d5e 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@biomejs/biome": "^2.2.2", + "@biomejs/biome": "^2.2.3", "@react-native-community/cli": "^20.0.0", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.12", From fb2bdb0e2c495ece831be4e1f077010bdb8628b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 20:10:57 +0200 Subject: [PATCH 18/33] chore(deps): Update github/codeql-action action to v3.30.3 (#1055) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index 0934f953..a1e6fc18 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -31,13 +31,13 @@ jobs: fetch-depth: 0 - name: ๐Ÿ Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - name: ๐Ÿ› ๏ธ Autobuild - uses: github/codeql-action/autobuild@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 - name: ๐Ÿงช Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 From 7436ad90eb39c014e64ea635919c1b99c0da671b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 20:14:31 +0200 Subject: [PATCH 19/33] chore(deps): Update dependency @biomejs/biome to v2.2.4 (#1054) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Uruk --- biome.json | 2 +- bun.lock | 20 ++++++++++---------- package.json | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/biome.json b/biome.json index 884f247f..487c9bae 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "files": { "includes": [ "**/*", diff --git a/bun.lock b/bun.lock index 20890bcb..afbf5d03 100644 --- a/bun.lock +++ b/bun.lock @@ -84,7 +84,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@biomejs/biome": "^2.2.3", + "@biomejs/biome": "^2.2.4", "@react-native-community/cli": "^20.0.0", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.12", @@ -297,23 +297,23 @@ "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - "@biomejs/biome": ["@biomejs/biome@2.2.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.3", "@biomejs/cli-darwin-x64": "2.2.3", "@biomejs/cli-linux-arm64": "2.2.3", "@biomejs/cli-linux-arm64-musl": "2.2.3", "@biomejs/cli-linux-x64": "2.2.3", "@biomejs/cli-linux-x64-musl": "2.2.3", "@biomejs/cli-win32-arm64": "2.2.3", "@biomejs/cli-win32-x64": "2.2.3" }, "bin": { "biome": "bin/biome" } }, "sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg=="], + "@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="], "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.9.2", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-IZZKllcaqCGsKIgeXmYFGU95IXxbBpXtwKws4Lg2GJw/qqAYYsPFEl0JBvnymSD7G1zkHYEilg5UHuTd0NmX7A=="], diff --git a/package.json b/package.json index dff98d5e..091ca20a 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@biomejs/biome": "^2.2.3", + "@biomejs/biome": "^2.2.4", "@react-native-community/cli": "^20.0.0", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.12", From 9175c6a135e022cb7244950f8701b05a91fd5b93 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:08:42 -0400 Subject: [PATCH 20/33] fix: external deep links not working (#1063) --- app/_layout.tsx | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index e1f3149a..f4e3d9eb 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -20,8 +20,8 @@ import { } from "@/utils/background-tasks"; import { LogProvider, - writeDebugLog, writeErrorLog, + writeInfoLog, writeToLog, } from "@/utils/log"; import { storage } from "@/utils/mmkv"; @@ -84,19 +84,19 @@ SplashScreen.setOptions({ fade: true, }); +function redirect(notification: typeof Notifications.Notification) { + const url = notification.request.content.data?.url; + if (url) { + router.push(url); + } +} + function useNotificationObserver() { useEffect(() => { if (Platform.isTV) return; let isMounted = true; - function redirect(notification: typeof Notifications.Notification) { - const url = notification.request.content.data?.url; - if (url) { - router.push(url); - } - } - Notifications.getLastNotificationResponseAsync().then( (response: { notification: any }) => { if (!isMounted || !response?.notification) { @@ -106,15 +106,8 @@ function useNotificationObserver() { }, ); - const subscription = Notifications.addNotificationResponseReceivedListener( - (response: { notification: any }) => { - redirect(response.notification); - }, - ); - return () => { isMounted = false; - subscription.remove(); }; }, []); } @@ -317,9 +310,12 @@ function Layout() { responseListener.current = Notifications?.addNotificationResponseReceivedListener( (response: NotificationResponse) => { + // redirect if internal notification + redirect(response?.notification); + // Currently the notifications supported by the plugin will send data for deep links. const { title, data } = response.notification.request.content; - writeDebugLog( + writeInfoLog( `Notification ${title} opened`, response.notification.request.content, ); From 09b3cc78352c693d470d0770b11217a89ff89de1 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Sun, 14 Sep 2025 04:55:04 -0400 Subject: [PATCH 21/33] fix: external deep links not working pt2 (#1064) --- app/_layout.tsx | 55 +++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index f4e3d9eb..c94f540e 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -315,36 +315,37 @@ function Layout() { // Currently the notifications supported by the plugin will send data for deep links. const { title, data } = response.notification.request.content; - writeInfoLog( - `Notification ${title} opened`, - response.notification.request.content, - ); - if (data && Object.keys(data).length > 0) { - const type = (data?.type ?? "").toString().toLowerCase(); - const itemId = data?.id; + writeInfoLog(`Notification ${title} opened`, data); - switch (type) { - case "movie": - router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`); - break; - case "episode": - // We just clicked a notification for an individual episode. - if (itemId) { - router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`); - // summarized season notification for multiple episodes. Bring them to series season + let url: any; + const type = (data?.type ?? "").toString().toLowerCase(); + const itemId = data?.id; + + switch (type) { + case "movie": + url = `/(auth)/(tabs)/home/items/page?id=${itemId}`; + break; + case "episode": + // `/(auth)/(tabs)/${from}/items/page?id=${item.Id}`; + // We just clicked a notification for an individual episode. + if (itemId) { + url = `/(auth)/(tabs)/home/items/page?id=${itemId}`; + // summarized season notification for multiple episodes. Bring them to series season + } else { + const seriesId = data.seriesId; + const seasonIndex = data.seasonIndex; + if (seasonIndex) { + url = `/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`; } else { - const seriesId = data.seriesId; - const seasonIndex = data.seasonIndex; - if (seasonIndex) { - router.push( - `/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`, - ); - } else { - router.push(`/(auth)/(tabs)/home/series/${seriesId}`); - } + url = `/(auth)/(tabs)/home/series/${seriesId}`; } - break; - } + } + break; + } + + writeInfoLog(`Notification attempting to redirect to ${url}`); + if (url) { + router.push(url); } }, ); From 1feb22854c7b443a9100d8e570aef334f6374870 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 17 Sep 2025 09:38:03 -0400 Subject: [PATCH 22/33] chore: update jellyseerr submodule (#1059) --- utils/jellyseerr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From fa8af5bc110a9923773a11ab8ff8dc03b7dfd5e1 Mon Sep 17 00:00:00 2001 From: lostb1t Date: Fri, 19 Sep 2025 15:57:13 +0200 Subject: [PATCH 23/33] Update linting.yml --- .github/workflows/linting.yml | 1 + 1 file changed, 1 insertion(+) 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 From 77895983b03d47fc2fe6d97203195a680bd5694e Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:47:58 +0200 Subject: [PATCH 24/33] fix: add better offline messages and checks (#1051) --- bun.lock | 4 +-- components/settings/HomeIndex.tsx | 37 ++++++++++++++++++------ hooks/useNetworkStatus.ts | 48 ++++++++++++++++++++++++------- package.json | 2 +- providers/JellyfinProvider.tsx | 2 +- translations/en.json | 5 ++++ 6 files changed, 76 insertions(+), 22 deletions(-) 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/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/package.json b/package.json index 091ca20a..22b710d1 100644 --- a/package.json +++ b/package.json @@ -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/translations/en.json b/translations/en.json index b1ccbb80..6373fc79 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", From 2d4d3f5b1bf1d1fad170a7e3ecf1932b730895f1 Mon Sep 17 00:00:00 2001 From: Chris <182387676+whoopsi-daisy@users.noreply.github.com> Date: Sat, 20 Sep 2025 14:38:19 +0200 Subject: [PATCH 25/33] Update splash screen background from gray to off-black Changing splash screen background color from a medium gray (#2E2E2E( to an "off-black" (#010101) for a darker, sleeker appearance on app launch --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From 9b367fd8c26deeb72aeb627efb40d5548904fc2a Mon Sep 17 00:00:00 2001 From: lostb1t Date: Sun, 21 Sep 2025 12:33:26 +0200 Subject: [PATCH 26/33] fix: remove redudant episode requests (#1076) --- components/series/SeasonEpisodesCarousel.tsx | 43 +------------------- components/series/SeasonPicker.tsx | 19 +-------- 2 files changed, 2 insertions(+), 60 deletions(-) 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); From 388342147e5e41caefeb9a029b788456cba36f14 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:31:56 +0200 Subject: [PATCH 27/33] refactor: replace inline typecheck with dedicated script (#1075) --- package.json | 2 +- scripts/typecheck.js | 256 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 scripts/typecheck.js diff --git a/package.json b/package.json index 22b710d1..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 .", 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}`, + ); +} From 91c4df1585d68be24c1f0c57a5e2f8f47a6a07b5 Mon Sep 17 00:00:00 2001 From: Simon Eklundh Date: Mon, 22 Sep 2025 11:11:14 +0100 Subject: [PATCH 28/33] feat: fix playback reporting (#1077) --- app/(auth)/player/direct-player.tsx | 29 +++++------------------------ hooks/usePlaybackManager.ts | 23 ++++++++--------------- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index ca906a78..b7681174 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"; @@ -264,12 +265,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(); @@ -387,12 +383,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, ); }, [ @@ -499,12 +490,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(); @@ -515,12 +501,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/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); From 97d930985582deef8da4719f3c91a8a78d3c7a63 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:34:18 -0400 Subject: [PATCH 29/33] fix: missing optional chaining for plugin setting (#1062) --- components/settings/DownloadSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx index 483d895b..6c12440c 100644 --- a/components/settings/DownloadSettings.tsx +++ b/components/settings/DownloadSettings.tsx @@ -13,7 +13,7 @@ export default function DownloadSettings({ ...props }) { const allDisabled = useMemo( () => pluginSettings?.remuxConcurrentLimit?.locked === true && - pluginSettings?.autoDownload.locked === true, + pluginSettings?.autoDownload?.locked === true, [pluginSettings], ); From 74c83c4f0099cfa09a3ee72bcdc64693972d189c Mon Sep 17 00:00:00 2001 From: asmithrsa <84108607+asmithrsa@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:19:20 -0400 Subject: [PATCH 30/33] feat: standardize capitalization for english menu options (#1068) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Uruk Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> --- translations/en.json | 294 +++++++++++++++++-------------------------- 1 file changed, 116 insertions(+), 178 deletions(-) diff --git a/translations/en.json b/translations/en.json index 6373fc79..f6efdc68 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,46 +1,46 @@ { "login": { - "username_required": "Username is required", + "username_required": "Username Is Required", "error_title": "Error", - "login_title": "Log in", + "login_title": "Log In", "login_to_title": "Log in to", "username_placeholder": "Username", "password_placeholder": "Password", - "login_button": "Log in", + "login_button": "Log In", "quick_connect": "Quick Connect", "enter_code_to_login": "Enter code {{code}} to login", "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", - "got_it": "Got it", - "connection_failed": "Connection failed", + "got_it": "Got It", + "connection_failed": "Connection Failed", "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", - "an_unexpected_error_occured": "An unexpected error occurred", - "change_server": "Change server", - "invalid_username_or_password": "Invalid username or password", + "an_unexpected_error_occured": "An Unexpected Error Occurred", + "change_server": "Change Server", + "invalid_username_or_password": "Invalid Username or Password", "user_does_not_have_permission_to_log_in": "User does not have permission to log in", "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", "there_is_a_server_error": "There is a server error", "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", - "too_old_server_text": "Unsupported jellyfin server discovered", - "too_old_server_description": "Please update jellyfin to the latest version" + "too_old_server_text": "Unsupported Jellyfin Server Discovered", + "too_old_server_description": "Please update Jellyfin to the latest version" }, "server": { "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", "server_url_placeholder": "http(s)://your-server.com", "connect_button": "Connect", - "previous_servers": "previous servers", + "previous_servers": "Previous Servers", "clear_button": "Clear", - "search_for_local_servers": "Search for local servers", + "search_for_local_servers": "Search for Local Servers", "searching": "Searching...", "servers": "Servers" }, "home": { "checking_server_connection": "Checking server connection...", "no_internet": "No Internet", - "no_items": "No items", + "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", + "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.", @@ -53,7 +53,7 @@ "suggested_episodes": "Suggested Episodes", "intro": { "welcome_to_streamyfin": "Welcome to Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", + "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.", "features_title": "Features", "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", @@ -63,12 +63,12 @@ "centralised_settings_plugin_title": "Centralised Settings Plugin", "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", "done_button": "Done", - "go_to_settings_button": "Go to settings", - "read_more": "Read more" + "go_to_settings_button": "Go to Settings", + "read_more": "Read More" }, "settings": { "settings_title": "Settings", - "log_out_button": "Log out", + "log_out_button": "Log Out", "user_info": { "user_info_title": "User Info", "user": "User", @@ -81,41 +81,41 @@ "authorize_button": "Authorize Quick Connect", "enter_the_quick_connect_code": "Enter the quick connect code...", "success": "Success", - "quick_connect_autorized": "Quick Connect authorized", + "quick_connect_autorized": "Quick Connect Authorized", "error": "Error", - "invalid_code": "Invalid code", + "invalid_code": "Invalid Code", "authorize": "Authorize" }, "media_controls": { "media_controls_title": "Media Controls", - "forward_skip_length": "Forward skip length", - "rewind_length": "Rewind length", + "forward_skip_length": "Forward Skip Length", + "rewind_length": "Rewind Length", "seconds_unit": "s" }, "gesture_controls": { "gesture_controls_title": "Gesture Controls", - "horizontal_swipe_skip": "Horizontal swipe to skip", + "horizontal_swipe_skip": "Horizontal Swipe to Skip", "horizontal_swipe_skip_description": "Swipe left/right when controls are hidden to skip", - "left_side_brightness": "Left side brightness control", + "left_side_brightness": "Left Side Brightness Control", "left_side_brightness_description": "Swipe up/down on left side to adjust brightness", - "right_side_volume": "Right side volume control", + "right_side_volume": "Right Side Volume Control", "right_side_volume_description": "Swipe up/down on right side to adjust volume" }, "audio": { "audio_title": "Audio", "set_audio_track": "Set Audio Track From Previous Item", - "audio_language": "Audio language", + "audio_language": "Audio Language", "audio_hint": "Choose a default audio language.", "none": "None", "language": "Language" }, "subtitles": { "subtitle_title": "Subtitles", - "subtitle_language": "Subtitle language", + "subtitle_language": "Subtitle Language", "subtitle_mode": "Subtitle Mode", "set_subtitle_track": "Set Subtitle Track From Previous Item", "subtitle_size": "Subtitle Size", - "subtitle_hint": "Configure subtitle preference.", + "subtitle_hint": "Configure Subtitle Preference.", "none": "None", "language": "Language", "loading": "Loading", @@ -124,13 +124,13 @@ "Smart": "Smart", "Always": "Always", "None": "None", - "OnlyForced": "OnlyForced" + "OnlyForced": "Only Forced" } }, "other": { "other_title": "Other", - "follow_device_orientation": "Auto rotate", - "video_orientation": "Video orientation", + "follow_device_orientation": "Auto Rotate", + "video_orientation": "Video Orientation", "orientation": "Orientation", "orientations": { "DEFAULT": "Default", @@ -144,8 +144,8 @@ "OTHER": "Other", "UNKNOWN": "Unknown" }, - "safe_area_in_controls": "Safe area in controls", - "video_player": "Video player", + "safe_area_in_controls": "Safe Area in Controls", + "video_player": "Video Player", "video_players": { "VLC_3": "VLC 3", "VLC_4": "VLC 4 (Experimental + PiP)" @@ -154,24 +154,13 @@ "hide_libraries": "Hide Libraries", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "disable_haptic_feedback": "Disable Haptic Feedback", - "default_quality": "Default quality", - "max_auto_play_episode_count": "Max auto play episode count", + "default_quality": "Default Quality", + "max_auto_play_episode_count": "Max Auto Play Episode Count", "disabled": "Disabled" }, "downloads": { "downloads_title": "Downloads", - "download_method": "Download method", - "remux_max_download": "Remux max download", - "auto_download": "Auto download", - "optimized_versions_server": "Optimized versions server", - "save_button": "Save", - "optimized_server": "Optimized Server", - "optimized": "Optimized", - "default": "Default", - "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", - "read_more_about_optimized_server": "Read more about the optimize server.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" + "remux_max_download": "Remux Max Download" }, "plugins": { "plugins_title": "Plugins", @@ -182,17 +171,15 @@ "server_url_placeholder": "Jellyseerr URL...", "password": "Password", "password_placeholder": "Enter password for Jellyfin user {{username}}", - "save_button": "Save", - "clear_button": "Clear", "login_button": "Login", - "total_media_requests": "Total media requests", - "movie_quota_limit": "Movie quota limit", - "movie_quota_days": "Movie quota days", - "tv_quota_limit": "TV quota limit", - "tv_quota_days": "TV quota days", - "reset_jellyseerr_config_button": "Reset Jellyseerr config", + "total_media_requests": "Total Media Requests", + "movie_quota_limit": "Movie Quota Limit", + "movie_quota_days": "Movie Quota Days", + "tv_quota_limit": "TV Quota Limit", + "tv_quota_days": "TV Quota Days", + "reset_jellyseerr_config_button": "Reset Jellyseerr Config", "unlimited": "Unlimited", - "plus_n_more": "+{{n}} more", + "plus_n_more": "+{{n}} More", "order_by": { "DEFAULT": "Default", "VOTE_COUNT_AND_AVERAGE": "Vote count and average", @@ -204,7 +191,7 @@ "url": "URL", "server_url_placeholder": "http(s)://domain.org:port", "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", - "read_more_about_marlin": "Read more about Marlin.", + "read_more_about_marlin": "Read More About Marlin.", "save_button": "Save", "toasts": { "saved": "Saved" @@ -215,39 +202,33 @@ "storage_title": "Storage", "app_usage": "App {{usedSpace}}%", "device_usage": "Device {{availableSpace}}%", - "size_used": "{{used}} of {{total}} used", + "size_used": "{{used}} of {{total}} Used", "delete_all_downloaded_files": "Delete All Downloaded Files" }, "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" + "show_intro": "Show Intro", + "reset_intro": "Reset Intro" }, "logs": { "logs_title": "Logs", - "export_logs": "Export logs", - "click_for_more_info": "Click for more info", + "export_logs": "Export Logs", + "click_for_more_info": "Click for More Info", "level": "Level", - "no_logs_available": "No logs available", - "delete_all_logs": "Delete all logs" + "no_logs_available": "No Logs Available", + "delete_all_logs": "Delete All Logs" }, "languages": { "title": "Languages", - "app_language": "App language", - "app_language_description": "Select the language for the app.", + "app_language": "App Language", "system": "System" }, "toasts": { - "error_deleting_files": "Error deleting files", - "background_downloads_enabled": "Background downloads enabled", - "background_downloads_disabled": "Background downloads disabled", - "connected": "Connected", - "could_not_connect": "Could not connect", - "invalid_url": "Invalid URL" + "error_deleting_files": "Error Deleting Files" } }, "sessions": { "title": "Sessions", - "no_active_sessions": "No active sessions" + "no_active_sessions": "No Active Sessions" }, "downloads": { "downloads_title": "Downloads", @@ -255,116 +236,80 @@ "movies": "Movies", "queue": "Queue", "queue_hint": "Queue and downloads will be lost on app restart", - "no_items_in_queue": "No items in queue", - "no_downloaded_items": "No downloaded items", - "delete_all_movies_button": "Delete all Movies", - "delete_all_tvseries_button": "Delete all TV-Series", - "delete_all_button": "Delete all", - "active_download": "Active download", - "no_active_downloads": "No active downloads", - "active_downloads": "Active downloads", + "no_items_in_queue": "No Items in Queue", + "no_downloaded_items": "No Downloaded Items", + "delete_all_movies_button": "Delete All Movies", + "delete_all_tvseries_button": "Delete All TV-Series", + "delete_all_button": "Delete All", + "active_download": "Active Download", + "no_active_downloads": "No Active Downloads", + "active_downloads": "Active Downloads", "new_app_version_requires_re_download": "New app version requires re-download", "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", "back": "Back", "delete": "Delete", - "something_went_wrong": "Something went wrong", + "something_went_wrong": "Something Went Wrong", "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", "eta": "ETA {{eta}}", - "methods": "Methods", "toasts": { "you_are_not_allowed_to_download_files": "You are not allowed to download files.", - "deleted_all_movies_successfully": "Deleted all movies successfully!", - "failed_to_delete_all_movies": "Failed to delete all movies", - "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", - "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", - "download_deleted": "Download deleted", - "could_not_delete_download": "Could not delete download", - "download_paused": "Download paused", - "could_not_pause_download": "Could not pause download", - "download_resumed": "Download resumed", - "could_not_resume_download": "Could not resume download", - "download_completed": "Download completed", - "download_started_for": "Download started for {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", - "download_stated_for_item": "Download started for {{item}}", + "deleted_all_movies_successfully": "Deleted All Movies Successfully!", + "failed_to_delete_all_movies": "Failed to Delete All Movies", + "deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!", + "failed_to_delete_all_tvseries": "Failed to Delete All TV-Series", + "download_deleted": "Download Deleted", + "could_not_delete_download": "Could Not Delete Download", + "download_paused": "Download Paused", + "could_not_pause_download": "Could Not Pause Download", + "download_resumed": "Download Resumed", + "could_not_resume_download": "Could Not Resume Download", + "download_completed": "Download Completed", "download_failed_for_item": "Download failed for {{item}} - {{error}}", - "download_completed_for_item": "Download completed for {{item}}", - "queued_item_for_optimization": "Queued {{item}} for optimization", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", + "download_completed_for_item": "Download Completed for {{item}}", "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", - "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", - "go_to_downloads": "Go to downloads" + "go_to_downloads": "Go to Downloads" } } }, "search": { - "search_here": "Search here...", "search": "Search...", - "x_items": "{{count}} items", + "x_items": "{{count}} Items", "library": "Library", "discover": "Discover", - "no_results": "No results", - "no_results_found_for": "No results found for", + "no_results": "No Results", + "no_results_found_for": "No Results Found For", "movies": "Movies", "series": "Series", "episodes": "Episodes", "collections": "Collections", "actors": "Actors", "request_movies": "Request Movies", - "request_series": "Request Series", - "recently_added": "Recently Added", - "recent_requests": "Recent Requests", - "plex_watchlist": "Plex Watchlist", - "trending": "Trending", - "popular_movies": "Popular Movies", - "movie_genres": "Movie Genres", - "upcoming_movies": "Upcoming Movies", - "studios": "Studios", - "popular_tv": "Popular TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Upcoming TV", - "networks": "Networks", - "tmdb_movie_keyword": "TMDB Movie Keyword", - "tmdb_movie_genre": "TMDB Movie Genre", - "tmdb_tv_keyword": "TMDB TV Keyword", - "tmdb_tv_genre": "TMDB TV Genre", - "tmdb_search": "TMDB Search", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", - "tmdb_tv_streaming_services": "TMDB TV Streaming Services" + "request_series": "Request Series" }, "library": { - "no_items_found": "No items found", - "no_results": "No results", - "no_libraries_found": "No libraries found", + "no_results": "No Results", + "no_libraries_found": "No Libraries Found", "item_types": { - "movies": "movies", - "series": "series", - "boxsets": "box sets", - "items": "items" + "movies": "Movies", + "series": "Series", + "boxsets": "Box Sets", + "items": "Items" }, "options": { "display": "Display", "row": "Row", "list": "List", - "image_style": "Image style", + "image_style": "Image Style", "poster": "Poster", "cover": "Cover", - "show_titles": "Show titles", - "show_stats": "Show stats" + "show_titles": "Show Titles", + "show_stats": "Show Stats" }, "filters": { "genres": "Genres", "years": "Years", "sort_by": "Sort By", "sort_order": "Sort Order", - "asc": "Ascending", - "desc": "Descending", "tags": "Tags" } }, @@ -375,34 +320,30 @@ "videos": "Videos", "boxsets": "Boxsets", "playlists": "Playlists", - "noDataTitle": "No favorites yet", + "noDataTitle": "No Favorites Yet", "noData": "Mark items as favorites to see them appear here for quick access." }, "custom_links": { - "no_links": "No links" + "no_links": "No Links" }, "player": { "error": "Error", "failed_to_get_stream_url": "Failed to get the stream URL", "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", - "client_error": "Client error", + "client_error": "Client Error", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", - "message_from_server": "Message from server: {{message}}", - "video_has_finished_playing": "Video has finished playing!", - "no_video_source": "No video source...", + "message_from_server": "Message from Server: {{message}}", "next_episode": "Next Episode", "refresh_tracks": "Refresh Tracks", - "subtitle_tracks": "Subtitle Tracks:", "audio_tracks": "Audio Tracks:", "playback_state": "Playback State:", - "no_data_available": "No data available", "index": "Index:", "continue_watching": "Continue Watching", - "go_back": "Go back" + "go_back": "Go Back" }, "item_card": { - "next_up": "Next up", - "no_items_to_display": "No items to display", + "next_up": "Next Up", + "no_items_to_display": "No Items to Display", "cast_and_crew": "Cast & Crew", "series": "Series", "seasons": "Seasons", @@ -410,36 +351,33 @@ "no_episodes_for_this_season": "No episodes for this season", "overview": "Overview", "more_with": "More with {{name}}", - "similar_items": "Similar items", - "no_similar_items_found": "No similar items found", + "similar_items": "Similar Items", + "no_similar_items_found": "No Similar Items Found", "video": "Video", - "more_details": "More details", + "more_details": "More Details", "quality": "Quality", "audio": "Audio", "subtitles": "Subtitle", - "show_more": "Show more", - "show_less": "Show less", - "appeared_in": "Appeared in", - "could_not_load_item": "Could not load item", + "show_more": "Show More", + "show_less": "Show Less", + "appeared_in": "Appeared In", + "could_not_load_item": "Could Not Load Item", "none": "None", "download": { "download_season": "Download Season", "download_series": "Download Series", "download_episode": "Download Episode", "download_movie": "Download Movie", - "download_x_item": "Download {{item_count}} items", + "download_x_item": "Download {{item_count}} Items", "download_unwatched_only": "Unwatched Only", - "download_button": "Download", - "using_optimized_server": "Using optimized server", - "using_default_method": "Using default method" + "download_button": "Download" } }, "live_tv": { "next": "Next", "previous": "Previous", - "live_tv": "Live TV", - "coming_soon": "Coming soon", - "on_now": "On now", + "coming_soon": "Coming Soon", + "on_now": "On Now", "shows": "Shows", "movies": "Movies", "sports": "Sports", @@ -450,16 +388,16 @@ "confirm": "Confirm", "cancel": "Cancel", "yes": "Yes", - "whats_wrong": "What's wrong?", - "issue_type": "Issue type", - "select_an_issue": "Select an issue", + "whats_wrong": "What's Wrong?", + "issue_type": "Issue Type", + "select_an_issue": "Select an Issue", "types": "Types", - "describe_the_issue": "(optional) Describe the issue...", + "describe_the_issue": "(Optional) Describe the Issue...", "submit_button": "Submit", - "report_issue_button": "Report issue", + "report_issue_button": "Report Issue", "request_button": "Request", "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", - "failed_to_login": "Failed to login", + "failed_to_login": "Failed to Login", "cast": "Cast", "details": "Details", "status": "Status", @@ -474,13 +412,13 @@ "production_country": "Production Country", "studios": "Studios", "network": "Network", - "currently_streaming_on": "Currently Streaming on", + "currently_streaming_on": "Currently Streaming On", "advanced": "Advanced", "request_as": "Request As", "tags": "Tags", "quality_profile": "Quality Profile", "root_folder": "Root Folder", - "season_all": "Season (all)", + "season_all": "Season (All)", "season_number": "Season {{season_number}}", "number_episodes": "{{episode_number}} Episodes", "born": "Born", @@ -489,7 +427,7 @@ "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", - "issue_submitted": "Issue submitted!", + "issue_submitted": "Issue Submitted!", "requested_item": "Requested {{item}}!", "you_dont_have_permission_to_request": "You don't have permission to request!", "something_went_wrong_requesting_media": "Something went wrong requesting media!" From 2a195d9ba520b79d4e97c4b4826ed461b2aa5f3f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:15:46 +0200 Subject: [PATCH 31/33] chore(deps): Update actions/cache action to v4.3.0 (#1084) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-android.yml | 6 +++--- .github/workflows/build-ios.yml | 2 +- .github/workflows/check-lockfile.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 2b79e2dd..bc9b9ea1 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -39,7 +39,7 @@ jobs: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} @@ -53,7 +53,7 @@ jobs: bun run submodule-reload - name: ๐Ÿ’พ Cache Gradle global - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ~/.gradle/caches @@ -70,7 +70,7 @@ jobs: fi - name: ๐Ÿ’พ Cache project Gradle (.gradle) - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: android/.gradle key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index b7486da5..98b587b3 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -44,7 +44,7 @@ jobs: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index 6134ad5f..cc606b19 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -32,7 +32,7 @@ jobs: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ~/.bun/install/cache From ead37aa8065c288dd88ccbfdbd74d4dc42e02c38 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Thu, 25 Sep 2025 22:47:49 +0200 Subject: [PATCH 32/33] fix: resolve type issues and improve component reliability (#1078) --- .../jellyseerr/page.tsx | 26 ++++++--- components/common/JellyseerrItemRouter.tsx | 2 +- components/common/TouchableItemRouter.tsx | 55 +++++++++++++++++-- components/home/LargeMovieCarousel.tsx | 9 ++- components/inputs/PinInput.tsx | 14 ++++- components/jellyseerr/PersonPoster.tsx | 2 +- .../jellyseerr/discover/CompanySlide.tsx | 5 +- components/jellyseerr/discover/GenreSlide.tsx | 5 +- .../discover/RecentRequestsSlide.tsx | 19 ++++++- components/series/CastAndCrew.tsx | 18 +++--- components/series/SeasonEpisodesCarousel.tsx | 3 +- components/series/SeasonPicker.tsx | 5 +- components/settings/HomeIndex.tsx | 8 +-- components/video-player/controls/Controls.tsx | 3 +- .../controls/contexts/VideoContext.tsx | 3 +- .../controls/dropdown/DropdownView.tsx | 3 +- hooks/useJellyseerr.ts | 4 +- translations/en.json | 23 +++++++- utils/atoms/settings.ts | 1 + 19 files changed, 147 insertions(+), 61 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx index 7113c24f..3b076235 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -139,7 +139,15 @@ const Page: React.FC = () => { } requestMedia(mediaTitle, body, refetch); - }, [details, result, requestMedia, hasAdvancedRequestPermission]); + }, [ + details, + result, + requestMedia, + hasAdvancedRequestPermission, + mediaTitle, + refetch, + mediaType, + ]); const isAnime = useMemo( () => @@ -277,12 +285,16 @@ const Page: React.FC = () => {