fix(library): the phone's shelf opens on the frame the studio held

Coming back from the studio on a phone left the grid where it was: the
frame the studio was holding was lit by nothing and looked for by hand.
The shelf already stamps the frame it was left on and the wide screen
already opens on it; the phone's grid never read it.

The stamp travels down as a landing, and the grid lights that frame,
walks the list out to it when it sits past the rows drawn so far, and
scrolls it under the eye. Each landing is spent once, so a list that
changes under the reader is not dragged back to it.

probe-mobile-return.mjs, on a phone: the frame the studio held is the one
lit and the one in view, both when it is at the top of the shelf and when
it is buried past the first window.
This commit is contained in:
2026-10-15 20:41:36 +07:00
parent d17ae406e0
commit 694de421cf
2 changed files with 25 additions and 1 deletions
+6
View File
@@ -2390,6 +2390,10 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
// in another folder of the same tree (a frame id and a tree key are spelled the
// same way), and a frame the open node cannot list is a frame the shelf cannot
// raise at all, so the folder goes with it.
// The frame the shelf is to open on after the studio, stamped so that coming
// back to the same frame a second time is still a landing (the phone's grid
// reads it — see MobileLibrary's `focus`).
const [landing, setLanding] = useState<{ id: string; at: number } | null>(null);
const wasHere = useRef(here);
useEffect(() => {
const came = here && !wasHere.current;
@@ -2397,6 +2401,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
if (!came) return;
const last = recall(LAST_KEY);
if (!last) return;
setLanding({ id: last, at: Date.now() });
setUp(last);
scrolledFrameRef.current = null;
centredRef.current = last;
@@ -2753,6 +2758,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
user={user}
admin={admin}
onOpenStudio={open}
focus={landing}
onAddImmich={() => setAddImmich(true)}
onEditAlbums={(folder) => setEditServer(folder.server ?? null)}
onSyncServer={syncServer}
+19 -1
View File
@@ -20,6 +20,9 @@ interface MobileLibraryProps {
user: User | null;
admin: boolean;
onOpenStudio: (photo: LibraryPhoto) => Promise<void>;
// The frame the shelf is to open on when the reader comes back from the
// studio, stamped so that a second visit to the same frame lands again.
focus?: { id: string; at: number } | null;
onAddImmich: () => void;
onEditAlbums: (folder: LibraryFolder) => void;
onSyncServer: (folder: LibraryFolder) => Promise<void>;
@@ -101,6 +104,7 @@ export function MobileLibrary({
user,
admin,
onOpenStudio,
focus,
onAddImmich,
onEditAlbums,
onSyncServer,
@@ -647,12 +651,26 @@ export function MobileLibrary({
sortOrder,
]);
// Landing on the frame the studio was holding: the grid is sent to it rather
// than left where it was. The frame may sit past the rows drawn so far, and it
// is only in the DOM on the render that walks them out — which is why the
// scroll below runs again when `renderLimit` moves. Each landing is spent
// once, so a list that changes under the reader is not dragged back to it.
const landed = useRef(0);
useEffect(() => {
if (!focus || landed.current === focus.at) return;
landed.current = focus.at;
setActiveId(focus.id);
const at = displayedPhotos.findIndex((p) => p.id === focus.id);
if (at >= 0) setRenderLimit((prev) => Math.max(prev, at + 1));
}, [focus, displayedPhotos]);
// Landing on that frame: the grid brings it under the eye rather than leaving
// the reader to hunt for it.
useEffect(() => {
if (viewMode !== 'grid' || !activeId) return;
scrollContainerRef.current?.querySelector('.mob-lib-thumb-cell.selected')?.scrollIntoView({ block: 'center' });
}, [viewMode, activeId]);
}, [viewMode, activeId, renderLimit]);
// Available years from photos & server timeline buckets
const availableYears = useMemo(() => {