feat(library): tighten duplicates scope, drop in-app upload, polish loaders
- duplicates: restrict the /library/duplicates/groups query to photos whose folder path actually lives under an active SourceRoot in the user's settings. Nextcloud's "move to trash" flow was leaving .delete/purge-1 Folder rows wired to the original source_root_id, leaking those entries into the Duplicates view as ghost paths that the user never opted into. - discard: add a spinner to the "Delete N" and "Empty discard pile" buttons (and their confirm dialogs) while the destructive mutation is in flight, so the user gets immediate feedback for a slow operation. - timeline: render a bottom-of-grid "Loading more photos…" indicator while usePhotosQuery's background cursor loop is still pulling pages. Backed by a tiny Zustand store the loop drives via a balanced start/stop (counter, not boolean, so rapid filter changes can't flip the flag false while a fresh loop is alive). - remove client-side upload UI + /upload endpoint. Nextcloud is the authoritative ingress now; the duplicate path created confusion and the backend route is gone too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useFilterStore, filtersToParams } from '../store/filterStore'
|
||||
import { usePhotosBackgroundLoadingStore } from '../store/photosBackgroundLoadingStore'
|
||||
import api from '../services/api'
|
||||
import type { Photo } from '../types/photo'
|
||||
|
||||
/** Reactive selector for the "more pages still streaming" flag. Used by
|
||||
* thumbnail views to render a bottom-of-list spinner while
|
||||
* usePhotosQuery's background cursor loop is still pulling pages. */
|
||||
export function usePhotosLoadingMore(): boolean {
|
||||
return usePhotosBackgroundLoadingStore((s) => s.isLoadingMore)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimistically strip the supplied photo ids from every cached
|
||||
* timeline list. Used by discard / delete flows so the photos vanish
|
||||
@@ -100,29 +108,39 @@ export function usePhotosQuery() {
|
||||
|
||||
if (nextCursor) {
|
||||
// Fire-and-forget background loop using cursor chaining.
|
||||
// The background store is reactive — Timeline et al. subscribe
|
||||
// to render a "loading more…" indicator at the bottom of the
|
||||
// grid while pages keep arriving. start/stop is balanced in a
|
||||
// try/finally so an aborted/erroring loop can't leak the flag.
|
||||
const bg = usePhotosBackgroundLoadingStore.getState()
|
||||
bg.start()
|
||||
void (async () => {
|
||||
for (let i = 0; i < MAX_PAGES && nextCursor; i++) {
|
||||
if (signal?.aborted) return
|
||||
try {
|
||||
const page = await fetchCursorPage(
|
||||
{ per_page: PER_PAGE_BACKGROUND, cursor: nextCursor, ...filterParams },
|
||||
signal,
|
||||
)
|
||||
try {
|
||||
for (let i = 0; i < MAX_PAGES && nextCursor; i++) {
|
||||
if (signal?.aborted) return
|
||||
const more = page.photos || []
|
||||
nextCursor = page.next_cursor
|
||||
queryClient.setQueryData<Photo[]>(
|
||||
['photos', filterParams],
|
||||
(prev) => (prev ? [...prev, ...more] : more)
|
||||
)
|
||||
if (!nextCursor || more.length < PER_PAGE_BACKGROUND) return
|
||||
// Yield a beat between pages so the main thread stays
|
||||
// responsive (thumbnail decode, scroll handling) while
|
||||
// we're back-filling in the background.
|
||||
await new Promise((r) => setTimeout(r, INTER_PAGE_DELAY_MS))
|
||||
} catch {
|
||||
return
|
||||
try {
|
||||
const page = await fetchCursorPage(
|
||||
{ per_page: PER_PAGE_BACKGROUND, cursor: nextCursor, ...filterParams },
|
||||
signal,
|
||||
)
|
||||
if (signal?.aborted) return
|
||||
const more = page.photos || []
|
||||
nextCursor = page.next_cursor
|
||||
queryClient.setQueryData<Photo[]>(
|
||||
['photos', filterParams],
|
||||
(prev) => (prev ? [...prev, ...more] : more)
|
||||
)
|
||||
if (!nextCursor || more.length < PER_PAGE_BACKGROUND) return
|
||||
// Yield a beat between pages so the main thread stays
|
||||
// responsive (thumbnail decode, scroll handling) while
|
||||
// we're back-filling in the background.
|
||||
await new Promise((r) => setTimeout(r, INTER_PAGE_DELAY_MS))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
usePhotosBackgroundLoadingStore.getState().stop()
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user