mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -265,10 +265,13 @@ function App() {
|
||||
return (
|
||||
<div className="app-shell flex h-screen w-screen overflow-hidden text-text-primary">
|
||||
<div
|
||||
className={`app-sidebar-shell relative z-20 shrink-0 ${
|
||||
isSidebarVisible ? 'opacity-100' : 'w-0 opacity-0 pointer-events-none'
|
||||
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
|
||||
isSidebarVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
style={isSidebarVisible ? { width: sidebarWidth } : undefined}
|
||||
style={{
|
||||
width: sidebarWidth,
|
||||
marginLeft: isSidebarVisible ? 0 : -sidebarWidth
|
||||
}}
|
||||
>
|
||||
<div className="app-sidebar-panel h-full w-full">
|
||||
<Sidebar
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
--item-hover: 0 0% 0% / 0.05;
|
||||
--item-selected: 211 100% 50%;
|
||||
--accent-color: 211 100% 50%;
|
||||
--stripe-bg: 0 0% 0% / 0.04;
|
||||
|
||||
--text-primary: 0 0% 10%;
|
||||
--text-secondary: 0 0% 40%;
|
||||
@@ -36,6 +37,7 @@
|
||||
--item-hover: 0 0% 0% / 0.05;
|
||||
--item-selected: 211 100% 50%;
|
||||
--accent-color: 211 100% 50%;
|
||||
--stripe-bg: 0 0% 0% / 0.04;
|
||||
--text-primary: 0 0% 10%;
|
||||
--text-secondary: 0 0% 40%;
|
||||
--text-muted: 0 0% 55%;
|
||||
@@ -55,7 +57,7 @@
|
||||
|
||||
.theme-dark {
|
||||
color-scheme: dark;
|
||||
--main-bg: 0 0% 10%;
|
||||
--main-bg: 0 0% 11%;
|
||||
--sidebar-bg: 0 0% 13%;
|
||||
--sidebar-glass: 0 0% 13%;
|
||||
--surface-raised: 0 0% 12%;
|
||||
@@ -65,6 +67,7 @@
|
||||
--item-hover: 0 0% 100% / 0.07;
|
||||
--item-selected: 211 100% 56%;
|
||||
--accent-color: 211 100% 56%;
|
||||
--stripe-bg: 0 0% 100% / 0.04;
|
||||
--text-primary: 0 0% 91%;
|
||||
--text-secondary: 0 0% 75%;
|
||||
--text-muted: 0 0% 50%;
|
||||
@@ -95,6 +98,7 @@
|
||||
--item-hover: 326 100% 74% / 0.10;
|
||||
--item-selected: 326 100% 74%;
|
||||
--accent-color: 326 100% 74%;
|
||||
--stripe-bg: 0 0% 100% / 0.04;
|
||||
--text-primary: 60 30% 96%;
|
||||
--text-secondary: 228 14% 74%;
|
||||
--text-muted: 229 12% 58%;
|
||||
@@ -125,6 +129,7 @@
|
||||
--item-hover: 193 43% 67% / 0.12;
|
||||
--item-selected: 193 43% 67%;
|
||||
--accent-color: 193 43% 67%;
|
||||
--stripe-bg: 0 0% 100% / 0.04;
|
||||
--text-primary: 218 27% 92%;
|
||||
--text-secondary: 219 28% 80%;
|
||||
--text-muted: 218 17% 63%;
|
||||
@@ -376,7 +381,7 @@
|
||||
|
||||
.app-sidebar-shell {
|
||||
padding: 9px 0 9px 9px;
|
||||
background: hsl(var(--sidebar-shell-bg));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle {
|
||||
@@ -960,17 +965,18 @@
|
||||
}
|
||||
|
||||
.download-row {
|
||||
height: 31px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid hsl(var(--border-color));
|
||||
margin: 2px 12px;
|
||||
border-radius: 6px;
|
||||
color: hsl(var(--text-primary));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.download-row:nth-child(even) {
|
||||
background: hsl(var(--item-hover));
|
||||
background: hsl(var(--stripe-bg));
|
||||
}
|
||||
|
||||
.download-row:hover {
|
||||
@@ -1014,12 +1020,14 @@
|
||||
}
|
||||
|
||||
.download-ghost-row {
|
||||
height: 31px;
|
||||
border-bottom: 1px solid hsl(var(--border-color));
|
||||
height: 32px;
|
||||
margin: 2px 12px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.download-ghost-row:nth-child(even) {
|
||||
background: hsl(var(--item-hover));
|
||||
.download-ghost-row.striped {
|
||||
background: hsl(var(--stripe-bg));
|
||||
}
|
||||
|
||||
.download-status-cell {
|
||||
|
||||
Reference in New Issue
Block a user