163 lines
4.3 KiB
JavaScript
163 lines
4.3 KiB
JavaScript
// == 配置 ==
|
|
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 [];
|
|
}
|