From c82757fc2e07ae1b0966a83f2e7656634a98805c Mon Sep 17 00:00:00 2001 From: NimBold Date: Sun, 14 Jun 2026 09:40:57 +0330 Subject: [PATCH] feat(ui): match Swift download workspace --- apps/desktop/src-tauri/tauri.conf.json | 8 +- apps/desktop/src/App.tsx | 60 ++++-- apps/desktop/src/components/DownloadTable.tsx | 114 ++++++----- apps/desktop/src/index.css | 178 +++++++++++++++--- 4 files changed, 266 insertions(+), 94 deletions(-) diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 827b747..14f96c7 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -13,10 +13,10 @@ "windows": [ { "title": "Firelink", - "width": 1024, - "height": 768, - "minWidth": 800, - "minHeight": 600, + "width": 1280, + "height": 760, + "minWidth": 1180, + "minHeight": 720, "titleBarStyle": "Overlay", "trafficLightPosition": { "x": 17, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index b1e93d2..f58394a 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -33,6 +33,10 @@ const handleDeepLinks = (deepLinks: string[]) => { function App() { const [filter, setFilter] = useState('all'); + const [sidebarWidth, setSidebarWidth] = useState(() => { + const stored = Number(window.localStorage.getItem('firelink-sidebar-width')); + return Number.isFinite(stored) && stored >= 190 && stored <= 260 ? stored : 220; + }); const updateDownload = useDownloadStore(state => state.updateDownload); const theme = useSettingsStore(state => state.theme); const isSidebarVisible = useSettingsStore(state => state.isSidebarVisible); @@ -50,6 +54,31 @@ function App() { const previousSpeedLimit = useRef(globalSpeedLimit); const maxConcurrentDownloads = useSettingsStore(state => state.maxConcurrentDownloads); + const startSidebarResize = (event: React.PointerEvent) => { + event.preventDefault(); + const startX = event.clientX; + const startWidth = sidebarWidth; + + const handlePointerMove = (moveEvent: PointerEvent) => { + const nextWidth = Math.min(260, Math.max(190, startWidth + moveEvent.clientX - startX)); + setSidebarWidth(nextWidth); + }; + + const handlePointerUp = () => { + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + document.body.classList.remove('is-resizing'); + }; + + document.body.classList.add('is-resizing'); + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + }; + + useEffect(() => { + window.localStorage.setItem('firelink-sidebar-width', String(sidebarWidth)); + }, [sidebarWidth]); + useEffect(() => { useDownloadStore.getState().initDB(); }, []); @@ -236,11 +265,12 @@ function App() { return (
-
+
{ @@ -249,6 +279,11 @@ function App() { }} />
+
@@ -260,21 +295,12 @@ function App() {
{/* Status Bar */} -
- - - Ready - +
+ Ready
- - {activeDownloadCount} active - - - {queuedCount} queued - - - {doneCount} done - + {activeDownloadCount} active + {queuedCount} queued + {doneCount} done
diff --git a/apps/desktop/src/components/DownloadTable.tsx b/apps/desktop/src/components/DownloadTable.tsx index 93aea3d..23bce05 100644 --- a/apps/desktop/src/components/DownloadTable.tsx +++ b/apps/desktop/src/components/DownloadTable.tsx @@ -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 } from 'lucide-react'; +import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft, ArrowDownCircle } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { homeDir } from '@tauri-apps/api/path'; @@ -15,6 +15,31 @@ export const DownloadTable: React.FC = ({ filter }) => { const { isSidebarVisible, toggleSidebar } = useSettingsStore(); 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]; + const tableGridTemplate = columnWidths.map(width => `${width}px`).join(' '); + + const startColumnResize = (index: number, event: React.PointerEvent) => { + event.preventDefault(); + event.stopPropagation(); + const startX = event.clientX; + const startWidth = columnWidths[index]; + + const handlePointerMove = (moveEvent: PointerEvent) => { + const nextWidth = Math.max(columnMinimums[index], startWidth + moveEvent.clientX - startX); + setColumnWidths(widths => widths.map((width, columnIndex) => columnIndex === index ? nextWidth : width)); + }; + + const handlePointerUp = () => { + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + document.body.classList.remove('is-resizing'); + }; + + document.body.classList.add('is-resizing'); + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + }; useEffect(() => { const handleCloseMenu = () => setContextMenu(null); @@ -161,42 +186,36 @@ export const DownloadTable: React.FC = ({ filter }) => {
-
-
File Name
-
Size
-
Status
-
Speed
-
ETA
-
Date Added
-
- -
- {filteredDownloads.length === 0 ? ( -
-
-
- - - No downloads yet · Use + to add a link - + {filteredDownloads.length === 0 ? ( +
+
+ ) : ( + <> +
+
+ {['File Name', 'Size', 'Status', 'Speed', 'ETA', 'Date Added'].map((label, index) => ( +
+ {label} +
startColumnResize(index, event)} + />
-
-
Idle
-
-
-
-
- - {Array.from({ length: 12 }).map((_, index) => ( -
))}
- ) : ( + +
{filteredDownloads.map(d => (
{ e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, id: d.id }); @@ -213,25 +232,28 @@ export const DownloadTable: React.FC = ({ filter }) => {
- {d.status === 'downloading' || d.status === 'paused' - ? `${((d.fraction || 0) * parseInt((d.size || '').replace(/[^0-9.]/g, '') || '0')).toFixed(1)} GB / ${d.size || '-'}` - : d.size || '-'} + {d.size && d.size !== '-' ? d.size : 'Unknown'}
-
-
-
- - {d.status === 'downloading' || d.status === 'paused' ? `${((d.fraction || 0) * 100).toFixed(0)}%` : d.status.charAt(0).toUpperCase() + d.status.slice(1)} - -
- {(d.status === 'downloading' || d.status === 'paused') && ( +
+ {d.status === 'completed' ? ( + Completed + ) : ( + <>
-
+
- )} -
+ + {d.status === 'downloading' + ? `${((d.fraction || 0) * 100).toFixed(0)}%` + : d.status.charAt(0).toUpperCase() + d.status.slice(1)} + + + )}
@@ -276,8 +298,10 @@ export const DownloadTable: React.FC = ({ filter }) => {
))}
- )} -
+
+
+ + )}
{/* Floating Context Menu */} diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index 3d8a7c6..340d6e3 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -104,6 +104,12 @@ height: 100%; } + body.is-resizing, + body.is-resizing * { + cursor: col-resize !important; + user-select: none !important; + } + body { margin: 0; background-color: transparent; @@ -304,6 +310,32 @@ background: hsl(0 0% 11%); } + .sidebar-resize-handle { + position: absolute; + top: 18px; + right: -4px; + bottom: 18px; + z-index: 60; + width: 8px; + cursor: col-resize; + } + + .sidebar-resize-handle::after { + content: ''; + position: absolute; + top: 0; + right: 3px; + bottom: 0; + width: 1px; + background: transparent; + transition: background-color 100ms ease; + } + + .sidebar-resize-handle:hover::after, + body.is-resizing .sidebar-resize-handle::after { + background: hsl(var(--accent-color) / 0.55); + } + .app-sidebar-panel { position: relative; display: flex; @@ -599,18 +631,18 @@ } .downloads-content-header { - height: 56px; + height: 58px; display: flex; align-items: center; - padding: 0 18px; + padding: 0 16px; background: hsl(var(--main-bg)); } .downloads-title { display: flex; align-items: center; - gap: 8px; - font-size: 16px; + gap: 9px; + font-size: 17px; line-height: 1; font-weight: 700; color: hsl(var(--text-primary)); @@ -638,17 +670,95 @@ background: hsl(var(--main-bg)); } - .download-table-header { - height: 34px; - display: grid; - grid-template-columns: minmax(260px, 1fr) 132px 220px 126px 100px 190px; + .downloads-empty-state { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; align-items: center; - padding: 0 18px; - border-top: 1px solid hsl(var(--border-color)); - border-bottom: 1px solid hsl(var(--border-color)); + justify-content: center; + padding: 32px; + color: hsl(var(--text-muted)); + text-align: center; + } + + .downloads-empty-state > svg { + width: 38px; + height: 38px; + margin-bottom: 12px; + stroke-width: 1.35; + color: hsl(var(--text-muted)); + } + + .downloads-empty-title { + margin-bottom: 5px; color: hsl(var(--text-secondary)); + font-size: 15px; + font-weight: 600; + } + + .downloads-empty-description { + max-width: 360px; font-size: 12px; - font-weight: 700; + line-height: 1.45; + } + + .keyboard-symbol { + color: hsl(var(--text-secondary)); + font-size: 13px; + font-weight: 600; + } + + .download-table-header { + height: 32px; + display: grid; + align-items: center; + column-gap: 0; + padding: 0 16px; + border-bottom: 1px solid hsl(var(--border-color)); + color: hsl(var(--text-primary)); + font-size: 12px; + font-weight: 600; + } + + .download-table-scroll { + flex: 1; + min-height: 0; + overflow-x: auto; + overflow-y: hidden; + } + + .download-table-header, + .download-row { + width: max-content; + min-width: 100%; + } + + .download-table-header > div { + position: relative; + height: 100%; + display: flex; + align-items: center; + min-width: 0; + } + + .download-table-header > div:not(:first-child) { + border-left: 1px solid hsl(var(--border-color)); + padding-left: 12px; + } + + .column-resize-handle { + position: absolute; + top: 0; + right: -4px; + bottom: 0; + z-index: 5; + width: 8px; + cursor: col-resize; + } + + .column-resize-handle:hover { + background: hsl(var(--accent-color) / 0.18); } .download-table-body { @@ -657,18 +767,17 @@ } .download-row { - height: 34px; + height: 31px; display: grid; - grid-template-columns: minmax(260px, 1fr) 132px 220px 126px 100px 190px; align-items: center; - padding: 0 18px; - border-bottom: 1px solid hsl(0 0% 100% / 0.055); - color: hsl(var(--text-secondary)); + padding: 0 16px; + border-bottom: 1px solid hsl(0 0% 100% / 0.045); + color: hsl(var(--text-primary)); font-size: 12px; } .download-row:nth-child(even) { - background: hsl(0 0% 100% / 0.025); + background: hsl(0 0% 100% / 0.035); } .download-row:hover { @@ -678,7 +787,7 @@ .download-file-cell { display: flex; align-items: center; - gap: 12px; + gap: 9px; min-width: 0; } @@ -688,7 +797,7 @@ text-overflow: ellipsis; white-space: nowrap; color: hsl(var(--text-primary)); - font-weight: 600; + font-weight: 650; } .download-cell-muted { @@ -712,7 +821,7 @@ } .download-ghost-row { - height: 34px; + height: 31px; border-bottom: 1px solid hsl(0 0% 100% / 0.045); } @@ -720,19 +829,28 @@ background: hsl(0 0% 100% / 0.025); } - .download-progress-status { + .download-status-cell { display: flex; align-items: center; - gap: 10px; + gap: 8px; + padding-right: 12px; + } + + .download-status-cell > span { + min-width: 48px; + flex-shrink: 0; + font-size: 10px; + font-variant-numeric: tabular-nums; } .download-progress-track { - width: 128px; - height: 7px; - border-radius: 999px; + width: 100%; + min-width: 58px; + height: 14px; + border-radius: 4px; overflow: hidden; - background: hsl(0 0% 100% / 0.12); - box-shadow: inset 0 1px 1px hsl(0 0% 0% / 0.35); + background: hsl(0 0% 100% / 0.09); + box-shadow: inset 0 1px 1px hsl(0 0% 0% / 0.22); } .download-progress-fill { @@ -745,6 +863,10 @@ background: hsl(28 95% 56%); } + .download-status { + font-weight: 500; + } + .download-status-paused { color: hsl(var(--status-paused)); font-weight: 600;