feat: Add upload and scan modals on DownloadPage

This commit is contained in:
Peifan Li
2025-12-01 14:16:47 -05:00
parent c88909b658
commit 7969412091
13 changed files with 122 additions and 69 deletions

View File

@@ -2,8 +2,10 @@ import {
Cancel as CancelIcon,
CheckCircle as CheckCircleIcon,
ClearAll as ClearAllIcon,
CloudUpload,
Delete as DeleteIcon,
Error as ErrorIcon,
FindInPage,
PlaylistAdd as PlaylistAddIcon
} from '@mui/icons-material';
import {
@@ -24,6 +26,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import React, { useState } from 'react';
import BatchDownloadModal from '../components/BatchDownloadModal';
import ConfirmationModal from '../components/ConfirmationModal';
import UploadModal from '../components/UploadModal';
import { useDownload } from '../contexts/DownloadContext';
import { useLanguage } from '../contexts/LanguageContext';
import { useSnackbar } from '../contexts/SnackbarContext';
@@ -76,6 +80,26 @@ const DownloadPage: React.FC = () => {
const queryClient = useQueryClient();
const [tabValue, setTabValue] = useState(0);
const [showBatchModal, setShowBatchModal] = useState(false);
const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [showScanConfirmModal, setShowScanConfirmModal] = useState(false);
// Scan files mutation
const scanMutation = useMutation({
mutationFn: async () => {
const res = await axios.post(`${API_URL}/scan-files`);
return res.data;
},
onSuccess: (data) => {
showSnackbar(t('scanFilesSuccess').replace('{count}', data.addedCount.toString()) || `Scan complete. ${data.addedCount} files added.`);
},
onError: (error: any) => {
showSnackbar(`${t('scanFilesFailed') || 'Scan failed'}: ${error.response?.data?.details || error.message}`);
}
});
const handleUploadSuccess = () => {
window.location.reload();
};
const handleBatchSubmit = async (urls: string[]) => {
// We'll process them sequentially to be safe, or just fire them all.
@@ -230,18 +254,44 @@ const DownloadPage: React.FC = () => {
return (
<Box sx={{ width: '100%', p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center' },
mb: 2,
gap: { xs: 2, sm: 0 }
}}>
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
{t('downloads') || 'Downloads'}
</Typography>
<Button
variant="contained"
size="small"
startIcon={<PlaylistAddIcon />}
onClick={() => setShowBatchModal(true)}
>
{t('addBatchTasks') || 'Add batch tasks'}
</Button>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', width: { xs: '100%', sm: 'auto' } }}>
<Button
variant="outlined"
size="small"
startIcon={<FindInPage />}
onClick={() => setShowScanConfirmModal(true)}
disabled={scanMutation.isPending}
>
{scanMutation.isPending ? (t('scanning') || 'Scanning...') : (t('scanFiles') || 'Scan Files')}
</Button>
<Button
variant="contained"
size="small"
startIcon={<PlaylistAddIcon />}
onClick={() => setShowBatchModal(true)}
>
{t('addBatchTasks') || 'Add batch tasks'}
</Button>
<Button
variant="contained"
size="small"
startIcon={<CloudUpload />}
onClick={() => setUploadModalOpen(true)}
>
{t('uploadVideo') || 'Upload Video'}
</Button>
</Box>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
@@ -404,6 +454,23 @@ const DownloadPage: React.FC = () => {
onClose={() => setShowBatchModal(false)}
onConfirm={handleBatchSubmit}
/>
<UploadModal
open={uploadModalOpen}
onClose={() => setUploadModalOpen(false)}
onUploadSuccess={handleUploadSuccess}
/>
<ConfirmationModal
isOpen={showScanConfirmModal}
onClose={() => setShowScanConfirmModal(false)}
onConfirm={() => {
setShowScanConfirmModal(false);
scanMutation.mutate();
}}
title={t('scanFiles') || 'Scan Files'}
message={t('scanFilesConfirmMessage') || 'The system will scan the root folder of the video path to find undocumented video files.'}
confirmText={t('continue') || 'Continue'}
cancelText={t('cancel') || 'Cancel'}
/>
</Box>
);
};

View File

@@ -2,7 +2,7 @@ import {
ArrowBack,
Check,
Close,
CloudUpload,
Delete,
Edit,
Folder,
@@ -35,7 +35,7 @@ import { useState } from 'react';
import { Link } from 'react-router-dom';
import ConfirmationModal from '../components/ConfirmationModal';
import DeleteCollectionModal from '../components/DeleteCollectionModal';
import UploadModal from '../components/UploadModal';
import { useCollection } from '../contexts/CollectionContext';
import { useLanguage } from '../contexts/LanguageContext';
import { useVideo } from '../contexts/VideoContext';
@@ -54,7 +54,7 @@ const ManagePage: React.FC = () => {
const [isDeletingCollection, setIsDeletingCollection] = useState<boolean>(false);
const [videoToDelete, setVideoToDelete] = useState<string | null>(null);
const [showVideoDeleteModal, setShowVideoDeleteModal] = useState<boolean>(false);
const [uploadModalOpen, setUploadModalOpen] = useState<boolean>(false);
// Editing state
const [editingVideoId, setEditingVideoId] = useState<string | null>(null);
@@ -230,9 +230,7 @@ const ManagePage: React.FC = () => {
return video.thumbnailUrl || 'https://via.placeholder.com/120x90?text=No+Thumbnail';
};
const handleUploadSuccess = () => {
window.location.reload();
};
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
@@ -241,13 +239,7 @@ const ManagePage: React.FC = () => {
{t('manageContent')}
</Typography>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="contained"
startIcon={<CloudUpload />}
onClick={() => setUploadModalOpen(true)}
>
{t('uploadVideo')}
</Button>
<Button
component={Link}
to="/"
@@ -259,11 +251,7 @@ const ManagePage: React.FC = () => {
</Box>
</Box>
<UploadModal
open={uploadModalOpen}
onClose={() => setUploadModalOpen(false)}
onUploadSuccess={handleUploadSuccess}
/>
<DeleteCollectionModal
isOpen={!!collectionToDelete}

View File

@@ -127,29 +127,7 @@ const SettingsPage: React.FC = () => {
saveMutation.mutate(settings);
};
// Scan files mutation
const scanMutation = useMutation({
mutationFn: async () => {
const res = await axios.post(`${API_URL}/scan-files`);
return res.data;
},
onSuccess: (data) => {
setInfoModal({
isOpen: true,
title: t('success'),
message: t('scanFilesSuccess').replace('{count}', data.addedCount.toString()),
type: 'success'
});
},
onError: (error: any) => {
setInfoModal({
isOpen: true,
title: t('error'),
message: `${t('scanFilesFailed')}: ${error.response?.data?.details || error.message}`,
type: 'error'
});
}
});
// Migrate data mutation
const migrateMutation = useMutation({
@@ -289,7 +267,7 @@ const SettingsPage: React.FC = () => {
setSettings(prev => ({ ...prev, tags: updatedTags }));
};
const isSaving = saveMutation.isPending || scanMutation.isPending || migrateMutation.isPending || cleanupMutation.isPending || deleteLegacyMutation.isPending;
const isSaving = saveMutation.isPending || migrateMutation.isPending || cleanupMutation.isPending || deleteLegacyMutation.isPending;
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
@@ -534,15 +512,7 @@ const SettingsPage: React.FC = () => {
{t('migrateDataButton')}
</Button>
<Button
variant="outlined"
color="primary"
onClick={() => scanMutation.mutate()}
disabled={isSaving}
sx={{ ml: 2 }}
>
{t('scanFiles')}
</Button>
<Box sx={{ mt: 3 }}>
<Typography variant="h6" gutterBottom>{t('removeLegacyData')}</Typography>

View File

@@ -59,8 +59,10 @@ export const ar = {
migrateDataButton: "نقل البيانات من JSON",
scanFiles: "فحص الملفات",
scanFilesSuccess: "اكتمل الفحص. تمت إضافة {count} فيديوهات جديدة.",
scanFilesFailed: "فشل الفحص",
migrateConfirmation: "هل أنت متأكد أنك تريد نقل البيانات؟ قد يستغرق هذا بضع لحظات.",
scanFilesFailed: "فشل المسح",
scanFilesConfirmMessage: "سيقوم النظام بفحص المجلد الجذر لمسار الفيديو للعثور على ملفات الفيديو غير الموثقة.",
scanning: "جارٍ المسح...",
migrateConfirmation: "هل أنت متأكد أنك تريد ترحيل البيانات؟ قد يستغرق هذا بضع لحظات.",
migrationResults: "نتائج النقل",
migrationReport: "تقرير النقل",
migrationSuccess: "اكتمل النقل. انظر التفاصيل في التنبيه.",
@@ -212,6 +214,7 @@ export const ar = {
save: "حفظ",
on: "تشغيل",
off: "إيقاف",
continue: "متابعة",
// Video Card
unknownDate: "تاريخ غير معروف",

View File

@@ -23,6 +23,8 @@ export const de = {
database: "Datenbank", migrateDataDescription: "Daten von Legacy-JSON-Dateien zur neuen SQLite-Datenbank migrieren. Diese Aktion kann sicher mehrmals ausgeführt werden (Duplikate werden übersprungen).",
migrateDataButton: "Daten aus JSON migrieren", scanFiles: "Dateien Scannen",
scanFilesSuccess: "Scan abgeschlossen. {count} neue Videos hinzugefügt.", scanFilesFailed: "Scan fehlgeschlagen",
scanFilesConfirmMessage: "Das System scannt den Stammordner des Videopfads, um nicht dokumentierte Videodateien zu finden.",
scanning: "Scannen...",
migrateConfirmation: "Sind Sie sicher, dass Sie Daten migrieren möchten? Dies kann einige Momente dauern.",
migrationResults: "Migrationsergebnisse", migrationReport: "Migrationsbericht",
migrationSuccess: "Migration abgeschlossen. Details in der Warnung anzeigen.", migrationNoData: "Migration abgeschlossen, aber keine Daten gefunden.",
@@ -93,7 +95,8 @@ export const de = {
deleteCollectionTitle: "Sammlung Löschen", deleteCollectionConfirmation: "Sind Sie sicher, dass Sie die Sammlung löschen möchten",
collectionContains: "Diese Sammlung enthält", deleteCollectionOnly: "Nur Sammlung Löschen",
deleteCollectionAndVideos: "Sammlung und Alle Videos Löschen", loading: "Laden...", error: "Fehler",
success: "Erfolg", cancel: "Abbrechen", confirm: "Bestätigen", save: "Speichern", on: "Ein", off: "Aus",
success: "Erfolg", cancel: "Abbrechen", confirm: "Bestätigen", save: "Speichern", on: "Ein", off: "Aus",
continue: "Weiter",
unknownDate: "Unbekanntes Datum", part: "Teil", collection: "Sammlung", selectVideoFile: "Videodatei Auswählen",
pleaseSelectVideo: "Bitte wählen Sie eine Videodatei aus", uploadFailed: "Upload fehlgeschlagen",
failedToUpload: "Fehler beim Hochladen des Videos", uploading: "Hochladen...", upload: "Hochladen",

View File

@@ -60,6 +60,8 @@ export const en = {
scanFiles: "Scan Files",
scanFilesSuccess: "Scan complete. Added {count} new videos.",
scanFilesFailed: "Scan failed",
scanFilesConfirmMessage: "The system will scan the root folder of the video path to find undocumented video files.",
scanning: "Scanning...",
migrateConfirmation: "Are you sure you want to migrate data? This may take a few moments.",
migrationResults: "Migration Results",
migrationReport: "Migration Report",
@@ -203,6 +205,7 @@ export const en = {
save: "Save",
on: "On",
off: "Off",
continue: "Continue",
// Video Card
unknownDate: "Unknown date",

View File

@@ -22,7 +22,9 @@ export const es = {
tagsManagementNote: "Recuerde hacer clic en \"Guardar Configuración\" después de agregar o eliminar etiquetas para aplicar los cambios.",
database: "Base de Datos", migrateDataDescription: "Migrar datos de archivos JSON heredados a la nueva base de datos SQLite. Esta acción es segura para ejecutar varias veces (se omitirán duplicados).",
migrateDataButton: "Migrar Datos desde JSON", scanFiles: "Escanear Archivos",
scanFilesSuccess: "Escaneo completo. Se agregaron {count} nuevos videos.", scanFilesFailed: "Escaneo fallido",
scanFilesSuccess: "Escaneo completo. Se agregaron {count} nuevos videos.", scanFilesFailed: "Escaneo fallido",
scanFilesConfirmMessage: "El sistema escaneará la carpeta raíz de la ruta de video para encontrar archivos de video no documentados.",
scanning: "Escaneando...",
migrateConfirmation: "¿Está seguro de que desea migrar los datos? Esto puede tardar unos momentos.",
migrationResults: "Resultados de Migración", migrationReport: "Informe de Migración",
migrationSuccess: "Migración completada. Ver detalles en la alerta.", migrationNoData: "Migración finalizada pero no se encontraron datos.",

View File

@@ -60,6 +60,8 @@ export const fr = {
scanFiles: "Scanner les fichiers",
scanFilesSuccess: "Scan terminé. {count} nouvelles vidéos ajoutées.",
scanFilesFailed: "Échec du scan",
scanFilesConfirmMessage: "Le système analysera le dossier racine du chemin vidéo pour trouver des fichiers vidéo non documentés.",
scanning: "Analyse en cours...",
migrateConfirmation: "Êtes-vous sûr de vouloir migrer les données ? Cela peut prendre quelques instants.",
migrationResults: "Résultats de la migration",
migrationReport: "Rapport de migration",

View File

@@ -60,6 +60,8 @@ export const ja = {
scanFiles: "ファイルをスキャン",
scanFilesSuccess: "スキャンが完了しました。{count}個の新しい動画を追加しました。",
scanFilesFailed: "スキャンに失敗しました",
scanFilesConfirmMessage: "システムはビデオパスのルートフォルダをスキャンして、未登録のビデオファイルを検索します。",
scanning: "スキャン中...",
migrateConfirmation: "データを移行してもよろしいですか?これには時間がかかる場合があります。",
migrationResults: "移行結果",
migrationReport: "移行レポート",
@@ -212,6 +214,7 @@ export const ja = {
save: "保存",
on: "オン",
off: "オフ",
continue: "続行",
// Video Card
unknownDate: "不明な日付",

View File

@@ -60,6 +60,8 @@ export const ko = {
scanFiles: "파일 스캔",
scanFilesSuccess: "스캔 완료. {count}개의 새 동영상이 추가되었습니다.",
scanFilesFailed: "스캔 실패",
scanFilesConfirmMessage: "시스템이 비디오 경로의 루트 폴더를 스캔하여 문서화되지 않은 비디오 파일을 찾습니다.",
scanning: "스캔 중...",
migrateConfirmation: "데이터를 마이그레이션하시겠습니까? 잠시 시간이 걸릴 수 있습니다.",
migrationResults: "마이그레이션 결과",
migrationReport: "마이그레이션 보고서",
@@ -212,6 +214,7 @@ export const ko = {
save: "저장",
on: "켜기",
off: "끄기",
continue: "계속",
// Video Card
unknownDate: "알 수 없는 날짜",

View File

@@ -59,7 +59,9 @@ export const pt = {
migrateDataButton: "Migrar Dados do JSON",
scanFiles: "Escanear Arquivos",
scanFilesSuccess: "Escaneamento completo. {count} novos vídeos adicionados.",
scanFilesFailed: "Falha no escaneamento",
scanFilesFailed: "A verificação falhou",
scanFilesConfirmMessage: "O sistema verificará a pasta raiz do caminho do vídeo para encontrar arquivos de vídeo não documentados.",
scanning: "Verificando...",
migrateConfirmation: "Tem certeza de que deseja migrar os dados? Isso pode levar alguns instantes.",
migrationResults: "Resultados da Migração",
migrationReport: "Relatório de Migração",
@@ -211,6 +213,7 @@ export const pt = {
save: "Salvar",
on: "Ligado",
off: "Desligado",
continue: "Continuar",
// Video Card
unknownDate: "Data desconhecida",

View File

@@ -59,7 +59,9 @@ export const ru = {
migrateDataButton: "Перенести данные из JSON",
scanFiles: "Сканировать файлы",
scanFilesSuccess: "Сканирование завершено. Добавлено {count} новых видео.",
scanFilesFailed: "Ошибка сканирования",
scanFilesFailed: "Сканирование не удалось",
scanFilesConfirmMessage: "Система просканирует корневую папку с видео, чтобы найти недовкументированные видеофайлы.",
scanning: "Сканирование...",
migrateConfirmation: "Вы уверены, что хотите перенести данные? Это может занять некоторое время.",
migrationResults: "Результаты миграции",
migrationReport: "Отчет о миграции",
@@ -211,7 +213,8 @@ export const ru = {
confirm: "Подтвердить",
save: "Сохранить",
on: "Вкл.",
off: "Выкл.",
off: "Выкл",
continue: "Продолжить",
// Video Card
unknownDate: "Неизвестная дата",

View File

@@ -60,7 +60,9 @@ export const zh = {
scanFiles: "扫描文件",
scanFilesSuccess: "扫描完成。添加了 {count} 个新视频。",
scanFilesFailed: "扫描失败",
migrateConfirmation: "确定要迁移数据吗?这可能需要一些时间。",
scanFilesConfirmMessage: "系统将扫描视频路径的根文件夹以查找未记录的视频文件。",
scanning: "扫描中...",
migrateConfirmation: "您确定要迁移数据吗?这可能需要一些时间。",
migrationResults: "迁移结果",
migrationReport: "迁移报告",
migrationSuccess: "迁移完成。请查看警报中的详细信息。",
@@ -211,7 +213,8 @@ export const zh = {
confirm: "确认",
save: "保存",
on: "开启",
off: "关",
off: "关",
continue: "继续",
// Video Card
unknownDate: "未知日期",