'use client'; import { useState } from 'react'; import { GRID_ZOOM_STEPS } from '../constants'; import { useGallery, useGalleryStoreApi } from '../store/context'; import { mediaForView } from '../store/selectors'; import type { GridFilter } from '../store/store'; import type { AlbumId, LibraryScale, MapMode, ViewId } from '../types'; import { confirmAction, openSecuritySettings, openShareModal } from './modals'; import { openUploadModal } from './UploadModal'; import { openContextMenu } from './ContextMenu'; import { Icon } from '../icons'; function Segmented({ options, value, onChange, }: { options: Array<{ value: T; label: string }>; value: T; onChange: (v: T) => void; }) { return (
{options.map((o) => ( ))}
); } const GRID_VIEWS = new Set([ 'library', 'favourites', 'recently-saved', 'videos', 'screenshots', 'recently-deleted', 'duplicates', 'search', ]); const SCALE_OPTIONS: Array<{ value: LibraryScale; label: string }> = [ { value: 'years', label: 'Years' }, { value: 'months', label: 'Months' }, { value: 'all', label: 'All Photos' }, ]; const FILTER_OPTIONS: Array<{ value: GridFilter; label: string }> = [ { value: 'all', label: 'All Items' }, { value: 'favourites', label: 'Favourites' }, { value: 'edited', label: 'Edited' }, { value: 'photos', label: 'Photos' }, { value: 'videos', label: 'Videos' }, { value: 'screenshots', label: 'Screenshots' }, { value: 'not-in-album', label: 'Not in an Album' }, ]; export function TopToolbar() { const api = useGalleryStoreApi(); const view = useGallery((s) => s.view); const libraryScale = useGallery((s) => s.libraryScale); const mapMode = useGallery((s) => s.mapMode); const gridFilter = useGallery((s) => s.gridFilter); const searchQuery = useGallery((s) => s.searchQuery); const zoomIndex = useGallery((s) => s.zoomIndex); const features = useGallery((s) => s.config.features); const theme = useGallery((s) => s.theme); // A host that owns light/dark must not be overridden from inside the gallery. const showThemeSwitcher = useGallery((s) => s.config.chrome.themeSwitcher); const aiStatus = useGallery((s) => s.aiStatus); const infoOpen = useGallery((s) => s.infoOpen); const fullscreen = useGallery((s) => s.fullscreen); const selectionCount = useGallery((s) => s.selection.size); const [searchExpanded, setSearchExpanded] = useState(false); const isGrid = GRID_VIEWS.has(view) || view.startsWith('album:'); const isAlbumish = view !== 'library' && view !== 'collections'; // Uploads default into the album you're viewing (else Library). const currentAlbumId = view.startsWith('album:') ? (view as unknown as AlbumId) : undefined; const openFilterMenu = (e: React.MouseEvent) => { const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); openContextMenu( r.left, r.bottom + 4, FILTER_OPTIONS.map((o) => ({ label: o.label, checked: gridFilter === o.value, onClick: () => api.getState().setGridFilter(o.value), })), ); }; // Library-scale as a compact dropdown (replaces the wide 3-item segmented that // overlapped the zoom-out button on narrow toolbars). Reuses the `.apg-menu` // popover, which supplies the menu role + checkmarks + arrow-key navigation. const scaleLabel = SCALE_OPTIONS.find((o) => o.value === libraryScale)?.label ?? 'All Photos'; const openScaleMenu = (e: React.MouseEvent) => { const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); openContextMenu( r.left, r.bottom + 4, SCALE_OPTIONS.map((o) => ({ label: o.label, checked: libraryScale === o.value, onClick: () => api.getState().setLibraryScale(o.value), })), ); }; const openMoreMenu = (e: React.MouseEvent) => { const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); const state = api.getState(); const viewIds = mediaForView(state).map((m) => m.id); const allSelected = viewIds.length > 0 && viewIds.every((id) => state.selection.has(id)); openContextMenu(r.left - 150, r.bottom + 4, [ allSelected ? { label: 'Unselect All', icon: 'close', onClick: () => state.clearSelection(), } : { label: 'Select All', icon: 'check', onClick: () => state.selectMany(viewIds), }, ...(features.import ? [{ label: 'Import…', icon: 'download' as const, onClick: () => openUploadModal(currentAlbumId) }] : []), ...(showThemeSwitcher ? [ { type: 'separator' as const }, { type: 'label' as const, label: 'Appearance' }, { label: 'Light', icon: 'adjust' as const, checked: theme === 'light', onClick: () => state.setTheme('light') }, { label: 'Dark', icon: 'adjust' as const, checked: theme === 'dark', onClick: () => state.setTheme('dark') }, { label: 'Semi-Dark (Glass)', icon: 'adjust' as const, checked: theme === 'semi-dark', onClick: () => state.setTheme('semi-dark'), }, { label: 'System', icon: 'adjust' as const, checked: theme === 'system', onClick: () => state.setTheme('system') }, ] : []), { type: 'separator' as const }, { label: state.lockConfigured ? 'Recently Deleted Lock…' : 'Lock Recently Deleted…', icon: state.lockConfigured ? 'lock' : 'unlock', onClick: () => openSecuritySettings(), }, ...(view === 'recently-deleted' ? [ { type: 'separator' as const }, { label: 'Delete All', icon: 'trash' as const, danger: true, onClick: () => confirmAction({ title: 'Delete All Items?', message: 'These items will be permanently deleted. This cannot be undone.', confirmLabel: 'Delete All', danger: true, onConfirm: () => api.getState().emptyTrash(), }), }, ] : []), ]); }; return (
{features.import ? ( ) : null} {features.camera ? ( ) : null} {/* Center control (context dependent). Flanked by two flex spacers so it centres on wide toolbars and simply sits in flow (never overlapping the right group) when the toolbar is narrow — e.g. embedded, where the host + gallery sidebars eat the width. */}
{view === 'library' || view === 'search' ? ( ) : view === 'map' ? ( value={mapMode} onChange={(v) => api.getState().setMapMode(v)} options={[ { value: 'map', label: 'Map' }, { value: 'satellite', label: 'Satellite' }, { value: 'grid', label: 'Grid' }, ]} /> ) : null}
{isGrid ? ( <> ) : null} {features.sharing && (selectionCount > 0 || view.startsWith('album:')) ? ( ) : null} {aiStatus.running ? ( ) : null}
setSearchExpanded(true)} > setSearchExpanded(true)} onChange={(e) => { const q = e.target.value; api.getState().setSearch(q); api.getState().setView((q ? 'search' : 'library') as ViewId); }} onBlur={() => setTimeout(() => setSearchExpanded(false), 120)} /> {searchExpanded && !searchQuery ? (
Recents
{( [ ['viewed', 'Recently Viewed'], ['edited', 'Recently Edited'], ['added', 'Recently Added'], ] as Array<['viewed' | 'edited' | 'added', string]> ).map(([key, label]) => ( ))}
) : null}
); }