feat: add core domain types for Photo Gallery SDK including media items, albums, and annotations

This commit is contained in:
2026-07-23 07:39:01 +05:30
parent 7bcd1a2a2d
commit 2613c767a6
107 changed files with 20699 additions and 7 deletions
@@ -0,0 +1,161 @@
'use client';
import { useRef, useState } from 'react';
import { useViewMedia } from '../../hooks/useViewMedia';
import { groupByTime } from '../../lib/grouping';
import { Icon, type IconName } from '../../icons';
import { useGallery, useGalleryStoreApi } from '../../store/context';
import { liveMedia } from '../../store/selectors';
import { MediaGrid } from '../MediaGrid';
function Welcome({ onImport }: { onImport: () => void }) {
// Host-neutral copy: the SDK is embedded in other products (see `title`), so the
// empty state must not name a specific photo app or reference its settings.
const hints: Array<{ icon: IconName; text: string }> = [
{ icon: 'download', text: 'Click the + button to import.' },
{ icon: 'duplicates', text: 'Drag photos and videos straight in.' },
{ icon: 'camera', text: 'Capture a shot with your camera.' },
{ icon: 'image', text: 'Everything you add is searchable.' },
];
const title = useGallery((s) => s.config.title);
return (
<div className="apg-empty" onClick={onImport} role="button" tabIndex={0}>
<div className="apg-empty__title">Welcome to {title}</div>
<div className="apg-empty__subtitle">To get started, do any of the following:</div>
<div className="apg-empty__hints">
{hints.map((h, i) => (
<div className="apg-empty__hint" key={i}>
<span style={{ color: 'var(--apg-text-tertiary)' }}>
<Icon name={h.icon} size={40} />
</span>
<span>{h.text}</span>
</div>
))}
</div>
</div>
);
}
export function LibraryView() {
const api = useGalleryStoreApi();
const items = useViewMedia();
const scale = useGallery((s) => s.libraryScale);
const searchQuery = useGallery((s) => s.searchQuery);
const objectFocus = useGallery((s) => s.objectFocus);
const tagFocus = useGallery((s) => s.tagFocus);
const personFocus = useGallery((s) => s.personFocus);
const personName = useGallery((s) => {
if (!s.personFocus) return null;
const p = s.people.find((x) => x.id === s.personFocus);
return p?.name ?? 'Unnamed person';
});
const totalLive = useGallery((s) => liveMedia(s.media).length);
const fileRef = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false);
const importFiles = async (files: FileList | null) => {
if (!files?.length) return;
await api.getState().importFiles(files);
if (fileRef.current) fileRef.current.value = '';
};
const onDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragging(false);
void importFiles(e.dataTransfer.files);
};
let content: React.ReactNode;
if (items.length === 0) {
if (totalLive === 0) {
content = <Welcome onImport={() => fileRef.current?.click()} />;
} else {
content = (
<div className="apg-empty">
<div className="apg-empty__card">
<div className="apg-empty__title" style={{ fontSize: 24 }}>
No Results
</div>
<div className="apg-empty__subtitle">
{objectFocus
? `No photos containing “${objectFocus}”.`
: searchQuery
? 'Try a different search term.'
: 'No items match this filter.'}
</div>
</div>
</div>
);
}
} else if (scale === 'all') {
content = <MediaGrid items={items} />;
} else {
const granularity = scale === 'years' ? 'year' : scale === 'months' ? 'month' : 'day';
const sections = groupByTime(items, granularity);
content = (
<>
{sections.map((s) => (
<MediaGrid key={s.key} items={s.items} title={s.title} />
))}
</>
);
}
return (
<div
className="apg-scroll"
onDragOver={(e) => {
e.preventDefault();
setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={onDrop}
style={dragging ? { outline: '3px dashed var(--apg-accent)', outlineOffset: -8 } : undefined}
>
{objectFocus || tagFocus || personFocus ? (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '12px 12px 0',
fontSize: 14,
fontWeight: 600,
textTransform: 'capitalize',
}}
>
<Icon name={objectFocus ? 'search' : personFocus ? 'person-circle' : 'tag'} size={16} />
{objectFocus
? `Object: ${objectFocus}`
: personFocus
? `Person: ${personName}`
: `Tag: ${tagFocus}`}
<button
type="button"
className="apg-btn"
style={{ marginLeft: 8, padding: '3px 10px', textTransform: 'none' }}
onClick={() =>
objectFocus
? api.getState().setObjectFocus(null)
: personFocus
? api.getState().setPersonFocus(null)
: api.getState().setTagFocus(null)
}
>
Clear
</button>
</div>
) : null}
{content}
<input
ref={fileRef}
type="file"
accept="image/*,video/*"
multiple
hidden
onChange={(e) => void importFiles(e.target.files)}
/>
</div>
);
}