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.
This commit is contained in:
NimBold
2026-06-12 23:19:21 +03:30
parent f378fc0d8d
commit 8cd6c1972d
8 changed files with 376 additions and 62 deletions
@@ -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<string>(MAIN_QUEUE_ID);
const [urls, setUrls] = useState('');
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
const [parsedItems, setParsedItems] = useState<ParsedDownloadItem[]>([]);
@@ -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<string>('fetch_media_metadata', { url, cookieBrowser: browserArg });
const jsonStr = await invoke<string>('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 = () => {
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<Settings size={16} className="text-blue-500" /> Transfer Settings
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Connections per File</label>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Target Queue</label>
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="w-32 bg-bg-input border border-border-modal rounded-md px-2 py-1 text-xs text-text-primary focus:border-blue-500 focus:outline-none">
{queues.map(q => (
<option key={q.id} value={q.id}>{q.name}</option>
))}
</select>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Connections per File</label>
<div className="flex items-center gap-2">
<input type="range" min="1" max="16" value={connections} onChange={e=>setConnections(Number(e.target.value))} className="w-24 accent-blue-500" disabled={parsedItems.some(i => i.isMedia)} />
<span className="text-xs text-text-primary font-mono w-4 text-right">{connections}</span>
@@ -44,6 +44,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ 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<DownloadTableProps> = ({ 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';
@@ -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');
};
+27 -10
View File
@@ -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<string>('Checking...');
const [ytdlpVersion, setYtdlpVersion] = useState<string>('Checking...');
const [ffmpegVersion, setFfmpegVersion] = useState<string>('Checking...');
const [denoVersion, setDenoVersion] = useState<string>('Checking...');
const getEngineStatus = (v: string) => {
if (v === 'Checking...') return <span className="text-text-muted font-medium">Checking...</span>;
if (v.startsWith('Error')) return <span className="text-red-500 font-medium">Error / Missing</span>;
return <span className="text-green-500 font-medium">Ready</span>;
};
// Local state for adding site login
const [loginPattern, setLoginPattern] = useState('');
@@ -59,6 +66,10 @@ export default function SettingsView() {
invoke<string>('test_ffmpeg')
.then(v => setFfmpegVersion(v))
.catch(e => setFfmpegVersion('Error: ' + e));
invoke<string>('test_deno')
.then(v => setDenoVersion(v))
.catch(e => setDenoVersion('Error: ' + e));
}
}, [settings.activeView, activeTab]);
@@ -626,7 +637,7 @@ export default function SettingsView() {
</div>
<div className="grid grid-cols-[120px_1fr] text-[13px] items-center">
<span className="text-text-secondary">Status:</span>
<span className="text-green-500 font-medium">Ready</span>
{getEngineStatus(aria2Version)}
</div>
</div>
@@ -634,17 +645,23 @@ export default function SettingsView() {
<h4 className="text-[13px] font-bold text-text-primary flex items-center gap-2 border-b border-border-modal pb-1">
<Terminal size={14} className="text-orange-500" /> Media Extractors
</h4>
<div className="grid grid-cols-[120px_1fr] text-[13px] pb-1">
<div className="grid grid-cols-[120px_1fr_80px] text-[13px] pb-1 items-center">
<span className="text-text-secondary font-semibold">yt-dlp:</span>
<span className="font-mono text-xs text-text-muted select-all">{ytdlpVersion}</span>
<span className="font-mono text-xs text-text-muted select-all truncate pr-4">{ytdlpVersion}</span>
{getEngineStatus(ytdlpVersion)}
</div>
<div className="grid grid-cols-[120px_1fr] text-[13px] pb-1">
<div className="grid grid-cols-[120px_1fr_80px] text-[13px] pb-1 items-center">
<span className="text-text-secondary font-semibold">FFmpeg:</span>
<span className="font-mono text-xs text-text-muted select-all">{ffmpegVersion}</span>
<span className="font-mono text-xs text-text-muted select-all truncate pr-4">{ffmpegVersion}</span>
{getEngineStatus(ffmpegVersion)}
</div>
<div className="grid grid-cols-[120px_1fr] text-[13px] pb-1">
<div className="grid grid-cols-[120px_1fr_80px] text-[13px] pb-1 items-center">
<span className="text-text-secondary font-semibold">Deno:</span>
<span className="text-red-500 text-xs font-semibold">Not Installed (Local Extension Only)</span>
<span className="font-mono text-xs text-text-muted select-all truncate pr-4">{denoVersion}</span>
{getEngineStatus(denoVersion)}
</div>
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px] border-t border-border-modal/50 pt-3 mt-2">
+182 -19
View File
@@ -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<SidebarProps> = (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<string | null>(null);
const [editingQueueName, setEditingQueueName] = useState('');
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const addInputRef = useRef<HTMLInputElement>(null);
const renameInputRef = useRef<HTMLInputElement>(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<SidebarProps> = (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 (
<div className="flex items-center px-2.5 py-1 rounded-lg mb-0.5 bg-item-hover">
<List className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} />
<input
ref={renameInputRef}
type="text"
className="flex-1 bg-transparent border border-accent rounded px-1 text-[13px] text-text-primary outline-none min-w-0"
value={editingQueueName}
onChange={e => setEditingQueueName(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') handleRenameQueueSubmit();
if (e.key === 'Escape') setRenamingQueueId(null);
}}
onBlur={handleRenameQueueSubmit}
/>
</div>
);
}
return (
<button
type="button"
onContextMenu={e => handleQueueContextMenu(e, queue.id)}
onClick={() => onSelectFilter(filterId)}
className={`group flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-left cursor-default transition-colors mb-0.5 ${
isSelected
? 'bg-accent text-white font-medium'
: 'text-text-primary hover:bg-item-hover'
}`}
>
<List className={`w-4 h-4 mr-2 shrink-0 ${isSelected ? 'text-white' : 'text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 2} />
<span className="truncate">{queue.name}</span>
{getCount(filterId) > 0 && (
<span className={`ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-semibold shrink-0 ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary'
}`}>
{getCount(filterId)}
</span>
)}
</button>
);
};
const ToolItem = ({ icon: Icon, label, view }: { icon: any; label: string; view: ActiveView }) => {
const isSelected = activeView === view;
return (
<button
type="button"
onClick={() => useSettingsStore.getState().setActiveView(view)}
onClick={() => setActiveView(view)}
className={`flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-left cursor-default transition-colors mb-0.5 ${
isSelected ? 'bg-accent text-white font-medium' : 'text-text-primary hover:bg-item-hover'
}`}
@@ -98,14 +192,36 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<section className="mb-4">
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Queues</div>
<div className="flex items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-primary hover:bg-item-hover cursor-default transition-colors mb-0.5">
<List className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} />
<span>Main Queue</span>
</div>
<div className="flex items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-secondary hover:bg-item-hover cursor-default transition-colors">
<Plus className="w-4 h-4 mr-2" strokeWidth={2} />
<span>Add new queue</span>
</div>
{queues.map(queue => (
<QueueItem key={queue.id} queue={queue} />
))}
{isAddingQueue ? (
<div className="flex items-center px-2.5 py-1 rounded-lg bg-item-hover">
<Plus className="w-4 h-4 mr-2 text-text-secondary shrink-0" strokeWidth={2} />
<input
ref={addInputRef}
type="text"
placeholder="Queue name"
className="flex-1 bg-transparent border border-accent rounded px-1 text-[13px] text-text-primary outline-none min-w-0"
value={newQueueName}
onChange={e => setNewQueueName(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') handleAddQueueSubmit();
if (e.key === 'Escape') setIsAddingQueue(false);
}}
onBlur={handleAddQueueSubmit}
/>
</div>
) : (
<button
type="button"
onClick={() => { setIsAddingQueue(true); setNewQueueName(''); }}
className="flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-secondary hover:bg-item-hover cursor-default transition-colors"
>
<Plus className="w-4 h-4 mr-2 shrink-0" strokeWidth={2} />
<span className="truncate">Add new queue</span>
</button>
)}
</section>
<section>
@@ -118,7 +234,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<div className="shrink-0 border-t border-border-color bg-sidebar-glass px-2 py-2 backdrop-blur-xl">
<button
type="button"
onClick={() => useSettingsStore.getState().setActiveView('settings')}
onClick={() => setActiveView('settings')}
className={`flex w-full items-center px-2.5 py-2 rounded-lg text-[13px] text-left cursor-default transition-colors ${
activeView === 'settings'
? 'bg-accent text-white font-medium'
@@ -129,6 +245,53 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<span>Settings</span>
</button>
</div>
{contextMenu && (
<div
className="fixed z-50 w-48 py-1 rounded-xl shadow-lg border border-border-modal bg-bg-context-menu backdrop-blur-xl animate-fade-in text-[13px] text-text-primary overflow-hidden"
style={{ top: contextMenu.y, left: contextMenu.x }}
onClick={e => e.stopPropagation()}
>
<button
className="w-full text-left px-3 py-1.5 flex items-center hover:bg-item-hover"
onClick={() => { startQueue(contextMenu.id); setContextMenu(null); }}
>
<Play size={14} className="mr-2 text-text-secondary" />
Start Queue
</button>
<button
className="w-full text-left px-3 py-1.5 flex items-center hover:bg-item-hover"
onClick={() => { pauseQueue(contextMenu.id); setContextMenu(null); }}
>
<Pause size={14} className="mr-2 text-text-secondary" />
Pause Queue
</button>
<div className="h-px bg-border-color my-1 mx-2" />
<button
className="w-full text-left px-3 py-1.5 flex items-center hover:bg-item-hover"
onClick={() => {
const q = queues.find(q => q.id === contextMenu.id);
if (q) {
setEditingQueueName(q.name);
setRenamingQueueId(q.id);
}
setContextMenu(null);
}}
>
<Edit2 size={14} className="mr-2 text-text-secondary" />
Rename Queue
</button>
{!queues.find(q => q.id === contextMenu.id)?.isMain && (
<button
className="w-full text-left px-3 py-1.5 flex items-center hover:bg-red-500/20 text-red-400"
onClick={() => { removeQueue(contextMenu.id); setContextMenu(null); }}
>
<Trash2 size={14} className="mr-2" />
Delete Queue
</button>
)}
</div>
)}
</aside>
);
};