From bb0038e0a1b5d2f4195326262aaecebb8b548388 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sun, 28 Jun 2026 14:17:58 +0100 Subject: [PATCH] chore: post-review hardening + changelog link tooltip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security / robustness / a11y polish on top of the color-search + tooltips work: - URL opening: route through validated backend commands (open_map_location with lat/lon bounds-checking, open_changelog_url with a fixed URL) instead of the frontend opener:allow-open-url capability, which is now removed. - EXIF GPS parsing: validate coordinate ranges and require the correct N/S/E/W hemisphere ref byte, then clamp. - Guard double-submit on album create / add-to-album (Lightbox, BulkActionBar, Sidebar) and discard stale autocomplete responses in the bulk tag editor. - Gallery tile: stop nesting diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index 5612157..82facb2 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -113,7 +113,7 @@ export function ImageTile({ }: { image: ImageRecord; onClick: () => void; - onContextMenu: (event: React.MouseEvent) => void; + onContextMenu: (event: React.MouseEvent) => void; }) { const [loaded, setLoaded] = useState(false); const [errored, setErrored] = useState(false); @@ -127,27 +127,35 @@ export function ImageTile({ return ( - {/* Image / placeholder */} {src && !errored ? ( <> @@ -265,7 +273,7 @@ export function ImageTile({ )} + ); } diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 45c7e46..f4e08b3 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,7 +1,7 @@ import { useEffect, useCallback, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { revealItemInDir, openUrl } from "@tauri-apps/plugin-opener"; +import { convertFileSrc, invoke } from "@tauri-apps/api/core"; +import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { useGalleryStore, ImageTag, ImageExif, AiRating } from "../store"; import { VideoPlayer } from "./VideoPlayer"; @@ -177,6 +177,7 @@ export function Lightbox() { const [albumMenuOpen, setAlbumMenuOpen] = useState(false); const [albumAddedTo, setAlbumAddedTo] = useState(null); const [newAlbumName, setNewAlbumName] = useState(""); + const [albumAdding, setAlbumAdding] = useState(false); const [regionSelectMode, setRegionSelectMode] = useState(false); const [isDragging, setIsDragging] = useState(false); const [dragRect, setDragRect] = useState(null); @@ -821,9 +822,14 @@ export function Lightbox() { key={album.id} className="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1 text-left text-xs text-gray-300 transition-colors hover:bg-white/5 hover:text-white" onClick={() => { - void addToAlbum(album.id, [selectedImage.id]); - setAlbumAddedTo(album.id); + if (albumAdding) return; + setAlbumAdding(true); + void addToAlbum(album.id, [selectedImage.id]) + .then(() => setAlbumAddedTo(album.id)) + .catch(() => undefined) + .finally(() => setAlbumAdding(false)); }} + disabled={albumAdding} > {album.name} {albumAddedTo === album.id ? ( @@ -840,12 +846,16 @@ export function Lightbox() { onSubmit={(e) => { e.preventDefault(); const name = newAlbumName.trim(); - if (!name) return; - void createAlbum(name).then((album) => { - void addToAlbum(album.id, [selectedImage.id]); - setAlbumAddedTo(album.id); - }); - setNewAlbumName(""); + if (!name || albumAdding) return; + setAlbumAdding(true); + void createAlbum(name) + .then(async (album) => { + await addToAlbum(album.id, [selectedImage.id]); + setAlbumAddedTo(album.id); + setNewAlbumName(""); + }) + .catch(() => undefined) + .finally(() => setAlbumAdding(false)); }} > setNewAlbumName(e.target.value)} + disabled={albumAdding} /> @@ -897,9 +908,9 @@ export function Lightbox() { className="inline-flex items-center gap-1 text-xs text-sky-400 transition-colors hover:text-sky-300" title="Open location in your browser" onClick={() => - void openUrl( - `https://www.openstreetmap.org/?mlat=${imageExif.gps_lat}&mlon=${imageExif.gps_lon}#map=15/${imageExif.gps_lat}/${imageExif.gps_lon}`, - ) + void invoke("open_map_location", { + params: { lat: imageExif.gps_lat, lon: imageExif.gps_lon }, + }) } > {imageExif.gps_lat.toFixed(5)}, {imageExif.gps_lon.toFixed(5)} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b4165f9..3c67282 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -376,7 +376,8 @@ function AlbumContextMenu({ ? "text-red-400 hover:bg-red-500/15 hover:text-red-300" : "text-gray-300 hover:bg-white/8 hover:text-white" }`} - onClick={() => { + onClick={(event) => { + event.stopPropagation(); onClick(); onClose(); }} @@ -448,6 +449,10 @@ function AlbumItem({ const row = (
{ + if (e.target !== e.currentTarget) return; + if (renaming || (e.key !== "Enter" && e.key !== " ")) return; + e.preventDefault(); + if (manageMode) { + onToggleManage?.(); + } else { + viewAlbum(album.id); + } + }} onContextMenu={(e) => { if (manageMode) return; e.preventDefault(); @@ -603,6 +618,7 @@ export function Sidebar() { const deleteAlbums = useGalleryStore((state) => state.deleteAlbums); const reorderAlbums = useGalleryStore((state) => state.reorderAlbums); const [creatingAlbum, setCreatingAlbum] = useState(false); + const [createAlbumPending, setCreateAlbumPending] = useState(false); const [newAlbumName, setNewAlbumName] = useState(""); const newAlbumInputRef = useRef(null); const [manageAlbums, setManageAlbums] = useState(false); @@ -634,6 +650,15 @@ export function Sidebar() { const nextIds = orderedAlbumsRef.current.map((album) => album.id); // Read live store order (not the render-time closure) in case albums changed. const currentIds = useGalleryStore.getState().albums.map((album) => album.id); + const snapshotIds = albums.map((album) => album.id); + if ( + snapshotIds.length !== currentIds.length || + snapshotIds.some((id, index) => id !== currentIds[index]) + ) { + orderedAlbumsRef.current = useGalleryStore.getState().albums; + setOrderedAlbums(orderedAlbumsRef.current); + return; + } if ( nextIds.length !== currentIds.length || nextIds.some((id, index) => id !== currentIds[index]) @@ -768,10 +793,16 @@ export function Sidebar() { setCreatingAlbum(false); return; } - const album = await createAlbum(name); - setNewAlbumName(""); - setCreatingAlbum(false); - useGalleryStore.getState().viewAlbum(album.id); + if (createAlbumPending) return; + setCreateAlbumPending(true); + try { + const album = await createAlbum(name); + setNewAlbumName(""); + setCreatingAlbum(false); + useGalleryStore.getState().viewAlbum(album.id); + } finally { + setCreateAlbumPending(false); + } }; const exitManageAlbums = () => { @@ -1041,7 +1072,9 @@ export function Sidebar() { placeholder="Album name…" value={newAlbumName} onChange={(e) => setNewAlbumName(e.target.value)} + disabled={createAlbumPending} onKeyDown={(e) => { + if (createAlbumPending) return; if (e.key === "Escape") { setCreatingAlbum(false); setNewAlbumName(""); diff --git a/src/components/TagCloud.tsx b/src/components/TagCloud.tsx index f6a473e..e31b88a 100644 --- a/src/components/TagCloud.tsx +++ b/src/components/TagCloud.tsx @@ -327,9 +327,9 @@ function TagManageRow({ setBusy(true); try { await onRename(entry.tag, next); + setEditing(false); } finally { setBusy(false); - setEditing(false); } }; @@ -382,7 +382,7 @@ function TagManageRow({
) : ( -
+
+ + +