-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathCalendar.scriptable
12 lines (11 loc) · 58.2 KB
/
Calendar.scriptable
1
2
3
4
5
6
7
8
9
10
11
12
{
"always_run_in_app" : false,
"icon" : {
"color" : "orange",
"glyph" : "calendar-alt"
},
"name" : "Calendar",
"script" : "\/**\n * @version 1.4.4\n * @author Honye\n *\/\n\n\/**\n * Thanks @mzeryck\n *\n * @param {number} [height] The screen height measured in pixels\n *\/\nconst phoneSize = (height) => {\n const phones = {\n \/** 14 Pro Max *\/\n 2796: {\n small: 510,\n medium: 1092,\n large: 1146,\n left: 99,\n right: 681,\n top: 282,\n middle: 918,\n bottom: 1554\n },\n \/** 14 Pro *\/\n 2556: {\n small: 474,\n medium: 1014,\n large: 1062,\n left: 82,\n right: 622,\n top: 270,\n middle: 858,\n bottom: 1446\n },\n \/** 13 Pro Max, 12 Pro Max *\/\n 2778: {\n small: 510,\n medium: 1092,\n large: 1146,\n left: 96,\n right: 678,\n top: 246,\n middle: 882,\n bottom: 1518\n },\n \/** 13, 13 Pro, 12, 12 Pro *\/\n 2532: {\n small: 474,\n medium: 1014,\n large: 1062,\n left: 78,\n right: 618,\n top: 231,\n middle: 819,\n bottom: 1407\n },\n \/** 11 Pro Max, XS Max *\/\n 2688: {\n small: 507,\n medium: 1080,\n large: 1137,\n left: 81,\n right: 654,\n top: 228,\n middle: 858,\n bottom: 1488\n },\n \/** 11, XR *\/\n 1792: {\n small: 338,\n medium: 720,\n large: 758,\n left: 55,\n right: 437,\n top: 159,\n middle: 579,\n bottom: 999\n },\n \/** 13 mini, 12 mini \/ 11 Pro, XS, X *\/\n 2436: {\n small: 465,\n medium: 987,\n large: 1035,\n x: {\n left: 69,\n right: 591,\n top: 213,\n middle: 783,\n bottom: 1353\n },\n mini: {\n left: 69,\n right: 591,\n top: 231,\n middle: 801,\n bottom: 1371\n }\n },\n \/** Plus phones *\/\n 2208: {\n small: 471,\n medium: 1044,\n large: 1071,\n left: 99,\n right: 672,\n top: 114,\n middle: 696,\n bottom: 1278\n },\n \/** SE2 and 6\/6S\/7\/8 *\/\n 1334: {\n small: 296,\n medium: 642,\n large: 648,\n left: 54,\n right: 400,\n top: 60,\n middle: 412,\n bottom: 764\n },\n \/** SE1 *\/\n 1136: {\n small: 282,\n medium: 584,\n large: 622,\n left: 30,\n right: 332,\n top: 59,\n middle: 399,\n bottom: 399\n },\n \/** 11 and XR in Display Zoom mode *\/\n 1624: {\n small: 310,\n medium: 658,\n large: 690,\n left: 46,\n right: 394,\n top: 142,\n middle: 522,\n bottom: 902\n },\n \/** Plus in Display Zoom mode *\/\n 2001: {\n small: 444,\n medium: 963,\n large: 972,\n left: 81,\n right: 600,\n top: 90,\n middle: 618,\n bottom: 1146\n }\n };\n height = height || Device.screenResolution().height;\n const scale = Device.screenScale();\n\n const phone = phones[height];\n if (phone) {\n return phone\n }\n\n if (config.runsInWidget) {\n const pc = {\n small: 164 * scale,\n medium: 344 * scale,\n large: 354 * scale\n };\n return pc\n }\n\n \/\/ in app screen fixed 375x812 pt\n return {\n small: 155 * scale,\n medium: 329 * scale,\n large: 345 * scale\n }\n};\n\n\/**\n * 多语言国际化\n * @param {{[language: string]: string} | [en:string, zh:string]} langs\n *\/\nconst i18n = (langs) => {\n const language = Device.language();\n if (Array.isArray(langs)) {\n langs = {\n en: langs[0],\n zh: langs[1],\n others: langs[0]\n };\n } else {\n langs.others = langs.others || langs.en;\n }\n return langs[language] || langs.others\n};\n\n\/**\n * 是否同一天\n * @param {string|number|Date} a\n * @param {string|number|Date} b\n *\/\nconst isSameDay = (a, b) => {\n const leftDate = new Date(a);\n leftDate.setHours(0);\n const rightDate = new Date(b);\n rightDate.setHours(0);\n return Math.abs(leftDate - rightDate) < 3600000\n};\n\n\/**\n * 是否是今天\n * @param {string|number|Date} date\n *\/\nconst isToday = (date) => isSameDay(new Date(), date);\n\n\/**\n * 图标换色\n * @param {Image} image\n * @param {Color} color\n *\/\nconst tintedImage = async (image, color) => {\n const html =\n `<img id=\"image\" src=\"data:image\/png;base64,${Data.fromPNG(image).toBase64String()}\" \/>\n <canvas id=\"canvas\"><\/canvas>`;\n const js =\n `let img = document.getElementById(\"image\");\n let canvas = document.getElementById(\"canvas\");\n let color = 0x${color.hex};\n\n canvas.width = img.width;\n canvas.height = img.height;\n let ctx = canvas.getContext(\"2d\");\n ctx.drawImage(img, 0, 0);\n let imgData = ctx.getImageData(0, 0, img.width, img.height);\n \/\/ ordered in RGBA format\n let data = imgData.data;\n for (let i = 0; i < data.length; i++) {\n \/\/ skip alpha channel\n if (i % 4 === 3) continue;\n \/\/ bit shift the color value to get the correct channel\n data[i] = (color >> (2 - i % 4) * 8) & 0xFF\n }\n ctx.putImageData(imgData, 0, 0);\n canvas.toDataURL(\"image\/png\").replace(\/^data:image\\\\\/png;base64,\/, \"\");`;\n const wv = new WebView();\n await wv.loadHTML(html);\n const base64 = await wv.evaluateJavaScript(js);\n return Image.fromData(Data.fromBase64String(base64))\n};\n\n\/**\n * @param {...string} paths\n *\/\nconst joinPath = (...paths) => {\n const fm = FileManager.local();\n return paths.reduce((prev, curr) => {\n return fm.joinPath(prev, curr)\n }, '')\n};\n\n\/**\n * 规范使用 FileManager。每个脚本使用独立文件夹\n *\n * 注意:桌面组件无法写入 cacheDirectory 和 temporaryDirectory\n * @param {object} options\n * @param {boolean} [options.useICloud]\n * @param {string} [options.basePath]\n *\/\nconst useFileManager = (options = {}) => {\n const { useICloud, basePath } = options;\n const fm = useICloud ? FileManager.iCloud() : FileManager.local();\n const paths = [fm.documentsDirectory(), Script.name()];\n if (basePath) {\n paths.push(basePath);\n }\n const cacheDirectory = joinPath(...paths);\n \/**\n * 删除路径末尾所有的 \/\n * @param {string} filePath\n *\/\n const safePath = (filePath) => {\n return fm.joinPath(cacheDirectory, filePath).replace(\/\\\/+$\/, '')\n };\n \/**\n * 如果上级文件夹不存在,则先创建文件夹\n * @param {string} filePath\n *\/\n const preWrite = (filePath) => {\n const i = filePath.lastIndexOf('\/');\n const directory = filePath.substring(0, i);\n if (!fm.fileExists(directory)) {\n fm.createDirectory(directory, true);\n }\n };\n\n const writeString = (filePath, content) => {\n const nextPath = safePath(filePath);\n preWrite(nextPath);\n fm.writeString(nextPath, content);\n };\n\n \/**\n * @param {string} filePath\n * @param {*} jsonData\n *\/\n const writeJSON = (filePath, jsonData) => writeString(filePath, JSON.stringify(jsonData));\n \/**\n * @param {string} filePath\n * @param {Image} image\n *\/\n const writeImage = (filePath, image) => {\n const nextPath = safePath(filePath);\n preWrite(nextPath);\n return fm.writeImage(nextPath, image)\n };\n\n \/**\n * 文件不存在时返回 null\n * @param {string} filePath\n * @returns {string|null}\n *\/\n const readString = (filePath) => {\n const fullPath = fm.joinPath(cacheDirectory, filePath);\n if (fm.fileExists(fullPath)) {\n return fm.readString(\n fm.joinPath(cacheDirectory, filePath)\n )\n }\n return null\n };\n\n \/**\n * @param {string} filePath\n *\/\n const readJSON = (filePath) => JSON.parse(readString(filePath));\n\n \/**\n * @param {string} filePath\n *\/\n const readImage = (filePath) => {\n return fm.readImage(fm.joinPath(cacheDirectory, filePath))\n };\n\n return {\n cacheDirectory,\n writeString,\n writeJSON,\n writeImage,\n readString,\n readJSON,\n readImage\n }\n};\n\n\/** 规范使用文件缓存。每个脚本使用独立文件夹 *\/\nconst useCache = () => useFileManager({ basePath: 'cache' });\n\n\/**\n * @param {WidgetStack} stack\n * @param {object} options\n * @param {number} [options.column] column count\n * @param {number | [number, number]} [options.gap]\n * @param {'row' | 'column'} [options.direction]\n *\/\nconst useGrid = async (stack, options) => {\n const {\n column,\n gap = 0,\n direction = 'row'\n } = options;\n const [columnGap, rowGap] = typeof gap === 'number' ? [gap, gap] : gap;\n\n if (direction === 'row') {\n stack.layoutVertically();\n } else {\n stack.layoutHorizontally();\n }\n\n let i = -1;\n const rows = [];\n\n const add = async (fn) => {\n i++;\n const r = Math.floor(i \/ column);\n if (i % column === 0) {\n if (r > 0) {\n stack.addSpacer(rowGap);\n }\n const rowStack = stack.addStack();\n if (direction === 'row') {\n rowStack.layoutHorizontally();\n } else {\n rowStack.layoutVertically();\n }\n rows.push(rowStack);\n }\n\n if (i % column > 0) {\n rows[r].addSpacer(columnGap);\n }\n await fn(rows[r]);\n };\n\n return { add }\n};\n\n\/\/ Variables used by Scriptable.\n\/\/ These must be at the very top of the file. Do not edit.\n\/\/ icon-color: light-gray; icon-glyph: cube;\n\/* 公历转农历代码思路:\n1、建立农历年份查询表\n2、计算输入公历日期与公历基准的相差天数\n3、从农历基准开始遍历农历查询表,计算自农历基准之后每一年的天数,并用相差天数依次相减,确定农历年份\n4、利用剩余相差天数以及农历每个月的天数确定农历月份\n5、利用剩余相差天数确定农历哪一天 *\/\n\n\/\/ 农历1949-2100年查询表\nconst lunarYearArr = [\n 0x0b557, \/\/ 1949\n 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, \/\/ 1950-1959\n 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, \/\/ 1960-1969\n 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, \/\/ 1970-1979\n 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, \/\/ 1980-1989\n 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, \/\/ 1990-1999\n 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, \/\/ 2000-2009\n 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, \/\/ 2010-2019\n 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, \/\/ 2020-2029\n 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, \/\/ 2030-2039\n 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, \/\/ 2040-2049\n 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, \/\/ 2050-2059\n 0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, \/\/ 2060-2069\n 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, \/\/ 2070-2079\n 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, \/\/ 2080-2089\n 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, \/\/ 2090-2099\n 0x0d520 \/\/ 2100\n];\nconst lunarMonth = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];\nconst lunarDay = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '初', '廿'];\nconst tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];\nconst diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];\n\n\/\/ 公历转农历函数\nfunction sloarToLunar (sy, sm, sd) {\n \/\/ 输入的月份减1处理\n sm -= 1;\n\n \/\/ 计算与公历基准的相差天数\n \/\/ Date.UTC()返回的是距离公历1970年1月1日的毫秒数,传入的月份需要减1\n let daySpan = (Date.UTC(sy, sm, sd) - Date.UTC(1949, 0, 29)) \/ (24 * 60 * 60 * 1000) + 1;\n let ly, lm, ld;\n \/\/ 确定输出的农历年份\n for (let j = 0; j < lunarYearArr.length; j++) {\n daySpan -= lunarYearDays(lunarYearArr[j]);\n if (daySpan <= 0) {\n ly = 1949 + j;\n \/\/ 获取农历年份确定后的剩余天数\n daySpan += lunarYearDays(lunarYearArr[j]);\n break\n }\n }\n\n \/\/ 确定输出的农历月份\n for (let k = 0; k < lunarYearMonths(lunarYearArr[ly - 1949]).length; k++) {\n daySpan -= lunarYearMonths(lunarYearArr[ly - 1949])[k];\n if (daySpan <= 0) {\n \/\/ 有闰月时,月份的数组长度会变成13,因此,当闰月月份小于等于k时,lm不需要加1\n if (hasLeapMonth(lunarYearArr[ly - 1949]) && hasLeapMonth(lunarYearArr[ly - 1949]) <= k) {\n if (hasLeapMonth(lunarYearArr[ly - 1949]) < k) {\n lm = k;\n } else if (hasLeapMonth(lunarYearArr[ly - 1949]) === k) {\n lm = '闰' + k;\n } else {\n lm = k + 1;\n }\n } else {\n lm = k + 1;\n }\n \/\/ 获取农历月份确定后的剩余天数\n daySpan += lunarYearMonths(lunarYearArr[ly - 1949])[k];\n break\n }\n }\n\n \/\/ 确定输出农历哪一天\n ld = daySpan;\n\n \/\/ 将计算出来的农历月份转换成汉字月份,闰月需要在前面加上闰字\n if (hasLeapMonth(lunarYearArr[ly - 1949]) && (typeof (lm) === 'string' && lm.indexOf('闰') > -1)) {\n lm = `闰${lunarMonth[\/\\d\/.exec(lm) - 1]}`;\n } else {\n lm = lunarMonth[lm - 1];\n }\n\n \/\/ 将计算出来的农历年份转换为天干地支年\n ly = getTianGan(ly) + getDiZhi(ly);\n\n \/\/ 将计算出来的农历天数转换成汉字\n if (ld < 11) {\n ld = `${lunarDay[10]}${lunarDay[ld - 1]}`;\n } else if (ld > 10 && ld < 20) {\n ld = `${lunarDay[9]}${lunarDay[ld - 11]}`;\n } else if (ld === 20) {\n ld = `${lunarDay[1]}${lunarDay[9]}`;\n } else if (ld > 20 && ld < 30) {\n ld = `${lunarDay[11]}${lunarDay[ld - 21]}`;\n } else if (ld === 30) {\n ld = `${lunarDay[2]}${lunarDay[9]}`;\n }\n\n \/\/ console.log(ly, lm, ld);\n\n return {\n lunarYear: ly,\n lunarMonth: lm,\n lunarDay: ld\n }\n}\n\n\/\/ 计算农历年是否有闰月,参数为存储农历年的16进制\n\/\/ 农历年份信息用16进制存储,其中16进制的最后1位可以用于判断是否有闰月\nfunction hasLeapMonth (ly) {\n \/\/ 获取16进制的最后1位,需要用到&与运算符\n if (ly & 0xf) {\n return ly & 0xf\n } else {\n return false\n }\n}\n\n\/\/ 如果有闰月,计算农历闰月天数,参数为存储农历年的16进制\n\/\/ 农历年份信息用16进制存储,其中16进制的第1位(0x除外)可以用于表示闰月是大月还是小月\nfunction leapMonthDays (ly) {\n if (hasLeapMonth(ly)) {\n \/\/ 获取16进制的第1位(0x除外)\n return (ly & 0xf0000) ? 30 : 29\n } else {\n return 0\n }\n}\n\n\/\/ 计算农历一年的总天数,参数为存储农历年的16进制\n\/\/ 农历年份信息用16进制存储,其中16进制的第2-4位(0x除外)可以用于表示正常月是大月还是小月\nfunction lunarYearDays (ly) {\n let totalDays = 0;\n\n \/\/ 获取正常月的天数,并累加\n \/\/ 获取16进制的第2-4位,需要用到>>移位运算符\n for (let i = 0x8000; i > 0x8; i >>= 1) {\n const monthDays = (ly & i) ? 30 : 29;\n totalDays += monthDays;\n }\n \/\/ 如果有闰月,需要把闰月的天数加上\n if (hasLeapMonth(ly)) {\n totalDays += leapMonthDays(ly);\n }\n\n return totalDays\n}\n\n\/\/ 获取农历每个月的天数\n\/\/ 参数需传入16进制数值\nfunction lunarYearMonths (ly) {\n const monthArr = [];\n\n \/\/ 获取正常月的天数,并添加到monthArr数组中\n \/\/ 获取16进制的第2-4位,需要用到>>移位运算符\n for (let i = 0x8000; i > 0x8; i >>= 1) {\n monthArr.push((ly & i) ? 30 : 29);\n }\n \/\/ 如果有闰月,需要把闰月的天数加上\n if (hasLeapMonth(ly)) {\n monthArr.splice(hasLeapMonth(ly), 0, leapMonthDays(ly));\n }\n\n return monthArr\n}\n\n\/\/ 将农历年转换为天干,参数为农历年\nfunction getTianGan (ly) {\n let tianGanKey = (ly - 3) % 10;\n if (tianGanKey === 0) tianGanKey = 10;\n return tianGan[tianGanKey - 1]\n}\n\n\/\/ 将农历年转换为地支,参数为农历年\nfunction getDiZhi (ly) {\n let diZhiKey = (ly - 3) % 12;\n if (diZhiKey === 0) diZhiKey = 12;\n return diZhi[diZhiKey - 1]\n}\n\n\/**\n * @file Scriptable WebView JSBridge native SDK\n * @version 1.0.2\n * @author Honye\n *\/\n\n\/**\n * @typedef Options\n * @property {Record<string, () => void>} methods\n *\/\n\nconst sendResult = (() => {\n let sending = false;\n \/** @type {{ code: string; data: any }[]} *\/\n const list = [];\n\n \/**\n * @param {WebView} webView\n * @param {string} code\n * @param {any} data\n *\/\n return async (webView, code, data) => {\n if (sending) return\n\n sending = true;\n list.push({ code, data });\n const arr = list.splice(0, list.length);\n for (const { code, data } of arr) {\n const eventName = `ScriptableBridge_${code}_Result`;\n const res = data instanceof Error ? { err: data.message } : data;\n await webView.evaluateJavaScript(\n `window.dispatchEvent(\n new CustomEvent(\n '${eventName}',\n { detail: ${JSON.stringify(res)} }\n )\n )`\n );\n }\n if (list.length) {\n const { code, data } = list.shift();\n sendResult(webView, code, data);\n } else {\n sending = false;\n }\n }\n})();\n\n\/**\n * @param {WebView} webView\n * @param {Options} options\n *\/\nconst inject = async (webView, options) => {\n const js =\n`(() => {\n const queue = window.__scriptable_bridge_queue\n if (queue && queue.length) {\n completion(queue)\n }\n window.__scriptable_bridge_queue = null\n\n if (!window.ScriptableBridge) {\n window.ScriptableBridge = {\n invoke(name, data, callback) {\n const detail = { code: name, data }\n\n const eventName = \\`ScriptableBridge_\\${name}_Result\\`\n const controller = new AbortController()\n window.addEventListener(\n eventName,\n (e) => {\n callback && callback(e.detail)\n controller.abort()\n },\n { signal: controller.signal }\n )\n\n if (window.__scriptable_bridge_queue) {\n window.__scriptable_bridge_queue.push(detail)\n completion()\n } else {\n completion(detail)\n window.__scriptable_bridge_queue = []\n }\n }\n }\n window.dispatchEvent(\n new CustomEvent('ScriptableBridgeReady')\n )\n }\n})()`;\n\n const res = await webView.evaluateJavaScript(js, true);\n if (!res) return inject(webView, options)\n\n const methods = options.methods || {};\n const events = Array.isArray(res) ? res : [res];\n \/\/ 同时执行多次 webView.evaluateJavaScript Scriptable 存在问题\n \/\/ 可能是因为 JavaScript 是单线程导致的\n const sendTasks = events.map(({ code, data }) => {\n return (() => {\n try {\n return Promise.resolve(methods[code](data))\n } catch (e) {\n return Promise.reject(e)\n }\n })()\n .then((res) => sendResult(webView, code, res))\n .catch((e) => sendResult(webView, code, e instanceof Error ? e : new Error(e)))\n });\n await Promise.all(sendTasks);\n inject(webView, options);\n};\n\n\/**\n * @param {WebView} webView\n * @param {object} args\n * @param {string} args.html\n * @param {string} [args.baseURL]\n * @param {Options} options\n *\/\nconst loadHTML = async (webView, args, options = {}) => {\n const { html, baseURL } = args;\n await webView.loadHTML(html, baseURL);\n inject(webView, options).catch((err) => console.error(err));\n};\n\n\/**\n * 轻松实现桌面组件可视化配置\n *\n * - 颜色选择器及更多表单控件\n * - 快速预览\n *\n * GitHub: https:\/\/github.com\/honye\n *\n * @version 1.4.1\n * @author Honye\n *\/\n\nconst fm = FileManager.local();\nconst fileName = 'settings.json';\n\nconst toast = (message) => {\n const notification = new Notification();\n notification.title = Script.name();\n notification.body = message;\n notification.schedule();\n};\n\nconst isUseICloud = () => {\n const ifm = useFileManager({ useICloud: true });\n const filePath = fm.joinPath(ifm.cacheDirectory, fileName);\n return fm.fileExists(filePath)\n};\n\n\/** 查看配置文件可导出分享 *\/\nconst exportSettings = () => {\n const scopedFM = useFileManager({ useICloud: isUseICloud() });\n const filePath = fm.joinPath(scopedFM.cacheDirectory, fileName);\n if (fm.isFileStoredIniCloud(filePath)) {\n fm.downloadFileFromiCloud(filePath);\n }\n if (fm.fileExists(filePath)) {\n QuickLook.present(filePath);\n } else {\n const alert = new Alert();\n alert.message = i18n(['Using default configuration', '使用的默认配置,未做任何修改']);\n alert.addCancelAction(i18n(['OK', '好的']));\n alert.present();\n }\n};\n\nconst importSettings = async () => {\n const alert1 = new Alert();\n alert1.message = i18n([\n 'Will replace existing configuration',\n '会替换已有配置,确认导入吗?可将现有配置导出备份后再导入其他配置'\n ]);\n alert1.addAction(i18n(['Import', '导入']));\n alert1.addCancelAction(i18n(['Cancel', '取消']));\n const i = await alert1.present();\n if (i === -1) return\n\n const pathList = await DocumentPicker.open(['public.json']);\n for (const path of pathList) {\n const fileName = fm.fileName(path, true);\n const scopedFM = useFileManager({ useICloud: isUseICloud() });\n const destPath = fm.joinPath(scopedFM.cacheDirectory, fileName);\n if (fm.fileExists(destPath)) {\n fm.remove(destPath);\n }\n const i = destPath.lastIndexOf('\/');\n const directory = destPath.substring(0, i);\n if (!fm.fileExists(directory)) {\n fm.createDirectory(directory, true);\n }\n fm.copy(path, destPath);\n }\n const alert = new Alert();\n alert.message = i18n(['Imported success', '导入成功']);\n alert.addAction(i18n(['Restart', '重新运行']));\n await alert.present();\n const callback = new CallbackURL('scriptable:\/\/\/run');\n callback.addParameter('scriptName', Script.name());\n callback.open();\n};\n\n\/**\n * @returns {Promise<Settings>}\n *\/\nconst readSettings = async () => {\n const useICloud = isUseICloud();\n console.log(`[info] use ${useICloud ? 'iCloud' : 'local'} settings`);\n const fm = useFileManager({ useICloud });\n const settings = fm.readJSON(fileName);\n return settings\n};\n\n\/**\n * @param {Record<string, unknown>} data\n * @param {{ useICloud: boolean; }} options\n *\/\nconst writeSettings = async (data, { useICloud }) => {\n const fm = useFileManager({ useICloud });\n fm.writeJSON(fileName, data);\n};\n\nconst removeSettings = async (settings) => {\n const cache = useFileManager({ useICloud: settings.useICloud });\n fm.remove(\n fm.joinPath(cache.cacheDirectory, fileName)\n );\n};\n\nconst moveSettings = (useICloud, data) => {\n const localFM = useFileManager();\n const iCloudFM = useFileManager({ useICloud: true });\n const [i, l] = [\n fm.joinPath(iCloudFM.cacheDirectory, fileName),\n fm.joinPath(localFM.cacheDirectory, fileName)\n ];\n try {\n \/\/ 移动文件需要创建父文件夹,写入操作会自动创建文件夹\n writeSettings(data, { useICloud });\n if (useICloud) {\n if (fm.fileExists(l)) fm.remove(l);\n } else {\n if (fm.fileExists(i)) fm.remove(i);\n }\n } catch (e) {\n console.error(e);\n }\n};\n\n\/**\n * @typedef {object} NormalFormItem\n * @property {string} name\n * @property {string} label\n * @property {'text'|'number'|'color'|'select'|'date'|'cell'} [type]\n * - HTML <input> type 属性\n * - `'cell'`: 可点击的\n * @property {{ label: string; value: unknown }[]} [options]\n * @property {unknown} [default]\n *\/\n\/**\n * @typedef {Pick<NormalFormItem, 'label'|'name'> & { type: 'group', items: FormItem[] }} GroupFormItem\n *\/\n\/**\n * @typedef {Omit<NormalFormItem, 'type'> & { type: 'page' } & Pick<Options, 'formItems'|'onItemClick'>} PageFormItem 单独的页面\n *\/\n\/**\n * @typedef {NormalFormItem|GroupFormItem|PageFormItem} FormItem\n *\/\n\/**\n * @typedef {object} CommonSettings\n * @property {boolean} useICloud\n * @property {string} [backgroundImage] 背景图路径\n * @property {string} [backgroundColorLight]\n * @property {string} [backgroundColorDark]\n *\/\n\/**\n * @typedef {CommonSettings & Record<string, unknown>} Settings\n *\/\n\/**\n * @typedef {object} Options\n * @property {(data: {\n * settings: Settings;\n * family?: typeof config.widgetFamily;\n * }) => ListWidget | Promise<ListWidget>} render\n * @property {string} [head] 顶部插入 HTML\n * @property {FormItem[]} [formItems]\n * @property {(item: FormItem) => void} [onItemClick]\n * @property {string} [homePage] 右上角分享菜单地址\n * @property {(data: any) => void} [onWebEvent]\n *\/\n\/**\n * @template T\n * @typedef {T extends infer O ? {[K in keyof O]: O[K]} : never} Expand\n *\/\n\nconst previewsHTML =\n`<div class=\"actions\">\n <button class=\"preview\" data-size=\"small\"><i class=\"iconfont icon-yingyongzhongxin\"><\/i>${i18n(['Small', '预览小号'])}<\/button>\n <button class=\"preview\" data-size=\"medium\"><i class=\"iconfont icon-daliebiao\"><\/i>${i18n(['Medium', '预览中号'])}<\/button>\n <button class=\"preview\" data-size=\"large\"><i class=\"iconfont icon-dantupailie\"><\/i>${i18n(['Large', '预览大号'])}<\/button>\n<\/div>`;\n\nconst copyrightHTML =\n`<footer>\n <div class=\"copyright\">© UI powered by <a href=\"javascript:invoke('safari','https:\/\/www.imarkr.com');\">iMarkr<\/a>.<\/div>\n<\/footer>`;\n\n\/**\n * @param {Expand<Options>} options\n * @param {boolean} [isFirstPage]\n * @param {object} [others]\n * @param {Settings} [others.settings]\n * @returns {Promise<ListWidget|undefined>} 仅在 Widget 中运行时返回 ListWidget\n *\/\nconst present = async (options, isFirstPage, others = {}) => {\n const {\n formItems = [],\n onItemClick,\n render,\n head,\n homePage = 'https:\/\/www.imarkr.com',\n onWebEvent\n } = options;\n const cache = useCache();\n\n const settings = others.settings || await readSettings() || {};\n\n \/**\n * @param {Parameters<Options['render']>[0]} param\n *\/\n const getWidget = async (param) => {\n const widget = await render(param);\n const { backgroundImage, backgroundColorLight, backgroundColorDark } = settings;\n if (backgroundImage && fm.fileExists(backgroundImage)) {\n widget.backgroundImage = fm.readImage(backgroundImage);\n }\n if (!widget.backgroundColor || backgroundColorLight || backgroundColorDark) {\n widget.backgroundColor = Color.dynamic(\n new Color(backgroundColorLight || '#ffffff'),\n new Color(backgroundColorDark || '#242426')\n );\n }\n return widget\n };\n\n if (config.runsInWidget) {\n const widget = await getWidget({ settings });\n Script.setWidget(widget);\n return widget\n }\n\n \/\/ ====== web start =======\n const style =\n`:root {\n --color-primary: #007aff;\n --divider-color: rgba(60,60,67,0.36);\n --card-background: #fff;\n --card-radius: 10px;\n --list-header-color: rgba(60,60,67,0.6);\n}\n* {\n -webkit-user-select: none;\n user-select: none;\n}\nbody {\n margin: 10px 0;\n -webkit-font-smoothing: antialiased;\n font-family: \"SF Pro Display\",\"SF Pro Icons\",\"Helvetica Neue\",\"Helvetica\",\"Arial\",sans-serif;\n accent-color: var(--color-primary);\n}\ninput {\n -webkit-user-select: auto;\n user-select: auto;\n}\nbody {\n background: #f2f2f7;\n}\nbutton {\n font-size: 16px;\n background: var(--color-primary);\n color: #fff;\n border-radius: 8px;\n border: none;\n padding: 0.24em 0.5em;\n}\nbutton .iconfont {\n margin-right: 6px;\n}\n.list {\n margin: 15px;\n}\n.list__header {\n margin: 0 20px;\n color: var(--list-header-color);\n font-size: 13px;\n}\n.list__body {\n margin-top: 10px;\n background: var(--card-background);\n border-radius: var(--card-radius);\n border-radius: 12px;\n overflow: hidden;\n}\n.form-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 16px;\n min-height: 2em;\n padding: 0.5em 20px;\n position: relative;\n}\n.form-item--link .icon-arrow_right {\n color: #86868b;\n}\n.form-item + .form-item::before {\n content: \"\";\n position: absolute;\n top: 0;\n left: 20px;\n right: 0;\n border-top: 0.5px solid var(--divider-color);\n}\n.form-item .iconfont {\n margin-right: 4px;\n}\n.form-item input,\n.form-item select {\n font-size: 14px;\n text-align: right;\n}\n.form-item input[type=\"checkbox\"] {\n width: 1.25em;\n height: 1.25em;\n}\ninput[type=\"number\"] {\n width: 4em;\n}\ninput[type=\"date\"] {\n min-width: 6.4em;\n}\ninput[type='checkbox'][role='switch'] {\n position: relative;\n display: inline-block;\n appearance: none;\n width: 40px;\n height: 24px;\n border-radius: 24px;\n background: #ccc;\n transition: 0.3s ease-in-out;\n}\ninput[type='checkbox'][role='switch']::before {\n content: '';\n position: absolute;\n left: 2px;\n top: 2px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n transition: 0.3s ease-in-out;\n}\ninput[type='checkbox'][role='switch']:checked {\n background: var(--color-primary);\n}\ninput[type='checkbox'][role='switch']:checked::before {\n transform: translateX(16px);\n}\n.actions {\n margin: 15px;\n}\n.copyright {\n margin: 15px;\n margin-inline: 18px;\n font-size: 12px;\n color: #86868b;\n}\n.copyright a {\n color: #515154;\n text-decoration: none;\n}\n.preview.loading {\n pointer-events: none;\n}\n.icon-loading {\n display: inline-block;\n animation: 1s linear infinite spin;\n}\n@keyframes spin {\n 0% {\n transform: rotate(0);\n }\n 100% {\n transform: rotate(1turn);\n }\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --divider-color: rgba(84,84,88,0.65);\n --card-background: #1c1c1e;\n --list-header-color: rgba(235,235,245,0.6);\n }\n body {\n background: #000;\n color: #fff;\n }\n}\n`;\n\n const js =\n`(() => {\n const settings = ${JSON.stringify({\n ...settings,\n useICloud: isUseICloud()\n })}\n const formItems = ${JSON.stringify(formItems)}\n\n window.invoke = (code, data, cb) => {\n ScriptableBridge.invoke(code, data, cb)\n }\n\n const formData = {};\n\n const createFormItem = (item) => {\n const value = settings[item.name] ?? item.default ?? null\n formData[item.name] = value;\n const label = document.createElement(\"label\");\n label.className = \"form-item\";\n const div = document.createElement(\"div\");\n div.innerText = item.label;\n label.appendChild(div);\n if (item.type === 'select') {\n const select = document.createElement('select')\n select.className = 'form-item__input'\n select.name = item.name\n select.value = value\n for (const opt of (item.options || [])) {\n const option = document.createElement('option')\n option.value = opt.value\n option.innerText = opt.label\n option.selected = value === opt.value\n select.appendChild(option)\n }\n select.addEventListener('change', (e) => {\n formData[item.name] = e.target.value\n invoke('changeSettings', formData)\n })\n label.appendChild(select)\n } else if (\n item.type === 'cell' ||\n item.type === 'page'\n ) {\n label.classList.add('form-item--link')\n const icon = document.createElement('i')\n icon.className = 'iconfont icon-arrow_right'\n label.appendChild(icon)\n label.addEventListener('click', () => {\n const { name } = item\n switch (name) {\n case 'backgroundImage':\n invoke('chooseBgImg')\n break\n case 'clearBackgroundImage':\n invoke('clearBgImg')\n break\n case 'reset':\n reset()\n break\n default:\n invoke('itemClick', item)\n }\n })\n } else {\n const input = document.createElement(\"input\")\n input.className = 'form-item__input'\n input.name = item.name\n input.type = item.type || \"text\";\n input.enterKeyHint = 'done'\n input.value = value\n \/\/ Switch\n if (item.type === 'switch') {\n input.type = 'checkbox'\n input.role = 'switch'\n input.checked = value\n if (item.name === 'useICloud') {\n input.addEventListener('change', (e) => {\n invoke('moveSettings', e.target.checked)\n })\n }\n }\n if (item.type === 'number') {\n input.inputMode = 'decimal'\n }\n if (input.type === 'text') {\n input.size = 12\n }\n input.addEventListener(\"change\", (e) => {\n formData[item.name] =\n item.type === 'switch'\n ? e.target.checked\n : item.type === 'number'\n ? Number(e.target.value)\n : e.target.value;\n invoke('changeSettings', formData)\n });\n label.appendChild(input);\n }\n return label\n }\n\n const createList = (list, title) => {\n const fragment = document.createDocumentFragment()\n\n let elBody;\n for (const item of list) {\n if (item.type === 'group') {\n const grouped = createList(item.items, item.label)\n fragment.appendChild(grouped)\n } else {\n if (!elBody) {\n const groupDiv = fragment.appendChild(document.createElement('div'))\n groupDiv.className = 'list'\n if (title) {\n const elTitle = groupDiv.appendChild(document.createElement('div'))\n elTitle.className = 'list__header'\n elTitle.textContent = title\n }\n elBody = groupDiv.appendChild(document.createElement('div'))\n elBody.className = 'list__body'\n }\n const label = createFormItem(item)\n elBody.appendChild(label)\n }\n }\n return fragment\n }\n\n const fragment = createList(formItems)\n document.getElementById('settings').appendChild(fragment)\n\n for (const btn of document.querySelectorAll('.preview')) {\n btn.addEventListener('click', (e) => {\n const target = e.currentTarget\n target.classList.add('loading')\n const icon = e.currentTarget.querySelector('.iconfont')\n const className = icon.className\n icon.className = 'iconfont icon-loading'\n invoke(\n 'preview',\n e.currentTarget.dataset.size,\n () => {\n target.classList.remove('loading')\n icon.className = className\n }\n )\n })\n }\n\n const setFieldValue = (name, value) => {\n const input = document.querySelector(\\`.form-item__input[name=\"\\${name}\"]\\`)\n if (!input) return\n if (input.type === 'checkbox') {\n input.checked = value\n } else {\n input.value = value\n }\n }\n\n const reset = (items = formItems) => {\n for (const item of items) {\n if (item.type === 'group') {\n reset(item.items)\n } else if (item.type === 'page') {\n continue;\n } else {\n setFieldValue(item.name, item.default)\n }\n }\n invoke('removeSettings', formData)\n }\n})()`;\n\n const html =\n`<html>\n <head>\n <meta name='viewport' content='width=device-width, user-scalable=no'>\n <link rel=\"stylesheet\" href=\"\/\/at.alicdn.com\/t\/c\/font_3772663_kmo790s3yfq.css\" type=\"text\/css\">\n <style>${style}<\/style>\n <\/head>\n <body>\n ${head || ''}\n <section id=\"settings\"><\/section>\n ${isFirstPage ? (previewsHTML + copyrightHTML) : ''}\n <script>${js}<\/script>\n <\/body>\n<\/html>`;\n\n const webView = new WebView();\n const methods = {\n async preview (data) {\n const widget = await getWidget({ settings, family: data });\n widget[`present${data.replace(data[0], data[0].toUpperCase())}`]();\n },\n safari (data) {\n Safari.openInApp(data, true);\n },\n changeSettings (data) {\n Object.assign(settings, data);\n writeSettings(settings, { useICloud: settings.useICloud });\n },\n moveSettings (data) {\n settings.useICloud = data;\n moveSettings(data, settings);\n },\n removeSettings (data) {\n Object.assign(settings, data);\n clearBgImg();\n removeSettings(settings);\n },\n chooseBgImg (data) {\n chooseBgImg();\n },\n clearBgImg () {\n clearBgImg();\n },\n async itemClick (data) {\n if (data.type === 'page') {\n \/\/ `data` 经传到 HTML 后丢失了不可序列化的数据,因为需要从源数据查找\n const item = (() => {\n const find = (items) => {\n for (const el of items) {\n if (el.name === data.name) return el\n\n if (el.type === 'group') {\n const r = find(el.items);\n if (r) return r\n }\n }\n return null\n };\n return find(formItems)\n })();\n await present(item, false, { settings });\n } else {\n await onItemClick?.(data, { settings });\n }\n },\n native (data) {\n onWebEvent?.(data);\n }\n };\n await loadHTML(\n webView,\n { html, baseURL: homePage },\n { methods }\n );\n\n const clearBgImg = () => {\n const { backgroundImage } = settings;\n delete settings.backgroundImage;\n if (backgroundImage && fm.fileExists(backgroundImage)) {\n fm.remove(backgroundImage);\n }\n writeSettings(settings, { useICloud: settings.useICloud });\n toast(i18n(['Cleared success!', '背景已清除']));\n };\n\n const chooseBgImg = async () => {\n try {\n const image = await Photos.fromLibrary();\n cache.writeImage('bg.png', image);\n const imgPath = fm.joinPath(cache.cacheDirectory, 'bg.png');\n settings.backgroundImage = imgPath;\n writeSettings(settings, { useICloud: settings.useICloud });\n } catch (e) {\n console.log('[info] 用户取消选择图片');\n }\n };\n\n webView.present();\n \/\/ ======= web end =========\n};\n\n\/**\n * @param {Options} options\n *\/\nconst withSettings = async (options) => {\n const { formItems, onItemClick, ...restOptions } = options;\n return present({\n formItems: [\n {\n label: i18n(['Common', '通用']),\n type: 'group',\n items: [\n {\n label: i18n(['Sync with iCloud', 'iCloud 同步']),\n type: 'switch',\n name: 'useICloud',\n default: false\n },\n {\n label: i18n(['Background', '背景']),\n type: 'page',\n name: 'background',\n formItems: [\n {\n label: i18n(['Background', '背景']),\n type: 'group',\n items: [\n {\n name: 'backgroundColorLight',\n type: 'color',\n label: i18n(['Background color (light)', '背景色(白天)']),\n default: '#ffffff'\n },\n {\n name: 'backgroundColorDark',\n type: 'color',\n label: i18n(['Background color (dark)', '背景色(夜间)']),\n default: '#242426'\n },\n {\n label: i18n(['Background image', '背景图']),\n type: 'cell',\n name: 'backgroundImage'\n }\n ]\n },\n {\n type: 'group',\n items: [\n {\n label: i18n(['Clear background image', '清除背景图']),\n type: 'cell',\n name: 'clearBackgroundImage'\n }\n ]\n }\n ]\n },\n {\n label: i18n(['Reset', '重置']),\n type: 'cell',\n name: 'reset'\n }\n ]\n },\n {\n type: 'group',\n items: [\n {\n label: i18n(['Export settings', '导出配置']),\n type: 'cell',\n name: 'export'\n },\n {\n label: i18n(['Import settings', '导入配置']),\n type: 'cell',\n name: 'import'\n }\n ]\n },\n {\n label: i18n(['Settings', '设置']),\n type: 'group',\n items: formItems\n }\n ],\n onItemClick: (item, ...args) => {\n const { name } = item;\n if (name === 'export') {\n exportSettings();\n }\n if (name === 'import') {\n importSettings().catch((err) => {\n console.error(err);\n throw err\n });\n }\n onItemClick?.(item, ...args);\n },\n ...restOptions\n }, true)\n};\n\n\/**\n * @param {string} hex\n *\/\nconst hexToRGBA = (hex) => {\n const red = Number.parseInt(hex.substr(-6, 2), 16);\n const green = Number.parseInt(hex.substr(-4, 2), 16);\n const blue = Number.parseInt(hex.substr(-2, 2), 16);\n let alpha = 1;\n\n if (hex.length >= 8) {\n Number.parseInt(hex.substr(-8, 2), 16);\n Number.parseInt(hex.substr(-6, 2), 16);\n Number.parseInt(hex.substr(-4), 2);\n const number = Number.parseInt(hex.substr(-2, 2), 16);\n alpha = Number.parseFloat((number \/ 255).toFixed(3));\n }\n return { red, green, blue, alpha }\n};\n\nconst _RGBToHex = (r, g, b) => {\n r = r.toString(16);\n g = g.toString(16);\n b = b.toString(16);\n\n if (r.length === 1) { r = '0' + r; }\n if (g.length === 1) { g = '0' + g; }\n if (b.length === 1) { b = '0' + b; }\n\n return '#' + r + g + b\n};\n\nconst RGBToHSL = (r, g, b) => {\n r \/= 255;\n g \/= 255;\n b \/= 255;\n\n const cmin = Math.min(r, g, b);\n const cmax = Math.max(r, g, b);\n const delta = cmax - cmin;\n let h = 0;\n let s = 0;\n let l = 0;\n\n if (delta === 0) {\n h = 0;\n } else if (cmax === r) {\n h = ((g - b) \/ delta) % 6;\n } else if (cmax === g) {\n h = (b - r) \/ delta + 2;\n } else {\n h = (r - g) \/ delta + 4;\n }\n h = Math.round(h * 60);\n if (h < 0) {\n h += 360;\n }\n\n l = (cmax + cmin) \/ 2;\n s = delta === 0 ? 0 : delta \/ (1 - Math.abs(2 * l - 1));\n s = +(s * 100).toFixed(1);\n l = +(l * 100).toFixed(1);\n return { h, s, l }\n};\n\nconst _HSLToRGB = (h, s, l) => {\n \/\/ Must be fractions of 1\n s \/= 100;\n l \/= 100;\n\n const c = (1 - Math.abs(2 * l - 1)) * s;\n const x = c * (1 - Math.abs((h \/ 60) % 2 - 1));\n const m = l - c \/ 2;\n let r = 0;\n let g = 0;\n let b = 0;\n if (h >= 0 && h < 60) {\n r = c; g = x; b = 0;\n } else if (h >= 60 && h < 120) {\n r = x; g = c; b = 0;\n } else if (h >= 120 && h < 180) {\n r = 0; g = c; b = x;\n } else if (h >= 180 && h < 240) {\n r = 0; g = x; b = c;\n } else if (h >= 240 && h < 300) {\n r = x; g = 0; b = c;\n } else if (h >= 300 && h < 360) {\n r = c; g = 0; b = x;\n }\n r = Math.round((r + m) * 255);\n g = Math.round((g + m) * 255);\n b = Math.round((b + m) * 255);\n return { r, g, b }\n};\n\nconst lightenDarkenColor = (hsl, amount) => {\n const rgb = _HSLToRGB(hsl.h, hsl.s, hsl.l + amount);\n const hex = _RGBToHex(rgb.r, rgb.g, rgb.b);\n return hex\n};\n\nconst preference = {\n themeColor: '#ff0000',\n textColor: '#222222',\n textColorDark: '#ffffff',\n weekendColor: '#8e8e93',\n weekendColorDark: '#8e8e93',\n symbolName: 'flag.fill',\n eventMax: 3,\n eventFontSize: 13,\n includesReminder: false,\n eventDays: 7,\n \/** @type {'calendar_events'|'events_calendar'} *\/\n layout: 'calendar_events'\n};\nconst $12Animals = {\n 子: '鼠',\n 丑: '牛',\n 寅: '虎',\n 卯: '兔',\n 辰: '龙',\n 巳: '蛇',\n 午: '马',\n 未: '羊',\n 申: '猴',\n 酉: '鸡',\n 戌: '狗',\n 亥: '猪'\n};\nconst today = new Date();\nconst firstDay = (() => {\n const date = new Date(today);\n date.setDate(1);\n return date\n})();\nconst lastDay = (() => {\n const date = new Date(today);\n date.setMonth(date.getMonth() + 1, 0);\n return date\n})();\nlet dates = [];\nlet calendar;\nconst [calendarTitle, theme] = (args.widgetParameter || '').split(',').map((text) => text.trim());\nif (calendarTitle) {\n calendar = await Calendar.forEventsByTitle(calendarTitle);\n const events = await CalendarEvent.between(firstDay, lastDay, [calendar]);\n dates = events.map((item) => item.startDate);\n}\n\nconst titleSize = 12;\nconst columnGap = 2;\nconst rowGap = 2;\n\n\/**\n * @param {ListWidget|WidgetStack} container\n * @param {object} options\n * @param {(\n * stack: WidgetStack,\n * options: {\n * date: Date;\n * width: number;\n * addItem: (stack: WidgetStack, data: { text: string; color: Color }) => WidgetStack\n * }\n * ) => void} [options.addDay] 自定义添加日期\n *\/\nconst addCalendar = async (container, options = {}) => {\n const {\n itemWidth = 18,\n fontSize = 10,\n gap = [columnGap, rowGap],\n addWeek,\n addDay\n } = options;\n const { textColor, textColorDark, weekendColor, weekendColorDark } = preference;\n const family = config.widgetFamily;\n const stack = container.addStack();\n const { add } = await useGrid(stack, {\n column: 7,\n gap\n });\n \/**\n * @param {WidgetStack} stack\n * @param {object} param1\n * @param {string} param1.text\n * @param {Color} param1.color\n *\/\n const _addItem = (stack, { text, color } = {}) => {\n const item = stack.addStack();\n item.size = new Size(itemWidth, itemWidth);\n item.centerAlignContent();\n if (text) {\n const content = item.addStack();\n content.layoutVertically();\n const textInner = content.addText(text);\n textInner.rightAlignText();\n textInner.font = Font.semiboldSystemFont(fontSize);\n textInner.lineLimit = 1;\n textInner.minimumScaleFactor = 0.2;\n textInner.textColor = theme === 'light'\n ? new Color(textColor)\n : theme === 'dark'\n ? new Color(textColorDark)\n : Color.dynamic(new Color(textColor), new Color(textColorDark));\n if (color) {\n textInner.textColor = color;\n }\n\n item.$content = content;\n item.$text = textInner;\n }\n\n return item\n };\n const _addWeek = (stack, { day }) => {\n const sunday = new Date('1970\/01\/04');\n const weekFormat = new Intl.DateTimeFormat([], { weekday: family === 'large' ? 'short' : 'narrow' }).format;\n\n return _addItem(stack, {\n text: weekFormat(new Date(sunday.getTime() + day * 86400000)),\n color: (day === 0 || day === 6) &&\n Color.dynamic(new Color(weekendColor), new Color(weekendColorDark))\n })\n };\n const _addDay = (stack, { date }) => {\n const color = (() => {\n const week = date.getDay();\n if (isToday(date)) {\n return Color.white()\n }\n return (week === 0 || week === 6) && Color.gray()\n })();\n const item = _addItem(stack, {\n text: `${date.getDate()}`,\n color\n });\n if (isToday(date)) {\n item.cornerRadius = itemWidth \/ 2;\n item.backgroundColor = Color.red();\n }\n\n return item\n };\n for (let i = 0; i < 7; i++) {\n await add((stack) => _addWeek(stack, { day: i }));\n }\n for (let i = 0; i < firstDay.getDay(); i++) {\n await add((stack) => _addItem(stack));\n }\n for (let i = 1; i <= lastDay.getDate(); i++) {\n const date = new Date(lastDay);\n date.setDate(i);\n await add(\n async (stack) => addDay\n ? await addDay(stack, {\n date,\n width: itemWidth,\n addItem: _addItem\n })\n : _addDay(stack, { date })\n );\n }\n\n return stack\n};\n\n\/**\n * @param {ListWidget} widget\n *\/\nconst addTitle = (widget) => {\n const { themeColor } = preference;\n const family = config.widgetFamily;\n const head = widget.addStack();\n head.setPadding(0, 4, 0, 4);\n const title = head.addText(\n new Date().toLocaleString('default', {\n month: family !== 'small' ? 'long' : 'short'\n }).toUpperCase()\n );\n title.font = Font.semiboldSystemFont(11);\n title.textColor = new Color(themeColor);\n head.addSpacer();\n const lunarDate = sloarToLunar(\n today.getFullYear(),\n today.getMonth() + 1,\n today.getDate()\n );\n let lunarString = `${lunarDate.lunarMonth}月${lunarDate.lunarDay}`;\n if (family !== 'small') {\n lunarString = `${lunarDate.lunarYear}${$12Animals[lunarDate.lunarYear[1]]}年${lunarString}`;\n }\n const lunar = head.addText(lunarString);\n lunar.font = Font.semiboldSystemFont(11);\n lunar.textColor = new Color(themeColor);\n};\n\n\/**\n * @type {Parameters<typeof addCalendar>[1]['addDay']}\n *\/\nconst addDay = async (\n stack,\n { date, width, addItem } = {}\n) => {\n const { themeColor, textColor, textColorDark, weekendColor, weekendColorDark, symbolName } = preference;\n const family = config.widgetFamily;\n const text = `${date.getDate()}`;\n const i = dates.findIndex((item) => isSameDay(item, date));\n const _dateColor = theme === 'light'\n ? new Color(textColor)\n : theme === 'dark'\n ? new Color(textColorDark)\n : Color.dynamic(new Color(textColor), new Color(textColorDark));\n const _weekendColor = theme === 'light'\n ? new Color(weekendColor)\n : theme === 'dark'\n ? new Color(weekendColorDark)\n : Color.dynamic(new Color(weekendColor), new Color(weekendColorDark));\n let color = (() => {\n const week = date.getDay();\n return (week === 0 || week === 6) ? _weekendColor : _dateColor\n })();\n if (isToday(date) || i > -1) {\n color = Color.white();\n }\n const item = addItem(stack, { text, color });\n if (family === 'large') {\n const lunar = sloarToLunar(\n date.getFullYear(),\n date.getMonth() + 1,\n date.getDate()\n );\n const lunarText = item.$content.addText(\n lunar.lunarDay === '初一' ? `${lunar.lunarMonth}月` : lunar.lunarDay\n );\n lunarText.font = Font.systemFont(10);\n lunarText.textColor = color;\n }\n if (isToday(date)) {\n if (family !== 'large') {\n item.cornerRadius = width \/ 2;\n item.backgroundColor = new Color(themeColor);\n } else {\n const cw = Math.min(12 * Math.sqrt(2) * 2, width);\n const cp = cw \/ 2 - 10;\n item.$content.size = new Size(cw, cw);\n item.$content.setPadding(0, cp, 0, 0);\n item.$content.cornerRadius = cw \/ 2;\n item.$content.backgroundColor = new Color(themeColor);\n }\n } else if (i > -1) {\n dates.splice(i, 1);\n const sfs = SFSymbol.named(symbolName);\n sfs.applyFont(Font.systemFont(18));\n const image = sfs.image;\n item.backgroundImage = await tintedImage(image, calendar.color);\n item.$text.shadowColor = calendar.color;\n item.$text.shadowOffset = new Point(0.5, 0.5);\n item.$text.shadowRadius = 0.5;\n }\n};\n\n\/**\n * @param {WidgetStack} stack\n * @param {CalendarEvent | Reminder} event\n *\/\nconst addEvent = (stack, event) => {\n const { eventFontSize } = preference;\n const { color } = event.calendar;\n const row = stack.addStack();\n row.layoutHorizontally();\n row.centerAlignContent();\n row.size = new Size(-1, 28);\n const line = row.addStack();\n line.layoutVertically();\n line.size = new Size(2.4, -1);\n line.cornerRadius = 1.2;\n line.backgroundColor = color;\n line.addSpacer();\n\n row.addSpacer(6);\n const content = row.addStack();\n content.layoutVertically();\n const title = content.addText(event.title);\n title.font = Font.boldSystemFont(eventFontSize);\n const rgba = hexToRGBA(color.hex);\n const hsl = RGBToHSL(rgba.red, rgba.green, rgba.blue);\n const lightColor = hsl.l > 30 ? new Color(lightenDarkenColor(hsl, 30 - hsl.l)) : color;\n const darkColor = hsl.l < 60 ? new Color(lightenDarkenColor(hsl, 60 - hsl.l)) : color;\n title.textColor = Color.dynamic(lightColor, darkColor);\n const dateFormat = new Intl.DateTimeFormat([], {\n month: '2-digit',\n day: '2-digit'\n }).format;\n const timeFormat = new Intl.DateTimeFormat([], {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false\n }).format;\n const items = [];\n const eventDate = event.startDate || event.dueDate;\n if (isToday(eventDate)) {\n items.push(i18n(['Today', '今天']));\n } else {\n items.push(dateFormat(eventDate));\n }\n \/\/ Don't use `!isAllDay`, Reminder does not have `isAllDay` attribute\n if (event.isAllDay === false || event.dueDateIncludesTime) items.push(timeFormat(eventDate));\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n const startDayDate = new Date(eventDate);\n startDayDate.setHours(0, 0, 0, 0);\n const diff = (startDayDate - today) \/ (24 * 3600000);\n if (diff > 0) items.push(`T+${Math.round(diff)}`);\n const date = content.addText(items.join(' '));\n date.font = Font.systemFont(eventFontSize * 12 \/ 13);\n date.textColor = Color.gray();\n row.addSpacer();\n};\n\nconst getReminders = async () => {\n const { eventDays } = preference;\n const calendars = await Calendar.forReminders();\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n const later7Date = new Date(today.getTime() + eventDays * 24 * 3600000);\n today.setHours(0, 0, 0, -1);\n const reminders = await Reminder.incompleteDueBetween(today, later7Date, calendars);\n return reminders\n};\n\nconst getEvents = async () => {\n const { eventDays } = preference;\n const calendars = await Calendar.forEvents();\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n const later7Date = new Date(today.getTime() + eventDays * 24 * 3600000);\n const events = await CalendarEvent.between(today, later7Date, calendars);\n return events\n};\n\n\/**\n * @param {WidgetStack} stack\n *\/\nconst addEvents = async (stack) => {\n const { eventMax, includesReminder } = preference;\n const promises = [getEvents()];\n if (includesReminder) {\n promises.push(getReminders());\n }\n const eventsList = await Promise.all(promises);\n const _events = eventsList.flat().sort(\n (a, b) => (a.startDate || a.dueDate) - (b.startDate || b.dueDate)\n );\n const list = stack.addStack();\n const holder = stack.addStack();\n holder.layoutHorizontally();\n holder.addSpacer();\n list.layoutVertically();\n for (const event of _events.slice(0, eventMax)) {\n list.addSpacer(4);\n addEvent(list, event);\n }\n return list\n};\n\nconst createWidget = async () => {\n const { layout } = preference;\n const phone = phoneSize();\n const scale = Device.screenScale();\n const family = config.widgetFamily;\n const widgetWidth = phone[family === 'large' ? 'medium' : family] \/ scale;\n const widgetHeight = phone[family === 'medium' ? 'small' : family] \/ scale;\n const is7Rows = (firstDay.getDay() + lastDay.getDate()) > 35;\n let itemWidth = (widgetHeight - titleSize - 12 * 2 + rowGap) \/ (is7Rows ? 7 : 6) - rowGap;\n const w = (widgetWidth - 15 * 2 + columnGap) \/ 7 - columnGap;\n itemWidth = Math.min(itemWidth, w);\n\n const widget = new ListWidget();\n widget.url = 'calshow:\/\/';\n const lightColor = new Color('#fff');\n const darkColor = new Color('#242426');\n widget.backgroundColor = theme === 'light'\n ? lightColor\n : theme === 'dark'\n ? darkColor\n : Color.dynamic(lightColor, darkColor);\n widget.setPadding(12, 15, 12, 15);\n addTitle(widget);\n const row = widget.addStack();\n const actions = [\n () =>\n addCalendar(row, {\n itemWidth,\n gap: is7Rows ? [columnGap, rowGap - 1] : [columnGap, rowGap],\n addDay\n })\n ];\n if (family === 'medium') {\n if (layout === 'calendar_events') {\n actions.push(() => addEvents(row));\n } else {\n actions.unshift(() => addEvents(row));\n }\n }\n for (const [i, action] of actions.entries()) {\n if (layout === 'calendar_events' && i > 0) {\n row.addSpacer(10);\n }\n await action();\n }\n return widget\n};\n\nconst {\n themeColor,\n textColor,\n textColorDark,\n weekendColor,\n weekendColorDark,\n symbolName\n} = preference;\nconst eventSettings = {\n name: 'event',\n type: 'group',\n label: i18n(['Events', '事件']),\n items: [\n {\n name: 'eventFontSize',\n type: 'number',\n label: i18n(['Text size', '字体大小']),\n default: preference.eventFontSize\n },\n {\n name: 'eventMax',\n type: 'number',\n label: i18n(['Max count', '最大显示数量']),\n default: preference.eventMax\n },\n {\n name: 'includesReminder',\n type: 'switch',\n label: i18n(['Show reminders', '显示提醒事项']),\n default: preference.includesReminder\n },\n {\n name: 'eventDays',\n type: 'number',\n label: i18n(['Days limit', '天数限制']),\n default: preference.eventDays\n },\n {\n name: 'layout',\n type: 'select',\n label: i18n(['Content placement', '排列方式']),\n options: [\n { label: i18n(['Calendar-Events', '日历-事件']), value: 'calendar_events' },\n { label: i18n(['Events-Calendar', '事件-日历']), value: 'events_calendar' }\n ],\n default: preference.layout\n }\n ]\n};\nconst widget = await withSettings({\n formItems: [\n {\n name: 'themeColor',\n type: 'color',\n label: i18n(['Theme color', '主题色']),\n default: themeColor\n },\n {\n name: 'textColor',\n type: 'color',\n label: i18n(['Text color (light)', '文字颜色(白天)']),\n default: textColor\n },\n {\n name: 'textColorDark',\n type: 'color',\n label: i18n(['Text color (dark)', '文字颜色(夜晚)']),\n default: textColorDark\n },\n {\n name: 'weekendColor',\n type: 'color',\n label: i18n(['Weekend color (light)', '周末文字颜色(白天)']),\n default: weekendColor\n },\n {\n name: 'weekendColorDark',\n type: 'color',\n label: i18n(['Weekend color (dark)', '周末文字颜色(夜晚)']),\n default: weekendColorDark\n },\n {\n name: 'symbolName',\n label: i18n(['Calendar SFSymbol icon', '事件 SFSymbol 图标']),\n default: symbolName\n },\n eventSettings\n ],\n render: async ({ family, settings }) => {\n if (family) {\n config.widgetFamily = family;\n }\n Object.assign(preference, settings);\n const widget = await createWidget();\n return widget\n }\n});\nif (config.runsInWidget) {\n Script.setWidget(widget);\n}\n",
"share_sheet_inputs" : [
]
}