From efe5165c9e2f5308809c86bf49dced0962dbc650 Mon Sep 17 00:00:00 2001 From: taoya7 Date: Thu, 2 Jun 2022 17:00:01 +0800 Subject: [PATCH] =?UTF-8?q?[fix]=E9=A1=B5=E9=9D=A2=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + README.md | 42 + Scripts/「小件件」开发环境.js | 1092 ++++++++++++++++++++++++++ Scripts/「源码」V2EX 社区.js | 372 +++++++++ Scripts/「源码」iOS限免.js | 135 ++++ Scripts/「源码」一言.js | 163 ++++ Scripts/「源码」京东白条数据.js | 244 ++++++ Scripts/「源码」人生电量.js | 342 +++++++++ Scripts/「源码」小组件示例.js | 106 +++ Scripts/「源码」微博热搜.js | 154 ++++ Scripts/「源码」百度热榜.js | 138 ++++ Scripts/「源码」蚂蚁庄园.js | 120 +++ app.js | 138 ++++ encode.js | 46 ++ install-runtime.js | 8 + jsconfig.json | 9 + pack.js | 45 ++ package.json | 19 + template/guide.html | 62 ++ yarn.lock | 1274 +++++++++++++++++++++++++++++++ 20 files changed, 4514 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 Scripts/「小件件」开发环境.js create mode 100644 Scripts/「源码」V2EX 社区.js create mode 100644 Scripts/「源码」iOS限免.js create mode 100644 Scripts/「源码」一言.js create mode 100644 Scripts/「源码」京东白条数据.js create mode 100644 Scripts/「源码」人生电量.js create mode 100644 Scripts/「源码」小组件示例.js create mode 100644 Scripts/「源码」微博热搜.js create mode 100644 Scripts/「源码」百度热榜.js create mode 100644 Scripts/「源码」蚂蚁庄园.js create mode 100644 app.js create mode 100644 encode.js create mode 100644 install-runtime.js create mode 100644 jsconfig.json create mode 100644 pack.js create mode 100644 package.json create mode 100644 template/guide.html create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e419a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +package-lock.json +Widget.js +.DS_File +Dist/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..efcdd47 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# 介绍 +这是一个便于开发者在电脑上开发、测试、预览 iOS 小组件(Scriptable)的开发框架 +通过简单安装,就可以获得一个舒适的Scriptable脚本开发环境,支持语法高亮、自动补全、实时同步测试预览。 +不用再在手机上敲代码了!并且该开发框架封装了很多常用的操作接口,让开发者专注数据解析+小组件UI设计,大大节省开发时间! + +# 开始 + +**首先,我们配置电脑开发环境:** + + +3. VSCode打开代码目录,进入终端,运行安装依赖命令:`npm install` +4. 安装好依赖,开启开发服务命令:`npm start` + + +**然后,配置手机运行环境:** +1. 运行服务后,会输出地址,手机访问该地址即可按照步骤初始化。或手动复制 [install-runtime.js](install-runtime.js) 脚本代码,打开 `Scriptable` 应用,点击右上角➕,粘贴代码,点击运行 +2. 如果成功,应该新加了两个插件文件:`「小件件」开发环境`、「`源码」小组件示例` +3. 点击 `「源码」小组件示例` 或者其他任何基于此框架开发的小组件,点击操作菜单的远程开发,即可连接电脑,开启远程开发体验! + + + +# 发布 + +开发测试完毕后,可以 `pull` 到本分支进行开源分享 +小组件源码存放在 [Scrips](Scripts) 目录,你也可以复制其他的小组件进行修改使用。 + + +**打包分享**: 你可以使用如下命令,打包你的小组件成一个单独的文件,从而可以分享给其他用户使用: +``` bash +$ node pack.js Scripts/「源码」你的小组件.js +``` +> 将会生成在 `Dist` 目录 + +**压缩代码**:打包的文件过大,如果需要压缩减少体积、加密敏感信息,可以通过如下脚本处理打包后的文件: +``` bash +$ node encode.js Dist/「小件件」你的小组件.js +``` +> 将会在 `Dist` 目录生成 `「小件件」你的小组件.enc.js` 文件 +> 该脚本需要`javascript-obfuscator`库,如未安装请先在项目目录 `npm install` + + + diff --git a/Scripts/「小件件」开发环境.js b/Scripts/「小件件」开发环境.js new file mode 100644 index 0000000..b5603c1 --- /dev/null +++ b/Scripts/「小件件」开发环境.js @@ -0,0 +1,1092 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: deep-gray; icon-glyph: code-branch; +// +// 「小件件」 +// 开发环境,用于小组件调用 +// https://x.im3x.cn +// https://github.com/im3x/Scriptables +// + +// 组件基础类 +const RUNTIME_VERSION = 20201209 + +class Base { + constructor(arg = "") { + this.arg = arg + this._actions = {} + this.init() + } + + init(widgetFamily = config.widgetFamily) { + // 组件大小:small,medium,large + this.widgetFamily = widgetFamily + // 系统设置的key,这里分为三个类型: + // 1. 全局 + // 2. 不同尺寸的小组件 + // 3. 不同尺寸+小组件自定义的参数 + // 当没有key2时,获取key1,没有key1获取全局key的设置 + // this.SETTING_KEY = this.md5(Script.name()+'@'+this.widgetFamily+"@"+this.arg) + // this.SETTING_KEY1 = this.md5(Script.name()+'@'+this.widgetFamily) + this.SETTING_KEY = this.md5(Script.name()) + // 文件管理器 + // 提示:缓存数据不要用这个操作,这个是操作源码目录的,缓存建议存放在local temp目录中 + this.FILE_MGR = FileManager[module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() + // 本地,用于存储图片等 + this.FILE_MGR_LOCAL = FileManager.local() + this.BACKGROUND_KEY = this.FILE_MGR_LOCAL.joinPath(this.FILE_MGR_LOCAL.documentsDirectory(), `bg_${this.SETTING_KEY}.jpg`) + // this.BACKGROUND_KEY1 = this.FILE_MGR_LOCAL.joinPath(this.FILE_MGR_LOCAL.documentsDirectory(), `bg_${this.SETTING_KEY1}.jpg`) + // this.BACKGROUND_KEY2 = this.FILE_MGR_LOCAL.joinPath(this.FILE_MGR_LOCAL.documentsDirectory(), `bg_${this.SETTING_KEY2}.jpg`) + // // 插件设置 + this.settings = this.getSettings() + } + + /** + * 注册点击操作菜单 + * @param {string} name 操作函数名 + * @param {func} func 点击后执行的函数 + */ + registerAction(name, func) { + this._actions[name] = func.bind(this) + } + + /** + * 生成操作回调URL,点击后执行本脚本,并触发相应操作 + * @param {string} name 操作的名称 + * @param {string} data 传递的数据 + */ + actionUrl(name = '', data = '') { + let u = URLScheme.forRunningScript() + let q = `act=${encodeURIComponent(name)}&data=${encodeURIComponent(data)}&__arg=${encodeURIComponent(this.arg)}&__size=${this.widgetFamily}` + let result = '' + if (u.includes('run?')) { + result = `${u}&${q}` + } else { + result = `${u}?${q}` + } + return result + } + + /** + * 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的数据 + */ + 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 << t % 32, n[14 + (t + 64 >>> 9 << 4)] = t; + for (var c = 1732584193, f = -271733879, i = -1732584194, a = 271733878, h = 0; h < n.length; h += 16) c = l(r = c, e = f, o = i, u = a, n[h], 7, -680876936), a = l(a, c, f, i, n[h + 1], 12, -389564586), i = l(i, a, c, f, n[h + 2], 17, 606105819), f = l(f, i, a, c, n[h + 3], 22, -1044525330), c = l(c, f, i, a, n[h + 4], 7, -176418897), a = l(a, c, f, i, n[h + 5], 12, 1200080426), i = l(i, a, c, f, n[h + 6], 17, -1473231341), f = l(f, i, a, c, n[h + 7], 22, -45705983), c = l(c, f, i, a, n[h + 8], 7, 1770035416), a = l(a, c, f, i, n[h + 9], 12, -1958414417), i = l(i, a, c, f, n[h + 10], 17, -42063), f = l(f, i, a, c, n[h + 11], 22, -1990404162), c = l(c, f, i, a, n[h + 12], 7, 1804603682), a = l(a, c, f, i, n[h + 13], 12, -40341101), i = l(i, a, c, f, n[h + 14], 17, -1502002290), c = v(c, f = l(f, i, a, c, n[h + 15], 22, 1236535329), i, a, n[h + 1], 5, -165796510), a = v(a, c, f, i, n[h + 6], 9, -1069501632), i = v(i, a, c, f, n[h + 11], 14, 643717713), f = v(f, i, a, c, n[h], 20, -373897302), c = v(c, f, i, a, n[h + 5], 5, -701558691), a = v(a, c, f, i, n[h + 10], 9, 38016083), i = v(i, a, c, f, n[h + 15], 14, -660478335), f = v(f, i, a, c, n[h + 4], 20, -405537848), c = v(c, f, i, a, n[h + 9], 5, 568446438), a = v(a, c, f, i, n[h + 14], 9, -1019803690), i = v(i, a, c, f, n[h + 3], 14, -187363961), f = v(f, i, a, c, n[h + 8], 20, 1163531501), c = v(c, f, i, a, n[h + 13], 5, -1444681467), a = v(a, c, f, i, n[h + 2], 9, -51403784), i = v(i, a, c, f, n[h + 7], 14, 1735328473), c = g(c, f = v(f, i, a, c, n[h + 12], 20, -1926607734), i, a, n[h + 5], 4, -378558), a = g(a, c, f, i, n[h + 8], 11, -2022574463), i = g(i, a, c, f, n[h + 11], 16, 1839030562), f = g(f, i, a, c, n[h + 14], 23, -35309556), c = g(c, f, i, a, n[h + 1], 4, -1530992060), a = g(a, c, f, i, n[h + 4], 11, 1272893353), i = g(i, a, c, f, n[h + 7], 16, -155497632), f = g(f, i, a, c, n[h + 10], 23, -1094730640), c = g(c, f, i, a, n[h + 13], 4, 681279174), a = g(a, c, f, i, n[h], 11, -358537222), i = g(i, a, c, f, n[h + 3], 16, -722521979), f = g(f, i, a, c, n[h + 6], 23, 76029189), c = g(c, f, i, a, n[h + 9], 4, -640364487), a = g(a, c, f, i, n[h + 12], 11, -421815835), i = g(i, a, c, f, n[h + 15], 16, 530742520), c = m(c, f = g(f, i, a, c, n[h + 2], 23, -995338651), i, a, n[h], 6, -198630844), a = m(a, c, f, i, n[h + 7], 10, 1126891415), i = m(i, a, c, f, n[h + 14], 15, -1416354905), f = m(f, i, a, c, n[h + 5], 21, -57434055), c = m(c, f, i, a, n[h + 12], 6, 1700485571), a = m(a, c, f, i, n[h + 3], 10, -1894986606), i = m(i, a, c, f, n[h + 10], 15, -1051523), f = m(f, i, a, c, n[h + 1], 21, -2054922799), c = m(c, f, i, a, n[h + 8], 6, 1873313359), a = m(a, c, f, i, n[h + 15], 10, -30611744), i = m(i, a, c, f, n[h + 6], 15, -1560198380), f = m(f, i, a, c, n[h + 13], 21, 1309151649), c = m(c, f, i, a, n[h + 4], 6, -145523070), a = m(a, c, f, i, n[h + 11], 10, -1120210379), i = m(i, a, c, f, n[h + 2], 15, 718787259), f = m(f, i, a, c, n[h + 9], 21, -343485551), c = d(c, r), f = d(f, e), i = d(i, o), a = d(a, u); + return [c, f, i, a] + } + + function a(n) { + for (var t = "", r = 32 * n.length, e = 0; e < r; e += 8) t += String.fromCharCode(n[e >> 5] >>> e % 32 & 255); + return t + } + + function h(n) { + var t = []; + for (t[(n.length >> 2) - 1] = void 0, e = 0; e < t.length; e += 1) t[e] = 0; + for (var r = 8 * n.length, e = 0; e < r; e += 8) t[e >> 5] |= (255 & n.charCodeAt(e / 8)) << e % 32; + return t + } + + function e(n) { + for (var t, r = "0123456789abcdef", e = "", o = 0; o < n.length; o += 1) t = n.charCodeAt(o), e += r.charAt(t >>> 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 < o.length && (o = i(o, 8 * n.length)), r = 0; r < 16; r += 1) u[r] = 909522486 ^ o[r], c[r] = 1549556828 ^ o[r]; + return e = i(u.concat(h(t)), 512 + 8 * t.length), a(i(c.concat(e), 640)) + }(r(n), r(t)) + } + + function t(n, t, r) { + return t ? r ? u(t, n) : e(u(t, n)) : r ? o(n) : e(o(n)) + } + return t(str) + } + + + /** + * HTTP 请求接口 + * @param {string} url 请求的url + * @param {bool} json 返回数据是否为 json,默认 true + * @param {bool} useCache 是否采用离线缓存(请求失败后获取上一次结果), + * @return {string | json | null} + */ + async httpGet(url, json = true, useCache = false) { + let data = null + const cacheKey = this.md5(url) + if (useCache && Keychain.contains(cacheKey)) { + let cache = Keychain.get(cacheKey) + return json ? JSON.parse(cache) : cache + } + try { + let req = new Request(url) + data = await (json ? req.loadJSON() : req.loadString()) + } catch (e) {} + // 判断数据是否为空(加载失败) + if (!data && Keychain.contains(cacheKey)) { + // 判断是否有缓存 + let cache = Keychain.get(cacheKey) + return json ? JSON.parse(cache) : cache + } + // 存储缓存 + Keychain.set(cacheKey, json ? JSON.stringify(data) : data) + return data + } + + async httpPost(url, data) {} + + /** + * 获取远程图片内容 + * @param {string} url 图片地址 + * @param {bool} useCache 是否使用缓存(请求失败时获取本地缓存) + */ + async getImageByUrl(url, useCache = true) { + const cacheKey = this.md5(url) + const cacheFile = FileManager.local().joinPath(FileManager.local().temporaryDirectory(), cacheKey) + // 判断是否有缓存 + if (useCache && FileManager.local().fileExists(cacheFile)) { + return Image.fromFile(cacheFile) + } + try { + const req = new Request(url) + const img = await req.loadImage() + // 存储到缓存 + FileManager.local().writeImage(cacheFile, img) + return img + } catch (e) { + // 没有缓存+失败情况下,返回自定义的绘制图片(红色背景) + let ctx = new DrawContext() + ctx.size = new Size(100, 100) + ctx.setFillColor(Color.red()) + ctx.fillRect(new Rect(0, 0, 100, 100)) + return await ctx.getImage() + } + } + + /** + * 渲染标题内容 + * @param {object} widget 组件对象 + * @param {string} icon 图标地址 + * @param {string} title 标题内容 + * @param {bool|color} color 字体的颜色(自定义背景时使用,默认系统) + */ + async renderHeader(widget, icon, title, color = false) { + widget.addSpacer(10) + let header = widget.addStack() + header.centerAlignContent() + let _icon = header.addImage(await this.getImageByUrl(icon)) + _icon.imageSize = new Size(14, 14) + _icon.cornerRadius = 4 + header.addSpacer(10) + let _title = header.addText(title) + if (color) _title.textColor = color + _title.textOpacity = 0.7 + _title.font = Font.boldSystemFont(12) + widget.addSpacer(10) + return widget + } + + /** + * 获取截图中的组件剪裁图 + * 可用作透明背景 + * 返回图片image对象 + * 代码改自:https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9 + * @param {string} title 开始处理前提示用户截图的信息,可选(适合用在组件自定义透明背景时提示) + */ + async getWidgetScreenShot(title = null) { + // Generate an alert with the provided array of options. + async function generateAlert(message, options) { + + let alert = new Alert() + alert.message = message + + for (const option of options) { + alert.addAction(option) + } + + let response = await alert.presentAlert() + return response + } + + // 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() + } + + async function blurImage(img, style) { + const blur = 150 + const js = ` +var mul_table=[512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,289,287,285,282,280,278,275,273,271,269,267,265,263,261,259];var shg_table=[9,11,12,13,13,14,14,15,15,15,15,16,16,16,16,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24];function stackBlurCanvasRGB(id,top_x,top_y,width,height,radius){if(isNaN(radius)||radius<1)return;radius|=0;var canvas=document.getElementById(id);var context=canvas.getContext("2d");var imageData;try{try{imageData=context.getImageData(top_x,top_y,width,height)}catch(e){try{netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");imageData=context.getImageData(top_x,top_y,width,height)}catch(e){alert("Cannot access local image");throw new Error("unable to access local image data: "+e);return}}}catch(e){alert("Cannot access image");throw new Error("unable to access image data: "+e);}var pixels=imageData.data;var x,y,i,p,yp,yi,yw,r_sum,g_sum,b_sum,r_out_sum,g_out_sum,b_out_sum,r_in_sum,g_in_sum,b_in_sum,pr,pg,pb,rbs;var div=radius+radius+1;var w4=width<<2;var widthMinus1=width-1;var heightMinus1=height-1;var radiusPlus1=radius+1;var sumFactor=radiusPlus1*(radiusPlus1+1)/2;var stackStart=new BlurStack();var stack=stackStart;for(i=1;i>shg_sum;pixels[yi+1]=(g_sum*mul_sum)>>shg_sum;pixels[yi+2]=(b_sum*mul_sum)>>shg_sum;r_sum-=r_out_sum;g_sum-=g_out_sum;b_sum-=b_out_sum;r_out_sum-=stackIn.r;g_out_sum-=stackIn.g;b_out_sum-=stackIn.b;p=(yw+((p=x+radius+1)>shg_sum;pixels[p+1]=(g_sum*mul_sum)>>shg_sum;pixels[p+2]=(b_sum*mul_sum)>>shg_sum;r_sum-=r_out_sum;g_sum-=g_out_sum;b_sum-=b_out_sum;r_out_sum-=stackIn.r;g_out_sum-=stackIn.g;b_out_sum-=stackIn.b;p=(x+(((p=y+radiusPlus1) 0.5 ? d / (2 - max - min) : d / (max + min); + switch(max){ + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return [h, s, l]; + } + + function hslToRgb(h, s, l){ + var r, g, b; + + if(s == 0){ + r = g = b = l; // achromatic + }else{ + var hue2rgb = function hue2rgb(p, q, t){ + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; + } + + function lightBlur(hsl) { + + // Adjust the luminance. + let lumCalc = 0.35 + (0.3 / hsl[2]); + if (lumCalc < 1) { lumCalc = 1; } + else if (lumCalc > 3.3) { lumCalc = 3.3; } + const l = hsl[2] * lumCalc; + + // Adjust the saturation. + const colorful = 2 * hsl[1] * l; + const s = hsl[1] * colorful * 1.5; + + return [hsl[0],s,l]; + + } + + function darkBlur(hsl) { + + // Adjust the saturation. + const colorful = 2 * hsl[1] * hsl[2]; + const s = hsl[1] * (1 - hsl[2]) * 3; + + return [hsl[0],s,hsl[2]]; + + } + + // Set up the canvas. + const img = document.getElementById("blurImg"); + const canvas = document.getElementById("mainCanvas"); + + const w = img.naturalWidth; + const h = img.naturalHeight; + + canvas.style.width = w + "px"; + canvas.style.height = h + "px"; + canvas.width = w; + canvas.height = h; + + const context = canvas.getContext("2d"); + context.clearRect( 0, 0, w, h ); + context.drawImage( img, 0, 0 ); + + // Get the image data from the context. + var imageData = context.getImageData(0,0,w,h); + var pix = imageData.data; + + var isDark = "${style}" == "dark"; + var imageFunc = isDark ? darkBlur : lightBlur; + + for (let i=0; i < pix.length; i+=4) { + + // Convert to HSL. + let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]); + + // Apply the image function. + hsl = imageFunc(hsl); + + // Convert back to RGB. + const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); + + // Put the values back into the data. + pix[i] = rgb[0]; + pix[i+1] = rgb[1]; + pix[i+2] = rgb[2]; + + } + + // Draw over the old image. + context.putImageData(imageData,0,0); + + // Blur the image. + stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blur}); + + // Perform the additional processing for dark images. + if (isDark) { + + // Draw the hard light box over it. + context.globalCompositeOperation = "hard-light"; + context.fillStyle = "rgba(55,55,55,0.2)"; + context.fillRect(0, 0, w, h); + + // Draw the soft light box over it. + context.globalCompositeOperation = "soft-light"; + context.fillStyle = "rgba(55,55,55,1)"; + context.fillRect(0, 0, w, h); + + // Draw the regular box over it. + context.globalCompositeOperation = "source-over"; + context.fillStyle = "rgba(55,55,55,0.4)"; + context.fillRect(0, 0, w, h); + + // Otherwise process light images. + } else { + context.fillStyle = "rgba(255,255,255,0.4)"; + context.fillRect(0, 0, w, h); + } + + // Return a base64 representation. + canvas.toDataURL(); + ` + + // Convert the images and create the HTML. + let blurImgData = Data.fromPNG(img).toBase64String() + let html = ` + + + ` + + // Make the web view and get its return value. + let view = new WebView() + await view.loadHTML(html) + let returnValue = await view.evaluateJavaScript(js) + + // Remove the data type from the string and convert to data. + let imageDataString = returnValue.slice(22) + let imageData = Data.fromBase64String(imageDataString) + + // Convert to image and crop before returning. + let imageFromData = Image.fromData(imageData) + // return cropImage(imageFromData) + return imageFromData + } + + + // Pixel sizes and positions for widgets on all supported phones. + function phoneSizes() { + let phones = { + // 12 and 12 Pro + "2532": { + small: 474, + medium: 1014, + large: 1062, + left: 78, + right: 618, + top: 231, + middle: 819, + bottom: 1407 + }, + + // 11 Pro Max, XS Max + "2688": { + small: 507, + medium: 1080, + large: 1137, + left: 81, + right: 654, + top: 228, + middle: 858, + bottom: 1488 + }, + + // 11, XR + "1792": { + small: 338, + medium: 720, + large: 758, + left: 54, + right: 436, + top: 160, + middle: 580, + bottom: 1000 + }, + + + // 11 Pro, XS, X + "2436": { + small: 465, + medium: 987, + large: 1035, + left: 69, + right: 591, + top: 213, + middle: 783, + bottom: 1353 + }, + + // Plus phones + "2208": { + small: 471, + medium: 1044, + large: 1071, + left: 99, + right: 672, + top: 114, + middle: 696, + bottom: 1278 + }, + + // SE2 and 6/6S/7/8 + "1334": { + small: 296, + medium: 642, + large: 648, + left: 54, + right: 400, + top: 60, + middle: 412, + bottom: 764 + }, + + + // SE1 + "1136": { + small: 282, + medium: 584, + large: 622, + left: 30, + right: 332, + top: 59, + middle: 399, + bottom: 399 + }, + + // 11 and XR in Display Zoom mode + "1624": { + small: 310, + medium: 658, + large: 690, + left: 46, + right: 394, + top: 142, + middle: 522, + bottom: 902 + }, + + // Plus in Display Zoom mode + "2001": { + small: 444, + medium: 963, + large: 972, + left: 81, + right: 600, + top: 90, + middle: 618, + bottom: 1146 + } + } + return phones + } + + var message + message = title || "开始之前,请先前往桌面,截取空白界面的截图。然后回来继续" + let exitOptions = ["我已截图", "前去截图 >"] + let shouldExit = await 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 = "好像您选择的照片不是正确的截图,或者您的机型我们暂时不支持。点击确定前往社区讨论" + let _id = await generateAlert(message, ["帮助", "取消"]) + if (_id === 0) Safari.openInApp('https://support.qq.com/products/287371', false) + return + } + + // Prompt for widget size and position. + message = "截图中要设置透明背景组件的尺寸类型是?" + let sizes = ["小尺寸", "中尺寸", "大尺寸"] + let size = await 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 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 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 generateAlert(message, positions) + + // Large widgets at the bottom have the "middle" y-value. + crop.y = position ? phone.middle : phone.top + } + + // 透明/模糊选项 + message = "需要给背景图片加什么显示效果?" + let blurOptions = ["透明", "白色 模糊", "黑色 模糊"] + let blurred = await generateAlert(message, blurOptions) + + // Crop image and finalize the widget. + if (blurred) { + const style = (blurred === 1) ? 'light' : 'dark' + img = await blurImage(img, style) + } + let imgCrop = cropImage(img, new Rect(crop.x, crop.y, crop.w, crop.h)) + + + return imgCrop + + } + + /** + * 弹出一个通知 + * @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) { + 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'])) + + let res = await ctx.getImage() + return res + } + + /** + * 获取当前插件的设置 + * @param {boolean} json 是否为json格式 + */ + getSettings(json = true) { + let res = json ? {} : "" + let cache = "" + // if (global && Keychain.contains(this.SETTING_KEY2)) { + // cache = Keychain.get(this.SETTING_KEY2) + // } else if (Keychain.contains(this.SETTING_KEY)) { + // cache = Keychain.get(this.SETTING_KEY) + // } else if (Keychain.contains(this.SETTING_KEY1)) { + // cache = Keychain.get(this.SETTING_KEY1) + // } else if (Keychain.contains(this.SETTING_KEY2)){ + 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 + } + + /** + * 存储当前设置 + * @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("设置成功", "桌面组件稍后将自动刷新") + } + + /** + * 获取当前插件是否有自定义背景图片 + * @reutrn img | false + */ + getBackgroundImage() { + // 如果有KEY则优先加载,key>key1>key2 + // key2是全局 + let result = null + if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY)) { + result = Image.fromFile(this.BACKGROUND_KEY) + // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY1)) { + // result = Image.fromFile(this.BACKGROUND_KEY1) + // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY2)) { + // result = Image.fromFile(this.BACKGROUND_KEY2) + } + return result + } + + /** + * 设置当前组件的背景图片 + * @param {image} img + */ + setBackgroundImage(img, notify = true) { + if (!img) { + // 移除背景 + if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY)) { + this.FILE_MGR_LOCAL.remove(this.BACKGROUND_KEY) + // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY1)) { + // this.FILE_MGR_LOCAL.remove(this.BACKGROUND_KEY1) + // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY2)) { + // this.FILE_MGR_LOCAL.remove(this.BACKGROUND_KEY2) + } + if (notify) this.notify("移除成功", "小组件背景图片已移除,稍后刷新生效") + } else { + // 设置背景 + // 全部设置一遍, + this.FILE_MGR_LOCAL.writeImage(this.BACKGROUND_KEY, img) + // this.FILE_MGR_LOCAL.writeImage(this.BACKGROUND_KEY1, img) + // this.FILE_MGR_LOCAL.writeImage(this.BACKGROUND_KEY2, img) + if (notify) this.notify("设置成功", "小组件背景图片已设置!稍后刷新生效") + } + } + +} +// @base.end +// 运行环境 +// @running.start +const Running = async (Widget, default_args = "") => { + let M = null + // 判断hash是否和当前设备匹配 + if (config.runsInWidget) { + M = new Widget(args.widgetParameter || '') + const W = await M.render() + Script.setWidget(W) + Script.complete() + } else { + let { + act, + data, + __arg, + __size + } = args.queryParameters + M = new Widget(__arg || default_args || '') + if (__size) M.init(__size) + if (!act || !M['_actions']) { + // 弹出选择菜单 + const actions = M['_actions'] + const _actions = [ + async () => { + Safari.openInApp("https://support.qq.com/products/287371", false) + } + ] + const alert = new Alert() + alert.title = M.name + alert.message = M.desc + alert.addAction("反馈交流") + for (let _ in actions) { + alert.addAction(_) + _actions.push(actions[_]) + } + alert.addCancelAction("取消操作") + const idx = await alert.presentSheet() + if (_actions[idx]) { + const func = _actions[idx] + await func() + } + return + } + let _tmp = act.split('-').map(_ => _[0].toUpperCase() + _.substr(1)).join('') + let _act = `action${_tmp}` + if (M[_act] && typeof M[_act] === 'function') { + const func = M[_act].bind(M) + await func(data) + } + } +} +// @running.end + +// 测试环境 +const Testing = async (Widget, default_args = "") => { + let M = null + // 判断hash是否和当前设备匹配 + if (config.runsInWidget) { + M = new Widget(args.widgetParameter || '') + const W = await M.render() + Script.setWidget(W) + Script.complete() + } else { + let { + act, + data, + __arg, + __size + } = args.queryParameters + M = new Widget(__arg || default_args || '') + if (__size) M.init(__size) + if (!act || !M['_actions']) { + // 弹出选择菜单 + const actions = M['_actions'] + const _actions = [ + // 远程开发 + async () => { + // 1. 获取服务器ip + const a = new Alert() + a.title = "服务器 IP" + a.message = "请输入远程开发服务器(电脑)IP地址" + let xjj_debug_server = "192.168.1.3" + if (Keychain.contains("xjj_debug_server")) { + xjj_debug_server = Keychain.get("xjj_debug_server") + } + a.addTextField("server-ip", xjj_debug_server) + a.addAction("连接") + a.addCancelAction("取消") + const id = await a.presentAlert() + if (id === -1) return + const ip = a.textFieldValue(0) + // 保存到本地 + Keychain.set("xjj_debug_server", ip) + const server_api = `http://${ip}:5566` + // 2. 发送当前文件到远程服务器 + const SELF_FILE = module.filename.replace('「小件件」开发环境', Script.name()) + const req = new Request(`${server_api}/sync`) + req.method = "POST" + req.addFileToMultipart(SELF_FILE, "Widget", Script.name()) + try { + const res = await req.loadString() + if (res !== "ok") { + return M.notify("连接失败", res) + } + } catch (e) { + return M.notify("连接错误", e.message) + } + M.notify("连接成功", "编辑文件后保存即可进行下一步预览操作") + // 重写console.log方法,把数据传递到nodejs + const rconsole_log = async (data, t = 'log') => { + const _req = new Request(`${server_api}/console`) + _req.method = "POST" + _req.headers = { + 'Content-Type': 'application/json' + } + _req.body = JSON.stringify({ + t, + data + }) + return await _req.loadString() + } + const lconsole_log = console.log.bind(console) + const lconsole_warn = console.warn.bind(console) + const lconsole_error = console.error.bind(console) + console.log = d => { + lconsole_log(d) + rconsole_log(d, 'log') + } + console.warn = d => { + lconsole_warn(d) + rconsole_log(d, 'warn') + } + console.error = d => { + lconsole_error(d) + rconsole_log(d, 'error') + } + // 3. 同步 + while (1) { + let _res = "" + try { + const _req = new Request(`${server_api}/sync?name=${encodeURIComponent(Script.name())}`) + _res = await _req.loadString() + } catch (e) { + M.notify("停止调试", "与开发服务器的连接已终止") + break + } + if (_res === "stop") { + console.log("[!] 停止同步") + break + } else if (_res === "no") { + // console.log("[-] 没有更新内容") + } else if (_res.length > 0) { + M.notify("同步成功", "新文件已同步,大小:" + _res.length) + // 重新加载组件 + // 1. 读取当前源码 + const _code = _res.split('// @组件代码开始')[1].split('// @组件代码结束')[0] + // 2. 解析 widget class + let NewWidget = null + try { + const _func = new Function(`const _Debugger = Base => {\n${_code}\nreturn Widget\n}\nreturn _Debugger`) + NewWidget = _func()(Base) + } catch (e) { + M.notify("解析失败", e.message) + } + if (!NewWidget) continue; + // 3. 重新执行 widget class + delete M; + M = new NewWidget(__arg || default_args || '') + if (__size) M.init(__size) + // 写入文件 + FileManager.local().writeString(SELF_FILE, _res) + // 执行预览 + let i = await _actions[1](true) + if (i === (4 + Object.keys(actions).length)) break + } + } + }, + // 预览组件 + async (debug = false) => { + let a = new Alert() + a.title = "预览组件" + a.message = "测试桌面组件在各种尺寸下的显示效果" + a.addAction("小尺寸 Small") + a.addAction("中尺寸 Medium") + a.addAction("大尺寸 Large") + a.addAction("全部 All") + a.addCancelAction("取消操作") + const funcs = [] + if (debug) { + for (let _ in actions) { + a.addAction(_) + funcs.push(actions[_].bind(M)) + } + a.addDestructiveAction("停止调试") + } + let i = await a.presentSheet() + if (i === -1) return + let w + switch (i) { + case 0: + M.widgetFamily = 'small' + w = await M.render() + await w.presentSmall() + break; + case 1: + M.widgetFamily = 'medium' + w = await M.render() + await w.presentMedium() + break + case 2: + M.widgetFamily = 'large' + w = await M.render() + await w.presentLarge() + break + case 3: + M.widgetFamily = 'small' + w = await M.render() + await w.presentSmall() + M.widgetFamily = 'medium' + w = await M.render() + await w.presentMedium() + M.widgetFamily = 'large' + w = await M.render() + await w.presentLarge() + break + default: + const func = funcs[i - 4]; + if (func) await func(); + break; + } + + return i + }, + // 复制源码 + async () => { + const SELF_FILE = module.filename.replace('「小件件」开发环境', Script.name()) + const source = FileManager.local().readString(SELF_FILE) + Pasteboard.copyString(source) + await M.notify("复制成功", "当前脚本的源代码已复制到剪贴板!") + }, + async () => { + Safari.openInApp("https://www.kancloud.cn/im3x/scriptable/content", false) + }, + async () => { + Safari.openInApp("https://support.qq.com/products/287371", false) + } + ] + const alert = new Alert() + alert.title = M.name + alert.message = M.desc + alert.addAction("远程开发") + alert.addAction("预览组件") + alert.addAction("复制源码") + alert.addAction("开发文档") + alert.addAction("反馈交流") + for (let _ in actions) { + alert.addAction(_) + _actions.push(actions[_]) + } + alert.addCancelAction("取消操作") + const idx = await alert.presentSheet() + if (_actions[idx]) { + const func = _actions[idx] + await func() + } + return + } + let _tmp = act.split('-').map(_ => _[0].toUpperCase() + _.substr(1)).join('') + let _act = `action${_tmp}` + if (M[_act] && typeof M[_act] === 'function') { + const func = M[_act].bind(M) + await func(data) + } + } +} + +module.exports = { + Base, + Testing, + Running, +} + +// 自更新 +// 流程: +// 1. 获取远程gitee仓库的本文件代码 +// 2. 对比sha,如果和本地存储的不一致,则下载 +// 3. 下载保存,存储sha +// 4. 更新时间为每小时一次 +// +; +(async () => { + const UPDATE_KEY = "XJJ_UPDATE_AT" + let UPDATED_AT = 0 + const UPDATE_FILE = '「小件件」开发环境.js' + const FILE_MGR = FileManager[module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() + if (Keychain.contains(UPDATE_KEY)) { + UPDATED_AT = parseInt(Keychain.get(UPDATE_KEY)) + } + if (UPDATED_AT > (+new Date - 1000 * 60 * 60)) return console.warn('[-] 1 小时内已检查过更新') + console.log('[*] 检测开发环境是否有更新..') + const req = new Request('https://gitee.com/im3x/Scriptables/raw/v2-dev/package.json') + const res = await req.loadJSON() + console.log(`[+] 远程开发环境版本:${res['runtime_ver']}`) + if (res['runtime_ver'] === RUNTIME_VERSION) return console.warn('[-] 远程版本一致,暂无更新') + console.log('[+] 开始更新开发环境..') + const REMOTE_REQ = new Request('https://gitee.com/im3x/Scriptables/raw/v2-dev/Scripts/%E3%80%8C%E5%B0%8F%E4%BB%B6%E4%BB%B6%E3%80%8D%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83.js') + const REMOTE_RES = await REMOTE_REQ.load() + FILE_MGR.write(FILE_MGR.joinPath(FILE_MGR.documentsDirectory(), UPDATE_FILE), REMOTE_RES); + const n = new Notification() + n.title = "更新成功" + n.body = "「小件件」开发环境已自动更新!" + n.schedule() + UPDATED_AT = +new Date + Keychain.set(UPDATE_KEY, String(UPDATED_AT)) +})() diff --git a/Scripts/「源码」V2EX 社区.js b/Scripts/「源码」V2EX 社区.js new file mode 100644 index 0000000..26055f3 --- /dev/null +++ b/Scripts/「源码」V2EX 社区.js @@ -0,0 +1,372 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: deep-green; icon-glyph: angle-double-right; +// +// iOS 桌面组件脚本 @「小件件」 +// 开发说明:请从 Widget 类开始编写,注释请勿修改 +// https://x.im3x.cn +// + +// 添加require,是为了vscode中可以正确引入包,以获得自动补全等功能 +if (typeof require === 'undefined') require = importModule +const { Base } = require("./「小件件」开发环境") + +// @组件代码开始 +class Widget extends Base { + constructor (arg) { + super(arg) + this.logo = "https://www.v2ex.com/static/img/icon_rayps_64.png" + this.name = "V2EX" + this.desc = "创意工作者们的社区" + // 请求数据接口列表(收集整理中) + this.API = [ + // api + [ + { + id: 'latest', + name: '最新' + }, { + id: 'hot', + name: '最热' + } + ], + // tab + [ + { + id: 'all', + name: '全部' + }, { + id: 'hot', + name: '最热' + }, { + id: 'tech', + name: '技术' + }, { + id: 'creative', + name: '创意' + }, { + id: 'playplay', + name: '好玩' + }, { + id: 'apple', + name: 'Apple' + }, { + id: 'jobs', + name: '酷工作' + }, { + id: 'deals', + name: '交易' + }, { + id: 'city', + name: '城市' + }, { + id: 'qna', + name: '问与答' + } + ], + // go + [], + ] + // 当前设置的存储key(提示:可通过桌面设置不同参数,来保存多个设置) + let _md5 = this.md5(module.filename) + this.CACHE_KEY = `cache_${_md5}` + // 获取设置 + // 格式:type@name,比如 go@create、api@hot、tab@all + this.SETTINGS = this.settings['node'] || 'tab@all' + + // 注册操作菜单 + this.registerAction("节点设置", this.actionSettings) + } + + // 渲染组件 + async render () { + // 加载节点列表 + await this._loadNodes() + const data = await this.getData() + console.log(data) + if (this.widgetFamily === 'medium') { + return await this.renderMedium(data) + } else if (this.widgetFamily === 'large') { + return await this.renderLarge(data) + } else { + return await this.renderSmall(data) + } + } + async renderSmall (data) { + let w = new ListWidget() + let topic = data[0] + w.url = this.actionUrl('open-url', topic['url']) + w = await this.renderHeader(w, this.logo, this.name + ' / ' + this.SETTINGS.split('@')[0], Color.white()) + let content = w.addText(topic['title']) + content.font = Font.lightSystemFont(16) + content.textColor = Color.white() + content.lineLimit = 3 + + w.backgroundImage = await this.shadowImage(await this.getImageByUrl(topic['member']['avatar_large'].replace('mini', 'large'))) + + w.addSpacer() + let footer = w.addText(`@${topic['member']['username']} / ${topic['node']['title']}`) + footer.font = Font.lightSystemFont(10) + footer.textColor = Color.white() + footer.textOpacity = 0.5 + footer.lineLimit = 1 + return w + } + // 中尺寸组件 + async renderMedium (data) { + let w = new ListWidget() + // w.addSpacer(10) + // 设置名称 + let tmp = this.SETTINGS.split('@') + let tid = tmp[0] === 'api' ? 0 : (tmp[0] === 'tab' ? 1 : 2) + let current = '' + this.API[tid].map(a => { + if (a['id'] === tmp[1]) current = a['name'] + }) + await this.renderHeader(w, this.logo, this.name + ' / ' + current) + w.addSpacer() + + let body = w.addStack() + let bodyleft= body.addStack() + bodyleft.layoutVertically() + for (let i = 0; i < 2; i ++) { + bodyleft = await this.renderCell(bodyleft, data[i]) + bodyleft.addSpacer() + } + // body.addSpacer() + w.url = this.actionUrl("settings") + + return w + } + // 大尺寸组件 + async renderLarge (data) { + let w = new ListWidget() + // w.addSpacer(10) + // 设置名称 + let tmp = this.SETTINGS.split('@') + let tid = tmp[0] === 'api' ? 0 : (tmp[0] === 'tab' ? 1 : 2) + let current = '' + this.API[tid].map(a => { + if (a['id'] === tmp[1]) current = a['name'] + }) + await this.renderHeader(w, this.logo, this.name + ' / ' + current) + w.addSpacer() + + + let body = w.addStack() + let bodyleft= body.addStack() + bodyleft.layoutVertically() + for (let i = 0; i < 5; i ++) { + bodyleft = await this.renderCell(bodyleft, data[i]) + bodyleft.addSpacer() + } + // body.addSpacer() + + // w.addSpacer() + w.url = this.actionUrl("settings") + + return w + } + async renderCell (widget, topic) { + let body = widget.addStack() + body.url = this.actionUrl('open-url', topic['url']) + + let left = body.addStack() + let avatar = left.addImage(await this.getImageByUrl(topic['member']['avatar_large'].replace('mini', 'large'))) + avatar.imageSize = new Size(35, 35) + avatar.cornerRadius = 5 + + body.addSpacer(10) + + let right = body.addStack() + right.layoutVertically() + let content = right.addText(topic['title']) + content.font = Font.lightSystemFont(14) + content.lineLimit = 1 + + right.addSpacer(5) + + let info = right.addText(`@${topic['member']['username']} / ${topic['node']['title']}`) + info.font = Font.lightSystemFont(10) + info.textOpacity = 0.6 + info.lineLimit = 2 + + widget.addSpacer() + + return widget + } + + async getData () { + // 解析设置,判断类型,获取对应数据 + const tmp = this.SETTINGS.split('@') + switch (tmp[0]) { + case 'tab': + return await this.getDataForTab(tmp[1]) + case 'go': + return await this.getDataForGo(tmp[1]) + case 'api': + return await this.getDataForApi(tmp[1]) + } + } + + /** + * 获取首页tab数据 + * @param {string} tab tab首页名称 + */ + async getDataForTab (tab = 'all') { + let url = `https://www.v2ex.com/?tab=${tab}` + let html = await this.fetchAPI(url, false) + + // 解析html + let tmp = html.split(`
`)[1].split(`
`)[0] + let arr = tmp.split('
') + arr.shift() + + let datas = [] + for (let i = 0; i < arr.length; i ++) { + let t = arr[i] + let title = t.split(`class="topic-link">`)[1].split('')[0].split('>')[1] + let user = t.split(``)[1].split('class="')[0].split('"')[1] + datas.push({ + title, + url: `https://www.v2ex.com${link}`, + member: { + username: user, + 'avatar_large': avatar + }, + node: { + title: node + } + }) + } + + return datas + } + + async getDataForApi (api) { + return await this.httpGet(`https://www.v2ex.com/api/topics/${api}.json`) + } + + /** + * 加载数据 + * 节点列表 + */ + async getDataForGo (arg = 'create') { + let url = `https://www.v2ex.com/go/${arg}` + let html = await this.fetchAPI(url, false) + + // 解析html + let tmp = html.split(`
`)[1].split(`