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 80a6dd8a..f21a013d 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 @@ -19,6 +19,7 @@ import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; import { CreatePlaylistModal } from "@/components/music/CreatePlaylistModal"; import { MusicTrackItem } from "@/components/music/MusicTrackItem"; +import { PlaylistOptionsSheet } from "@/components/music/PlaylistOptionsSheet"; import { PlaylistPickerSheet } from "@/components/music/PlaylistPickerSheet"; import { TrackOptionsSheet } from "@/components/music/TrackOptionsSheet"; import { useRemoveFromPlaylist } from "@/hooks/usePlaylistMutations"; @@ -45,6 +46,7 @@ export default function PlaylistDetailScreen() { const [trackOptionsOpen, setTrackOptionsOpen] = useState(false); const [playlistPickerOpen, setPlaylistPickerOpen] = useState(false); const [createPlaylistOpen, setCreatePlaylistOpen] = useState(false); + const [playlistOptionsOpen, setPlaylistOptionsOpen] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const removeFromPlaylist = useRemoveFromPlaylist(); @@ -101,6 +103,14 @@ export default function PlaylistDetailScreen() { headerTransparent: true, headerStyle: { backgroundColor: "transparent" }, headerShadowVisible: false, + headerRight: () => ( + setPlaylistOptionsOpen(true)} + className='p-1.5' + > + + + ), }); }, [playlist?.Name, navigation]); @@ -299,6 +309,11 @@ export default function PlaylistDetailScreen() { setOpen={setCreatePlaylistOpen} initialTrackId={selectedTrack?.Id} /> + } /> diff --git a/components/music/PlaylistOptionsSheet.tsx b/components/music/PlaylistOptionsSheet.tsx new file mode 100644 index 00000000..02d16cd2 --- /dev/null +++ b/components/music/PlaylistOptionsSheet.tsx @@ -0,0 +1,136 @@ +import { Ionicons } from "@expo/vector-icons"; +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetView, +} from "@gorhom/bottom-sheet"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useRouter } from "expo-router"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Alert, StyleSheet, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "@/components/common/Text"; +import { useDeletePlaylist } from "@/hooks/usePlaylistMutations"; + +interface Props { + open: boolean; + setOpen: (open: boolean) => void; + playlist: BaseItemDto | null; +} + +export const PlaylistOptionsSheet: React.FC = ({ + open, + setOpen, + playlist, +}) => { + const bottomSheetModalRef = useRef(null); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { t } = useTranslation(); + const deletePlaylist = useDeletePlaylist(); + + const snapPoints = useMemo(() => ["25%"], []); + + 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 handleDeletePlaylist = useCallback(() => { + if (!playlist?.Id) return; + + Alert.alert( + t("music.playlists.delete_playlist"), + t("music.playlists.delete_confirm", { name: playlist.Name }), + [ + { + text: t("common.cancel"), + style: "cancel", + }, + { + text: t("common.delete"), + style: "destructive", + onPress: () => { + deletePlaylist.mutate( + { playlistId: playlist.Id! }, + { + onSuccess: () => { + setOpen(false); + router.back(); + }, + }, + ); + }, + }, + ], + ); + }, [playlist, deletePlaylist, setOpen, router, t]); + + if (!playlist) return null; + + return ( + + + + + + + {t("music.playlists.delete_playlist")} + + + + + + ); +}; + +const _styles = StyleSheet.create({ + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: "#404040", + }, +}); diff --git a/hooks/usePlaylistMutations.ts b/hooks/usePlaylistMutations.ts index 9f35e660..7dca1f64 100644 --- a/hooks/usePlaylistMutations.ts +++ b/hooks/usePlaylistMutations.ts @@ -1,4 +1,4 @@ -import { getPlaylistsApi } from "@jellyfin/sdk/lib/utils/api"; +import { getLibraryApi, getPlaylistsApi } from "@jellyfin/sdk/lib/utils/api"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { useTranslation } from "react-i18next"; @@ -154,3 +154,40 @@ export const useRemoveFromPlaylist = () => { return mutation; }; + +/** + * Hook to delete a playlist + */ +export const useDeletePlaylist = () => { + const api = useAtomValue(apiAtom); + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + const mutation = useMutation({ + mutationFn: async ({ + playlistId, + }: { + playlistId: string; + }): Promise => { + if (!api) { + throw new Error("API not configured"); + } + + await getLibraryApi(api).deleteItem({ + itemId: playlistId, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["music-playlists"], + refetchType: "all", + }); + toast.success(t("music.playlists.deleted")); + }, + onError: (error: Error) => { + toast.error(error.message || t("music.playlists.failed_to_delete")); + }, + }); + + return mutation; +}; diff --git a/translations/en.json b/translations/en.json index 9261e248..098e6a1d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -411,7 +411,9 @@ "subtitle": "Subtitle", "play": "Play", "none": "None", - "track": "Track" + "track": "Track", + "cancel": "Cancel", + "delete": "Delete" }, "search": { "search": "Search...", @@ -673,7 +675,11 @@ "create_new": "Create New Playlist", "failed_to_add": "Failed to add to playlist", "failed_to_remove": "Failed to remove from playlist", - "failed_to_create": "Failed to create playlist" + "failed_to_create": "Failed to create playlist", + "delete_playlist": "Delete Playlist", + "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.", + "deleted": "Playlist deleted", + "failed_to_delete": "Failed to delete playlist" } }, "watchlists": {