Pick and add-to-heap were two ways of saying "I want to keep this
one". Merging them: P now toggles the selection's membership in the
active heap. The is_picked flag goes away (orphaned in the DB the
same way is_trashed was).
Backend
- Drop is_picked from PhotoBase / PhotoUpdate / PhotoResponse and
from the photos list filter param.
- Drop the is_picked Column from the Photo model (DB column stays
on legacy installs but is no longer read or written).
- Drop the bulk action 'pick' branch.
- New GET /heaps/{id}/photo_ids returns just the flat string list.
Used by the frontend for fast client-side membership lookups
without fetching full photo records.
Frontend
- New hooks/useActiveHeapMembersQuery.ts → returns
{ activeHeap, memberIds: Set<string> }. Subscribes once at the
Timeline level and passes a derived isInActiveHeap bool down to
each PhotoThumbnail (avoids hundreds of thumbnails subscribing
to the same query).
- PhotoThumbnail: replaces the old check-icon Pick affordance with
a clear basket badge in the bottom-right corner — a small filled
pick-colour pill containing a ShoppingBasket icon — visible only
when the photo belongs to the active heap.
- P shortcut (useKeyboardShortcuts) now toggles membership: if every
selected photo is already a member, it removes them; otherwise it
adds the missing ones. T binding removed (P fully replaces it).
- RightSidebar Pick button is now a Pick / Picked toggle bound to
the active heap. Disabled with a hint when no heap is active.
Shows the heap name in its title attr.
- filterStore drops 'picked' and 'unflagged' from FlagFilter.
FilterBar's flag dropdown is now just Any / Discarded.
- LeftSidebar drops the "Flagged" virtual node (it just set
flag=picked, which no longer exists).
- KeyboardHints: P → "Pick → heap".
- Photo TS type drops is_picked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
221 lines
5.1 KiB
TypeScript
221 lines
5.1 KiB
TypeScript
import axios from 'axios'
|
|
|
|
const API_BASE_URL = 'http://localhost:8001/api/v1'
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
|
|
// Source Folders API
|
|
export const sourceFolders = {
|
|
list: async () => {
|
|
const response = await api.get('/folders')
|
|
return response.data
|
|
},
|
|
|
|
add: async (path: string, recursive: boolean = true) => {
|
|
const response = await api.post('/folders', {
|
|
path,
|
|
recursive,
|
|
watch: false, // Can be made configurable later
|
|
})
|
|
return response.data
|
|
},
|
|
|
|
scan: async (folderId: string) => {
|
|
const response = await api.post(`/folders/${folderId}/scan`)
|
|
return response.data
|
|
},
|
|
|
|
delete: async (folderId: string) => {
|
|
const response = await api.delete(`/folders/${folderId}`)
|
|
return response.data
|
|
},
|
|
}
|
|
|
|
// Photos API
|
|
export const photos = {
|
|
list: async (params?: {
|
|
limit?: number
|
|
offset?: number
|
|
folder_id?: string
|
|
heap_id?: string
|
|
rating?: number
|
|
flag?: string
|
|
}) => {
|
|
const response = await api.get('/photos', { params })
|
|
return response.data
|
|
},
|
|
|
|
get: async (photoId: string) => {
|
|
const response = await api.get(`/photos/${photoId}`)
|
|
return response.data
|
|
},
|
|
|
|
update: async (photoId: string, data: {
|
|
rating?: number
|
|
user_title?: string | null
|
|
user_notes?: string | null
|
|
color_label?: string | null
|
|
is_picked?: boolean
|
|
is_discarded?: boolean
|
|
taken_at?: string
|
|
}) => {
|
|
const response = await api.patch(`/photos/${photoId}`, data)
|
|
return response.data
|
|
},
|
|
|
|
bulkUpdate: async (photoIds: string[], data: {
|
|
rating?: number
|
|
flag?: string
|
|
heap_id?: string
|
|
discard?: boolean
|
|
}) => {
|
|
const response = await api.post('/photos/bulk', {
|
|
photo_ids: photoIds,
|
|
...data,
|
|
})
|
|
return response.data
|
|
},
|
|
|
|
getThumbnailUrl: (photoId: string, size: 'small' | 'medium' | 'large' = 'medium') => {
|
|
return `${API_BASE_URL}/photos/${photoId}/thumb/${size}`
|
|
},
|
|
|
|
getOriginalUrl: (photoId: string) => {
|
|
return `${API_BASE_URL}/photos/${photoId}/original`
|
|
},
|
|
|
|
/** Full-resolution display URL. Backend serves the original for web-safe
|
|
* formats and a transcoded WebP for RAW/HEIC/TIFF. */
|
|
getProxyUrl: (photoId: string) => {
|
|
return `${API_BASE_URL}/photos/${photoId}/proxy`
|
|
},
|
|
}
|
|
|
|
// Library API
|
|
export const library = {
|
|
scan: async () => {
|
|
const response = await api.post('/library/scan')
|
|
return response.data
|
|
},
|
|
|
|
scanStatus: async () => {
|
|
const response = await api.get('/library/scan/status')
|
|
return response.data
|
|
},
|
|
|
|
stats: async () => {
|
|
const response = await api.get('/library/stats')
|
|
return response.data
|
|
},
|
|
}
|
|
|
|
// Heaps API
|
|
export interface Heap {
|
|
id: string
|
|
name: string
|
|
is_active: boolean
|
|
created_at: string
|
|
updated_at: string | null
|
|
photo_count: number
|
|
}
|
|
|
|
export const heaps = {
|
|
list: async (): Promise<Heap[]> => {
|
|
const response = await api.get('/heaps')
|
|
return response.data
|
|
},
|
|
|
|
create: async (name: string): Promise<Heap> => {
|
|
const response = await api.post('/heaps', { name })
|
|
return response.data
|
|
},
|
|
|
|
update: async (
|
|
heapId: string,
|
|
data: { name?: string; is_active?: boolean }
|
|
): Promise<Heap> => {
|
|
const response = await api.patch(`/heaps/${heapId}`, data)
|
|
return response.data
|
|
},
|
|
|
|
delete: async (heapId: string): Promise<void> => {
|
|
await api.delete(`/heaps/${heapId}`)
|
|
},
|
|
|
|
/** Lightweight: just the photo ids in a heap, for client-side membership
|
|
* lookups (the basket affordance on thumbnails). */
|
|
photoIds: async (heapId: string): Promise<string[]> => {
|
|
const response = await api.get(`/heaps/${heapId}/photo_ids`)
|
|
return response.data
|
|
},
|
|
|
|
addPhotos: async (heapId: string, photoIds: string[]) => {
|
|
const response = await api.post(`/heaps/${heapId}/photos`, {
|
|
photo_ids: photoIds,
|
|
})
|
|
return response.data
|
|
},
|
|
|
|
removePhotos: async (heapId: string, photoIds: string[]) => {
|
|
const response = await api.delete(`/heaps/${heapId}/photos`, {
|
|
data: { photo_ids: photoIds },
|
|
})
|
|
return response.data
|
|
},
|
|
}
|
|
|
|
// Tags API
|
|
export const tags = {
|
|
list: async () => {
|
|
const response = await api.get('/tags')
|
|
return response.data
|
|
},
|
|
|
|
create: async (name: string, color?: string) => {
|
|
const response = await api.post('/tags', {
|
|
name,
|
|
color,
|
|
})
|
|
return response.data
|
|
},
|
|
|
|
update: async (tagId: string, data: {
|
|
name?: string
|
|
color?: string
|
|
}) => {
|
|
const response = await api.patch(`/tags/${tagId}`, data)
|
|
return response.data
|
|
},
|
|
|
|
delete: async (tagId: string) => {
|
|
const response = await api.delete(`/tags/${tagId}`)
|
|
return response.data
|
|
},
|
|
}
|
|
|
|
// Discard API
|
|
export const discard = {
|
|
list: async () => {
|
|
const response = await api.get('/discard')
|
|
return response.data
|
|
},
|
|
|
|
restore: async (photoIds: string[]) => {
|
|
const response = await api.post('/discard/restore', {
|
|
photo_ids: photoIds,
|
|
})
|
|
return response.data
|
|
},
|
|
|
|
empty: async () => {
|
|
const response = await api.delete('/discard/empty')
|
|
return response.data
|
|
},
|
|
}
|
|
|
|
export default api |