feat: Add deleteVideos function in VideoContext

This commit is contained in:
Peifan Li
2025-12-23 13:28:08 -05:00
parent 98f1902ef4
commit faf09f4958
15 changed files with 136 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ interface VideoContextType {
error: string | null;
fetchVideos: () => Promise<void>;
deleteVideo: (id: string, options?: { showSnackbar?: boolean }) => Promise<{ success: boolean; error?: string }>;
deleteVideos: (ids: string[]) => Promise<{ success: boolean; error?: string }>;
updateVideo: (id: string, updates: Partial<Video>) => Promise<{ success: boolean; error?: string }>;
refreshThumbnail: (id: string) => Promise<{ success: boolean; error?: string }>;
searchLocalVideos: (query: string) => Video[];
@@ -138,6 +139,35 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
}
};
const deleteVideos = async (ids: string[]) => {
try {
// Delete videos sequentially to avoid overwhelming the server
// or we could implement a batch delete API endpoint if available, but for now loop client-side
let successCount = 0;
let failCount = 0;
for (const id of ids) {
try {
await deleteVideoMutation.mutateAsync({ id, options: { showSnackbar: false } });
successCount++;
} catch (error) {
console.error(`Failed to delete video ${id}:`, error);
failCount++;
}
}
if (failCount === 0) {
showSnackbar(t('deleteFilteredVideosSuccess', { count: successCount }));
return { success: true };
} else {
showSnackbar(`${t('deleteFilteredVideosSuccess', { count: successCount })} (${failCount} failed)`);
return { success: failCount === 0 }; // Consider partial success as success? strict: fail if any fail
}
} catch (error) {
return { success: false, error: t('failedToDeleteVideo') };
}
};
const searchLocalVideos = (query: string) => {
if (!query || !videos.length) return [];
const searchTermLower = query.toLowerCase();
@@ -360,6 +390,7 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
error: videosError ? (videosError as Error).message : null,
fetchVideos,
deleteVideo,
deleteVideos,
updateVideo,
refreshThumbnail,
searchLocalVideos,

View File

@@ -153,6 +153,7 @@ const AuthorVideos: React.FC = () => {
title={t('deleteAuthor')}
message={t('deleteAuthorConfirmation', { author: author || '' })}
confirmText={isDeleting ? t('deleting') : t('delete')}
cancelText={t('cancel')}
isDanger={true}
/>
</Container>

View File

@@ -1,5 +1,5 @@
import { Collections as CollectionsIcon, GridView, History, ViewSidebar } from '@mui/icons-material';
import { Collections as CollectionsIcon, Delete as DeleteIcon, GridView, History, ViewSidebar } from '@mui/icons-material';
import {
Alert,
Box,
@@ -8,9 +8,11 @@ import {
Collapse,
Container,
Grid,
IconButton,
Pagination,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography
} from '@mui/material';
import axios from 'axios';
@@ -20,6 +22,7 @@ import { VirtuosoGrid } from 'react-virtuoso';
import AuthorsList from '../components/AuthorsList';
import CollectionCard from '../components/CollectionCard';
import Collections from '../components/Collections';
import ConfirmationModal from '../components/ConfirmationModal';
import SortControl from '../components/SortControl';
import TagsList from '../components/TagsList';
import VideoCard from '../components/VideoCard';
@@ -39,7 +42,8 @@ const Home: React.FC = () => {
availableTags,
selectedTags,
handleTagToggle,
deleteVideo
deleteVideo,
deleteVideos
} = useVideo();
const { collections } = useCollection();
@@ -58,6 +62,7 @@ const Home: React.FC = () => {
const [settingsLoaded, setSettingsLoaded] = useState(false);
const [infiniteScroll, setInfiniteScroll] = useState(false);
const [videoColumns, setVideoColumns] = useState(4);
const [isDeleteFilteredOpen, setIsDeleteFilteredOpen] = useState(false);
// Determine Grid props based on sidebar and columns settings
// Hoisted memoization to be used by both specialized and paginated views
@@ -353,6 +358,38 @@ const Home: React.FC = () => {
// Regular home view (not in search mode)
return (
<Container maxWidth={false} sx={{ py: 4, px: { xs: 0, sm: 3 } }}>
{/* Delete Filtered Videos Modal */}
<ConfirmationModal
isOpen={isDeleteFilteredOpen}
onClose={() => setIsDeleteFilteredOpen(false)}
onConfirm={async () => {
const videosToDelete = videoArray.filter(video => {
if (selectedTags.length === 0) return false;
const videoTags = video.tags || [];
return selectedTags.every(tag => videoTags.includes(tag));
});
if (videosToDelete.length > 0) {
await deleteVideos(videosToDelete.map(v => v.id));
// Optionally clear tags after delete, or keep them? Keeping them might show empty list.
// Let's keep them for now, user can clear if they want.
// Actually, better to clear tags if all videos are gone?
// No, simpler is better.
}
}}
title={t('deleteAllFilteredVideos')}
message={t('confirmDeleteFilteredVideos', {
count: videoArray.filter(video => {
if (selectedTags.length === 0) return false;
const videoTags = video.tags || [];
return selectedTags.every(tag => videoTags.includes(tag));
}).length
})}
confirmText={t('delete')}
cancelText={t('cancel')}
isDanger={true}
/>
{videoArray.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h5" color="text.secondary">
@@ -423,6 +460,18 @@ const Home: React.FC = () => {
<Box component="span" sx={{ display: { xs: 'none', md: 'block' } }}>
{t('videos')}
</Box>
{selectedTags.length > 0 && (
<Tooltip title={t('deleteAllFilteredVideos')}>
<IconButton
color="error"
onClick={() => setIsDeleteFilteredOpen(true)}
size="small"
sx={{ ml: 1 }}
>
<DeleteIcon />
</IconButton>
</Tooltip>
)}
<Box component="span" sx={{ display: { xs: 'block', md: 'none' } }}>
{{
'collections': t('collections'),

View File

@@ -150,6 +150,7 @@ const SubscriptionsPage: React.FC = () => {
title={t('unsubscribe')}
message={t('confirmUnsubscribe', { author: selectedSubscription?.author || '' })}
confirmText={t('unsubscribe')}
cancelText={t('cancel')}
isDanger
/>
</Container >

View File

@@ -259,6 +259,10 @@ export const ar = {
searchCancelled: "تم إلغاء البحث",
openInExternalPlayer: "فتح في مشغل خارجي",
playWith: "تشغيل بواسطة...",
deleteAllFilteredVideos: "حذف جميع الفيديوهات المصفاة",
confirmDeleteFilteredVideos: "هل أنت متأكد أنك تريد حذف {count} فيديو مصفى بواسطة العلامات المحددة؟",
deleteFilteredVideosSuccess: "تم حذف {count} فيديو بنجاح.",
deletingVideos: "جاري حذف الفيديوهات...",
// Login
signIn: "تسجيل الدخول",

View File

@@ -254,6 +254,10 @@ export const de = {
searchCancelled: "Suche abgebrochen",
openInExternalPlayer: "In externem Player öffnen",
playWith: "Abspielen mit...",
deleteAllFilteredVideos: "Alle gefilterten Videos löschen",
confirmDeleteFilteredVideos: "Sind Sie sicher, dass Sie {count} Videos löschen möchten, die nach den ausgewählten Tags gefiltert wurden?",
deleteFilteredVideosSuccess: "Erfolgreich {count} Videos gelöscht.",
deletingVideos: "Videos werden gelöscht...",
signIn: "Anmelden",
verifying: "Überprüfen...",
incorrectPassword: "Falsches Passwort",

View File

@@ -255,6 +255,10 @@ export const en = {
searchCancelled: "Search was cancelled",
openInExternalPlayer: "Open in external player",
playWith: "Play with...",
deleteAllFilteredVideos: "Delete All Filtered Videos",
confirmDeleteFilteredVideos: "Are you sure you want to delete {count} videos filtered by the selected tags?",
deleteFilteredVideosSuccess: "Successfully deleted {count} videos.",
deletingVideos: "Deleting videos...",
// Login
signIn: "Sign in",

View File

@@ -279,6 +279,10 @@ export const es = {
searchCancelled: "Búsqueda cancelada",
openInExternalPlayer: "Abrir en reproductor externo",
playWith: "Reproducir con...",
deleteAllFilteredVideos: "Eliminar todos los videos filtrados",
confirmDeleteFilteredVideos: "¿Está seguro de que desea eliminar {count} videos filtrados por las etiquetas seleccionadas?",
deleteFilteredVideosSuccess: "Se han eliminado {count} videos con éxito.",
deletingVideos: "Eliminando videos...",
signIn: "Iniciar Sesión",
verifying: "Verificando...",
incorrectPassword: "Contraseña incorrecta",

View File

@@ -278,6 +278,10 @@ export const fr = {
searchCancelled: "Recherche annulée",
openInExternalPlayer: "Ouvrir dans un lecteur externe",
playWith: "Lire avec...",
deleteAllFilteredVideos: "Supprimer toutes les vidéos filtrées",
confirmDeleteFilteredVideos: "Êtes-vous sûr de vouloir supprimer {count} vidéos filtrées par les tags sélectionnés ?",
deleteFilteredVideosSuccess: "{count} vidéos supprimées avec succès.",
deletingVideos: "Suppression des vidéos...",
// Login
signIn: "Se connecter",

View File

@@ -264,6 +264,10 @@ export const ja = {
searchCancelled: "検索がキャンセルされました",
openInExternalPlayer: "外部プレーヤーで開く",
playWith: "で再生...",
deleteAllFilteredVideos: "フィルタリングされた動画をすべて削除",
confirmDeleteFilteredVideos: "選択されたタグでフィルタリングされた {count} 本の動画を削除してもよろしいですか?",
deleteFilteredVideosSuccess: "{count} 本の動画を削除しました。",
deletingVideos: "動画を削除中...",
// Login
signIn: "サインイン",

View File

@@ -261,6 +261,10 @@ export const ko = {
searchCancelled: "검색이 취소되었습니다",
openInExternalPlayer: "외부 플레이어에서 열기",
playWith: "다음으로 재생...",
deleteAllFilteredVideos: "필터링된 모든 동영상 삭제",
confirmDeleteFilteredVideos: "선택한 태그로 필터링된 {count}개의 동영상을 삭제하시겠습니까?",
deleteFilteredVideosSuccess: "{count}개의 동영상을 성공적으로 삭제했습니다.",
deletingVideos: "동영상 삭제 중...",
// Login
signIn: "로그인",

View File

@@ -273,6 +273,10 @@ export const pt = {
searchCancelled: "Pesquisa cancelada",
openInExternalPlayer: "Abrir no player externo",
playWith: "Reproduzir com...",
deleteAllFilteredVideos: "Excluir todos os vídeos filtrados",
confirmDeleteFilteredVideos: "Tem certeza de que deseja excluir {count} vídeos filtrados pelas tags selecionadas?",
deleteFilteredVideosSuccess: "{count} vídeos excluídos com sucesso.",
deletingVideos: "Excluindo vídeos...",
// Login
signIn: "Entrar",

View File

@@ -275,6 +275,10 @@ export const ru = {
searchCancelled: "Поиск отменен",
openInExternalPlayer: "Открыть во внешнем плеере",
playWith: "Воспроизвести с помощью...",
deleteAllFilteredVideos: "Удалить все отфильтрованные видео",
confirmDeleteFilteredVideos: "Вы уверены, что хотите удалить {count} видео, отфильтрованных по выбранным тегам?",
deleteFilteredVideosSuccess: "Успешно удалено {count} видео.",
deletingVideos: "Удаление видео...",
// Login
signIn: "Войти",

View File

@@ -253,6 +253,10 @@ export const zh = {
searchCancelled: "搜索已取消",
openInExternalPlayer: "在外部播放器中打开",
playWith: "使用此应用播放...",
deleteAllFilteredVideos: "删除所有过滤后的视频",
confirmDeleteFilteredVideos: "您确定要删除通过选定标签过滤的 {count} 个视频吗?",
deleteFilteredVideosSuccess: "成功删除 {count} 个视频。",
deletingVideos: "正在删除视频...",
// Login
signIn: "登录",

View File

@@ -34,6 +34,18 @@ if [ "$CURRENT_BRANCH" != "master" ]; then
fi
fi
# Run tests
echo "🧪 Running tests..."
npm run test
# Build Frontend
echo "🏗️ Building frontend..."
npm run build
# Build Backend
echo "🏗️ Building backend..."
cd backend && npm run build && cd ..
# Update version in package.json files
echo "🔄 Updating version numbers..."
npm version $INPUT_VERSION --no-git-tag-version --allow-same-version