|
|
|
@@ -110,6 +110,7 @@ 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';
|
|
|
|
@@ -460,6 +461,13 @@ 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. On to begin with: a folder is its branch, so the root draws the
|
|
|
|
|
// frames of every folder under it — a server's own folder among them — and a
|
|
|
|
|
// frame two levels down is not a frame the reader has to go and find. The
|
|
|
|
|
// toggle keeps the last word, and the answer is picked up where the last visit
|
|
|
|
|
// left it, the same as the open node below.
|
|
|
|
|
const [deep, setDeep] = useState(() => recall(DEEP_KEY) !== 'false');
|
|
|
|
|
// 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
|
|
|
|
@@ -959,6 +967,9 @@ 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]);
|
|
|
|
@@ -1533,7 +1544,10 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
|
|
|
|
// folder — that one is not a folder of this roll.
|
|
|
|
|
const relOf = useCallback(
|
|
|
|
|
(key: string): string | null => {
|
|
|
|
|
if (!rootName) return null;
|
|
|
|
|
// `null` and not falsy: a picked drive root is named `\`, which folds to the
|
|
|
|
|
// empty root, and a guard on the name itself would take every row of such a
|
|
|
|
|
// roll for a row of another folder.
|
|
|
|
|
if (rootName === null) return null;
|
|
|
|
|
const normRoot = normPath(rootName);
|
|
|
|
|
const normKey = normPath(key);
|
|
|
|
|
if (normKey === normRoot) return '';
|
|
|
|
@@ -1602,9 +1616,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: true, rows });
|
|
|
|
|
void makeThumbsFor(rootFolder, rel, { below: deep, rows });
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [loaded, makeThumbsFor, node, photos.length, relOf, rootFolder]);
|
|
|
|
|
}, [deep, 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
|
|
|
|
@@ -1805,28 +1819,36 @@ 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 those of every folder under it,
|
|
|
|
|
// which is the whole of the branch the row above it counted.
|
|
|
|
|
// What the open node draws: its own frames and, while the toggle is on, the
|
|
|
|
|
// ones in every folder under it — the whole of the branch the row above it
|
|
|
|
|
// counted, a server's own folder among them.
|
|
|
|
|
const shown = useMemo(() => {
|
|
|
|
|
if (!rootName) return photos;
|
|
|
|
|
// The empty root is a root: `D:\` is what the picker calls a drive, and `''`
|
|
|
|
|
// is what it folds to. Read as "no folder picked" it hands back the whole
|
|
|
|
|
// catalogue for every row of that roll — the root's frames under a year, and
|
|
|
|
|
// a server's frames under both.
|
|
|
|
|
if (rootName === null) return photos;
|
|
|
|
|
const normRoot = normPath(rootName);
|
|
|
|
|
const normNode = normPath(node ?? '');
|
|
|
|
|
const dir = normNode === normRoot ? '' : normNode.slice(normRoot.length + 1);
|
|
|
|
|
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 ?? '');
|
|
|
|
|
if (pDir === dir || below(p)) both.push(p);
|
|
|
|
|
const isOwn = pDir === dir;
|
|
|
|
|
if (isOwn) own.push(p);
|
|
|
|
|
if (isOwn || below(p)) both.push(p);
|
|
|
|
|
}
|
|
|
|
|
// 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]);
|
|
|
|
|
// A row counts what sits at or under its folder, so a folder with nothing of
|
|
|
|
|
// its own to draw is drawn as its branch whatever the toggle says: a folder
|
|
|
|
|
// whose frames all lie one level down would otherwise open on an empty screen
|
|
|
|
|
// under a number that says otherwise.
|
|
|
|
|
return deep || !own.length ? both : own;
|
|
|
|
|
}, [deep, 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
|
|
|
|
@@ -1919,7 +1941,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, rootName, years]);
|
|
|
|
|
}, [node, deep, rootName, years]);
|
|
|
|
|
|
|
|
|
|
// On-demand fetch for selected year if user has Immich servers and local photos for that year are missing
|
|
|
|
|
const fetchYearPhotos = useCallback(
|
|
|
|
@@ -2908,6 +2930,19 @@ 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>
|
|
|
|
|