diff --git a/README-zh.md b/README-zh.md index 95baca6..7fa91fb 100644 --- a/README-zh.md +++ b/README-zh.md @@ -17,6 +17,7 @@ - **视频上传**:直接上传本地视频文件到您的库,并自动生成缩略图。 - **Bilibili 支持**:支持下载单个视频、多P视频以及整个合集/系列。 - **并行下载**:支持队列下载,可同时追踪多个下载任务的进度。 +- **批量下载**:一次性添加多个视频链接到下载队列。 - **并发下载限制**:设置同时下载的数量限制以管理带宽。 - **本地库**:自动保存视频缩略图和元数据,提供丰富的浏览体验。 - **视频播放器**:自定义播放器,支持播放/暂停、循环、快进/快退、全屏和调光控制。 diff --git a/README.md b/README.md index e845ea9..68e274e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A YouTube/Bilibili/MissAV video downloader and player application that allows yo - **Video Upload**: Upload local video files directly to your library with automatic thumbnail generation. - **Bilibili Support**: Support for downloading single videos, multi-part videos, and entire collections/series. - **Parallel Downloads**: Queue multiple downloads and track their progress simultaneously. +- **Batch Download**: Add multiple video URLs at once to the download queue. - **Concurrent Download Limit**: Set a limit on the number of simultaneous downloads to manage bandwidth. - **Local Library**: Automatically save video thumbnails and metadata for a rich browsing experience. - **Video Player**: Custom player with Play/Pause, Loop, Seek, Full-screen, and Dimming controls. diff --git a/frontend/src/components/BatchDownloadModal.tsx b/frontend/src/components/BatchDownloadModal.tsx new file mode 100644 index 0000000..c15514a --- /dev/null +++ b/frontend/src/components/BatchDownloadModal.tsx @@ -0,0 +1,71 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField +} from '@mui/material'; +import React, { useState } from 'react'; +import { useLanguage } from '../contexts/LanguageContext'; + +interface BatchDownloadModalProps { + open: boolean; + onClose: () => void; + onConfirm: (urls: string[]) => void; +} + +const BatchDownloadModal: React.FC = ({ open, onClose, onConfirm }) => { + const { t } = useLanguage(); + const [text, setText] = useState(''); + + const handleConfirm = () => { + const urls = text + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); + + onConfirm(urls); + setText(''); + onClose(); + }; + + const handleClose = () => { + setText(''); + onClose(); + }; + + return ( + + {t('batchDownload') || 'Batch Download'} + + + {t('batchDownloadDescription') || 'Paste multiple URLs below, one per line.'} + + setText(e.target.value)} + placeholder="https://www.youtube.com/watch?v=...\nhttps://www.bilibili.com/video/..." + /> + + + + + + + ); +}; + +export default BatchDownloadModal; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index bc817ce..0dbeebe 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -2,7 +2,6 @@ import { Brightness4, Brightness7, Clear, - CloudUpload, Download, Menu as MenuIcon, Search, @@ -17,7 +16,7 @@ import { CircularProgress, ClickAwayListener, Collapse, - Divider, + Fade, IconButton, InputAdornment, Menu, @@ -39,7 +38,7 @@ import { Collection, Video } from '../types'; import AuthorsList from './AuthorsList'; import Collections from './Collections'; import TagsList from './TagsList'; -import UploadModal from './UploadModal'; + interface DownloadInfo { id: string; @@ -84,7 +83,6 @@ const Header: React.FC = ({ const [error, setError] = useState(''); const [anchorEl, setAnchorEl] = useState(null); const [manageAnchorEl, setManageAnchorEl] = useState(null); - const [uploadModalOpen, setUploadModalOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const navigate = useNavigate(); const theme = useTheme(); @@ -166,21 +164,11 @@ const Header: React.FC = ({ } }; - const handleUploadSuccess = () => { - if (window.location.pathname === '/') { - window.location.reload(); - } else { - navigate('/'); - } - }; + const renderActionButtons = () => ( - - setUploadModalOpen(true)}> - - - + {( <> @@ -201,6 +189,8 @@ const Header: React.FC = ({ filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))', mt: 1.5, width: 320, + maxHeight: '50vh', + overflowY: 'auto', '& .MuiAvatar-root': { width: 32, height: 32, @@ -224,7 +214,17 @@ const Header: React.FC = ({ }} transformOrigin={{ horizontal: 'right', vertical: 'top' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} + slots={{ transition: Fade }} > + { handleDownloadsClose(); navigate('/downloads'); }}> + + + + {t('manageDownloads') || 'Manage Downloads'} + + + + {activeDownloads.map((download) => ( @@ -284,12 +284,6 @@ const Header: React.FC = ({ )) ]} - - { handleDownloadsClose(); navigate('/downloads'); }}> - - {t('manageDownloads') || 'Manage Downloads'} - - )} @@ -318,6 +312,7 @@ const Header: React.FC = ({ overflow: 'visible', filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))', mt: 1.5, + width: 320, '&:before': { content: '""', display: 'block', @@ -335,6 +330,7 @@ const Header: React.FC = ({ }} transformOrigin={{ horizontal: 'right', vertical: 'top' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} + slots={{ transition: Fade }} > { handleManageClose(); navigate('/manage'); }}> {t('manageContent')} @@ -484,11 +480,7 @@ const Header: React.FC = ({ )} - setUploadModalOpen(false)} - onUploadSuccess={handleUploadSuccess} - /> + diff --git a/frontend/src/components/VideoPlayer/VideoControls.tsx b/frontend/src/components/VideoPlayer/VideoControls.tsx index d2ba967..d1c6f9b 100644 --- a/frontend/src/components/VideoPlayer/VideoControls.tsx +++ b/frontend/src/components/VideoPlayer/VideoControls.tsx @@ -92,6 +92,26 @@ const VideoControls: React.FC = ({ }; }, []); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if typing in an input or textarea + if (document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement) { + return; + } + + if (e.key === 'ArrowLeft') { + handleSeek(-10); + } else if (e.key === 'ArrowRight') { + handleSeek(10); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, []); + const handlePlayPause = () => { if (videoRef.current) { if (isPlaying) { @@ -217,7 +237,7 @@ const VideoControls: React.FC = ({ {/* Row 2 on Mobile: Seek Controls */} - + + + @@ -368,6 +398,12 @@ const DownloadPage: React.FC = () => { )} + + setShowBatchModal(false)} + onConfirm={handleBatchSubmit} + /> ); }; diff --git a/frontend/src/pages/ManagePage.tsx b/frontend/src/pages/ManagePage.tsx index fef258e..f972bfb 100644 --- a/frontend/src/pages/ManagePage.tsx +++ b/frontend/src/pages/ManagePage.tsx @@ -2,6 +2,7 @@ import { ArrowBack, Check, Close, + CloudUpload, Delete, Edit, Folder, @@ -34,6 +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'; @@ -52,6 +54,7 @@ const ManagePage: React.FC = () => { const [isDeletingCollection, setIsDeletingCollection] = useState(false); const [videoToDelete, setVideoToDelete] = useState(null); const [showVideoDeleteModal, setShowVideoDeleteModal] = useState(false); + const [uploadModalOpen, setUploadModalOpen] = useState(false); // Editing state const [editingVideoId, setEditingVideoId] = useState(null); @@ -227,22 +230,41 @@ const ManagePage: React.FC = () => { return video.thumbnailUrl || 'https://via.placeholder.com/120x90?text=No+Thumbnail'; }; + const handleUploadSuccess = () => { + window.location.reload(); + }; + return ( {t('manageContent')} - + + + + + setUploadModalOpen(false)} + onUploadSuccess={handleUploadSuccess} + /> + !isDeletingCollection && setCollectionToDelete(null)} diff --git a/frontend/src/pages/VideoPlayer.tsx b/frontend/src/pages/VideoPlayer.tsx index 9c51049..83a91a0 100644 --- a/frontend/src/pages/VideoPlayer.tsx +++ b/frontend/src/pages/VideoPlayer.tsx @@ -388,12 +388,14 @@ const VideoPlayer: React.FC = () => { onTagsUpdate={handleUpdateTags} /> - + {(video.source === 'youtube' || video.source === 'bilibili') && ( + + )} @@ -435,8 +437,8 @@ const VideoPlayer: React.FC = () => { /> )} - - + + {relatedVideo.title} @@ -445,11 +447,9 @@ const VideoPlayer: React.FC = () => { {formatDate(relatedVideo.date)} - {relatedVideo.viewCount !== undefined && ( - - {relatedVideo.viewCount} {t('views')} - - )} + + {relatedVideo.viewCount || 0} {t('views')} + ))} diff --git a/frontend/src/utils/locales/ar.ts b/frontend/src/utils/locales/ar.ts index 47c63aa..36cc65e 100644 --- a/frontend/src/utils/locales/ar.ts +++ b/frontend/src/utils/locales/ar.ts @@ -265,4 +265,12 @@ export const ar = { speed: "السرعة", finishedAt: "انتهى في", failed: "فشل", + + // Batch Download + batchDownload: "تحميل مجمع", + batchDownloadDescription: "الصق روابط متعددة أدناه، واحد في كل سطر.", + urls: "الروابط", + addToQueue: "إضافة إلى قائمة الانتظار", + batchTasksAdded: "تمت إضافة {count} مهام", + addBatchTasks: "إضافة مهام مجمعة", }; diff --git a/frontend/src/utils/locales/de.ts b/frontend/src/utils/locales/de.ts index 5ccba21..269d3d2 100644 --- a/frontend/src/utils/locales/de.ts +++ b/frontend/src/utils/locales/de.ts @@ -136,4 +136,12 @@ export const de = { speed: "Geschwindigkeit", finishedAt: "Beendet am", failed: "Fehlgeschlagen", + + // Batch Download + batchDownload: "Stapel-Download", + batchDownloadDescription: "Fügen Sie unten mehrere URLs ein, eine pro Zeile.", + urls: "URLs", + addToQueue: "Zur Warteschlange hinzufügen", + batchTasksAdded: "{count} Aufgaben hinzugefügt", + addBatchTasks: "Stapelaufgaben hinzufügen", }; diff --git a/frontend/src/utils/locales/en.ts b/frontend/src/utils/locales/en.ts index b221486..5939365 100644 --- a/frontend/src/utils/locales/en.ts +++ b/frontend/src/utils/locales/en.ts @@ -266,4 +266,12 @@ export const en = { videoRemovedFromCollection: "Video removed from collection", collectionDeletedSuccessfully: "Collection deleted successfully", failedToDeleteCollection: "Failed to delete collection", + + // Batch Download + batchDownload: "Batch Download", + batchDownloadDescription: "Paste multiple URLs below, one per line.", + urls: "URLs", + addToQueue: "Add to Queue", + batchTasksAdded: "{count} tasks added", + addBatchTasks: "Add batch tasks", }; diff --git a/frontend/src/utils/locales/es.ts b/frontend/src/utils/locales/es.ts index 3dc5fe8..1b5e4e2 100644 --- a/frontend/src/utils/locales/es.ts +++ b/frontend/src/utils/locales/es.ts @@ -134,4 +134,12 @@ export const es = { speed: "Velocidad", finishedAt: "Finalizado en", failed: "Fallido", + + // Batch Download + batchDownload: "Descarga por lotes", + batchDownloadDescription: "Pegue varias URL a continuación, una por línea.", + urls: "URLs", + addToQueue: "Añadir a la cola", + batchTasksAdded: "{count} tareas añadidas", + addBatchTasks: "Añadir tareas por lotes", }; diff --git a/frontend/src/utils/locales/fr.ts b/frontend/src/utils/locales/fr.ts index 71fdbf9..0aef96b 100644 --- a/frontend/src/utils/locales/fr.ts +++ b/frontend/src/utils/locales/fr.ts @@ -264,4 +264,12 @@ export const fr = { speed: "Vitesse", finishedAt: "Terminé à", failed: "Échoué", + + // Batch Download + batchDownload: "Téléchargement par lot", + batchDownloadDescription: "Collez plusieurs URL ci-dessous, une par ligne.", + urls: "URLs", + addToQueue: "Ajouter à la file d'attente", + batchTasksAdded: "{count} tâches ajoutées", + addBatchTasks: "Ajouter des tâches par lot", }; diff --git a/frontend/src/utils/locales/ja.ts b/frontend/src/utils/locales/ja.ts index 2a3c874..6cdff7c 100644 --- a/frontend/src/utils/locales/ja.ts +++ b/frontend/src/utils/locales/ja.ts @@ -265,4 +265,12 @@ export const ja = { speed: "速度", finishedAt: "完了日時", failed: "失敗", + + // Batch Download + batchDownload: "一括ダウンロード", + batchDownloadDescription: "以下に複数のURLを1行に1つずつ貼り付けてください。", + urls: "URL", + addToQueue: "キューに追加", + batchTasksAdded: "{count} 件のタスクを追加しました", + addBatchTasks: "一括タスクを追加", }; diff --git a/frontend/src/utils/locales/ko.ts b/frontend/src/utils/locales/ko.ts index cd398cc..b847181 100644 --- a/frontend/src/utils/locales/ko.ts +++ b/frontend/src/utils/locales/ko.ts @@ -265,4 +265,12 @@ export const ko = { speed: "속도", finishedAt: "완료 시간", failed: "실패", + + // Batch Download + batchDownload: "일괄 다운로드", + batchDownloadDescription: "아래에 여러 URL을 한 줄에 하나씩 붙여넣으세요.", + urls: "URL", + addToQueue: "대기열에 추가", + batchTasksAdded: "{count}개의 작업이 추가되었습니다", + addBatchTasks: "일괄 작업 추가", }; diff --git a/frontend/src/utils/locales/pt.ts b/frontend/src/utils/locales/pt.ts index fea3532..ddbd6ec 100644 --- a/frontend/src/utils/locales/pt.ts +++ b/frontend/src/utils/locales/pt.ts @@ -264,4 +264,12 @@ export const pt = { speed: "Velocidade", finishedAt: "Terminado em", failed: "Falhou", + + // Batch Download + batchDownload: "Download em lote", + batchDownloadDescription: "Cole vários URLs abaixo, um por linha.", + urls: "URLs", + addToQueue: "Adicionar à fila", + batchTasksAdded: "{count} tarefas adicionadas", + addBatchTasks: "Adicionar tarefas em lote", }; diff --git a/frontend/src/utils/locales/ru.ts b/frontend/src/utils/locales/ru.ts index d382789..b79894e 100644 --- a/frontend/src/utils/locales/ru.ts +++ b/frontend/src/utils/locales/ru.ts @@ -265,4 +265,12 @@ export const ru = { speed: "Скорость", finishedAt: "Завершено в", failed: "Ошибка", + + // Batch Download + batchDownload: "Пакетная загрузка", + batchDownloadDescription: "Вставьте несколько URL ниже, по одному в строке.", + urls: "URL", + addToQueue: "Добавить в очередь", + batchTasksAdded: "Добавлено {count} задач", + addBatchTasks: "Добавить пакетные задачи", }; diff --git a/frontend/src/utils/locales/zh.ts b/frontend/src/utils/locales/zh.ts index 3a9054a..2912a95 100644 --- a/frontend/src/utils/locales/zh.ts +++ b/frontend/src/utils/locales/zh.ts @@ -266,4 +266,12 @@ export const zh = { speed: "速度", finishedAt: "完成时间", failed: "失败", + + // Batch Download + batchDownload: "批量下载", + batchDownloadDescription: "在下方粘贴多个链接,每行一个。", + urls: "链接", + addToQueue: "添加到队列", + batchTasksAdded: "已添加 {count} 个任务", + addBatchTasks: "添加批量任务", };