style(ui): polish desktop interface design and interactions

- Replace abrupt sidebar toggle with modern negative-margin swipe animation
- Match stripe styling to original macOS native application with pill-shaped rows and proper faint opacity
- Remove borders between rows and background stripes
- Add dynamic flex-1 CSS clipping trick to perfectly fill viewport with stripes without causing infinite scroll
- Enhance empty state text with physical-looking shortcut keycaps, Lucide icons, OS detection, and accent colors
- Fix background color seam behind sidebar panel in Dark theme
- Restore bright accent colors for active navigation items
This commit is contained in:
NimBold
2026-06-14 13:11:41 +03:30
parent b1b17b7ad8
commit 77fdc30b80
4 changed files with 79 additions and 38 deletions
+27 -7
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useDownloadStore, DownloadItem } from '../store/useDownloadStore';
import { useSettingsStore } from '../store/useSettingsStore';
import { SidebarFilter } from './Sidebar';
import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft, ArrowDownCircle } from 'lucide-react';
import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft, ArrowDownCircle, Command } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { homeDir } from '@tauri-apps/api/path';
@@ -14,6 +14,8 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const isMac = navigator.userAgent.includes('Mac');
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [columnWidths, setColumnWidths] = useState([340, 100, 220, 100, 80, 170]);
const columnMinimums = [200, 80, 170, 80, 70, 120];
@@ -190,8 +192,18 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="downloads-empty-state">
<ArrowDownCircle aria-hidden="true" />
<div className="downloads-empty-title">No Downloads</div>
<div className="downloads-empty-description">
Use Add or press <span className="keyboard-symbol"></span>V to paste one or more links.
<div className="downloads-empty-description flex items-center justify-center mt-2.5 text-[13px] text-text-muted">
Click <Plus size={15} className="text-accent stroke-[3] mx-1.5" /> button or
<span className="flex items-center mx-1.5">
<span className="flex items-center justify-center px-1.5 py-0.5 bg-item-hover rounded border border-border-color shadow-sm min-w-[22px] min-h-[22px]">
{isMac ? <Command size={12} strokeWidth={2.5} className="text-text-primary" /> : <span className="text-[10px] font-bold text-text-primary">Ctrl</span>}
</span>
<span className="text-accent font-bold mx-1.5 text-[14px]">+</span>
<span className="flex items-center justify-center px-1.5 py-0.5 bg-item-hover rounded border border-border-color shadow-sm min-w-[22px] min-h-[22px]">
<span className="text-[11px] font-bold text-text-primary">V</span>
</span>
</span>
to add downloads
</div>
</div>
) : (
@@ -210,7 +222,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</div>
<div className="download-table-body">
<div className="h-full overflow-auto">
<div className="h-full overflow-auto flex flex-col">
{filteredDownloads.map(d => (
<div
key={d.id}
@@ -294,9 +306,17 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</div>
</div>
))}
{Array.from({ length: Math.max(0, 10 - filteredDownloads.length) }).map((_, index) => (
<div key={`ghost-${index}`} className="download-ghost-row" />
))}
<div className="flex-1 overflow-hidden flex flex-col pointer-events-none">
{Array.from({ length: 50 }).map((_, index) => {
const isEven = (filteredDownloads.length + index) % 2 === 1;
return (
<div
key={`ghost-${index}`}
className={`download-ghost-row ${isEven ? 'striped' : ''}`}
/>
);
})}
</div>
</div>
</div>
</div>
+29 -19
View File
@@ -247,7 +247,7 @@ export default function SettingsView() {
type="number" min="1" max="16"
value={settings.perServerConnections}
onChange={(e) => settings.setPerServerConnections(Number(e.target.value))}
className="app-control w-16 text-center"
className="app-control w-24 text-center"
/>
</div>
<div className="mac-settings-row">
@@ -259,7 +259,7 @@ export default function SettingsView() {
type="number" min="1" max="12"
value={settings.maxConcurrentDownloads}
onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))}
className="app-control w-16 text-center"
className="app-control w-24 text-center"
/>
</div>
<div className="mac-settings-row">
@@ -267,15 +267,15 @@ export default function SettingsView() {
<span>Global speed limit:</span>
<small>0 = unlimited speed</small>
</div>
<div className="flex items-center gap-2">
<input
type="text"
value={settings.globalSpeedLimit}
onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)}
placeholder="0"
className="app-control w-20 text-right font-mono px-2"
/>
<span className="text-[12px] text-text-muted">KiB/s</span>
<div className="relative">
<input
type="text"
value={settings.globalSpeedLimit}
onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)}
placeholder="0"
className="app-control w-24 text-center font-mono pr-9"
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-text-muted pointer-events-none">KiB/s</span>
</div>
</div>
<div className="mac-settings-row">
@@ -287,7 +287,7 @@ export default function SettingsView() {
type="number" min="0" max="10"
value={settings.maxAutomaticRetries}
onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))}
className="app-control w-16 text-center"
className="app-control w-24 text-center"
/>
</div>
</div>
@@ -476,13 +476,23 @@ export default function SettingsView() {
<div className="mac-settings-group">
<div className="mac-settings-row">
<span className="text-[13px] text-text-primary">Custom User Agent</span>
<input
type="text"
value={settings.customUserAgent}
onChange={(e) => settings.setCustomUserAgent(e.target.value)}
placeholder="e.g. Mozilla/5.0..."
className="app-control flex-1 ml-4 font-mono text-[11px]"
/>
<div className="flex-1 ml-4 relative">
<input
type="text"
list="user-agents"
value={settings.customUserAgent}
onChange={(e) => settings.setCustomUserAgent(e.target.value)}
placeholder="e.g. Mozilla/5.0..."
className="app-control w-full font-mono text-[11px]"
/>
<datalist id="user-agents">
<option value="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36">Chrome (Windows)</option>
<option value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36">Chrome (macOS)</option>
<option value="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0">Firefox (Windows)</option>
<option value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0">Firefox (macOS)</option>
<option value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15">Safari (macOS)</option>
</datalist>
</div>
</div>
<p className="settings-group-footer">Spoofs the browser User-Agent to bypass download restrictions. Leave blank for default.</p>
</div>