Skip to content

Commit 3d0cfac

Browse files
authored
feat: search other pages when photo not found in suggested page (#4311)
1 parent 808efd0 commit 3d0cfac

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

resources/js/stores/AlbumState.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,14 +335,61 @@ export const useAlbumStore = defineStore("album-store", {
335335
return this.loadPhotos(1, false);
336336
},
337337

338+
/**
339+
* Search for a photo across pages that have not yet been loaded.
340+
*
341+
* The search first goes backward (prepending pages from startPage-1 down to 1),
342+
* then forward (appending pages from startPage+1 up to photos_last_page), stopping
343+
* as soon as the photo is found. Each iteration guards against concurrent
344+
* navigation by comparing this.albumId with requestedAlbumId.
345+
*
346+
* @param photoId - The photo ID to search for
347+
* @param startPage - The page that was already loaded (search starts around it)
348+
* @param requestedAlbumId - The album ID captured at the start of load(); used to
349+
* abort the search when the user navigates away
350+
*/
351+
async _searchPhotoInPages(photoId: string, startPage: number, requestedAlbumId: string): Promise<void> {
352+
const photosState = usePhotosStore();
353+
354+
// Clamp to the actual last page so an out-of-range `?page=N` doesn't trigger
355+
// a long chain of requests for pages that do not exist.
356+
const effectiveStart = Math.min(startPage, this.photos_last_page);
357+
358+
// Search backward: prepend pages from startPage-1 down to 1
359+
for (let p = effectiveStart - 1; p >= 1; p--) {
360+
if (this.albumId !== requestedAlbumId) {
361+
return;
362+
}
363+
await this.loadPhotos(p, false, true);
364+
if (photosState.photos.some((ph) => ph.id === photoId)) {
365+
return;
366+
}
367+
}
368+
369+
// Search forward: append pages from startPage+1 up to photos_last_page
370+
for (let p = effectiveStart + 1; p <= this.photos_last_page; p++) {
371+
if (this.albumId !== requestedAlbumId) {
372+
return;
373+
}
374+
await this.loadPhotos(p, true, false);
375+
if (photosState.photos.some((ph) => ph.id === photoId)) {
376+
return;
377+
}
378+
}
379+
},
380+
338381
/**
339382
* Load the album metadata and first batch of photos.
340383
*
341384
* @param startPage - The page to load first. When provided (>1), that page is loaded
342385
* immediately so a directly linked photo can be displayed, then
343386
* pages 1…startPage-1 are loaded in the background (prepended).
387+
* @param photoId - Optional photo ID expected on this page. When given and not
388+
* found in startPage, other pages are searched sequentially
389+
* (backward then forward) before giving up and showing the album.
390+
* The ?page=N query parameter is treated as a hint, not a guarantee.
344391
*/
345-
async load(startPage: number = 1): Promise<void> {
392+
async load(startPage: number = 1, photoId?: string): Promise<void> {
346393
const togglableState = useTogglablesStateStore();
347394
const photosState = usePhotosStore();
348395
const albumsStore = useAlbumsStore();
@@ -396,17 +443,29 @@ export const useAlbumStore = defineStore("album-store", {
396443
this.smartAlbum = data.data.resource as App.Http.Resources.Models.HeadSmartAlbumResource;
397444
}
398445

446+
// When a specific photo is expected but was not found in the initial page,
447+
// search other pages sequentially (backward then forward). This is run
448+
// concurrently with album loading so the album header appears quickly.
449+
const photoFoundInInitialPage = photoId !== undefined && photosState.photos.some((p) => p.id === photoId);
450+
if (photoId !== undefined && !photoFoundInInitialPage) {
451+
loader.push(this._searchPhotoInPages(photoId, resolvedStart, requestedAlbumId));
452+
}
453+
399454
await Promise.all(loader);
400455

401456
// Fire off background loading of immediately preceding pages (prepend).
457+
// Only done when we are NOT searching (the sequential search already covers
458+
// those pages, and running both would load them twice).
402459
// Capped at the 5 most-recent previous pages to avoid issuing too many
403460
// concurrent requests when jumping to a high page number (e.g. page 50).
404461
// These are intentionally NOT awaited so the photo panel can render
405462
// while earlier pages stream in, without blocking albumStore.load().
406-
const backgroundPagesLimit = 5;
407-
const firstBackgroundPage = Math.max(1, resolvedStart - backgroundPagesLimit);
408-
for (let p = resolvedStart - 1; p >= firstBackgroundPage; p--) {
409-
void this.loadPhotos(p, false, true);
463+
if (photoId === undefined || photoFoundInInitialPage) {
464+
const backgroundPagesLimit = 5;
465+
const firstBackgroundPage = Math.max(1, resolvedStart - backgroundPagesLimit);
466+
for (let p = resolvedStart - 1; p >= firstBackgroundPage; p--) {
467+
void this.loadPhotos(p, false, true);
468+
}
410469
}
411470
})
412471
.catch((error) => {

resources/js/views/gallery-panels/Album.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ function getStartPage(): number {
234234
235235
async function load() {
236236
const startPage = getStartPage();
237-
await Promise.allSettled([layoutStore.load(), lycheeStore.load(), userStore.load(), albumStore.load(startPage)]);
237+
await Promise.allSettled([layoutStore.load(), lycheeStore.load(), userStore.load(), albumStore.load(startPage, photoId.value)]);
238238
catalogStore.albumId = albumId.value;
239239
catalogStore.load();
240240
orderManagement.load();

0 commit comments

Comments
 (0)