Files
shpm/client/src/api/photos.js
T
nmemmertandClaude Sonnet 4.6 4ff5a6c8ca feat: photo sync (iCloud + Amazon import), larger gear icon, Luma branding
- Settings: iCloud Photos section with icloudpd-backed sync — Apple ID,
  password, download path, cookie dir, live log output via SSE
- Settings: Amazon Photos one-time import — path to extracted data export,
  Add & Index button wires it directly into Watch Folders
- scripts/amazon_sync.py: stdlib-only downloader using Netscape cookie file
- server: POST /api/sync/icloud, /api/sync/amazon, GET /api/sync/stream
  (SSE log streaming matching ingest stream pattern)
- db: icloud/amazon setting keys; getSettings handles bool + string fields
- Gear icon: fontSize 16→20, button 38×38, color #555→#888
- favicon.svg: 6-blade aperture icon in Luma blue
- Browser tab title: shpm → Luma
- Dockerfile: installs icloudpd via pip3, copies scripts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 09:30:43 -04:00

124 lines
3.3 KiB
JavaScript

async function api(path, options) {
const res = await fetch(path, options);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.status === 204 ? null : res.json();
}
const json = (body) => ({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
// Library
export function fetchPhotos({ cursor, limit = 100, q, from, to, tagId, collectionId, starred, city } = {}) {
const p = new URLSearchParams({ limit });
if (cursor) p.set('cursor', cursor);
if (q) p.set('q', q);
if (from) p.set('from', from);
if (to) p.set('to', to);
if (tagId) p.set('tagId', tagId);
if (collectionId) p.set('collectionId', collectionId);
if (starred) p.set('starred', starred);
if (city) p.set('city', city);
return api(`/api/photos?${p}`);
}
export function fetchDirBrowser(dirPath) {
return api(`/api/fs/browse?path=${encodeURIComponent(dirPath)}`);
}
export function fetchPhotoDetail(id) {
return api(`/api/photos/${id}`);
}
export function fetchMapPhotos() {
return api('/api/photos/map');
}
export function fetchLibraryStats() {
return api('/api/library/stats');
}
export function toggleStar(id, starred) {
return api(`/api/photos/${id}`, { method: 'PATCH', ...json({ starred }) });
}
// Tags
export function fetchTags() {
return api('/api/tags');
}
export function addTag(photoId, name) {
return api(`/api/photos/${photoId}/tags`, { method: 'POST', ...json({ name }) });
}
export function removeTag(photoId, tagId) {
return api(`/api/photos/${photoId}/tags/${tagId}`, { method: 'DELETE' });
}
// Collections
export function fetchCollections() {
return api('/api/collections');
}
export function createCollection(name, description) {
return api('/api/collections', { method: 'POST', ...json({ name, description }) });
}
export function addToCollection(photoId, collectionId) {
return api(`/api/photos/${photoId}/collections`, { method: 'POST', ...json({ collectionId }) });
}
export function removeFromCollection(photoId, collectionId) {
return api(`/api/photos/${photoId}/collections/${collectionId}`, { method: 'DELETE' });
}
// Settings
export function fetchSettings() {
return api('/api/settings');
}
export function patchSettings(updates) {
return api('/api/settings', { method: 'PATCH', ...json(updates) });
}
// Watch folders
export function fetchWatchFolders() {
return api('/api/watch-folders');
}
export function addWatchFolder(path) {
return api('/api/watch-folders', { method: 'POST', ...json({ path }) });
}
export function removeWatchFolder(id) {
return api(`/api/watch-folders/${id}`, { method: 'DELETE' });
}
export function toggleWatchFolder(id, enabled) {
return api(`/api/watch-folders/${id}`, { method: 'PATCH', ...json({ enabled }) });
}
// Admin
export function clearDismissals() {
return api('/api/admin/clear-dismissals', { method: 'POST' });
}
// Photo sync
export function triggerSync(source) {
return api(`/api/sync/${source}`, { method: 'POST' });
}
// Duplicates
export function fetchDuplicates() {
return api('/api/duplicates');
}
export function scanDuplicates() {
return api('/api/dedupe/scan', { method: 'POST' });
}
export function dismissGroup(photoIds) {
return api('/api/duplicates/dismiss', { method: 'POST', ...json({ photoIds }) });
}