Files
malkuthro b5b46d7fcc feat(sidebar): Snapshot Compare — read-only A/B side-by-side with diff tint (#181) (#183)
Phase 1 of #181: read-only side-by-side Compare. A/B toggle renders the existing sidebar twice — live working copy (A) + a loaded snapshot (B), B labeled with its own name + diff-tinted (green=only in comparison, red=same name/graph differs ignoring savedAt). Non-destructive: read-only capture guard on B, onChoose chooser never calls applySnapshot. Phase 2 (pull-in + A<->B swap) tracked in #197.
2026-06-02 08:22:15 +02:00

133 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: 2025-2026 Kforge Labs <https://github.com/malkuthro/ComfyUI-Koolook>
// SPDX-License-Identifier: GPL-3.0-only
// =============================================================================
// User picks (localStorage, browser-local persistence)
// =============================================================================
import {
STORAGE_KEY,
PICKS_CHANGED_EVENT,
AUTOPULL_HIDDEN_KEY,
} from "./constants.js";
// Read-only render-source override (issue #181, Compare mode). When set, the
// render path (`loadUserPicks`) returns this list instead of the user's live
// localStorage picks, so the Compare view can render a *second* sidebar from a
// loaded snapshot without touching live state. Set only synchronously around a
// read-only render and always cleared afterwards (see `withSnapshotSource` in
// tree.js); never written back to localStorage.
let renderSourcePicks = null;
export function setPicksRenderSource(picks) {
renderSourcePicks = Array.isArray(picks) ? picks.filter(p => typeof p === "string") : [];
}
export function clearPicksRenderSource() {
renderSourcePicks = null;
}
export function loadUserPicks() {
if (renderSourcePicks) return [...renderSourcePicks];
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.filter(x => typeof x === "string") : [];
} catch (e) {
// Surface corruption so the user has a chance to spot it in the console
// before the next saveUserPicks() overwrites the bad blob with a fresh list.
console.warn("[Koolook] failed to parse user picks; returning empty list:", e);
return [];
}
}
function saveUserPicks(picks) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(picks));
return true;
} catch (e) {
console.warn("[Koolook] failed to save picks:", e);
return false;
}
}
// Tri-state: "added" | "duplicate" | "failed". Callers need to distinguish
// "save failed" from "already in list" so the user isn't told a write
// succeeded when localStorage rejected it (quota / private mode).
export function addToMyPicks(typeName) {
if (!typeName) return "failed";
// The `+` action always means "make this visible in favorites." If the
// user previously hid this type via `×` (when it was auto-pulled by a
// REPOS{select: "all"} entry), clear that hide as a side effect — so a
// subsequent gather pass surfaces it again either via picks or via auto-
// pull. Without this, `×` then `+` on the same Koolook node would do
// nothing visible — the hide would silently outlast the re-add.
unhideAutoPullType(typeName);
const picks = loadUserPicks();
if (picks.includes(typeName)) return "duplicate";
picks.push(typeName);
return saveUserPicks(picks) ? "added" : "failed";
}
export function removeFromMyPicks(typeName) {
saveUserPicks(loadUserPicks().filter(p => p !== typeName));
}
// =============================================================================
// Auto-pull hidden set — types the user has explicitly excluded from the
// REPOS{select: "all"} auto-pull. See AUTOPULL_HIDDEN_KEY in constants.js.
// =============================================================================
export function loadAutoPullHidden() {
try {
const raw = localStorage.getItem(AUTOPULL_HIDDEN_KEY);
if (!raw) return new Set();
const parsed = JSON.parse(raw);
return new Set(Array.isArray(parsed) ? parsed.filter(x => typeof x === "string") : []);
} catch (e) {
console.warn("[Koolook] failed to parse autoPullHidden; starting empty:", e);
return new Set();
}
}
function saveAutoPullHidden(set) {
try {
localStorage.setItem(AUTOPULL_HIDDEN_KEY, JSON.stringify([...set]));
return true;
} catch (e) {
console.warn("[Koolook] failed to save autoPullHidden:", e);
return false;
}
}
export function hideAutoPullType(typeName) {
if (!typeName) return;
const set = loadAutoPullHidden();
if (set.has(typeName)) return;
set.add(typeName);
saveAutoPullHidden(set);
}
export function unhideAutoPullType(typeName) {
if (!typeName) return;
const set = loadAutoPullHidden();
if (!set.delete(typeName)) return;
saveAutoPullHidden(set);
}
// Bulk-replace the entire pick list. Used by the snapshot import flow —
// the caller has just deserialized a preset and wants picks to mirror it
// exactly. Filters non-strings defensively (snapshot files are user-
// editable and may have been hand-touched). Returns true on persist
// success, false on localStorage rejection (quota / private mode).
export function setAllPicks(picks) {
const cleaned = Array.isArray(picks)
? picks.filter(p => typeof p === "string")
: [];
return saveUserPicks(cleaned);
}
export function notifyPicksChanged() {
window.dispatchEvent(new CustomEvent(PICKS_CHANGED_EVENT));
}