diff --git a/src/app.js b/src/app.js
index 90f3282..a8b52ba 100644
--- a/src/app.js
+++ b/src/app.js
@@ -70,6 +70,7 @@ const els = {
editCancel: document.getElementById("edit-cancel"),
searchInput: document.getElementById("search-input"),
searchMode: document.getElementById("search-mode"),
+ searchModeBtns: document.querySelectorAll("#search-mode .searchbar__mode-btn"),
semanticSearchInput: document.getElementById("semantic-search-input"),
btnSelectMode: document.getElementById("btn-select-mode"),
selectionBar: document.getElementById("selection-bar"),
@@ -211,27 +212,53 @@ function clearSharedFilters() {
minRating = 0;
filterFocalMin.value = "";
filterFocalMax.value = "";
- ratingFilterEl.value = "0";
- els.ratingFilterRejects.value = "0";
+ activeRatings.clear();
+ for (let i = 0; i <= 5; i++) activeRatings.add(i);
+ if (ratingFilterEl) {
+ ratingFilterEl.querySelectorAll('input[type="checkbox"]').forEach((cb) => { cb.checked = true; });
+ }
+ if (els.ratingFilterRejects) renderRejectRatings();
renderFilterDots();
updateFilterButton();
}
-// --- Theme (dark / light) — persisted in localStorage, no backend needed ---
+// --- Theme (dark / light / liquid-glass) — persisted in localStorage, no backend needed ---
const THEME_KEY = "tiol-theme";
+const MENU_BG_KEY = "menu_bg_color";
+
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
+ if (theme === "liquid-glass") {
+ document.body.classList.add("theme-liquid-glass");
+ } else {
+ document.body.classList.remove("theme-liquid-glass");
+ }
}
function initTheme() {
let saved = "dark";
try { saved = localStorage.getItem(THEME_KEY) || "dark"; } catch (e) {}
applyTheme(saved);
}
+
+async function applyMenuBgColor(color) {
+ if (!color) return;
+ document.documentElement.style.setProperty("--menu-bg-custom", color);
+ try { await invoke("set_setting", { key: MENU_BG_KEY, value: color }); } catch (e) {}
+}
+
+async function initMenuBgColor() {
+ let saved = "#ffffff";
+ try { saved = (await invoke("get_setting", { key: MENU_BG_KEY })) || "#ffffff"; } catch (e) {}
+ if (els.menuBgColor) els.menuBgColor.value = saved;
+ applyMenuBgColor(saved);
+}
+
function renderThemeButtons() {
const cur = document.documentElement.getAttribute("data-theme") || "dark";
els.themeOptions.querySelectorAll("[data-theme]").forEach((btn) => {
btn.classList.toggle("btn--active", btn.dataset.theme === cur);
});
}
+
els.themeOptions.addEventListener("click", (ev) => {
const btn = ev.target.closest("[data-theme]");
if (!btn) return;
@@ -270,6 +297,10 @@ els.toggleFxShadow.addEventListener("click", () => {
try { localStorage.setItem(FX_SHADOW_KEY, next); } catch (e) {}
applyFx();
});
+
+els.menuBgColor?.addEventListener("input", (ev) => {
+ applyMenuBgColor(ev.target.value);
+});
els.navPhotos.addEventListener("click", () => {
switchView("photos");
onboardingOnPhotosClicked();
@@ -311,6 +342,7 @@ async function renderSettings() {
});
renderThemeButtons();
applyFx();
+ await initMenuBgColor();
renderHwDecode();
detectAndReportRenderer();
refreshModelStatus();
@@ -1176,12 +1208,16 @@ const COLOR_HEX = {
// The unfiltered result of the current query — filters re-apply to it.
let allPhotos = [];
-// Star rating filter (C-17): only show photos with rating >= minRating.
-// 0 = no filter. Driven by the #rating-filter dropdown in the search bar.
-let minRating = 0;
+// Star rating filter (C-17): show only photos whose rating is in the active
+// set. Each checkbox toggles one rating (0–5). Unchecked ratings are hidden.
+// Default: all ratings selected.
+const activeRatings = new Set([0, 1, 2, 3, 4, 5]);
const ratingFilterEl = document.getElementById("rating-filter");
ratingFilterEl.addEventListener("change", () => {
- minRating = parseInt(ratingFilterEl.value, 10) || 0;
+ activeRatings.clear();
+ ratingFilterEl.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
+ if (cb.checked) activeRatings.add(parseInt(cb.value, 10));
+ });
renderPhotos(applyFilters(allPhotos));
});
@@ -1191,7 +1227,7 @@ function hasActiveFilters() {
activeLensFilters.size > 0 ||
focalMin != null ||
focalMax != null ||
- minRating > 0
+ activeRatings.size < 6
);
}
@@ -1217,8 +1253,8 @@ function applyFilters(photos) {
if (focalMin != null && p.focal_length < focalMin) return false;
if (focalMax != null && p.focal_length > focalMax) return false;
}
- // Star rating (C-17): photos without a rating (0) fail a >= filter.
- if (minRating > 0 && (p.rating || 0) < minRating) return false;
+ // Star rating (C-17): photo must have a rating currently checked.
+ if (activeRatings.size < 6 && !activeRatings.has(p.rating || 0)) return false;
return true;
});
}
@@ -1410,13 +1446,17 @@ document.getElementById("btn-color-filter-clear").addEventListener("click", () =
focalMax = null;
filterFocalMin.value = "";
filterFocalMax.value = "";
- minRating = 0;
- ratingFilterEl.value = "0";
+ activeRatings.clear();
+ for (let i = 0; i <= 5; i++) activeRatings.add(i);
+ ratingFilterEl.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
+ cb.checked = true;
+ });
renderFilterDots();
renderFilterLens();
updateFilterButton();
renderPhotos(applyFilters(allPhotos));
});
+
document.addEventListener("click", (e) => {
if (
!colorFilterPanel.hidden &&
@@ -1735,7 +1775,7 @@ async function setPhotoRating(p, n) {
const stars = p._card.querySelector(".card__stars");
if (stars) renderCardStars(stars, p.rating);
}
- if (minRating > 0) renderPhotos(applyFilters(allPhotos));
+ if (activeRatings.size < 6) renderPhotos(applyFilters(allPhotos));
} catch (e) {
alert(String(e));
}
@@ -2187,12 +2227,25 @@ function scheduleSearch() {
}
els.searchInput.addEventListener("input", scheduleSearch);
els.semanticSearchInput.addEventListener("input", scheduleSearch);
-els.searchMode.addEventListener("change", scheduleSearch);
+function setSearchMode(mode) {
+ els.searchModeBtns.forEach((btn) => {
+ btn.classList.toggle("searchbar__mode-btn--active", btn.dataset.mode === mode);
+ });
+}
+
+els.searchMode.addEventListener("click", (e) => {
+ const btn = e.target.closest(".searchbar__mode-btn");
+ if (!btn) return;
+ setSearchMode(btn.dataset.mode);
+ scheduleSearch();
+});
+// Default to semantic on startup.
+setSearchMode("semantic");
async function runSearch() {
const q2 = els.semanticSearchInput.value.trim();
const qName = els.searchInput.value.trim();
- const mode = els.searchMode.value;
+ const mode = els.searchMode.querySelector(".searchbar__mode-btn--active")?.dataset.mode || "semantic";
if (q2) {
try {
const res = await invoke("search", { query: q2, mode });
@@ -2284,19 +2337,8 @@ async function runRejectSearch() {
}
}
-// Rating dropdown of the rejects view shares `minRating` with the photos
-// view — keep both selects in sync (C-19).
-els.ratingFilterRejects.addEventListener("change", () => {
- minRating = parseInt(els.ratingFilterRejects.value, 10) || 0;
- ratingFilterEl.value = String(minRating);
- if (els.viewRejects.classList.contains("view--hidden")) {
- renderPhotos(applyFilters(allPhotos));
- } else {
- // Reject conditions must still apply — a bare applyFilters would show
- // every photo on the rejects page (C-19.11).
- renderPhotos(applyRejectConds(applyFilters(allRejects)));
- }
-});
+// Rejects rating filter uses the same activeRatings Set as the photos view.
+// The checkbox panel lives inside #reject-cond-panel and mirrors #color-filter.
// Both "Filter" buttons (photos + rejects) open the same global panel,
// anchored under whichever button was clicked.
@@ -2369,6 +2411,29 @@ listen("reject-analysis-complete", () => {
loadRejects();
});
+function readRejectRatings() {
+ activeRatings.clear();
+ els.ratingFilterRejects.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
+ if (cb.checked) activeRatings.add(parseInt(cb.value, 10));
+ });
+}
+
+function renderRejectRatings() {
+ if (!els.ratingFilterRejects) return;
+ els.ratingFilterRejects.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
+ cb.checked = activeRatings.has(parseInt(cb.value, 10));
+ });
+}
+
+els.ratingFilterRejects?.addEventListener("change", () => {
+ readRejectRatings();
+ if (els.viewRejects.classList.contains("view--hidden")) {
+ renderPhotos(applyFilters(allPhotos));
+ } else {
+ renderPhotos(applyRejectConds(applyFilters(allRejects)));
+ }
+});
+
function renderRejectConds() {
els.rejectCondItems.textContent = "";
for (const c of REJECT_CONDS) {
@@ -2412,6 +2477,10 @@ els.btnRejectCond.addEventListener("click", (e) => {
els.btnRejectCondClear.addEventListener("click", () => {
activeRejectConds.clear();
renderRejectConds();
+ activeRatings.clear();
+ for (let i = 0; i <= 5; i++) activeRatings.add(i);
+ renderRejectRatings();
+ updateFilterButton();
els.btnRejectCond.classList.remove("searchbar__filter--active");
});
document.addEventListener("click", (e) => {
@@ -2717,6 +2786,11 @@ function onboardingOnPhotosClicked() {
console.error(e);
}
applyStaticI18n();
+ // Re-render dynamic i18n content that was created at module-load time with
+ // the default (en-US) locale before initI18n resolved the saved language.
+ renderFilterDots();
+ renderRejectConds();
+ renderRejectRatings();
initTheme();
applyFx();
// Debug flag gates AI-confidence badges — read it before first render.
diff --git a/src/index.html b/src/index.html
index 9c113c0..1821bbc 100644
--- a/src/index.html
+++ b/src/index.html
@@ -23,23 +23,23 @@