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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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" && ( + + + + )} + + + ); +};