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 @@
+
0 photos
@@ -81,14 +81,6 @@ -
@@ -122,6 +114,9 @@

设置

+
@@ -181,7 +176,7 @@

设置