mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
feat: delete playlist
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
This commit is contained in:
@@ -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: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => setPlaylistOptionsOpen(true)}
|
||||
className='p-1.5'
|
||||
>
|
||||
<Ionicons name='ellipsis-horizontal' size={24} color='white' />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [playlist?.Name, navigation]);
|
||||
|
||||
@@ -299,6 +309,11 @@ export default function PlaylistDetailScreen() {
|
||||
setOpen={setCreatePlaylistOpen}
|
||||
initialTrackId={selectedTrack?.Id}
|
||||
/>
|
||||
<PlaylistOptionsSheet
|
||||
open={playlistOptionsOpen}
|
||||
setOpen={setPlaylistOptionsOpen}
|
||||
playlist={playlist}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
136
components/music/PlaylistOptionsSheet.tsx
Normal file
136
components/music/PlaylistOptionsSheet.tsx
Normal file
@@ -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<Props> = ({
|
||||
open,
|
||||
setOpen,
|
||||
playlist,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(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) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
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 (
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<View className='flex-col rounded-xl overflow-hidden bg-neutral-800'>
|
||||
<TouchableOpacity
|
||||
onPress={handleDeletePlaylist}
|
||||
className='flex-row items-center px-4 py-3.5'
|
||||
>
|
||||
<Ionicons name='trash-outline' size={22} color='#ef4444' />
|
||||
<Text className='text-red-500 ml-4 text-base'>
|
||||
{t("music.playlists.delete_playlist")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
};
|
||||
|
||||
const _styles = StyleSheet.create({
|
||||
separator: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: "#404040",
|
||||
},
|
||||
});
|
||||
@@ -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<void> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user