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": {