'use client'; import { useState } from 'react'; import { useIsMobile } from '../hooks/useMediaQuery'; import { Icon, type IconName } from '../icons'; import { useGallery, useGalleryStoreApi } from '../store/context'; import type { Album, ViewId } from '../types'; import { closeContextMenu, openContextMenu } from './ContextMenu'; import { confirmAction, openSecuritySettings, promptAlbumName } from './modals'; interface RowProps { icon: IconName; label: string; view?: ViewId; trailing?: IconName; indent?: boolean; disclosure?: boolean; open?: boolean; /** Suppress active highlight (for duplicate rows that share a view). */ noActive?: boolean; onToggle?: () => void; onClick?: () => void; onContextMenu?: (e: React.MouseEvent) => void; /** When set, the trailing icon becomes its own clickable control. */ onTrailingClick?: () => void; trailingLabel?: string; } function Row({ icon, label, view, trailing, indent, disclosure, open, noActive, onToggle, onClick, onContextMenu, onTrailingClick, trailingLabel, }: RowProps) { const api = useGalleryStoreApi(); const isMobile = useIsMobile(); const active = useGallery((s) => (view && !noActive ? s.view === view : false)); const handleClick = () => { if (onClick) onClick(); else if (view) api.getState().setView(view); if (isMobile) api.getState().setSidebar(false); }; return ( ); } function SectionLabel({ children }: { children: React.ReactNode }) { return
{children}
; } export function Sidebar() { const api = useGalleryStoreApi(); const sidebarOpen = useGallery((s) => s.sidebarOpen); const albums = useGallery((s) => s.albums); // Views the host asked to hide (e.g. Screenshots, Documents). A row is dropped // when its `view` is listed; a section label is dropped when every row under it // would be hidden. const hiddenViews = useGallery((s) => s.config.hiddenViews); const hidden = new Set(hiddenViews ?? []); const shown = (view: ViewId) => !hidden.has(view); const anyShown = (...views: ViewId[]) => views.some(shown); // Recently Deleted lock state → closed lock when protected & not yet opened. const locked = useGallery((s) => s.lockConfigured && !s.lockUnlocked); const lockIcon: IconName = locked ? 'lock' : 'unlock'; const [sharingOpen, setSharingOpen] = useState(true); const [albumsOpen, setAlbumsOpen] = useState(true); const [objectsOpen, setObjectsOpen] = useState(true); // Sidebar lock toggle: no password → set one; unlocked → re-lock; locked → go unlock. const toggleLock = () => { const s = api.getState(); if (!s.lockConfigured) openSecuritySettings(); else if (s.lockUnlocked) s.relock(); else s.setView('recently-deleted'); }; const userAlbums = albums.filter((a) => a.kind === 'user' || a.kind === 'folder'); // Auto "one album per detected object" (chair, table, person, car…), sorted by count. const objectAlbums = albums.filter((a) => a.id.startsWith('sys:obj:')); const albumMenu = (album: Album) => (e: React.MouseEvent) => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, [ { label: 'Rename Album', icon: 'collections', onClick: () => promptAlbumName('Rename Album', album.name, (name) => api.getState().renameAlbum(album.id, name), ), }, { label: 'Duplicate Album', icon: 'duplicates', onClick: () => api.getState().duplicateAlbum(album.id), }, { type: 'separator' }, { label: 'Delete Album', icon: 'trash', danger: true, onClick: () => api.getState().deleteAlbum(album.id), }, ]); }; // Right-click an auto object album → permanently rename its tag (e.g. car → excavator). const objectMenu = (album: Album) => (e: React.MouseEvent) => { e.preventDefault(); const label = album.id.slice('sys:obj:'.length); openContextMenu(e.clientX, e.clientY, [ { label: 'Rename Tag', icon: 'tag', onClick: () => promptAlbumName( 'Rename Tag', album.name, (name) => api.getState().renameLabel(label, name), { placeholder: 'Tag name' }, ), }, { label: 'Delete Tag', icon: 'trash', danger: true, onClick: () => confirmAction({ title: 'Delete Tag', message: `Remove the "${album.name}" tag? It's deleted from all photos and won't be created again.`, confirmLabel: 'Delete', danger: true, onConfirm: () => api.getState().deleteLabel(label), }), }, ]); }; const newAlbum = () => promptAlbumName('New Album', '', (name) => { const id = api.getState().createAlbum(name); api.getState().setView(`album:${id}` as ViewId); }); return ( ); }