feat: picker wheel settings are saved with the project
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xw1SXwGfadnrcxvgivgffg
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createDom } from "../../shell/dom.js";
|
||||
import { setupDom, teardownDom } from "../../composer/tests/_dom.js";
|
||||
|
||||
let hub, readPrefs, createProject;
|
||||
test.before(async () => {
|
||||
setupDom();
|
||||
({ createProject } = await import("../../shell/project.js"));
|
||||
({ default: hub, readPrefs } = await import("./index.js"));
|
||||
});
|
||||
test.after(() => teardownDom());
|
||||
|
||||
/** A server holding two projects, as the backend does: whatever was PUT comes back on the next GET. */
|
||||
function server() {
|
||||
const store = {
|
||||
aaaaaaaaaaaa: { id: "aaaaaaaaaaaa", name: "A", scenes: [], updated_at: 1 },
|
||||
bbbbbbbbbbbb: { id: "bbbbbbbbbbbb", name: "B", scenes: [], updated_at: 1 },
|
||||
};
|
||||
globalThis.fetch = async (path, opts = {}) => {
|
||||
if (path === "/funpack/api/projects" && !opts.method) return { ok: true, status: 200, json: async () => ({ projects: Object.values(store).map((p) => ({ id: p.id })) }) };
|
||||
const id = decodeURIComponent(String(path).split("/").pop());
|
||||
if (opts.method === "PUT") { const body = JSON.parse(opts.body); store[id] = { ...body, updated_at: 2 }; return { ok: true, status: 200, json: async () => store[id] }; }
|
||||
return { ok: true, status: 200, json: async () => JSON.parse(JSON.stringify(store[id])) };
|
||||
};
|
||||
}
|
||||
|
||||
test("the wheel's settings are saved with the project and come back on a fresh browser, per project", async () => {
|
||||
server();
|
||||
localStorage.clear();
|
||||
const project = createProject({});
|
||||
await project.start(); // project A
|
||||
const app = { dom: createDom(), project, actions: [], editorSettings: [] };
|
||||
const off = hub.setup({ host: document.createElement("div"), app });
|
||||
try {
|
||||
// change it through the real settings panel
|
||||
const panel = app.editorSettings[0]();
|
||||
document.body.append(panel);
|
||||
const half = [...panel.querySelectorAll("button,[role=radio]")].find((b) => b.textContent.trim() === "Half wheel");
|
||||
const left = [...panel.querySelectorAll("button,[role=radio]")].find((b) => b.textContent.trim() === "Left");
|
||||
half.click(); left.click();
|
||||
await project.flush();
|
||||
assert.deepEqual(readPrefs(app), { shape: "half", side: "left", enabled: null });
|
||||
|
||||
// a rental with an empty browser: same project, no localStorage
|
||||
localStorage.clear();
|
||||
const fresh = createProject({});
|
||||
await fresh.start();
|
||||
assert.deepEqual(readPrefs({ project: fresh }), { shape: "half", side: "left", enabled: null }, "the choice did not travel with the project");
|
||||
|
||||
// another project keeps its own choice
|
||||
await fresh.open("bbbbbbbbbbbb");
|
||||
assert.deepEqual(readPrefs({ project: fresh }), { shape: "full", side: "right", enabled: null });
|
||||
} finally { off(); }
|
||||
});
|
||||
@@ -2,13 +2,18 @@
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { offer } from "../../shell/actions.js";
|
||||
|
||||
// Per browser, not per project: the wheel is a habit of the hand. enabled null = everything offered except the opt-in actions.
|
||||
// Saved with the project (a new rental has an empty browser); the browser copy is only the starting point for a project that has no choice yet. enabled null = everything offered except the opt-in actions.
|
||||
const KEY = "funpack_wheel";
|
||||
export function readPrefs() {
|
||||
try { const p = JSON.parse(localStorage.getItem(KEY)) || {}; return { shape: p.shape === "half" ? "half" : "full", side: p.side === "left" ? "left" : "right", enabled: Array.isArray(p.enabled) ? p.enabled : null }; }
|
||||
catch { return { shape: "full", side: "right", enabled: null }; }
|
||||
const clean = (p) => ({ shape: p.shape === "half" ? "half" : "full", side: p.side === "left" ? "left" : "right", enabled: Array.isArray(p.enabled) ? p.enabled : null });
|
||||
const fromBrowser = () => { try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch { return {}; } };
|
||||
export function readPrefs(app) {
|
||||
const saved = app && app.project && app.project.pref ? app.project.pref("wheel", null) : null;
|
||||
return clean(saved && typeof saved === "object" ? saved : fromBrowser());
|
||||
}
|
||||
function writePrefs(app, p) {
|
||||
if (app.project && app.project.setPref) app.project.setPref("wheel", p);
|
||||
try { localStorage.setItem(KEY, JSON.stringify(p)); } catch { /* private window: lasts until reload */ }
|
||||
}
|
||||
const writePrefs = (p) => { try { localStorage.setItem(KEY, JSON.stringify(p)); } catch { /* private window: lasts until reload */ } };
|
||||
|
||||
export default {
|
||||
id: "wheel",
|
||||
@@ -35,7 +40,7 @@ export default {
|
||||
const onDown = (e) => {
|
||||
if (e.button !== 1 || wheel || app.dom.dialogOpen()) return; // nothing behind a dialog
|
||||
e.preventDefault();
|
||||
const prefs = readPrefs(), on = (a) => prefs.enabled ? prefs.enabled.includes(a.id) : !a.optIn;
|
||||
const prefs = readPrefs(app), on = (a) => prefs.enabled ? prefs.enabled.includes(a.id) : !a.optIn;
|
||||
const items = (app.actions || []).filter((a) => on(a) && (!a.when || a.when())).slice(0, 12).map((a) => ({ id: a.id, label: a.label, icon: a.icon }));
|
||||
if (!items.length) return;
|
||||
const shared = { items, onClose: () => { wheel = null; },
|
||||
@@ -43,7 +48,7 @@ export default {
|
||||
wheel = prefs.shape === "half" ? c.wheel.half({ ...shared, edge: prefs.side, at: e.clientY }) : c.wheel.picker({ ...shared, x: e.clientX, y: e.clientY });
|
||||
};
|
||||
app.editorSettings.push(() => {
|
||||
const prefs = readPrefs(), set = (patch) => writePrefs({ ...readPrefs(), ...patch });
|
||||
const prefs = readPrefs(app), set = (patch) => writePrefs(app, { ...readPrefs(app), ...patch });
|
||||
const offered = (app.actions || []).map((a) => ({ value: a.id, label: `${a.icon || ""} ${a.label}`.trim() }));
|
||||
return c.region.stack({ gap: "sm", children: [c.label.section({ text: "Picker wheel" }),
|
||||
c.hint.default({ text: "Hold the middle mouse button anywhere, flick toward an action and let go." }),
|
||||
|
||||
Reference in New Issue
Block a user