From a0a90e48d8e2018cc96ffa80953ed735749ee37c Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 30 Sep 2025 16:45:18 +0200 Subject: [PATCH] feat: home page design --- app/(auth)/(tabs)/(home)/_layout.tsx | 55 +++++++++ app/(auth)/(tabs)/(home)/index.tsx | 2 +- app/(auth)/(tabs)/(home)/settings.tsx | 2 +- components/PlayButton.tsx | 46 +++++++- .../AppleTVCarousel.tsx | 105 +++++++++++++++--- .../MarkAsPlayedLargeButton.tsx | 51 +++++++++ components/{settings => home}/HomeIndex.tsx | 2 +- 7 files changed, 242 insertions(+), 21 deletions(-) rename components/{ => apple-tv-carousel}/AppleTVCarousel.tsx (86%) create mode 100644 components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx rename components/{settings => home}/HomeIndex.tsx (99%) diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 5e43476d..1275f1a0 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -22,6 +22,11 @@ export default function IndexLayout() { options={{ headerShown: !Platform.isTV, headerTitle: t("tabs.home"), + headerLeft: () => ( + _router.back()} className='pl-0.5'> + + + ), headerBlurEffect: "none", headerTransparent: Platform.OS === "ios", headerShadowVisible: false, @@ -43,48 +48,88 @@ export default function IndexLayout() { name='downloads/index' options={{ title: t("home.downloads.downloads_title"), + headerLeft: () => ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), }} /> ( + _router.back()} className='pl-0.5'> + + + ), presentation: "modal", }} /> @@ -102,6 +152,11 @@ export default function IndexLayout() { name='collections/[collectionId]' options={{ title: "", + headerLeft: () => ( + _router.back()} className='pl-0.5'> + + + ), headerShown: true, headerBlurEffect: "prominent", headerTransparent: Platform.OS === "ios", diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index dc04e43b..63471ad2 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -1,4 +1,4 @@ -import { HomeIndex } from "@/components/settings/HomeIndex"; +import { HomeIndex } from "@/components/home/HomeIndex"; export default function page() { return ; diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 91f569df..387c4d99 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -46,7 +46,7 @@ export default function settings() { logout(); }} > - + {t("home.settings.log_out_button")} diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index 6ac1956e..58736de6 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -1,11 +1,12 @@ import { useActionSheet } from "@expo/react-native-action-sheet"; +import { Button, Host } from "@expo/ui/swift-ui"; import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useRouter } from "expo-router"; import { useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { Alert, TouchableOpacity, View } from "react-native"; +import { Alert, Platform, TouchableOpacity, View } from "react-native"; import CastContext, { CastButton, PlayServicesState, @@ -33,10 +34,9 @@ import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { chromecast } from "@/utils/profiles/chromecast"; import { chromecasth265 } from "@/utils/profiles/chromecasth265"; import { runtimeTicksToMinutes } from "@/utils/time"; -import type { Button } from "./Button"; import type { SelectedOptions } from "./ItemContent"; -interface Props extends React.ComponentProps { +interface Props extends React.ComponentProps { item: BaseItemDto; selectedOptions: SelectedOptions; isOffline?: boolean; @@ -364,6 +364,46 @@ export const PlayButton: React.FC = ({ * ********************* */ + if (Platform.OS === "ios") + return ( + + + + ); + return ( = ({ userId: user.Id, enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"], includeItemTypes: ["Movie", "Series", "Episode"], - fields: ["Genres"], + fields: ["Genres", "Overview"], limit: 2, }); return response.data.Items || []; @@ -183,7 +190,7 @@ export const AppleTVCarousel: React.FC = ({ if (!api || !user?.Id) return []; const response = await getTvShowsApi(api).getNextUp({ userId: user.Id, - fields: ["MediaSourceCount", "Genres"], + fields: ["MediaSourceCount", "Genres", "Overview"], limit: 2, enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"], enableResumable: false, @@ -202,7 +209,7 @@ export const AppleTVCarousel: React.FC = ({ const response = await getUserLibraryApi(api).getLatestMedia({ userId: user.Id, limit: 2, - fields: ["PrimaryImageAspectRatio", "Path", "Genres"], + fields: ["PrimaryImageAspectRatio", "Path", "Genres", "Overview"], imageTypeLimit: 1, enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"], }); @@ -348,6 +355,8 @@ export const AppleTVCarousel: React.FC = ({ }; }); + const togglePlayedStatus = useMarkAsPlayed(items); + const renderDots = () => { if (!hasItems || items.length <= 1) return null; @@ -473,6 +482,36 @@ export const AppleTVCarousel: React.FC = ({ /> + {/* Overview Skeleton */} + + + + + {/* Controls Skeleton */} = ({ + {/* Overview Section - for Episodes and Movies */} + {(item.Type === "Episode" || item.Type === "Movie") && + item.Overview && ( + + navigateToItem(item)}> + + {item.Overview} + + + + )} + {/* Controls Section */} = ({ {/* Mark as Played */} - + diff --git a/components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx b/components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx new file mode 100644 index 00000000..ea9bd98d --- /dev/null +++ b/components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx @@ -0,0 +1,51 @@ +import { Button, Host } from "@expo/ui/swift-ui"; +import { Ionicons } from "@expo/vector-icons"; +import { Platform, View } from "react-native"; +import { RoundButton } from "../RoundButton"; + +interface MarkAsPlayedLargeButtonProps { + isPlayed: boolean; + onToggle: (isPlayed: boolean) => void; +} + +export const MarkAsPlayedLargeButton: React.FC< + MarkAsPlayedLargeButtonProps +> = ({ isPlayed, onToggle }) => { + if (Platform.OS === "ios") + return ( + + + + ); + + return ( + + onToggle(isPlayed)} + /> + + ); +}; diff --git a/components/settings/HomeIndex.tsx b/components/home/HomeIndex.tsx similarity index 99% rename from components/settings/HomeIndex.tsx rename to components/home/HomeIndex.tsx index e1e08d09..d5237eae 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/home/HomeIndex.tsx @@ -37,7 +37,7 @@ import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; -import { AppleTVCarousel } from "../AppleTVCarousel"; +import { AppleTVCarousel } from "../apple-tv-carousel/AppleTVCarousel"; type ScrollingCollectionListSection = { type: "ScrollingCollectionList";