From 8cd6c1972d7cc86cb17d01f97f3beed4ae08b86b Mon Sep 17 00:00:00 2001 From: NimBold Date: Fri, 12 Jun 2026 23:19:21 +0330 Subject: [PATCH] feat: implement site logins, queues parity, and engine settings updates - Implemented Site Logins parity by integrating auth credentials to Rust backend (Aria2 and yt-dlp) and fetching metadata. - Implemented Queues functionality including context menu actions (start, pause, rename, delete). - Updated Engine settings UI to include Deno and render clean version output without extra trademark info. --- apps/desktop/src-tauri/src/lib.rs | 88 +++++++- apps/desktop/src/App.tsx | 6 +- .../src/components/AddDownloadsModal.tsx | 44 +++- apps/desktop/src/components/DownloadTable.tsx | 8 + apps/desktop/src/components/SchedulerView.tsx | 8 +- apps/desktop/src/components/SettingsView.tsx | 37 +++- apps/desktop/src/components/Sidebar.tsx | 201 ++++++++++++++++-- apps/desktop/src/store/useDownloadStore.ts | 46 +++- 8 files changed, 376 insertions(+), 62 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 32a586f..f587f07 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -15,7 +15,7 @@ struct MetadataResponse { } #[tauri::command] -async fn fetch_metadata(url: String, user_agent: Option) -> Result { +async fn fetch_metadata(url: String, user_agent: Option, username: Option, password: Option) -> Result { let mut builder = reqwest::Client::builder(); if let Some(ua) = user_agent { if !ua.is_empty() { @@ -29,10 +29,22 @@ async fn fetch_metadata(url: String, user_agent: Option) -> Result) -> Result) -> Result { +async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_browser: Option, username: Option, password: Option) -> Result { println!("fetch_media_metadata called for: {}", url); let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?; let ytdlp_path = resource_dir.join("binaries").join("yt-dlp"); @@ -102,6 +114,16 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_ cmd.arg("--cookies-from-browser").arg(&browser); } } + if let Some(user) = username { + if !user.is_empty() { + cmd.arg("--username").arg(&user); + } + } + if let Some(pass) = password { + if !pass.is_empty() { + cmd.arg("--password").arg(&pass); + } + } cmd.arg(&url); @@ -172,8 +194,9 @@ async fn test_aria2c(app_handle: tauri::AppHandle) -> Result { let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); // aria2c prints a lot, just get the first line for the version let first_line = text.lines().next().unwrap_or("").to_string(); - println!("aria2c output: {}", first_line); - Ok(first_line) + let clean = first_line.replace("aria2 version ", ""); + println!("aria2c output: {}", clean); + Ok(clean) } else { let err = String::from_utf8_lossy(&output.stderr); println!("aria2c error output: {}", err); @@ -200,8 +223,10 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result { if output.status.success() { let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); let first_line = text.lines().next().unwrap_or("").to_string(); - println!("ffmpeg output: {}", first_line); - Ok(first_line) + let parts: Vec<&str> = first_line.split_whitespace().collect(); + let clean = parts.get(2).unwrap_or(&first_line.as_str()).split('-').next().unwrap_or("").to_string(); + println!("ffmpeg output: {}", clean); + Ok(clean) } else { let err = String::from_utf8_lossy(&output.stderr); println!("ffmpeg error output: {}", err); @@ -209,6 +234,36 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result { } } +#[tauri::command] +async fn test_deno(app_handle: tauri::AppHandle) -> Result { + println!("test_deno called!"); + let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?; + let deno_path = resource_dir.join("binaries").join("deno"); + println!("Resolved deno path: {:?}", deno_path); + + let output = Command::new(&deno_path) + .arg("--version") + .output() + .map_err(|e| { + println!("Failed to execute: {}", e); + format!("Failed to execute deno: {}", e) + })?; + + println!("deno execution finished with status: {}", output.status); + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let first_line = text.lines().next().unwrap_or("").to_string(); + let parts: Vec<&str> = first_line.split_whitespace().collect(); + let clean = parts.get(1).unwrap_or(&first_line.as_str()).to_string(); + println!("deno output: {}", clean); + Ok(clean) + } else { + let err = String::from_utf8_lossy(&output.stderr); + println!("deno error output: {}", err); + Err(format!("deno error: {}", err)) + } +} + #[tauri::command] async fn open_file(path: String) -> Result<(), String> { println!("open_file called for path: {}", path); @@ -363,11 +418,13 @@ async fn start_download( if let Some(user) = username { if !user.is_empty() { cmd.arg(format!("--http-user={}", user)); + cmd.arg(format!("--ftp-user={}", user)); } } if let Some(pass) = password { if !pass.is_empty() { cmd.arg(format!("--http-passwd={}", pass)); + cmd.arg(format!("--ftp-passwd={}", pass)); } } if let Some(hdr) = headers { @@ -471,6 +528,8 @@ async fn start_media_download( filename: String, format_selector: Option, speed_limit: Option, + username: Option, + password: Option, ) -> Result<(), String> { println!("start_media_download called for id: {}", id); let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?; @@ -537,6 +596,17 @@ async fn start_media_download( } } + if let Some(user) = username { + if !user.is_empty() { + cmd.arg("--username").arg(&user); + } + } + if let Some(pass) = password { + if !pass.is_empty() { + cmd.arg("--password").arg(&pass); + } + } + cmd.arg(&url); cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); // Also pipe stderr for better error reporting @@ -842,7 +912,7 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_notification::init()) .invoke_handler(tauri::generate_handler![ - greet, test_ytdlp, test_aria2c, test_ffmpeg, open_file, show_in_folder, + greet, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder, start_download, start_media_download, pause_download, fetch_metadata, fetch_media_metadata, update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action, request_automation_permission, open_automation_settings diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 6ef9f79..9961253 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -5,7 +5,7 @@ import { AddDownloadsModal } from "./components/AddDownloadsModal"; import SettingsView from "./components/SettingsView"; import { PropertiesModal } from "./components/PropertiesModal"; import { listen } from "@tauri-apps/api/event"; -import { useDownloadStore } from "./store/useDownloadStore"; +import { useDownloadStore, MAIN_QUEUE_ID } from './store/useDownloadStore'; import { useSettingsStore } from "./store/useSettingsStore"; import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification'; import { invoke } from "@tauri-apps/api/core"; @@ -68,7 +68,7 @@ function App() { const triggerKey = `${dateKey}-${currentTime}`; if (state.schedulerLastStartKey !== triggerKey) { state.setSchedulerLastStartKey(triggerKey); - const started = await useDownloadStore.getState().startMainQueue(); + const started = await useDownloadStore.getState().startQueue(MAIN_QUEUE_ID); state.setSchedulerRunning(started > 0); } } @@ -77,7 +77,7 @@ function App() { const triggerKey = `${dateKey}-${currentTime}`; if (state.schedulerLastStopKey !== triggerKey) { state.setSchedulerLastStopKey(triggerKey); - await useDownloadStore.getState().pauseMainQueue(); + await useDownloadStore.getState().pauseQueue(MAIN_QUEUE_ID); state.setSchedulerRunning(false); } } diff --git a/apps/desktop/src/components/AddDownloadsModal.tsx b/apps/desktop/src/components/AddDownloadsModal.tsx index 1ba111b..14d013d 100644 --- a/apps/desktop/src/components/AddDownloadsModal.tsx +++ b/apps/desktop/src/components/AddDownloadsModal.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useDownloadStore } from '../store/useDownloadStore'; +import { useDownloadStore, MAIN_QUEUE_ID, getSiteLogin } from '../store/useDownloadStore'; import { useSettingsStore } from '../store/useSettingsStore'; import { DownloadCategory } from '../store/useDownloadStore'; import { FolderPlus, Settings, Shield, RefreshCw, FileText, HardDrive, Database, Link, ArrowRight, Play, ChevronDown, ChevronRight, Video, Film, Music } from 'lucide-react'; @@ -249,9 +249,11 @@ const isMediaUrl = (url: string) => { export const AddDownloadsModal = () => { - const { isAddModalOpen, toggleAddModal, addDownload } = useDownloadStore(); + const { isAddModalOpen, toggleAddModal, addDownload, queues } = useDownloadStore(); const { defaultDownloadPath } = useSettingsStore(); + const [selectedQueueId, setSelectedQueueId] = useState(MAIN_QUEUE_ID); + const [urls, setUrls] = useState(''); const [selectedItemIndex, setSelectedItemIndex] = useState(null); const [parsedItems, setParsedItems] = useState([]); @@ -281,6 +283,7 @@ export const AddDownloadsModal = () => { setUrls(''); setParsedItems([]); setSelectedItemIndex(null); + setSelectedQueueId(queues.find(q => q.isMain)?.id || MAIN_QUEUE_ID); } }, [isAddModalOpen, defaultDownloadPath]); @@ -319,10 +322,17 @@ export const AddDownloadsModal = () => { try { new URL(url); if (isMediaUrl(url)) { - const { mediaCookieSource } = useSettingsStore.getState(); + const settingsStore = useSettingsStore.getState(); + const { mediaCookieSource } = settingsStore; const browserArg = mediaCookieSource !== 'none' ? mediaCookieSource : null; + const login = getSiteLogin(url, settingsStore); - const jsonStr = await invoke('fetch_media_metadata', { url, cookieBrowser: browserArg }); + const jsonStr = await invoke('fetch_media_metadata', { + url, + cookieBrowser: browserArg, + username: login?.username || null, + password: login?.password || null + }); const mediaData = parseMediaFormats(jsonStr); if (mediaData && mediaData.formats.length > 0) { updatedItems[i] = { @@ -339,7 +349,13 @@ export const AddDownloadsModal = () => { throw new Error("Invalid media metadata or no formats found"); } } else { - const meta = await invoke<{filename: string, size: string, size_bytes: number}>('fetch_metadata', { url }); + const settingsStore = useSettingsStore.getState(); + const login = getSiteLogin(url, settingsStore); + const meta = await invoke<{filename: string, size: string, size_bytes: number}>('fetch_metadata', { + url, + username: login?.username || null, + password: login?.password || null + }); updatedItems[i] = { url, file: meta.filename, size: meta.size, sizeBytes: meta.size_bytes, status: 'Ready' }; } if (firstReadyIndex === null) firstReadyIndex = i; @@ -423,7 +439,8 @@ export const AddDownloadsModal = () => { headers: headers.trim() || undefined, destination: finalLocation, isMedia: item.isMedia, - mediaFormatSelector: formatSelector + mediaFormatSelector: formatSelector, + queueId: selectedQueueId }); } catch (e) { console.error("Invalid URL or failed to add:", e); @@ -627,9 +644,18 @@ export const AddDownloadsModal = () => {
Transfer Settings
-
-
- +
+
+ + +
+ +
+
setConnections(Number(e.target.value))} className="w-24 accent-blue-500" disabled={parsedItems.some(i => i.isMedia)} /> {connections} diff --git a/apps/desktop/src/components/DownloadTable.tsx b/apps/desktop/src/components/DownloadTable.tsx index 78538c1..2e6ce2e 100644 --- a/apps/desktop/src/components/DownloadTable.tsx +++ b/apps/desktop/src/components/DownloadTable.tsx @@ -44,6 +44,9 @@ export const DownloadTable: React.FC = ({ filter }) => { }; const filteredDownloads = downloads.filter((d: DownloadItem) => { + if (filter.startsWith('queue:')) { + return d.queueId === filter.replace('queue:', ''); + } switch (filter) { case 'all': return true; case 'active': return d.status === 'downloading'; @@ -54,6 +57,11 @@ export const DownloadTable: React.FC = ({ filter }) => { }); const getFilterTitle = () => { + if (filter.startsWith('queue:')) { + const qid = filter.replace('queue:', ''); + const queue = useDownloadStore.getState().queues.find(q => q.id === qid); + return queue ? queue.name : 'Unknown Queue'; + } switch (filter) { case 'all': return 'All Downloads'; case 'active': return 'Active'; diff --git a/apps/desktop/src/components/SchedulerView.tsx b/apps/desktop/src/components/SchedulerView.tsx index ea33a96..9ec57cf 100644 --- a/apps/desktop/src/components/SchedulerView.tsx +++ b/apps/desktop/src/components/SchedulerView.tsx @@ -1,11 +1,11 @@ import { useEffect, useMemo, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { - CheckCircle2, Clock3, List, LockKeyhole, Moon, + CheckCircle2, Clock3, List, Moon, LockKeyhole, Pause, Play, Power, RotateCcw, Save } from 'lucide-react'; import { PostQueueAction, SchedulerSettings, useSettingsStore } from '../store/useSettingsStore'; -import { useDownloadStore } from '../store/useDownloadStore'; +import { useDownloadStore, MAIN_QUEUE_ID } from '../store/useDownloadStore'; import { WindowDragRegion } from './WindowDragRegion'; const days = [ @@ -97,7 +97,7 @@ export default function SchedulerView() { }; const runNow = async () => { - const count = await useDownloadStore.getState().startMainQueue(); + const count = await useDownloadStore.getState().startQueue(MAIN_QUEUE_ID); if (count > 0) { useSettingsStore.getState().setSchedulerRunning(true); setToast(`Started ${count} download${count === 1 ? '' : 's'}`); @@ -107,7 +107,7 @@ export default function SchedulerView() { }; const pauseNow = async () => { - const count = await useDownloadStore.getState().pauseMainQueue(); + const count = await useDownloadStore.getState().pauseQueue(MAIN_QUEUE_ID); useSettingsStore.getState().setSchedulerRunning(false); setToast(count > 0 ? `Paused ${count} active download${count === 1 ? '' : 's'}` : 'No active downloads'); }; diff --git a/apps/desktop/src/components/SettingsView.tsx b/apps/desktop/src/components/SettingsView.tsx index 7167f5b..f43e917 100644 --- a/apps/desktop/src/components/SettingsView.tsx +++ b/apps/desktop/src/components/SettingsView.tsx @@ -25,9 +25,16 @@ export default function SettingsView() { const activeTab = settings.activeSettingsTab; // Local state for versions - const [aria2Version, setAria2Version] = useState('Checking...'); - const [ytdlpVersion, setYtdlpVersion] = useState('Checking...'); - const [ffmpegVersion, setFfmpegVersion] = useState('Checking...'); + const [aria2Version, setAria2Version] = useState('Checking...'); + const [ytdlpVersion, setYtdlpVersion] = useState('Checking...'); + const [ffmpegVersion, setFfmpegVersion] = useState('Checking...'); + const [denoVersion, setDenoVersion] = useState('Checking...'); + + const getEngineStatus = (v: string) => { + if (v === 'Checking...') return Checking...; + if (v.startsWith('Error')) return Error / Missing; + return Ready; + }; // Local state for adding site login const [loginPattern, setLoginPattern] = useState(''); @@ -59,6 +66,10 @@ export default function SettingsView() { invoke('test_ffmpeg') .then(v => setFfmpegVersion(v)) .catch(e => setFfmpegVersion('Error: ' + e)); + + invoke('test_deno') + .then(v => setDenoVersion(v)) + .catch(e => setDenoVersion('Error: ' + e)); } }, [settings.activeView, activeTab]); @@ -626,7 +637,7 @@ export default function SettingsView() {
Status: - Ready + {getEngineStatus(aria2Version)}
@@ -634,17 +645,23 @@ export default function SettingsView() {

Media Extractors

-
+ +
yt-dlp: - {ytdlpVersion} + {ytdlpVersion} + {getEngineStatus(ytdlpVersion)}
-
+ +
FFmpeg: - {ffmpegVersion} + {ffmpegVersion} + {getEngineStatus(ffmpegVersion)}
-
+ +
Deno: - Not Installed (Local Extension Only) + {denoVersion} + {getEngineStatus(denoVersion)}
diff --git a/apps/desktop/src/components/Sidebar.tsx b/apps/desktop/src/components/Sidebar.tsx index d5c104a..6d14790 100644 --- a/apps/desktop/src/components/Sidebar.tsx +++ b/apps/desktop/src/components/Sidebar.tsx @@ -1,14 +1,14 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Inbox, Zap, CheckCircle2, CircleDashed, Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion, - List, CalendarClock, Gauge, Settings, Plus + List, CalendarClock, Gauge, Settings, Plus, Play, Pause, Edit2, Trash2 } from 'lucide-react'; -import { useDownloadStore, DownloadCategory } from '../store/useDownloadStore'; +import { useDownloadStore, DownloadCategory, Queue } from '../store/useDownloadStore'; import { ActiveView, useSettingsStore } from '../store/useSettingsStore'; import { WindowDragRegion } from './WindowDragRegion'; -export type SidebarFilter = 'all' | 'active' | 'completed' | 'unfinished' | DownloadCategory | 'settings'; +export type SidebarFilter = 'all' | 'active' | 'completed' | 'unfinished' | DownloadCategory | 'settings' | string; interface SidebarProps { selectedFilter: SidebarFilter; @@ -16,18 +16,44 @@ interface SidebarProps { } export const Sidebar: React.FC = (props) => { - const selectedFilter = props.selectedFilter; - const onSelectFilter = props.onSelectFilter; - const downloads = useDownloadStore(state => state.downloads); - const activeView = useSettingsStore(state => state.activeView); + const { selectedFilter, onSelectFilter } = props; + const { downloads, queues, addQueue, renameQueue, removeQueue, startQueue, pauseQueue } = useDownloadStore(); + const { activeView, setActiveView } = useSettingsStore(); + + const [isAddingQueue, setIsAddingQueue] = useState(false); + const [newQueueName, setNewQueueName] = useState(''); + const [renamingQueueId, setRenamingQueueId] = useState(null); + const [editingQueueName, setEditingQueueName] = useState(''); + const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null); + + const addInputRef = useRef(null); + const renameInputRef = useRef(null); + + useEffect(() => { + const handleCloseMenu = () => setContextMenu(null); + window.addEventListener('click', handleCloseMenu); + return () => window.removeEventListener('click', handleCloseMenu); + }, []); + + useEffect(() => { + if (isAddingQueue) addInputRef.current?.focus(); + }, [isAddingQueue]); + + useEffect(() => { + if (renamingQueueId) renameInputRef.current?.focus(); + }, [renamingQueueId]); const getCount = (filter: SidebarFilter) => { + if (filter.startsWith('queue:')) { + const qid = filter.replace('queue:', ''); + return downloads.filter(d => d.queueId === qid).length; + } switch (filter) { case 'all': return downloads.length; case 'active': return downloads.filter(d => d.status === 'downloading').length; case 'completed': return downloads.filter(d => d.status === 'completed').length; case 'unfinished': return downloads.filter(d => d.status !== 'completed').length; - default: return downloads.filter(d => d.category === filter).length; + default: return downloads.filter(d => d.category === filter as DownloadCategory).length; } }; @@ -57,12 +83,80 @@ export const Sidebar: React.FC = (props) => { ); }; + const handleQueueContextMenu = (e: React.MouseEvent, id: string) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenu({ x: e.clientX, y: e.clientY, id }); + }; + + const handleAddQueueSubmit = () => { + if (newQueueName.trim()) addQueue(newQueueName.trim()); + setNewQueueName(''); + setIsAddingQueue(false); + }; + + const handleRenameQueueSubmit = () => { + if (renamingQueueId && editingQueueName.trim()) { + renameQueue(renamingQueueId, editingQueueName.trim()); + } + setRenamingQueueId(null); + }; + + const QueueItem = ({ queue }: { queue: Queue }) => { + const filterId = `queue:${queue.id}`; + const isSelected = activeView === 'downloads' && selectedFilter === filterId; + const isRenaming = renamingQueueId === queue.id; + + if (isRenaming) { + return ( +
+ + setEditingQueueName(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameQueueSubmit(); + if (e.key === 'Escape') setRenamingQueueId(null); + }} + onBlur={handleRenameQueueSubmit} + /> +
+ ); + } + + return ( + + ); + }; + const ToolItem = ({ icon: Icon, label, view }: { icon: any; label: string; view: ActiveView }) => { const isSelected = activeView === view; return ( + )}
@@ -118,7 +234,7 @@ export const Sidebar: React.FC = (props) => {
+ + {contextMenu && ( +
e.stopPropagation()} + > + + +
+ + {!queues.find(q => q.id === contextMenu.id)?.isMain && ( + + )} +
+ )} ); }; diff --git a/apps/desktop/src/store/useDownloadStore.ts b/apps/desktop/src/store/useDownloadStore.ts index df72abd..2ccbf2f 100644 --- a/apps/desktop/src/store/useDownloadStore.ts +++ b/apps/desktop/src/store/useDownloadStore.ts @@ -9,7 +9,7 @@ const getProxyArgs = (settings: ReturnType) => return null; }; -const getSiteLogin = (url: string, settings: ReturnType) => { +export const getSiteLogin = (url: string, settings: ReturnType) => { try { const urlObj = new URL(url); const host = urlObj.hostname.toLowerCase(); @@ -79,6 +79,14 @@ const effectiveSpeedLimit = ( export type DownloadStatus = 'downloading' | 'paused' | 'completed' | 'failed' | 'queued'; export type DownloadCategory = 'Documents' | 'Images' | 'Audio' | 'Video' | 'Apps' | 'Archives' | 'Other'; +export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001'; + +export interface Queue { + id: string; + name: string; + isMain: boolean; +} + export interface DownloadItem { id: string; url: string; @@ -99,10 +107,12 @@ export interface DownloadItem { destination?: string; isMedia?: boolean; mediaFormatSelector?: string; + queueId: string; } interface DownloadState { downloads: DownloadItem[]; + queues: Queue[]; isAddModalOpen: boolean; selectedPropertiesDownloadId: string | null; toggleAddModal: (isOpen: boolean) => void; @@ -113,13 +123,17 @@ interface DownloadState { clearFinished: () => void; redownload: (id: string) => void; processQueue: () => Promise; - startMainQueue: () => Promise; - pauseMainQueue: () => Promise; + startQueue: (queueId: string) => Promise; + pauseQueue: (queueId: string) => Promise; + addQueue: (name: string) => void; + renameQueue: (id: string, name: string) => void; + removeQueue: (id: string) => void; restartActiveDownloads: () => Promise; } export const useDownloadStore = create((set, get) => ({ downloads: [], + queues: [{ id: MAIN_QUEUE_ID, name: 'Main Queue', isMain: true }], isAddModalOpen: false, selectedPropertiesDownloadId: null, toggleAddModal: (isOpen) => set({ isAddModalOpen: isOpen }), @@ -184,9 +198,9 @@ export const useDownloadStore = create((set, get) => ({ })); get().processQueue(); }, - startMainQueue: async () => { + startQueue: async (queueId) => { const runnableIds = get().downloads - .filter(item => item.status === 'queued' || item.status === 'paused' || item.status === 'failed') + .filter(item => item.queueId === queueId && (item.status === 'queued' || item.status === 'paused' || item.status === 'failed')) .map(item => item.id); if (runnableIds.length === 0) return 0; @@ -201,9 +215,9 @@ export const useDownloadStore = create((set, get) => ({ await get().processQueue(); return runnableIds.length; }, - pauseMainQueue: async () => { + pauseQueue: async (queueId) => { const activeIds = get().downloads - .filter(item => item.status === 'downloading') + .filter(item => item.queueId === queueId && item.status === 'downloading') .map(item => item.id); if (activeIds.length === 0) return 0; @@ -219,6 +233,20 @@ export const useDownloadStore = create((set, get) => ({ syncSystemIntegrations(); return activeIds.length; }, + addQueue: (name) => { + set((state) => ({ queues: [...state.queues, { id: crypto.randomUUID(), name, isMain: false }] })); + }, + renameQueue: (id, name) => { + set((state) => ({ + queues: state.queues.map(q => q.id === id ? { ...q, name } : q) + })); + }, + removeQueue: (id) => { + set((state) => ({ + queues: state.queues.filter(q => q.id !== id || q.isMain), + downloads: state.downloads.map(d => d.queueId === id ? { ...d, queueId: MAIN_QUEUE_ID } : d) + })); + }, restartActiveDownloads: async () => { const activeIds = get().downloads .filter(item => item.status === 'downloading') @@ -281,7 +309,9 @@ export const useDownloadStore = create((set, get) => ({ destination: destPath, filename: item.fileName, formatSelector: item.mediaFormatSelector || null, - speedLimit + speedLimit, + username: item.username || (login ? login.username : null), + password: item.password || (login ? login.password : null) }); } else { const speedLimit = effectiveSpeedLimit(