feat(library): mint a server frame's tile once and keep it, always show the whole branch under the open node, and draw the year tick as a grey rule
This commit is contained in:
@@ -110,7 +110,6 @@ const OPEN_KEY = 'recipescam.library.open';
|
||||
// shut them out, and the chip wrote that first answer down on the very first
|
||||
// visit — so the old key holds 'true' for every reader who never touched it, and
|
||||
// reading it would keep them on a default they never chose.
|
||||
const DEEP_KEY = 'recipescam.library.deep2';
|
||||
const VIEW_KEY = 'recipescam.library.view';
|
||||
const SORT_BY_KEY = 'recipescam.library.sortBy';
|
||||
const SORT_DIR_KEY = 'recipescam.library.sortDir';
|
||||
@@ -461,13 +460,6 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
for (const parent of nodeParents) set.add(parent);
|
||||
return set;
|
||||
});
|
||||
// Whether the strip draws the whole branch under the open node or only the
|
||||
// node itself. The node the reader picked is what the screen shows: the frames
|
||||
// in the folders under it are asked for, not assumed. A roll whose top level
|
||||
// holds date folders then reads as those folders, and the frames behind them
|
||||
// arrive when the reader says so. The answer is picked up where the last visit
|
||||
// left it, the same as the open node above.
|
||||
const [deep, setDeep] = useState(() => recall(DEEP_KEY) === 'true');
|
||||
// The frame that is up in the open node — raised by a click on its thumbnail,
|
||||
// and picked up where the last visit left it, so a reader who leaves for the
|
||||
// studio comes back to the negative they were looking at rather than to the
|
||||
@@ -967,9 +959,6 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
useEffect(() => {
|
||||
keep(OPEN_KEY, [...expanded].join(','));
|
||||
}, [expanded]);
|
||||
useEffect(() => {
|
||||
keep(DEEP_KEY, String(deep));
|
||||
}, [deep]);
|
||||
useEffect(() => {
|
||||
keep(VIEW_KEY, view);
|
||||
}, [view]);
|
||||
@@ -1613,9 +1602,9 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
// not set the job off again; the rows of that same render are what goes.
|
||||
const normRoot = normPath(rootFolder.name);
|
||||
const rows = photos.filter((p) => normPath(p.folder ?? '') === normRoot);
|
||||
void makeThumbsFor(rootFolder, rel, { below: deep, rows });
|
||||
void makeThumbsFor(rootFolder, rel, { below: true, rows });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [deep, loaded, makeThumbsFor, node, photos.length, relOf, rootFolder]);
|
||||
}, [loaded, makeThumbsFor, node, photos.length, relOf, rootFolder]);
|
||||
|
||||
// The tree: one row per picked folder, then one per subfolder, indented by its
|
||||
// depth — the folders the walk has named, whether or not a frame has been read
|
||||
@@ -1816,8 +1805,8 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
setNode(nodes.find((n) => n.key === rootName || n.key === normPath(rootName ?? ''))?.key ?? nodes[0]?.key ?? null);
|
||||
}, [loaded, node, nodes, rootName]);
|
||||
|
||||
// What the open node draws: its own frames, and — when the reader has asked
|
||||
// for the branch — the ones in every subfolder under it.
|
||||
// What the open node draws: its own frames and those of every folder under it,
|
||||
// which is the whole of the branch the row above it counted.
|
||||
const shown = useMemo(() => {
|
||||
if (!rootName) return photos;
|
||||
const normRoot = normPath(rootName);
|
||||
@@ -1826,21 +1815,18 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
const below = dir === ''
|
||||
? (p: LibraryPhoto) => normPath(p.dir ?? '') !== ''
|
||||
: (p: LibraryPhoto) => normPath(p.dir ?? '').startsWith(`${dir}/`);
|
||||
const own: LibraryPhoto[] = [];
|
||||
const both: LibraryPhoto[] = [];
|
||||
for (const p of photos) {
|
||||
if (normPath(p.folder ?? '') !== normRoot) continue;
|
||||
const pDir = normPath(p.dir ?? '');
|
||||
const isOwn = pDir === dir;
|
||||
if (isOwn) own.push(p);
|
||||
if (isOwn || below(p)) both.push(p);
|
||||
if (pDir === dir || below(p)) both.push(p);
|
||||
}
|
||||
// A row counts what sits at or under its folder, so a folder whose frames all
|
||||
// lie one level down would open on an empty screen under a number that says
|
||||
// otherwise. The branch is drawn then, and only then: a folder holding frames
|
||||
// of its own opens on those, and the toggle keeps the last word.
|
||||
return deep || !own.length ? both : own;
|
||||
}, [deep, node, photos, rootName]);
|
||||
// A folder is its branch: the row counts what sits at or under the folder, so
|
||||
// the wall under it holds the same frames, and a frame two levels down is not
|
||||
// a frame the reader has to go and find. A sibling's frames are the one thing
|
||||
// left out.
|
||||
return both;
|
||||
}, [node, photos, rootName]);
|
||||
|
||||
// What the thumbnail view draws, out of what the open node holds. A frame's
|
||||
// hour is the hour the shutter fired in, on this machine's clock — the same
|
||||
@@ -1933,7 +1919,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
// ponytail: only the year option list is folder-derived; stars and hours are
|
||||
// fixed lists that cannot go stale. Re-check the others if they ever grow.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [node, deep, rootName, years]);
|
||||
}, [node, rootName, years]);
|
||||
|
||||
// On-demand fetch for selected year if user has Immich servers and local photos for that year are missing
|
||||
const fetchYearPhotos = useCallback(
|
||||
@@ -2642,13 +2628,10 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
// it raises, not to the studio. The word is only for a frame that has no
|
||||
// thumbnail to give: one that has one and has not been handed it yet — the wall
|
||||
// hands pictures out only around the eye — keeps the box and says nothing.
|
||||
// A frame the wall has no tile for yet: a server's own frame is drawn from the
|
||||
// address the proxy serves it at rather than left as an empty box. Keeping a
|
||||
// cloud library's tiles as blobs is a copy of every picture anyone scrolled past
|
||||
// (`!p.remoteId` in `useThumbUrls` is the other half of that: the wall does not
|
||||
// mint them), and the browser already keeps what it fetched for the day
|
||||
// `/api/immich/thumb` says it may — so the wall scrolls on and the visit after
|
||||
// draws without asking twice.
|
||||
// A frame the wall has no tile for yet: a server's own frame is drawn straight
|
||||
// from the address the proxy serves it at rather than left as an empty box, and
|
||||
// the tile is minted and kept beside it (`useThumbUrls`), so what the reader
|
||||
// sees first is the picture and what the visit after reads is this machine.
|
||||
const tile = (photo: LibraryPhoto, onPick?: () => void, onAr?: (ar: number) => void) => {
|
||||
const src = urls[photo.id] ?? (photo.remoteId ? remoteUrl('thumbnail', photo) : null);
|
||||
return src ? (
|
||||
@@ -2925,19 +2908,6 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
||||
<rect x="9" y="9" width="5" height="5" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Whether the strip draws the branch under the open folder or
|
||||
only the folder itself. The label says what the click does,
|
||||
`on` says where the strip is now. */}
|
||||
<button
|
||||
type="button"
|
||||
className={`chip${deep ? ' on' : ''}`}
|
||||
data-key="lib-subs"
|
||||
aria-pressed={deep}
|
||||
title={t('lib.subsHint')}
|
||||
onClick={() => setDeep((prev) => !prev)}
|
||||
>
|
||||
<span className="chip-label">{t(deep ? 'lib.subsHide' : 'lib.subsShow')}</span>
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -4214,9 +4184,12 @@ function useThumbUrls(photos: LibraryPhoto[], also?: LibraryPhoto | null, stamp?
|
||||
setUrls((prev) => ({ ...prev, ...landed }));
|
||||
}
|
||||
|
||||
// A frame off the disk is one no server owns: its file is asked of the
|
||||
// folder's handle when the tile is made, so there is no handle to test for.
|
||||
const queue = list.filter((p) => !(p.id in landed) && !p.remoteId);
|
||||
// A server's own frame is minted here as well: its tile is the one Immich
|
||||
// drew, asked for once and kept in the store, so the day the reader jumps
|
||||
// back to that year the wall draws it out of this machine instead of
|
||||
// paying the round trip again — the wall is read up and down the roll, and
|
||||
// every jump is otherwise the same frames asked of the server a second time.
|
||||
const queue = list.filter((p) => !(p.id in landed));
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(WALL_LANES, queue.length) }, async () => {
|
||||
while (alive && queue.length) {
|
||||
|
||||
Reference in New Issue
Block a user