From 23878c2ef818ec56e2fa82ab848bc3ca8e66d966 Mon Sep 17 00:00:00 2001 From: NimBold Date: Sun, 19 Jul 2026 03:10:42 +0330 Subject: [PATCH] fix(ui): harden sidebar and custom window controls Prevent stale inline queue edits, preserve focused queue deletion semantics, and keep Windows custom chrome decoration-free across sidebar layouts. Refs #17 --- src-tauri/tauri.windows.conf.json | 1 + src/components/Sidebar.tsx | 110 +++++++++++++++++++++++------- src/index.css | 10 +++ 3 files changed, 96 insertions(+), 25 deletions(-) diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 94f7e8d..a8f7b84 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -9,6 +9,7 @@ "minWidth": 960, "minHeight": 640, "transparent": true, + "decorations": false, "windowEffects": { "effects": ["mica"], "state": "active" diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ebb6b5f..6d03e47 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -41,7 +41,11 @@ export const Sidebar: React.FC = (props) => { const addInputRef = useRef(null); const renameInputRef = useRef(null); const addQueueSubmitRef = useRef(false); + const addQueueCancelRef = useRef(false); const renameQueueSubmitRef = useRef(false); + const renameQueueCancelRef = useRef(null); + const renamingQueueIdRef = useRef(null); + const editingQueueNameRef = useRef(''); useEffect(() => { const handleCloseMenu = () => setContextMenu(null); @@ -74,6 +78,13 @@ export const Sidebar: React.FC = (props) => { } }, [foldersCollapsed]); + const handleFoldersToggle = () => { + if (foldersListRef.current?.contains(document.activeElement)) { + foldersToggleRef.current?.focus(); + } + setFoldersCollapsed(collapsed => !collapsed); + }; + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (document.querySelector('.app-modal-backdrop') || document.querySelector('.app-modal')) return; @@ -82,10 +93,14 @@ export const Sidebar: React.FC = (props) => { if (!isInput && (e.key === 'Delete' || e.key === 'Backspace')) { if (activeEl && activeEl.closest('.sidebar-inner')) { - if (activeView === 'downloads' && selectedFilter.startsWith('queue:')) { - const queueId = selectedFilter.replace('queue:', ''); + const focusedQueueId = activeEl + .closest('[data-sidebar-queue-id]') + ?.dataset.sidebarQueueId; + if (activeView === 'downloads' && focusedQueueId) { + const queueId = focusedQueueId; const q = queues.find(q => q.id === queueId); if (q && !q.isMain) { + e.preventDefault(); if (!window.confirm(t($ => $.sidebar.deleteQueueConfirm, { name: q.name }))) { return; } @@ -99,7 +114,7 @@ export const Sidebar: React.FC = (props) => { }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [addToast, activeView, queues, removeQueue, selectedFilter]); + }, [addToast, activeView, queues, removeQueue]); const getCount = (filter: SidebarFilter) => { if (filter.startsWith('queue:')) { @@ -142,10 +157,19 @@ export const Sidebar: React.FC = (props) => { setContextMenu({ x: e.clientX, y: e.clientY, id }); }; - const handleAddQueueSubmit = () => { + const handleAddQueueSubmit = (trigger: 'submit' | 'blur' = 'submit') => { + if (addQueueCancelRef.current) { + addQueueCancelRef.current = false; + return; + } if (addQueueSubmitRef.current) return; const normalizedName = newQueueName.trim(); if (!normalizedName) { + if (trigger === 'blur') { + setNewQueueName(''); + setIsAddingQueue(false); + return; + } addToast({ message: t($ => $.sidebar.queueNameEmpty), variant: 'error', isActionable: true }); return; } @@ -158,19 +182,31 @@ export const Sidebar: React.FC = (props) => { setIsAddingQueue(false); }; - const handleRenameQueueSubmit = () => { + const handleRenameQueueSubmit = (queueId: string, trigger: 'submit' | 'blur' = 'submit') => { + if (renameQueueCancelRef.current === queueId) { + renameQueueCancelRef.current = null; + return; + } + if (renamingQueueIdRef.current !== queueId) return; if (renameQueueSubmitRef.current) return; - const normalizedName = editingQueueName.trim(); - if (!renamingQueueId) return; + const normalizedName = editingQueueNameRef.current.trim(); if (!normalizedName) { + if (trigger === 'blur') { + renamingQueueIdRef.current = null; + editingQueueNameRef.current = ''; + setEditingQueueName(''); + setRenamingQueueId(null); + return; + } addToast({ message: t($ => $.sidebar.queueNameEmpty), variant: 'error', isActionable: true }); return; } - if (!renameQueue(renamingQueueId, normalizedName)) { + if (!renameQueue(queueId, normalizedName)) { addToast({ message: t($ => $.sidebar.queueNameExists), variant: 'error', isActionable: true }); return; } renameQueueSubmitRef.current = true; + renamingQueueIdRef.current = null; setRenamingQueueId(null); }; @@ -188,12 +224,22 @@ export const Sidebar: React.FC = (props) => { 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); + onChange={e => { + editingQueueNameRef.current = e.target.value; + setEditingQueueName(e.target.value); }} - onBlur={handleRenameQueueSubmit} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameQueueSubmit(queue.id); + if (e.key === 'Escape') { + e.preventDefault(); + renameQueueCancelRef.current = queue.id; + renamingQueueIdRef.current = null; + editingQueueNameRef.current = ''; + setEditingQueueName(''); + setRenamingQueueId(null); + } + }} + onBlur={() => handleRenameQueueSubmit(queue.id, 'blur')} /> ); @@ -203,6 +249,7 @@ export const Sidebar: React.FC = (props) => {
{!queues.find(q => q.id === contextMenu.id)?.isMain && ( )} diff --git a/src/index.css b/src/index.css index c952ecd..838aca2 100644 --- a/src/index.css +++ b/src/index.css @@ -1120,6 +1120,16 @@ html[data-list-density="relaxed"] { direction: rtl; } + html[dir="ltr"] .sidebar-nav-label { + direction: ltr; + unicode-bidi: isolate; + } + + html[dir="rtl"] .sidebar-nav-label { + direction: rtl; + unicode-bidi: isolate; + } + .app-statusbar { height: 26px; font-size: 10px;