/* desktop-settings-shared.js — the shared engine behind every direction * prototype. One mock state model, one widget factory, and the net panel's * verify-everything contract simulated: a control applies, then a mock backing * "reads back" the real value and confirms (a green verify flash + a toast). * * Each prototype builds a different LAYOUT from these same widgets and binds to * this same state, so the five directions are genuinely comparable. */ const DS = (() => { // ---- mock state (what the real settings.py engine would read/write) ------ const state = { autodim: false, caffeine: false, touchpad: true, mouse: true, airplane: false, nightlight: false, dnd: false, brightness: 70, kbd: 40, powerprofile: "balanced", // performance | balanced | saver scene: null, // Focus | Presentation | Battery | Night | null wallpaper: { dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"], current: 2, day: 1, night: 4, }, laptop: true, // gates the airplane row }; // control metadata: glyph (nerd-font), label, and the "on/off" wording. const TOGGLES = { autodim: { glyph: "\u{f0335}", name: "Auto-dim", on: "dimming", off: "off" }, caffeine: { glyph: "\u{f0176}", name: "Caffeine", on: "awake", off: "idle ok" }, touchpad: { glyph: "\u{f0168}", name: "Touchpad", on: "on", off: "off" }, mouse: { glyph: "\u{f037d}", name: "Mouse", on: "on", off: "off" }, airplane: { glyph: "\u{f001d}", name: "Airplane", on: "on", off: "off" }, nightlight:{ glyph: "\u{f0594}", name: "Night light", on: "warm", off: "off" }, dnd: { glyph: "\u{f009b}", name: "Do Not Disturb",on:"silenced",off: "notifying" }, }; const SLIDERS = { brightness: { glyph: "\u{f00df}", name: "Brightness", floor: 5 }, kbd: { glyph: "\u{f60f}", name: "Keyboard", floor: 0 }, }; const PROFILES = [ { id: "performance", label: "Perf" }, { id: "balanced", label: "Balanced" }, { id: "saver", label: "Saver" }, ]; // scenes flip several toggles + a profile at once (the iOS-Focus pattern). const SCENES = { Focus: { glyph: "\u{f0210}", sets: { dnd: true, autodim: false, nightlight: false, powerprofile: "balanced" } }, Presentation: { glyph: "\u{f0379}", sets: { caffeine: true, autodim: false, dnd: true, powerprofile: "performance" } }, Battery: { glyph: "\u{f0079}", sets: { autodim: true, powerprofile: "saver", nightlight: false } }, Night: { glyph: "\u{f0594}", sets: { nightlight: true, dnd: true, autodim: true, powerprofile: "saver" } }, }; let toastEl = null; const listeners = []; const onChange = (fn) => listeners.push(fn); const emit = () => listeners.forEach((f) => f(state)); function toast(msg, ok) { if (!toastEl) return; toastEl.classList.remove("empty"); toastEl.innerHTML = ok ? `${msg} ✓ verified` : msg; clearTimeout(toast._t); toast._t = setTimeout(() => { toastEl.classList.add("empty"); toastEl.textContent = "waiting…"; }, 2600); } // The verify-everything contract: apply, then a mock backing reads back the // actual value and confirms. cb runs on the confirmed readback. function applyVerified(label, mutate, el, cb) { mutate(); emit(); setTimeout(() => { if (el) { el.classList.remove("verified"); void el.offsetWidth; el.classList.add("verified"); } toast(label, true); if (cb) cb(); }, 140); } function setToggle(key, el) { const meta = TOGGLES[key]; applyVerified( `${meta.name}: ${state[key] ? meta.off : meta.on}`, () => { state[key] = !state[key]; state.scene = null; }, el ); } function setSlider(key, value, el) { const meta = SLIDERS[key]; const v = Math.max(meta.floor, Math.min(100, Math.round(value))); applyVerified(`${meta.name}: ${v}%`, () => { state[key] = v; }, el); } function setProfile(id, el) { applyVerified(`Power profile: ${id}`, () => { state.powerprofile = id; state.scene = null; }, el); } function applyScene(name, el) { const sc = SCENES[name]; applyVerified(`Scene: ${name}`, () => { Object.assign(state, sc.sets); state.scene = name; }, el); } function momentary(label) { toast(`${label}…`, false); } // ---- small render helpers the prototypes call ---------------------------- const h = (tag, cls, txt) => { const e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; }; function toggleKey(key) { const meta = TOGGLES[key]; const on = state[key]; const el = h("div", "ds-key" + (on ? " on" : "")); el.innerHTML = `` + `${meta.glyph}` + `${meta.name}` + `${on ? meta.on : meta.off}`; el.onclick = () => setToggle(key, el); return el; } function toggleTile(key, detailCb) { const meta = TOGGLES[key]; const on = state[key]; const el = h("div", "ds-tile" + (on ? " on" : "")); el.innerHTML = `
` + `${meta.glyph}` + (detailCb ? `` : "") + `
` + `
${meta.name}
` + `
${on ? meta.on : meta.off}
`; el.onclick = (ev) => { if (detailCb && ev.target.classList.contains("t-detail")) { detailCb(); return; } setToggle(key, el); }; return el; } function slider(key) { const meta = SLIDERS[key]; const row = h("div", "ds-slider"); row.innerHTML = `${meta.glyph}` + `${meta.name}`; const inp = h("input"); inp.type = "range"; inp.min = meta.floor; inp.max = 100; inp.value = state[key]; const pct = h("span", "s-pct", state[key] + "%"); inp.oninput = () => { pct.textContent = inp.value + "%"; }; inp.onchange = () => setSlider(key, +inp.value, row); row.appendChild(inp); row.appendChild(pct); return row; } function profileSeg() { const seg = h("div", "ds-seg"); PROFILES.forEach((p) => { const b = h("button", state.powerprofile === p.id ? "cur" : "", p.label); b.onclick = () => setProfile(p.id, seg); seg.appendChild(b); }); return seg; } function sceneKey(name) { const sc = SCENES[name]; const el = h("div", "ds-scene" + (state.scene === name ? " cur" : "")); el.innerHTML = `${sc.glyph}${name}`; el.onclick = () => applyScene(name, el); return el; } function actionBtn(label, glyph, danger, fn) { const b = h("button", "ds-act" + (danger ? " danger" : "")); b.innerHTML = `${glyph}${label}`; b.onclick = fn || (() => momentary(label)); return b; } // Wallpaper sub-view: directory list, thumbnails, day/night pair, sun-time. const SWATCHES = [ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)", "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)", "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)", ]; function wallpaperSubview(closeCb) { const sv = h("div", "ds-subview"); const head = h("div", "ds-sub-head"); head.innerHTML = `‹ BackWallpaper`; head.querySelector(".back").onclick = closeCb; sv.appendChild(head); sv.appendChild(engrave("Current")); const thumbs = h("div", "ds-thumbs"); SWATCHES.forEach((g, i) => { const t = h("div", "ds-thumb" + (state.wallpaper.current === i ? " cur" : "")); t.style.background = g; if (i === state.wallpaper.day) t.appendChild(tag("day")); if (i === state.wallpaper.night) t.appendChild(tag("night")); t.onclick = () => { state.wallpaper.current = i; applyVerified(`Wallpaper #${i + 1}`, () => {}, t); sv.querySelectorAll(".ds-thumb").forEach((x, j) => x.classList.toggle("cur", j === i)); }; thumbs.appendChild(t); }); sv.appendChild(thumbs); sv.appendChild(engrave("Day / night pair")); const dn = h("div", "ds-daynight"); ["day", "night"].forEach((k) => { const cell = h("div", "dn"); const sw = h("div", "sw"); sw.style.background = SWATCHES[state.wallpaper[k]]; cell.appendChild(sw); cell.appendChild(h("div", "lbl", k === "day" ? "☀ sunup" : "☾ sundown")); dn.appendChild(cell); }); sv.appendChild(dn); sv.appendChild(h("div", "ds-note", "Swaps at local sunrise/sundown (sun-time from lat/long).")); sv.appendChild(engrave("Directories")); const dirs = h("div", "ds-dirs"); state.wallpaper.dirs.forEach((d) => { const r = h("div", "ds-dir"); r.innerHTML = `${d}`; dirs.appendChild(r); }); sv.appendChild(dirs); return sv; } function tag(t) { const e = h("span", "tag", t); return e; } function engrave(txt) { return h("div", "ds-engrave", txt); } function bindToast(el) { toastEl = el; el.classList.add("empty"); el.textContent = "waiting…"; } return { state, TOGGLES, SLIDERS, PROFILES, SCENES, onChange, toast, bindToast, toggleKey, toggleTile, slider, profileSeg, sceneKey, actionBtn, wallpaperSubview, engrave, h, setToggle, setSlider, setProfile, applyScene, momentary, }; })();