Skip to content
149 changes: 121 additions & 28 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -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();
Expand Down Expand Up @@ -311,6 +360,7 @@ async function renderSettings() {
});
renderThemeButtons();
applyFx();
await initMenuBgColor();
renderHwDecode();
detectAndReportRenderer();
refreshModelStatus();
Expand Down Expand Up @@ -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));
});

Expand All @@ -1191,7 +1245,7 @@ function hasActiveFilters() {
activeLensFilters.size > 0 ||
focalMin != null ||
focalMax != null ||
minRating > 0
activeRatings.size < 6
);
}

Expand All @@ -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;
});
}
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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";
Expand Down
Loading
Loading