From ef7472011d73906471c6e1644ec32df495f7db76 Mon Sep 17 00:00:00 2001 From: Ching L Date: Tue, 6 May 2025 09:52:24 +0800 Subject: [PATCH] chore: Refactor app.py and widget.js --- app.py | 2 +- widget.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 widget.js diff --git a/app.py b/app.py index 40d8505..b54b449 100644 --- a/app.py +++ b/app.py @@ -90,7 +90,7 @@ def require_api_key(func): def _to_datetime(value): """Ensure value is a timezone‑aware datetime.""" - if value is None: + if not value: return None if isinstance(value, dt.datetime): return value if value.tzinfo else value.replace(tzinfo=TZ) diff --git a/widget.js b/widget.js new file mode 100644 index 0000000..b8f2e6b --- /dev/null +++ b/widget.js @@ -0,0 +1,162 @@ +// == 配置 == +const API_KEY = "change-me"; +const API_HOST = "https://calendar-widget-api.tunpok.com"; +const GET_URL = `${API_HOST}/tasks`; +const CACHE_FN = "calendar-widget-task-cache.json"; // 本地缓存文件 + +// == 处理「点击 cell」== +if (args.queryParameters?.action === "schedule" && args.queryParameters?.id) { + await scheduleTask(args.queryParameters.id); + // 小提示:让 Scriptable 返回桌面并刷新 widget + Script.complete(); + return; +} + +// == 入口:拉取数据 & 渲染 widget == +const data = await safeFetchTasks(); // 若接口失败则自动读取缓存 +const widget = buildWidget(data); +Script.setWidget(widget); +Script.complete(); + +// == 每分钟自动刷新 == +widget.refreshAfterDate = new Date(Date.now() + 60 * 1000); + +// ----------------------------------------------------------------------------- +// ⬇️ 工具函数 ⬇️ +// ----------------------------------------------------------------------------- + +/** + * 从接口安全拉取任务;失败时返回缓存内容 + */ +async function safeFetchTasks() { + try { + const req = new Request(GET_URL); + req.method = "GET"; + req.headers = { "X-Api-Key": API_KEY }; + const json = await req.loadJSON(); + cacheJson(json); // 拉取成功即写入缓存 + return json; + } catch (e) { + return readCache(); + } +} + +/** + * 点击 cell 后调度任务 + */ +async function scheduleTask(id) { + try { + const url = `${API_HOST}/tasks/${id}/schedule`; + const req = new Request(url); + req.method = "POST"; + req.headers = { "X-Api-Key": API_KEY }; + await req.load(); // 无需关心返回值 + } catch (e) { + // 失败时静默忽略,保持 widget 不变 + } +} + +/** + * 构建并返回 ListWidget + */ +function buildWidget(tasks) { + // == Widget 初始化 == + const w = new ListWidget(); + w.backgroundColor = new Color("#f5f5f9"); + w.setPadding(10, 10, 10, 10); + + const main = w.addStack(); + main.layoutVertically(); + main.centerAlignContent(); + main.spacing = 12; + + // == 布局判定 == + let columns = 4, rows = 1; + switch (config.widgetFamily) { + case "medium": columns = 2; rows = 2; break; + case "large": columns = 4; rows = 2; break; + } + + let idx = 0; + const total = tasks.length; + + for (let r = 0; r < rows; r++) { + const row = main.addStack(); + row.layoutHorizontally(); + row.centerAlignContent(); + row.spacing = 10; + + for (let c = 0; c < columns; c++) { + if (idx >= total) break; + const item = tasks[idx++]; + + const cell = row.addStack(); + cell.layoutVertically(); + cell.backgroundColor = new Color("#f2f2f2"); + cell.cornerRadius = 16; + cell.setPadding(10, 25, 10, 25); + cell.centerAlignContent(); + cell.size = new Size(0, 65); + + // ===== 标题 ===== + const title = cell.addText(item.name); + title.font = Font.mediumSystemFont(14); + title.textColor = Color.black(); + title.centerAlignText(); + + cell.addSpacer(8); + + // ===== 色条 ===== + const bar = cell.addStack(); + bar.backgroundColor = colorMap(item.color); + bar.cornerRadius = 8; + bar.size = new Size(Device.screenSize().width / (columns * 1.8), 10); + + // ===== 点击跳转 ===== + const url = `scriptable:///run?scriptName=${encodeURIComponent(Script.name())}` + + `&action=schedule&id=${item.id}`; + cell.url = url; // 整个 cell 可点击 + } + } + return w; +} + +/** + * 根据接口给出的 color 字段返回 Scriptable 的颜色对象 + */ +function colorMap(color) { + switch ((color || "").toLowerCase()) { + case "green": return Color.green(); + case "yellow": return Color.yellow(); + case "red": return Color.red(); + default: return Color.gray(); + } +} + +/** + * 写缓存 + */ +function cacheJson(obj) { + try { + const fm = FileManager.local(); + const dir = fm.documentsDirectory(); + const path = fm.joinPath(dir, CACHE_FN); + fm.writeString(path, JSON.stringify(obj)); + } catch (_) {} +} + +/** + * 读缓存,若无缓存则返回空数组 + */ +function readCache() { + try { + const fm = FileManager.local(); + const dir = fm.documentsDirectory(); + const path = fm.joinPath(dir, CACHE_FN); + if (fm.fileExists(path)) { + const str = fm.readString(path); + return JSON.parse(str); + } + } catch (_) {} + return []; +}