chore: post-review hardening + changelog link tooltip
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 <button>s — non-interactive tile div with an overlay button for open/toggle; checkbox and Similar promoted with z-index + focus rings. - Accessibility: keyboard handlers, focus-visible rings, and aria on album rows, tag-manage actions, and tile controls; Tooltip uses a block <div> wrapper in block mode and aria-hidden when hidden. - Show the destination URL in a tooltip on the "Full changelog" link so the user can see where it goes before clicking. - Toolbar "All" filter also clears the color filter; color-filtered views no longer get unfiltered newly-indexed images injected. - Sidebar reorder: bail if the album set changed mid-drag. - Tooling: add cargo fmt scripts; alphabetize package.json scripts.
This commit is contained in:
@@ -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 = (
|
||||
<div
|
||||
role={manageMode ? "checkbox" : "button"}
|
||||
tabIndex={renaming ? -1 : 0}
|
||||
aria-checked={manageMode ? selectedForManage : undefined}
|
||||
aria-current={!manageMode && selected ? "page" : undefined}
|
||||
className={`group relative flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all duration-150 ${
|
||||
selectedForManage
|
||||
? "bg-blue-500/10 text-white ring-1 ring-inset ring-blue-400/50"
|
||||
@@ -462,6 +467,16 @@ function AlbumItem({
|
||||
viewAlbum(album.id);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
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<HTMLInputElement>(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("");
|
||||
|
||||
Reference in New Issue
Block a user