mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 08:14:42 +01:00
refactor: improve music design
This commit is contained in:
85
components/music/AnimatedEqualizer.tsx
Normal file
85
components/music/AnimatedEqualizer.tsx
Normal file
@@ -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<Props> = ({
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
height,
|
||||
gap,
|
||||
marginRight: 6,
|
||||
}}
|
||||
>
|
||||
{animations.map((anim, index) => (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={{
|
||||
width: barWidth,
|
||||
height,
|
||||
backgroundColor: color,
|
||||
borderRadius: 1,
|
||||
transform: [{ scaleY: anim }],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const MusicAlbumCard: React.FC<Props> = ({ album, width = 150 }) => {
|
||||
export const MusicAlbumCard: React.FC<Props> = ({ album, width = 130 }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
70
components/music/MusicAlbumRowCard.tsx
Normal file
70
components/music/MusicAlbumRowCard.tsx
Normal file
@@ -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<Props> = ({ 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 (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className='flex-row items-center py-2'
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: IMAGE_SIZE,
|
||||
height: IMAGE_SIZE,
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<Image
|
||||
source={{ uri: imageUrl }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
contentFit='cover'
|
||||
cachePolicy='memory-disk'
|
||||
/>
|
||||
) : (
|
||||
<View className='flex-1 items-center justify-center bg-neutral-800'>
|
||||
<Text className='text-2xl'>🎵</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='flex-1 ml-3'>
|
||||
<Text numberOfLines={1} className='text-white text-base font-medium'>
|
||||
{album.Name}
|
||||
</Text>
|
||||
<Text numberOfLines={1} className='text-neutral-400 text-sm mt-0.5'>
|
||||
{album.AlbumArtist || album.Artists?.join(", ")}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,9 @@ interface Props {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const MusicArtistCard: React.FC<Props> = ({ artist, size = 100 }) => {
|
||||
const IMAGE_SIZE = 48;
|
||||
|
||||
export const MusicArtistCard: React.FC<Props> = ({ artist }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const router = useRouter();
|
||||
|
||||
@@ -32,14 +34,13 @@ export const MusicArtistCard: React.FC<Props> = ({ artist, size = 100 }) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={{ width: size }}
|
||||
className='flex flex-col items-center'
|
||||
className='flex-row items-center py-2'
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
width: IMAGE_SIZE,
|
||||
height: IMAGE_SIZE,
|
||||
borderRadius: IMAGE_SIZE / 2,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#1a1a1a",
|
||||
}}
|
||||
@@ -53,13 +54,13 @@ export const MusicArtistCard: React.FC<Props> = ({ artist, size = 100 }) => {
|
||||
/>
|
||||
) : (
|
||||
<View className='flex-1 items-center justify-center bg-neutral-800'>
|
||||
<Text className='text-3xl'>👤</Text>
|
||||
<Text className='text-xl'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
className='text-white text-xs font-medium mt-2 text-center'
|
||||
numberOfLines={1}
|
||||
className='text-white text-base font-medium ml-3 flex-1'
|
||||
>
|
||||
{artist.Name}
|
||||
</Text>
|
||||
|
||||
@@ -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<Props> = ({
|
||||
playlist,
|
||||
width = 150,
|
||||
}) => {
|
||||
const IMAGE_SIZE = 56;
|
||||
|
||||
export const MusicPlaylistCard: React.FC<Props> = ({ playlist }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const router = useRouter();
|
||||
|
||||
const imageUrl = useMemo(
|
||||
@@ -25,6 +29,37 @@ export const MusicPlaylistCard: React.FC<Props> = ({
|
||||
[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<Props> = ({
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={{ width }}
|
||||
className='flex flex-col'
|
||||
className='flex-row items-center py-2'
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width,
|
||||
height: width,
|
||||
width: IMAGE_SIZE,
|
||||
height: IMAGE_SIZE,
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#1a1a1a",
|
||||
@@ -56,16 +90,31 @@ export const MusicPlaylistCard: React.FC<Props> = ({
|
||||
/>
|
||||
) : (
|
||||
<View className='flex-1 items-center justify-center bg-neutral-800'>
|
||||
<Text className='text-4xl'>🎶</Text>
|
||||
<Text className='text-2xl'>🎶</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text numberOfLines={1} className='text-white text-sm font-medium mt-2'>
|
||||
{playlist.Name}
|
||||
</Text>
|
||||
<Text numberOfLines={1} className='text-neutral-400 text-xs'>
|
||||
{playlist.ChildCount} tracks
|
||||
</Text>
|
||||
<View className='flex-1 ml-3'>
|
||||
<Text numberOfLines={1} className='text-white text-base font-medium'>
|
||||
{playlist.Name}
|
||||
</Text>
|
||||
<Text numberOfLines={1} className='text-neutral-400 text-sm mt-0.5'>
|
||||
{playlist.ChildCount} tracks
|
||||
</Text>
|
||||
</View>
|
||||
{/* Download status indicator */}
|
||||
{allDownloaded ? (
|
||||
<Ionicons
|
||||
name='checkmark-circle'
|
||||
size={18}
|
||||
color='#22c55e'
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
) : hasDownloads ? (
|
||||
<Text className='text-neutral-500 text-xs mr-1'>
|
||||
{downloadStatus.downloaded}/{downloadStatus.total}
|
||||
</Text>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Props> = ({
|
||||
track,
|
||||
index,
|
||||
index: _index,
|
||||
queue,
|
||||
showArtwork = true,
|
||||
onOptionsPress,
|
||||
@@ -118,24 +119,15 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
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 && (
|
||||
<View className='w-8 items-center'>
|
||||
{isCurrentTrack && isPlaying ? (
|
||||
<Ionicons name='musical-note' size={16} color='#9334E9' />
|
||||
) : (
|
||||
<Text className='text-neutral-500 text-sm'>{index}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Album artwork */}
|
||||
{showArtwork && (
|
||||
<View
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#1a1a1a",
|
||||
@@ -151,7 +143,7 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
/>
|
||||
) : (
|
||||
<View className='flex-1 items-center justify-center bg-neutral-800'>
|
||||
<Ionicons name='musical-note' size={20} color='#737373' />
|
||||
<Ionicons name='musical-note' size={18} color='#737373' />
|
||||
</View>
|
||||
)}
|
||||
{isTrackLoading && (
|
||||
@@ -173,20 +165,22 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Track info */}
|
||||
<View className='flex-1 mr-3'>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className={`text-sm ${isCurrentTrack ? "text-purple-400 font-medium" : "text-white"}`}
|
||||
>
|
||||
{track.Name}
|
||||
</Text>
|
||||
<Text numberOfLines={1} className='text-neutral-400 text-xs mt-0.5'>
|
||||
<View className='flex-row items-center'>
|
||||
{isCurrentTrack && isPlaying && <AnimatedEqualizer />}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className={`flex-1 text-sm ${isCurrentTrack ? "text-purple-400 font-medium" : "text-white"}`}
|
||||
>
|
||||
{track.Name}
|
||||
</Text>
|
||||
</View>
|
||||
<Text numberOfLines={1} className='text-neutral-500 text-xs mt-0.5'>
|
||||
{track.Artists?.join(", ") || track.AlbumArtist}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className='text-neutral-500 text-xs mr-2'>{duration}</Text>
|
||||
|
||||
{/* Download status indicator */}
|
||||
{downloadStatus === "downloading" && (
|
||||
<ActivityIndicator
|
||||
@@ -198,19 +192,23 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
{downloadStatus === "downloaded" && (
|
||||
<Ionicons
|
||||
name='checkmark-circle'
|
||||
size={16}
|
||||
size={14}
|
||||
color='#22c55e'
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Duration */}
|
||||
<Text className='text-neutral-500 text-xs'>{duration}</Text>
|
||||
|
||||
{/* Options button */}
|
||||
{onOptionsPress && (
|
||||
<TouchableOpacity
|
||||
onPress={handleOptionsPress}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
className='p-1'
|
||||
className='pl-3 py-1'
|
||||
>
|
||||
<Ionicons name='ellipsis-vertical' size={18} color='#737373' />
|
||||
<Ionicons name='ellipsis-vertical' size={16} color='#737373' />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
173
components/music/PlaylistSortSheet.tsx
Normal file
173
components/music/PlaylistSortSheet.tsx
Normal file
@@ -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<Props> = ({
|
||||
open,
|
||||
setOpen,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onSortChange,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(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) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
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 (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingLeft: Math.max(16, insets.left),
|
||||
paddingRight: Math.max(16, insets.right),
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
<Text className='text-white text-lg font-semibold mb-4'>
|
||||
{t("music.sort.title")}
|
||||
</Text>
|
||||
<View className='flex-col rounded-xl overflow-hidden bg-neutral-800'>
|
||||
{SORT_OPTIONS.map((option, index) => {
|
||||
const isSelected = sortBy === option.key;
|
||||
return (
|
||||
<React.Fragment key={option.key}>
|
||||
{index > 0 && <View style={styles.separator} />}
|
||||
<TouchableOpacity
|
||||
onPress={() => handleSortSelect(option.key)}
|
||||
className='flex-row items-center px-4 py-3.5'
|
||||
>
|
||||
<Ionicons
|
||||
name={option.icon as any}
|
||||
size={22}
|
||||
color={isSelected ? "#9334E9" : "#fff"}
|
||||
/>
|
||||
<Text
|
||||
className={`ml-4 text-base flex-1 ${isSelected ? "text-purple-500 font-medium" : "text-white"}`}
|
||||
>
|
||||
{t(option.label)}
|
||||
</Text>
|
||||
{isSelected && (
|
||||
<View className='flex-row items-center'>
|
||||
<Ionicons
|
||||
name={
|
||||
sortOrder === "Ascending" ? "arrow-up" : "arrow-down"
|
||||
}
|
||||
size={18}
|
||||
color='#9334E9'
|
||||
/>
|
||||
<Ionicons
|
||||
name='checkmark'
|
||||
size={22}
|
||||
color='#9334E9'
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
separator: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: "#404040",
|
||||
},
|
||||
});
|
||||
@@ -85,8 +85,6 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
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<Props> = ({
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
enableDynamicSizing
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
|
||||
Reference in New Issue
Block a user