feat(widgets): add clock, daily summary, and quick actions

This commit is contained in:
Codex committed 2026-09-28 11:42:43 +09:00
1 parent 72c4cfd0c2
commit 47fa2896ba
6 files changed
+115 -4

No files matched your search

+41
View File
@@ -227,6 +227,47 @@ assert.equal(layoutWindows.tasks.style.top, "137px");
assert.equal(layoutWindows.tasks.classList.contains("min"), true);
layoutContext.closeWin("tasks");
assert.deepEqual(JSON.parse(layoutStorage.getItem("lifeos_window_layout_v1")).entries.map(entry => entry.id), ["calendar"], "closed windows stay closed on the next launch");
const widgetStorage = makeStorage({ lifeos_settings: "{}" });
widgetStorage.removeItem = key => widgetStorage.values.delete(key);
class WidgetNode {
constructor() { this.style = {}; this.dataset = {}; this.children = []; this.listeners = {}; this.flags = new Set(); this.classList = { add: key => this.flags.add(key), remove: key => this.flags.delete(key), contains: key => this.flags.has(key), toggle: (key, enabled) => enabled ? this.flags.add(key) : this.flags.delete(key) }; }
addEventListener(type, callback) { (this.listeners[type] ||= []).push(callback); }
removeEventListener(type, callback) { this.listeners[type] = (this.listeners[type] || []).filter(fn => fn !== callback); }
emit(type, event) { (this.listeners[type] || []).forEach(fn => fn(event)); }
append(node) { this.children.push(node); }
setAttribute() {}
setPointerCapture() {}
querySelector(selector) { if (selector === '.widget-empty-message') return null; return this[selector] ||= new WidgetNode(); }
insertAdjacentHTML() {}
}
const widgetDesktop = new WidgetNode(), widgetGoals = new WidgetNode();
const widgetContext = vm.createContext({
document: { querySelector: selector => selector === '#desktop' ? widgetDesktop : widgetGoals, createElement: () => new WidgetNode(), head: { append() {} } },
localStorage: widgetStorage, settings: () => JSON.parse(widgetStorage.getItem('lifeos_settings')),
save: (key, value) => widgetStorage.setItem(key, JSON.stringify(value)),
innerWidth: 1200, innerHeight: 800, addEventListener() {}, setInterval() {}, setTimeout() {}, clearTimeout() {},
renderDesktopGoalCountdowns() {}, renderAll() {}, systemApp() {}, openApp() {},
mems: () => [], sys: () => ({ tasksBoard: { tasks: [] } }), day: () => '2026-09-28',
LifeI18n: { t: key => key, locale: () => 'en-US', date: () => 'Sep 28', number: value => String(value) }
});
widgetContext.window = widgetContext;
vm.runInContext(assets.desktopWidgetsJs, widgetContext);
const widgetLayer = widgetDesktop.children[0], goalWidget = widgetLayer.children[0];
const goalHandle = goalWidget.querySelector('.widget-drag');
goalWidget.emit('pointerdown', { button: 0, target: { closest: selector => selector === '.widget-drag' ? goalHandle : null }, clientX: 0, clientY: 0, preventDefault() {} });
goalHandle.emit('pointermove', { clientX: -110, clientY: 98 });
goalHandle.emit('pointerup', {});
let savedWidget = JSON.parse(widgetStorage.getItem('lifeos_widget_layout_v1')).goals;
assert.equal(savedWidget.row, 1, "goal countdown moves by one visible grid cell");
const resizeHandle = goalWidget.querySelector('.widget-resize');
goalWidget.emit('pointerdown', { button: 0, target: { closest: selector => selector === '.widget-resize' ? resizeHandle : null }, clientX: 0, clientY: 0, preventDefault() {} });
resizeHandle.emit('pointermove', { clientX: -110, clientY: -98 });
resizeHandle.emit('pointerup', {});
savedWidget = JSON.parse(widgetStorage.getItem('lifeos_widget_layout_v1')).goals;
assert.equal(savedWidget.w, 2, "goal countdown width can be reduced");
assert.equal(savedWidget.h, 2, "goal countdown height can be reduced");
widgetContext.toggleDesktopWidget('actions');
assert.equal(JSON.parse(widgetStorage.getItem('lifeos_settings')).desktopWidgets.includes('actions'), true);
const zhLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "zh-CN,zh;q=0.9" } }), {});
assert.match(await zhLogin.text(), /登录 ChatGPT/);
const enLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "en-US,en;q=0.9" } }), {});