From ee2a1b204e470df36f3fb7c9a2c111ba56cc241d Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sat, 4 Jul 2026 12:11:26 +0100 Subject: [PATCH] refactor(sidebar): extract InlineRename, InlineConfirm, and NavItem FolderItem and AlbumItem each carried their own copy of the in-place rename input (state, select-on-open effect, commit/cancel handling) and the Confirm/Cancel pair for destructive actions. Both are now shared components: InlineRename mounts fresh per rename (an external name change mid-rename no longer clobbers typing), InlineConfirm is the compact red/gray pair. The four copy-pasted nav rows collapse into a local NavItem component. --- src/components/InlineConfirm.tsx | 28 ++++ src/components/InlineRename.tsx | 50 ++++++++ src/components/Sidebar.tsx | 213 ++++++++++--------------------- 3 files changed, 142 insertions(+), 149 deletions(-) create mode 100644 src/components/InlineConfirm.tsx create mode 100644 src/components/InlineRename.tsx diff --git a/src/components/InlineConfirm.tsx b/src/components/InlineConfirm.tsx new file mode 100644 index 0000000..7e4b5f4 --- /dev/null +++ b/src/components/InlineConfirm.tsx @@ -0,0 +1,28 @@ +/** + * Compact Confirm/Cancel pair for destructive row actions (remove folder, + * delete album). Swap it in where the hover actions normally sit. + */ +export function InlineConfirm({ + onConfirm, + onCancel, +}: { + onConfirm: () => void; + onCancel: () => void; +}) { + return ( +
event.stopPropagation()}> + + +
+ ); +} diff --git a/src/components/InlineRename.tsx b/src/components/InlineRename.tsx new file mode 100644 index 0000000..ce873da --- /dev/null +++ b/src/components/InlineRename.tsx @@ -0,0 +1,50 @@ +import { useEffect, useRef, useState } from "react"; + +/** + * In-place rename input for sidebar rows (folders, albums). Mount it in + * place of the row label while renaming: commits on Enter or blur (only when + * the trimmed name is non-empty and actually changed), cancels on Escape. + */ +export function InlineRename({ + name, + onRename, + onClose, +}: { + name: string; + onRename: (next: string) => Promise | void; + onClose: () => void; +}) { + const [value, setValue] = useState(name); + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, []); + + const commit = async () => { + const trimmed = value.trim(); + if (trimmed && trimmed !== name) { + await onRename(trimmed); + } + onClose(); + }; + + return ( + setValue(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void commit(); + } + if (event.key === "Escape") onClose(); + }} + onBlur={() => void commit()} + onClick={(event) => event.stopPropagation()} + /> + ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index a9f821c..83cfe5f 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,12 +3,40 @@ import { Reorder, useDragControls } from "framer-motion"; import { open } from "@tauri-apps/plugin-dialog"; import { useGalleryStore, Folder, Album, IndexProgress } from "../store"; import { ContextMenu, Dropdown, MenuItem, MenuSeparator } from "./menu"; +import { InlineConfirm } from "./InlineConfirm"; +import { InlineRename } from "./InlineRename"; import { mediaSrc } from "../lib/mediaSrc"; import { Tooltip } from "./Tooltip"; type LibrarySort = "az" | "za" | "custom"; const LIBRARY_SORT_KEY = "phokus-library-sort"; +function NavItem({ + label, + iconPath, + active, + onClick, +}: { + label: string; + iconPath: string; + active: boolean; + onClick: () => void; +}) { + return ( +
+ + + + {label} +
+ ); +} + function FolderItem({ folder, selected, @@ -42,16 +70,7 @@ function FolderItem({ const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [renaming, setRenaming] = useState(false); - const [renameValue, setRenameValue] = useState(folder.name); const [confirmingRemoval, setConfirmingRemoval] = useState(false); - const renameInputRef = useRef(null); - - useEffect(() => { - if (renaming) { - setRenameValue(folder.name); - setTimeout(() => renameInputRef.current?.select(), 0); - } - }, [renaming, folder.name]); const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); @@ -66,19 +85,6 @@ function FolderItem({ } }; - const commitRename = async () => { - const trimmed = renameValue.trim(); - if (trimmed && trimmed !== folder.name) { - await renameFolder(folder.id, trimmed); - } - setRenaming(false); - }; - - const handleRenameKey = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { e.preventDefault(); void commitRename(); } - if (e.key === "Escape") { setRenaming(false); } - }; - return ( {renaming ? ( - setRenameValue(e.target.value)} - onKeyDown={handleRenameKey} - onBlur={() => void commitRename()} - onClick={(e) => e.stopPropagation()} + renameFolder(folder.id, next)} + onClose={() => setRenaming(false)} /> ) : (
@@ -180,20 +182,10 @@ function FolderItem({ {/* Hover action buttons */} {!renaming && ( confirmingRemoval ? ( -
e.stopPropagation()}> - - -
+ { void removeFolder(folder.id); setConfirmingRemoval(false); }} + onCancel={() => setConfirmingRemoval(false)} + /> ) : (
@@ -294,24 +286,7 @@ function AlbumItem({ const [menu, setMenu] = useState<{ x: number; y: number } | null>(null); const [renaming, setRenaming] = useState(false); - const [renameValue, setRenameValue] = useState(album.name); const [confirmingRemoval, setConfirmingRemoval] = useState(false); - const renameInputRef = useRef(null); - - useEffect(() => { - if (renaming) { - setRenameValue(album.name); - setTimeout(() => renameInputRef.current?.select(), 0); - } - }, [renaming, album.name]); - - const commitRename = async () => { - const trimmed = renameValue.trim(); - if (trimmed && trimmed !== album.name) { - await renameAlbum(album.id, trimmed); - } - setRenaming(false); - }; const cover = mediaSrc(album.cover_thumbnail_path); @@ -404,17 +379,10 @@ function AlbumItem({
{renaming ? ( - setRenameValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { e.preventDefault(); void commitRename(); } - if (e.key === "Escape") setRenaming(false); - }} - onBlur={() => void commitRename()} - onClick={(e) => e.stopPropagation()} + renameAlbum(album.id, next)} + onClose={() => setRenaming(false)} /> ) : (
@@ -425,20 +393,10 @@ function AlbumItem({
{!renaming && confirmingRemoval ? ( -
e.stopPropagation()}> - - -
+ { void deleteAlbum(album.id); setConfirmingRemoval(false); }} + onCancel={() => setConfirmingRemoval(false)} + /> ) : null} {menu ? ( @@ -714,73 +672,30 @@ export function Sidebar() { {/* Nav */}
-
selectFolder(null)} - > - - - - - All Media - -
- -
+ setView("explore")} - > - - - - - Explore - -
- -
+ setView("timeline")} - > - - - - - Timeline - -
- -
+ setView("duplicates")} - > - - - - - Duplicates - -
+ iconPath="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" + />
{/* Section label */}