feat(studio & library): sync Light & Color tab parameters (vibrance, highlight, shadow, white, black) to Gradient Mask & add multi-field sorting options

This commit is contained in:
2026-10-07 12:36:55 +07:00
parent 1c7be6dfd4
commit eb47afc63e
6 changed files with 82 additions and 29 deletions
+2 -1
View File
@@ -2676,7 +2676,7 @@ export function Workspace() {
// this helper too: same ±10 unit, same ×10 ruler, same green↔magenta gain, read
// on the mask's pixels — a local white balance.
const maskKnobRow = (
key: 'highlights' | 'shadows' | 'whites' | 'blacks' | 'clarity' | 'dehaze' | 'tint',
key: 'highlights' | 'shadows' | 'whites' | 'blacks' | 'clarity' | 'dehaze' | 'tint' | 'vibrance',
label: string
): SliderDef => {
const v = selMask?.[key] ?? 0;
@@ -2752,6 +2752,7 @@ export function Workspace() {
onChange: (v) => setMaskKnob({ saturation: Math.round(v) / 10 }),
onReset: () => setMaskKnob({ saturation: 0 }),
},
maskKnobRow('vibrance', 'VIBRANCE'),
maskKnobRow('highlights', 'HIGHLIGHT'),
maskKnobRow('shadows', 'SHADOW'),
maskKnobRow('whites', 'WHITE'),
+47 -12
View File
@@ -247,7 +247,8 @@ function Catalogue() {
// 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 [order, setOrder] = useState<'newest' | 'oldest' | 'star'>('newest');
const [sortBy, setSortBy] = useState<'taken' | 'mtime' | 'type' | 'name' | 'star'>('taken');
const [sortDir, setSortDir] = useState<'desc' | 'asc'>('desc');
const [minStar, setMinStar] = useState(0);
const [year, setYear] = useState('all');
const [from, setFrom] = useState('any');
@@ -965,12 +966,34 @@ function Catalogue() {
if (to !== 'any' && at.getHours() > Number(to)) return false;
return true;
});
// `shown` is already newest shutter first, so the oldest is the same list
// read backwards; the score comes first in the third, and the shutter breaks
// its ties the way the shelf does.
if (order === 'star') return [...rows].sort((a, b) => (b.star ?? 0) - (a.star ?? 0) || b.taken - a.taken);
return order === 'oldest' ? [...rows].reverse() : rows;
}, [shown, minStar, year, from, to, order]);
const getExt = (filename: string) => {
const idx = filename.lastIndexOf('.');
return idx !== -1 ? filename.slice(idx + 1).toLowerCase() : '';
};
const multiplier = sortDir === 'asc' ? 1 : -1;
return [...rows].sort((a, b) => {
let diff = 0;
if (sortBy === 'taken') {
diff = a.taken - b.taken;
} else if (sortBy === 'mtime') {
diff = (a.mtime ?? a.taken) - (b.mtime ?? b.taken);
} else if (sortBy === 'name') {
diff = a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
} else if (sortBy === 'type') {
const extA = getExt(a.name);
const extB = getExt(b.name);
diff = extA.localeCompare(extB) || a.name.localeCompare(b.name);
} else if (sortBy === 'star') {
diff = (a.star ?? 0) - (b.star ?? 0);
}
if (diff === 0) {
diff = a.taken - b.taken;
}
return diff * multiplier;
});
}, [shown, minStar, year, from, to, sortBy, sortDir]);
const handleSelectPhoto = useCallback(
(photo: LibraryPhoto, e: React.MouseEvent) => {
@@ -1578,15 +1601,27 @@ function Catalogue() {
<div className="chip-row lib-filters">
<select
className="adm-input"
data-key="lib-sort"
data-key="lib-sort-by"
aria-label={t('lib.sort')}
value={order}
onChange={(e) => setOrder(e.target.value as 'newest' | 'oldest' | 'star')}
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
>
<option value="newest">{t('lib.sortNewest')}</option>
<option value="oldest">{t('lib.sortOldest')}</option>
<option value="taken">{t('lib.sortDateTaken')}</option>
<option value="mtime">{t('lib.sortDateEdited')}</option>
<option value="type">{t('lib.sortFileType')}</option>
<option value="name">{t('lib.sortName')}</option>
<option value="star">{t('lib.sortStars')}</option>
</select>
<button
type="button"
className="adm-input"
data-key="lib-sort-dir"
title={sortDir === 'desc' ? t('lib.sortDesc') : t('lib.sortAsc')}
onClick={() => setSortDir((prev) => (prev === 'desc' ? 'asc' : 'desc'))}
style={{ minWidth: '40px', padding: '0 8px', textAlign: 'center', cursor: 'pointer', fontWeight: 600 }}
>
{sortDir === 'desc' ? '⬇' : '⬆'}
</button>
<select
className="adm-input"
data-key="lib-filter-star"
+6
View File
@@ -274,6 +274,12 @@ export const en: Dict = {
'lib.sortNewest': 'Newest first',
'lib.sortOldest': 'Oldest first',
'lib.sortStars': 'Most stars',
'lib.sortDateTaken': 'Date taken',
'lib.sortDateEdited': 'Date edited',
'lib.sortFileType': 'File type',
'lib.sortName': 'File name',
'lib.sortAsc': 'Ascending',
'lib.sortDesc': 'Descending',
'lib.rating': 'RATING',
'lib.ratingAll': 'Any rating',
'lib.ratingAtLeast': '{n}★ and up',
+6
View File
@@ -287,6 +287,12 @@ export const vi = {
'lib.sortNewest': 'Mới chụp trước',
'lib.sortOldest': 'Cũ nhất trước',
'lib.sortStars': 'Nhiều sao nhất',
'lib.sortDateTaken': 'Ngày chụp',
'lib.sortDateEdited': 'Ngày sửa/lưu',
'lib.sortFileType': 'Loại tệp',
'lib.sortName': 'Tên tệp',
'lib.sortAsc': 'Tăng dần',
'lib.sortDesc': 'Giảm dần',
'lib.rating': 'XẾP HẠNG',
'lib.ratingAll': 'Mọi mức sao',
'lib.ratingAtLeast': 'Từ {n} sao',