chore: Refactor app.py and widget.js
This commit is contained in:
parent
8da5a41c6f
commit
ef7472011d
2
app.py
2
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)
|
||||
|
||||
162
widget.js
Normal file
162
widget.js
Normal file
@ -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 [];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user