Files
advance-photo-gallery-web-sdk/packages/photo-sdk/src/components/Sidebar.tsx
T
KaushikRK99 7673fc63ea feat: enhance styling and layout for toolbar, map components, and editor
- Updated toolbar styles for improved layout and appearance, including padding and margin adjustments.
- Introduced new styles for scale menu and compact dropdowns.
- Enhanced map pin tooltips with better positioning and hover effects.
- Added new styles for map sheet filters, including search and date range controls.
- Improved editor styles for a more cohesive look and feel, including transport controls for video editing.
- Expanded GeoLocation interface to include additional address fields and formatted address.
- Updated MediaItem interface to include storage reference and uploadedBy fields.
- Enhanced MediaComment interface with author details for better user identification.
- Introduced GalleryUser interface to represent signed-in users in the host app.
2026-07-23 07:34:20 +05:30

353 lines
11 KiB
TypeScript

'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 (
<button
type="button"
className={[
'apg-sidebar__item',
indent ? 'apg-sidebar__child' : '',
active ? 'apg-sidebar__item--active' : '',
]
.filter(Boolean)
.join(' ')}
onClick={handleClick}
onContextMenu={onContextMenu}
aria-current={active ? 'page' : undefined}
>
{disclosure ? (
<span
className={['apg-sidebar__disclosure', open ? 'apg-sidebar__disclosure--open' : ''].join(
' ',
)}
onClick={(e) => {
e.stopPropagation();
onToggle?.();
}}
>
<Icon name="chevron-right" size={13} />
</span>
) : null}
<span className="apg-sidebar__item-icon">
<Icon name={icon} size={17} />
</span>
<span className="apg-sidebar__item-label">{label}</span>
{trailing ? (
onTrailingClick ? (
<span
className="apg-sidebar__item-trail apg-sidebar__item-trail--btn"
role="button"
tabIndex={0}
aria-label={trailingLabel}
title={trailingLabel}
onClick={(e) => {
e.stopPropagation();
onTrailingClick();
}}
>
<Icon name={trailing} size={14} />
</span>
) : (
<span className="apg-sidebar__item-trail">
<Icon name={trailing} size={14} />
</span>
)
) : null}
</button>
);
}
function SectionLabel({ children }: { children: React.ReactNode }) {
return <div className="apg-sidebar__section-label">{children}</div>;
}
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<ViewId>(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 (
<nav
className={['apg-sidebar', sidebarOpen ? '' : 'apg-sidebar--collapsed'].join(' ')}
aria-label="Library navigation"
>
<Row icon="library" label="Library" view="library" />
<Row icon="collections" label="Collections" view="collections" />
{anyShown(
'favourites',
'recently-saved',
'map',
'videos',
'screenshots',
'sys:documents',
'people',
'recently-deleted',
) ? (
<SectionLabel>Pinned</SectionLabel>
) : null}
{shown('favourites') ? <Row icon="heart" label="Favourites" view="favourites" /> : null}
{shown('recently-saved') ? (
<Row icon="download" label="Recently Saved" view="recently-saved" />
) : null}
{shown('map') ? <Row icon="map" label="Map" view="map" /> : null}
{shown('videos') ? <Row icon="video" label="Videos" view="videos" /> : null}
{shown('screenshots') ? (
<Row icon="screenshot" label="Screenshots" view="screenshots" />
) : null}
{shown('sys:documents') ? (
<Row icon="document" label="Documents" view="sys:documents" />
) : null}
{shown('people') ? <Row icon="person-circle" label="People" view="people" /> : null}
{shown('recently-deleted') ? (
<Row
icon="trash"
label="Recently Deleted"
view="recently-deleted"
trailing={lockIcon}
onTrailingClick={toggleLock}
trailingLabel={locked ? 'Unlock Recently Deleted' : 'Lock Recently Deleted'}
/>
) : null}
<div
onContextMenu={(e) => {
e.preventDefault();
openContextMenu(e.clientX, e.clientY, [
{ label: 'New Album', icon: 'collections', onClick: newAlbum },
]);
}}
>
<SectionLabel>Albums</SectionLabel>
</div>
<Row
icon="collections"
label="All Albums"
view="albums"
disclosure
open={albumsOpen}
onToggle={() => {
setAlbumsOpen((v) => !v);
closeContextMenu();
}}
/>
{albumsOpen ? (
<>
<Row icon="plus" label="New Album" onClick={newAlbum} indent />
{userAlbums.map((a) => (
<Row
key={a.id}
icon={a.kind === 'folder' ? 'folder' : 'image'}
label={a.name}
view={a.id as ViewId}
indent
onContextMenu={albumMenu(a)}
/>
))}
</>
) : null}
{objectAlbums.length ? (
<>
<SectionLabel>Objects</SectionLabel>
<Row
icon="tag"
label="All Objects"
view="collections"
disclosure
open={objectsOpen}
onToggle={() => {
setObjectsOpen((v) => !v);
closeContextMenu();
}}
/>
{objectsOpen
? objectAlbums.map((a) => (
<Row
key={a.id}
icon={(a.icon as IconName) ?? 'tag'}
label={a.name}
view={a.id as ViewId}
indent
onContextMenu={objectMenu(a)}
/>
))
: null}
</>
) : null}
<SectionLabel>Sharing</SectionLabel>
<Row
icon="people"
label="Shared Albums"
view="shared-albums"
disclosure
open={sharingOpen}
onToggle={() => {
setSharingOpen((v) => !v);
closeContextMenu();
}}
/>
{sharingOpen ? <Row icon="chat" label="Activity" view="activity" indent /> : null}
{anyShown('recently-deleted', 'duplicates', 'versions', 'map') ? (
<SectionLabel>Utilities</SectionLabel>
) : null}
{shown('recently-deleted') ? (
<Row
icon="trash"
label="Recently Deleted"
view="recently-deleted"
trailing={lockIcon}
noActive
onTrailingClick={toggleLock}
trailingLabel={locked ? 'Unlock Recently Deleted' : 'Lock Recently Deleted'}
/>
) : null}
{shown('duplicates') ? <Row icon="duplicates" label="Duplicates" view="duplicates" /> : null}
{shown('versions') ? <Row icon="clock" label="Versions & Audit" view="versions" /> : null}
{shown('map') ? <Row icon="map" label="Map" view="map" noActive /> : null}
</nav>
);
}