-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathGitHub Contributions.scriptable
12 lines (11 loc) · 31.7 KB
/
GitHub Contributions.scriptable
1
2
3
4
5
6
7
8
9
10
11
12
{
"always_run_in_app" : false,
"icon" : {
"color" : "deep-gray",
"glyph" : "braille"
},
"name" : "GitHub Contributions",
"script" : "\/**\n * @version 1.4.0\n * @author Honye\n *\/\n\n\/**\n * @param {object} options\n * @param {string} [options.title]\n * @param {string} [options.message]\n * @param {Array<{ title: string; [key: string]: any }>} options.options\n * @param {boolean} [options.showCancel = true]\n * @param {string} [options.cancelText = 'Cancel']\n *\/\nconst presentSheet = async (options) => {\n options = {\n showCancel: true,\n cancelText: 'Cancel',\n ...options\n };\n const alert = new Alert();\n if (options.title) {\n alert.title = options.title;\n }\n if (options.message) {\n alert.message = options.message;\n }\n if (!options.options) {\n throw new Error('The \"options\" property of the parameter cannot be empty')\n }\n for (const option of options.options) {\n alert.addAction(option.title);\n }\n if (options.showCancel) {\n alert.addCancelAction(options.cancelText);\n }\n const value = await alert.presentSheet();\n return {\n value,\n option: options.options[value]\n }\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 * @param {{[language: string]: string} | 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\nconst getImage = async (url) => {\n const request = new Request(url);\n const image = await request.loadImage();\n return image\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 * 注意:桌面组件无法写入 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 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 const readString = (filePath) => {\n return fm.readString(\n fm.joinPath(cacheDirectory, filePath)\n )\n };\n\n const readJSON = (filePath) => JSON.parse(readString(filePath));\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\nconst useCache = () => useFileManager({ basePath: 'cache' });\n\n\/**\n * @param {ListWidget | WidgetStack} stack container widget\n * @param {object} options\n * @param {string} [options.src] image url\n * @param {Image} [options.image]\n * @param {number} options.size\n *\/\nconst addAvatar = async (stack, options) => {\n const { image, src, size } = options;\n const _image = stack.addImage(image || await getImage(src));\n _image.imageSize = new Size(size, size);\n _image.cornerRadius = size;\n return _image\n};\n\n\/**\n * @param {ListWidget | 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\/**\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\n\/**\n * 轻松实现桌面组件可视化配置\n *\n * - 颜色选择器及更多表单控件\n * - 快速预览\n *\n * GitHub: https:\/\/github.com\/honye\n *\n * @version 1.2.0\n * @author Honye\n *\/\n\n\/**\n * @returns {Promise<Settings>}\n *\/\nconst readSettings = async () => {\n const localFM = useFileManager();\n let settings = localFM.readJSON('settings.json');\n if (settings) {\n console.log('[info] use local settings');\n return settings\n }\n\n const iCloudFM = useFileManager({ useICloud: true });\n settings = iCloudFM.readJSON('settings.json');\n if (settings) {\n console.log('[info] use iCloud settings');\n }\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('settings.json', data);\n};\n\nconst removeSettings = async (settings) => {\n const cache = useFileManager({ useICloud: settings.useICloud });\n FileManager.local().remove(\n FileManager.local().joinPath(\n cache.cacheDirectory,\n 'settings.json'\n )\n );\n};\n\nconst moveSettings = (useICloud, data) => {\n const localFM = useFileManager();\n const iCloudFM = useFileManager({ useICloud: true });\n const [i, l] = [\n FileManager.local().joinPath(\n iCloudFM.cacheDirectory,\n 'settings.json'\n ),\n FileManager.local().joinPath(\n localFM.cacheDirectory,\n 'settings.json'\n )\n ];\n try {\n writeSettings(data, { useICloud });\n if (useICloud) {\n FileManager.local().remove(l);\n } else {\n FileManager.iCloud().remove(i);\n }\n } catch (e) {\n console.error(e);\n }\n};\n\n\/**\n * @typedef {object} FormItem\n * @property {string} name\n * @property {string} label\n * @property {string} [type]\n * @property {{ label: string; value: unknown }[]} [options]\n * @property {unknown} [default]\n *\/\n\/**\n * @typedef {Record<string, unknown>} Settings\n * @property {boolean} useICloud\n * @property {string} [backgroundImage]\n *\/\n\/**\n * @param {object} options\n * @param {FormItem[]} [options.formItems]\n * @param {(data: {\n * settings: Settings;\n * family?: 'small'|'medium'|'large';\n * }) => Promise<ListWidget>} options.render\n * @param {string} [options.homePage]\n * @param {(item: FormItem) => void} [options.onItemClick]\n * @returns {Promise<ListWidget|undefined>} 在 Widget 中运行时返回 ListWidget,其它无返回\n *\/\nconst withSettings = async (options) => {\n const {\n formItems = [],\n onItemClick,\n render,\n homePage = 'https:\/\/www.imarkr.com'\n } = options;\n const cache = useCache();\n\n let settings = await readSettings() || {};\n const imgPath = FileManager.local().joinPath(\n cache.cacheDirectory,\n 'bg.png'\n );\n\n if (config.runsInWidget) {\n const widget = await render({ settings });\n if (settings.backgroundImage) {\n widget.backgroundImage = FileManager.local().readImage(imgPath);\n }\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 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 const js =\n`(() => {\n const settings = JSON.parse('${JSON.stringify(settings)}')\n const formItems = JSON.parse('${JSON.stringify(formItems)}')\n \n window.invoke = (code, data) => {\n window.dispatchEvent(\n new CustomEvent(\n 'JBridge',\n { detail: { code, data } }\n )\n )\n }\n \n const iCloudInput = document.querySelector('input[name=\"useICloud\"]')\n iCloudInput.checked = settings.useICloud\n iCloudInput\n .addEventListener('change', (e) => {\n invoke('moveSettings', e.target.checked)\n })\n \n const formData = {};\n\n const fragment = document.createDocumentFragment()\n for (const item of formItems) {\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 (item.type === 'cell') {\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 invoke('itemClick', item)\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 }\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 fragment.appendChild(label);\n }\n document.getElementById('form').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 const listener = (event) => {\n const { code } = event.detail\n if (code === 'previewStart') {\n target.classList.remove('loading')\n icon.className = className\n window.removeEventListener('JWeb', listener);\n }\n }\n window.addEventListener('JWeb', listener)\n invoke('preview', e.currentTarget.dataset.size)\n })\n }\n\n const reset = () => {\n for (const item of formItems) {\n const el = document.querySelector(\\`.form-item__input[name=\"\\${item.name}\"]\\`)\n formData[item.name] = item.default\n if (item.type === 'switch') {\n el.checked = item.default\n } else {\n el && (el.value = item.default)\n }\n }\n invoke('removeSettings', formData)\n }\n document.getElementById('reset').addEventListener('click', () => reset())\n\n document.getElementById('chooseBgImg')\n .addEventListener('click', () => invoke('chooseBgImg'))\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 <div class=\"list\">\n <div class=\"list__header\">${i18n(['Common', '通用'])}<\/div>\n <form class=\"list__body\" action=\"javascript:void(0);\">\n <label class=\"form-item\">\n <div>${i18n(['Sync with iCloud', 'iCloud 同步'])}<\/div>\n <input name=\"useICloud\" type=\"checkbox\" role=\"switch\">\n <\/label>\n <label id=\"chooseBgImg\" class=\"form-item form-item--link\">\n <div>${i18n(['Background image', '背景图'])}<\/div>\n <i class=\"iconfont icon-arrow_right\"><\/i>\n <\/label>\n <label id='reset' class=\"form-item form-item--link\">\n <div>${i18n(['Reset', '重置'])}<\/div>\n <i class=\"iconfont icon-arrow_right\"><\/i>\n <\/label>\n <\/form>\n <\/div>\n <div class=\"list\">\n <div class=\"list__header\">${i18n(['Settings', '设置'])}<\/div>\n <form id=\"form\" class=\"list__body\" action=\"javascript:void(0);\"><\/form>\n <\/div>\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 <footer>\n <div class=\"copyright\">Copyright © 2022 <a href=\"javascript:invoke('safari','https:\/\/www.imarkr.com');\">iMarkr<\/a> All rights reserved.<\/div>\n <\/footer>\n <script>${js}<\/script>\n <\/body>\n<\/html>`;\n\n const webView = new WebView();\n await webView.loadHTML(html, homePage);\n\n const clearBgImg = () => {\n delete settings.backgroundImage;\n const fm = FileManager.local();\n if (fm.fileExists(imgPath)) {\n fm.remove(imgPath);\n }\n };\n\n const chooseBgImg = async () => {\n const { option } = await presentSheet({\n options: [\n { key: 'choose', title: i18n(['Choose photo', '选择图片']) },\n { key: 'clear', title: i18n(['Clear background image', '清除背景图']) }\n ],\n cancelText: i18n(['Cancel', '取消'])\n });\n switch (option?.key) {\n case 'choose': {\n try {\n const image = await Photos.fromLibrary();\n cache.writeImage('bg.png', image);\n settings.backgroundImage = imgPath;\n writeSettings(settings, { useICloud: settings.useICloud });\n } catch (e) {}\n break\n }\n case 'clear':\n clearBgImg();\n writeSettings(settings, { useICloud: settings.useICloud });\n break\n }\n };\n\n const injectListener = async () => {\n const event = await webView.evaluateJavaScript(\n `(() => {\n const controller = new AbortController()\n const listener = (e) => {\n completion(e.detail)\n controller.abort()\n }\n window.addEventListener(\n 'JBridge',\n listener,\n { signal: controller.signal }\n )\n })()`,\n true\n ).catch((err) => {\n console.error(err);\n throw err\n });\n const { code, data } = event;\n switch (code) {\n case 'preview': {\n const widget = await render({ settings, family: data });\n const { backgroundImage } = settings;\n if (backgroundImage) {\n widget.backgroundImage = FileManager.local().readImage(backgroundImage);\n }\n webView.evaluateJavaScript(\n 'window.dispatchEvent(new CustomEvent(\\'JWeb\\', { detail: { code: \\'previewStart\\' } }))',\n false\n );\n widget[`present${data.replace(data[0], data[0].toUpperCase())}`]();\n break\n }\n case 'safari':\n Safari.openInApp(data, true);\n break\n case 'changeSettings':\n settings = { ...settings, ...data };\n writeSettings(data, { useICloud: settings.useICloud });\n break\n case 'moveSettings':\n settings.useICloud = data;\n moveSettings(data, settings);\n break\n case 'removeSettings':\n settings = { ...settings, ...data };\n clearBgImg();\n removeSettings(settings);\n break\n case 'chooseBgImg':\n await chooseBgImg();\n break\n case 'itemClick':\n onItemClick?.(data);\n break\n }\n injectListener();\n };\n\n injectListener().catch((e) => {\n console.error(e);\n throw e\n });\n webView.present();\n \/\/ ======= web end =========\n};\n\nif (typeof require === 'undefined') require = importModule;\n\nlet user = 'Honye';\nlet theme = 'system';\nlet useOfficial = true;\nconst officialColors = [\n ['#9be9a8', '#0e4429'],\n ['#40c463', '#006d32'],\n ['#30a14e', '#26a641'],\n ['#216e39', '#39d353']\n];\nconst halloweenColors = [\n ['#ffee4a', '#631c03'],\n ['#ffc501', '#bd561d'],\n ['#fe9600', '#fa7a18'],\n ['#03001c', '#fddf68']\n];\nlet themeColor = '#9be9a8';\nconst themes = {\n dark: {\n background: new Color('#242426')\n },\n light: {\n background: new Color('#ffffff')\n }\n};\n\nconst gap = { x: 3, y: 2 };\n\nconst screen = Device.screenResolution();\nconst scale = Device.screenScale();\nconst size = phoneSize(screen.height);\nconst cache = useCache();\n\n\/**\n * @param {string} user\n *\/\nconst fetchData = async (user) => {\n const url = `https:\/\/www.imarkr.com\/api\/github\/${user}`;\n const req = new Request(url);\n let data;\n try {\n data = await req.loadJSON();\n cache.writeJSON(`${user}.json`, data);\n } catch (e) {\n data = cache.readJSON(`${user}.json`);\n }\n return data\n};\n\nconst isHalloween = () => {\n const date = new Date();\n const month = date.getUTCMonth() + 1;\n const day = date.getUTCDate();\n return (month === 10 && day === 31) || (month === 11 && day === 1)\n};\n\nconst render = async () => {\n if (config.runsInWidget) {\n [\n user = user,\n theme = theme\n ] = (args.widgetParameter || '')\n .split(',')\n .map(item => item.trim() || undefined);\n }\n\n const resp = await fetchData(user);\n\n const { widgetFamily } = config;\n const columns = widgetFamily === 'small' ? 9 : 20;\n const widgetWidth = size[widgetFamily === 'large' ? 'medium' : widgetFamily] \/ scale;\n const rectWidth = (widgetWidth - 24 - gap.x * (columns - 1)) \/ columns;\n const widget = new ListWidget();\n widget.url = `https:\/\/github.com\/${user}`;\n widget.backgroundColor = theme === 'system'\n ? Color.dynamic(\n themes.light.background,\n themes.dark.background\n )\n : themes[theme].background;\n\n const {\n avatar,\n contributions,\n contribution_colors: respContributionColors\n } = resp;\n \/** GitHub 接口返回的主题色 *\/\n const contributionColors = respContributionColors\n ? respContributionColors.map((hex, index) => [hex, respContributionColors[respContributionColors.length - 1 - index]])\n : null;\n const name = resp.name || user;\n const countText = `${resp.contributions_count} contributions`;\n const latestDate = new Date(contributions.slice(-1)[0].date.replace(\/-\/g, '\/'));\n const sliceCount = columns * 7 - 7 + latestDate.getDay() + 1;\n const colorsData = contributions\n .slice(-sliceCount).map((item) => item.level);\n\n const head = widget.addStack();\n head.layoutHorizontally();\n head.centerAlignContent();\n\n \/\/ avatar\n let image;\n try {\n image = await getImage(avatar);\n cache.writeImage(`${user}.jpeg`, image);\n } catch (e) {\n image = cache.readImage(`${user}.jpeg`);\n }\n await addAvatar(head, { image, size: 20 });\n head.addSpacer(3);\n\n \/\/ user name\n const textName = head.addText(name.toUpperCase());\n textName.lineLimit = 1;\n textName.minimumScaleFactor = 0.5;\n textName.font = Font.boldSystemFont(13);\n textName.textColor = new Color('#aeaeb7', 1);\n head.addSpacer(3);\n\n \/\/ contributions count, would not show on small\n if (widgetFamily !== 'small') {\n const textCount = head.addText(`(${countText})`.toUpperCase());\n textCount.font = Font.systemFont(12);\n textCount.textColor = new Color('#aeaeb7', 1);\n }\n\n widget.addSpacer(10);\n\n const gridStack = widget.addStack();\n const { add } = await useGrid(gridStack, {\n direction: 'vertical',\n column: 7,\n gap: [gap.y, gap.x]\n });\n\n const rgba = hexToRGBA(themeColor);\n const hsl = RGBToHSL(rgba.red, rgba.green, rgba.blue);\n const itemColors = useOfficial\n ? contributionColors || (isHalloween() ? halloweenColors : officialColors)\n : Array(4).fill({}).map((_, index) => lightenDarkenColor(hsl, -index * 18));\n const colors = [['#ebedf0', '#45454a'], ...itemColors];\n\n const addItem = (stack, level) => {\n const rect = stack.addStack();\n rect.size = new Size(rectWidth, rectWidth);\n rect.cornerRadius = 2;\n const color = colors[level];\n rect.backgroundColor = theme === 'system'\n ? (Array.isArray(color)\n ? Color.dynamic(new Color(color[0]), new Color(color[1]))\n : new Color(color)\n )\n : new Color(colors[theme][level], 1);\n };\n\n for (const [, level] of colorsData.entries()) {\n await add((stack) => addItem(stack, level));\n }\n\n return widget\n};\n\nconst main = async () => {\n const widget = await withSettings({\n formItems: [\n {\n name: 'user',\n label: i18n(['User name', '用户名']),\n type: 'text',\n default: user\n },\n {\n name: 'useOfficial',\n label: i18n(['Official theme', '使用官方主题']),\n type: 'switch',\n default: useOfficial\n },\n {\n name: 'themeColor',\n label: i18n(['Theme color', '自定义主题色']),\n type: 'color',\n default: themeColor\n }\n ],\n render: async ({ family, settings }) => {\n if (family) {\n config.widgetFamily = family;\n }\n user = settings.user || user;\n themeColor = settings.themeColor || themeColor;\n useOfficial = settings.useOfficial ?? useOfficial;\n const widget = await render()\n .catch((e) => {\n console.error(e);\n throw e\n });\n return widget\n }\n });\n if (config.runsInWidget) {\n Script.setWidget(widget);\n }\n};\n\nawait main();\n",
"share_sheet_inputs" : [
]
}