diff --git a/src/app.js b/src/app.js
index 90f3282..d7f760f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -47,6 +47,8 @@ const els = {
toggleFxShadow: document.getElementById("toggle-fx-shadow"),
fxAnimState: document.getElementById("fx-anim-state"),
fxShadowState: document.getElementById("fx-shadow-state"),
+ toggleFxGlass: document.getElementById("toggle-fx-glass"),
+ fxGlassState: document.getElementById("fx-glass-state"),
toggleHwDecode: document.getElementById("toggle-hw-decode"),
hwDecodeHint: document.getElementById("hw-decode-hint"),
btnRestart: document.getElementById("btn-restart"),
@@ -70,6 +72,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 +214,54 @@ 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);
}
function initTheme() {
let saved = "dark";
try { saved = localStorage.getItem(THEME_KEY) || "dark"; } catch (e) {}
+ // Legacy: the old "liquid-glass" theme is now split into dark/light + glass toggle.
+ if (saved === "liquid-glass") {
+ saved = "dark";
+ try { localStorage.setItem(THEME_KEY, "dark"); } catch (e) {}
+ try { localStorage.setItem(FX_GLASS_KEY, "1"); } 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;
@@ -244,6 +274,8 @@ els.themeOptions.addEventListener("click", (ev) => {
// --- FX toggles (C-19.11): animations / shadows — localStorage, default ON ---
const FX_ANIM_KEY = "tiol-fx-anim";
const FX_SHADOW_KEY = "tiol-fx-shadow";
+const FX_GLASS_KEY = "tiol-fx-glass";
+const FX_GLASS_KEY = "tiol-fx-glass";
function applyFx() {
let anim = "1";
@@ -270,6 +302,23 @@ els.toggleFxShadow.addEventListener("click", () => {
try { localStorage.setItem(FX_SHADOW_KEY, next); } catch (e) {}
applyFx();
});
+
+function applyFxGlass() {
+ const glass = (localStorage.getItem(FX_GLASS_KEY) || "1") === "1";
+ document.body.classList.toggle("fx-glass", glass);
+ if (els.fxGlassState) els.fxGlassState.textContent = t(glass ? "settings.on" : "settings.off");
+ if (els.toggleFxGlass) els.toggleFxGlass.classList.toggle("btn--active", glass);
+}
+
+els.toggleFxGlass?.addEventListener("click", () => {
+ const next = ((localStorage.getItem(FX_GLASS_KEY) || "1") === "1") ? "0" : "1";
+ try { localStorage.setItem(FX_GLASS_KEY, next); } catch (e) {}
+ applyFxGlass();
+});
+
+els.menuBgColor?.addEventListener("input", (ev) => {
+ applyMenuBgColor(ev.target.value);
+});
els.navPhotos.addEventListener("click", () => {
switchView("photos");
onboardingOnPhotosClicked();
@@ -311,6 +360,7 @@ async function renderSettings() {
});
renderThemeButtons();
applyFx();
+ await initMenuBgColor();
renderHwDecode();
detectAndReportRenderer();
refreshModelStatus();
@@ -1176,12 +1226,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 +1245,7 @@ function hasActiveFilters() {
activeLensFilters.size > 0 ||
focalMin != null ||
focalMax != null ||
- minRating > 0
+ activeRatings.size < 6
);
}
@@ -1217,8 +1271,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 +1464,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 +1793,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 +2245,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 +2355,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 +2429,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 +2495,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,8 +2804,14 @@ 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();
+ applyFxGlass();
// Debug flag gates AI-confidence badges — read it before first render.
try {
debugMode = (await invoke("get_setting", { key: "debug" })) === "1";
diff --git a/src/index.html b/src/index.html
index 9c113c0..069652b 100644
--- a/src/index.html
+++ b/src/index.html
@@ -23,23 +23,23 @@