Signed-in users get their preferences (hidden feeds, weather location, weather toggle) stored server-side keyed by their OIDC subject and synced across devices. Anonymous visitors keep using browser localStorage, so the site stays public. First sign-in migrates existing localStorage prefs up. - config: [web.auth] section (issuer, client_id/secret, redirect, session_secret) - storage: user_preferences table + Get/PutUserPrefs - web/auth: OIDC code flow, HMAC-signed session cookie, CSRF state + nonce - web/prefs_api: GET/PUT /api/preferences (auth-gated, 64KB cap) - frontend: prefs.js sync layer seeds localStorage from server, pushes on write - header: sign-in / account control OIDC discovery is non-fatal at boot: if Authentik is down, Pete serves anonymously rather than refusing to start.
146 lines
5.3 KiB
JavaScript
146 lines
5.3 KiB
JavaScript
// Per-feed visibility, stored in localStorage. Server still ships every story;
|
|
// we just hide cards whose data-source is in the disabled set.
|
|
(function () {
|
|
var KEY = "pete.disabledSources.v1";
|
|
|
|
function load() {
|
|
try {
|
|
var raw = localStorage.getItem(KEY);
|
|
if (!raw) return {};
|
|
var obj = JSON.parse(raw);
|
|
return obj && typeof obj === "object" ? obj : {};
|
|
} catch (e) { return {}; }
|
|
}
|
|
function save(set) {
|
|
try { localStorage.setItem(KEY, JSON.stringify(set)); } catch (e) {}
|
|
if (window.PetePrefs) window.PetePrefs.push();
|
|
}
|
|
|
|
var disabled = load();
|
|
|
|
function apply() {
|
|
var cards = document.querySelectorAll("[data-story-card]");
|
|
cards.forEach(function (c) {
|
|
var src = c.getAttribute("data-source") || "";
|
|
c.style.display = disabled[src] ? "none" : "";
|
|
});
|
|
}
|
|
|
|
function knownSources() {
|
|
// Prefer the server-provided full list (grouped by channel below).
|
|
var server = Array.isArray(window.PETE_SOURCES) ? window.PETE_SOURCES : [];
|
|
if (server.length > 0) {
|
|
var copy = server.slice();
|
|
// Surface any persisted-disabled sources that aren't in the config anymore.
|
|
var present = Object.create(null);
|
|
copy.forEach(function (s) { present[s.name] = true; });
|
|
Object.keys(disabled).forEach(function (n) {
|
|
if (!present[n]) copy.push({ name: n, channel: "" });
|
|
});
|
|
copy.sort(function (a, b) {
|
|
var ca = (a.channel || "~").toLowerCase();
|
|
var cb = (b.channel || "~").toLowerCase();
|
|
if (ca !== cb) return ca.localeCompare(cb);
|
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
});
|
|
return copy;
|
|
}
|
|
// Fallback: derive from DOM if the server didn't ship a list.
|
|
var seen = Object.create(null);
|
|
document.querySelectorAll("[data-story-card]").forEach(function (c) {
|
|
var src = c.getAttribute("data-source");
|
|
var ch = c.getAttribute("data-channel") || "";
|
|
if (src && !seen[src]) seen[src] = { name: src, channel: ch };
|
|
});
|
|
Object.keys(disabled).forEach(function (s) {
|
|
if (!seen[s]) seen[s] = { name: s, channel: "" };
|
|
});
|
|
return Object.keys(seen).map(function (k) { return seen[k]; }).sort(function (a, b) {
|
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
});
|
|
}
|
|
|
|
function render() {
|
|
var list = document.querySelector("[data-settings-list]");
|
|
var meta = document.querySelector("[data-settings-meta]");
|
|
if (!list) return;
|
|
var sources = knownSources();
|
|
list.innerHTML = "";
|
|
if (sources.length === 0) {
|
|
list.innerHTML = '<p class="px-2 py-4 text-sm text-center text-[color:var(--ink)]/50">No feeds configured.</p>';
|
|
} else {
|
|
var lastChannel = null;
|
|
sources.forEach(function (src, i) {
|
|
if (src.channel !== lastChannel) {
|
|
lastChannel = src.channel;
|
|
var hdr = document.createElement("div");
|
|
hdr.className = "px-3 pt-3 pb-1 text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 font-bold";
|
|
hdr.textContent = src.channel || "other";
|
|
list.appendChild(hdr);
|
|
}
|
|
var id = "pete-feed-" + i;
|
|
var row = document.createElement("label");
|
|
row.setAttribute("for", id);
|
|
row.className = "flex items-center justify-between gap-3 rounded-2xl px-3 py-2 hover:bg-[color:var(--ink)]/5 cursor-pointer";
|
|
var nameEl = document.createElement("span");
|
|
nameEl.className = "text-sm font-semibold truncate";
|
|
nameEl.textContent = src.name;
|
|
var box = document.createElement("input");
|
|
box.type = "checkbox";
|
|
box.id = id;
|
|
box.className = "h-4 w-4 shrink-0 accent-[color:var(--accent)]";
|
|
box.checked = !disabled[src.name];
|
|
box.addEventListener("change", function () {
|
|
if (box.checked) delete disabled[src.name];
|
|
else disabled[src.name] = true;
|
|
save(disabled);
|
|
apply();
|
|
updateMeta();
|
|
});
|
|
row.appendChild(nameEl);
|
|
row.appendChild(box);
|
|
list.appendChild(row);
|
|
});
|
|
}
|
|
updateMeta();
|
|
|
|
function updateMeta() {
|
|
if (!meta) return;
|
|
var off = Object.keys(disabled).length;
|
|
meta.textContent = off === 0 ? "All feeds enabled" : off + " hidden";
|
|
}
|
|
}
|
|
|
|
function open() {
|
|
render();
|
|
var dlg = document.getElementById("pete-settings");
|
|
if (dlg) dlg.classList.remove("hidden");
|
|
}
|
|
function close() {
|
|
var dlg = document.getElementById("pete-settings");
|
|
if (dlg) dlg.classList.add("hidden");
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
apply();
|
|
var trigger = document.querySelector("[data-settings-trigger]");
|
|
if (trigger) trigger.addEventListener("click", open);
|
|
var closeBtn = document.querySelector("[data-settings-close]");
|
|
if (closeBtn) closeBtn.addEventListener("click", close);
|
|
var dlg = document.getElementById("pete-settings");
|
|
if (dlg) dlg.addEventListener("click", function (e) {
|
|
if (e.target === dlg) close();
|
|
});
|
|
var reset = document.querySelector("[data-settings-reset]");
|
|
if (reset) reset.addEventListener("click", function () {
|
|
disabled = {};
|
|
save(disabled);
|
|
apply();
|
|
render();
|
|
});
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.key === "Escape") close();
|
|
});
|
|
});
|
|
})();
|