feat: Add deleteVideos function in VideoContext
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 >
|
||||
|
||||
@@ -259,6 +259,10 @@ export const ar = {
|
||||
searchCancelled: "تم إلغاء البحث",
|
||||
openInExternalPlayer: "فتح في مشغل خارجي",
|
||||
playWith: "تشغيل بواسطة...",
|
||||
deleteAllFilteredVideos: "حذف جميع الفيديوهات المصفاة",
|
||||
confirmDeleteFilteredVideos: "هل أنت متأكد أنك تريد حذف {count} فيديو مصفى بواسطة العلامات المحددة؟",
|
||||
deleteFilteredVideosSuccess: "تم حذف {count} فيديو بنجاح.",
|
||||
deletingVideos: "جاري حذف الفيديوهات...",
|
||||
|
||||
// Login
|
||||
signIn: "تسجيل الدخول",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -264,6 +264,10 @@ export const ja = {
|
||||
searchCancelled: "検索がキャンセルされました",
|
||||
openInExternalPlayer: "外部プレーヤーで開く",
|
||||
playWith: "で再生...",
|
||||
deleteAllFilteredVideos: "フィルタリングされた動画をすべて削除",
|
||||
confirmDeleteFilteredVideos: "選択されたタグでフィルタリングされた {count} 本の動画を削除してもよろしいですか?",
|
||||
deleteFilteredVideosSuccess: "{count} 本の動画を削除しました。",
|
||||
deletingVideos: "動画を削除中...",
|
||||
|
||||
// Login
|
||||
signIn: "サインイン",
|
||||
|
||||
@@ -261,6 +261,10 @@ export const ko = {
|
||||
searchCancelled: "검색이 취소되었습니다",
|
||||
openInExternalPlayer: "외부 플레이어에서 열기",
|
||||
playWith: "다음으로 재생...",
|
||||
deleteAllFilteredVideos: "필터링된 모든 동영상 삭제",
|
||||
confirmDeleteFilteredVideos: "선택한 태그로 필터링된 {count}개의 동영상을 삭제하시겠습니까?",
|
||||
deleteFilteredVideosSuccess: "{count}개의 동영상을 성공적으로 삭제했습니다.",
|
||||
deletingVideos: "동영상 삭제 중...",
|
||||
|
||||
// Login
|
||||
signIn: "로그인",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -275,6 +275,10 @@ export const ru = {
|
||||
searchCancelled: "Поиск отменен",
|
||||
openInExternalPlayer: "Открыть во внешнем плеере",
|
||||
playWith: "Воспроизвести с помощью...",
|
||||
deleteAllFilteredVideos: "Удалить все отфильтрованные видео",
|
||||
confirmDeleteFilteredVideos: "Вы уверены, что хотите удалить {count} видео, отфильтрованных по выбранным тегам?",
|
||||
deleteFilteredVideosSuccess: "Успешно удалено {count} видео.",
|
||||
deletingVideos: "Удаление видео...",
|
||||
|
||||
// Login
|
||||
signIn: "Войти",
|
||||
|
||||
@@ -253,6 +253,10 @@ export const zh = {
|
||||
searchCancelled: "搜索已取消",
|
||||
openInExternalPlayer: "在外部播放器中打开",
|
||||
playWith: "使用此应用播放...",
|
||||
deleteAllFilteredVideos: "删除所有过滤后的视频",
|
||||
confirmDeleteFilteredVideos: "您确定要删除通过选定标签过滤的 {count} 个视频吗?",
|
||||
deleteFilteredVideosSuccess: "成功删除 {count} 个视频。",
|
||||
deletingVideos: "正在删除视频...",
|
||||
|
||||
// Login
|
||||
signIn: "登录",
|
||||
|
||||
12
release.sh
12
release.sh
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user