├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── config.md ├── questions.md ├── quick-start.md └── widget-element.md ├── nodemon.json ├── package.json ├── prettier.config.js ├── scriptable.config.js ├── src ├── index.ts ├── lib │ ├── basic.ts │ ├── compile.ts │ ├── components │ │ ├── index.ts │ │ └── layout.tsx │ ├── constants.ts │ ├── env.ts │ ├── help.ts │ ├── jsx-runtime.ts │ ├── server.ts │ └── static │ │ ├── dev-help.html │ │ └── 基础包.js ├── scripts │ ├── bili.tsx │ ├── china10010.tsx │ ├── funds.tsx │ ├── helloWorld.tsx │ ├── launcher.tsx │ ├── music163.tsx │ ├── newsTop.tsx │ └── yiyan.tsx └── types │ ├── JSX.d.ts │ ├── index.d.ts │ └── widget │ ├── index.d.ts │ ├── wbox.d.ts │ ├── wdate.d.ts │ ├── wimage.d.ts │ ├── wspacer.d.ts │ ├── wstack.d.ts │ └── wtext.d.ts ├── tsconfig.json └── 打包好的成品 ├── install.json ├── 一言.js ├── 个性磁贴启动器.js ├── 今日热榜.js ├── 哔哩粉丝.js ├── 基金小部件.js ├── 网易云歌单.js └── 联通流量话费小部件.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | HELLO = '你好 env' 2 | MOMENT = '这是我 .env 独享的 moment' 3 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | HELLO = '你好 development' 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | HELLO = '你好 production' 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | module.exports = /** @type { import('eslint').Linter.Config } */ ({ 4 | root: true, 5 | env: { 6 | node: true, 7 | es6: true, 8 | browser: true, 9 | }, 10 | // 指定 ESLint parser 11 | parser: '@typescript-eslint/parser', 12 | extends: [ 13 | // 使用 @typescript-eslint/eslint-plugin 中推荐的规则 14 | 'plugin:@typescript-eslint/recommended', 15 | // 使用 eslint-config-prettier 来禁止 @typescript-eslint/eslint-plugin 中那些和 prettier 冲突的规则 16 | 'prettier/@typescript-eslint', 17 | // 确保下面这行配置是这个数组里的最后一行配置 18 | 'plugin:prettier/recommended', 19 | ], 20 | rules: { 21 | 'import/no-unresolved': 'off', 22 | 'no-unused-vars': 'off', 23 | '@typescript-eslint/no-unused-vars': ['warn', {varsIgnorePattern: '^(h|MODULE|Fragment){1}$'}], 24 | '@typescript-eslint/no-this-alias': 'off', 25 | '@typescript-eslint/triple-slash-reference': 'off', 26 | '@typescript-eslint/no-var-requires': 'off', 27 | }, 28 | parserOptions: { 29 | parser: '@typescript-eslint/parser', 30 | ecmaversion: 2015, // Allows for the parsing of modern ECMAScript features 31 | sourceType: 'module', // Allows for the use of imports 32 | ecmaFeatures: { 33 | tsx: true, 34 | }, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 保存时使用VSCode 自身格式化程序格式化 3 | "editor.formatOnSave": true, 4 | // 启用eslint 5 | "eslint.enable": true, 6 | // 保存时用eslint格式化 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true 9 | }, 10 | // 两者会在格式化js时冲突,所以需要关闭默认js格式化程序 11 | "javascript.format.enable": false, 12 | "html.format.enable": false, 13 | "json.format.enable": false, 14 | "typescript.format.enable": false 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present, Yang xiaoming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ios-scriptable-tsx logo

2 | 3 |

4 | 5 | Version 6 | 7 | Github stars 8 | 9 | Issues 10 | 11 | Repo size 12 | 13 | Language 14 | 15 | Last commit date 16 | 17 | License 18 | 19 | Keywords 20 |

21 |

ios-scriptable-tsx

22 | 23 |

24 | 25 | ## 介绍 26 | 27 | 本项目旨在给 `Scriptable` 开发者提供舒适的开发体验, `Scriptable` 是 ios 上一个用 js 开发桌面小组件的 app ,如果你还没安装,可以[点我下载](https://apps.apple.com/us/app/scriptable/id1405459188) 28 | 29 |
30 | 31 | `ios-scriptable-tsx` 是一个二次封装 `Scriptable` 官方 api 的开发框架,它具有以下特点: 32 | 33 |
34 | 35 | **1. 支持在 pc 开发,支持实时监听修改、编译同步到手机运行。** 36 | 37 |
38 | 39 | **2. 支持使用 typescript 和 tsx 开发小组件,支持 api 类型提示、自动补全。** 40 | 41 |
42 | 43 | **3. 支持打包混淆、加密 js。** 44 | 45 |
46 | 47 | **4. 支持远程输出 console 日志到 pc 的命令窗口。** 48 | 49 |
50 | 51 | **5. 支持环境变量定义,打包时自动替换环境变量为预设值。** 52 | 53 |
54 | 55 | **6. 常用函数封装,使用更便捷。** 56 | 57 |
58 | 59 | ## 使用 60 | 61 | 1. 先克隆本仓库 62 | 63 | ```bash 64 | git clone https://github.com/2214962083/ios-scriptable-tsx.git 65 | ``` 66 | 67 |
68 | 69 | 2. 进入到 `ios-scriptable-tsx` 目录里,执行 `npm install` 安装依赖 70 | 71 | 72 |
73 | 74 | 3. 打开 vscode 愉快开发,打包入口文件默认是 `./src/index.ts` 75 | 76 | 77 |
78 | 79 | 4. 执行 `npm run build` 打包到 `./dist` 文件夹 80 | 81 |
82 | 83 | ## 文档 84 | - [快速上手](./docs/quick-start.md#quick-start) 85 | - [命令说明](./docs/quick-start.md#command-introduction) 86 | - [项目目录说明和配置指南](./docs/config.md#config-introduction) 87 | - [项目目录说明](./docs/config.md#project-dir-introduction) 88 | - [打包配置](./docs/config.md#scriptable-config) 89 | - [其他配置](./docs/config.md#others-config) 90 | - [环境变量](./docs/config.md#env-config) 91 | - [JSX 组件](./docs/widget-element.md#jsx-element) 92 | - [wbox 盒子组件](./docs/widget-element.md#wbox) 93 | - [wstack 容器组件](./docs/widget-element.md#wstack) 94 | - [wimage 图片组件](./docs/widget-element.md#wimage) 95 | - [wspacer 空格占位组件](./docs/widget-element.md#wspacer) 96 | - [wtext 文字组件](./docs/widget-element.md#wtext) 97 | - [wdate 日期组件](./docs/widget-element.md#wdate) 98 | - [常用函数封装](https://github.com/2214962083/ios-scriptable-tsx/blob/master/src/lib/help.ts) 99 | - [常见问题](./docs/questions.md) 100 | - [Scriptable 官方 api 文档](https://docs.scriptable.app/)

101 | 102 | ## 讨论 103 | 104 | - [Scriptable 官方论坛](https://talk.automators.fm/c/scriptable/13)

105 | 106 | ## 项目参考 107 | 108 | - [「小件件」开发框架](https://github.com/im3x/Scriptables) 109 | - [Scriptable](https://github.com/dompling/Scriptable) 110 | - [Transparent-Scriptable-Widget](https://github.com/mzeryck/Transparent-Scriptable-Widget)

111 | 112 | ## 许可证 113 | 114 | [MIT](https://opensource.org/licenses/MIT) 115 | 116 | Copyright (c) 2020-present, Yang xiaoming 117 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # 项目目录说明和配置指南 2 | 3 |
4 | 5 | ## 项目目录说明 6 | 7 | ``` 8 | ios-scriptable-tsx 9 | ├── .vscode // vscode 配置 10 | ├── 打包好的成品 // 可以粘贴到 scriptable 运行的成品 11 | ├── docs // 文档 12 | ├── src // 代码目录 13 | │ ├── lib // 本项目主要文件存放处,请勿挪动 14 | │ │ └── static // 存放开发引导文件的文件夹 15 | │ │ └── basic // 基础包源码 16 | │ │ └── compile.ts // 打包脚本,请勿随意更改 17 | │ │ └── constants.ts // 常量 18 | │ │ └── env.ts // 加载 dotenv 文件的脚本 19 | │ │ └── help.ts // 常用 api 函数封装 20 | │ │ └── jsx-runtime.ts // tsx、jsx 编译后解释回 scriptable api 的文件 21 | │ │ └── server.ts // 开启监听服务器,用于手机代码同步 22 | │ ├── scripts // 开发者存放小组件源代码的目录 23 | │ └── types // 存放类型声明目录 24 | │ │ └── widget // 存放 jsx、tsx element 可接收参数类型的目录 25 | │ └── index.ts // 打包主入口文件 26 | ├── .editorconfig // 编辑器配置 27 | ├── .env // dotenv 文件,打包就加载进环境变量 28 | ├── .env.development // dotenv 文件,开发环境时,加载进环境变量 29 | ├── .env.production // dotenv 文件,生产环境时,加载进环境变量 30 | ├── .eslintrc.js // eslint 配置,统一代码风格 31 | ├── .gitignore // gitignore 文件 32 | ├── nodemon.json // nodemon监听配置,在 watch 下生效 33 | ├── package.json // 项目信息、依赖声明文件 34 | ├── prettier.config.js // prettier 配置,用于美化、对齐代码 35 | ├── README.md // 文档 36 | ├── scriptable.config.js // scriptable 打包配置 37 | └── tsconfig.json // TypeScript 编译选项 38 | ``` 39 | 40 |
41 | 42 | ## scriptable.config.js 打包配置 43 | 44 | | 属性 | 类型 | 必填 | 默认 | 描述 | 45 | |----------------|---------|----|----------------------|------------------------------| 46 | | rootPath | string | 否 | \./ | 项目根目录, 不建议修改 | 47 | | inputFile | string | 否 | \./src/index\.ts | 输入文件,当执行编译时生效,不建议修改 | 48 | | inputDir | string | 否 | \./src/scripts/ | 输入文件夹,当执行批量编译时生效,不建议修改 | 49 | | outputDir | string | 否 | \./dist/ | 输出文件夹,不建议修改 | 50 | | minify | boolean | 否 | 开发环境为false,生存环境为true | 是否压缩代码 | 51 | | encrypt | boolean | 否 | 开发环境为false,生存环境为true | 是否加密代码 | 52 | | header | string | 否 | | 往编译后的代码头部插入的代码(一般是作者信息) | 53 | | [esbuild](https://esbuild.github.io/api/#simple-options) | object | 否 | | esbuild 自定义配置 | 54 | | [encryptOptions](https://github.com/javascript-obfuscator/javascript-obfuscator) | object | 否 | | javascript\-obfuscator 自定义配置(加密代码配置) | 55 | 56 |
57 | 58 | ## 其他配置 59 | 60 | | 配置文件名 | 用途 | 61 | | -------------------------------------------------------- | ------------------------------ | 62 | | [.editorconfig](http://editorconfig.org) | 统一各个编辑器编辑风格(可删) | 63 | | [.eslintrc.js](https://cn.eslint.org/) | 定义代码规范(可删) | 64 | | [prettier.config.js](https://prettier.io) | 自动对齐、美化代码用(可删) | 65 | | [tsconfig.json](https://www.typescriptlang.org/tsconfig) | typescript配置文件 (不可删) | 66 | 67 |
68 | 69 | ## 环境变量配置 70 | 71 | `ios-scriptable-tsx` 提供两个环境模式,开发环境 `development` 模式和生产环境 `production`模式 ,你可以用代码 `process.env.NODE_ENV` 获取到这个值。你可以在项目根目录下的 `package.json` 文件里的 `scripts` 看到他们是怎么传进去的。 72 | 73 | ```bash 74 | npm run watch #development开发环境 75 | npm run dev #development开发环境 76 | npm run dev:all #development开发环境 77 | npm run build #production生产环境 78 | npm run build:all #production生产环境 79 | ``` 80 | 81 |
82 | 83 | **本项目集成了[dotenv](https://github.com/motdotla/dotenv),你可以替换你的项目根目录中的下列文件来指定环境变量(如果你用过 [vue-cli](https://cli.vuejs.org/zh/guide/mode-and-env.html#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E5%92%8C%E6%A8%A1%E5%BC%8F) 你会很熟悉它):** 84 | 85 | ``` 86 | .env # 在所有的环境中被载入 87 | .env.local # 在所有的环境中被载入,但会被 git 忽略 88 | .env.[mode] # 只在指定的模式中被载入 89 | .env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略 90 | ``` 91 | 92 |
93 | 94 | 一个环境文件只包含环境变量的“键=值”对: 95 | 96 | ``` 97 | FOO=bar 98 | HELLO=你好 99 | ``` 100 | 101 | **环境变量将会载入挂载到 `process.env `上。例如在打包时,`process.env.FOO`将会被替换成字符串 `bar` ,`process.env.HELLO`将会被替换成字符串`你好`。被载入的变量将会对./src 目录下的所有代码可用。** 102 | 103 |
当为`development`开发环境打包时,下面的文件会被依次载入: 104 | 105 | ``` 106 | .env 107 | .env.local 108 | .env.development 109 | .env.development.local 110 | ``` 111 | 112 |
113 | 114 | 当为`production`生产环境打包时,下面的文件会被依次载入: 115 | 116 | ``` 117 | .env 118 | .env.local 119 | .env.production 120 | .env.production.local 121 | ``` 122 | 123 |
124 | -------------------------------------------------------------------------------- /docs/questions.md: -------------------------------------------------------------------------------- 1 | ## 常见问题 2 | 3 |
4 | 5 | **A、为什么执行 `npm run watch` 同步后,修改编译但还是不同步?** 6 | 7 | 1、检查你的同步地址有没有填错 8 | 9 | 2、同步功能依赖基础包轮询 pc 的 server。如果你在基础包运行的时候,运行了其他脚本, `Scriptable` 就会终止基础包的进程。
10 | 11 | 此时再点击基础包执行同步就好,目前无解,因为 `Scriptable` 只能保持一个脚本在运行。
12 | 13 | 虽然可以通过注入同步代码到编译后的脚本来解决,但是会引入额外的代码干扰。本框架遵循一致性原则,编译是啥,同步过去就是啥。
14 | 15 | 所见即所得,不额外引入复杂度。即便断开同步也能如期运行。
16 | 17 |
18 | 19 |
20 | 21 | **B、如何停止同步代码?** 22 | 23 | 代码同步轮询,请求错误到一定次数就会断开,不用手动停止。 24 | 25 |
26 | 27 |
28 | 29 | **C、可以用 js 或 jsx 开发吗,without the typescript ?** 30 | 31 | 可以,只要你的代码在 `./src/index.ts` 引入就能打包。 32 | 33 |
34 | 35 |
36 | 37 | **D、为什么小部件偶尔会出现下面这种状况?** 38 | 39 | 小部件出错 40 | 41 | 这种状况是因为 `Scriptable` 内部处理多个小部件异步渲染时没处理好。导致部分小部件渲染为空。([在此可以看到相关讨论](https://talk.automators.fm/t/hellp-call-script-setwidget-to-set-the-content-of-the-widget-run-in-widget/9615/7)) 42 | 43 | 解决方法也很简单,在渲染时最外层加个 await。由于打包器不支持 `top-level-await`,所以本框架内置了一个末尾底部等待的函数 `EndAwait` ,使用方法见下 44 | 45 | ```tsx 46 | Class HelloWorld { 47 | async init() { 48 | .... 49 | } 50 | async render() { 51 | ..... 52 | } 53 | } 54 | 55 | // 使用前 56 | // new HelloWorld().init() 57 | 58 | // 使用后,这样就不会出现以上状况了 59 | EndAwait(() => new HelloWorld().init()) 60 | ``` 61 | 62 |
63 | 64 |
65 | 66 | **E、为什么 jsx widget 是异步渲染?** 67 | 68 | 为了方便引入网络资源,比如图片,实现 `wimage` 的 src 填写网络连接就能自动加载图片,是需要异步等待的。所以渲染小部件,返回 ListWidget 实例也是异步。 69 | 70 |
71 | 72 |
73 | 74 | **F、基础包同步运行时报错,而同步后,直接运行所同步的脚本却没报错?** 75 | 76 | 可能是基础包版本过旧原因,再扫二维码进引导页重新安装一下基础包即可。 77 | 78 |
79 | 80 |
-------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | ## 快速上手 2 | 3 |
4 | 5 | 1. 克隆本项目: 6 | 7 | ```bash 8 | git clone https://github.com/2214962083/ios-scriptable-tsx.git 9 | ``` 10 | 11 |
12 | 13 | 2. 进入 `ios-scriptable-tsx` 目录,执行 `npm install` 安装依赖 14 | 15 |
16 | 17 | 3. 在 `./src/scripts` 目录下新创建一个文件叫 `helloWorld.tsx` 18 | 19 |
20 | 21 | 4. 把下面代码粘贴上去: 22 | 23 | ```tsx 24 | class HelloWorld { 25 | async init() { 26 | // ListWidget 实例 27 | const widget = (await this.render()) as ListWidget 28 | // 注册小部件 29 | Script.setWidget(widget) 30 | // 调试用 31 | !config.runsInWidget && (await widget.presentMedium()) 32 | // 脚本结束 33 | Script.complete() 34 | } 35 | async render(): Promise { 36 | return ( 37 | 38 | 39 | 40 | Hello World 41 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | new HelloWorld().init() 49 | ``` 50 | 51 |
52 | 53 | 5. 然后,去 `./src/index.ts` 删除已有的东西,引入`helloWorld.tsx` 54 | 55 | ```tsx 56 | // 打包入口默认是 ./src/index.ts 57 | // 这样引入其他位置的脚本,就会打包进来 58 | import './scripts/helloWorld.tsx' 59 | ``` 60 | 61 |
62 | 63 | 6. 执行 `npm run watch`,并用手机扫描命令终端的二维码,打开引导页(**手机和 pc 必须处于同一个局域网下面**),我的是这样的: 64 | 65 | 服务同步地址展示 66 | 67 |
68 | 69 | 7. 根据引导页提示操作,**此举是用来安装基础包,若已安装基础包,无需再进引导页**: 70 | 71 | 开发者引导页 72 | 73 |
74 | 75 | 8. 在 `scriptable` 右上角 `+` 创建一个空脚本,命名为 `你好世界` : 76 | 77 | 创建你好世界脚本 78 | 79 |
80 | 81 | 9. 点击基础包,选择远程开发, 选择 `你好世界.js`: 82 | 83 | 选择远程开发 选择脚本 84 | 85 |
86 | 87 | 10. 输入同步地址,`http://IP地址:端口/打包后的文件` ,默认是 pc 上提示的链接加上 `/index.js`,同步成功,看到小部件预览效果了: 88 | 89 | 输入同步地址 同步成功 90 | 91 | 92 |
93 | 94 | 11. 长按桌面,点击添加 `scriptable` 小部件,然后长按小部件,点击编辑小部件 : 95 | 96 | 添加 scriptable 小部件 编辑小部件 97 | 98 | 99 |
100 | 101 | 12. `Script` 选择 `你好世界`,就能看到效果了,恭喜你完成新手教学 : 102 | 103 | 选择要显示的脚本 最终效果 104 | 105 | 106 |
107 | 108 |
109 | 110 | ## 命令说明 111 | 112 | | 命令 | 命令作用 | 补充说明 | 113 | | ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 114 | | `npm run dev` | 以开发环境打包单个文件 | `process.env.NODE_ENV = development`
默认不混淆代码、不加密代码
单文件打包入口默认是`./src/index.ts`
可在 [scriptable.config.js](./config.md#scriptable-config) 里配置 `inputFile` 以修改它的打包入口文件。 | 115 | | `npm run dev:all` | 以开发环境打包多个文件 | `process.env.NODE_ENV = development`
默认不混淆代码、不加密代码
多文件打包入口默认是`./src/scripts/`
可在 [scriptable.config.js](./config.md#scriptable-config) 里配置 `inputDir` 以修改它的打包入口文件夹。 | 116 | | `npm run build` | 以生产环境打包单个文件 | `process.env.NODE_ENV = production`
默认混淆代码、加密代码
单文件打包入口默认是`./src/index.ts`
可在 [scriptable.config.js](./config.md#scriptable-config) 里配置 `inputFile` 以修改它的打包入口文件。 | 117 | | `npm run build:all` | 以生产环境打包多个文件 | `process.env.NODE_ENV = production`
默认混淆代码、加密代码
多文件打包入口默认是`./src/scripts/`
可在 [scriptable.config.js](./config.md#scriptable-config) 里配置 `inputDir` 以修改它的打包入口文件夹。 | 118 | | `npm run watch` | 以开发环境打包单个文件
并开启 server ,监听文件修改实时编译 | 在执行 `npm run dev` 的基础上,开启了本地 9090 端口的 server
server 会自动映射打包后的文件夹到 `http:// 127.0.0.1:9090/`
默认是 `./dist` 文件夹,可在 [scriptable.config.js](./config.md#scriptable-config) 的 `outputDir` 配置它。 | 119 | 120 | 所有打包输出文件夹默认都是 `./dist`,你可以在 [scriptable.config.js](./config.md#scriptable-config) 里配置 `outputDir` 以修改它的打包输出的文件夹 121 | 122 |
123 | 124 | ## 注意 125 | 126 | 由于打包器保留字 `module` 和 `Scriptable` 的全局变量 `module` 冲突,所以建议在访问 `Scriptable`  `module` 变量时,改用 `MODULE` 访问。 127 | 128 | -------------------------------------------------------------------------------- /docs/widget-element.md: -------------------------------------------------------------------------------- 1 | # JSX 组件 2 | 3 |
4 | 5 | ## wbox 6 | 7 | 盒子组件,必须处于 jsx 最外层,映射为 [ListWidget](https://docs.scriptable.app/listwidget/) 8 | 9 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 10 | | ------------------------------------------------------------ | ---------------- | --------------------------------------- | ---- | ------------------------------------------------------------ | 11 | | [background](https://docs.scriptable.app/listwidget/#backgroundcolor) | string \| object | 浅色模式是#ffffff
暗黑模式是#000000 | 否 | 背景
可以为hex 字符串,例子:#ffffff
可以为网络图片链接,例子:http://example.com/a.jpg
可以为 [Color](https://docs.scriptable.app/color/) 对象
可以为 [Image](https://docs.scriptable.app/image/) 对象
可以为 [LinearGradient](https://docs.scriptable.app/lineargradient/) 渐变对象
wbox不能设置透明背景,ios不支持。
不过,可以通过[裁剪桌面截图,营造视觉上的透明](https://github.com/2214962083/ios-scriptable-tsx/blob/b528a7ceeed719f8cc7cd79bb1b422244c98cb91/src/lib/help.ts#L753) | 12 | | [spacing](https://docs.scriptable.app/listwidget/#spacing) | number | 0 | 否 | 间隔距离 | 13 | | [href](https://docs.scriptable.app/listwidget/#url) | string | | 否 | 点击时打开的url
不与 `onClick` 共存,当 `onClick` 存在时,只执行 `onClick` | 14 | | [updateDate](https://docs.scriptable.app/listwidget/#refreshafterdate) | object | | 否 | 小组件更新日期
只接收 [Date](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date) 对象
该属性指示何时可以再次刷新窗口小部件。在到达日期之前,不会刷新小部件。**不保证小部件将在指定的日期完全刷新。**
小部件的刷新率部分取决于iOS / iPadOS。例如,如果设备电池电量低或用户很少看小部件,则小部件可能不会刷新。 | 15 | | [padding](https://docs.scriptable.app/listwidget/#-setpadding) | Array\ | [0,0,0,0] | 否 | 内边距,**依次是上、左、下、右**,四个都要填 | 16 | | onClick | function | | 否 | 用 [URLScheme](https://docs.scriptable.app/urlscheme/) 实现的点击事件
不与 `href` 共存,当 `href` 存在时,只执行 `onClick` | 17 | 18 |
19 | 20 |
21 | 22 | ## wstack 23 | 24 | 容器组件,类似 `div` ,映射为 [WidgetStack](https://docs.scriptable.app/widgetstack/) 25 | 26 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 27 | | ------------------------------------------------------------ | ---------------- | --------- | ---- | ------------------------------------------------------------ | 28 | | [background](https://docs.scriptable.app/widgetstack/#backgroundcolor) | string \| object | | 否 | 背景
可以为hex 字符串,例子:#ffffff
可以为网络图片链接,例子:http://example.com/a.jpg
可以为 [Color](https://docs.scriptable.app/color/) 对象
可以为 [Image](https://docs.scriptable.app/image/) 对象
可以为 [LinearGradient](https://docs.scriptable.app/lineargradient/) 渐变对象 | 29 | | [spacing](https://docs.scriptable.app/widgetstack/#spacing) | number | 0 | 否 | 间隔距离 | 30 | | [padding](https://docs.scriptable.app/widgetstack/#-setpadding) | Array\ | [0,0,0,0] | 否 | 内边距,**依次是上、左、下、右**,四个都要填 | 31 | | [width](https://docs.scriptable.app/widgetstack/#size) | number | 0 | 否 | 组件宽,**当宽度设置 <= 0 时,小部件将自动确定该尺寸的长度。** | 32 | | [height](https://docs.scriptable.app/widgetstack/#size) | number | 0 | 否 | 组件高,**当高度设置 <= 0 时,小部件将自动确定该尺寸的长度。** | 33 | | [borderRadius](https://docs.scriptable.app/widgetstack/#cornerradius) | number | 0 | 否 | 边框四个角的圆角程度 | 34 | | [borderWidth](https://docs.scriptable.app/widgetstack/#borderwidth) | number | 0 | 否 | 边框宽度 | 35 | | [borderColor](https://docs.scriptable.app/widgetstack/#bordercolor) | string \| object | #000000 | 否 | 边框颜色
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 36 | | [href](https://docs.scriptable.app/widgetstack/#url) | string | | 否 | 点击时打开的url
不与 `onClick` 共存,当 `onClick` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 37 | | [verticalAlign](#wstack-verticalAlign) | string | top | 否 | 内容垂直方向对齐方式 | 38 | | [flexDirection](#wstack-flexDirection) | string | row | 否 | 排版方向 | 39 | | onClick | function | | 否 | 用 [URLScheme](https://docs.scriptable.app/urlscheme/) 实现的点击事件
不与 `href` 共存,当 `href` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 40 |
41 | 42 | ##### verticalAlign 的合法值 43 | 44 | | 值 | 说明 | 45 | | ------------------------------------------------------------ | -------------------- | 46 | | [top](https://docs.scriptable.app/widgetstack/#-topaligncontent) | 顶部对齐内容(默认) | 47 | | [center](https://docs.scriptable.app/widgetstack/#-centeraligncontent) | 居中对齐内容 | 48 | | [bottom](https://docs.scriptable.app/widgetstack/#-bottomaligncontent) | 底部对齐内容 | 49 |
50 | 51 | ##### flexDirection 的合法值 52 | 53 | | 值 | 说明 | 54 | | ------------------------------------------------------------ | ---------------- | 55 | | [row](https://docs.scriptable.app/widgetstack/#-layouthorizontally) | 横向排版(默认) | 56 | | [column](https://docs.scriptable.app/widgetstack/#-layoutvertically) | 纵向排版 | 57 | 58 |
59 | 60 |
61 | 62 | ## wimage 63 | 64 | 图片组件 ,映射为 [WidgetImage](https://docs.scriptable.app/widgetimage/) ,组件里面不可包裹其他组件 65 | 66 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 67 | | ------------------------------------------------------------ | ---------------- | ------- | ---- | ------------------------------------------------------------ | 68 | | [src](https://docs.scriptable.app/widgetimage/#image) | string \| object | | 是 | 图片资源地址
可以为网络连接,例子:http://example.com/a.jpg
可以为 [Image](https://docs.scriptable.app/image/) 对象
可以为 [SFSymbol 的 icon名字](https://docs.scriptable.app/sfsymbol/) ,就是 [ios 自带图标库里](https://apps.apple.com/us/app/sf-symbols-browser/id1491161336) 某图标的iconName,例子:tv.circle.fill
| 69 | | [href](https://docs.scriptable.app/widgetimage/#url) | string | | 否 | 点击时打开的url
不与 `onClick` 共存,当 `onClick` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 70 | | [resizable](https://docs.scriptable.app/widgetimage/#resizable) | boolean | true | 否 | 图片是否可以调整大小 | 71 | | [width](https://docs.scriptable.app/widgetimage/#imagesize) | number | | 否 | 图片宽,**当宽高都为空时,图片将显示原尺寸。** | 72 | | [height](https://docs.scriptable.app/widgetimage/#imagesize) | number | | 否 | 图片高,**当宽高都为空时,图片将显示原尺寸。** | 73 | | [opacity](https://docs.scriptable.app/widgetimage/#imageopacity) | number | 1 | 否 | 透明度,范围0到1,0为完全透明,1为完全不透明。 | 74 | | [borderRadius](https://docs.scriptable.app/widgetimage/#cornerradius) | number | 0 | 否 | 边框四个角的圆角程度 | 75 | | [borderWidth](https://docs.scriptable.app/widgetimage/#borderwidth) | number | 0 | 否 | 边框宽度 | 76 | | [borderColor](https://docs.scriptable.app/widgetimage/#bordercolor) | string \| object | #000000 | 否 | 边框颜色
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 77 | | [containerRelativeShape](https://docs.scriptable.app/widgetimage/#containerrelativeshape) | boolean | false | 否 | 如果为true,则图片的角将相对于包含的小部件进行四舍五入。
如果为true,则会忽略borderRadius的值
我知道你看不懂,我也是,可以[看官方文档解释](https://docs.scriptable.app/widgetimage/#containerrelativeshape) | 78 | | [filter](https://docs.scriptable.app/widgetimage/#tintcolor) | string \| object | | 否 | 加滤镜
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 79 | | [imageAlign](#wimage-imageAlign) | string | left | | 图片横向对齐方式 | 80 | | [mode](#wimage-mode) | string | fit | 否 | 图片显示模式 | 81 | | onClick | function | | 否 | 用 [URLScheme](https://docs.scriptable.app/urlscheme/) 实现的点击事件
不与 `href` 共存,当 `href` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 82 | 83 |
84 | 85 | ##### imageAlign 的合法值 86 | 87 | | 值 | 说明 | 88 | | ------------------------------------------------------------ | ------------------ | 89 | | [left](https://docs.scriptable.app/widgetimage/#-leftalignimage) | 左对齐图片(默认) | 90 | | [center](https://docs.scriptable.app/widgetimage/#-centeralignimage) | 中间对齐图片 | 91 | | [right](https://docs.scriptable.app/widgetimage/#-rightalignimage) | 右对齐图片 | 92 | 93 |
94 | 95 | ##### mode 的合法值 96 | 97 | | 值 | 说明 | 98 | | ------------------------------------------------------------ | -------------------------- | 99 | | [fit](https://docs.scriptable.app/widgetimage/#-applyfittingcontentmode) | 图片将适应可用空间(默认) | 100 | | [fill](https://docs.scriptable.app/widgetimage/#-applyfillingcontentmode) | 图片将填充可用空间 | 101 | 102 |
103 | 104 |
105 | 106 | ## wspacer 107 | 108 | 空格占位组件,映射为 [WidgetSpacer](https://docs.scriptable.app/widgetspacer/) ,组件里面不可包裹其他组件 109 | 110 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 111 | | ---------------------------------------------------------- | ------ | ------ | ---- | --------------------------------------- | 112 | | [length](https://docs.scriptable.app/widgetspacer/#length) | number | 0 | 否 | 空格长度,当为0时是弹性占位(能占则占) | 113 | 114 |
115 | 116 |
117 | 118 | ## wtext 119 | 120 | 文字组件,映射为 [WidgetText](https://docs.scriptable.app/widgettext/) ,组件里面不可包裹其他组件,只可以包裹文字 121 | 122 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 123 | | ------------------------------------------------------------ | ---------------- | --------------------------------------- | ---- | ------------------------------------------------------------ | 124 | | [textColor](https://docs.scriptable.app/widgettext/#textcolor) | string \| object | 浅色模式是#000000
暗黑模式是#ffffff | 否 | 文字颜色
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 125 | | [font](https://docs.scriptable.app/widgettext/#font) | number \| object | | 否 | 文字字体样式
为number时,使用正常系统字体,并设置为这个尺寸。
也可以传 [Font](https://docs.scriptable.app/font/) 字体对象进来 | 126 | | [opacity](https://docs.scriptable.app/widgettext/#textopacity) | number | 1 | 否 | 透明度,范围0到1,0为完全透明,1为完全不透明。 | 127 | | [maxLine](https://docs.scriptable.app/widgettext/#linelimit) | number | 0 | 否 | 最大行数。
显示的最大行数。该值 <= 0 时,将禁用该限制。 | 128 | | [scale](https://docs.scriptable.app/widgettext/#minimumscalefactor) | number | 1 | 否 | 文字可缩最小的倍数,取值范围 0 到 1 。
例如:0.5 时,允许布局调整时缩小文字到原来的 0.5 倍大小
有点拗口,[看官方文档](https://docs.scriptable.app/widgettext/#minimumscalefactor) | 129 | | [shadowColor](https://docs.scriptable.app/widgettext/#shadowcolor) | string \| object | #000000 | 否 | 阴影颜色, `shadowRadius` 属性的值必须大于零,此属性才能生效。
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 130 | | [shadowRadius](https://docs.scriptable.app/widgettext/#shadowradius) | number | 0 | 否 | 阴影模糊距离 | 131 | | [shadowOffset](https://docs.scriptable.app/widgettext/#shadowoffset) | object | new Point(0, 0) | 否 | 阴影的偏移位置。 `shadowRadius` 属性的值必须大于零,此属性才能生效。
传入 [Point](https://docs.scriptable.app/point/#point) 位置对象 | 132 | | [href](https://docs.scriptable.app/widgettext/#url) | string | | 否 | 点击时打开的url
不与 `onClick` 共存,当 `onClick` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 133 | | [textAlign](#wtext-textAlign) | string | left | 否 | | 134 | | onClick | function | | 否 | 用 [URLScheme](https://docs.scriptable.app/urlscheme/) 实现的点击事件
不与 `href` 共存,当 `href` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 135 | 136 |
137 | 138 | ##### textAlign 的合法值 139 | 140 | | 值 | 说明 | 141 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 142 | | [left](https://docs.scriptable.app/widgettext/#-leftaligntext) | 文字左对齐(在`wstack`里左对齐文本,应该在文本组件右边放一个 `wspacer` ) | 143 | | [center](https://docs.scriptable.app/widgettext/#-centeraligntext) | 文字居中对齐(在`wstack`里居中对齐文本,应该在文本组件两边放一个 `wspacer` ) | 144 | | [right](https://docs.scriptable.app/widgettext/#-rightaligntext) | 文字右对齐(在`wstack`里右对齐文本,应该在文本组件左边放一个 `wspacer` ) | 145 | 146 |
147 | 148 |
149 | 150 | ## wdate 151 | 152 | 日期组件,映射为 [WidgetDate](https://docs.scriptable.app/widgetdate/) ,组件里面不可包裹其他组件,有点类似 `wtext` 组件,区别在文字改成了日期 153 | 154 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 155 | | ------------------------------------------------------------ | ---------------- | --------------------------------------- | ---- | ------------------------------------------------------------ | 156 | | [date](https://docs.scriptable.app/widgetdate/#date) | object | | 是 | 需要显示的时间
只接收 [Date](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date) 对象 | 157 | | [mode](#wdate-mode) | string | | 是 | 显示的时间格式 | 158 | | [textColor](https://docs.scriptable.app/widgetdate/#textcolor) | string \| object | 浅色模式是#000000
暗黑模式是#ffffff | 否 | 文字颜色
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 159 | | [font](https://docs.scriptable.app/widgetdate/#font) | number \| object | | 否 | 文字字体样式
为number时,使用正常系统字体,并设置为这个尺寸。
也可以传 [Font](https://docs.scriptable.app/font/) 字体对象进来 | 160 | | [opacity](https://docs.scriptable.app/widgetdate/#textopacity) | number | 1 | 否 | 透明度,范围0到1,0为完全透明,1为完全不透明。 | 161 | | [maxLine](https://docs.scriptable.app/widgetdate/#linelimit) | number | 0 | 否 | 最大行数。
显示的最大行数。该值 <= 0 时,将禁用该限制。 | 162 | | [scale](https://docs.scriptable.app/widgetdate/#minimumscalefactor) | number | 1 | 否 | 文字可缩最小的倍数,取值范围 0 到 1 。
例如:0.5 时,允许布局调整时缩小文字到原来的 0.5 倍大小
有点拗口,[看官方文档](https://docs.scriptable.app/widgettext/#minimumscalefactor) | 163 | | [shadowColor](https://docs.scriptable.app/widgetdate/#shadowcolor) | string \| object | #000000 | 否 | 阴影颜色, `shadowRadius` 属性的值必须大于零,此属性才能生效。
可以为hex 字符串,例子:#ffffff
可以为 [Color](https://docs.scriptable.app/color/) 对象 | 164 | | [shadowRadius](https://docs.scriptable.app/widgetdate/#shadowradius) | number | 0 | 否 | 阴影模糊距离 | 165 | | [shadowOffset](https://docs.scriptable.app/widgetdate/#shadowoffset) | object | new Point(0, 0) | 否 | 阴影的偏移位置。 `shadowRadius` 属性的值必须大于零,此属性才能生效。
传入 [Point](https://docs.scriptable.app/point/#point) 位置对象 | 166 | | [href](https://docs.scriptable.app/widgetdate/#url) | string | | 否 | 点击时打开的url
不与 `onClick` 共存,当 `onClick` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 167 | | [textAlign](#wtext-textAlign) | string | left | 否 | | 168 | | onClick | function | | 否 | 用 [URLScheme](https://docs.scriptable.app/urlscheme/) 实现的点击事件
不与 `href` 共存,当 `href` 存在时,只执行 `onClick`
**当小部件为小尺寸时,此属性不生效** | 169 | 170 |
171 | 172 | ##### mode 的合法值 173 | 174 | | 值 | 说明 | 175 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 176 | | [time](https://docs.scriptable.app/widgetdate/#-applytimestyle) | 显示日期的时间部分。例如:11:23PM | 177 | | [date](https://docs.scriptable.app/widgetdate/#-applydatestyle) | 显示整个日期。例如:June 3, 2019 | 178 | | [relative](https://docs.scriptable.app/widgetdate/#-applyrelativestyle) | 将日期显示为相对于现在的日期。例如:2 hours, 23 minutes 1 year, 1 month | 179 | | [offset](https://docs.scriptable.app/widgetdate/#-applyoffsetstyle) | 将日期显示为从现在开始的偏移量。例如:+2 hours -3 months | 180 | | [timer](https://docs.scriptable.app/widgetdate/#-applytimerstyle) | 从现在开始将日期显示为计时器计数。例如:2:32 36:59:01 | 181 | 182 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["src/**/*.spec.ts"], 3 | "watch": ["src/"], 4 | "ext": "ts,tsx,json,js", 5 | "exec": "node -e \"console.clear()\"&& npm run dev" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptable-types", 3 | "version": "1.0.0", 4 | "author": "xiaomingyang", 5 | "description": "", 6 | "main": "index.js", 7 | "keywords": [ 8 | "scriptable", 9 | "ios", 10 | "widget" 11 | ], 12 | "scripts": { 13 | "watch": "cross-env watching=true nodemon --config nodemon.json", 14 | "dev": "cross-env NODE_ENV=development compileType=one ts-node ./src/lib/compile.ts", 15 | "dev:all": "cross-env NODE_ENV=development compileType=all ts-node ./src/lib/compile.ts", 16 | "build": "cross-env NODE_ENV=production compileType=one ts-node ./src/lib/compile.ts", 17 | "build:all": "cross-env NODE_ENV=production compileType=all ts-node ./src/lib/compile.ts" 18 | }, 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@types/dotenv": "^8.2.0", 22 | "@types/eslint": "^7.2.2", 23 | "@types/express": "^4.17.9", 24 | "@types/fs-extra": "^9.0.5", 25 | "@types/ip": "^1.1.0", 26 | "@types/lodash": "^4.14.165", 27 | "@types/node": "^14.6.0", 28 | "@types/prettier": "^2.1.1", 29 | "@types/react": "^17.0.0", 30 | "@types/react-dom": "^17.0.0", 31 | "@types/scriptable-ios": "^1.6.1", 32 | "@typescript-eslint/eslint-plugin": "^4.1.1", 33 | "@typescript-eslint/parser": "^4.1.1", 34 | "cross-env": "^7.0.3", 35 | "esbuild": "^0.8.12", 36 | "eslint": "^7.9.0", 37 | "eslint-config-prettier": "^6.11.0", 38 | "eslint-plugin-prettier": "^3.1.4", 39 | "javascript-obfuscator": "^2.9.4", 40 | "nodemon": "^2.0.6", 41 | "prettier": "^2.1.1", 42 | "ts-loader": "^8.0.2", 43 | "ts-node": "^8.10.2", 44 | "typescript": "^4.0.2" 45 | }, 46 | "dependencies": { 47 | "body-parser": "^1.19.0", 48 | "chalk": "^4.1.0", 49 | "dotenv": "^8.2.0", 50 | "dotenv-expand": "^5.1.0", 51 | "express": "^4.17.1", 52 | "fs-extra": "^9.0.1", 53 | "ip": "^1.1.5", 54 | "lodash": "^4.17.20", 55 | "qrcode-terminal": "^0.12.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = /** @type { import ('prettier').RequiredOptions } */ ({ 4 | printWidth: 120, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | bracketSpacing: false, 9 | arrowParens: 'avoid', 10 | insertPragma: false, 11 | tabWidth: 2, 12 | useTabs: false, 13 | endOfLine: 'auto', 14 | }) 15 | -------------------------------------------------------------------------------- /scriptable.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require('path') 4 | 5 | /**项目根目录,不建议修改*/ 6 | const rootPath = __dirname 7 | 8 | /**输入文件,当 compileType 为 one 时生效,不建议修改*/ 9 | const inputFile = path.resolve(rootPath, './src/index.ts') 10 | 11 | /**输入文件夹,当 compileType 为 all 时生效,不建议修改*/ 12 | const inputDir = path.resolve(rootPath, './src/scripts') 13 | 14 | /**输出文件夹,不建议修改*/ 15 | const outputDir = path.resolve(rootPath, './dist') 16 | 17 | /**是否压缩代码*/ 18 | const minify = process.env.NODE_ENV === 'production' 19 | 20 | /**是否加密代码*/ 21 | const encrypt = process.env.NODE_ENV === 'production' 22 | 23 | /**往编译后的代码头部插入的代码*/ 24 | const header = ` 25 | /** 26 | * 作者: 小明 27 | * 版本: 1.0.0 28 | * 更新时间:${new Date().toLocaleDateString()} 29 | * github: https://github.com/2214962083/ios-scriptable-tsx 30 | */ 31 | ` 32 | 33 | module.exports = /** @type { import ('./src/lib/compile').CompileOptions } */ ({ 34 | rootPath, 35 | inputFile, 36 | inputDir, 37 | outputDir, 38 | minify, 39 | encrypt, 40 | header, 41 | 42 | /** 43 | * esbuild 自定义配置 44 | * see: https://esbuild.github.io/api/#simple-options 45 | */ 46 | esbuild: {}, 47 | 48 | /** 49 | * javascript-obfuscator 自定义配置 50 | * see: https://github.com/javascript-obfuscator/javascript-obfuscator 51 | */ 52 | encryptOptions: {}, 53 | }) 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './scripts/helloWorld.tsx' 2 | -------------------------------------------------------------------------------- /src/lib/basic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础包源码 3 | */ 4 | import {port} from '@app/lib/constants' 5 | import {useStorage, showActionSheet, showModal, showNotification, sleep, getSciptableTopComment} from '@app/lib/help' 6 | 7 | interface DevelopRemoteParams { 8 | /**被同步的脚本的路径*/ 9 | syncScriptName?: string | null 10 | 11 | /**被同步的脚本的文件名*/ 12 | syncScriptPath?: string | null 13 | 14 | /**远程文件地址*/ 15 | remoteFileAddress?: string | null 16 | } 17 | 18 | const {setStorage, getStorage} = useStorage('basic-storage') 19 | const runScriptDate = Date.now() 20 | setStorage('runScriptDate', runScriptDate) 21 | 22 | class Basic { 23 | /**间隔多久同步一次脚本,单位:毫秒*/ 24 | private syncInterval = 1 * 1000 25 | 26 | /**本地脚本编译时间*/ 27 | private lastCompileDate = 0 28 | 29 | /**请求超时,单位:毫秒*/ 30 | private timeout = 5 * 1000 31 | 32 | /**已连续请求失败次数*/ 33 | private requestFailTimes = 0 34 | 35 | /**连续请求失败最大次数,单位:毫秒*/ 36 | private maxRequestFailTimes = 10 37 | 38 | async init() { 39 | await this.showMenu() 40 | } 41 | /**获取本地脚本列表,name 是脚本名字,path 是脚本路径*/ 42 | getLocalScripts(): Record<'name' | 'path', string>[] { 43 | const dirPath = MODULE.filename.split('/').slice(0, -1).join('/') 44 | let scriptNames: string[] = FileManager.local().listContents(dirPath) || [] 45 | // 过滤非.js结尾的文件 46 | scriptNames = scriptNames.filter(scriptName => /\.js$/.test(scriptName)) 47 | return scriptNames.map(scriptName => ({ 48 | name: scriptName, 49 | path: FileManager.local().joinPath(dirPath, scriptName), 50 | })) 51 | } 52 | /** 53 | * 请求获取脚本内容 54 | * @param url 远程文件地址 55 | */ 56 | async getScriptText(url: string): Promise { 57 | try { 58 | const req = new Request(url) 59 | req.timeoutInterval = this.timeout / 1000 60 | const res = await req.loadString() 61 | this.requestFailTimes = 0 62 | return res || '' 63 | } catch (err) { 64 | // 如果失败时间戳为 null,则记录本次失败时间 65 | this.requestFailTimes += 1 66 | return '' 67 | } 68 | } 69 | /**显示菜单*/ 70 | async showMenu() { 71 | const that = this 72 | let itemList = ['远程开发'] 73 | const syncScriptName = getStorage('syncScriptName') 74 | const syncScriptPath = getStorage('syncScriptPath') 75 | const remoteFileAddress = getStorage('remoteFileAddress') 76 | const scriptText = getStorage('scriptText') 77 | 78 | if (syncScriptName && syncScriptPath && remoteFileAddress) { 79 | // 如果上次有远程开发过,则加入直接同步选项 80 | itemList = ['远程开发', `同步${syncScriptName}`] 81 | 82 | if (scriptText) { 83 | itemList.push(`运行缓存里的${syncScriptName}`) 84 | } 85 | } 86 | const selectIndex = await showActionSheet({ 87 | itemList, 88 | }) 89 | switch (selectIndex) { 90 | case 0: 91 | await that.developRemote() 92 | break 93 | case 1: 94 | await that.developRemote({ 95 | syncScriptName, 96 | syncScriptPath, 97 | remoteFileAddress, 98 | }) 99 | break 100 | case 2: 101 | // 执行远程代码 102 | await that.runCode(syncScriptName as string, scriptText as string) 103 | break 104 | } 105 | } 106 | /**远程开发同步*/ 107 | async developRemote(params: DevelopRemoteParams = {}): Promise { 108 | const that = this 109 | 110 | /**被同步的脚本的路径*/ 111 | let _syncScriptPath = params.syncScriptPath 112 | 113 | /**被同步的脚本的文件名*/ 114 | let _syncScriptName = params.syncScriptName 115 | 116 | /**远程api*/ 117 | let _remoteFileAddress = params.remoteFileAddress 118 | 119 | if (!_syncScriptPath || !_syncScriptName) { 120 | // 选择要开发的脚本 121 | const scripts = that.getLocalScripts() 122 | const selectIndex = await showActionSheet({ 123 | title: '选择你要开发的脚本', 124 | itemList: scripts.map(script => script.name), 125 | }) 126 | if (selectIndex < 0) return 127 | 128 | /**被同步的脚本的路径*/ 129 | _syncScriptPath = scripts[selectIndex].path 130 | setStorage('syncScriptPath', _syncScriptPath) 131 | 132 | /**被同步的脚本的文件名*/ 133 | _syncScriptName = scripts[selectIndex].name 134 | setStorage('syncScriptName', _syncScriptName) 135 | } 136 | 137 | if (!_remoteFileAddress) { 138 | /**内存中的远程文件地址*/ 139 | _remoteFileAddress = getStorage('remoteFileAddress') || '' 140 | 141 | // 输入远程文件地址 142 | const {cancel, texts} = await showModal({ 143 | title: '远程文件地址', 144 | content: '请输入远程开发服务器(电脑)要同步的文件地址', 145 | confirmText: '连接', 146 | inputItems: [ 147 | { 148 | placeholder: '输入远程文件地址', 149 | text: _remoteFileAddress || `http://192.168.1.3:${port}/index.js`, 150 | }, 151 | ], 152 | }) 153 | if (cancel) return 154 | 155 | /**远程文件地址*/ 156 | _remoteFileAddress = texts[0] 157 | 158 | if (!_remoteFileAddress) return 159 | 160 | setStorage('remoteFileAddress', _remoteFileAddress) 161 | } 162 | 163 | if (!_remoteFileAddress || !_syncScriptName || !_syncScriptPath) { 164 | await showNotification({ 165 | title: '信息不完整,运行终止', 166 | body: '没选择脚本或远程ip没填写', 167 | sound: 'failure', 168 | }) 169 | return 170 | } 171 | 172 | await showNotification({title: '开始同步代码'}) 173 | 174 | /**同步脚本*/ 175 | const syncScript = async () => { 176 | /**远程脚本字符串*/ 177 | let scriptText = await that.getScriptText(_remoteFileAddress as string) 178 | 179 | /**匹配时间戳,例子:'// @编译时间 1606834773399' */ 180 | const compileDateRegExp = /\/\/\s*?\@编译时间\s*?([\d]+)/ 181 | 182 | /**匹配结果*/ 183 | const dateMatchResult = scriptText.match(compileDateRegExp) 184 | 185 | /**本次远程脚本的编译时间*/ 186 | const thisCompileDate = Number(dateMatchResult && dateMatchResult[1]) || null 187 | 188 | // 如果没有获取到脚本内容、此次脚本编译时间为空,则不写入 189 | if (!scriptText || !thisCompileDate) return 190 | 191 | //如果此次脚本编译时间和上次相同,则不写入 192 | if (thisCompileDate && thisCompileDate <= that.lastCompileDate) return 193 | 194 | try { 195 | // 写入代码到文件 196 | const comment = getSciptableTopComment(_syncScriptPath as string) 197 | await FileManager.local().writeString(_syncScriptPath as string, `${comment}\n${scriptText}`) 198 | 199 | // 添加 console 重写代码 200 | const serverApi = (_remoteFileAddress?.match(/http\:\/\/[\d\.]+?\:[\d]+/) || [])[0] 201 | scriptText = `${that.getRewriteConsoleCode(serverApi)}\n${scriptText}` 202 | 203 | // 写入代码到缓存 204 | setStorage('scriptText', scriptText) 205 | 206 | that.lastCompileDate = thisCompileDate 207 | await showNotification({title: '同步代码成功'}) 208 | 209 | // 执行远程代码 210 | await that.runCode(_syncScriptName as string, scriptText) 211 | } catch (err) { 212 | console.log('代码写入失败') 213 | console.log(err) 214 | await showNotification({ 215 | title: '代码同步失败', 216 | body: err.message, 217 | sound: 'failure', 218 | }) 219 | } 220 | } 221 | 222 | // 循环执行 223 | while (1) { 224 | const _runScriptDate = getStorage('runScriptDate') 225 | if (runScriptDate !== Number(_runScriptDate)) { 226 | // 本脚本重新运行了,停止这次同步 227 | break 228 | } 229 | if (this.requestFailTimes >= this.maxRequestFailTimes) { 230 | // 太久请求不成功了,终结远程同步 231 | await showNotification({ 232 | title: '已停止同步', 233 | subtitle: '远程开发已停止,连接超时。', 234 | sound: 'complete', 235 | }) 236 | break 237 | } 238 | await syncScript() 239 | await sleep(that.syncInterval) 240 | } 241 | } 242 | /** 243 | * 执行远程代码 244 | * @param syncScriptName 脚本名称 245 | * @param scriptText 脚本内容 246 | */ 247 | async runCode(syncScriptName: string, scriptText: string) { 248 | try { 249 | const runRemoteCode = new Function(`(async () => { 250 | ${scriptText} 251 | })()`) 252 | // 执行远程代码 253 | runRemoteCode() 254 | } catch (err) { 255 | console.log('同步的代码执行失败') 256 | console.log(err) 257 | await showNotification({ 258 | title: `${syncScriptName}执行失败`, 259 | body: err.message, 260 | sound: 'failure', 261 | }) 262 | } 263 | } 264 | /** 265 | * 获取重写 console 的方法 266 | * @param serverApi 远程链接api地址,如 http://192.168.2.4:9090 267 | */ 268 | getRewriteConsoleCode(serverApi: string) { 269 | return ` 270 | // 保留日志原始打印方法 271 | const __log__ = console.log; 272 | const __warn__ = console.warn; 273 | const __error__ = console.error; 274 | 275 | /**发到日志远程控制台*/ 276 | const __sendLogToRemote__ = async (type = 'log', data = '') => { 277 | const req = new Request('${serverApi}/console'); 278 | req.method = 'POST'; 279 | req.headers = { 280 | 'Content-Type': 'application/json', 281 | }; 282 | req.body = JSON.stringify({ 283 | type, 284 | data, 285 | }); 286 | return await req.loadJSON() 287 | } 288 | 289 | /**存储上个console 的promise*/ 290 | let __lastConsole__ = Promise.resolve() 291 | 292 | /**重写生成日志函数*/ 293 | const __generateLog__ = (type = 'log', oldFunc) => { 294 | return function(...args) { 295 | /**为了同步打印,finally 兼容性太差*/ 296 | __lastConsole__.then(() => { 297 | __lastConsole__ = __sendLogToRemote__(type, args[0]).catch(err => {}) 298 | }).catch(() => { 299 | __lastConsole__ = __sendLogToRemote__(type, args[0]).catch(err => {}) 300 | }) 301 | oldFunc.apply(this, args); 302 | } 303 | }; 304 | if (!console.__rewrite__) { 305 | console.log = __generateLog__('log', __log__).bind(console); 306 | console.warn = __generateLog__('warn', __warn__).bind(console); 307 | console.error = __generateLog__('error', __error__).bind(console); 308 | } 309 | console.__rewrite__ = true; 310 | ` 311 | } 312 | } 313 | 314 | new Basic().init() 315 | -------------------------------------------------------------------------------- /src/lib/compile.ts: -------------------------------------------------------------------------------- 1 | import {build, BuildOptions, OutputFile} from 'esbuild' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import {promisify} from 'util' 5 | import {merge} from 'lodash' 6 | import {obfuscate, ObfuscatorOptions} from 'javascript-obfuscator' 7 | import {createServer} from './server' 8 | import {loadEnvFiles} from './env' 9 | import compileOptions from '../../scriptable.config' 10 | import {ensureFile, remove} from 'fs-extra' 11 | 12 | /**打包模式*/ 13 | export enum CompileType { 14 | /**打包一个文件夹*/ 15 | ALL = 'all', 16 | 17 | /**为打包入口文件*/ 18 | ONE = 'one', 19 | } 20 | 21 | export interface CompileOptions { 22 | /**项目根目录*/ 23 | rootPath: string 24 | 25 | /**输入文件,当 compileType 为 one 时生效*/ 26 | inputFile: string 27 | 28 | /**输入文件夹,当 compileType 为 all 时生效*/ 29 | inputDir: string 30 | 31 | /**输出文件夹*/ 32 | outputDir: string 33 | 34 | /**打包模式,all 为打包一个文件夹,one为打包入口文件*/ 35 | compileType?: CompileType 36 | 37 | /**是否在 watch 开发*/ 38 | watch?: boolean 39 | 40 | /**是否显示二维码*/ 41 | showQrcode?: boolean 42 | 43 | /** 44 | * esbuild 自定义配置 45 | * see: https://esbuild.github.io/api/#simple-options 46 | */ 47 | esbuild?: BuildOptions 48 | 49 | /**是否压缩代码*/ 50 | minify?: boolean 51 | 52 | /**在编译中添加额外的头部,一般是作者信息*/ 53 | header?: string 54 | 55 | /**是否加密代码*/ 56 | encrypt?: boolean 57 | 58 | /** 59 | * javascript-obfuscator 自定义配置 60 | * see: https://github.com/javascript-obfuscator/javascript-obfuscator 61 | */ 62 | encryptOptions?: ObfuscatorOptions 63 | } 64 | 65 | /**项目根目录*/ 66 | const rootPath = path.resolve(__dirname, '../') 67 | 68 | /**输入文件,当 compileType 为 one 时生效*/ 69 | const inputFile: string = path.resolve(rootPath, './src/index.ts') 70 | 71 | /**输入文件夹,当 compileType 为 all 时生效*/ 72 | const inputDir: string = path.resolve(rootPath, './src/scripts') 73 | 74 | /**输出文件夹*/ 75 | const outputDir: string = path.resolve(rootPath, './dist') 76 | 77 | /**打包模式,all 为打包一个文件夹,one为打包入口文件*/ 78 | const compileType = (process.env.compileType as CompileType) || CompileType.ONE 79 | 80 | /**是否在 watch 开发*/ 81 | const watch = Boolean(process.env.watching) 82 | 83 | /**是否压缩代码*/ 84 | const minify = process.env.NODE_ENV === 'production' 85 | 86 | /**是否加密代码*/ 87 | const encrypt = process.env.NODE_ENV === 'production' 88 | 89 | const _compileOptions = { 90 | rootPath, 91 | inputFile, 92 | inputDir, 93 | outputDir, 94 | compileType, 95 | watch, 96 | minify, 97 | encrypt, 98 | } 99 | 100 | compile(merge(_compileOptions, compileOptions || {})) 101 | 102 | async function compile(options: CompileOptions) { 103 | const { 104 | rootPath, 105 | inputDir, 106 | inputFile, 107 | outputDir, 108 | compileType = CompileType.ONE, 109 | watch = false, 110 | showQrcode = true, 111 | esbuild = {}, 112 | minify = false, 113 | encrypt = false, 114 | encryptOptions = {}, 115 | header = '', 116 | } = options 117 | 118 | /**加载环境变量 .env 文件*/ 119 | loadEnvFiles(rootPath) 120 | 121 | if (watch) { 122 | /**创建服务器*/ 123 | createServer({ 124 | staticDir: outputDir, 125 | showQrcode, 126 | }) 127 | } 128 | 129 | // 编译时,把 process.env 环境变量替换成 dotenv 文件参数 130 | const define: Record = {} 131 | for (const key in process.env) { 132 | // 不能含有括号、-号、空格 133 | if (/[\(\)\-\s]/.test(key)) continue 134 | define[`process.env.${key}`] = JSON.stringify(process.env[key]) 135 | } 136 | 137 | // 深度获取某个文件夹里所有文件路径(包括子文件夹) 138 | const readdir = promisify(fs.readdir) 139 | const stat = promisify(fs.stat) 140 | async function getFilesFromDir(dir: string): Promise { 141 | const subdirs = await readdir(dir) 142 | const files = await Promise.all( 143 | subdirs.map(async subdir => { 144 | const res = path.resolve(dir, subdir) 145 | return (await stat(res)).isDirectory() ? getFilesFromDir(res) : res 146 | }), 147 | ) 148 | return files.reduce((a: string[], f: string | string[]) => a.concat(f), []) 149 | } 150 | 151 | /**所有输出的文件信息集合*/ 152 | let outputFilesInfo: OutputFile[] = [] 153 | 154 | try { 155 | /**计算输入文件路径集合*/ 156 | const inputPaths: string[] = compileType === CompileType.ALL ? await getFilesFromDir(inputDir) : [inputFile] 157 | 158 | /** esbuild 配置*/ 159 | const esbuildOptions: BuildOptions = { 160 | entryPoints: [...inputPaths], 161 | platform: 'node', 162 | charset: 'utf8', 163 | bundle: true, 164 | outdir: outputDir, 165 | banner: `${header} 166 | // @编译时间 ${Date.now()} 167 | const MODULE = module; 168 | let __topLevelAwait__ = () => Promise.resolve(); 169 | function EndAwait(promiseFunc) { 170 | __topLevelAwait__ = promiseFunc 171 | }; 172 | `, 173 | footer: ` 174 | await __topLevelAwait__(); 175 | `, 176 | jsxFactory: 'h', 177 | jsxFragment: 'Fragment', 178 | define, 179 | minify, 180 | write: false, 181 | inject: [path.resolve(rootPath, './src/lib/jsx-runtime.ts')], 182 | } 183 | 184 | // 最终打包环节 185 | // 先清空输出文件夹 186 | await remove(outputDir) 187 | 188 | // esbuild 打包 189 | outputFilesInfo = (await build(merge(esbuildOptions, esbuild))).outputFiles || [] 190 | console.error('esbuild打包结束') 191 | } catch (err) { 192 | console.error('esbuild打包出错', err) 193 | process.exit(1) 194 | } 195 | 196 | const writeFile = promisify(fs.writeFile) 197 | for (const outputFile of outputFilesInfo) { 198 | let writeText = outputFile.text 199 | if (encrypt) { 200 | // 加密环节 201 | try { 202 | /**加密配置*/ 203 | const _encryptOptions: ObfuscatorOptions = { 204 | rotateStringArray: true, 205 | selfDefending: true, 206 | stringArray: true, 207 | splitStringsChunkLength: 100, 208 | stringArrayEncoding: ['rc4', 'base64'], 209 | } 210 | // 读取失败就跳下一轮 211 | if (!outputFile.text) continue 212 | 213 | // 加密代码 214 | const transformCode = obfuscate(outputFile.text, merge(_encryptOptions, encryptOptions)).getObfuscatedCode() 215 | 216 | // 写入加入代码、和头部信息 217 | writeText = `${header}\n${transformCode}` 218 | } catch (err) { 219 | console.error('加密代码失败', err) 220 | process.exit(1) 221 | } 222 | } 223 | // 确保路径存在 224 | await ensureFile(outputFile.path) 225 | // 写入代码 226 | await writeFile(outputFile.path, writeText, {encoding: 'utf8'}) 227 | } 228 | 229 | console.log('加密代码结束') 230 | console.log('打包完成') 231 | } 232 | -------------------------------------------------------------------------------- /src/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout' 2 | -------------------------------------------------------------------------------- /src/lib/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import {WstackProps} from '@app/types/widget' 2 | import {FC} from 'react' 3 | 4 | interface StackProps extends WstackProps { 5 | justifyContent?: 'flex-start' | 'center' | 'flex-end' 6 | alignItems?: 'flex-start' | 'center' | 'flex-end' 7 | } 8 | export const Stack: FC = ({children, ...props}) => { 9 | const {justifyContent = 'flex-start', alignItems = 'flex-start', ...wstackProps} = props 10 | const {flexDirection = 'row'} = wstackProps 11 | 12 | const JustifyContentFlexStart: FC = ({children, ...props}) => { 13 | const {flexDirection = 'row'} = props 14 | return ( 15 | 16 | {children} 17 | 18 | 19 | ) 20 | } 21 | const JustifyContentCenter: FC = ({children, ...props}) => { 22 | const {flexDirection = 'row'} = props 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | ) 30 | } 31 | const JustifyContentFlexEnd: FC = ({children, ...props}) => { 32 | const {flexDirection = 'row'} = props 33 | return ( 34 | 35 | 36 | {children} 37 | 38 | ) 39 | } 40 | const AlignItemsFlexStart: FC = ({children, ...props}) => { 41 | const {flexDirection = 'row', ...wstackProps} = props 42 | return ( 43 | 44 | {children} 45 | 46 | 47 | ) 48 | } 49 | const AlignItemsCenter: FC = ({children, ...props}) => { 50 | const {flexDirection = 'row', ...wstackProps} = props 51 | return ( 52 | 53 | 54 | {children} 55 | 56 | 57 | ) 58 | } 59 | const AlignItemsFlexEnd: FC = ({children, ...props}) => { 60 | const {flexDirection = 'row', ...wstackProps} = props 61 | return ( 62 | 63 | 64 | {children} 65 | 66 | ) 67 | } 68 | const JustifyContentMap: Record, FC> = { 69 | 'flex-start': JustifyContentFlexStart, 70 | center: JustifyContentCenter, 71 | 'flex-end': JustifyContentFlexEnd, 72 | } 73 | const AlignItemsMap: Record, FC> = { 74 | 'flex-start': AlignItemsFlexStart, 75 | center: AlignItemsCenter, 76 | 'flex-end': AlignItemsFlexEnd, 77 | } 78 | 79 | const _children = Array.isArray(children) 80 | ? children.map(child => { 81 | return h(AlignItemsMap[alignItems], {flexDirection}, child) 82 | }) 83 | : h(AlignItemsMap[alignItems], {flexDirection}, children) 84 | 85 | return h(JustifyContentMap[justifyContent], {...wstackProps}, _children) 86 | } 87 | 88 | export const Row: FC = ({children, ...props}) => { 89 | props.flexDirection = 'row' 90 | return {children} 91 | } 92 | 93 | export const Col: FC = ({children, ...props}) => { 94 | props.flexDirection = 'column' 95 | return {children} 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /**url启动脚本的来源*/ 2 | export enum URLSchemeFrom { 3 | /**从 widget 点击事件进入会携带上这个参数*/ 4 | WIDGET = 'widget', 5 | } 6 | 7 | /**服务端口*/ 8 | export const port = 9090 9 | -------------------------------------------------------------------------------- /src/lib/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import path from 'path' 3 | import dotenvExpand from 'dotenv-expand' 4 | 5 | /** 6 | * 加载环境变量 dotenv 文件 7 | * @param projectRootPath 项目根目录 8 | */ 9 | export function loadEnvFiles(projectRootPath: string): void { 10 | /** 11 | * 加载 dotenv 文件 12 | * .env # 在所有的环境中被载入 13 | * .env.local # 在所有的环境中被载入,但会被 git 忽略 14 | * .env.[mode] # 只在指定的模式中被载入 15 | * .env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略 16 | * @param mode 环境 17 | */ 18 | const loadEnv = (mode?: string): void => { 19 | const basePath = path.resolve(projectRootPath, `.env${mode ? `.${mode}` : ``}`) 20 | const localPath = `${basePath}.local` 21 | 22 | const load = (envPath: string) => { 23 | try { 24 | const env = dotenv.config({path: envPath}) 25 | dotenvExpand(env) 26 | } catch (err) {} 27 | } 28 | // console.log(localPath, basePath) 29 | load(localPath) 30 | load(basePath) 31 | } 32 | 33 | const mode = process.env.NODE_ENV 34 | 35 | // 这里的环境变量不会被下面覆盖,所以优先级最高 36 | // .env.production、.env.production.local、.env.development、.env.development.local 37 | if (mode) { 38 | loadEnv(mode) 39 | } 40 | 41 | //.env、.env.local 42 | loadEnv() 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | import {WboxProps, WimageProps, WdateProps, WspacerProps, WstackProps, WtextProps} from '../types/widget' 2 | import {getImage, hash} from './help' 3 | import {URLSchemeFrom} from './constants' 4 | 5 | type WidgetType = 'wbox' | 'wdate' | 'wimage' | 'wspacer' | 'wstack' | 'wtext' 6 | type WidgetProps = WboxProps | WdateProps | WspacerProps | WstackProps | WtextProps | WimageProps 7 | type Children = ((instance: T) => Promise)[] 8 | /**属性对应关系*/ 9 | type KeyMap = Record, () => void> 10 | 11 | class GenrateView { 12 | public static listWidget: ListWidget 13 | static setListWidget(listWidget: ListWidget): void { 14 | this.listWidget = listWidget 15 | } 16 | // 根组件 17 | static async wbox(props: WboxProps, ...children: Children) { 18 | const {background, spacing, href, updateDate, padding, onClick} = props 19 | try { 20 | // background 21 | isDefined(background) && (await setBackground(this.listWidget, background)) 22 | // spacing 23 | isDefined(spacing) && (this.listWidget.spacing = spacing) 24 | // href 25 | isDefined(href) && (this.listWidget.url = href) 26 | // updateDate 27 | isDefined(updateDate) && (this.listWidget.refreshAfterDate = updateDate) 28 | // padding 29 | isDefined(padding) && this.listWidget.setPadding(...padding) 30 | // onClick 31 | isDefined(onClick) && runOnClick(this.listWidget, onClick) 32 | await addChildren(this.listWidget, children) 33 | } catch (err) { 34 | console.error(err) 35 | } 36 | return this.listWidget 37 | } 38 | // 容器组件 39 | static wstack(props: WstackProps, ...children: Children) { 40 | return async ( 41 | parentInstance: Scriptable.Widget & { 42 | addStack(): WidgetStack 43 | }, 44 | ) => { 45 | const widgetStack = parentInstance.addStack() 46 | const { 47 | background, 48 | spacing, 49 | padding, 50 | width = 0, 51 | height = 0, 52 | borderRadius, 53 | borderWidth, 54 | borderColor, 55 | href, 56 | verticalAlign, 57 | flexDirection, 58 | onClick, 59 | } = props 60 | try { 61 | // background 62 | isDefined(background) && (await setBackground(widgetStack, background)) 63 | // spacing 64 | isDefined(spacing) && (widgetStack.spacing = spacing) 65 | // padding 66 | isDefined(padding) && widgetStack.setPadding(...padding) 67 | // borderRadius 68 | isDefined(borderRadius) && (widgetStack.cornerRadius = borderRadius) 69 | // borderWidth 70 | isDefined(borderWidth) && (widgetStack.borderWidth = borderWidth) 71 | // borderColor 72 | isDefined(borderColor) && (widgetStack.borderColor = getColor(borderColor)) 73 | // href 74 | isDefined(href) && (widgetStack.url = href) 75 | // width、height 76 | widgetStack.size = new Size(width, height) 77 | // verticalAlign 78 | const verticalAlignMap: KeyMap = { 79 | bottom: () => widgetStack.bottomAlignContent(), 80 | center: () => widgetStack.centerAlignContent(), 81 | top: () => widgetStack.topAlignContent(), 82 | } 83 | isDefined(verticalAlign) && verticalAlignMap[verticalAlign]() 84 | // flexDirection 85 | const flexDirectionMap: KeyMap = { 86 | row: () => widgetStack.layoutHorizontally(), 87 | column: () => widgetStack.layoutVertically(), 88 | } 89 | isDefined(flexDirection) && flexDirectionMap[flexDirection]() 90 | // onClick 91 | isDefined(onClick) && runOnClick(widgetStack, onClick) 92 | } catch (err) { 93 | console.error(err) 94 | } 95 | await addChildren(widgetStack, children) 96 | } 97 | } 98 | // 图片组件 99 | static wimage(props: WimageProps) { 100 | return async ( 101 | parentInstance: Scriptable.Widget & { 102 | addImage(image: Image): WidgetImage 103 | }, 104 | ) => { 105 | const { 106 | src, 107 | href, 108 | resizable, 109 | width = 0, 110 | height = 0, 111 | opacity, 112 | borderRadius, 113 | borderWidth, 114 | borderColor, 115 | containerRelativeShape, 116 | filter, 117 | imageAlign, 118 | mode, 119 | onClick, 120 | } = props 121 | 122 | let _image: Image = src as Image 123 | 124 | // src 为网络连接时 125 | typeof src === 'string' && isUrl(src) && (_image = await getImage({url: src})) 126 | 127 | // src 为 icon name 时 128 | typeof src === 'string' && !isUrl(src) && (_image = SFSymbol.named(src).image) 129 | const widgetImage = parentInstance.addImage(_image) 130 | widgetImage.image = _image 131 | 132 | try { 133 | // href 134 | isDefined(href) && (widgetImage.url = href) 135 | // resizable 136 | isDefined(resizable) && (widgetImage.resizable = resizable) 137 | // width、height 138 | widgetImage.imageSize = new Size(width, height) 139 | // opacity 140 | isDefined(opacity) && (widgetImage.imageOpacity = opacity) 141 | // borderRadius 142 | isDefined(borderRadius) && (widgetImage.cornerRadius = borderRadius) 143 | // borderWidth 144 | isDefined(borderWidth) && (widgetImage.borderWidth = borderWidth) 145 | // borderColor 146 | isDefined(borderColor) && (widgetImage.borderColor = getColor(borderColor)) 147 | // containerRelativeShape 148 | isDefined(containerRelativeShape) && (widgetImage.containerRelativeShape = containerRelativeShape) 149 | // filter 150 | isDefined(filter) && (widgetImage.tintColor = getColor(filter)) 151 | // imageAlign 152 | const imageAlignMap: KeyMap = { 153 | left: () => widgetImage.leftAlignImage(), 154 | center: () => widgetImage.centerAlignImage(), 155 | right: () => widgetImage.rightAlignImage(), 156 | } 157 | isDefined(imageAlign) && imageAlignMap[imageAlign]() 158 | // mode 159 | const modeMap: KeyMap = { 160 | fit: () => widgetImage.applyFittingContentMode(), 161 | fill: () => widgetImage.applyFillingContentMode(), 162 | } 163 | isDefined(mode) && modeMap[mode]() 164 | // onClick 165 | isDefined(onClick) && runOnClick(widgetImage, onClick) 166 | } catch (err) { 167 | console.error(err) 168 | } 169 | } 170 | } 171 | // 占位空格组件 172 | static wspacer(props: WspacerProps) { 173 | return async ( 174 | parentInstance: Scriptable.Widget & { 175 | addSpacer(length?: number): WidgetSpacer 176 | }, 177 | ) => { 178 | const widgetSpacer = parentInstance.addSpacer() 179 | const {length} = props 180 | try { 181 | // length 182 | isDefined(length) && (widgetSpacer.length = length) 183 | } catch (err) { 184 | console.error(err) 185 | } 186 | } 187 | } 188 | // 文字组件 189 | static wtext(props: WtextProps, ...children: string[]) { 190 | return async ( 191 | parentInstance: Scriptable.Widget & { 192 | addText(text?: string): WidgetText 193 | }, 194 | ) => { 195 | const widgetText = parentInstance.addText('') 196 | const { 197 | textColor, 198 | font, 199 | opacity, 200 | maxLine, 201 | scale, 202 | shadowColor, 203 | shadowRadius, 204 | shadowOffset, 205 | href, 206 | textAlign, 207 | onClick, 208 | } = props 209 | 210 | if (children && Array.isArray(children)) { 211 | widgetText.text = children.join('') 212 | } 213 | try { 214 | // textColor 215 | isDefined(textColor) && (widgetText.textColor = getColor(textColor)) 216 | // font 217 | isDefined(font) && (widgetText.font = typeof font === 'number' ? Font.systemFont(font) : font) 218 | // opacity 219 | isDefined(opacity) && (widgetText.textOpacity = opacity) 220 | // maxLine 221 | isDefined(maxLine) && (widgetText.lineLimit = maxLine) 222 | // scale 223 | isDefined(scale) && (widgetText.minimumScaleFactor = scale) 224 | // shadowColor 225 | isDefined(shadowColor) && (widgetText.shadowColor = getColor(shadowColor)) 226 | // shadowRadius 227 | isDefined(shadowRadius) && (widgetText.shadowRadius = shadowRadius) 228 | // shadowOffset 229 | isDefined(shadowOffset) && (widgetText.shadowOffset = shadowOffset) 230 | // href 231 | isDefined(href) && (widgetText.url = href) 232 | //textAlign 233 | const textAlignMap: KeyMap = { 234 | left: () => widgetText.leftAlignText(), 235 | center: () => widgetText.centerAlignText(), 236 | right: () => widgetText.rightAlignText(), 237 | } 238 | isDefined(textAlign) && textAlignMap[textAlign]() 239 | // onClick 240 | isDefined(onClick) && runOnClick(widgetText, onClick) 241 | } catch (err) { 242 | console.error(err) 243 | } 244 | } 245 | } 246 | // 日期组件 247 | static wdate(props: WdateProps) { 248 | return async ( 249 | parentInstance: Scriptable.Widget & { 250 | addDate(date: Date): WidgetDate 251 | }, 252 | ) => { 253 | const widgetDate = parentInstance.addDate(new Date()) 254 | const { 255 | date, 256 | mode, 257 | textColor, 258 | font, 259 | opacity, 260 | maxLine, 261 | scale, 262 | shadowColor, 263 | shadowRadius, 264 | shadowOffset, 265 | href, 266 | textAlign, 267 | onClick, 268 | } = props 269 | 270 | try { 271 | // date 272 | isDefined(date) && (widgetDate.date = date) 273 | // textColor 274 | isDefined(textColor) && (widgetDate.textColor = getColor(textColor)) 275 | // font 276 | isDefined(font) && (widgetDate.font = typeof font === 'number' ? Font.systemFont(font) : font) 277 | // opacity 278 | isDefined(opacity) && (widgetDate.textOpacity = opacity) 279 | // maxLine 280 | isDefined(maxLine) && (widgetDate.lineLimit = maxLine) 281 | // scale 282 | isDefined(scale) && (widgetDate.minimumScaleFactor = scale) 283 | // shadowColor 284 | isDefined(shadowColor) && (widgetDate.shadowColor = getColor(shadowColor)) 285 | // shadowRadius 286 | isDefined(shadowRadius) && (widgetDate.shadowRadius = shadowRadius) 287 | // shadowOffset 288 | isDefined(shadowOffset) && (widgetDate.shadowOffset = shadowOffset) 289 | // href 290 | isDefined(href) && (widgetDate.url = href) 291 | // mode 292 | const modeMap: KeyMap = { 293 | time: () => widgetDate.applyTimeStyle(), 294 | date: () => widgetDate.applyDateStyle(), 295 | relative: () => widgetDate.applyRelativeStyle(), 296 | offset: () => widgetDate.applyOffsetStyle(), 297 | timer: () => widgetDate.applyTimerStyle(), 298 | } 299 | isDefined(mode) && modeMap[mode]() 300 | // textAlign 301 | const textAlignMap: KeyMap = { 302 | left: () => widgetDate.leftAlignText(), 303 | center: () => widgetDate.centerAlignText(), 304 | right: () => widgetDate.rightAlignText(), 305 | } 306 | isDefined(textAlign) && textAlignMap[textAlign]() 307 | // onClick 308 | isDefined(onClick) && runOnClick(widgetDate, onClick) 309 | } catch (err) { 310 | console.error(err) 311 | } 312 | } 313 | } 314 | } 315 | 316 | const listWidget = new ListWidget() 317 | GenrateView.setListWidget(listWidget) 318 | export function h( 319 | type: WidgetType | (() => () => void), 320 | props?: WidgetProps, 321 | ...children: Children | string[] 322 | ): Promise | unknown { 323 | props = props || {} 324 | 325 | // 由于 Fragment 的存在,children 可能为多维数组混合,先把它展平 326 | const _children = flatteningArr(children as unknown[]) as typeof children 327 | 328 | switch (type) { 329 | case 'wbox': 330 | return GenrateView.wbox(props as WboxProps, ...(_children as Children)) 331 | break 332 | case 'wdate': 333 | return GenrateView.wdate(props as WdateProps) 334 | break 335 | case 'wimage': 336 | return GenrateView.wimage(props as WimageProps) 337 | break 338 | case 'wspacer': 339 | return GenrateView.wspacer(props as WspacerProps) 340 | break 341 | case 'wstack': 342 | return GenrateView.wstack(props as WstackProps, ...(_children as Children)) 343 | break 344 | case 'wtext': 345 | return GenrateView.wtext(props as WtextProps, ...(_children as string[])) 346 | break 347 | default: 348 | // custom component 349 | return type instanceof Function 350 | ? ((type as unknown) as (props: WidgetProps) => Promise)({children: _children, ...props}) 351 | : null 352 | break 353 | } 354 | } 355 | 356 | export function Fragment({children}: {children: typeof h[]}): typeof h[] { 357 | return children 358 | } 359 | 360 | /** 361 | * 展平所有维度数组 362 | * @param arr 数组 363 | */ 364 | function flatteningArr(arr: T[]): T[] { 365 | return [].concat( 366 | ...arr.map((item: T | T[]) => { 367 | return (Array.isArray(item) ? flatteningArr(item) : item) as never[] 368 | }), 369 | ) as T[] 370 | } 371 | 372 | /** 373 | * 输出真正颜色(比如string转color) 374 | * @param color 375 | */ 376 | function getColor(color: Color | string): Color { 377 | return typeof color === 'string' ? new Color(color, 1) : color 378 | } 379 | 380 | /** 381 | * 输出真正背景(比如string转color) 382 | * @param bg 输入背景参数 383 | */ 384 | async function getBackground(bg: Color | Image | LinearGradient | string): Promise { 385 | bg = (typeof bg === 'string' && !isUrl(bg)) || bg instanceof Color ? getColor(bg) : bg 386 | if (typeof bg === 'string') { 387 | bg = await getImage({url: bg}) 388 | } 389 | return bg 390 | } 391 | 392 | /** 393 | * 设置背景 394 | * @param widget 实例 395 | * @param bg 背景 396 | */ 397 | async function setBackground( 398 | widget: Scriptable.Widget & { 399 | backgroundColor: Color 400 | backgroundImage: Image 401 | backgroundGradient: LinearGradient 402 | }, 403 | bg: Color | Image | LinearGradient | string, 404 | ): Promise { 405 | const _bg = await getBackground(bg) 406 | if (_bg instanceof Color) { 407 | widget.backgroundColor = _bg 408 | } 409 | if (_bg instanceof Image) { 410 | widget.backgroundImage = _bg 411 | } 412 | if (_bg instanceof LinearGradient) { 413 | widget.backgroundGradient = _bg 414 | } 415 | } 416 | 417 | /** 418 | * 添加子组件列表(把当前实例传下去) 419 | * @param instance 当前实例 420 | * @param children 子组件列表 421 | */ 422 | async function addChildren(instance: T, children: Children): Promise { 423 | if (children && Array.isArray(children)) { 424 | for (const child of children) { 425 | child instanceof Function ? await child(instance) : '' 426 | } 427 | } 428 | } 429 | 430 | /** 431 | * 如果某值不是 undefined、null、NaN 则返回 true 432 | * @param value 值 433 | */ 434 | function isDefined(value: T | undefined | null): value is T { 435 | if (typeof value === 'number' && !isNaN(value)) { 436 | return true 437 | } 438 | return value !== undefined && value !== null 439 | } 440 | 441 | /** 442 | * 判断一个值是否为网络连接 443 | * @param value 值 444 | */ 445 | function isUrl(value: string): boolean { 446 | const reg = /^(http|https)\:\/\/[\w\W]+/ 447 | return reg.test(value) 448 | } 449 | 450 | /** 451 | * 执行点击事件 452 | * @param instance 当前实例 453 | * @param onClick 点击事件 454 | */ 455 | function runOnClick(instance: T, onClick: () => unknown) { 456 | const _eventId = hash(onClick.toString()) 457 | instance.url = `${URLScheme.forRunningScript()}?eventId=${encodeURIComponent(_eventId)}&from=${URLSchemeFrom.WIDGET}` 458 | const {eventId, from} = args.queryParameters 459 | if (eventId && eventId === _eventId && from === URLSchemeFrom.WIDGET) { 460 | onClick() 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /src/lib/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import ip from 'ip' 3 | import bodyParser from 'body-parser' 4 | import {port} from './constants' 5 | import chalk from 'chalk' 6 | import fs from 'fs' 7 | import path from 'path' 8 | const qrcode = require('qrcode-terminal') 9 | 10 | interface CreateServerParams { 11 | /**要映射的静态文件夹*/ 12 | staticDir: string 13 | 14 | /**是否显示二维码*/ 15 | showQrcode?: boolean 16 | } 17 | 18 | interface ConsoleApiBody { 19 | type: 'log' | 'warn' | 'error' 20 | data: unknown 21 | } 22 | 23 | enum ResCode { 24 | SUCCESS = 0, 25 | FAIL = 1, 26 | } 27 | 28 | interface Res { 29 | code: ResCode 30 | msg: string 31 | data?: T 32 | } 33 | 34 | /** 35 | * 创建服务器 36 | * @param staticDir 服务器映射静态文件夹 37 | */ 38 | export function createServer(params: CreateServerParams): {serverApi: string} { 39 | const {staticDir, showQrcode = true} = params 40 | const app = express() 41 | 42 | /** server api 地址*/ 43 | const serverApi = `http://${ip.address('public')}:${port}` 44 | 45 | // 解析请求参数 46 | app.use( 47 | bodyParser.urlencoded({ 48 | extended: false, 49 | }), 50 | ) 51 | 52 | app.use(bodyParser.json()) 53 | 54 | // 映射静态文件夹 55 | app.use(express.static(staticDir)) 56 | 57 | app.get('/', (req, res) => { 58 | let html = fs.readFileSync(path.resolve(__dirname, './static', './dev-help.html'), {encoding: 'utf8'}).toString() 59 | 60 | /**自动安装基础包并命名的代码*/ 61 | const installBasicCode = ` 62 | (async() => { 63 | const fm = FileManager[module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() 64 | const notify = new Notification() 65 | notify.sound = 'default' 66 | try { 67 | const req = new Request('${serverApi}/basic.js') 68 | const code = await req.loadString() 69 | const newFilename = module.filename.split('/').slice(0, -1).concat(['基础包.js']).join('/') 70 | fm.writeString( 71 | newFilename, 72 | \`// Variables used by Scriptable. 73 | // These must be at the very top of the file. Do not edit. 74 | // icon-color: deep-gray; icon-glyph: mobile-alt; 75 | \${code}\`) 76 | 77 | // 通知 78 | notify.title = '安装基础包成功' 79 | notify.schedule() 80 | 81 | fm.remove(module.filename) 82 | Safari.open("scriptable:///open?scriptName="+encodeURIComponent('基础包')); 83 | } catch(err) { 84 | console.error(err) 85 | notify.title = '安装基础包失败' 86 | notify.body = err.message || '' 87 | notify.schedule() 88 | } 89 | })() 90 | ` 91 | html = html.replace('@@code@@', installBasicCode) 92 | res.send(html) 93 | }) 94 | 95 | app.get('/basic.js', (req, res) => { 96 | const js = fs.readFileSync(path.resolve(__dirname, './static', './基础包.js'), {encoding: 'utf8'}).toString() 97 | 98 | res.send(js) 99 | }) 100 | 101 | app.post('/console', (req, res) => { 102 | const {type = 'log', data = ''} = req.body as ConsoleApiBody 103 | const logTime = new Date().toLocaleString().split(' ')[1] 104 | const logParams = [`[${type} ${logTime}]`, typeof data !== 'object' ? data : JSON.stringify(data, null, 2)] 105 | switch (type) { 106 | case 'log': 107 | console.log(chalk.green(...logParams)) 108 | break 109 | case 'warn': 110 | console.warn(chalk.yellow(...logParams)) 111 | break 112 | case 'error': 113 | console.error(chalk.red(...logParams)) 114 | break 115 | default: 116 | console.log(chalk.green(...logParams)) 117 | break 118 | } 119 | 120 | res.send({ 121 | code: ResCode.SUCCESS, 122 | msg: 'success', 123 | } as Res) 124 | }) 125 | 126 | app.listen(port) 127 | 128 | // console.clear() 129 | console.log(`手机访问 ${serverApi}`) 130 | showQrcode && qrcode.generate(serverApi, {small: true}) 131 | return {serverApi} 132 | } 133 | -------------------------------------------------------------------------------- /src/lib/static/dev-help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Scriptable开发引导 10 | 64 | 65 | 66 |
67 |
1、点击复制下方的代码
68 |
2、点击打开 Scriptable 粘贴运行
69 | 70 |
71 | 72 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/scripts/bili.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 哔哩粉丝 3 | * 改写于 https://github.com/im3x/Scriptables/blob/main/bilibili/fans.js 4 | */ 5 | import { 6 | getImage, 7 | request, 8 | showActionSheet, 9 | useStorage, 10 | showPreviewOptions, 11 | showModal, 12 | ResponseType, 13 | isLaunchInsideApp, 14 | } from '@app/lib/help' 15 | 16 | export interface BiliUpData { 17 | code: number 18 | message: string 19 | ttl: number 20 | data: { 21 | mid: number 22 | following: number 23 | whisper: number 24 | black: number 25 | follower: number 26 | } 27 | } 28 | 29 | const {setStorage, getStorage} = useStorage('bilibili-fans') 30 | 31 | class BiliFans { 32 | async init() { 33 | if (isLaunchInsideApp()) { 34 | return await this.showMenu() 35 | } 36 | const widget = (await this.render()) as ListWidget 37 | Script.setWidget(widget) 38 | Script.complete() 39 | } 40 | 41 | //渲染组件 42 | async render(): Promise { 43 | // up 主 id 44 | const upId = getStorage('up-id') || 0 45 | 46 | // 粉丝数 47 | let follower = -1 48 | try { 49 | // 响应数据 50 | const getUpDataRes = (await this.getUpData(upId)).data as BiliUpData 51 | follower = getUpDataRes.data.follower as number 52 | } catch (err) { 53 | console.warn('获取粉丝数失败') 54 | } 55 | 56 | // icon 57 | const icon = await getImage({url: 'https://www.bilibili.com/favicon.ico'}) 58 | 59 | // 粉丝数文字 60 | const FollowerText = () => { 61 | if (follower < 0) { 62 | return ( 63 | 64 | 请填写B站UP主的ID 65 | 66 | ) 67 | } else { 68 | return ( 69 | 70 | {this.toThousands(follower)} 71 | 72 | ) 73 | } 74 | } 75 | 76 | // 渲染 77 | return ( 78 | 79 | 80 | 81 | 82 | 83 | 哔哩哔哩粉丝 84 | 85 | 86 | 87 | 88 | 89 | 90 | 更新于:{this.nowTime()} 91 | 92 | 93 | ) 94 | } 95 | 96 | // 显示菜单 97 | async showMenu(): Promise { 98 | const selectIndex = await showActionSheet({ 99 | title: '菜单', 100 | itemList: ['设置 up 主 id', '预览尺寸'], 101 | }) 102 | switch (selectIndex) { 103 | case 0: 104 | // 设置 up 主 id 105 | const {cancel, texts} = await showModal({ 106 | title: '请输入 up 主的 id', 107 | inputItems: [ 108 | { 109 | text: getStorage('up-id') || '', 110 | placeholder: '去网页版 up 主页,可以看到 id', 111 | }, 112 | ], 113 | }) 114 | if (cancel) return 115 | // 保存 id 116 | if (texts && texts[0]) setStorage('up-id', texts[0]) 117 | break 118 | case 1: 119 | // 预览尺寸 120 | await showPreviewOptions(this.render.bind(this)) 121 | break 122 | } 123 | } 124 | 125 | // 获取b站up数据 126 | async getUpData(id: number): Promise> { 127 | return await request({ 128 | url: `http://api.bilibili.com/x/relation/stat?vmid=${id}`, 129 | dataType: 'json', 130 | }) 131 | } 132 | 133 | //格式化粉丝数量,加入千分号 134 | toThousands(num: number): string { 135 | return (num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') 136 | } 137 | 138 | //根据粉丝数量返回不同的字体大小 139 | getFontsize(num: number) { 140 | if (num < 99) { 141 | return 38 142 | } else if (num < 9999 && num > 100) { 143 | return 30 144 | } else if (num < 99999 && num > 10000) { 145 | return 28 146 | } else if (num < 999999 && num > 100000) { 147 | return 24 148 | } else if (num < 9999999 && num > 1000000) { 149 | return 22 150 | } else { 151 | return 20 152 | } 153 | } 154 | 155 | //当前时间 156 | nowTime(): string { 157 | const date = new Date() 158 | return date.toLocaleTimeString('chinese', {hour12: false}) 159 | } 160 | } 161 | 162 | EndAwait(() => new BiliFans().init()) 163 | -------------------------------------------------------------------------------- /src/scripts/china10010.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 联通话费流量查询小部件 3 | */ 4 | 5 | import { 6 | isLaunchInsideApp, 7 | setTransparentBackground, 8 | showActionSheet, 9 | showModal, 10 | showNotification, 11 | showPreviewOptions, 12 | useStorage, 13 | request, 14 | sleep, 15 | } from '@app/lib/help' 16 | import {FC} from 'react' 17 | import {WtextProps, WstackProps} from '@app/types/widget' 18 | 19 | /**手机卡数据列表*/ 20 | interface PhoneDatas { 21 | data: { 22 | dataList: PhoneData[] 23 | } 24 | } 25 | 26 | /**手机卡数据*/ 27 | interface PhoneData { 28 | pointUpdateTimeStamp: string 29 | paperwork4: string 30 | buttonBacImageUrlBig: string 31 | type: PhoneDataType 32 | remainTitle: string 33 | buttonText7: string 34 | buttonLinkMode: string 35 | displayTime: string 36 | buttonText7TextColor: string 37 | buttonBacImageUrlSmall: string 38 | button: string 39 | remainTitleColoer: string 40 | buttonBacImageUrl: string 41 | buttonTextColor: string 42 | isShake: string 43 | number: string 44 | isWarn?: string 45 | paperwork4Coloer: string 46 | url: string 47 | buttonUrl7: string 48 | unit: string 49 | button7LinkMode: string 50 | persent: string 51 | persentColor: string 52 | buttonAddress: string 53 | usedTitle: string 54 | ballRippleColor1: string 55 | ballRippleColor2: string 56 | markerImg?: string 57 | warningTextColor?: string 58 | warningPointColor?: string 59 | } 60 | 61 | /**有用的手机卡数据*/ 62 | interface UsefulPhoneData { 63 | /**类型*/ 64 | type: PhoneDataType 65 | 66 | /**剩余百分比数字*/ 67 | percent: number 68 | 69 | /**单位*/ 70 | unit: string 71 | 72 | /**剩余量数字*/ 73 | count: number 74 | 75 | /**描述*/ 76 | label: string 77 | } 78 | 79 | /**手机卡数据类型*/ 80 | enum PhoneDataType { 81 | /**流量*/ 82 | FLOW = 'flow', 83 | 84 | /**话费*/ 85 | FEE = 'fee', 86 | 87 | /**语音*/ 88 | VOICE = 'voice', 89 | 90 | /**积分*/ 91 | POINT = 'point', 92 | 93 | /**信用分*/ 94 | CREDIT = 'credit', 95 | 96 | /**电子券*/ 97 | WOPAY = 'woPay', 98 | } 99 | 100 | const typeDesc: Record = { 101 | [PhoneDataType.FLOW]: '流量', 102 | [PhoneDataType.FEE]: '话费', 103 | [PhoneDataType.VOICE]: '语音', 104 | [PhoneDataType.POINT]: '积分', 105 | [PhoneDataType.CREDIT]: '信用分', 106 | [PhoneDataType.WOPAY]: '电子券', 107 | } 108 | 109 | // 格式化数字 110 | const formatNum = (num: number) => parseFloat(Number(num).toFixed(1)) 111 | 112 | const {setStorage, getStorage} = useStorage('china10010-xiaoming') 113 | 114 | /**默认背景颜色*/ 115 | const defaultBgColor = Color.dynamic(new Color('#ffffff', 1), new Color('#000000', 1)) 116 | 117 | /**文字颜色*/ 118 | const textColor = getStorage('textColor') || Color.dynamic(new Color('#000000', 1), new Color('#dddddd', 1)) 119 | 120 | /**透明背景*/ 121 | const transparentBg: Image | Color = getStorage('transparentBg') || defaultBgColor 122 | 123 | /**背景颜色或背景图链接*/ 124 | const boxBg = getStorage('boxBg') || defaultBgColor 125 | 126 | class China10010 { 127 | async init() { 128 | if (isLaunchInsideApp()) { 129 | return await this.showMenu() 130 | } 131 | const widget = (await this.render()) as ListWidget 132 | Script.setWidget(widget) 133 | Script.complete() 134 | } 135 | 136 | //渲染组件 137 | async render(): Promise { 138 | if (isLaunchInsideApp()) { 139 | await showNotification({title: '稍等片刻', body: '小部件渲染中...', sound: 'alert'}) 140 | } 141 | // 多久(毫秒)更新一次小部件(默认3分钟) 142 | const updateInterval = 1 * 60 * 1000 143 | 144 | // 渲染尺寸 145 | const size = config.widgetFamily 146 | 147 | // 获取数据 148 | const usefulPhoneDatas = await this.getUserfulPhoneData(getStorage('phoneNumber') || '') 149 | 150 | if (typeof usefulPhoneDatas === 'string') { 151 | return ( 152 | 153 | 154 | {usefulPhoneDatas} 155 | 156 | 157 | ) 158 | } 159 | console.log(usefulPhoneDatas) 160 | return ( 161 | 166 | 167 | 168 | {/* 标题和logo */} 169 | 170 | 176 | 177 | 178 | 中国联通 179 | 180 | 181 | 182 | {/* 内容 */} 183 | {size === 'small' && this.renderSmall(usefulPhoneDatas)} 184 | {size === 'medium' && this.renderMedium(usefulPhoneDatas)} 185 | {size === 'large' && this.renderLarge(usefulPhoneDatas)} 186 | 187 | 188 | ) 189 | } 190 | 191 | // 渲染小尺寸 192 | renderSmall(usefulPhoneDatas: UsefulPhoneData[]) { 193 | // 流量 194 | const flow = usefulPhoneDatas.find(item => item.type === PhoneDataType.FLOW) as UsefulPhoneData 195 | // 话费 196 | const fee = usefulPhoneDatas.find(item => item.type === PhoneDataType.FEE) as UsefulPhoneData 197 | return ( 198 | <> 199 | 200 | 剩余流量{formatNum(flow.count || 0) + flow.unit} 201 | 202 | 203 | 204 | 剩余话费{formatNum(fee.count || 0) + fee.unit} 205 | 206 | 207 | 208 | ) 209 | } 210 | 211 | // 渲染中尺寸 212 | renderMedium(usefulPhoneDatas: UsefulPhoneData[]) { 213 | const showDataType: PhoneDataType[] = [PhoneDataType.FLOW, PhoneDataType.FEE, PhoneDataType.VOICE] 214 | return this.renderLarge(usefulPhoneDatas.filter(data => showDataType.indexOf(data.type) >= 0)) 215 | } 216 | 217 | // 渲染大尺寸 218 | renderLarge(usefulPhoneDatas: UsefulPhoneData[]) { 219 | /**进度条*/ 220 | const Progress: FC<{ 221 | color: WstackProps['background'] 222 | bgcolor: WstackProps['background'] 223 | progress: number 224 | width: number 225 | height: number 226 | borderRadius?: number 227 | }> = ({...props}) => { 228 | const {color, bgcolor, progress, width, height, borderRadius = 0} = props 229 | return ( 230 | 231 | 232 | 233 | 234 | {progress < 1 && } 235 | 236 | ) 237 | } 238 | 239 | /**表格格子*/ 240 | const TableGrid: FC< 241 | WtextProps & {text: string | React.ReactNode; width: number; align: 'left' | 'center' | 'right'} 242 | > = ({text, width, align, ...props}) => ( 243 | 244 | {(align === 'center' || align === 'right') && } 245 | {typeof text === 'string' ? ( 246 | 247 | {text} 248 | 249 | ) : ( 250 | text 251 | )} 252 | {(align === 'center' || align === 'left') && } 253 | 254 | ) 255 | 256 | /**表格行*/ 257 | const TableRow: FC = ({texts, ...props}) => ( 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | ) 266 | return ( 267 | <> 268 | 269 | {usefulPhoneDatas.map(item => ( 270 | <> 271 | 272 | 287 | 288 | ))} 289 | 290 | 291 | ) 292 | } 293 | 294 | // 显示菜单 295 | async showMenu() { 296 | const selectIndex = await showActionSheet({ 297 | title: '菜单', 298 | itemList: ['登录获取cookie', '设置手机号和cookie', '设置颜色', '设置透明背景', '预览组件'], 299 | }) 300 | switch (selectIndex) { 301 | case 0: 302 | const {cancel: cancelLogin} = await showModal({ 303 | title: '为什么要登录', 304 | content: 305 | '获取手机号码信息需要 cookie,而 cookie 不登录获取不到\n\n登录完成后,关闭网页,网页会再自动打开\n\n此时点击底部按钮复制 cookie ,然后关网页去设置cookie\n\n若 cookie 失效,再次登录复制即可', 306 | confirmText: '去登录', 307 | }) 308 | if (cancelLogin) return 309 | const loginUrl = 'http://wap.10010.com/mobileService/myunicom.htm' 310 | const webview = new WebView() 311 | await webview.loadURL(loginUrl) 312 | await webview.present() 313 | 314 | /**循环插入脚本等待时间,单位:毫秒*/ 315 | const sleepTime = 1000 316 | 317 | /**循环时间统计,单位:毫秒*/ 318 | let waitTimeCount = 0 319 | 320 | /**最大循环时间,单位:毫秒*/ 321 | const maxWaitTime = 10 * 60 * sleepTime 322 | 323 | while (true) { 324 | if (waitTimeCount >= maxWaitTime) break 325 | const {isAddCookieBtn} = (await webview.evaluateJavaScript(` 326 | window.isAddCookieBtn = false 327 | if (document.cookie.match('jsessionid')) { 328 | const copyWrap = document.createElement('div') 329 | copyWrap.innerHTML = \` 330 |
复制cookie
331 | \` 332 | function copy(text) { 333 | var input = document.createElement('input'); 334 | input.setAttribute('value', text); 335 | document.body.appendChild(input); 336 | input.select(); 337 | var result = document.execCommand('copy'); 338 | document.body.removeChild(input); 339 | return result; 340 | } 341 | document.body.appendChild(copyWrap) 342 | const copyBtn = document.querySelector('#copy-btn') 343 | copyBtn.onclick = () => { 344 | copy(document.cookie) 345 | copyBtn.innerText = '复制成功' 346 | copyBtn.style.background = 'green' 347 | } 348 | window.isAddCookieBtn = true 349 | } 350 | Object.assign({}, {isAddCookieBtn: window.isAddCookieBtn}) 351 | `)) as {isAddCookieBtn: boolean} 352 | if (isAddCookieBtn) break 353 | await sleep(sleepTime) 354 | waitTimeCount += sleepTime 355 | } 356 | await webview.present() 357 | break 358 | case 1: 359 | const {texts: phoneInfo, cancel: phoneInfoCancel} = await showModal({ 360 | title: '设置手机号和cookie', 361 | content: '请务必先登录,在登录处复制好 cookie 再来,不懂就仔细看登录说明', 362 | inputItems: [ 363 | { 364 | text: getStorage('phoneNumber') || '', 365 | placeholder: '这里填你的手机号', 366 | }, 367 | { 368 | text: getStorage('cookie') || '', 369 | placeholder: '这里填cookie', 370 | }, 371 | ], 372 | }) 373 | if (phoneInfoCancel) return 374 | setStorage('phoneNumber', phoneInfo[0]) 375 | setStorage('cookie', phoneInfo[1]) 376 | await showNotification({title: '设置完成', sound: 'default'}) 377 | break 378 | case 2: 379 | const {texts, cancel} = await showModal({ 380 | title: '设置全局背景和颜色', 381 | content: '如果为空,则还原默认', 382 | inputItems: [ 383 | { 384 | text: getStorage('boxBg') || '', 385 | placeholder: '全局背景:可以是颜色、图链接', 386 | }, 387 | { 388 | text: getStorage('textColor') || '', 389 | placeholder: '这里填文字颜色', 390 | }, 391 | ], 392 | }) 393 | if (cancel) return 394 | setStorage('boxBg', texts[0]) 395 | setStorage('textColor', texts[1]) 396 | await showNotification({title: '设置完成', sound: 'default'}) 397 | break 398 | case 3: 399 | const img: Image | null = (await setTransparentBackground()) || null 400 | if (img) { 401 | setStorage('transparentBg', img) 402 | setStorage('boxBg', '透明背景') 403 | await showNotification({title: '设置透明背景成功', sound: 'default'}) 404 | } 405 | break 406 | case 4: 407 | await showPreviewOptions(this.render.bind(this)) 408 | break 409 | } 410 | } 411 | 412 | // 获取手机卡数据 413 | async getUserfulPhoneData(phoneNumber: string): Promise { 414 | if (!phoneNumber) return '请设置手机号' 415 | if (!isLaunchInsideApp() && !getStorage('cookie')) return 'cookie 不存在,请先登录' 416 | const api = `https://wap.10010.com/mobileService/home/queryUserInfoSeven.htm?version=iphone_c@7.0403&desmobiel=${phoneNumber}&showType=3` 417 | // 获取手机卡信息列表 418 | const res = await request({ 419 | url: api, 420 | dataType: 'text', 421 | header: { 422 | 'user-agent': 423 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', 424 | cookie: getStorage('cookie') || '', 425 | }, 426 | }) 427 | // isLaunchInsideApp() && cookie && setStorage('cookie', cookie) 428 | let usefulPhoneDatas: UsefulPhoneData[] = [] 429 | try { 430 | const phoneDatas: PhoneData[] = (JSON.parse(res.data || '') as PhoneDatas).data.dataList || [] 431 | // 提取有用的信息 432 | usefulPhoneDatas = phoneDatas.map(info => { 433 | const percent = info.usedTitle.replace(/(已用|剩余)([\d\.]+)?\%/, (...args) => { 434 | return args[1] === '剩余' ? args[2] : 100 - args[2] 435 | }) 436 | return { 437 | type: info.type, 438 | percent: Number(percent) > 100 ? 100 : Number(percent), 439 | unit: info.unit, 440 | count: Number(info.number), 441 | label: info.remainTitle, 442 | } 443 | }) 444 | } catch (err) { 445 | console.warn(`获取联通卡信息失败: ${err}`) 446 | await showNotification({title: '获取联通卡信息失败', body: '检查一下网络,或重新登录', sound: 'failure'}) 447 | return '获取联通卡信息失败\n检查一下网络,或重新登录' 448 | } 449 | return usefulPhoneDatas 450 | } 451 | } 452 | 453 | EndAwait(() => new China10010().init()) 454 | -------------------------------------------------------------------------------- /src/scripts/funds.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 基金小部件 3 | */ 4 | 5 | import { 6 | isLaunchInsideApp, 7 | request, 8 | ResponseType, 9 | setTransparentBackground, 10 | showActionSheet, 11 | showModal, 12 | showNotification, 13 | showPreviewOptions, 14 | useStorage, 15 | } from '@app/lib/help' 16 | 17 | interface FundInfo { 18 | /**基金代码*/ 19 | FCODE: string 20 | 21 | /**基金名字*/ 22 | SHORTNAME: string 23 | 24 | /**日期*/ 25 | PDATE: string 26 | 27 | /**单位净值数值*/ 28 | NAV: string 29 | 30 | /**累计净值数值*/ 31 | ACCNAV: string 32 | 33 | /**单位净值涨跌百分比*/ 34 | NAVCHGRT: string 35 | 36 | /**单位净值估算数值*/ 37 | GSZ: string 38 | 39 | /**单位净值估算涨跌百分比*/ 40 | GSZZL: string 41 | 42 | /**更新时间*/ 43 | GZTIME: string 44 | NEWPRICE: string 45 | CHANGERATIO: string 46 | ZJL: string 47 | HQDATE: string 48 | ISHAVEREDPACKET: boolean 49 | } 50 | 51 | interface FundsData { 52 | Datas: FundInfo[] 53 | ErrCode: number 54 | Success: boolean 55 | ErrMsg: null | string 56 | Message: null | string 57 | ErrorCode: string 58 | ErrorMessage: null | string 59 | ErrorMsgLst: null | string 60 | TotalCount: number 61 | Expansion: { 62 | GZTIME: string 63 | FSRQ: string 64 | } 65 | } 66 | 67 | const {setStorage, getStorage} = useStorage('Funds') 68 | const transparentBg: Image | string = getStorage('transparentBg') || '#fff' 69 | const textColor = getStorage('textColor') || (transparentBg === '#fff' ? '#000' : '#fff') 70 | const bgColor = getStorage('bgColor') || '#ffffff00' 71 | const fundsCode = getStorage('fundsCode') || [] 72 | 73 | /**生成表格列组件*/ 74 | function getFundColumn(fundsInfo: FundInfo[], title: string, keyName: keyof FundInfo) { 75 | // 数值个性化 76 | function showValue(value: FundInfo[keyof FundInfo]): Partial> { 77 | return { 78 | // 涨跌幅文字特别处理(涨绿,跌红) 79 | GSZZL: ( 80 | 81 | {value + '%'} 82 | 83 | ), 84 | } 85 | } 86 | return ( 87 | 88 | 89 | {title} 90 | 91 | {fundsInfo.map(fundInfo => { 92 | const value = fundInfo[keyName] 93 | return ( 94 | <> 95 | 96 | {showValue(value)[keyName] || ( 97 | 98 | {value} 99 | 100 | )} 101 | 102 | ) 103 | })} 104 | 105 | ) 106 | } 107 | 108 | class Funds { 109 | async init() { 110 | if (isLaunchInsideApp()) { 111 | return await this.showMenu() 112 | } 113 | const widget = (await this.render()) as ListWidget 114 | Script.setWidget(widget) 115 | Script.complete() 116 | } 117 | 118 | //渲染组件 119 | async render(): Promise { 120 | let fundsInfo: FundInfo[] = [] 121 | try { 122 | const result = (await this.getFundsData(this.getFundsCode())).data as FundsData 123 | fundsInfo = result.Datas || [] 124 | } catch (err) {} 125 | 126 | return ( 127 | 128 | 129 | 130 | {getFundColumn(fundsInfo, '基金名字', 'SHORTNAME')} 131 | 132 | {getFundColumn(fundsInfo, '估算净值', 'GSZ')} 133 | 134 | {getFundColumn(fundsInfo, '涨跌幅', 'GSZZL')} 135 | 136 | 137 | 138 | 139 | 140 | 更新时间: {new Date().toLocaleTimeString('chinese', {hour12: false})} 141 | 142 | 143 | 144 | 145 | 146 | ) 147 | } 148 | 149 | // 显示菜单 150 | async showMenu() { 151 | const selectIndex = await showActionSheet({ 152 | title: '菜单', 153 | itemList: ['设置基金代码', '预览组件', '设置透明背景', '设置文字和背景颜色'], 154 | }) 155 | switch (selectIndex) { 156 | case 0: 157 | const {texts: fundsCodeTexts} = await showModal({ 158 | title: '设置基金代码', 159 | content: '最多只能设置9个,在中尺寸下只显示前3个, 多个代码用逗号隔开。例如:002207, 002446, 161005', 160 | inputItems: [ 161 | { 162 | text: (getStorage('fundsCode') || []).join(', '), 163 | placeholder: '例如:002207, 002446, 161005', 164 | }, 165 | ], 166 | }) 167 | if (fundsCodeTexts[0]) { 168 | const fundsCode = fundsCodeTexts[0].match(/[\d]{6}/g) || [] 169 | setStorage('fundsCode', fundsCode) 170 | } 171 | break 172 | case 1: 173 | await showPreviewOptions(this.render.bind(this)) 174 | break 175 | case 2: 176 | const img: Image | null = (await setTransparentBackground()) || null 177 | img && setStorage('transparentBg', img) 178 | img && (await showNotification({title: '设置透明背景成功', sound: 'default'})) 179 | break 180 | case 3: 181 | const {texts} = await showModal({ 182 | title: '设置文字和背景颜色', 183 | content: '黑色是#000,白色是#fff,半透明白是 #ffffff88, 半透明黑是 #00000088', 184 | inputItems: [ 185 | { 186 | placeholder: `文字颜色,${ 187 | getStorage('textColor') ? '当前是' + getStorage('textColor') + ',' : '' 188 | }默认黑色#000`, 189 | }, 190 | { 191 | placeholder: `背景颜色,${ 192 | getStorage('bgColor') ? '当前是' + getStorage('bgColor') + ',' : '' 193 | }默认白色#fff`, 194 | }, 195 | ], 196 | }) 197 | if (texts[0]) setStorage('textColor', texts[0]) 198 | if (texts[1]) setStorage('bgColor', texts[1]) 199 | break 200 | } 201 | } 202 | // 获取基金代码数组 203 | getFundsCode(): string[] { 204 | // 大号组件显示9个 205 | // 中号组件显示3个 206 | const defaultFundsCode = ['002207', '002446', '161005', '163406', '008282', '001790', '008641', '001838', '001475'] 207 | const _fundsCode = fundsCode.length > 0 ? fundsCode : defaultFundsCode 208 | return config.widgetFamily === 'medium' ? _fundsCode.slice(0, 3) : _fundsCode.slice(0, 9) 209 | } 210 | 211 | // 生成 device id 212 | getDeviceId(): string { 213 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 214 | const r = (Math.random() * 16) | 0 215 | const v = c == 'x' ? r : (r & 0x3) | 0x8 216 | return v.toString(16) 217 | }) 218 | } 219 | 220 | // 获取基金数据 221 | async getFundsData(fundsId: string[]): Promise> { 222 | return request({ 223 | url: `https://fundmobapi.eastmoney.com/FundMNewApi/FundMNFInfo?pageIndex=1&pageSize=100&plat=Android&appType=ttjj&product=EFund&Version=1&deviceid=${this.getDeviceId()}&Fcodes=${fundsId.join( 224 | ',', 225 | )}`, 226 | dataType: 'json', 227 | }) 228 | } 229 | } 230 | 231 | EndAwait(() => new Funds().init()) 232 | -------------------------------------------------------------------------------- /src/scripts/helloWorld.tsx: -------------------------------------------------------------------------------- 1 | class HelloWorld { 2 | async init() { 3 | // ListWidget 实例 4 | const widget = (await this.render()) as ListWidget 5 | // 注册小部件 6 | Script.setWidget(widget) 7 | // 调试用 8 | !config.runsInWidget && (await widget.presentMedium()) 9 | // 脚本结束 10 | Script.complete() 11 | } 12 | async render(): Promise { 13 | return ( 14 | 15 | 16 | 17 | Hello World 18 | 19 | 20 | 21 | ) 22 | } 23 | } 24 | 25 | // 从 process.env 下可以加载 .env 文件的键值对 26 | // 详见 https://github.com/2214962083/ios-scriptable-tsx/blob/master/docs/config.md#env-config 27 | console.log(process.env.HELLO + ',' + process.env.MOMENT) 28 | 29 | new HelloWorld().init() 30 | -------------------------------------------------------------------------------- /src/scripts/launcher.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 个性磁铁小部件 3 | */ 4 | 5 | import { 6 | isLaunchInsideApp, 7 | setTransparentBackground, 8 | showActionSheet, 9 | showModal, 10 | showNotification, 11 | showPreviewOptions, 12 | useStorage, 13 | } from '@app/lib/help' 14 | import {Col} from '@app/lib/components' 15 | import {WstackProps} from '@app/types/widget' 16 | import {FC} from 'react' 17 | 18 | const {setStorage, getStorage} = useStorage('luancher-xiaoming') 19 | 20 | /**文字颜色*/ 21 | const textColor = getStorage('textColor') || '#ffffff' 22 | 23 | /**所有格子统一颜色*/ 24 | const gridColor: string | null = getStorage('gridColor') 25 | 26 | /**背景颜色或背景图链接*/ 27 | const boxBg = getStorage('boxBg') || '#222222' 28 | 29 | /**透明背景*/ 30 | const transparentBg: Image | string = getStorage('transparentBg') || '#222222' 31 | 32 | /**格子间隔*/ 33 | const space = getStorage('space') || 1 34 | 35 | // 好看的颜色 36 | const colors = { 37 | red: '#e54d42', 38 | orange: '#f37b1d', 39 | yellow: '#fbbd08', 40 | olive: '#8dc63f', 41 | green: '#39b54a', 42 | cyan: '#1cbbb4', 43 | blue: '#0081ff', 44 | purple: '#6739b6', 45 | mauve: '#9c26b0', 46 | pink: '#e03997', 47 | brown: '#a5673f', 48 | grey: '#8799a3', 49 | black: '#000000', 50 | } 51 | 52 | /**格子组件参数*/ 53 | interface GridProps { 54 | /** icon 名字*/ 55 | iconName?: string 56 | 57 | /**格子背景*/ 58 | background?: WstackProps['background'] 59 | 60 | /**文字*/ 61 | text?: string 62 | 63 | /**点击格子跳转的链接*/ 64 | href?: string 65 | } 66 | 67 | /**格子组件*/ 68 | const Grid: FC = ({...props}) => { 69 | const {iconName, background, text, href} = props 70 | const bgColors: string[] = Object.values(colors) 71 | const bgRandom = Math.floor(Math.random() * bgColors.length) 72 | const bgColor = bgColors[bgRandom] 73 | return ( 74 | 75 | 80 | {iconName && } 81 | 82 | {(!iconName || text) && ( 83 | 84 | {text || ''} 85 | 86 | )} 87 | 88 | 89 | ) 90 | } 91 | 92 | /**间隔组件*/ 93 | const Space: FC = () => 94 | 95 | class Launcher { 96 | private config: GridProps[] 97 | constructor(config: GridProps[]) { 98 | this.config = config 99 | } 100 | async init() { 101 | if (isLaunchInsideApp()) { 102 | return await this.showMenu() 103 | } 104 | const widget = (await this.render()) as ListWidget 105 | Script.setWidget(widget) 106 | Script.complete() 107 | } 108 | 109 | //渲染组件 110 | async render(): Promise { 111 | if (isLaunchInsideApp()) { 112 | await showNotification({title: '稍等片刻', body: '小部件渲染中...', sound: 'alert'}) 113 | } 114 | // 多久(毫秒)更新一次小部件(默认24小时) 115 | const updateInterval = 24 * 60 * 60 * 1000 116 | // 渲染尺寸 117 | const size = config.widgetFamily 118 | return ( 119 | 125 | {size === 'small' && this.renderSmall()} 126 | {size === 'medium' && this.renderMedium()} 127 | {size === 'large' && this.renderLarge()} 128 | 129 | ) 130 | } 131 | 132 | // 渲染小尺寸 133 | renderSmall() { 134 | return ( 135 | 136 | 137 | 138 | 139 | 140 | ) 141 | } 142 | 143 | // 渲染中尺寸 144 | renderMedium() { 145 | return ( 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | ) 164 | } 165 | 166 | // 渲染大尺寸 167 | renderLarge() { 168 | return ( 169 | 170 | {/* 上半部分 */} 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | {/* 下半部分 */} 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | ) 213 | } 214 | 215 | // 显示菜单 216 | async showMenu() { 217 | const selectIndex = await showActionSheet({ 218 | title: '菜单', 219 | itemList: ['设置全局背景和颜色', '设置透明背景', '预览组件'], 220 | }) 221 | switch (selectIndex) { 222 | case 0: 223 | const {texts, cancel} = await showModal({ 224 | title: '设置全局背景和颜色', 225 | content: 226 | '此处不能修改单个格子的风格,只能统一覆盖,如果为空,则还原默认,只有在设置格子半透明下(如#00000055)才能看到背景', 227 | inputItems: [ 228 | { 229 | text: getStorage('boxBg') || '', 230 | placeholder: '这里填全局背景,可以是颜色、图片链接', 231 | }, 232 | { 233 | text: getStorage('gridColor') || '', 234 | placeholder: '这里填所有格子颜色', 235 | }, 236 | { 237 | text: getStorage('textColor') || '', 238 | placeholder: '这里填文字颜色', 239 | }, 240 | { 241 | text: String(getStorage('space')) || '', 242 | placeholder: '格子间间隔,默认是1', 243 | }, 244 | ], 245 | }) 246 | if (cancel) return 247 | setStorage('boxBg', texts[0]) 248 | setStorage('gridColor', texts[1]) 249 | setStorage('textColor', texts[2]) 250 | setStorage('space', texts[3]) 251 | await showNotification({title: '设置完成', sound: 'default'}) 252 | break 253 | case 1: 254 | const img: Image | null = (await setTransparentBackground()) || null 255 | if (img) { 256 | setStorage('transparentBg', img) 257 | setStorage('boxBg', '透明背景') 258 | await showNotification({title: '设置透明背景成功', sound: 'default'}) 259 | } 260 | break 261 | case 2: 262 | await showPreviewOptions(this.render.bind(this)) 263 | break 264 | } 265 | } 266 | } 267 | 268 | const luancherConfig: GridProps[] = [ 269 | { 270 | href: 'weixin://scanqrcode', 271 | background: colors.green, 272 | iconName: 'barcode.viewfinder', 273 | text: '微信扫一扫', 274 | }, 275 | { 276 | href: 'alipayqr://platformapi/startapp?saId=10000007', 277 | background: colors.blue, 278 | iconName: 'barcode.viewfinder', 279 | text: '支付宝', 280 | }, 281 | { 282 | href: 'weixin://', 283 | background: colors.green, 284 | iconName: 'message.fill', 285 | text: '微信', 286 | }, 287 | { 288 | href: 'orpheuswidget://', 289 | background: colors.red, 290 | iconName: 'music.note', 291 | text: '网易云', 292 | }, 293 | { 294 | href: 'alipay://platformapi/startapp?appId=20000056', 295 | background: colors.blue, 296 | iconName: 'qrcode', 297 | text: '付款码', 298 | }, 299 | { 300 | href: 'mqq://', 301 | background: colors.blue, 302 | iconName: 'paperplane.fill', 303 | text: 'QQ', 304 | }, 305 | { 306 | href: 'sinaweibo://', 307 | background: colors.red, 308 | iconName: 'eye', 309 | text: '微博', 310 | }, 311 | { 312 | href: 'bilibili://', 313 | background: colors.pink, 314 | iconName: 'tv', 315 | text: '哔哩哔哩', 316 | }, 317 | { 318 | href: 'zhihu://', 319 | //background: colors.blue, 320 | background: `https://bing.ioliu.cn/v1/rand?w=600&h=200×tamp=${Date.now()}`, 321 | iconName: 'questionmark', 322 | text: '知乎', 323 | }, 324 | { 325 | href: 'iqiyi://', 326 | background: colors.green, 327 | iconName: 'film', 328 | text: '爱奇艺', 329 | }, 330 | { 331 | href: 'tencentlaunch1104466820://', 332 | background: colors.orange, 333 | iconName: 'gamecontroller', 334 | text: '王者', 335 | }, 336 | ] 337 | EndAwait(() => new Launcher(luancherConfig).init()) 338 | -------------------------------------------------------------------------------- /src/scripts/music163.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 网易云歌单小部件 3 | */ 4 | 5 | import { 6 | getRandomInt, 7 | isLaunchInsideApp, 8 | request, 9 | showActionSheet, 10 | showModal, 11 | showNotification, 12 | showPreviewOptions, 13 | useStorage, 14 | } from '@app/lib/help' 15 | import {Col} from '@app/lib/components' 16 | import {WstackProps} from '@app/types/widget' 17 | import {FC} from 'react' 18 | 19 | /**网易云歌单数据格式*/ 20 | interface NetmusicListData { 21 | code: number 22 | playlist: Playlist 23 | } 24 | 25 | interface Playlist { 26 | /**歌曲信息存放*/ 27 | tracks: Track[] 28 | } 29 | 30 | interface Track { 31 | name: string 32 | id: number 33 | pst: number 34 | t: number 35 | alia: string[] 36 | pop: number 37 | st: number 38 | rt?: string 39 | fee: number 40 | v: number 41 | crbt?: unknown 42 | cf: string 43 | 44 | /**该首歌曲信息*/ 45 | al: MusicInfo 46 | dt: number 47 | a?: unknown 48 | cd: string 49 | no: number 50 | rtUrl?: unknown 51 | ftype: number 52 | rtUrls: unknown[] 53 | djId: number 54 | copyright: number 55 | s_id: number 56 | mark: number 57 | originCoverType: number 58 | noCopyrightRcmd?: unknown 59 | rtype: number 60 | rurl?: unknown 61 | mst: number 62 | cp: number 63 | mv: number 64 | publishTime: number 65 | alg: string 66 | } 67 | 68 | /**歌曲信息*/ 69 | interface MusicInfo { 70 | /**歌曲 id */ 71 | id: number 72 | 73 | /**歌曲名字*/ 74 | name: string 75 | 76 | /**歌曲封面图*/ 77 | picUrl: string 78 | 79 | tns: unknown[] 80 | pic_str: string 81 | pic: number 82 | } 83 | 84 | const {setStorage, getStorage} = useStorage('music163-grid') 85 | 86 | // Favorite 歌单ID 87 | const favoriteListId = getStorage('favoriteListId') || 3136952023 88 | 89 | // Like 歌单ID 90 | const likeListId = getStorage('likeListId') || 310970433 91 | 92 | // Cloud 歌单ID 93 | const cloudListId = getStorage('cloudListId') || 2463071445 94 | 95 | // 文字颜色 96 | const textColor = '#ffffff' 97 | 98 | /**格子组件参数*/ 99 | interface GridProps { 100 | /** icon 名字*/ 101 | iconName: string 102 | 103 | /**格子背景*/ 104 | background: WstackProps['background'] 105 | 106 | /**文字*/ 107 | text: string 108 | 109 | /**点击格子跳转的链接*/ 110 | href: string 111 | } 112 | 113 | /**格子组件*/ 114 | const Grid: FC = ({...props}) => { 115 | const {iconName, background, text, href} = props 116 | return ( 117 | 118 | 119 | 120 | 121 | {text} 122 | 123 | 124 | 125 | ) 126 | } 127 | 128 | class Music163 { 129 | async init() { 130 | if (isLaunchInsideApp()) { 131 | return await this.showMenu() 132 | } 133 | const widget = (await this.render()) as ListWidget 134 | Script.setWidget(widget) 135 | Script.complete() 136 | } 137 | 138 | //渲染组件 139 | async render(): Promise { 140 | if (isLaunchInsideApp()) { 141 | await showNotification({title: '稍等片刻', body: '小部件渲染中...', sound: 'alert'}) 142 | } 143 | const favoriteImageUrl = ((await this.getRandomMusic(favoriteListId)) || {}).picUrl 144 | const likeImageUrl = ((await this.getRandomMusic(likeListId)) || {}).picUrl 145 | const cloudImageUrl = ((await this.getRandomMusic(cloudListId)) || {}).picUrl 146 | // 多久(毫秒)更新一次小部件(默认3小时) 147 | const updateInterval = 3 * 60 * 60 * 1000 148 | return ( 149 | 150 | 151 | 157 | 158 | 159 | 165 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | ) 180 | } 181 | 182 | // 显示菜单 183 | async showMenu() { 184 | const selectIndex = await showActionSheet({ 185 | title: '菜单', 186 | itemList: ['自定义歌单', '预览组件'], 187 | }) 188 | let musicListId: number | null 189 | switch (selectIndex) { 190 | case 0: 191 | const {texts} = await showModal({ 192 | title: '设置歌单', 193 | content: '去网易云歌单 -> 分享 -> 复制链接, 然后粘贴到此', 194 | inputItems: [ 195 | { 196 | placeholder: '这里填 Favorite 的歌单链接', 197 | }, 198 | { 199 | placeholder: '这里填 Like 的歌单链接', 200 | }, 201 | { 202 | placeholder: '这里填 Cloud 的歌单链接', 203 | }, 204 | ], 205 | }) 206 | if (texts[0]) { 207 | musicListId = this.getListIdFromLink(texts[0]) 208 | musicListId && setStorage('favoriteListId', musicListId) 209 | !musicListId && 210 | (await showNotification({ 211 | title: '歌单链接错误', 212 | body: 'Favorite 的歌单链接检测不到歌单 id ', 213 | sound: 'failure', 214 | })) 215 | } 216 | if (texts[1]) { 217 | musicListId = this.getListIdFromLink(texts[1]) 218 | musicListId && setStorage('likeListId', musicListId) 219 | !musicListId && 220 | (await showNotification({title: '歌单链接错误', body: 'Like 的歌单链接检测不到歌单 id ', sound: 'failure'})) 221 | } 222 | if (texts[2]) { 223 | musicListId = this.getListIdFromLink(texts[2]) 224 | musicListId && setStorage('cloudListId', musicListId) 225 | !musicListId && 226 | (await showNotification({ 227 | title: '歌单链接错误', 228 | body: 'cloud 的歌单链接检测不到歌单 id ', 229 | sound: 'failure', 230 | })) 231 | } 232 | await showNotification({title: '设置完成', sound: 'default'}) 233 | break 234 | case 1: 235 | await showPreviewOptions(this.render.bind(this)) 236 | break 237 | } 238 | } 239 | /** 240 | * 根据歌单链接获取歌单 id 241 | * @param musicListLink 歌单链接 242 | */ 243 | getListIdFromLink(musicListLink: string): number | null { 244 | return Number((musicListLink.match(/\&id\=([\d]+)/) || [])[1]) || null 245 | } 246 | 247 | /** 248 | * 根据歌单id获取歌单数据 249 | * @param musicListId 歌单 id 250 | **/ 251 | async getMusicListData(musicListId: number): Promise { 252 | let tracks: Track[] = [] 253 | try { 254 | tracks = 255 | ( 256 | await request({ 257 | url: `https://api.imjad.cn/cloudmusic/?type=playlist&id=${musicListId}`, 258 | dataType: 'json', 259 | }) 260 | ).data?.playlist.tracks || [] 261 | } catch (err) { 262 | console.warn(`获取歌单数据失败:${err}`) 263 | } 264 | return tracks 265 | } 266 | 267 | /** 268 | * 从歌曲列表里随机选一首歌曲,返回该首歌曲信息 269 | * @param musicListId 歌单id 270 | */ 271 | async getRandomMusic(musicListId: number): Promise { 272 | const tracks = await this.getMusicListData(musicListId) 273 | if (tracks.length <= 0) { 274 | await showNotification({title: `歌单ID${musicListId}获取出错`, body: '该歌单没有歌曲或获取歌曲失败'}) 275 | return null 276 | } 277 | return tracks[getRandomInt(0, tracks.length - 1)].al 278 | } 279 | } 280 | 281 | EndAwait(() => new Music163().init()) 282 | -------------------------------------------------------------------------------- /src/scripts/newsTop.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 今日热榜小部件 3 | */ 4 | 5 | import { 6 | isLaunchInsideApp, 7 | setTransparentBackground, 8 | showActionSheet, 9 | showModal, 10 | showNotification, 11 | showPreviewOptions, 12 | useStorage, 13 | request, 14 | } from '@app/lib/help' 15 | import {FC} from 'react' 16 | 17 | /**榜单信息*/ 18 | interface TopInfo { 19 | /**榜单名字*/ 20 | title: string 21 | 22 | /**榜单链接*/ 23 | href: string 24 | } 25 | 26 | /**文章信息*/ 27 | interface ArticleInfo { 28 | /**文章标题*/ 29 | title: string 30 | 31 | /**原文地址*/ 32 | href: string 33 | 34 | /**文章热度*/ 35 | hot: string 36 | } 37 | 38 | /**页面信息*/ 39 | interface PageInfo { 40 | /**页面标题*/ 41 | title: string 42 | 43 | /**该榜单的 logo */ 44 | logo: string 45 | 46 | /**文章列表*/ 47 | articleList: ArticleInfo[] 48 | 49 | /**cookie*/ 50 | cookie: string | null 51 | 52 | /**js执行的错误信息*/ 53 | err: Error 54 | } 55 | 56 | /**内置榜单别名,可在桌面小部件编辑传入别名*/ 57 | const topList: TopInfo[] = [ 58 | { 59 | title: '知乎', 60 | href: 'https://tophub.today/n/mproPpoq6O', 61 | }, 62 | { 63 | title: '微博', 64 | href: 'https://tophub.today/n/KqndgxeLl9', 65 | }, 66 | { 67 | title: '微信', 68 | href: 'https://tophub.today/n/WnBe01o371', 69 | }, 70 | { 71 | title: '澎湃', 72 | href: 'https://tophub.today/n/wWmoO5Rd4E', 73 | }, 74 | { 75 | title: '百度', 76 | href: 'https://tophub.today/n/Jb0vmloB1G', 77 | }, 78 | { 79 | title: '知乎日报', 80 | href: 'https://tophub.today/n/KMZd7VOvrO', 81 | }, 82 | { 83 | title: '历史上的今天', 84 | href: 'https://tophub.today/n/KMZd7X3erO', 85 | }, 86 | { 87 | title: '神马搜索', 88 | href: 'https://tophub.today/n/n6YoVqDeZa', 89 | }, 90 | { 91 | title: '搜狗', 92 | href: 'https://tophub.today/n/NaEdZndrOM', 93 | }, 94 | { 95 | title: '今日头条', 96 | href: 'https://tophub.today/n/x9ozB4KoXb', 97 | }, 98 | { 99 | title: '360搜索', 100 | href: 'https://tophub.today/n/KMZd7x6erO', 101 | }, 102 | { 103 | title: '36氪', 104 | href: 'https://tophub.today/n/Q1Vd5Ko85R', 105 | }, 106 | { 107 | title: '好奇心日报', 108 | href: 'https://tophub.today/n/Y3QeLGAd7k', 109 | }, 110 | { 111 | title: '少数派', 112 | href: 'https://tophub.today/n/Y2KeDGQdNP', 113 | }, 114 | { 115 | title: '果壳', 116 | href: 'https://tophub.today/n/20MdK2vw1q', 117 | }, 118 | { 119 | title: '虎嗅网', 120 | href: 'https://tophub.today/n/5VaobgvAj1', 121 | }, 122 | { 123 | title: 'IT之家', 124 | href: 'https://tophub.today/n/74Kvx59dkx', 125 | }, 126 | { 127 | title: '爱范儿', 128 | href: 'https://tophub.today/n/74KvxK7okx', 129 | }, 130 | { 131 | title: 'GitHub', 132 | href: 'https://tophub.today/n/rYqoXQ8vOD', 133 | }, 134 | { 135 | title: '威锋网', 136 | href: 'https://tophub.today/n/n4qv90roaK', 137 | }, 138 | { 139 | title: 'CSDN', 140 | href: 'https://tophub.today/n/n3moBVoN5O', 141 | }, 142 | { 143 | title: '掘金', 144 | href: 'https://tophub.today/n/QaqeEaVe9R', 145 | }, 146 | { 147 | title: '哔哩哔哩', 148 | href: 'https://tophub.today/n/74KvxwokxM', 149 | }, 150 | { 151 | title: '抖音', 152 | href: 'https://tophub.today/n/DpQvNABoNE', 153 | }, 154 | { 155 | title: '吾爱破解', 156 | href: 'https://tophub.today/n/NKGoRAzel6', 157 | }, 158 | { 159 | title: '百度贴吧', 160 | href: 'https://tophub.today/n/Om4ejxvxEN', 161 | }, 162 | { 163 | title: '天涯', 164 | href: 'https://tophub.today/n/Jb0vmmlvB1', 165 | }, 166 | { 167 | title: 'V2EX', 168 | href: 'https://tophub.today/n/wWmoORe4EO', 169 | }, 170 | { 171 | title: '虎扑社区', 172 | href: 'https://tophub.today/n/G47o8weMmN', 173 | }, 174 | { 175 | title: '汽车之家', 176 | href: 'https://tophub.today/n/YqoXQGXvOD', 177 | }, 178 | ] 179 | 180 | const {setStorage, getStorage} = useStorage('newsTop-xiaoming') 181 | 182 | /**默认背景颜色*/ 183 | const defaultBgColor = Color.dynamic(new Color('#ffffff', 1), new Color('#000000', 1)) 184 | 185 | /**文字颜色*/ 186 | const textColor = getStorage('textColor') || Color.dynamic(new Color('#000000', 1), new Color('#dddddd', 1)) 187 | 188 | /**透明背景*/ 189 | const transparentBg: Image | Color = getStorage('transparentBg') || defaultBgColor 190 | 191 | /**背景颜色或背景图链接*/ 192 | const boxBg = getStorage('boxBg') || defaultBgColor 193 | 194 | /** 195 | * 文章组件 196 | * @param param.article 文章 197 | * @param param.sort 文章序号 198 | */ 199 | const Article: FC<{article: ArticleInfo; sort: number}> = ({article, sort}) => { 200 | return ( 201 | 202 | {sort > 1 && } 203 | 204 | 205 | {sort} 206 | 207 | 208 | 209 | {article.title} 210 | 211 | 212 | 213 | {article.hot} 214 | 215 | 216 | 217 | ) 218 | } 219 | 220 | class NewsTop { 221 | async init() { 222 | if (isLaunchInsideApp()) { 223 | return await this.showMenu() 224 | } 225 | const widget = (await this.render()) as ListWidget 226 | Script.setWidget(widget) 227 | Script.complete() 228 | } 229 | 230 | //渲染组件 231 | async render(): Promise { 232 | if (isLaunchInsideApp()) { 233 | await showNotification({title: '稍等片刻', body: '小部件渲染中...', sound: 'alert'}) 234 | } 235 | // 获取榜单url 236 | const topUrl = this.getTopUrl() 237 | // 获取榜单数据 238 | const {title, logo, articleList} = await this.getNewsTop(topUrl) 239 | 240 | // 多久(毫秒)更新一次小部件(默认1小时) 241 | const updateInterval = 1 * 60 * 60 * 1000 242 | // 渲染尺寸 243 | const size = config.widgetFamily 244 | 245 | const widgetBoxProps = size === 'small' ? {href: articleList[0] && articleList[0].href} : {} 246 | 247 | return ( 248 | 254 | 255 | 256 | 257 | {/* 标题和logo */} 258 | 259 | 260 | 261 | 262 | {title}排行榜 263 | 264 | 265 | 266 | 267 | {/* 内容 */} 268 | {size === 'small' && this.renderSmall(articleList)} 269 | {size === 'medium' && this.renderMedium(articleList)} 270 | {size === 'large' && this.renderLarge(articleList)} 271 | 272 | 273 | ) 274 | } 275 | 276 | // 渲染小尺寸 277 | renderSmall(articleList: ArticleInfo[]) { 278 | const article = articleList[0] 279 | return ( 280 | 281 | 282 | {article.title} 283 | 284 | 285 | 286 | 287 | 288 | {article.hot} 289 | 290 | 291 | 292 | 293 | ) 294 | } 295 | 296 | // 渲染中尺寸 297 | renderMedium(articleList: ArticleInfo[]) { 298 | const _articleList = articleList.slice(0, 4) 299 | return ( 300 | <> 301 | 302 | {_articleList.map((article, index) => ( 303 |
304 | ))} 305 |
306 | 307 | 308 | 309 | ) 310 | } 311 | 312 | // 渲染大尺寸 313 | renderLarge(articleList: ArticleInfo[]) { 314 | const _articleList = articleList.slice(0, 10) 315 | return ( 316 | <> 317 | 318 | {_articleList.map((article, index) => ( 319 |
320 | ))} 321 |
322 | 323 | 324 | 325 | ) 326 | } 327 | 328 | // 显示菜单 329 | async showMenu() { 330 | const selectIndex = await showActionSheet({ 331 | title: '菜单', 332 | itemList: ['使用其他排行榜', '设置颜色', '设置透明背景', '预览组件', '优化体验'], 333 | }) 334 | switch (selectIndex) { 335 | case 0: 336 | await showModal({ 337 | title: '使用其他排行榜方法', 338 | content: `把小部件添加到桌面后,长按编辑小部件,在 Parameter 栏输入以下任一关键词即可:\n\n${topList 339 | .map(top => top.title) 340 | .join('、')}`, 341 | }) 342 | break 343 | case 1: 344 | const {texts, cancel} = await showModal({ 345 | title: '设置全局背景和颜色', 346 | content: '如果为空,则还原默认', 347 | inputItems: [ 348 | { 349 | text: getStorage('boxBg') || '', 350 | placeholder: '全局背景:可以是颜色、图链接', 351 | }, 352 | { 353 | text: getStorage('textColor') || '', 354 | placeholder: '这里填文字颜色', 355 | }, 356 | ], 357 | }) 358 | if (cancel) return 359 | setStorage('boxBg', texts[0]) 360 | setStorage('textColor', texts[1]) 361 | await showNotification({title: '设置完成', sound: 'default'}) 362 | break 363 | case 2: 364 | const img: Image | null = (await setTransparentBackground()) || null 365 | if (img) { 366 | setStorage('transparentBg', img) 367 | setStorage('boxBg', '透明背景') 368 | await showNotification({title: '设置透明背景成功', sound: 'default'}) 369 | } 370 | break 371 | case 3: 372 | await showPreviewOptions(this.render.bind(this)) 373 | break 374 | case 4: 375 | const {cancel: cancelLogin} = await showModal({ 376 | title: '优化体验建议', 377 | content: 378 | '本组件数据来源于 tophub.today 这个网站,未登录状态获取的文章链接不是最终链接,有二次跳转,如果想获取真实链接,建议在此登录该网站。\n\n登录完成后,自行关闭网页', 379 | confirmText: '去登录', 380 | }) 381 | if (cancelLogin) return 382 | const loginUrl = 'https://tophub.today/login' 383 | const html = await new Request(loginUrl).loadString() 384 | await WebView.loadHTML(html, loginUrl, undefined, true) 385 | break 386 | } 387 | } 388 | 389 | // 获取热榜数据 390 | async getNewsTop(url: string): Promise { 391 | const cookieHeader: Record = isLaunchInsideApp() ? {} : {cookie: getStorage('cookie') || ''} 392 | const html = 393 | ( 394 | await request({ 395 | url, 396 | dataType: 'text', 397 | header: { 398 | 'user-agent': 399 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', 400 | ...cookieHeader, 401 | }, 402 | }) 403 | ).data || '' 404 | const webview = new WebView() 405 | await webview.loadHTML(html, url) 406 | await webview.waitForLoad() 407 | const {title = '今日热榜', logo = 'flame.fill', articleList = [], cookie, err} = (await webview.evaluateJavaScript( 408 | ` 409 | let title = '' 410 | let logo = '' 411 | let articleList = [] 412 | let cookie = document.cookie 413 | let err = '' 414 | try { 415 | title = document.title.split(' ')[0] 416 | logo = document.querySelector('.custom-info-content > img').src 417 | articleList = Array.from(document.querySelector('.divider > .weui-panel > .weui-panel__bd').querySelectorAll('a')).map(a => { 418 | return { 419 | title: a.querySelector('h4').innerText.replace(/\\d+?[、]\\s*/, ''), 420 | href: a.href, 421 | hot: a.querySelector('h4+p').innerText 422 | } 423 | }) 424 | } catch(err) { 425 | err = err 426 | } 427 | Object.assign({}, {title, logo, articleList, cookie, err}) 428 | `, 429 | )) as PageInfo 430 | err && console.warn(`热榜获取出错: ${err}`) 431 | if (isLaunchInsideApp() && cookie) setStorage('cookie', cookie) 432 | return {title, logo, articleList, cookie, err} 433 | } 434 | 435 | // 获取要显示的榜单 url 436 | getTopUrl(): string { 437 | // 默认为知乎榜单 438 | const defaultUrl = 'https://tophub.today/n/mproPpoq6O' 439 | // 从小部件编辑处获取关键词或链接 440 | const keyword = (args.widgetParameter as string) || defaultUrl 441 | if (this.isUrl(keyword)) return keyword 442 | const topInfo = topList.find(item => item.title.toLowerCase() === keyword.toLowerCase()) 443 | if (topInfo) return topInfo.href 444 | return defaultUrl 445 | } 446 | 447 | /** 448 | * 判断一个值是否为网络连接 449 | * @param value 值 450 | */ 451 | isUrl(value: string): boolean { 452 | const reg = /^(http|https)\:\/\/[\w\W]+/ 453 | return reg.test(value) 454 | } 455 | } 456 | 457 | EndAwait(() => new NewsTop().init()) 458 | -------------------------------------------------------------------------------- /src/scripts/yiyan.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 一言 3 | * 改写于:https://github.com/im3x/Scriptables/blob/main/%E4%B8%80%E8%A8%80/latest.js 4 | */ 5 | import {isLaunchInsideApp, request, ResponseType, showActionSheet, showPreviewOptions} from '@app/lib/help' 6 | 7 | interface RemoteData { 8 | id: number 9 | uuid: string 10 | hitokoto: string 11 | type: string 12 | from: string 13 | from_who: string 14 | creator: string 15 | creator_uid: number 16 | reviewer: number 17 | commit_from: string 18 | created_at: number 19 | length: number 20 | } 21 | 22 | class YiyanWidget { 23 | private widget!: ListWidget 24 | async init() { 25 | if (isLaunchInsideApp()) { 26 | return await showPreviewOptions(this.render.bind(this)) 27 | } 28 | this.widget = (await this.render()) as ListWidget 29 | Script.setWidget(this.widget) 30 | Script.complete() 31 | } 32 | async render(): Promise { 33 | const data = (await this.getRemoteData()).data || ({} as RemoteData) 34 | const {hitokoto = '', from = ''} = data 35 | return ( 36 | 37 | 38 | 44 | 45 | 46 | 一言 47 | 48 | 49 | 50 | this.menu()}> 51 | {hitokoto} 52 | 53 | 54 | 55 | {from} 56 | 57 | 58 | ) 59 | } 60 | async getRemoteData(): Promise> { 61 | return await request({ 62 | url: 'https://v1.hitokoto.cn', 63 | dataType: 'json', 64 | }) 65 | } 66 | async menu(): Promise { 67 | const selectIndex = await showActionSheet({ 68 | title: '菜单', 69 | itemList: [ 70 | { 71 | text: '预览组件', 72 | }, 73 | ], 74 | }) 75 | switch (selectIndex) { 76 | case 0: 77 | await showPreviewOptions(this.render.bind(this)) 78 | break 79 | } 80 | } 81 | } 82 | 83 | EndAwait(() => new YiyanWidget().init()) 84 | -------------------------------------------------------------------------------- /src/types/JSX.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | declare global { 3 | const h: typeof React.createElement 4 | const Fragment: typeof React.Fragment 5 | } 6 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | 9 | interface Module { 10 | filename: string 11 | exports: unknown 12 | } 13 | declare const MODULE: Module 14 | 15 | declare namespace Scriptable { 16 | class Widget {} 17 | } 18 | 19 | /** 20 | * 结尾生成顶部等待(top-level-await)渲染 21 | * @param promiseFunc 渲染函数,如: () => render() 22 | */ 23 | declare const EndAwait: (promiseFunc: () => Promise) => Promise 24 | -------------------------------------------------------------------------------- /src/types/widget/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './wbox' 2 | export * from './wdate' 3 | export * from './wimage' 4 | export * from './wspacer' 5 | export * from './wstack' 6 | export * from './wtext' 7 | -------------------------------------------------------------------------------- /src/types/widget/wbox.d.ts: -------------------------------------------------------------------------------- 1 | import {DetailedHTMLProps, HTMLAttributes} from 'react' 2 | 3 | /**组件盒子属性*/ 4 | export interface WboxProps extends HTMLAttributes { 5 | /** 6 | * 背景 7 | * 可以为 Color 对象、hex 字符串 8 | * 可以为 Image 对象、网络图片链接 9 | * 可以为渐变对象 LinearGradient 10 | */ 11 | background?: Color | Image | LinearGradient | string 12 | 13 | /**间隔距离*/ 14 | spacing?: number 15 | 16 | /**点击打开哪个 url, 不与 onClick 共存,当 onClick 存在时,只执行 onClick*/ 17 | href?: string 18 | 19 | /**小组件更新日期*/ 20 | updateDate?: Date 21 | 22 | /**内边距*/ 23 | padding?: [number, number, number, number] 24 | 25 | /**点击事件,不与 href 共存,当 href 存在时,只执行 onClick */ 26 | onClick?: () => unknown 27 | } 28 | 29 | declare global { 30 | namespace JSX { 31 | interface IntrinsicElements { 32 | /**组件盒子*/ 33 | wbox: DetailedHTMLProps 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/types/widget/wdate.d.ts: -------------------------------------------------------------------------------- 1 | import {DetailedHTMLProps, HTMLAttributes} from 'react' 2 | 3 | /**时间组件属性*/ 4 | export interface WdateProps extends HTMLAttributes { 5 | /**时间*/ 6 | date: Date 7 | 8 | /**显示模式*/ 9 | /** 10 | * time: 显示日期的时间部分。例如:11:23PM 11 | * date: 显示整个日期。例如:June 3, 2019 12 | * relative: 将日期显示为相对于现在的日期。例如:2 hours, 23 minutes 1 year, 1 month 13 | * offset: 将日期显示为从现在开始的偏移量。例如:+2 hours -3 months 14 | * timer: 从现在开始将日期显示为计时器计数。例如:2:32 36:59:01 15 | */ 16 | mode: 'time' | 'date' | 'relative' | 'offset' | 'timer' 17 | 18 | /**文字颜色*/ 19 | textColor?: Color | string 20 | 21 | /**字体和字体大小*/ 22 | font?: Font | number 23 | 24 | /**透明度0到1,0为完全透明*/ 25 | opacity?: number 26 | 27 | /**做多显示多少行,当小于等于0时,禁用,默认禁用*/ 28 | maxLine?: number 29 | 30 | /**文字缩放倍数,目前只支持缩小,数字为0到1,1是正常大小*/ 31 | scale?: number 32 | 33 | /**阴影颜色*/ 34 | shadowColor?: Color | string 35 | 36 | /**阴影虚化程度*/ 37 | shadowRadius?: number 38 | 39 | /**阴影偏移量*/ 40 | shadowOffset?: Point 41 | 42 | /**点击打开哪个 url, 不与 onClick 共存,当 onClick 存在时,只执行 onClick*/ 43 | href?: string 44 | 45 | /**文字横向对齐*/ 46 | textAlign?: 'left' | 'center' | 'right' 47 | 48 | /**点击事件,不与 href 共存,当 href 存在时,只执行 onClick */ 49 | onClick?: () => unknown 50 | } 51 | 52 | declare global { 53 | namespace JSX { 54 | interface IntrinsicElements { 55 | /**时间组件*/ 56 | wdate: DetailedHTMLProps 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/types/widget/wimage.d.ts: -------------------------------------------------------------------------------- 1 | import {DetailedHTMLProps, HTMLAttributes} from 'react' 2 | 3 | /**图片组件属性*/ 4 | export interface WimageProps extends HTMLAttributes { 5 | /**图片网络链接、 6 | * 图片对象 7 | * SF Symbol 的 name (ios 的 内置 icon 库的某个 icon name),详见 https://apps.apple.com/us/app/sf-symbols-browser/id1491161336 8 | */ 9 | src: string | Image 10 | 11 | /**点击打开哪个 url, 不与 onClick 共存,当 onClick 存在时,只执行 onClick*/ 12 | href?: string 13 | 14 | /**图片是否可以调整大小,默认是*/ 15 | resizable?: boolean 16 | 17 | /**宽*/ 18 | width?: number 19 | 20 | /**高*/ 21 | height?: number 22 | 23 | /**透明度0到1,0为完全透明*/ 24 | opacity?: number 25 | 26 | /**圆角*/ 27 | borderRadius?: number 28 | 29 | /**边框宽度*/ 30 | borderWidth?: number 31 | 32 | /**边框颜色*/ 33 | borderColor?: Color | string 34 | 35 | /** 36 | * 默认为false 37 | * 如果为true,则图片的角将相对于包含的小部件进行四舍五入。 38 | * 如果为true,则会忽略borderRadius的值 39 | * */ 40 | containerRelativeShape?: boolean 41 | 42 | /**加滤镜(tintColor)*/ 43 | filter?: Color | string 44 | 45 | /**横向对齐*/ 46 | imageAlign?: 'left' | 'center' | 'right' 47 | 48 | /** 49 | * fit 图片将适应可用空间,默认。 50 | * fill 图片将填充可用空间。 51 | */ 52 | mode?: 'fit' | 'fill' 53 | 54 | /**点击事件,不与 href 共存,当 href 存在时,只执行 onClick */ 55 | onClick?: () => unknown 56 | } 57 | 58 | declare global { 59 | namespace JSX { 60 | interface IntrinsicElements { 61 | /**图片组件*/ 62 | wimage: DetailedHTMLProps 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/types/widget/wspacer.d.ts: -------------------------------------------------------------------------------- 1 | import {DetailedHTMLProps, HTMLAttributes} from 'react' 2 | 3 | /**占位组件属性*/ 4 | export interface WspacerProps extends HTMLAttributes { 5 | /**空位长度,当为0时是弹性占位*/ 6 | length?: number 7 | } 8 | 9 | declare global { 10 | namespace JSX { 11 | interface IntrinsicElements { 12 | /**占位组件*/ 13 | wspacer: DetailedHTMLProps 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/types/widget/wstack.d.ts: -------------------------------------------------------------------------------- 1 | import {DetailedHTMLProps, HTMLAttributes} from 'react' 2 | 3 | /**容器组件属性*/ 4 | export interface WstackProps extends HTMLAttributes { 5 | /** 6 | * 背景 7 | * 可以为 Color 对象、hex 字符串 8 | * 可以为 Image 对象、网络图片链接 9 | * 可以为渐变对象 LinearGradient 10 | */ 11 | background?: Color | Image | LinearGradient | string 12 | 13 | /**与同级上一个元素的间隔*/ 14 | spacing?: number 15 | 16 | /**内边距*/ 17 | padding?: [number, number, number, number] 18 | 19 | /**组件宽*/ 20 | width?: number 21 | 22 | /**组件高*/ 23 | height?: number 24 | 25 | /**圆角*/ 26 | borderRadius?: number 27 | 28 | /**边框宽度*/ 29 | borderWidth?: number 30 | 31 | /**边框颜色*/ 32 | borderColor?: Color | string 33 | 34 | /**点击打开哪个 url, 不与 onClick 共存,当 onClick 存在时,只执行 onClick*/ 35 | href?: string 36 | 37 | /**内容垂直方向对齐方式*/ 38 | verticalAlign?: 'top' | 'center' | 'bottom' 39 | 40 | /**排版方向(默认横着排)*/ 41 | flexDirection?: 'row' | 'column' 42 | 43 | /**点击事件,不与 href 共存,当 href 存在时,只执行 onClick */ 44 | onClick?: () => unknown 45 | } 46 | 47 | declare global { 48 | namespace JSX { 49 | interface IntrinsicElements { 50 | /**容器组件*/ 51 | wstack: DetailedHTMLProps 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/types/widget/wtext.d.ts: -------------------------------------------------------------------------------- 1 | import {DetailedHTMLProps, HTMLAttributes} from 'react' 2 | 3 | /**文字组件属性*/ 4 | export interface WtextProps extends HTMLAttributes { 5 | /**文字颜色*/ 6 | textColor?: Color | string 7 | 8 | /**字体和字体大小*/ 9 | font?: Font | number 10 | 11 | /**透明度0到1,0为完全透明*/ 12 | opacity?: number 13 | 14 | /**最多显示多少行,当小于等于0时,禁用,默认禁用*/ 15 | maxLine?: number 16 | 17 | /**文字缩放倍数,目前只支持缩小,数字为0到1,1是正常大小*/ 18 | scale?: number 19 | 20 | /**阴影颜色*/ 21 | shadowColor?: Color | string 22 | 23 | /**阴影虚化程度*/ 24 | shadowRadius?: number 25 | 26 | /**阴影偏移量*/ 27 | shadowOffset?: Point 28 | 29 | /**点击打开哪个 url, 不与 onClick 共存,当 onClick 存在时,只执行 onClick*/ 30 | href?: string 31 | 32 | /**文字横向对齐*/ 33 | textAlign?: 'left' | 'center' | 'right' 34 | 35 | /**点击事件,不与 href 共存,当 href 存在时,只执行 onClick */ 36 | onClick?: () => unknown 37 | } 38 | 39 | declare global { 40 | namespace JSX { 41 | interface IntrinsicElements { 42 | /**文字组件*/ 43 | wtext: DetailedHTMLProps 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["./node_modules/@types", "src/types", "./src/types/widget"], 4 | // "types": ["node"], 5 | "lib": ["ES5", "ES2015", "ES2016", "ES2017", "Es2018", "ES2019", "ES2020", "ESNext"], 6 | "jsx": "react", 7 | "jsxFactory": "h", 8 | "jsxFragmentFactory": "Fragment", 9 | "outDir": "./dist", 10 | "target": "es2015", 11 | "allowJs": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "commonjs", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "sourceMap": true, 20 | "baseUrl": "./", 21 | "paths": { 22 | "@app/*": ["./src/*"] 23 | } 24 | }, 25 | "exclude": ["node_modules", "dist", "build"], 26 | "include": ["src/**/*.ts", "src/**/*.tsx"] 27 | } 28 | -------------------------------------------------------------------------------- /打包好的成品/install.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "葬爱非主流小明", 3 | "scriptable": true, 4 | "repo": "https://github.com/2214962083/ios-scriptable-tsx", 5 | "icon": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 6 | "apps": [ 7 | { 8 | "version": "1.0.0", 9 | "description": "小明出品,必属精品", 10 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/一言.js", 11 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 12 | "name": "一言", 13 | "title": "一言" 14 | }, 15 | { 16 | "version": "1.0.0", 17 | "description": "小明出品,必属精品", 18 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/个性磁贴启动器.js", 19 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 20 | "name": "个性磁贴启动器", 21 | "title": "个性磁贴启动器" 22 | }, 23 | { 24 | "version": "1.0.0", 25 | "description": "小明出品,必属精品", 26 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/今日热榜.js", 27 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 28 | "name": "今日热榜", 29 | "title": "今日热榜" 30 | }, 31 | { 32 | "version": "1.0.0", 33 | "description": "小明出品,必属精品", 34 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/哔哩粉丝.js", 35 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 36 | "name": "哔哩粉丝", 37 | "title": "哔哩粉丝" 38 | }, 39 | { 40 | "version": "1.0.0", 41 | "description": "小明出品,必属精品", 42 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/基金小部件.js", 43 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 44 | "name": "基金小部件", 45 | "title": "基金小部件" 46 | }, 47 | { 48 | "version": "1.0.0", 49 | "description": "小明出品,必属精品", 50 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/网易云歌单.js", 51 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 52 | "name": "网易云歌单", 53 | "title": "网易云歌单" 54 | }, 55 | { 56 | "version": "1.0.0", 57 | "description": "小明出品,必属精品", 58 | "scriptURL": "https://raw.githubusercontent.com/2214962083/ios-scriptable-tsx/master/打包好的成品/联通流量话费小部件.js", 59 | "thumb": "https://p.pstatp.com/origin/fe4400034e131f9e4e45", 60 | "name": "联通流量话费小部件", 61 | "title": "联通流量话费小部件" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /打包好的成品/一言.js: -------------------------------------------------------------------------------- 1 | // @编译时间 1607924347523 2 | const MODULE = module 3 | let __topLevelAwait__ = () => Promise.resolve() 4 | function EndAwait(promiseFunc) { 5 | __topLevelAwait__ = promiseFunc 6 | } 7 | 8 | // src/lib/constants.ts 9 | var URLSchemeFrom 10 | ;(function (URLSchemeFrom2) { 11 | URLSchemeFrom2['WIDGET'] = 'widget' 12 | })(URLSchemeFrom || (URLSchemeFrom = {})) 13 | 14 | // src/lib/help.ts 15 | function fm() { 16 | return FileManager[MODULE.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() 17 | } 18 | function setStorageDirectory(dirPath) { 19 | return { 20 | setStorage(key, value) { 21 | const hashKey = hash(key) 22 | const filePath = FileManager.local().joinPath(dirPath, hashKey) 23 | if (value instanceof Image) { 24 | FileManager.local().writeImage(filePath, value) 25 | return 26 | } 27 | if (value instanceof Data) { 28 | FileManager.local().write(filePath, value) 29 | } 30 | Keychain.set(hashKey, JSON.stringify(value)) 31 | }, 32 | getStorage(key) { 33 | const hashKey = hash(key) 34 | const filePath = FileManager.local().joinPath(FileManager.local().libraryDirectory(), hashKey) 35 | if (FileManager.local().fileExists(filePath)) { 36 | const image = Image.fromFile(filePath) 37 | const file = Data.fromFile(filePath) 38 | return image ? image : file ? file : null 39 | } 40 | if (Keychain.contains(hashKey)) { 41 | return JSON.parse(Keychain.get(hashKey)) 42 | } else { 43 | return null 44 | } 45 | }, 46 | removeStorage(key) { 47 | const hashKey = hash(key) 48 | const filePath = FileManager.local().joinPath(FileManager.local().libraryDirectory(), hashKey) 49 | if (FileManager.local().fileExists(filePath)) { 50 | FileManager.local().remove(hashKey) 51 | } 52 | if (Keychain.contains(hashKey)) { 53 | Keychain.remove(hashKey) 54 | } 55 | }, 56 | } 57 | } 58 | var setStorage = setStorageDirectory(fm().libraryDirectory()).setStorage 59 | var getStorage = setStorageDirectory(FileManager.local().libraryDirectory()).getStorage 60 | var removeStorage = setStorageDirectory(FileManager.local().libraryDirectory()).removeStorage 61 | var setCache = setStorageDirectory(FileManager.local().temporaryDirectory()).setStorage 62 | var getCache = setStorageDirectory(FileManager.local().temporaryDirectory()).getStorage 63 | var removeCache = setStorageDirectory(FileManager.local().temporaryDirectory()).removeStorage 64 | async function request(args2) { 65 | const { 66 | url, 67 | data, 68 | header, 69 | dataType = 'json', 70 | method = 'GET', 71 | timeout = 60 * 1e3, 72 | useCache = false, 73 | failReturnCache = true, 74 | } = args2 75 | const cacheKey = `url:${url}` 76 | const cache = getStorage(cacheKey) 77 | if (useCache && cache !== null) return cache 78 | const req = new Request(url) 79 | req.method = method 80 | header && (req.headers = header) 81 | data && (req.body = data) 82 | req.timeoutInterval = timeout / 1e3 83 | req.allowInsecureRequest = true 84 | let res 85 | try { 86 | switch (dataType) { 87 | case 'json': 88 | res = await req.loadJSON() 89 | break 90 | case 'text': 91 | res = await req.loadString() 92 | break 93 | case 'image': 94 | res = await req.loadImage() 95 | break 96 | case 'data': 97 | res = await req.load() 98 | break 99 | default: 100 | res = await req.loadJSON() 101 | } 102 | const result = {...req.response, data: res} 103 | setStorage(cacheKey, result) 104 | return result 105 | } catch (err) { 106 | if (cache !== null && failReturnCache) return cache 107 | return err 108 | } 109 | } 110 | async function showActionSheet(args2) { 111 | const {title, desc, cancelText = '取消', itemList} = args2 112 | const alert = new Alert() 113 | title && (alert.title = title) 114 | desc && (alert.message = desc) 115 | for (const item of itemList) { 116 | if (typeof item === 'string') { 117 | alert.addAction(item) 118 | } else { 119 | switch (item.type) { 120 | case 'normal': 121 | alert.addAction(item.text) 122 | break 123 | case 'warn': 124 | alert.addDestructiveAction(item.text) 125 | break 126 | default: 127 | alert.addAction(item.text) 128 | break 129 | } 130 | } 131 | } 132 | alert.addCancelAction(cancelText) 133 | const tapIndex = await alert.presentSheet() 134 | return tapIndex 135 | } 136 | async function getImage(args2) { 137 | const {filepath, url, useCache = true} = args2 138 | const generateDefaultImage = async () => { 139 | const ctx = new DrawContext() 140 | ctx.size = new Size(100, 100) 141 | ctx.setFillColor(Color.red()) 142 | ctx.fillRect(new Rect(0, 0, 100, 100)) 143 | return await ctx.getImage() 144 | } 145 | try { 146 | if (filepath) { 147 | return Image.fromFile(filepath) || (await generateDefaultImage()) 148 | } 149 | if (!url) return await generateDefaultImage() 150 | const cacheKey = `image:${url}` 151 | if (useCache) { 152 | const cache = getCache(url) 153 | if (cache instanceof Image) { 154 | return cache 155 | } else { 156 | removeCache(cacheKey) 157 | } 158 | } 159 | const res = await request({url, dataType: 'image'}) 160 | const image = res && res.data 161 | image && setCache(cacheKey, image) 162 | return image || (await generateDefaultImage()) 163 | } catch (err) { 164 | return await generateDefaultImage() 165 | } 166 | } 167 | function hash(string) { 168 | let hash2 = 0, 169 | i, 170 | chr 171 | for (i = 0; i < string.length; i++) { 172 | chr = string.charCodeAt(i) 173 | hash2 = (hash2 << 5) - hash2 + chr 174 | hash2 |= 0 175 | } 176 | return `hash_${hash2}` 177 | } 178 | function isLaunchInsideApp() { 179 | return !config.runsInWidget && args.queryParameters.from !== URLSchemeFrom.WIDGET 180 | } 181 | async function showPreviewOptions(render) { 182 | const selectIndex = await showActionSheet({ 183 | title: '预览组件', 184 | desc: '测试桌面组件在各种尺寸下的显示效果', 185 | itemList: ['小尺寸', '中尺寸', '大尺寸', '全部尺寸'], 186 | }) 187 | switch (selectIndex) { 188 | case 0: 189 | config.widgetFamily = 'small' 190 | await (await render()).presentSmall() 191 | break 192 | case 1: 193 | config.widgetFamily = 'medium' 194 | await (await render()).presentMedium() 195 | break 196 | case 2: 197 | config.widgetFamily = 'large' 198 | await (await render()).presentLarge() 199 | break 200 | case 3: 201 | config.widgetFamily = 'small' 202 | await (await render()).presentSmall() 203 | config.widgetFamily = 'medium' 204 | await (await render()).presentMedium() 205 | config.widgetFamily = 'large' 206 | await (await render()).presentLarge() 207 | break 208 | } 209 | return selectIndex 210 | } 211 | 212 | // src/lib/jsx-runtime.ts 213 | var GenrateView = class { 214 | static setListWidget(listWidget2) { 215 | this.listWidget = listWidget2 216 | } 217 | static async wbox(props, ...children) { 218 | const {background, spacing, href, updateDate, padding, onClick} = props 219 | try { 220 | isDefined(background) && (await setBackground(this.listWidget, background)) 221 | isDefined(spacing) && (this.listWidget.spacing = spacing) 222 | isDefined(href) && (this.listWidget.url = href) 223 | isDefined(updateDate) && (this.listWidget.refreshAfterDate = updateDate) 224 | isDefined(padding) && this.listWidget.setPadding(...padding) 225 | isDefined(onClick) && runOnClick(this.listWidget, onClick) 226 | await addChildren(this.listWidget, children) 227 | } catch (err) { 228 | console.error(err) 229 | } 230 | return this.listWidget 231 | } 232 | static wstack(props, ...children) { 233 | return async parentInstance => { 234 | const widgetStack = parentInstance.addStack() 235 | const { 236 | background, 237 | spacing, 238 | padding, 239 | width = 0, 240 | height = 0, 241 | borderRadius, 242 | borderWidth, 243 | borderColor, 244 | href, 245 | verticalAlign, 246 | flexDirection, 247 | onClick, 248 | } = props 249 | try { 250 | isDefined(background) && (await setBackground(widgetStack, background)) 251 | isDefined(spacing) && (widgetStack.spacing = spacing) 252 | isDefined(padding) && widgetStack.setPadding(...padding) 253 | isDefined(borderRadius) && (widgetStack.cornerRadius = borderRadius) 254 | isDefined(borderWidth) && (widgetStack.borderWidth = borderWidth) 255 | isDefined(borderColor) && (widgetStack.borderColor = getColor(borderColor)) 256 | isDefined(href) && (widgetStack.url = href) 257 | widgetStack.size = new Size(width, height) 258 | const verticalAlignMap = { 259 | bottom: () => widgetStack.bottomAlignContent(), 260 | center: () => widgetStack.centerAlignContent(), 261 | top: () => widgetStack.topAlignContent(), 262 | } 263 | isDefined(verticalAlign) && verticalAlignMap[verticalAlign]() 264 | const flexDirectionMap = { 265 | row: () => widgetStack.layoutHorizontally(), 266 | column: () => widgetStack.layoutVertically(), 267 | } 268 | isDefined(flexDirection) && flexDirectionMap[flexDirection]() 269 | isDefined(onClick) && runOnClick(widgetStack, onClick) 270 | } catch (err) { 271 | console.error(err) 272 | } 273 | await addChildren(widgetStack, children) 274 | } 275 | } 276 | static wimage(props) { 277 | return async parentInstance => { 278 | const { 279 | src, 280 | href, 281 | resizable, 282 | width = 0, 283 | height = 0, 284 | opacity, 285 | borderRadius, 286 | borderWidth, 287 | borderColor, 288 | containerRelativeShape, 289 | filter, 290 | imageAlign, 291 | mode, 292 | onClick, 293 | } = props 294 | let _image = src 295 | typeof src === 'string' && isUrl(src) && (_image = await getImage({url: src})) 296 | typeof src === 'string' && !isUrl(src) && (_image = SFSymbol.named(src).image) 297 | const widgetImage = parentInstance.addImage(_image) 298 | widgetImage.image = _image 299 | try { 300 | isDefined(href) && (widgetImage.url = href) 301 | isDefined(resizable) && (widgetImage.resizable = resizable) 302 | widgetImage.imageSize = new Size(width, height) 303 | isDefined(opacity) && (widgetImage.imageOpacity = opacity) 304 | isDefined(borderRadius) && (widgetImage.cornerRadius = borderRadius) 305 | isDefined(borderWidth) && (widgetImage.borderWidth = borderWidth) 306 | isDefined(borderColor) && (widgetImage.borderColor = getColor(borderColor)) 307 | isDefined(containerRelativeShape) && (widgetImage.containerRelativeShape = containerRelativeShape) 308 | isDefined(filter) && (widgetImage.tintColor = getColor(filter)) 309 | const imageAlignMap = { 310 | left: () => widgetImage.leftAlignImage(), 311 | center: () => widgetImage.centerAlignImage(), 312 | right: () => widgetImage.rightAlignImage(), 313 | } 314 | isDefined(imageAlign) && imageAlignMap[imageAlign]() 315 | const modeMap = { 316 | fit: () => widgetImage.applyFittingContentMode(), 317 | fill: () => widgetImage.applyFillingContentMode(), 318 | } 319 | isDefined(mode) && modeMap[mode]() 320 | isDefined(onClick) && runOnClick(widgetImage, onClick) 321 | } catch (err) { 322 | console.error(err) 323 | } 324 | } 325 | } 326 | static wspacer(props) { 327 | return async parentInstance => { 328 | const widgetSpacer = parentInstance.addSpacer() 329 | const {length} = props 330 | try { 331 | isDefined(length) && (widgetSpacer.length = length) 332 | } catch (err) { 333 | console.error(err) 334 | } 335 | } 336 | } 337 | static wtext(props, ...children) { 338 | return async parentInstance => { 339 | const widgetText = parentInstance.addText('') 340 | const { 341 | textColor, 342 | font, 343 | opacity, 344 | maxLine, 345 | scale, 346 | shadowColor, 347 | shadowRadius, 348 | shadowOffset, 349 | href, 350 | textAlign, 351 | onClick, 352 | } = props 353 | if (children && Array.isArray(children)) { 354 | widgetText.text = children.join('') 355 | } 356 | try { 357 | isDefined(textColor) && (widgetText.textColor = getColor(textColor)) 358 | isDefined(font) && (widgetText.font = typeof font === 'number' ? Font.systemFont(font) : font) 359 | isDefined(opacity) && (widgetText.textOpacity = opacity) 360 | isDefined(maxLine) && (widgetText.lineLimit = maxLine) 361 | isDefined(scale) && (widgetText.minimumScaleFactor = scale) 362 | isDefined(shadowColor) && (widgetText.shadowColor = getColor(shadowColor)) 363 | isDefined(shadowRadius) && (widgetText.shadowRadius = shadowRadius) 364 | isDefined(shadowOffset) && (widgetText.shadowOffset = shadowOffset) 365 | isDefined(href) && (widgetText.url = href) 366 | const textAlignMap = { 367 | left: () => widgetText.leftAlignText(), 368 | center: () => widgetText.centerAlignText(), 369 | right: () => widgetText.rightAlignText(), 370 | } 371 | isDefined(textAlign) && textAlignMap[textAlign]() 372 | isDefined(onClick) && runOnClick(widgetText, onClick) 373 | } catch (err) { 374 | console.error(err) 375 | } 376 | } 377 | } 378 | static wdate(props) { 379 | return async parentInstance => { 380 | const widgetDate = parentInstance.addDate(new Date()) 381 | const { 382 | date, 383 | mode, 384 | textColor, 385 | font, 386 | opacity, 387 | maxLine, 388 | scale, 389 | shadowColor, 390 | shadowRadius, 391 | shadowOffset, 392 | href, 393 | textAlign, 394 | onClick, 395 | } = props 396 | try { 397 | isDefined(date) && (widgetDate.date = date) 398 | isDefined(textColor) && (widgetDate.textColor = getColor(textColor)) 399 | isDefined(font) && (widgetDate.font = typeof font === 'number' ? Font.systemFont(font) : font) 400 | isDefined(opacity) && (widgetDate.textOpacity = opacity) 401 | isDefined(maxLine) && (widgetDate.lineLimit = maxLine) 402 | isDefined(scale) && (widgetDate.minimumScaleFactor = scale) 403 | isDefined(shadowColor) && (widgetDate.shadowColor = getColor(shadowColor)) 404 | isDefined(shadowRadius) && (widgetDate.shadowRadius = shadowRadius) 405 | isDefined(shadowOffset) && (widgetDate.shadowOffset = shadowOffset) 406 | isDefined(href) && (widgetDate.url = href) 407 | const modeMap = { 408 | time: () => widgetDate.applyTimeStyle(), 409 | date: () => widgetDate.applyDateStyle(), 410 | relative: () => widgetDate.applyRelativeStyle(), 411 | offset: () => widgetDate.applyOffsetStyle(), 412 | timer: () => widgetDate.applyTimerStyle(), 413 | } 414 | isDefined(mode) && modeMap[mode]() 415 | const textAlignMap = { 416 | left: () => widgetDate.leftAlignText(), 417 | center: () => widgetDate.centerAlignText(), 418 | right: () => widgetDate.rightAlignText(), 419 | } 420 | isDefined(textAlign) && textAlignMap[textAlign]() 421 | isDefined(onClick) && runOnClick(widgetDate, onClick) 422 | } catch (err) { 423 | console.error(err) 424 | } 425 | } 426 | } 427 | } 428 | var listWidget = new ListWidget() 429 | GenrateView.setListWidget(listWidget) 430 | function h(type, props, ...children) { 431 | props = props || {} 432 | const _children = flatteningArr(children) 433 | switch (type) { 434 | case 'wbox': 435 | return GenrateView.wbox(props, ..._children) 436 | break 437 | case 'wdate': 438 | return GenrateView.wdate(props) 439 | break 440 | case 'wimage': 441 | return GenrateView.wimage(props) 442 | break 443 | case 'wspacer': 444 | return GenrateView.wspacer(props) 445 | break 446 | case 'wstack': 447 | return GenrateView.wstack(props, ..._children) 448 | break 449 | case 'wtext': 450 | return GenrateView.wtext(props, ..._children) 451 | break 452 | default: 453 | return type instanceof Function ? type({children: _children, ...props}) : null 454 | break 455 | } 456 | } 457 | function flatteningArr(arr) { 458 | return [].concat( 459 | ...arr.map(item => { 460 | return Array.isArray(item) ? flatteningArr(item) : item 461 | }), 462 | ) 463 | } 464 | function getColor(color) { 465 | return typeof color === 'string' ? new Color(color, 1) : color 466 | } 467 | async function getBackground(bg) { 468 | bg = (typeof bg === 'string' && !isUrl(bg)) || bg instanceof Color ? getColor(bg) : bg 469 | if (typeof bg === 'string') { 470 | bg = await getImage({url: bg}) 471 | } 472 | return bg 473 | } 474 | async function setBackground(widget, bg) { 475 | const _bg = await getBackground(bg) 476 | if (_bg instanceof Color) { 477 | widget.backgroundColor = _bg 478 | } 479 | if (_bg instanceof Image) { 480 | widget.backgroundImage = _bg 481 | } 482 | if (_bg instanceof LinearGradient) { 483 | widget.backgroundGradient = _bg 484 | } 485 | } 486 | async function addChildren(instance, children) { 487 | if (children && Array.isArray(children)) { 488 | for (const child of children) { 489 | child instanceof Function ? await child(instance) : '' 490 | } 491 | } 492 | } 493 | function isDefined(value) { 494 | if (typeof value === 'number' && !isNaN(value)) { 495 | return true 496 | } 497 | return value !== void 0 && value !== null 498 | } 499 | function isUrl(value) { 500 | const reg = /^(http|https)\:\/\/[\w\W]+/ 501 | return reg.test(value) 502 | } 503 | function runOnClick(instance, onClick) { 504 | const _eventId = hash(onClick.toString()) 505 | instance.url = `${URLScheme.forRunningScript()}?eventId=${encodeURIComponent(_eventId)}&from=${URLSchemeFrom.WIDGET}` 506 | const {eventId, from} = args.queryParameters 507 | if (eventId && eventId === _eventId && from === URLSchemeFrom.WIDGET) { 508 | onClick() 509 | } 510 | } 511 | 512 | // src/scripts/tsx-yiyan.tsx 513 | var YiyanWidget = class { 514 | async init() { 515 | if (isLaunchInsideApp()) { 516 | return await showPreviewOptions(this.render.bind(this)) 517 | } 518 | this.widget = await this.render() 519 | Script.setWidget(this.widget) 520 | Script.complete() 521 | } 522 | async render() { 523 | const data = (await this.getRemoteData()).data || {} 524 | const {hitokoto = '', from = ''} = data 525 | return /* @__PURE__ */ h( 526 | 'wbox', 527 | null, 528 | /* @__PURE__ */ h( 529 | 'wstack', 530 | { 531 | verticalAlign: 'center', 532 | }, 533 | /* @__PURE__ */ h('wimage', { 534 | src: 'https://txc.gtimg.com/data/285778/2020/1012/f9cf50f08ebb8bd391a7118c8348f5d8.png', 535 | width: 14, 536 | height: 14, 537 | borderRadius: 4, 538 | }), 539 | /* @__PURE__ */ h('wspacer', { 540 | length: 10, 541 | }), 542 | /* @__PURE__ */ h( 543 | 'wtext', 544 | { 545 | opacity: 0.7, 546 | font: Font.boldSystemFont(12), 547 | }, 548 | '一言', 549 | ), 550 | ), 551 | /* @__PURE__ */ h('wspacer', null), 552 | /* @__PURE__ */ h( 553 | 'wtext', 554 | { 555 | font: Font.lightSystemFont(16), 556 | onClick: () => this.menu(), 557 | }, 558 | hitokoto, 559 | ), 560 | /* @__PURE__ */ h('wspacer', null), 561 | /* @__PURE__ */ h( 562 | 'wtext', 563 | { 564 | font: Font.lightSystemFont(12), 565 | opacity: 0.5, 566 | textAlign: 'right', 567 | maxLine: 1, 568 | }, 569 | from, 570 | ), 571 | ) 572 | } 573 | async getRemoteData() { 574 | return await request({ 575 | url: 'https://v1.hitokoto.cn', 576 | dataType: 'json', 577 | }) 578 | } 579 | async menu() { 580 | const selectIndex = await showActionSheet({ 581 | title: '菜单', 582 | itemList: [ 583 | { 584 | text: '预览组件', 585 | }, 586 | ], 587 | }) 588 | switch (selectIndex) { 589 | case 0: 590 | await showPreviewOptions(this.render.bind(this)) 591 | break 592 | } 593 | } 594 | } 595 | EndAwait(() => new YiyanWidget().init()) 596 | 597 | await __topLevelAwait__() 598 | -------------------------------------------------------------------------------- /打包好的成品/哔哩粉丝.js: -------------------------------------------------------------------------------- 1 | // @编译时间 1607924347523 2 | const MODULE = module 3 | let __topLevelAwait__ = () => Promise.resolve() 4 | function EndAwait(promiseFunc) { 5 | __topLevelAwait__ = promiseFunc 6 | } 7 | 8 | // src/lib/constants.ts 9 | var URLSchemeFrom 10 | ;(function (URLSchemeFrom2) { 11 | URLSchemeFrom2['WIDGET'] = 'widget' 12 | })(URLSchemeFrom || (URLSchemeFrom = {})) 13 | 14 | // src/lib/help.ts 15 | function fm() { 16 | return FileManager[MODULE.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() 17 | } 18 | function setStorageDirectory(dirPath) { 19 | return { 20 | setStorage(key, value) { 21 | const hashKey = hash(key) 22 | const filePath = FileManager.local().joinPath(dirPath, hashKey) 23 | if (value instanceof Image) { 24 | FileManager.local().writeImage(filePath, value) 25 | return 26 | } 27 | if (value instanceof Data) { 28 | FileManager.local().write(filePath, value) 29 | } 30 | Keychain.set(hashKey, JSON.stringify(value)) 31 | }, 32 | getStorage(key) { 33 | const hashKey = hash(key) 34 | const filePath = FileManager.local().joinPath(FileManager.local().libraryDirectory(), hashKey) 35 | if (FileManager.local().fileExists(filePath)) { 36 | const image = Image.fromFile(filePath) 37 | const file = Data.fromFile(filePath) 38 | return image ? image : file ? file : null 39 | } 40 | if (Keychain.contains(hashKey)) { 41 | return JSON.parse(Keychain.get(hashKey)) 42 | } else { 43 | return null 44 | } 45 | }, 46 | removeStorage(key) { 47 | const hashKey = hash(key) 48 | const filePath = FileManager.local().joinPath(FileManager.local().libraryDirectory(), hashKey) 49 | if (FileManager.local().fileExists(filePath)) { 50 | FileManager.local().remove(hashKey) 51 | } 52 | if (Keychain.contains(hashKey)) { 53 | Keychain.remove(hashKey) 54 | } 55 | }, 56 | } 57 | } 58 | var setStorage = setStorageDirectory(fm().libraryDirectory()).setStorage 59 | var getStorage = setStorageDirectory(FileManager.local().libraryDirectory()).getStorage 60 | var removeStorage = setStorageDirectory(FileManager.local().libraryDirectory()).removeStorage 61 | var setCache = setStorageDirectory(FileManager.local().temporaryDirectory()).setStorage 62 | var getCache = setStorageDirectory(FileManager.local().temporaryDirectory()).getStorage 63 | var removeCache = setStorageDirectory(FileManager.local().temporaryDirectory()).removeStorage 64 | function useStorage(nameSpace) { 65 | const _nameSpace = nameSpace || `${MODULE.filename}` 66 | return { 67 | setStorage(key, value) { 68 | setStorage(`${_nameSpace}${key}`, value) 69 | }, 70 | getStorage(key) { 71 | return getStorage(`${_nameSpace}${key}`) 72 | }, 73 | removeStorage(key) { 74 | removeStorage(`${_nameSpace}${key}`) 75 | }, 76 | } 77 | } 78 | async function request(args2) { 79 | const { 80 | url, 81 | data, 82 | header, 83 | dataType = 'json', 84 | method = 'GET', 85 | timeout = 60 * 1e3, 86 | useCache = false, 87 | failReturnCache = true, 88 | } = args2 89 | const cacheKey = `url:${url}` 90 | const cache = getStorage(cacheKey) 91 | if (useCache && cache !== null) return cache 92 | const req = new Request(url) 93 | req.method = method 94 | header && (req.headers = header) 95 | data && (req.body = data) 96 | req.timeoutInterval = timeout / 1e3 97 | req.allowInsecureRequest = true 98 | let res 99 | try { 100 | switch (dataType) { 101 | case 'json': 102 | res = await req.loadJSON() 103 | break 104 | case 'text': 105 | res = await req.loadString() 106 | break 107 | case 'image': 108 | res = await req.loadImage() 109 | break 110 | case 'data': 111 | res = await req.load() 112 | break 113 | default: 114 | res = await req.loadJSON() 115 | } 116 | const result = {...req.response, data: res} 117 | setStorage(cacheKey, result) 118 | return result 119 | } catch (err) { 120 | if (cache !== null && failReturnCache) return cache 121 | return err 122 | } 123 | } 124 | async function showActionSheet(args2) { 125 | const {title, desc, cancelText = '取消', itemList} = args2 126 | const alert = new Alert() 127 | title && (alert.title = title) 128 | desc && (alert.message = desc) 129 | for (const item of itemList) { 130 | if (typeof item === 'string') { 131 | alert.addAction(item) 132 | } else { 133 | switch (item.type) { 134 | case 'normal': 135 | alert.addAction(item.text) 136 | break 137 | case 'warn': 138 | alert.addDestructiveAction(item.text) 139 | break 140 | default: 141 | alert.addAction(item.text) 142 | break 143 | } 144 | } 145 | } 146 | alert.addCancelAction(cancelText) 147 | const tapIndex = await alert.presentSheet() 148 | return tapIndex 149 | } 150 | async function showModal(args2) { 151 | const {title, content, showCancel = true, cancelText = '取消', confirmText = '确定', inputItems = []} = args2 152 | const alert = new Alert() 153 | title && (alert.title = title) 154 | content && (alert.message = content) 155 | showCancel && cancelText && alert.addCancelAction(cancelText) 156 | alert.addAction(confirmText) 157 | for (const input of inputItems) { 158 | const {type = 'text', text = '', placeholder = ''} = input 159 | if (type === 'password') { 160 | alert.addSecureTextField(placeholder, text) 161 | } else { 162 | alert.addTextField(placeholder, text) 163 | } 164 | } 165 | const tapIndex = await alert.presentAlert() 166 | const texts = inputItems.map((item, index) => alert.textFieldValue(index)) 167 | return tapIndex === -1 168 | ? { 169 | cancel: true, 170 | confirm: false, 171 | texts, 172 | } 173 | : { 174 | cancel: false, 175 | confirm: true, 176 | texts, 177 | } 178 | } 179 | async function getImage(args2) { 180 | const {filepath, url, useCache = true} = args2 181 | const generateDefaultImage = async () => { 182 | const ctx = new DrawContext() 183 | ctx.size = new Size(100, 100) 184 | ctx.setFillColor(Color.red()) 185 | ctx.fillRect(new Rect(0, 0, 100, 100)) 186 | return await ctx.getImage() 187 | } 188 | try { 189 | if (filepath) { 190 | return Image.fromFile(filepath) || (await generateDefaultImage()) 191 | } 192 | if (!url) return await generateDefaultImage() 193 | const cacheKey = `image:${url}` 194 | if (useCache) { 195 | const cache = getCache(url) 196 | if (cache instanceof Image) { 197 | return cache 198 | } else { 199 | removeCache(cacheKey) 200 | } 201 | } 202 | const res = await request({url, dataType: 'image'}) 203 | const image = res && res.data 204 | image && setCache(cacheKey, image) 205 | return image || (await generateDefaultImage()) 206 | } catch (err) { 207 | return await generateDefaultImage() 208 | } 209 | } 210 | function hash(string) { 211 | let hash2 = 0, 212 | i, 213 | chr 214 | for (i = 0; i < string.length; i++) { 215 | chr = string.charCodeAt(i) 216 | hash2 = (hash2 << 5) - hash2 + chr 217 | hash2 |= 0 218 | } 219 | return `hash_${hash2}` 220 | } 221 | function isLaunchInsideApp() { 222 | return !config.runsInWidget && args.queryParameters.from !== URLSchemeFrom.WIDGET 223 | } 224 | async function showPreviewOptions(render) { 225 | const selectIndex = await showActionSheet({ 226 | title: '预览组件', 227 | desc: '测试桌面组件在各种尺寸下的显示效果', 228 | itemList: ['小尺寸', '中尺寸', '大尺寸', '全部尺寸'], 229 | }) 230 | switch (selectIndex) { 231 | case 0: 232 | config.widgetFamily = 'small' 233 | await (await render()).presentSmall() 234 | break 235 | case 1: 236 | config.widgetFamily = 'medium' 237 | await (await render()).presentMedium() 238 | break 239 | case 2: 240 | config.widgetFamily = 'large' 241 | await (await render()).presentLarge() 242 | break 243 | case 3: 244 | config.widgetFamily = 'small' 245 | await (await render()).presentSmall() 246 | config.widgetFamily = 'medium' 247 | await (await render()).presentMedium() 248 | config.widgetFamily = 'large' 249 | await (await render()).presentLarge() 250 | break 251 | } 252 | return selectIndex 253 | } 254 | 255 | // src/lib/jsx-runtime.ts 256 | var GenrateView = class { 257 | static setListWidget(listWidget2) { 258 | this.listWidget = listWidget2 259 | } 260 | static async wbox(props, ...children) { 261 | const {background, spacing, href, updateDate, padding, onClick} = props 262 | try { 263 | isDefined(background) && (await setBackground(this.listWidget, background)) 264 | isDefined(spacing) && (this.listWidget.spacing = spacing) 265 | isDefined(href) && (this.listWidget.url = href) 266 | isDefined(updateDate) && (this.listWidget.refreshAfterDate = updateDate) 267 | isDefined(padding) && this.listWidget.setPadding(...padding) 268 | isDefined(onClick) && runOnClick(this.listWidget, onClick) 269 | await addChildren(this.listWidget, children) 270 | } catch (err) { 271 | console.error(err) 272 | } 273 | return this.listWidget 274 | } 275 | static wstack(props, ...children) { 276 | return async parentInstance => { 277 | const widgetStack = parentInstance.addStack() 278 | const { 279 | background, 280 | spacing, 281 | padding, 282 | width = 0, 283 | height = 0, 284 | borderRadius, 285 | borderWidth, 286 | borderColor, 287 | href, 288 | verticalAlign, 289 | flexDirection, 290 | onClick, 291 | } = props 292 | try { 293 | isDefined(background) && (await setBackground(widgetStack, background)) 294 | isDefined(spacing) && (widgetStack.spacing = spacing) 295 | isDefined(padding) && widgetStack.setPadding(...padding) 296 | isDefined(borderRadius) && (widgetStack.cornerRadius = borderRadius) 297 | isDefined(borderWidth) && (widgetStack.borderWidth = borderWidth) 298 | isDefined(borderColor) && (widgetStack.borderColor = getColor(borderColor)) 299 | isDefined(href) && (widgetStack.url = href) 300 | widgetStack.size = new Size(width, height) 301 | const verticalAlignMap = { 302 | bottom: () => widgetStack.bottomAlignContent(), 303 | center: () => widgetStack.centerAlignContent(), 304 | top: () => widgetStack.topAlignContent(), 305 | } 306 | isDefined(verticalAlign) && verticalAlignMap[verticalAlign]() 307 | const flexDirectionMap = { 308 | row: () => widgetStack.layoutHorizontally(), 309 | column: () => widgetStack.layoutVertically(), 310 | } 311 | isDefined(flexDirection) && flexDirectionMap[flexDirection]() 312 | isDefined(onClick) && runOnClick(widgetStack, onClick) 313 | } catch (err) { 314 | console.error(err) 315 | } 316 | await addChildren(widgetStack, children) 317 | } 318 | } 319 | static wimage(props) { 320 | return async parentInstance => { 321 | const { 322 | src, 323 | href, 324 | resizable, 325 | width = 0, 326 | height = 0, 327 | opacity, 328 | borderRadius, 329 | borderWidth, 330 | borderColor, 331 | containerRelativeShape, 332 | filter, 333 | imageAlign, 334 | mode, 335 | onClick, 336 | } = props 337 | let _image = src 338 | typeof src === 'string' && isUrl(src) && (_image = await getImage({url: src})) 339 | typeof src === 'string' && !isUrl(src) && (_image = SFSymbol.named(src).image) 340 | const widgetImage = parentInstance.addImage(_image) 341 | widgetImage.image = _image 342 | try { 343 | isDefined(href) && (widgetImage.url = href) 344 | isDefined(resizable) && (widgetImage.resizable = resizable) 345 | widgetImage.imageSize = new Size(width, height) 346 | isDefined(opacity) && (widgetImage.imageOpacity = opacity) 347 | isDefined(borderRadius) && (widgetImage.cornerRadius = borderRadius) 348 | isDefined(borderWidth) && (widgetImage.borderWidth = borderWidth) 349 | isDefined(borderColor) && (widgetImage.borderColor = getColor(borderColor)) 350 | isDefined(containerRelativeShape) && (widgetImage.containerRelativeShape = containerRelativeShape) 351 | isDefined(filter) && (widgetImage.tintColor = getColor(filter)) 352 | const imageAlignMap = { 353 | left: () => widgetImage.leftAlignImage(), 354 | center: () => widgetImage.centerAlignImage(), 355 | right: () => widgetImage.rightAlignImage(), 356 | } 357 | isDefined(imageAlign) && imageAlignMap[imageAlign]() 358 | const modeMap = { 359 | fit: () => widgetImage.applyFittingContentMode(), 360 | fill: () => widgetImage.applyFillingContentMode(), 361 | } 362 | isDefined(mode) && modeMap[mode]() 363 | isDefined(onClick) && runOnClick(widgetImage, onClick) 364 | } catch (err) { 365 | console.error(err) 366 | } 367 | } 368 | } 369 | static wspacer(props) { 370 | return async parentInstance => { 371 | const widgetSpacer = parentInstance.addSpacer() 372 | const {length} = props 373 | try { 374 | isDefined(length) && (widgetSpacer.length = length) 375 | } catch (err) { 376 | console.error(err) 377 | } 378 | } 379 | } 380 | static wtext(props, ...children) { 381 | return async parentInstance => { 382 | const widgetText = parentInstance.addText('') 383 | const { 384 | textColor, 385 | font, 386 | opacity, 387 | maxLine, 388 | scale, 389 | shadowColor, 390 | shadowRadius, 391 | shadowOffset, 392 | href, 393 | textAlign, 394 | onClick, 395 | } = props 396 | if (children && Array.isArray(children)) { 397 | widgetText.text = children.join('') 398 | } 399 | try { 400 | isDefined(textColor) && (widgetText.textColor = getColor(textColor)) 401 | isDefined(font) && (widgetText.font = typeof font === 'number' ? Font.systemFont(font) : font) 402 | isDefined(opacity) && (widgetText.textOpacity = opacity) 403 | isDefined(maxLine) && (widgetText.lineLimit = maxLine) 404 | isDefined(scale) && (widgetText.minimumScaleFactor = scale) 405 | isDefined(shadowColor) && (widgetText.shadowColor = getColor(shadowColor)) 406 | isDefined(shadowRadius) && (widgetText.shadowRadius = shadowRadius) 407 | isDefined(shadowOffset) && (widgetText.shadowOffset = shadowOffset) 408 | isDefined(href) && (widgetText.url = href) 409 | const textAlignMap = { 410 | left: () => widgetText.leftAlignText(), 411 | center: () => widgetText.centerAlignText(), 412 | right: () => widgetText.rightAlignText(), 413 | } 414 | isDefined(textAlign) && textAlignMap[textAlign]() 415 | isDefined(onClick) && runOnClick(widgetText, onClick) 416 | } catch (err) { 417 | console.error(err) 418 | } 419 | } 420 | } 421 | static wdate(props) { 422 | return async parentInstance => { 423 | const widgetDate = parentInstance.addDate(new Date()) 424 | const { 425 | date, 426 | mode, 427 | textColor, 428 | font, 429 | opacity, 430 | maxLine, 431 | scale, 432 | shadowColor, 433 | shadowRadius, 434 | shadowOffset, 435 | href, 436 | textAlign, 437 | onClick, 438 | } = props 439 | try { 440 | isDefined(date) && (widgetDate.date = date) 441 | isDefined(textColor) && (widgetDate.textColor = getColor(textColor)) 442 | isDefined(font) && (widgetDate.font = typeof font === 'number' ? Font.systemFont(font) : font) 443 | isDefined(opacity) && (widgetDate.textOpacity = opacity) 444 | isDefined(maxLine) && (widgetDate.lineLimit = maxLine) 445 | isDefined(scale) && (widgetDate.minimumScaleFactor = scale) 446 | isDefined(shadowColor) && (widgetDate.shadowColor = getColor(shadowColor)) 447 | isDefined(shadowRadius) && (widgetDate.shadowRadius = shadowRadius) 448 | isDefined(shadowOffset) && (widgetDate.shadowOffset = shadowOffset) 449 | isDefined(href) && (widgetDate.url = href) 450 | const modeMap = { 451 | time: () => widgetDate.applyTimeStyle(), 452 | date: () => widgetDate.applyDateStyle(), 453 | relative: () => widgetDate.applyRelativeStyle(), 454 | offset: () => widgetDate.applyOffsetStyle(), 455 | timer: () => widgetDate.applyTimerStyle(), 456 | } 457 | isDefined(mode) && modeMap[mode]() 458 | const textAlignMap = { 459 | left: () => widgetDate.leftAlignText(), 460 | center: () => widgetDate.centerAlignText(), 461 | right: () => widgetDate.rightAlignText(), 462 | } 463 | isDefined(textAlign) && textAlignMap[textAlign]() 464 | isDefined(onClick) && runOnClick(widgetDate, onClick) 465 | } catch (err) { 466 | console.error(err) 467 | } 468 | } 469 | } 470 | } 471 | var listWidget = new ListWidget() 472 | GenrateView.setListWidget(listWidget) 473 | function h(type, props, ...children) { 474 | props = props || {} 475 | const _children = flatteningArr(children) 476 | switch (type) { 477 | case 'wbox': 478 | return GenrateView.wbox(props, ..._children) 479 | break 480 | case 'wdate': 481 | return GenrateView.wdate(props) 482 | break 483 | case 'wimage': 484 | return GenrateView.wimage(props) 485 | break 486 | case 'wspacer': 487 | return GenrateView.wspacer(props) 488 | break 489 | case 'wstack': 490 | return GenrateView.wstack(props, ..._children) 491 | break 492 | case 'wtext': 493 | return GenrateView.wtext(props, ..._children) 494 | break 495 | default: 496 | return type instanceof Function ? type({children: _children, ...props}) : null 497 | break 498 | } 499 | } 500 | function flatteningArr(arr) { 501 | return [].concat( 502 | ...arr.map(item => { 503 | return Array.isArray(item) ? flatteningArr(item) : item 504 | }), 505 | ) 506 | } 507 | function getColor(color) { 508 | return typeof color === 'string' ? new Color(color, 1) : color 509 | } 510 | async function getBackground(bg) { 511 | bg = (typeof bg === 'string' && !isUrl(bg)) || bg instanceof Color ? getColor(bg) : bg 512 | if (typeof bg === 'string') { 513 | bg = await getImage({url: bg}) 514 | } 515 | return bg 516 | } 517 | async function setBackground(widget, bg) { 518 | const _bg = await getBackground(bg) 519 | if (_bg instanceof Color) { 520 | widget.backgroundColor = _bg 521 | } 522 | if (_bg instanceof Image) { 523 | widget.backgroundImage = _bg 524 | } 525 | if (_bg instanceof LinearGradient) { 526 | widget.backgroundGradient = _bg 527 | } 528 | } 529 | async function addChildren(instance, children) { 530 | if (children && Array.isArray(children)) { 531 | for (const child of children) { 532 | child instanceof Function ? await child(instance) : '' 533 | } 534 | } 535 | } 536 | function isDefined(value) { 537 | if (typeof value === 'number' && !isNaN(value)) { 538 | return true 539 | } 540 | return value !== void 0 && value !== null 541 | } 542 | function isUrl(value) { 543 | const reg = /^(http|https)\:\/\/[\w\W]+/ 544 | return reg.test(value) 545 | } 546 | function runOnClick(instance, onClick) { 547 | const _eventId = hash(onClick.toString()) 548 | instance.url = `${URLScheme.forRunningScript()}?eventId=${encodeURIComponent(_eventId)}&from=${URLSchemeFrom.WIDGET}` 549 | const {eventId, from} = args.queryParameters 550 | if (eventId && eventId === _eventId && from === URLSchemeFrom.WIDGET) { 551 | onClick() 552 | } 553 | } 554 | 555 | // src/scripts/tsx-bili.tsx 556 | var {setStorage: setStorage2, getStorage: getStorage2} = useStorage('bilibili-fans') 557 | var BiliFans = class { 558 | async init() { 559 | if (isLaunchInsideApp()) { 560 | return await this.showMenu() 561 | } 562 | const widget = await this.render() 563 | Script.setWidget(widget) 564 | Script.complete() 565 | } 566 | async render() { 567 | const upId = getStorage2('up-id') || 0 568 | let follower = -1 569 | try { 570 | const getUpDataRes = (await this.getUpData(upId)).data 571 | follower = getUpDataRes.data.follower 572 | } catch (err) { 573 | console.warn('获取粉丝数失败') 574 | } 575 | const icon = await getImage({url: 'https://www.bilibili.com/favicon.ico'}) 576 | const FollowerText = () => { 577 | if (follower < 0) { 578 | return /* @__PURE__ */ h( 579 | 'wtext', 580 | { 581 | textAlign: 'center', 582 | textColor: '#fb7299', 583 | font: 14, 584 | }, 585 | '请填写B站UP主的ID', 586 | ) 587 | } else { 588 | return /* @__PURE__ */ h( 589 | 'wtext', 590 | { 591 | textAlign: 'center', 592 | textColor: '#fb7299', 593 | font: Font.boldRoundedSystemFont(this.getFontsize(follower)), 594 | }, 595 | this.toThousands(follower), 596 | ) 597 | } 598 | } 599 | return /* @__PURE__ */ h( 600 | 'wbox', 601 | { 602 | href: 'bilibili://', 603 | }, 604 | /* @__PURE__ */ h( 605 | 'wstack', 606 | null, 607 | /* @__PURE__ */ h('wimage', { 608 | src: icon, 609 | width: 15, 610 | height: 15, 611 | }), 612 | /* @__PURE__ */ h('wspacer', { 613 | length: 10, 614 | }), 615 | /* @__PURE__ */ h( 616 | 'wtext', 617 | { 618 | opacity: 0.9, 619 | font: 14, 620 | }, 621 | '哔哩哔哩粉丝', 622 | ), 623 | ), 624 | /* @__PURE__ */ h('wspacer', null), 625 | /* @__PURE__ */ h(FollowerText, null), 626 | /* @__PURE__ */ h('wspacer', null), 627 | /* @__PURE__ */ h( 628 | 'wtext', 629 | { 630 | font: 12, 631 | textAlign: 'center', 632 | opacity: 0.5, 633 | }, 634 | '更新于:', 635 | this.nowTime(), 636 | ), 637 | ) 638 | } 639 | async showMenu() { 640 | const selectIndex = await showActionSheet({ 641 | title: '菜单', 642 | itemList: ['设置 up 主 id', '预览尺寸'], 643 | }) 644 | switch (selectIndex) { 645 | case 0: 646 | const {cancel, texts} = await showModal({ 647 | title: '请输入 up 主的 id', 648 | inputItems: [ 649 | { 650 | text: getStorage2('up-id') || '', 651 | placeholder: '去网页版 up 主页,可以看到 id', 652 | }, 653 | ], 654 | }) 655 | if (cancel) return 656 | if (texts && texts[0]) setStorage2('up-id', texts[0]) 657 | break 658 | case 1: 659 | await showPreviewOptions(this.render.bind(this)) 660 | break 661 | } 662 | } 663 | async getUpData(id) { 664 | return await request({ 665 | url: `http://api.bilibili.com/x/relation/stat?vmid=${id}`, 666 | dataType: 'json', 667 | }) 668 | } 669 | toThousands(num) { 670 | return (num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') 671 | } 672 | getFontsize(num) { 673 | if (num < 99) { 674 | return 38 675 | } else if (num < 9999 && num > 100) { 676 | return 30 677 | } else if (num < 99999 && num > 1e4) { 678 | return 28 679 | } else if (num < 999999 && num > 1e5) { 680 | return 24 681 | } else if (num < 9999999 && num > 1e6) { 682 | return 22 683 | } else { 684 | return 20 685 | } 686 | } 687 | nowTime() { 688 | const date = new Date() 689 | return date.toLocaleTimeString('chinese', {hour12: false}) 690 | } 691 | } 692 | EndAwait(() => new BiliFans().init()) 693 | 694 | await __topLevelAwait__() 695 | --------------------------------------------------------------------------------