fix(library): a reading that never finished no longer says nought
`saveWalkedDirs` filled in a nought beside every folder a reading had listed but never opened. A reading stopped by a closed tab, and one kept to a branch, list the rest of the roll before they have been into any of it — 484 folders of one real roll read empty over tens of thousands of frames — and the record is folded back into itself, so the noughts stayed. Only a reading that met the whole roll may say nought now (`WalkedDirs.full`), and the screen drops a nought from a record that does not say so. An old record has no word to give, so its rows fall back on the catalogue instead. feat(library): search Immich from the filter row One box, one query, one node: the backend reads what is typed as a tag name when the key can see one and as a file name otherwise, and the answer is filed under the node itself. A share link searches its one album in place. The box is not a filter — it leaves the shelf, which is what the md's search permission is for. feat(ui): a shorter status line, and a wall that keeps its own row height The status line is one short 12px row at the foot of the library. A wall with fewer frames than there are columns no longer stretches its one row — and the cards' own borders with it — down to the stage.
This commit is contained in:
@@ -26,10 +26,14 @@ const KEY_NODL = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbb1234'; // the same key without ass
|
||||
const SHARE_KEY = 'share-key-1234567890';
|
||||
const ALBUM_A = '11111111-1111-4111-8111-111111111111';
|
||||
const ALBUM_B = '22222222-2222-4222-8222-222222222222';
|
||||
const TAG_HOME = '33333333-3333-4333-8333-333333333333';
|
||||
const TAG_TRIP = '44444444-4444-4444-8444-444444444444';
|
||||
const assetId = (n) => `00000000-0000-4000-8000-${String(n).padStart(12, '0')}`;
|
||||
const THUMB = Buffer.from('RIFF0000WEBPfake-tile-bytes');
|
||||
const ORIGINAL = Buffer.from('II*\0fake-original-bytes');
|
||||
|
||||
// The first frame is the one tagged `Nhà`, so a search for that word has exactly
|
||||
// one answer to find.
|
||||
const asset = (n, album) => ({
|
||||
id: assetId(n),
|
||||
type: 'IMAGE',
|
||||
@@ -39,6 +43,7 @@ const asset = (n, album) => ({
|
||||
height: 3000,
|
||||
exifInfo: { fileSizeInByte: 1000 * n },
|
||||
album,
|
||||
tags: n === 1 ? TAG_HOME : n === 2 ? TAG_TRIP : null,
|
||||
});
|
||||
|
||||
let pass = 0;
|
||||
@@ -103,9 +108,27 @@ function startFake() {
|
||||
]);
|
||||
}
|
||||
|
||||
// A tag list, and the one key that may not read it: searching by tag is the
|
||||
// one search a narrow key loses, and it has to lose it as an empty list
|
||||
// rather than as a failed search.
|
||||
if (url.pathname === '/api/tags') {
|
||||
if (!authed && !shareAuthed) return send(401, { message: 'Authentication required' });
|
||||
if (apiKey === KEY_NODL) return send(403, { message: 'Missing required permission: tag.read' });
|
||||
return send(200, [
|
||||
{ id: TAG_HOME, name: 'Nhà', value: 'nhà' },
|
||||
{ id: TAG_TRIP, name: 'du lịch', value: 'du lịch' },
|
||||
]);
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/search/metadata') {
|
||||
const all = [asset(1, ALBUM_A), asset(2, ALBUM_A), asset(3, ALBUM_B)];
|
||||
const filtered = Array.isArray(body.albumIds) ? all.filter((a) => body.albumIds.includes(a.album)) : all;
|
||||
let filtered = Array.isArray(body.albumIds) ? all.filter((a) => body.albumIds.includes(a.album)) : all;
|
||||
// The same reading the proxy makes of a search box: every field given is
|
||||
// one more thing an asset must be.
|
||||
if (Array.isArray(body.tagIds)) filtered = filtered.filter((a) => body.tagIds.includes(a.tags));
|
||||
if (typeof body.originalFileName === 'string') {
|
||||
filtered = filtered.filter((a) => a.originalFileName.includes(body.originalFileName));
|
||||
}
|
||||
const size = Number(body.size ?? 100);
|
||||
const page = Number(body.page ?? 1);
|
||||
const items = filtered.slice((page - 1) * size, page * size);
|
||||
@@ -301,6 +324,16 @@ try {
|
||||
const badAlbum = await user.req(`/immich/assets?server=${serverId}&album=nope`);
|
||||
check('a malformed album id is refused', badAlbum.status === 400, `got ${badAlbum.status}`);
|
||||
|
||||
// ---- the search box ------------------------------------------------------
|
||||
const byName = await user.req(`/immich/assets?server=${serverId}&q=photo-2`);
|
||||
check('a search reads the words as a file name', byName.body?.items?.length === 1 && byName.body?.items?.[0]?.name === 'photo-2.jpg', JSON.stringify(byName.body?.items));
|
||||
const byTag = await user.req(`/immich/assets?server=${serverId}&q=${encodeURIComponent('Nhà')}`);
|
||||
check('a search that names a tag finds that tag', byTag.body?.items?.length === 1 && byTag.body?.items?.[0]?.name === 'photo-1.jpg', JSON.stringify(byTag.body?.items));
|
||||
check('the tag ids it found travel as tag ids', seen.at(-1)?.path === '/api/search/metadata', JSON.stringify(seen.at(-1)));
|
||||
const noAnswer = await user.req(`/immich/assets?server=${serverId}&q=nothing-at-all`);
|
||||
check('a search that answers nothing is an empty page, not a failure', noAnswer.status === 200 && noAnswer.body?.items?.length === 0 && noAnswer.body?.hasMore === false, JSON.stringify(noAnswer.body));
|
||||
|
||||
|
||||
// ---- the pixels ---------------------------------------------------------
|
||||
const tile = await user.req(`/immich/thumb?server=${serverId}&id=${assetId(1)}`);
|
||||
check('a tile streams through the proxy', tile.status === 200 && tile.body.equals(THUMB), `got ${tile.status}`);
|
||||
@@ -329,6 +362,10 @@ try {
|
||||
check('a key without download is still accepted', noDownload.status === 200 && noDownload.body?.server?.canDownload === false, JSON.stringify(noDownload.body));
|
||||
const refused = await user.req(`/immich/original?server=${readerId}&id=${assetId(1)}`);
|
||||
check('its original is refused with the reason', refused.status === 502 && refused.body?.error === 'auth', JSON.stringify(refused.body));
|
||||
// The same narrow key searching by a tag name it cannot read: the 403 on the
|
||||
// tag list is an empty list of tags, not a search that came back broken.
|
||||
const narrowSearch = await user.req(`/immich/assets?server=${readerId}&q=${encodeURIComponent('Nhà')}`);
|
||||
check('a key without tag.read still searches by file name', narrowSearch.status === 200 && narrowSearch.body?.items?.length === 0, JSON.stringify(narrowSearch.body));
|
||||
|
||||
// ---- a share link is one album ------------------------------------------
|
||||
const shareProbe = await user.post('/immich/probe', { url, key: SHARE_KEY, type: 'share' });
|
||||
@@ -339,6 +376,8 @@ try {
|
||||
check('a share server is stored', shareSaved.status === 200 && typeof shareId === 'string', JSON.stringify(shareSaved.body));
|
||||
const shareAssets = await user.req(`/immich/assets?server=${shareId}&page=1&size=10`);
|
||||
check('a share key lists its album whole', shareAssets.body?.items?.length === 2, JSON.stringify(shareAssets.body));
|
||||
const shareSearch = await user.req(`/immich/assets?server=${shareId}&q=photo-5`);
|
||||
check('a share link searches its one album', shareSearch.body?.items?.length === 1 && shareSearch.body?.items?.[0]?.name === 'photo-5.jpg', JSON.stringify(shareSearch.body?.items));
|
||||
const shareTile = await user.req(`/immich/thumb?server=${shareId}&id=${assetId(5)}`);
|
||||
check('a share tile streams too', shareTile.status === 200 && shareTile.body.equals(THUMB), `got ${shareTile.status}`);
|
||||
check('a share request travels with ?key=', seen.at(-1)?.shareKey === SHARE_KEY, JSON.stringify(seen.at(-1)));
|
||||
|
||||
Reference in New Issue
Block a user