feat(library): let the shelf be read by month and by day

This commit is contained in:
2026-10-12 10:22:22 +07:00
parent 934a3aa056
commit 73b7398583
4 changed files with 61 additions and 9 deletions
+51 -7
View File
@@ -88,8 +88,8 @@ import { MobileLibrary } from './ui/MobileLibrary';
//
// ponytail: no search and no flag. A roll of date folders is three levels deep at
// most, and what a reader actually digs through a shelf with is the score they
// gave a frame, the year it was shot and the hours they shot it in — which are
// the three the thumbnail view filters on. Add the text box when a folder of
// gave a frame, and the year, month, day and hours it was shot in — which are
// what the thumbnail view filters on. Add the text box when a folder of
// mixed names makes one worth typing into.
interface Node {
// The tree's own key: a folder name, or `folder/sub/dir`.
@@ -116,6 +116,8 @@ const SORT_BY_KEY = 'recipescam.library.sortBy';
const SORT_DIR_KEY = 'recipescam.library.sortDir';
const MIN_STAR_KEY = 'recipescam.library.minStar';
const YEAR_KEY = 'recipescam.library.year';
const MONTH_KEY = 'recipescam.library.month';
const DAY_KEY = 'recipescam.library.day';
const FROM_KEY = 'recipescam.library.from';
const TO_KEY = 'recipescam.library.to';
// The frame the studio is holding (written on its side, see App.tsx): the shelf
@@ -451,10 +453,10 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
// mount: the address is what says the shelf is the screen up again.
const [here, setHere] = useState(() => window.location.pathname.startsWith('/library'));
// How the thumbnail view reads the open folder: every frame it holds, or only
// the ones that were given this many stars, shot in this year, shot between
// these hours — and in the order the reader picked. A way of looking, not part
// of the roll, so none of it outlives the visit; the strip below the frame does
// not follow it, because the strip is the shelf itself.
// the ones that were given this many stars, shot in this year, this month, on
// this day, between these hours — and in the order the reader picked. A way of
// looking, not part of the roll, so none of it outlives the visit; the strip
// below the frame does not follow it, because the strip is the shelf itself.
const [sortBy, setSortBy] = useState<'taken' | 'mtime' | 'type' | 'name' | 'star'>(
() => (recall(SORT_BY_KEY) as any) || 'taken'
);
@@ -463,6 +465,10 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
);
const [minStar, setMinStar] = useState(() => Number(recall(MIN_STAR_KEY)) || 0);
const [year, setYear] = useState(() => recall(YEAR_KEY) || 'all');
// Month and day of the shot, 'all' meaning every one. Numbers rather than
// names: a name is a language and this shelf is read in two of them.
const [month, setMonth] = useState(() => recall(MONTH_KEY) || 'all');
const [day, setDay] = useState(() => recall(DAY_KEY) || 'all');
const [from, setFrom] = useState(() => recall(FROM_KEY) || 'any');
const [to, setTo] = useState(() => recall(TO_KEY) || 'any');
// What the shelf's search box holds while it is being typed in. It is not a
@@ -475,6 +481,8 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
useEffect(() => { keep(SORT_DIR_KEY, sortDir); }, [sortDir]);
useEffect(() => { keep(MIN_STAR_KEY, String(minStar)); }, [minStar]);
useEffect(() => { keep(YEAR_KEY, year); }, [year]);
useEffect(() => { keep(MONTH_KEY, month); }, [month]);
useEffect(() => { keep(DAY_KEY, day); }, [day]);
useEffect(() => { keep(FROM_KEY, from); }, [from]);
useEffect(() => { keep(TO_KEY, to); }, [to]);
// How far the frame is magnified, and the pan offset relative to viewport center.
@@ -1805,6 +1813,8 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
if (minStar && (p.star ?? 0) < minStar) return false;
const at = new Date(p.taken);
if (year !== 'all' && at.getFullYear() !== Number(year)) return false;
if (month !== 'all' && at.getMonth() + 1 !== Number(month)) return false;
if (day !== 'all' && at.getDate() !== Number(day)) return false;
if (from !== 'any' && to !== 'any') {
const f = Number(from);
const tNum = Number(to);
@@ -1847,7 +1857,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
}
return diff * multiplier;
});
}, [shown, minStar, year, from, to, sortBy, sortDir]);
}, [shown, minStar, year, month, day, from, to, sortBy, sortDir]);
const frame = listed.find((p) => p.id === up) ?? listed[0] ?? null;
// The strip draws the tiles around the one under the eye, not the whole shelf.
@@ -2947,6 +2957,40 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
))}
</select>
{/* 3b. Month of the year */}
<select
className="adm-input"
data-key="lib-filter-month"
aria-label={t('lib.month')}
value={month}
onChange={(e) => setMonth(e.target.value)}
style={{ flexShrink: 0 }}
>
<option value="all">{t('lib.monthAll')}</option>
{Array.from({ length: 12 }, (_, i) => i + 1).map((m) => (
<option key={m} value={String(m)}>
{String(m).padStart(2, '0')}
</option>
))}
</select>
{/* 3c. Day of the month */}
<select
className="adm-input"
data-key="lib-filter-day"
aria-label={t('lib.day')}
value={day}
onChange={(e) => setDay(e.target.value)}
style={{ flexShrink: 0 }}
>
<option value="all">{t('lib.dayAll')}</option>
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
<option key={d} value={String(d)}>
{String(d).padStart(2, '0')}
</option>
))}
</select>
{/* 4. Time taken (From / To dropdown selects) */}
<TimeRangeSelectInput from={from} to={to} onChange={(f: string, tStr: string) => { setFrom(f); setTo(tStr); }} t={t} />
+4
View File
@@ -316,6 +316,10 @@ export const en: Dict = {
'lib.rotateRight': 'Turn this frame right',
'lib.year': 'YEAR',
'lib.yearAll': 'Any year',
'lib.month': 'MONTH',
'lib.monthAll': 'Any month',
'lib.day': 'DAY',
'lib.dayAll': 'Any day',
'lib.hourFrom': 'Shot from',
'lib.hourFromAny': 'Any hour',
'lib.hourTo': 'Shot until',
+4
View File
@@ -328,6 +328,10 @@ export const vi = {
'lib.rotateRight': 'Xoay ảnh sang phải',
'lib.year': 'NĂM',
'lib.yearAll': 'Mọi năm',
'lib.month': 'THÁNG',
'lib.monthAll': 'Mọi tháng',
'lib.day': 'NGÀY',
'lib.dayAll': 'Mọi ngày',
'lib.hourFrom': 'Chụp từ',
'lib.hourFromAny': 'Mọi giờ',
'lib.hourTo': 'Chụp đến',
+2 -2
View File
@@ -1447,8 +1447,8 @@ form[data-key='immich-modal'] {
card's border that much taller than the picture inside it. Rows keep the height
their own frames give them however few of them there are. */
.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; overflow-anchor: none; align-content: start; }
/* What the thumbnail wall is read through: the four selects and the count of what
is left. One row above the wall, and it does not scroll with it. */
/* What the thumbnail wall is read through: the selects and the count of what is
left. One row above the wall, and it does not scroll with it. */
.lib-root .adm-stage > .lib-filters,
.lib-filters {
flex: 0 0 auto;