From a26980ddab7d5eadbd5c66acb10f8f97143af5e6 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 5 Jan 2026 21:28:00 +0100 Subject: [PATCH] refactor: improve music design --- .../music/artist/[artistId].tsx | 2 +- .../music/playlist/[playlistId].tsx | 10 +- .../(libraries)/music/[libraryId]/_layout.tsx | 4 +- .../(libraries)/music/[libraryId]/albums.tsx | 28 +-- .../(libraries)/music/[libraryId]/artists.tsx | 26 +-- .../music/[libraryId]/playlists.tsx | 77 +++++--- .../music/[libraryId]/suggestions.tsx | 2 +- components/music/AnimatedEqualizer.tsx | 85 +++++++++ components/music/MusicAlbumCard.tsx | 2 +- components/music/MusicAlbumRowCard.tsx | 70 +++++++ components/music/MusicArtistCard.tsx | 19 +- components/music/MusicPlaylistCard.tsx | 81 ++++++-- components/music/MusicTrackItem.tsx | 52 +++--- components/music/PlaylistSortSheet.tsx | 173 ++++++++++++++++++ components/music/TrackOptionsSheet.tsx | 5 +- translations/en.json | 5 + 16 files changed, 497 insertions(+), 144 deletions(-) create mode 100644 components/music/AnimatedEqualizer.tsx create mode 100644 components/music/MusicAlbumRowCard.tsx create mode 100644 components/music/PlaylistSortSheet.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/artist/[artistId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/artist/[artistId].tsx index 8fb54202..fe2631e9 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/artist/[artistId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/artist/[artistId].tsx @@ -227,7 +227,7 @@ export default function ArtistDetailScreen() { {section.type === "albums" ? ( item.Id!} renderItem={(item) => } /> diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/playlist/[playlistId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/playlist/[playlistId].tsx index f21a013d..346e40aa 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/playlist/[playlistId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/music/playlist/[playlistId].tsx @@ -8,12 +8,7 @@ import { useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - ActivityIndicator, - Dimensions, - TouchableOpacity, - View, -} from "react-native"; +import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; @@ -30,8 +25,7 @@ import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { runtimeTicksToMinutes } from "@/utils/time"; -const { width: SCREEN_WIDTH } = Dimensions.get("window"); -const ARTWORK_SIZE = SCREEN_WIDTH * 0.5; +const ARTWORK_SIZE = 120; export default function PlaylistDetailScreen() { const { playlistId } = useLocalSearchParams<{ playlistId: string }>(); diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx index 94506c2c..69daf929 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx @@ -13,8 +13,7 @@ import { useTranslation } from "react-i18next"; const { Navigator } = createMaterialTopTabNavigator(); const TAB_LABEL_FONT_SIZE = 13; -const TAB_ITEM_HORIZONTAL_PADDING = 18; -const TAB_ITEM_MIN_WIDTH = 110; +const TAB_ITEM_HORIZONTAL_PADDING = 12; export const Tab = withLayoutContext< MaterialTopTabNavigationOptions, @@ -48,7 +47,6 @@ const Layout = () => { }, tabBarItemStyle: { width: "auto", - minWidth: TAB_ITEM_MIN_WIDTH, paddingHorizontal: TAB_ITEM_HORIZONTAL_PADDING, }, tabBarStyle: { backgroundColor: "black" }, diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx index 26688782..3fb5305b 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx @@ -6,11 +6,11 @@ import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Dimensions, RefreshControl, View } from "react-native"; +import { RefreshControl, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; -import { MusicAlbumCard } from "@/components/music/MusicAlbumCard"; +import { MusicAlbumRowCard } from "@/components/music/MusicAlbumRowCard"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; const ITEMS_PER_PAGE = 40; @@ -65,13 +65,6 @@ export default function AlbumsScreen() { return data?.pages.flatMap((page) => page.items) || []; }, [data]); - const numColumns = 2; - const screenWidth = Dimensions.get("window").width; - const gap = 12; - const padding = 16; - const itemWidth = - (screenWidth - padding * 2 - gap * (numColumns - 1)) / numColumns; - const handleEndReached = useCallback(() => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); @@ -98,11 +91,10 @@ export default function AlbumsScreen() { ( - - - - )} + renderItem={({ item }) => } keyExtractor={(item) => item.Id!} ListFooterComponent={ isFetchingNextPage ? ( diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx index 9afa2bbb..e8191404 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx @@ -6,7 +6,7 @@ import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Dimensions, RefreshControl, View } from "react-native"; +import { RefreshControl, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; @@ -71,13 +71,6 @@ export default function ArtistsScreen() { return data?.pages.flatMap((page) => page.items) || []; }, [data]); - const numColumns = 3; - const screenWidth = Dimensions.get("window").width; - const gap = 12; - const padding = 16; - const itemWidth = - (screenWidth - padding * 2 - gap * (numColumns - 1)) / numColumns; - const handleEndReached = useCallback(() => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); @@ -135,11 +128,10 @@ export default function ArtistsScreen() { ( - - - - )} + renderItem={({ item }) => } keyExtractor={(item) => item.Id!} ListFooterComponent={ isFetchingNextPage ? ( diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx index 3e8418d2..a03f9c3d 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx @@ -7,17 +7,17 @@ import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; import { useCallback, useLayoutEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Dimensions, - RefreshControl, - TouchableOpacity, - View, -} from "react-native"; +import { RefreshControl, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; import { CreatePlaylistModal } from "@/components/music/CreatePlaylistModal"; import { MusicPlaylistCard } from "@/components/music/MusicPlaylistCard"; +import { + type PlaylistSortOption, + type PlaylistSortOrder, + PlaylistSortSheet, +} from "@/components/music/PlaylistSortSheet"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; const ITEMS_PER_PAGE = 40; @@ -36,9 +36,20 @@ export default function PlaylistsScreen() { const { t } = useTranslation(); const [createModalOpen, setCreateModalOpen] = useState(false); + const [sortSheetOpen, setSortSheetOpen] = useState(false); + const [sortBy, setSortBy] = useState("SortName"); + const [sortOrder, setSortOrder] = useState("Ascending"); const isReady = Boolean(api && user?.Id && libraryId); + const handleSortChange = useCallback( + (newSortBy: PlaylistSortOption, newSortOrder: PlaylistSortOrder) => { + setSortBy(newSortBy); + setSortOrder(newSortOrder); + }, + [], + ); + useLayoutEffect(() => { navigation.setOptions({ headerRight: () => ( @@ -63,13 +74,13 @@ export default function PlaylistsScreen() { isFetchingNextPage, refetch, } = useInfiniteQuery({ - queryKey: ["music-playlists", libraryId, user?.Id], + queryKey: ["music-playlists", libraryId, user?.Id, sortBy, sortOrder], queryFn: async ({ pageParam = 0 }) => { const response = await getItemsApi(api!).getItems({ userId: user?.Id, includeItemTypes: ["Playlist"], - sortBy: ["SortName"], - sortOrder: ["Ascending"], + sortBy: [sortBy], + sortOrder: [sortOrder], limit: ITEMS_PER_PAGE, startIndex: pageParam, recursive: true, @@ -93,13 +104,6 @@ export default function PlaylistsScreen() { return data?.pages.flatMap((page) => page.items) || []; }, [data]); - const numColumns = 2; - const screenWidth = Dimensions.get("window").width; - const gap = 12; - const padding = 16; - const itemWidth = - (screenWidth - padding * 2 - gap * (numColumns - 1)) / numColumns; - const handleEndReached = useCallback(() => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); @@ -171,11 +175,10 @@ export default function PlaylistsScreen() { ( - setSortSheetOpen(true)} + className='flex-row items-center mb-2 py-1' > - - - )} + + + {t( + `music.sort.${sortBy === "SortName" ? "alphabetical" : "date_created"}`, + )} + + + + } + renderItem={({ item }) => } keyExtractor={(item) => item.Id!} ListFooterComponent={ isFetchingNextPage ? ( @@ -210,6 +222,13 @@ export default function PlaylistsScreen() { open={createModalOpen} setOpen={setCreateModalOpen} /> + ); } diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx index 21323a60..fb762862 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx @@ -290,7 +290,7 @@ export default function SuggestionsScreen() { {section.type === "albums" ? ( item.Id!} renderItem={(item) => } /> diff --git a/components/music/AnimatedEqualizer.tsx b/components/music/AnimatedEqualizer.tsx new file mode 100644 index 00000000..65cc141a --- /dev/null +++ b/components/music/AnimatedEqualizer.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useRef } from "react"; +import { Animated, Easing, View } from "react-native"; + +interface Props { + color?: string; + barWidth?: number; + barCount?: number; + height?: number; + gap?: number; +} + +export const AnimatedEqualizer: React.FC = ({ + color = "#9334E9", + barWidth = 3, + barCount = 3, + height = 12, + gap = 2, +}) => { + const animations = useRef( + Array.from({ length: barCount }, () => new Animated.Value(0)), + ).current; + + useEffect(() => { + const durations = [600, 700, 550]; + const minScale = [0.2, 0.3, 0.25]; + const maxScale = [1, 0.85, 0.95]; + + // Set initial staggered values + animations.forEach((anim, index) => { + anim.setValue(index === 1 ? 0.8 : index === 2 ? 0.4 : 0.2); + }); + + const barAnimations = animations.map((anim, index) => { + return Animated.loop( + Animated.sequence([ + Animated.timing(anim, { + toValue: maxScale[index % maxScale.length], + duration: durations[index % durations.length], + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(anim, { + toValue: minScale[index % minScale.length], + duration: durations[index % durations.length], + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + ]), + ); + }); + + Animated.parallel(barAnimations).start(); + + return () => { + animations.forEach((anim) => { + anim.stopAnimation(); + }); + }; + }, [animations]); + + return ( + + {animations.map((anim, index) => ( + + ))} + + ); +}; diff --git a/components/music/MusicAlbumCard.tsx b/components/music/MusicAlbumCard.tsx index 0efb0082..a4c9f780 100644 --- a/components/music/MusicAlbumCard.tsx +++ b/components/music/MusicAlbumCard.tsx @@ -13,7 +13,7 @@ interface Props { width?: number; } -export const MusicAlbumCard: React.FC = ({ album, width = 150 }) => { +export const MusicAlbumCard: React.FC = ({ album, width = 130 }) => { const [api] = useAtom(apiAtom); const router = useRouter(); diff --git a/components/music/MusicAlbumRowCard.tsx b/components/music/MusicAlbumRowCard.tsx new file mode 100644 index 00000000..4b08700a --- /dev/null +++ b/components/music/MusicAlbumRowCard.tsx @@ -0,0 +1,70 @@ +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Image } from "expo-image"; +import { useRouter } from "expo-router"; +import { useAtom } from "jotai"; +import React, { useCallback, useMemo } from "react"; +import { TouchableOpacity, View } from "react-native"; +import { Text } from "@/components/common/Text"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; + +interface Props { + album: BaseItemDto; +} + +const IMAGE_SIZE = 56; + +export const MusicAlbumRowCard: React.FC = ({ album }) => { + const [api] = useAtom(apiAtom); + const router = useRouter(); + + const imageUrl = useMemo( + () => getPrimaryImageUrl({ api, item: album }), + [api, album], + ); + + const handlePress = useCallback(() => { + router.push({ + pathname: "/music/album/[albumId]", + params: { albumId: album.Id! }, + }); + }, [router, album.Id]); + + return ( + + + {imageUrl ? ( + + ) : ( + + 🎵 + + )} + + + + {album.Name} + + + {album.AlbumArtist || album.Artists?.join(", ")} + + + + ); +}; diff --git a/components/music/MusicArtistCard.tsx b/components/music/MusicArtistCard.tsx index 22637fb5..e62edf80 100644 --- a/components/music/MusicArtistCard.tsx +++ b/components/music/MusicArtistCard.tsx @@ -13,7 +13,9 @@ interface Props { size?: number; } -export const MusicArtistCard: React.FC = ({ artist, size = 100 }) => { +const IMAGE_SIZE = 48; + +export const MusicArtistCard: React.FC = ({ artist }) => { const [api] = useAtom(apiAtom); const router = useRouter(); @@ -32,14 +34,13 @@ export const MusicArtistCard: React.FC = ({ artist, size = 100 }) => { return ( = ({ artist, size = 100 }) => { /> ) : ( - 👤 + 👤 )} {artist.Name} diff --git a/components/music/MusicPlaylistCard.tsx b/components/music/MusicPlaylistCard.tsx index b158be49..6aa3387c 100644 --- a/components/music/MusicPlaylistCard.tsx +++ b/components/music/MusicPlaylistCard.tsx @@ -1,11 +1,15 @@ +import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo } from "react"; import { TouchableOpacity, View } from "react-native"; import { Text } from "@/components/common/Text"; -import { apiAtom } from "@/providers/JellyfinProvider"; +import { getLocalPath } from "@/providers/AudioStorage"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; interface Props { @@ -13,11 +17,11 @@ interface Props { width?: number; } -export const MusicPlaylistCard: React.FC = ({ - playlist, - width = 150, -}) => { +const IMAGE_SIZE = 56; + +export const MusicPlaylistCard: React.FC = ({ playlist }) => { const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); const router = useRouter(); const imageUrl = useMemo( @@ -25,6 +29,37 @@ export const MusicPlaylistCard: React.FC = ({ [api, playlist], ); + // Fetch playlist tracks to check download status + const { data: tracks } = useQuery({ + queryKey: ["playlist-tracks-status", playlist.Id, user?.Id], + queryFn: async () => { + const response = await getItemsApi(api!).getItems({ + userId: user?.Id, + parentId: playlist.Id, + fields: ["MediaSources"], + }); + return response.data.Items || []; + }, + enabled: !!api && !!user?.Id && !!playlist.Id, + staleTime: 5 * 60 * 1000, // 5 minutes + }); + + // Calculate download status + const downloadStatus = useMemo(() => { + if (!tracks || tracks.length === 0) { + return { downloaded: 0, total: playlist.ChildCount || 0 }; + } + const downloaded = tracks.filter( + (track) => !!getLocalPath(track.Id), + ).length; + return { downloaded, total: tracks.length }; + }, [tracks, playlist.ChildCount]); + + const allDownloaded = + downloadStatus.total > 0 && + downloadStatus.downloaded === downloadStatus.total; + const hasDownloads = downloadStatus.downloaded > 0; + const handlePress = useCallback(() => { router.push({ pathname: "/music/playlist/[playlistId]", @@ -35,13 +70,12 @@ export const MusicPlaylistCard: React.FC = ({ return ( = ({ /> ) : ( - 🎶 + 🎶 )} - - {playlist.Name} - - - {playlist.ChildCount} tracks - + + + {playlist.Name} + + + {playlist.ChildCount} tracks + + + {/* Download status indicator */} + {allDownloaded ? ( + + ) : hasDownloads ? ( + + {downloadStatus.downloaded}/{downloadStatus.total} + + ) : null} ); }; diff --git a/components/music/MusicTrackItem.tsx b/components/music/MusicTrackItem.tsx index 190b52ad..fd26e1a5 100644 --- a/components/music/MusicTrackItem.tsx +++ b/components/music/MusicTrackItem.tsx @@ -5,6 +5,7 @@ import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { Text } from "@/components/common/Text"; +import { AnimatedEqualizer } from "@/components/music/AnimatedEqualizer"; import { useNetworkStatus } from "@/hooks/useNetworkStatus"; import { audioStorageEvents, @@ -27,7 +28,7 @@ interface Props { export const MusicTrackItem: React.FC = ({ track, - index, + index: _index, queue, showArtwork = true, onOptionsPress, @@ -118,24 +119,15 @@ export const MusicTrackItem: React.FC = ({ onLongPress={handleLongPress} delayLongPress={300} disabled={isUnavailableOffline} - className={`flex flex-row items-center py-3 ${isCurrentTrack ? "bg-purple-900/20" : ""}`} + className={`flex-row items-center py-1.5 pl-4 pr-3 ${isCurrentTrack ? "bg-purple-900/20" : ""}`} style={isUnavailableOffline ? { opacity: 0.5 } : undefined} > - {index !== undefined && ( - - {isCurrentTrack && isPlaying ? ( - - ) : ( - {index} - )} - - )} - + {/* Album artwork */} {showArtwork && ( = ({ /> ) : ( - + )} {isTrackLoading && ( @@ -173,20 +165,22 @@ export const MusicTrackItem: React.FC = ({ )} + {/* Track info */} - - {track.Name} - - + + {isCurrentTrack && isPlaying && } + + {track.Name} + + + {track.Artists?.join(", ") || track.AlbumArtist} - {duration} - {/* Download status indicator */} {downloadStatus === "downloading" && ( = ({ {downloadStatus === "downloaded" && ( )} + {/* Duration */} + {duration} + + {/* Options button */} {onOptionsPress && ( - + )} diff --git a/components/music/PlaylistSortSheet.tsx b/components/music/PlaylistSortSheet.tsx new file mode 100644 index 00000000..07c8467c --- /dev/null +++ b/components/music/PlaylistSortSheet.tsx @@ -0,0 +1,173 @@ +import { Ionicons } from "@expo/vector-icons"; +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetView, +} from "@gorhom/bottom-sheet"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "@/components/common/Text"; + +export type PlaylistSortOption = "SortName" | "DateCreated"; + +export type PlaylistSortOrder = "Ascending" | "Descending"; + +interface Props { + open: boolean; + setOpen: (open: boolean) => void; + sortBy: PlaylistSortOption; + sortOrder: PlaylistSortOrder; + onSortChange: ( + sortBy: PlaylistSortOption, + sortOrder: PlaylistSortOrder, + ) => void; +} + +const SORT_OPTIONS: { key: PlaylistSortOption; label: string; icon: string }[] = + [ + { key: "SortName", label: "music.sort.alphabetical", icon: "text-outline" }, + { + key: "DateCreated", + label: "music.sort.date_created", + icon: "time-outline", + }, + ]; + +export const PlaylistSortSheet: React.FC = ({ + open, + setOpen, + sortBy, + sortOrder, + onSortChange, +}) => { + const bottomSheetModalRef = useRef(null); + const insets = useSafeAreaInsets(); + const { t } = useTranslation(); + + const snapPoints = useMemo(() => ["40%"], []); + + useEffect(() => { + if (open) bottomSheetModalRef.current?.present(); + else bottomSheetModalRef.current?.dismiss(); + }, [open]); + + const handleSheetChanges = useCallback( + (index: number) => { + if (index === -1) { + setOpen(false); + } + }, + [setOpen], + ); + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [], + ); + + const handleSortSelect = useCallback( + (option: PlaylistSortOption) => { + // If selecting same option, toggle order; otherwise use sensible default + if (option === sortBy) { + onSortChange( + option, + sortOrder === "Ascending" ? "Descending" : "Ascending", + ); + } else { + // Default order based on sort type + const defaultOrder: PlaylistSortOrder = + option === "SortName" ? "Ascending" : "Descending"; + onSortChange(option, defaultOrder); + } + setOpen(false); + }, + [sortBy, sortOrder, onSortChange, setOpen], + ); + + return ( + + + + {t("music.sort.title")} + + + {SORT_OPTIONS.map((option, index) => { + const isSelected = sortBy === option.key; + return ( + + {index > 0 && } + handleSortSelect(option.key)} + className='flex-row items-center px-4 py-3.5' + > + + + {t(option.label)} + + {isSelected && ( + + + + + )} + + + ); + })} + + + + ); +}; + +const styles = StyleSheet.create({ + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: "#404040", + }, +}); diff --git a/components/music/TrackOptionsSheet.tsx b/components/music/TrackOptionsSheet.tsx index b0f5b91b..c452c2fe 100644 --- a/components/music/TrackOptionsSheet.tsx +++ b/components/music/TrackOptionsSheet.tsx @@ -85,8 +85,6 @@ export const TrackOptionsSheet: React.FC = ({ track ?? ({ Id: "", UserData: { IsFavorite: false } } as BaseItemDto), ); - const snapPoints = useMemo(() => ["65%"], []); - // Check download status (storageUpdateCounter triggers re-evaluation when download completes) const isAlreadyDownloaded = useMemo( () => isPermanentlyDownloaded(track?.Id), @@ -220,8 +218,7 @@ export const TrackOptionsSheet: React.FC = ({ return (