mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-02 22:17:56 +00:00
feat(ui): modernize add download window
This commit is contained in:
@@ -687,15 +687,30 @@ export const AddDownloadsModal = () => {
|
|||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex flex-col bg-bg-input/50 border border-border-modal/40 rounded-lg p-2.5 shadow-sm">
|
<div className="add-download-summary-card flex flex-col">
|
||||||
<div className="flex items-center gap-1.5 text-text-muted mb-1">
|
<div className="flex items-center gap-1.5 text-text-muted mb-1">
|
||||||
<Icon size={12} className={color} />
|
<Icon size={12} className={color} />
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider">{title}</span>
|
<span className="text-[10px] font-semibold uppercase tracking-wider">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-semibold text-text-primary truncate">{value}</span>
|
<span className="text-sm font-semibold text-text-primary truncate">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectMediaFormat = (index: number) => {
|
||||||
|
if (selectedItemIndex === null) return;
|
||||||
|
const newItems = [...parsedItems];
|
||||||
|
const selectedItem = newItems[selectedItemIndex];
|
||||||
|
const format = selectedItem?.formats?.[index];
|
||||||
|
if (!selectedItem || !format) return;
|
||||||
|
|
||||||
|
selectedItem.selectedFormat = index;
|
||||||
|
selectedItem.size = format.detail || 'Unknown';
|
||||||
|
selectedItem.sizeBytes = format.bytes || 0;
|
||||||
|
const baseName = selectedItem.file.substring(0, selectedItem.file.lastIndexOf('.')) || selectedItem.file;
|
||||||
|
selectedItem.file = `${baseName}.${format.ext}`;
|
||||||
|
setParsedItems(newItems);
|
||||||
|
};
|
||||||
|
|
||||||
const requiredBytes = parsedItems.reduce((acc, item) => acc + (item.sizeBytes || 0), 0);
|
const requiredBytes = parsedItems.reduce((acc, item) => acc + (item.sizeBytes || 0), 0);
|
||||||
const requiredStr = requiredBytes > 0
|
const requiredStr = requiredBytes > 0
|
||||||
? (requiredBytes < 1024 * 1024 ? `${(requiredBytes / 1024).toFixed(1)} KB`
|
? (requiredBytes < 1024 * 1024 ? `${(requiredBytes / 1024).toFixed(1)} KB`
|
||||||
@@ -716,24 +731,24 @@ export const AddDownloadsModal = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
|
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div className="app-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
|
<div className="app-modal add-download-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
|
||||||
|
|
||||||
{/* Main Content Split */}
|
{/* Main Content Split */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
|
||||||
{/* Left Column: URLs and Preview */}
|
{/* Left Column: URLs and Preview */}
|
||||||
<div className="w-[55%] border-r border-border-modal flex flex-col bg-main-bg/50">
|
<div className="add-download-left w-[55%] border-r border-border-modal flex flex-col">
|
||||||
<div className="p-5 flex-1 flex flex-col gap-5">
|
<div className="add-download-pane p-5 flex-1 flex flex-col gap-5">
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-text-primary font-semibold">
|
<div className="add-download-section-title flex items-center gap-2">
|
||||||
<Link size={16} className="text-blue-500" />
|
<Link size={16} className="text-blue-500" />
|
||||||
Download Links
|
Download Links
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-32 bg-bg-input/80 border border-border-modal rounded-lg p-3 text-[13px] text-text-primary focus:outline-none focus:border-accent resize-none font-mono shadow-inner transition-colors"
|
className="add-download-control add-download-links-input w-full h-32 p-3 text-[13px] resize-none font-mono"
|
||||||
placeholder="Paste HTTP, HTTPS, FTP, or SFTP URLs here..."
|
placeholder="Paste HTTP, HTTPS, FTP, or SFTP URLs here..."
|
||||||
value={urls}
|
value={urls}
|
||||||
onChange={(e) => setUrls(e.target.value)}
|
onChange={(e) => setUrls(e.target.value)}
|
||||||
@@ -743,7 +758,7 @@ export const AddDownloadsModal = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMetadataRefreshNonce(value => value + 1)}
|
onClick={() => setMetadataRefreshNonce(value => value + 1)}
|
||||||
className="flex items-center gap-1.5 text-[11px] text-blue-500 hover:text-blue-400 font-medium"
|
className="add-download-link-button flex items-center gap-1.5 text-[11px] font-medium"
|
||||||
>
|
>
|
||||||
<RefreshCw size={12} /> Refresh Metadata
|
<RefreshCw size={12} /> Refresh Metadata
|
||||||
</button>
|
</button>
|
||||||
@@ -758,19 +773,19 @@ export const AddDownloadsModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 flex-1 overflow-hidden">
|
<div className="flex flex-col gap-2 flex-1 overflow-hidden">
|
||||||
<div className="flex items-center gap-2 text-text-primary font-semibold">
|
<div className="add-download-section-title flex items-center gap-2">
|
||||||
<ArrowRight size={16} className="text-blue-500" />
|
<ArrowRight size={16} className="text-blue-500" />
|
||||||
Preview
|
Preview
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border-modal rounded-lg overflow-hidden bg-bg-input/30 flex flex-col">
|
<div className="add-download-preview flex-1 overflow-hidden flex flex-col">
|
||||||
<div className="bg-sidebar-bg/50 border-b border-border-modal px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
|
<div className="add-download-preview-header px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
|
||||||
<div className="flex-[2]">File</div>
|
<div className="flex-[2]">File</div>
|
||||||
<div className="flex-1">Size</div>
|
<div className="flex-1">Size</div>
|
||||||
<div className="flex-[1.5]">Status</div>
|
<div className="flex-[1.5]">Status</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
<div className="flex-1 overflow-y-auto p-2 space-y-1" role="listbox" aria-label="Download preview">
|
||||||
{parsedItems.length === 0 ? (
|
{parsedItems.length === 0 ? (
|
||||||
<div className="h-full flex items-center justify-center text-text-muted text-xs italic">
|
<div className="add-download-empty h-full flex items-center justify-center text-text-muted text-xs">
|
||||||
No links added yet.
|
No links added yet.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -778,10 +793,19 @@ export const AddDownloadsModal = () => {
|
|||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setSelectedItemIndex(i)}
|
onClick={() => setSelectedItemIndex(i)}
|
||||||
className={`flex flex-col text-xs px-2 py-2 cursor-pointer rounded-md transition-all group ${
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
setSelectedItemIndex(i);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="option"
|
||||||
|
aria-selected={selectedItemIndex === i}
|
||||||
|
tabIndex={0}
|
||||||
|
className={`add-download-preview-row flex flex-col text-xs px-2 py-2 cursor-pointer rounded-md group ${
|
||||||
selectedItemIndex === i
|
selectedItemIndex === i
|
||||||
? 'bg-blue-500/10 border border-blue-500/30 shadow-sm'
|
? 'is-selected border'
|
||||||
: 'hover:bg-item-hover border border-transparent'
|
: 'border border-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
@@ -808,16 +832,16 @@ export const AddDownloadsModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Settings */}
|
{/* Right Column: Settings */}
|
||||||
<div className="w-[45%] flex flex-col overflow-y-auto bg-bg-modal">
|
<div className="add-download-settings w-[45%] flex flex-col overflow-y-auto">
|
||||||
<div className="p-6 space-y-7">
|
<div className="p-6 space-y-5">
|
||||||
|
|
||||||
{/* Media Format (Dynamic) */}
|
{/* Media Format (Dynamic) */}
|
||||||
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
|
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
|
||||||
<section className="app-card relative overflow-hidden p-4">
|
<section className="add-download-section add-download-media-section relative overflow-hidden p-4">
|
||||||
<div className="absolute top-0 right-0 p-2 opacity-10">
|
<div className="absolute top-0 right-0 p-2 opacity-10">
|
||||||
<Video size={48} />
|
<Video size={48} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3 relative z-10">
|
<div className="add-download-section-title flex items-center gap-2 mb-3 relative z-10">
|
||||||
<Video size={16} className="text-purple-500" /> Media Format
|
<Video size={16} className="text-purple-500" /> Media Format
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -830,25 +854,25 @@ export const AddDownloadsModal = () => {
|
|||||||
<div className="space-y-3 relative z-10">
|
<div className="space-y-3 relative z-10">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] uppercase font-bold tracking-wider text-text-muted">Available Streams</label>
|
<label className="text-[10px] uppercase font-bold tracking-wider text-text-muted">Available Streams</label>
|
||||||
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1">
|
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1" role="radiogroup" aria-label="Available media streams">
|
||||||
{parsedItems[selectedItemIndex].formats!.map((f, idx) => {
|
{parsedItems[selectedItemIndex].formats!.map((f, idx) => {
|
||||||
const isSelected = parsedItems[selectedItemIndex].selectedFormat === idx;
|
const isSelected = parsedItems[selectedItemIndex].selectedFormat === idx;
|
||||||
const Icon = f.type === 'Audio' ? Music : Film;
|
const Icon = f.type === 'Audio' ? Music : Film;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => {
|
onClick={() => selectMediaFormat(idx)}
|
||||||
const newItems = [...parsedItems];
|
onKeyDown={(event) => {
|
||||||
newItems[selectedItemIndex].selectedFormat = idx;
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
newItems[selectedItemIndex].size = f.detail || 'Unknown';
|
event.preventDefault();
|
||||||
newItems[selectedItemIndex].sizeBytes = f.bytes || 0;
|
selectMediaFormat(idx);
|
||||||
// Update filename extension
|
}
|
||||||
const baseName = newItems[selectedItemIndex].file.substring(0, newItems[selectedItemIndex].file.lastIndexOf('.')) || newItems[selectedItemIndex].file;
|
|
||||||
newItems[selectedItemIndex].file = `${baseName}.${f.ext}`;
|
|
||||||
setParsedItems(newItems);
|
|
||||||
}}
|
}}
|
||||||
className={`flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer text-xs border transition-all ${
|
role="radio"
|
||||||
isSelected ? 'bg-purple-500/10 border-purple-500/30 text-purple-600 dark:text-purple-400 font-semibold shadow-sm' : 'bg-bg-input border-border-modal text-text-secondary hover:border-border-modal/80 hover:bg-item-hover/50'
|
aria-checked={isSelected}
|
||||||
|
tabIndex={0}
|
||||||
|
className={`add-download-format-row flex items-center justify-between px-3 py-2 cursor-pointer text-xs border ${
|
||||||
|
isSelected ? 'is-selected text-purple-600 dark:text-purple-400 font-semibold' : 'text-text-secondary'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@@ -874,8 +898,8 @@ export const AddDownloadsModal = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Save Location */}
|
{/* Save Location */}
|
||||||
<section>
|
<section className="add-download-section">
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
|
<div className="add-download-section-title flex items-center gap-2 mb-3">
|
||||||
<FolderPlus size={16} className="text-blue-500" /> Save Location
|
<FolderPlus size={16} className="text-blue-500" /> Save Location
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -883,11 +907,12 @@ export const AddDownloadsModal = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
value={saveLocation}
|
value={saveLocation}
|
||||||
className="flex-1 bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs text-text-muted font-mono"
|
className="add-download-control flex-1 px-3 py-1.5 text-xs text-text-muted font-mono"
|
||||||
|
aria-label="Save location"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleBrowse}
|
onClick={handleBrowse}
|
||||||
className="bg-item-hover hover:bg-item-hover/80 text-text-primary border border-border-modal px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
className="add-download-button add-download-button-secondary px-3 text-xs font-medium"
|
||||||
>
|
>
|
||||||
Browse
|
Browse
|
||||||
</button>
|
</button>
|
||||||
@@ -895,14 +920,14 @@ export const AddDownloadsModal = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Transfer Settings */}
|
{/* Transfer Settings */}
|
||||||
<section>
|
<section className="add-download-section">
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
|
<div className="add-download-section-title flex items-center gap-2 mb-3">
|
||||||
<Settings size={16} className="text-blue-500" /> Transfer Settings
|
<Settings size={16} className="text-blue-500" /> Transfer Settings
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs text-text-secondary font-medium">Target Queue</label>
|
<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-lg px-2 py-1 text-xs text-text-primary focus:border-accent focus:outline-none">
|
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="add-download-control add-download-select w-32 px-2 py-1 text-xs" aria-label="Target queue">
|
||||||
{queues.map(q => (
|
{queues.map(q => (
|
||||||
<option key={q.id} value={q.id}>{q.name}</option>
|
<option key={q.id} value={q.id}>{q.name}</option>
|
||||||
))}
|
))}
|
||||||
@@ -912,19 +937,19 @@ export const AddDownloadsModal = () => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs text-text-secondary font-medium">Connections per File</label>
|
<label className="text-xs text-text-secondary font-medium">Connections per File</label>
|
||||||
<div className="flex items-center gap-2">
|
<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)} />
|
<input type="range" min="1" max="16" value={connections} onChange={e=>setConnections(Number(e.target.value))} className="add-download-range w-24" disabled={parsedItems.some(i => i.isMedia)} aria-label="Connections per file" />
|
||||||
<span className="text-xs text-text-primary font-mono w-4 text-right">{connections}</span>
|
<span className="add-download-value text-xs text-text-primary font-mono w-6 text-center">{connections}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
|
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
|
||||||
<input type="checkbox" checked={speedLimitEnabled} onChange={e=>setSpeedLimitEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
|
<input type="checkbox" checked={speedLimitEnabled} onChange={e=>setSpeedLimitEnabled(e.target.checked)} className="add-download-checkbox" />
|
||||||
Limit speed per file
|
Limit speed per file
|
||||||
</label>
|
</label>
|
||||||
{speedLimitEnabled && (
|
{speedLimitEnabled && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
|
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="add-download-control w-16 px-2 py-1 text-xs font-mono" aria-label="Speed limit per file" />
|
||||||
<span className="text-[10px] text-text-muted">KiB/s</span>
|
<span className="text-[10px] text-text-muted">KiB/s</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -933,28 +958,29 @@ export const AddDownloadsModal = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Authorization */}
|
{/* Authorization */}
|
||||||
<section>
|
<section className="add-download-section">
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
|
<div className="add-download-section-title flex items-center gap-2 mb-3">
|
||||||
<Shield size={16} className="text-blue-500" /> Authorization
|
<Shield size={16} className="text-blue-500" /> Authorization
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer mb-3">
|
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer mb-3">
|
||||||
<input type="checkbox" checked={useAuth} onChange={e=>setUseAuth(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
|
<input type="checkbox" checked={useAuth} onChange={e=>setUseAuth(e.target.checked)} className="add-download-checkbox" />
|
||||||
Use authorization
|
Use authorization
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{useAuth && (
|
{useAuth && (
|
||||||
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
|
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
|
||||||
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
|
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="add-download-control w-full px-3 py-1.5 text-xs" />
|
||||||
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
|
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="add-download-control w-full px-3 py-1.5 text-xs" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Advanced */}
|
{/* Advanced */}
|
||||||
<section className="pt-2 border-t border-border-modal/50">
|
<section className="add-download-section add-download-advanced">
|
||||||
<button
|
<button
|
||||||
onClick={() => setAdvancedExpanded(!advancedExpanded)}
|
onClick={() => setAdvancedExpanded(!advancedExpanded)}
|
||||||
className="flex items-center gap-2 text-sm font-semibold text-text-primary w-full hover:text-blue-500 transition-colors"
|
className="add-download-advanced-toggle flex items-center gap-2 text-sm font-semibold text-text-primary w-full"
|
||||||
|
aria-expanded={advancedExpanded}
|
||||||
>
|
>
|
||||||
{advancedExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
{advancedExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
Advanced Transfer
|
Advanced Transfer
|
||||||
@@ -963,30 +989,30 @@ export const AddDownloadsModal = () => {
|
|||||||
{advancedExpanded && (
|
{advancedExpanded && (
|
||||||
<div className="mt-4 space-y-4 pl-6">
|
<div className="mt-4 space-y-4 pl-6">
|
||||||
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
|
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
|
||||||
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
|
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="add-download-checkbox" />
|
||||||
Verify Checksum
|
Verify Checksum
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{checksumEnabled && (
|
{checksumEnabled && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="w-24 bg-bg-input border border-border-modal rounded-lg px-2 text-xs text-text-primary focus:border-accent focus:outline-none">
|
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="add-download-control add-download-select w-24 px-2 text-xs" aria-label="Checksum algorithm">
|
||||||
<option>MD5</option><option>SHA-1</option><option>SHA-256</option>
|
<option>MD5</option><option>SHA-1</option><option>SHA-256</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="flex-1 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
|
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="add-download-control flex-1 px-3 py-1.5 text-xs font-mono" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Headers</label>
|
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Headers</label>
|
||||||
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
|
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none" aria-label="Request headers" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Cookies</label>
|
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Cookies</label>
|
||||||
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
|
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="add-download-control w-full px-3 py-1.5 text-xs font-mono" aria-label="Cookies" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Mirrors</label>
|
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Mirrors</label>
|
||||||
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
|
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none" aria-label="Mirrors" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -997,25 +1023,25 @@ export const AddDownloadsModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="p-4 bg-sidebar-bg/50 border-t border-border-modal flex items-center shrink-0">
|
<div className="add-download-footer p-4 flex items-center shrink-0">
|
||||||
<div className="text-[11px] text-text-muted font-medium flex-1">
|
<div className="text-[11px] text-text-muted font-medium flex-1">
|
||||||
{parsedItems.length === 0 ? "Paste one or more links." : `Ready to add ${parsedItems.length} download(s).`}
|
{parsedItems.length === 0 ? "Paste one or more links." : `Ready to add ${parsedItems.length} download(s).`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
<button onClick={() => toggleAddModal(false)} className="app-button border-transparent bg-transparent px-4 text-xs text-text-secondary">
|
<button onClick={() => toggleAddModal(false)} className="add-download-button add-download-button-cancel px-4 text-xs">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleStart(false)}
|
onClick={() => handleStart(false)}
|
||||||
disabled={parsedItems.length === 0}
|
disabled={parsedItems.length === 0}
|
||||||
className="app-button px-4 text-xs disabled:opacity-50"
|
className="add-download-button add-download-button-secondary px-4 text-xs"
|
||||||
>
|
>
|
||||||
Add to Queue
|
Add to Queue
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleStart(true)}
|
onClick={() => handleStart(true)}
|
||||||
disabled={parsedItems.length === 0}
|
disabled={parsedItems.length === 0}
|
||||||
className="app-button app-button-primary px-5 text-xs disabled:opacity-50"
|
className="add-download-button add-download-button-primary px-5 text-xs"
|
||||||
>
|
>
|
||||||
<Play size={12} fill="currentColor" /> Start Downloads
|
<Play size={12} fill="currentColor" /> Start Downloads
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+347
@@ -376,6 +376,353 @@
|
|||||||
animation: modal-in 150ms ease-out;
|
animation: modal-in 150ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add Download window */
|
||||||
|
.add-download-modal {
|
||||||
|
--add-surface: hsl(var(--surface-raised) / 0.72);
|
||||||
|
--add-highlight: hsl(0 0% 100% / 0.08);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, hsl(var(--surface-overlay) / 0.94), hsl(var(--bg-modal) / 0.98));
|
||||||
|
backdrop-filter: blur(22px) saturate(1.12);
|
||||||
|
-webkit-backdrop-filter: blur(22px) saturate(1.12);
|
||||||
|
border-color: hsl(var(--border-modal));
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.08),
|
||||||
|
0 28px 70px hsl(var(--shadow-color)),
|
||||||
|
0 2px 10px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-left {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 0%, hsl(var(--accent-color) / 0.055), transparent 34%),
|
||||||
|
hsl(var(--main-bg) / 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-settings {
|
||||||
|
background: hsl(var(--bg-modal) / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-section-title {
|
||||||
|
color: hsl(var(--text-primary));
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-section {
|
||||||
|
border: 1px solid hsl(var(--border-modal));
|
||||||
|
border-radius: 11px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, var(--add-highlight), transparent 38%),
|
||||||
|
var(--add-surface);
|
||||||
|
padding: 14px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.045),
|
||||||
|
0 1px 2px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-control {
|
||||||
|
min-height: 30px;
|
||||||
|
border: 1px solid hsl(var(--border-modal));
|
||||||
|
border-radius: 7px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, hsl(0 0% 100% / 0.035), transparent),
|
||||||
|
hsl(var(--bg-input) / 0.9);
|
||||||
|
color: hsl(var(--text-primary));
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 2px hsl(var(--shadow-color)),
|
||||||
|
0 1px 0 hsl(0 0% 100% / 0.035);
|
||||||
|
transition:
|
||||||
|
border-color 120ms ease,
|
||||||
|
background-color 120ms ease,
|
||||||
|
box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-control:hover:not(:disabled):not(:read-only) {
|
||||||
|
border-color: hsl(var(--text-muted) / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-control:focus,
|
||||||
|
.add-download-control:focus-visible {
|
||||||
|
border-color: hsl(var(--accent-color) / 0.9);
|
||||||
|
outline: none;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px hsl(var(--accent-color) / 0.18),
|
||||||
|
inset 0 1px 2px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-control:read-only {
|
||||||
|
background: hsl(var(--bg-input) / 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-control::placeholder {
|
||||||
|
color: hsl(var(--text-muted) / 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-links-input {
|
||||||
|
border-radius: 10px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-link-button {
|
||||||
|
border-radius: 6px;
|
||||||
|
color: hsl(var(--accent-color));
|
||||||
|
padding: 3px 5px;
|
||||||
|
transition: background-color 120ms ease, color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-link-button:hover {
|
||||||
|
background: hsl(var(--accent-color) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-summary-card {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 11px;
|
||||||
|
border: 1px solid hsl(var(--border-modal));
|
||||||
|
border-radius: 10px;
|
||||||
|
background:
|
||||||
|
linear-gradient(145deg, hsl(0 0% 100% / 0.055), transparent 55%),
|
||||||
|
var(--add-surface);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.045),
|
||||||
|
0 1px 2px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-preview {
|
||||||
|
border: 1px solid hsl(var(--border-modal));
|
||||||
|
border-radius: 11px;
|
||||||
|
background: hsl(var(--bg-input) / 0.35);
|
||||||
|
box-shadow: inset 0 1px 2px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-preview-header {
|
||||||
|
border-bottom: 1px solid hsl(var(--border-modal));
|
||||||
|
background: hsl(var(--surface-raised) / 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-preview-row {
|
||||||
|
transition:
|
||||||
|
background-color 100ms ease,
|
||||||
|
border-color 100ms ease,
|
||||||
|
box-shadow 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-preview-row:hover {
|
||||||
|
background: hsl(var(--item-hover));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-preview-row.is-selected {
|
||||||
|
border-color: hsl(var(--accent-color) / 0.34);
|
||||||
|
background: hsl(var(--accent-color) / 0.11);
|
||||||
|
box-shadow: inset 3px 0 0 hsl(var(--accent-color) / 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-preview-row:focus-visible,
|
||||||
|
.add-download-format-row:focus-visible {
|
||||||
|
outline: 2px solid hsl(var(--accent-color) / 0.6);
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-empty {
|
||||||
|
color: hsl(var(--text-muted) / 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-media-section {
|
||||||
|
border-color: hsl(272 70% 62% / 0.28);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 100% 0%, hsl(272 75% 60% / 0.09), transparent 46%),
|
||||||
|
var(--add-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-format-row {
|
||||||
|
border-color: hsl(var(--border-modal));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: hsl(var(--bg-input) / 0.7);
|
||||||
|
transition:
|
||||||
|
background-color 100ms ease,
|
||||||
|
border-color 100ms ease,
|
||||||
|
box-shadow 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-format-row:hover {
|
||||||
|
background: hsl(var(--item-hover));
|
||||||
|
border-color: hsl(var(--text-muted) / 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-format-row.is-selected {
|
||||||
|
border-color: hsl(272 74% 62% / 0.42);
|
||||||
|
background: hsl(272 74% 58% / 0.12);
|
||||||
|
box-shadow: inset 3px 0 0 hsl(272 74% 62% / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-select {
|
||||||
|
appearance: auto;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-value {
|
||||||
|
min-width: 25px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border: 1px solid hsl(var(--border-modal));
|
||||||
|
border-radius: 5px;
|
||||||
|
background: hsl(var(--bg-input) / 0.68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-checkbox {
|
||||||
|
appearance: none;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
flex: 0 0 15px;
|
||||||
|
border: 1px solid hsl(var(--border-modal));
|
||||||
|
border-radius: 4px;
|
||||||
|
background: hsl(var(--bg-input) / 0.92);
|
||||||
|
box-shadow: inset 0 1px 1px hsl(var(--shadow-color));
|
||||||
|
cursor: default;
|
||||||
|
transition:
|
||||||
|
border-color 100ms ease,
|
||||||
|
background-color 100ms ease,
|
||||||
|
box-shadow 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-checkbox:hover {
|
||||||
|
border-color: hsl(var(--text-muted) / 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-checkbox:checked {
|
||||||
|
border-color: hsl(var(--accent-color));
|
||||||
|
background-color: hsl(var(--accent-color));
|
||||||
|
background-image:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='m2.4 6.2 2.2 2.2 5-5' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-checkbox:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px hsl(var(--accent-color) / 0.2),
|
||||||
|
inset 0 1px 1px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-range {
|
||||||
|
height: 16px;
|
||||||
|
accent-color: hsl(var(--accent-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-range:disabled {
|
||||||
|
opacity: 0.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-advanced {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-advanced-toggle {
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 100ms ease, color 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-advanced-toggle:hover {
|
||||||
|
background: hsl(var(--item-hover));
|
||||||
|
color: hsl(var(--accent-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-advanced > div {
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-footer {
|
||||||
|
border-top: 1px solid hsl(var(--border-modal));
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, hsl(0 0% 100% / 0.035), transparent),
|
||||||
|
hsl(var(--surface-raised) / 0.78);
|
||||||
|
box-shadow: 0 -1px 0 hsl(0 0% 100% / 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 30px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: hsl(var(--text-primary));
|
||||||
|
font-weight: 560;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
transition:
|
||||||
|
background-color 100ms ease,
|
||||||
|
border-color 100ms ease,
|
||||||
|
box-shadow 100ms ease,
|
||||||
|
transform 70ms ease,
|
||||||
|
opacity 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button:active:not(:disabled) {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button-secondary {
|
||||||
|
border-color: hsl(var(--border-modal));
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, hsl(0 0% 100% / 0.055), transparent),
|
||||||
|
hsl(var(--bg-input) / 0.88);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.055),
|
||||||
|
0 1px 2px hsl(var(--shadow-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button-secondary:hover:not(:disabled) {
|
||||||
|
border-color: hsl(var(--text-muted) / 0.65);
|
||||||
|
background:
|
||||||
|
linear-gradient(hsl(var(--item-hover)), hsl(var(--item-hover))),
|
||||||
|
hsl(var(--bg-input));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button-cancel {
|
||||||
|
color: hsl(var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button-cancel:hover:not(:disabled) {
|
||||||
|
background: hsl(var(--item-hover));
|
||||||
|
color: hsl(var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button-primary {
|
||||||
|
border-color: hsl(var(--accent-color) / 0.86);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, hsl(0 0% 100% / 0.16), transparent 52%),
|
||||||
|
hsl(var(--accent-color));
|
||||||
|
color: white;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.18),
|
||||||
|
0 1px 2px hsl(var(--accent-color) / 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button-primary:hover:not(:disabled) {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, hsl(0 0% 100% / 0.2), transparent 52%),
|
||||||
|
hsl(var(--accent-color) / 0.92);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.2),
|
||||||
|
0 2px 5px hsl(var(--accent-color) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button:disabled {
|
||||||
|
opacity: 0.42;
|
||||||
|
filter: saturate(0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-download-button:focus-visible,
|
||||||
|
.add-download-link-button:focus-visible,
|
||||||
|
.add-download-advanced-toggle:focus-visible {
|
||||||
|
outline: 2px solid hsl(var(--accent-color) / 0.65);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.app-toast {
|
.app-toast {
|
||||||
border: 1px solid hsl(var(--border-modal));
|
border: 1px solid hsl(var(--border-modal));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user