Files
mule-image/frontend/src/services/api.ts
dtoro 351ccd7bb4 refactor: unify Pick with active heap membership
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>
2026-04-07 23:20:31 +02:00

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