// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: teal; icon-glyph: cogs; /* * Author: 2Ya * Github: https://github.com/dompling * UI 配置升级 感谢 @LSP 大佬提供代码 */ class DmYY { constructor(arg, defaultSettings) { this.arg = arg; this.defaultSettings = defaultSettings || {}; this.SETTING_KEY = this.md5(Script.name()); this._init(); this.isNight = Device.isUsingDarkAppearance(); } BaseCacheKey = 'DmYY'; _actions = []; _menuActions = []; widgetColor; backGroundColor; isNight; userConfigKey = ['avatar', 'nickname', 'homePageDesc']; // 获取 Request 对象 getRequest = (url = '') => { return new Request(url); }; // 发起请求 http = async ( options = { headers: {}, url: '' }, type = 'JSON', onError = () => { return SFSymbol.named('photo').image; } ) => { let request; try { if (type === 'IMG') { const fileName = `${this.cacheImage}/${this.md5(options.url)}`; request = this.getRequest(options.url); let response; if (await this.FILE_MGR.fileExistsExtra(fileName)) { request.loadImage().then((res) => { this.FILE_MGR.writeImage(fileName, res); }); return Image.fromFile(fileName); } else { response = await request.loadImage(); this.FILE_MGR.writeImage(fileName, response); } return response; } request = this.getRequest(); Object.keys(options).forEach((key) => { request[key] = options[key]; }); request.headers = { ...this.defaultHeaders, ...options.headers }; if (type === 'JSON') { return await request.loadJSON(); } if (type === 'STRING') { return await request.loadString(); } return await request.loadJSON(); } catch (e) { console.log('error:' + e); if (type === 'IMG') return onError?.(); } }; //request 接口请求 $request = { get: (url = '', options = {}, type = 'JSON') => { let params = { ...options, method: 'GET' }; if (typeof url === 'object') { params = { ...params, ...url }; } else { params.url = url; } let _type = type; if (typeof options === 'string') _type = options; return this.http(params, _type); }, post: (url = '', options = {}, type = 'JSON') => { let params = { ...options, method: 'POST' }; if (typeof url === 'object') { params = { ...params, ...url }; } else { params.url = url; } let _type = type; if (typeof options === 'string') _type = options; return this.http(params, _type); }, }; // 获取 boxJS 缓存 getCache = async (key = '', notify = true) => { try { let url = 'http://' + this.prefix + '/query/boxdata'; if (key) url = 'http://' + this.prefix + '/query/data/' + key; const boxdata = await this.$request.get( url, key ? { timeoutInterval: 1 } : {} ); if (key) { this.settings.BoxJSData = { ...this.settings.BoxJSData, [key]: boxdata.val, }; this.saveSettings(false); } if (boxdata.val) return boxdata.val; return boxdata.datas; } catch (e) { if (key && this.settings.BoxJSData[key]) { return this.settings.BoxJSData[key]; } if (notify) await this.notify( `${this.name} - BoxJS 数据读取失败`, '请检查 BoxJS 域名是否为代理复写的域名,如(boxjs.net 或 boxjs.com)。\n若没有配置 BoxJS 相关模块,请点击通知查看教程', 'https://chavyleung.gitbook.io/boxjs/awesome/videos' ); return false; } }; transforJSON = (str) => { if (typeof str == 'string') { try { return JSON.parse(str); } catch (e) { console.log(e); return str; } } console.log('It is not a string!'); }; // 选择图片并缓存 chooseImg = async (verify = false) => { const response = await Photos.fromLibrary().catch((err) => { console.log('图片选择异常:' + err); }); if (verify) { const bool = await this.verifyImage(response); if (bool) return response; return null; } return response; }; // 设置 widget 背景图片 getWidgetBackgroundImage = async (widget) => { const backgroundImage = await this.getBackgroundImage(); if (backgroundImage) { const opacity = Device.isUsingDarkAppearance() ? Number(this.settings.darkOpacity) : Number(this.settings.lightOpacity); widget.backgroundImage = await this.shadowImage( backgroundImage, '#000', opacity ); return true; } else { if (this.backGroundColor.colors) { widget.backgroundGradient = this.backGroundColor; } else { widget.backgroundColor = this.backGroundColor; } return false; } }; /** * 验证图片尺寸: 图片像素超过 1000 左右的时候会导致背景无法加载 * @param img Image */ verifyImage = async (img = {}) => { const { width, height } = img.size; const direct = true; if (width > 1000) { const options = ['取消', '打开图像处理']; const message = '您的图片像素为' + width + ' x ' + height + '\n' + '请将图片' + (direct ? '宽度' : '高度') + '调整到 1000 以下\n' + (!direct ? '宽度' : '高度') + '自动适应'; const index = await this.generateAlert(message, options); if (index === 1) Safari.openInApp('https://www.sojson.com/image/change.html', false); return false; } return true; }; /** * 获取截图中的组件剪裁图 * 可用作透明背景 * 返回图片image对象 * 代码改自:https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9 * @param {string} title 开始处理前提示用户截图的信息,可选(适合用在组件自定义透明背景时提示) */ async getWidgetScreenShot(title = null) { // Crop an image into the specified rect. function cropImage(img, rect) { let draw = new DrawContext(); draw.size = new Size(rect.width, rect.height); draw.drawImageAtPoint(img, new Point(-rect.x, -rect.y)); return draw.getImage(); } function phoneSizes(inputHeight) { return { /* Supported devices ================= The following device measurements have been confirmed in iOS 18. */ // 16 Pro Max 2868: { text: { small: 510, medium: 1092, large: 1146, left: 114, right: 696, top: 276, middle: 912, bottom: 1548, }, notext: { small: 530, medium: 1138, large: 1136, left: 91, right: 699, top: 276, middle: 882, bottom: 1488, }, }, // 16 Plus, 15 Plus, 15 Pro Max, 14 Pro Max 2796: { text: { small: 510, medium: 1092, large: 1146, left: 98, right: 681, top: 252, middle: 888, bottom: 1524, }, notext: { small: 530, medium: 1139, large: 1136, left: 75, right: 684, top: 252, middle: 858, bottom: 1464, }, }, // 16 Pro 2622: { text: { small: 486, medium: 1032, large: 1098, left: 87, right: 633, top: 261, middle: 872, bottom: 1485, }, notext: { small: 495, medium: 1037, large: 1035, left: 84, right: 626, top: 270, middle: 810, bottom: 1350, }, }, // 16, 15, 15 Pro, 14 Pro 2556: { text: { small: 474, medium: 1017, large: 1062, left: 81, right: 624, top: 240, middle: 828, bottom: 1416, }, notext: { small: 495, medium: 1047, large: 1047, left: 66, right: 618, top: 243, middle: 795, bottom: 1347, }, }, // SE3, SE2 1334: { text: { small: 296, medium: 642, large: 648, left: 54, right: 400, top: 60, middle: 412, bottom: 764, }, notext: { small: 309, medium: 667, large: 667, left: 41, right: 399, top: 67, middle: 425, bottom: 783, }, }, /* In-limbo devices ================= The following device measurements were confirmed in older versions of iOS. Please comment if you can confirm these for iOS 18. */ // 14 Plus, 13 Pro Max, 12 Pro Max 2778: { small: 510, medium: 1092, large: 1146, left: 96, right: 678, top: 246, middle: 882, bottom: 1518, }, // 11 Pro Max, XS Max 2688: { small: 507, medium: 1080, large: 1137, left: 81, right: 654, top: 228, middle: 858, bottom: 1488, }, // 14, 13, 13 Pro, 12, 12 Pro 2532: { small: 474, medium: 1014, large: 1062, left: 78, right: 618, top: 231, middle: 819, bottom: 1407, }, // 13 mini, 12 mini / 11 Pro, XS, X 2436: { x: { small: 465, medium: 987, large: 1035, left: 69, right: 591, top: 213, middle: 783, bottom: 1353, }, mini: { small: 465, medium: 987, large: 1035, left: 69, right: 591, top: 231, middle: 801, bottom: 1371, }, }, // 11, XR 1792: { small: 338, medium: 720, large: 758, left: 55, right: 437, top: 159, middle: 579, bottom: 999, }, // 11 and XR in Display Zoom mode 1624: { small: 310, medium: 658, large: 690, left: 46, right: 394, top: 142, middle: 522, bottom: 902, }, /* Older devices ================= The following devices cannot be updated to iOS 18 or later. */ // Home button Plus phones 2208: { small: 471, medium: 1044, large: 1071, left: 99, right: 672, top: 114, middle: 696, bottom: 1278, }, // Home button Plus in Display Zoom mode 2001: { small: 444, medium: 963, large: 972, left: 81, right: 600, top: 90, middle: 618, bottom: 1146, }, // SE1 1136: { small: 282, medium: 584, large: 622, left: 30, right: 332, top: 59, middle: 399, bottom: 399, }, }[inputHeight]; } let message = title || '开始之前,请先前往桌面,截取空白界面的截图。然后回来继续'; let exitOptions = ['我已截图', '前去截图 >']; let shouldExit = await this.generateAlert(message, exitOptions); if (shouldExit) return; // Get screenshot and determine phone size. let img = await Photos.fromLibrary(); let height = img.size.height; let phone = phoneSizes(height); if (!phone) { message = '好像您选择的照片不是正确的截图,请先前往桌面'; await this.generateAlert(message, ['我已知晓']); return; } // Extra setup needed for 2436-sized phones. if (height === 2436) { const files = this.FILE_MGR_LOCAL; let cacheName = 'mz-phone-type'; let cachePath = files.joinPath(files.libraryDirectory(), cacheName); // If we already cached the phone size, load it. if (files.fileExists(cachePath)) { let typeString = files.readString(cachePath); phone = phone[typeString]; // Otherwise, prompt the user. } else { message = '您的📱型号是?'; let types = ['iPhone 12 mini', 'iPhone 11 Pro, XS, or X']; let typeIndex = await this.generateAlert(message, types); let type = typeIndex === 0 ? 'mini' : 'x'; phone = phone[type]; files.writeString(cachePath, type); } } // If supported, check whether home screen has text labels or not. if (phone.text) { message = '主屏幕是否有文本标签?'; const textOptions = ['有', '无']; const _textOptions = ['text', 'notext']; const textResponse = await this.generateAlert(message, textOptions); phone = phone[_textOptions[textResponse]]; } // Prompt for widget size and position. message = '截图中要设置透明背景组件的尺寸类型是?'; let sizes = ['小尺寸', '中尺寸', '大尺寸']; let size = await this.generateAlert(message, sizes); let widgetSize = sizes[size]; message = '要设置透明背景的小组件在哪个位置?'; message += height === 1136 ? ' (备注:当前设备只支持两行小组件,所以下边选项中的「中间」和「底部」的选项是一致的)' : ''; // Determine image crop based on phone size. let crop = { w: '', h: '', x: '', y: '' }; if (widgetSize === '小尺寸') { crop.w = phone.small; crop.h = phone.small; let positions = [ '左上角', '右上角', '中间左', '中间右', '左下角', '右下角', ]; let _posotions = [ 'Top left', 'Top right', 'Middle left', 'Middle right', 'Bottom left', 'Bottom right', ]; let position = await this.generateAlert(message, positions); // Convert the two words into two keys for the phone size dictionary. let keys = _posotions[position].toLowerCase().split(' '); crop.y = phone[keys[0]]; crop.x = phone[keys[1]]; } else if (widgetSize === '中尺寸') { crop.w = phone.medium; crop.h = phone.small; // Medium and large widgets have a fixed x-value. crop.x = phone.left; let positions = ['顶部', '中间', '底部']; let _positions = ['Top', 'Middle', 'Bottom']; let position = await this.generateAlert(message, positions); let key = _positions[position].toLowerCase(); crop.y = phone[key]; } else if (widgetSize === '大尺寸') { crop.w = phone.medium; crop.h = phone.large; crop.x = phone.left; let positions = ['顶部', '底部']; let position = await this.generateAlert(message, positions); // Large widgets at the bottom have the "middle" y-value. crop.y = position ? phone.middle : phone.top; } // Crop image and finalize the widget. return cropImage(img, new Rect(crop.x, crop.y, crop.w, crop.h)); } setLightAndDark = async (title, desc, val, placeholder = '') => { try { const a = new Alert(); a.title = title; a.message = desc; a.addTextField(placeholder, `${this.settings[val] || ''}`); a.addAction('确定'); a.addCancelAction('取消'); const id = await a.presentAlert(); if (id === -1) return false; this.settings[val] = a.textFieldValue(0) || ''; this.saveSettings(); return true; } catch (e) { console.log(e); } }; /** * 弹出输入框 * @param title 标题 * @param desc 描述 * @param opt 属性 * @returns {Promise} */ setAlertInput = async (title, desc, opt = {}, isSave = true) => { const a = new Alert(); a.title = title; a.message = !desc ? '' : desc; Object.keys(opt).forEach((key) => { a.addTextField(opt[key], this.settings[key]); }); a.addAction('确定'); a.addCancelAction('取消'); const id = await a.presentAlert(); if (id === -1) return; const data = {}; Object.keys(opt).forEach((key, index) => { data[key] = a.textFieldValue(index) || ''; }); // 保存到本地 if (isSave) { this.settings = { ...this.settings, ...data }; return this.saveSettings(); } return data; }; setBaseAlertInput = async (title, desc, opt = {}, isSave = true) => { const a = new Alert(); a.title = title; a.message = !desc ? '' : desc; Object.keys(opt).forEach((key) => { a.addTextField(opt[key], this.baseSettings[key] || ''); }); a.addAction('确定'); a.addCancelAction('取消'); const id = await a.presentAlert(); if (id === -1) return; const data = {}; Object.keys(opt).forEach((key, index) => { data[key] = a.textFieldValue(index) || ''; }); // 保存到本地 if (isSave) return this.saveBaseSettings(data); return data; }; /** * 设置当前项目的 boxJS 缓存 * @param opt key value * @returns {Promise} */ setCacheBoxJSData = async (opt = {}) => { const options = ['取消', '确定']; const message = '代理缓存仅支持 BoxJS 相关的代理!'; const index = await this.generateAlert(message, options); if (index === 0) return; try { const boxJSData = await this.getCache(); Object.keys(opt).forEach((key) => { this.settings[key] = boxJSData[opt[key]] || ''; }); // 保存到本地 this.saveSettings(); } catch (e) { console.log(e); this.notify( this.name, 'BoxJS 缓存读取失败!点击查看相关教程', 'https://chavyleung.gitbook.io/boxjs/awesome/videos' ); } }; /** * 设置组件内容 * @returns {Promise} */ setWidgetConfig = async () => { const basic = [ { icon: { name: 'arrow.clockwise', color: '#1890ff' }, type: 'input', title: '刷新时间', desc: '刷新时间仅供参考,具体刷新时间由系统判断,单位:分钟', val: 'refreshAfterDate', }, { icon: { name: 'sun.max.fill', color: '#d48806' }, type: 'color', title: '白天字体颜色', desc: '请自行去网站上搜寻颜色(Hex 颜色)', val: 'lightColor', }, { icon: { name: 'moon.stars.fill', color: '#d4b106' }, type: 'color', title: '晚上字体颜色', desc: '请自行去网站上搜寻颜色(Hex 颜色)', val: 'darkColor', }, ]; return this.renderAppView([ { title: '基础设置', menu: basic }, { title: '背景设置', menu: [ { icon: { name: 'photo', color: '#13c2c2' }, type: 'color', title: '白天背景颜色', desc: '请自行去网站上搜寻颜色(Hex 颜色)\n支持渐变色,各颜色之间以英文逗号分隔', val: 'lightBgColor', }, { icon: { name: 'photo.fill', color: '#52c41a' }, type: 'color', title: '晚上背景颜色', desc: '请自行去网站上搜寻颜色(Hex 颜色)\n支持渐变色,各颜色之间以英文逗号分隔', val: 'darkBgColor', }, ], }, { menu: [ { icon: { name: 'photo.on.rectangle', color: '#fa8c16' }, name: 'dayBg', type: 'img', title: '日间背景', val: this.cacheImage, verify: true, }, { icon: { name: 'photo.fill.on.rectangle.fill', color: '#fa541c' }, name: 'nightBg', type: 'img', title: '夜间背景', val: this.cacheImage, verify: true, }, { icon: { name: 'text.below.photo', color: '#faad14' }, type: 'img', name: 'transparentBg', title: '透明背景', val: this.cacheImage, onClick: async (item, __, previewWebView) => { const backImage = await this.getWidgetScreenShot(); if (!backImage || !(await this.verifyImage(backImage))) return; const cachePath = `${item.val}/${item.name}`; await this.htmlChangeImage(backImage, cachePath, { previewWebView, id: item.name, }); }, }, ], }, { menu: [ { icon: { name: 'record.circle', color: '#722ed1' }, type: 'input', title: '日间蒙层', desc: '完全透明请设置为0', val: 'lightOpacity', }, { icon: { name: 'record.circle.fill', color: '#eb2f96' }, type: 'input', title: '夜间蒙层', desc: '完全透明请设置为0', val: 'darkOpacity', }, ], }, { menu: [ { icon: { name: 'clear', color: '#f5222d' }, name: 'removeBackground', title: '清空背景图片', val: `${this.cacheImage}/`, onClick: async (_, __, previewWebView) => { const ids = ['dayBg', 'nightBg', 'transparentBg']; const options = [ '清空日间', '清空夜间', '清空透明', `清空全部`, '取消', ]; const message = '该操作不可逆,会清空背景图片!'; const index = await this.generateAlert(message, options); if (index === 4) return; switch (index) { case 3: await this.htmlChangeImage(false, `${_.val}${ids[0]}`, { previewWebView, id: ids[0], }); await this.htmlChangeImage(false, `${_.val}${ids[1]}`, { previewWebView, id: ids[1], }); await this.htmlChangeImage(false, `${_.val}${ids[2]}`, { previewWebView, id: ids[2], }); return; default: await this.htmlChangeImage(false, `${_.val}${ids[index]}`, { previewWebView, id: ids[index], }); break; } }, }, ], }, { title: '重置组件', menu: [ { icon: { name: 'trash', color: '#D85888' }, title: '重置', desc: '重置当前组件配置', name: 'reset', val: 'reset', onClick: () => { this.settings = {}; this.saveSettings(); this.reopenScript(); }, }, ], }, ]).catch((e) => { console.log(e); }); }; drawTableIcon = async ( icon = 'square.grid.2x2', color = '#504ED5', cornerWidth = 42 ) => { let sfi = SFSymbol.named('square.grid.2x2'); try { sfi = SFSymbol.named(icon); sfi.applyFont(Font.mediumSystemFont(30)); } catch (e) { console.log(`图标(${icon})异常:` + e); } const imgData = Data.fromPNG(sfi.image).toBase64String(); const html = ` `; const js = ` var canvas = document.createElement("canvas"); var sourceImg = document.getElementById("sourceImg"); var silhouetteImg = document.getElementById("silhouetteImg"); var ctx = canvas.getContext('2d'); var size = sourceImg.width > sourceImg.height ? sourceImg.width : sourceImg.height; canvas.width = size; canvas.height = size; ctx.drawImage(sourceImg, (canvas.width - sourceImg.width) / 2, (canvas.height - sourceImg.height) / 2); var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); var pix = imgData.data; //convert the image into a silhouette for (var i=0, n = pix.length; i < n; i+= 4){ //set red to 0 pix[i] = 255; //set green to 0 pix[i+1] = 255; //set blue to 0 pix[i+2] = 255; //retain the alpha value pix[i+3] = pix[i+3]; } ctx.putImageData(imgData,0,0); silhouetteImg.src = canvas.toDataURL(); output=canvas.toDataURL() `; let wv = new WebView(); await wv.loadHTML(html); const base64Image = await wv.evaluateJavaScript(js); const iconImage = await new Request(base64Image).loadImage(); const size = new Size(160, 160); const ctx = new DrawContext(); ctx.opaque = false; ctx.respectScreenScale = true; ctx.size = size; const path = new Path(); const rect = new Rect(0, 0, size.width, size.width); path.addRoundedRect(rect, cornerWidth, cornerWidth); path.closeSubpath(); ctx.setFillColor(new Color(color)); ctx.addPath(path); ctx.fillPath(); const rate = 36; const iw = size.width - rate; const x = (size.width - iw) / 2; ctx.drawImageInRect(iconImage, new Rect(x, x, iw, iw)); return ctx.getImage(); }; dismissLoading = (webView) => { webView.evaluateJavaScript( "window.dispatchEvent(new CustomEvent('JWeb', { detail: { code: 'finishLoading' } }))", false ); }; insertTextByElementId = (webView, elementId, text) => { const scripts = `document.getElementById("${elementId}_val").innerHTML=\`${text}\`;`; webView.evaluateJavaScript(scripts, false); }; loadSF2B64 = async ( icon = 'square.grid.2x2', color = '#56A8D6', cornerWidth = 42 ) => { const sfImg = await this.drawTableIcon(icon, color, cornerWidth); return `data:image/png;base64,${Data.fromPNG(sfImg).toBase64String()}`; }; setUserInfo = async () => { const baseOnClick = async (item, _, previewWebView) => { const data = await this.setBaseAlertInput(item.title, item.desc, { [item.val]: item.placeholder, }); if (!data) return; this.insertTextByElementId(previewWebView, item.name, data[item.val]); }; return this.renderAppView([ { title: '个性设置', menu: [ { icon: { name: 'person', color: '#fa541c' }, name: this.userConfigKey[0], title: '首页头像', type: 'img', val: this.baseImage, onClick: async (_, __, previewWebView) => { const options = ['相册选择', '在线链接', '取消']; const message = '设置个性化头像'; const index = await this.generateAlert(message, options); if (index === 2) return; const cachePath = `${_.val}/${_.name}`; switch (index) { case 0: const albumOptions = ['选择图片', '清空图片', '取消']; const albumIndex = await this.generateAlert('', albumOptions); if (albumIndex === 2) return; if (albumIndex === 1) { await this.htmlChangeImage(false, cachePath, { previewWebView, id: _.name, }); return; } const backImage = await this.chooseImg(); if (backImage) { await this.htmlChangeImage(backImage, cachePath, { previewWebView, id: _.name, }); } break; case 1: const data = await this.setBaseAlertInput( '在线链接', '首页头像在线链接', { avatar: '🔗请输入 URL 图片链接', } ); if (!data) return; if (data[_.name] !== '') { const backImage = await this.$request.get( data[_.name], 'IMG' ); await this.htmlChangeImage(backImage, cachePath, { previewWebView, id: _.name, }); } else { await this.htmlChangeImage(false, cachePath, { previewWebView, id: _.name, }); } break; default: break; } }, }, { icon: { name: 'pencil', color: '#fa8c16' }, type: 'input', title: '首页昵称', desc: '个性化首页昵称', placeholder: '👤请输入头像昵称', val: this.userConfigKey[1], name: this.userConfigKey[1], defaultValue: this.baseSettings.nickname, onClick: baseOnClick, }, { icon: { name: 'lineweight', color: '#a0d911' }, type: 'input', title: '首页昵称描述', desc: '个性化首页昵称描述', placeholder: '请输入描述', val: this.userConfigKey[2], name: this.userConfigKey[2], defaultValue: this.baseSettings.homePageDesc, onClick: baseOnClick, }, ], }, { menu: [ { icon: { name: 'shippingbox', color: '#f7bb10' }, type: 'input', title: 'BoxJS 域名', desc: '设置BoxJS访问域名,如:boxjs.net 或 boxjs.com', val: 'boxjsDomain', name: 'boxjsDomain', placeholder: 'boxjs.net', defaultValue: this.baseSettings.boxjsDomain, onClick: baseOnClick, }, { icon: { name: 'clear', color: '#f5222d' }, title: '恢复默认设置', name: 'reset', onClick: async () => { const options = ['取消', '确定']; const message = '确定要恢复当前所有配置吗?'; const index = await this.generateAlert(message, options); if (index === 1) { this.settings = {}; this.baseSettings = {}; this.FILE_MGR.remove(this.cacheImage); for (const item of this.cacheImageBgPath) { await this.setBackgroundImage(false, item, false); } this.saveSettings(false); this.saveBaseSettings(); await this.notify( '重置成功', '请关闭窗口之后,重新运行当前脚本' ); this.reopenScript(); } }, }, ], }, ]); }; htmlChangeImage = async (image, path, { previewWebView, id }) => { const base64Img = await this.setBackgroundImage(image, path, false); console.log(path); this.insertTextByElementId( previewWebView, id, base64Img ? `` : '' ); }; reopenScript = () => { Safari.open(`scriptable:///run/${encodeURIComponent(Script.name())}`); }; async renderAppView( options = [], renderAvatar = false, previewWebView = new WebView() ) { const settingItemFontSize = 14, authorNameFontSize = 20, authorDescFontSize = 12; // ================== 配置界面样式 =================== const style = ` :root { --color-primary: #007aff; --divider-color: rgba(60,60,67,0.16); --card-background: #fff; --card-radius: 8px; --list-header-color: rgba(60,60,67,0.6); } * { -webkit-user-select: none; user-select: none; } body { margin: 10px 0; -webkit-font-smoothing: antialiased; font-family: "SF Pro Display","SF Pro Icons","Helvetica Neue","Helvetica","Arial",sans-serif; accent-color: var(--color-primary); background: #f6f6f6; } .list { margin: 15px; } .list__header { margin: 0 18px; color: var(--list-header-color); font-size: 13px; } .list__body { margin-top: 10px; background: var(--card-background); border-radius: var(--card-radius); overflow: hidden; } .form-item-auth { display: flex; align-items: center; justify-content: space-between; min-height: 4em; padding: 0.5em 18px; position: relative; } .form-item-auth-name { margin: 0px 12px; font-size: ${authorNameFontSize}px; font-weight: 430; } .form-item-auth-desc { margin: 0px 12px; font-size: ${authorDescFontSize}px; font-weight: 400; } .form-label-author-avatar { width: 62px; height: 62px; border-radius:50%; border: 1px solid #F6D377; } .form-item, .form-item-switch { display: flex; align-items: center; justify-content: space-between; font-size: ${settingItemFontSize}px; font-weight: 400; min-height: 2.2em; padding: 0.5em 10px; position: relative; } label > * { pointer-events: none; } .form-label { display: flex; align-items: center; flex-wrap:nowrap } .form-label-img { height: 30px; } .form-label-title { margin-left: 8px; white-space: nowrap; } .bottom-bg { margin: 30px 15px 15px 15px; } .form-item--link .icon-arrow-right { color: #86868b; } .form-item-right-desc { font-size: 13px; color: #86868b; margin: 0 4px 0 auto; max-width: 130px; overflow: hidden; text-overflow: ellipsis; display:flex; align-items: center; white-space: nowrap; } .form-item-right-desc img{ width:30px; height:30px; border-radius:3px; } .form-item + .form-item::before, .form-item + .form-item-switch::before, .form-item-switch + .form-item::before, .form-item-switch + .form-item-switch::before { content: ""; position: absolute; top: 0; left: 0; right: 0; border-top: 0.5px solid var(--divider-color); } .form-item input[type="checkbox"] { width: 2em; height: 2em; } input[type='input'],select,input[type='date'] { width: 100%; height: 2.3em; outline-style: none; text-align: right; padding: 0px 10px; border: 1px solid #ddd; font-size: 14px; color: #86868b; border-radius:4px; } input[type='checkbox'][role='switch'] { position: relative; display: inline-block; appearance: none; width: 40px; height: 24px; border-radius: 24px; background: #ccc; transition: 0.3s ease-in-out; } input[type='checkbox'][role='switch']::before { content: ''; position: absolute; left: 2px; top: 2px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: 0.3s ease-in-out; } input[type='checkbox'][role='switch']:checked { background: var(--color-primary); } input[type='checkbox'][role='switch']:checked::before { transform: translateX(16px); } .copyright { display: flex; align-items: center; justify-content: space-between; margin: 15px; font-size: 10px; color: #86868b; } .copyright a { color: #515154; text-decoration: none; } .preview.loading { pointer-events: none; } .icon-loading { display: inline-block; animation: 1s linear infinite spin; } .normal-loading { display: inline-block; animation: 20s linear infinite spin; } @keyframes spin { 0% { transform: rotate(0); } 100% { transform: rotate(1turn); } } @media (prefers-color-scheme: dark) { :root { --divider-color: rgba(84,84,88,0.65); --card-background: #1c1c1e; --list-header-color: rgba(235,235,245,0.6); } body { background: #000; color: #fff; } }`; const js = ` (() => { window.invoke = (code, data) => { window.dispatchEvent( new CustomEvent( 'JBridge', { detail: { code, data } } ) ) } // 切换ico的loading效果 const toggleIcoLoading = (e) => { try{ const target = e.currentTarget target.classList.add('loading') const icon = e.currentTarget.querySelector('.iconfont') const className = icon.className icon.className = 'iconfont icon-loading' const listener = (event) => { const { code } = event.detail if (code === 'finishLoading') { target.classList.remove('loading') icon.className = className window.removeEventListener('JWeb', listener); } } window.addEventListener('JWeb', listener) }catch(e){ for (const loading of document.querySelectorAll('.icon-loading')) { loading.classList.remove('loading'); loading.className = "iconfont icon-arrow-right"; } } }; for (const btn of document.querySelectorAll('.form-item')) { btn.addEventListener('click', (e) => { if(!e.target.id)return; toggleIcoLoading(e); invoke(e.target.id); }) } for (const btn of document.querySelectorAll('.form-item__input')) { btn.addEventListener('change', (e) => { if(!e.target.name)return; invoke(e.target.name,e.target.type==="checkbox"?\`\${e.target.checked}\`: e.target.value); }) } if(${renderAvatar}){ document.querySelectorAll('.form-item-auth')[0].addEventListener('click', (e) => { toggleIcoLoading(e); invoke("userInfo"); }) } })()`; let configList = ``; let actionsConfig = []; for (const key in options) { const item = options[key]; actionsConfig = [...item.menu, ...actionsConfig]; configList += `
${item.title || ''}
`; for (const menuItem of item.menu) { let iconBase64 = ``; if (menuItem.children) { menuItem.onClick = () => { return this.renderAppView( typeof menuItem.children === 'function' ? menuItem.children() : menuItem.children ); }; } if (menuItem.url) { const imageIcon = await this.http( { url: menuItem.url }, 'IMG', () => { return this.drawTableIcon('gear'); } ); if (menuItem.url.indexOf('png') !== -1) { iconBase64 = `data:image/png;base64,${Data.fromPNG( imageIcon ).toBase64String()}`; } else { iconBase64 = `data:image/png;base64,${Data.fromJPEG( imageIcon ).toBase64String()}`; } } else { const icon = menuItem.icon || {}; iconBase64 = await this.loadSF2B64(icon.name, icon.color); } const idName = menuItem.name || menuItem.val; let defaultHtml = ``; menuItem.defaultValue = this.settings[idName] || menuItem.defaultValue || ''; if (menuItem.type === 'input') { defaultHtml = menuItem.defaultValue || ''; } else if (menuItem.type === 'img') { const cachePath = `${menuItem.val}/${menuItem.name}`; if (await this.FILE_MGR.fileExistsExtra(cachePath)) { const imageSrc = `data:image/png;base64,${Data.fromFile( cachePath ).toBase64String()}`; defaultHtml = ``; } } else if (menuItem.type === 'select') { let selectOptions = ''; menuItem.options.forEach((option) => { let selected = `selected="selected"`; selectOptions += ``; }); defaultHtml = ``; } else if (menuItem.type === 'switch') { const checked = menuItem.defaultValue == 'true' ? `checked="checked"` : ''; defaultHtml += ``; } else if (menuItem.type) { defaultHtml = ``; } let addLable = ''; if (menuItem.type === 'switch' || menuItem.type === 'checkbox') { addLable = `
`; } let avatarHtml = ''; if (renderAvatar) { const cachePath = `${this.baseImage}/${this.userConfigKey[0]}`; const avatarConfig = { avatar: `https://avatars.githubusercontent.com/u/23498579?v=4`, nickname: this.baseSettings[this.userConfigKey[1]] || 'Dompling', homPageDesc: this.baseSettings[this.userConfigKey[2]] || '18岁,来自九仙山的设计师', }; if (await this.FILE_MGR.fileExistsExtra(cachePath)) { avatarConfig.avatar = `data:image/png;base64,${Data.fromFile( cachePath ).toBase64String()}`; } avatarHtml = `
`; } const html = ` ${avatarHtml} ${configList} `; // 预览web await previewWebView.loadHTML(html); const injectListener = async () => { const event = await previewWebView.evaluateJavaScript( `(() => { try { window.addEventListener( 'JBridge', (e)=>{ completion(JSON.stringify(e.detail||{})) } ) } catch (e) { alert("预览界面出错:" + e); throw new Error("界面处理出错: " + e); return; } })()`, true ); const { code, data } = JSON.parse(event); try { const actionItem = actionsConfig.find( (item) => (item.name || item.val) === code ); if (code === 'userInfo') await this.setUserInfo(); if (actionItem) { const idName = actionItem?.name || actionItem?.val; if (actionItem?.onClick) { await actionItem?.onClick?.(actionItem, data, previewWebView); } else if (actionItem.type == 'input') { if ( await this.setLightAndDark( actionItem['title'], actionItem['desc'], idName, actionItem['placeholder'] ) ) this.insertTextByElementId( previewWebView, idName, this.settings[idName] || '' ); } else if (actionItem.type === 'img') { const cachePath = `${actionItem.val}/${actionItem.name}`; const options = ['相册选择', '清空图片', '取消']; const message = '相册图片选择,请选择合适图片大小'; const index = await this.generateAlert(message, options); switch (index) { case 0: const backImage = await this.chooseImg(actionItem.verify); if (backImage) { const cachePath = `${actionItem.val}/${actionItem.name}`; await this.htmlChangeImage(backImage, cachePath, { previewWebView, id: idName, }); } break; case 1: await this.htmlChangeImage(false, cachePath, { previewWebView, id: idName, }); break; default: break; } } else { if (data !== undefined) { this.settings[idName] = data; this.saveSettings(false); } } } } catch (error) { console.log('异常操作:' + error); } this.dismissLoading(previewWebView); injectListener(); }; injectListener().catch((e) => { console.error(e); this.dismissLoading(previewWebView); if (!config.runsInApp) { this.notify('主界面', `🚫 ${e}`); } }); previewWebView.present(); } initSFSymbol() { const named = SFSymbol.named; SFSymbol.named = (str) => { const current = named(str); if (!current) { console.log(`图标异常,请在文中搜索并替换图标:${str}`); return named('photo'); } return current; }; return SFSymbol; } _init(widgetFamily = config.widgetFamily) { this.initSFSymbol(); // 组件大小:small,medium,large this.widgetFamily = widgetFamily; //用于配置所有的组件相关设置 // 文件管理器 // 提示:缓存数据不要用这个操作,这个是操作源码目录的,缓存建议存放在local temp目录中 this.FILE_MGR = FileManager[ module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local' ](); this.FILE_MGR.fileExistsExtra = async (filePath) => { const file = this.FILE_MGR.fileExists(filePath); if (file) await this.FILE_MGR.downloadFileFromiCloud(filePath); return file; }; this.cacheImage = this.FILE_MGR.joinPath( this.FILE_MGR.documentsDirectory(), `/images/${Script.name()}` ); this.baseImage = this.FILE_MGR.joinPath( this.FILE_MGR.documentsDirectory(), `/images/` ); this.cacheImageBgPath = [ `${this.cacheImage}/transparentBg`, `${this.cacheImage}/dayBg`, `${this.cacheImage}/nightBg`, `${this.baseImage}/avatar`, ]; if (!this.FILE_MGR.fileExists(this.cacheImage)) { this.FILE_MGR.createDirectory(this.cacheImage, true); } // 本地,用于存储图片等 this.FILE_MGR_LOCAL = FileManager.local(); this.settings = this.getSettings(); this.baseSettings = this.getBaseSettings(); this.settings = { ...this.defaultSettings, ...this.settings }; this.settings.lightColor = this.settings.lightColor || '#000000'; this.settings.darkColor = this.settings.darkColor || '#ffffff'; this.settings.lightBgColor = this.settings.lightBgColor || '#ffffff'; this.settings.darkBgColor = this.settings.darkBgColor || '#000000'; this.settings.boxjsDomain = this.baseSettings.boxjsDomain || 'boxjs.net'; this.settings.refreshAfterDate = this.settings.refreshAfterDate || '30'; this.settings.lightOpacity = this.settings.lightOpacity || '0.4'; this.settings.darkOpacity = this.settings.darkOpacity || '0.7'; this.prefix = this.settings.boxjsDomain; config.runsInApp && this.saveSettings(false); this.backGroundColor = Color.dynamic( new Color(this.settings.lightBgColor), new Color(this.settings.darkBgColor) ); // const lightBgColor = this.getColors(this.settings.lightBgColor); // const darkBgColor = this.getColors(this.settings.darkBgColor); // if (lightBgColor.length > 1 || darkBgColor.length > 1) { // this.backGroundColor = !Device.isUsingDarkAppearance() // ? this.getBackgroundColor(lightBgColor) // : this.getBackgroundColor(darkBgColor); // } else if (lightBgColor.length > 0 && darkBgColor.length > 0) { // this.backGroundColor = Color.dynamic( // new Color(this.settings.lightBgColor), // new Color(this.settings.darkBgColor) // ); // } this.widgetColor = Color.dynamic( new Color(this.settings.lightColor), new Color(this.settings.darkColor) ); } getColors = (color = '') => { const colors = typeof color === 'string' ? color.split(',') : color; return colors; }; getBackgroundColor = (colors) => { const locations = []; const linearColor = new LinearGradient(); const cLen = colors.length; linearColor.colors = colors.map((item, index) => { locations.push(Math.floor(((index + 1) / cLen) * 100) / 100); return new Color(item, 1); }); linearColor.locations = locations; return linearColor; }; /** * 注册点击操作菜单 * @param {string} name 操作函数名 * @param {func} func 点击后执行的函数 */ registerAction(name, func, icon = { name: 'gear', color: '#096dd9' }, type) { if (typeof name === 'object' && !name.menu) return this._actions.push(name); if (typeof name === 'object' && name.menu) return this._menuActions.push(name); const action = { name, type, title: name, onClick: func?.bind(this), }; if (typeof icon === 'string') { action.url = icon; } else { action.icon = icon; } this._actions.push(action); } /** * base64 编码字符串 * @param {string} str 要编码的字符串 */ base64Encode(str) { const data = Data.fromString(str); return data.toBase64String(); } /** * base64解码数据 返回字符串 * @param {string} b64 base64编码的数据 */ base64Decode(b64) { const data = Data.fromBase64String(b64); return data.toRawString(); } /** * md5 加密字符串 * @param {string} str 要加密成md5的数据 */ // prettier-ignore md5(str){function d(n,t){var r=(65535&n)+(65535&t);return(((n>>16)+(t>>16)+(r>>16))<<16)|(65535&r)}function f(n,t,r,e,o,u){return d(((c=d(d(t,n),d(e,u)))<<(f=o))|(c>>>(32-f)),r);var c,f}function l(n,t,r,e,o,u,c){return f((t&r)|(~t&e),n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f((t&e)|(r&~e),n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;(n[t>>5]|=128<>>9)<<4)]=t);for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h>5]>>>e%32)&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e>5]|=(255&n.charCodeAt(e/8))<>>4)&15)+r.charAt(15&t));return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h((t=r(n))),8*t.length));var t}function u(n,t){return(function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16} */ async generateAlert(message, options) { let alert = new Alert(); alert.message = message; for (const option of options) { alert.addAction(option); } return await alert.presentAlert(); } /** * 弹出一个通知 * @param {string} title 通知标题 * @param {string} body 通知内容 * @param {string} url 点击后打开的URL */ async notify(title, body, url, opts = {}) { let n = new Notification(); n = Object.assign(n, opts); n.title = title; n.body = body; if (url) n.openURL = url; return await n.schedule(); } /** * 给图片加一层半透明遮罩 * @param {Image} img 要处理的图片 * @param {string} color 遮罩背景颜色 * @param {float} opacity 透明度 */ async shadowImage(img, color = '#000000', opacity = 0.7) { if (!img) return; if (opacity === 0) return img; let ctx = new DrawContext(); // 获取图片的尺寸 ctx.size = img.size; ctx.drawImageInRect( img, new Rect(0, 0, img.size['width'], img.size['height']) ); ctx.setFillColor(new Color(color, opacity)); ctx.fillRect(new Rect(0, 0, img.size['width'], img.size['height'])); return await ctx.getImage(); } /** * 获取当前插件的设置 * @param {boolean} json 是否为json格式 */ getSettings(json = true) { let res = json ? {} : ''; let cache = ''; if (Keychain.contains(this.SETTING_KEY)) { cache = Keychain.get(this.SETTING_KEY); } if (json) { try { res = JSON.parse(cache); } catch (e) {} } else { res = cache; } return res; } getBaseSettings(json = true) { let res = json ? {} : ''; let cache = ''; if (Keychain.contains(this.BaseCacheKey)) { cache = Keychain.get(this.BaseCacheKey); } if (json) { try { res = JSON.parse(cache); } catch (e) {} } else { res = cache; } return res; } saveBaseSettings(res = {}, notify = true) { const data = { ...(this.baseSettings || {}), ...res }; this.baseSettings = data; Keychain.set(this.BaseCacheKey, JSON.stringify(data)); if (notify) this.notify('设置成功', '通用设置需重新运行脚本生效'); return data; } /** * 存储当前设置 * @param {bool} notify 是否通知提示 */ saveSettings(notify = true) { let res = typeof this.settings === 'object' ? JSON.stringify(this.settings) : String(this.settings); Keychain.set(this.SETTING_KEY, res); if (notify) this.notify('设置成功', '桌面组件稍后将自动刷新'); return res; } /** * 获取当前插件是否有自定义背景图片 * @reutrn img | false */ async getBackgroundImage() { if (await this.FILE_MGR.fileExistsExtra(this.cacheImageBgPath[0])) return Image.fromFile(this.cacheImageBgPath[0]); if (!this.isNight) return (await this.FILE_MGR.fileExistsExtra(this.cacheImageBgPath[1])) ? Image.fromFile(this.cacheImageBgPath[1]) : undefined; else return (await this.FILE_MGR.fileExistsExtra(this.cacheImageBgPath[2])) ? Image.fromFile(this.cacheImageBgPath[2]) : undefined; } /** * 设置当前组件的背景图片 * @param {Image} img */ async setBackgroundImage(img, filePath = this.baseImage, notify = true) { const cacheKey = filePath; if (!img) { // 移除背景 if (this.FILE_MGR.fileExists(cacheKey)) this.FILE_MGR.remove(cacheKey); if (notify) this.notify('移除成功', '背景图片已移除,稍后刷新生效'); } else { // 设置背景 this.FILE_MGR.writeImage(cacheKey, img); if (notify) this.notify('设置成功', '背景图片已设置!稍后刷新生效'); return `data:image/png;base64,${Data.fromFile( cacheKey ).toBase64String()}`; } } getRandomArrayElements(arr, count) { let shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index; min = min > 0 ? min : 0; while (i-- > min) { index = Math.floor((i + 1) * Math.random()); temp = shuffled[index]; shuffled[index] = shuffled[i]; shuffled[i] = temp; } return shuffled.slice(min); } textFormat = { defaultText: { size: 14, font: 'regular', color: this.widgetColor }, battery: { size: 10, font: 'bold', color: this.widgetColor }, title: { size: 16, font: 'semibold', color: this.widgetColor }, SFMono: { size: 12, font: 'SF Mono', color: this.widgetColor }, }; provideFont = (fontName, fontSize) => { const fontGenerator = { ultralight: function () { return Font.ultraLightSystemFont(fontSize); }, light: function () { return Font.lightSystemFont(fontSize); }, regular: function () { return Font.regularSystemFont(fontSize); }, medium: function () { return Font.mediumSystemFont(fontSize); }, semibold: function () { return Font.semiboldSystemFont(fontSize); }, bold: function () { return Font.boldSystemFont(fontSize); }, heavy: function () { return Font.heavySystemFont(fontSize); }, black: function () { return Font.blackSystemFont(fontSize); }, italic: function () { return Font.italicSystemFont(fontSize); }, }; const systemFont = fontGenerator[fontName]; if (systemFont) { return systemFont(); } return new Font(fontName, fontSize); }; provideText = (string, container, format) => { format = { font: 'light', size: 14, color: this.widgetColor, opacity: 1, minimumScaleFactor: 1, ...format, }; const textItem = container.addText(string); const textFont = format.font; const textSize = format.size; const textColor = format.color; textItem.font = this.provideFont(textFont, textSize); textItem.textColor = textColor; textItem.textOpacity = format.opacity || 1; textItem.minimumScaleFactor = format.minimumScaleFactor || 1; return textItem; }; } // @base.end const Runing = async (Widget, default_args = '', isDebug = true, extra) => { let M = null; // 判断hash是否和当前设备匹配 if (config.runsInWidget) { M = new Widget(args.widgetParameter || ''); if (extra) { Object.keys(extra).forEach((key) => { M[key] = extra[key]; }); } const W = await M.render(); try { if (M.settings.refreshAfterDate) { const refreshTime = parseInt(M.settings.refreshAfterDate) * 1000 * 60; const timeStr = new Date().getTime() + refreshTime; W.refreshAfterDate = new Date(timeStr); } } catch (e) { console.log(e); } if (W) { Script.setWidget(W); Script.complete(); } } else { let { act, __arg, __size } = args.queryParameters; M = new Widget(__arg || default_args || ''); if (extra) { Object.keys(extra).forEach((key) => { M[key] = extra[key]; }); } if (__size) M._init(__size); if (!act || !M['_actions']) { // 弹出选择菜单 const actions = M['_actions']; const onClick = async (item) => { M.widgetFamily = item.val; try { M._init(item.val); } catch (error) { console.log('初始化异常:' + error); } w = await M.render(); const fnc = item.val .toLowerCase() .replace(/( |^)[a-z]/g, (L) => L.toUpperCase()); if (w) return w[`present${fnc}`](); }; const preview = [], lockView = []; if (M.renderSmall) { preview.push({ url: `https://raw.githubusercontent.com/dompling/Scriptable/master/images/small.png`, title: '小尺寸', val: 'small', name: 'small', dismissOnSelect: true, onClick, }); } if (M.renderMedium) { preview.push({ url: `https://raw.githubusercontent.com/dompling/Scriptable/master/images/medium.png`, title: '中尺寸', val: 'medium', name: 'medium', dismissOnSelect: true, onClick, }); } if (M.renderLarge) { preview.push({ url: `https://raw.githubusercontent.com/dompling/Scriptable/master/images/large.png`, title: '大尺寸', val: 'large', name: 'large', dismissOnSelect: true, onClick, }); } if (M.renderAccessoryInline) { lockView.push({ icon: { color: '#4676EE', name: 'list.triangle', }, title: '锁屏列表', val: 'accessoryInline', name: 'accessoryInline', dismissOnSelect: true, onClick, }); } if (M.renderAccessoryRectangular) { lockView.push({ icon: { color: '#4676EE', name: 'arrow.rectanglepath', }, title: '锁屏 2x', val: 'accessoryRectangular', name: 'accessoryRectangular', dismissOnSelect: true, onClick, }); } if (M.renderAccessoryCircular) { lockView.push({ icon: { color: '#4676EE', name: 'circle.circle', }, title: '锁屏 1x', val: 'accessoryCircular', name: 'accessoryCircular', dismissOnSelect: true, onClick, }); } const menuConfig = [ ...(preview ? [{ title: '预览组件', menu: preview }] : []), ...(lockView.length ? [{ title: '锁屏组件', menu: lockView }] : []), ...M['_menuActions'], ]; if (actions.length) menuConfig.push({ title: '组件配置', menu: actions }); await M.renderAppView(menuConfig, true); } } }; // await new DmYY().setWidgetConfig(); module.exports = { DmYY, Runing };