[fix]页面创建

This commit is contained in:
陶林 2022-06-02 17:00:01 +08:00
commit efe5165c9e
20 changed files with 4514 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
package-lock.json
Widget.js
.DS_File
Dist/

42
README.md Normal file
View File

@ -0,0 +1,42 @@
# 介绍
这是一个便于开发者在电脑上开发、测试、预览 iOS 小组件Scriptable的开发框架
通过简单安装就可以获得一个舒适的Scriptable脚本开发环境支持语法高亮、自动补全、实时同步测试预览。
不用再在手机上敲代码了!并且该开发框架封装了很多常用的操作接口,让开发者专注数据解析+小组件UI设计大大节省开发时间
# 开始
**首先,我们配置电脑开发环境:**
3. VSCode打开代码目录进入终端运行安装依赖命令`npm install`
4. 安装好依赖,开启开发服务命令:`npm start`
**然后,配置手机运行环境:**
1. 运行服务后,会输出地址,手机访问该地址即可按照步骤初始化。或手动复制 [install-runtime.js](install-runtime.js) 脚本代码,打开 `Scriptable` 应用,点击右上角➕,粘贴代码,点击运行
2. 如果成功,应该新加了两个插件文件:`「小件件」开发环境`、「`源码」小组件示例`
3. 点击 `「源码」小组件示例` 或者其他任何基于此框架开发的小组件,点击操作菜单的远程开发,即可连接电脑,开启远程开发体验!
# 发布
开发测试完毕后,可以 `pull` 到本分支进行开源分享
小组件源码存放在 [Scrips](Scripts) 目录,你也可以复制其他的小组件进行修改使用。
**打包分享** 你可以使用如下命令,打包你的小组件成一个单独的文件,从而可以分享给其他用户使用:
``` bash
$ node pack.js Scripts/「源码」你的小组件.js
```
> 将会生成在 `Dist` 目录
**压缩代码**:打包的文件过大,如果需要压缩减少体积、加密敏感信息,可以通过如下脚本处理打包后的文件:
``` bash
$ node encode.js Dist/「小件件」你的小组件.js
```
> 将会在 `Dist` 目录生成 `「小件件」你的小组件.enc.js` 文件
> 该脚本需要`javascript-obfuscator`库,如未安装请先在项目目录 `npm install`

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,372 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-green; icon-glyph: angle-double-right;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
constructor (arg) {
super(arg)
this.logo = "https://www.v2ex.com/static/img/icon_rayps_64.png"
this.name = "V2EX"
this.desc = "创意工作者们的社区"
// 请求数据接口列表(收集整理中)
this.API = [
// api
[
{
id: 'latest',
name: '最新'
}, {
id: 'hot',
name: '最热'
}
],
// tab
[
{
id: 'all',
name: '全部'
}, {
id: 'hot',
name: '最热'
}, {
id: 'tech',
name: '技术'
}, {
id: 'creative',
name: '创意'
}, {
id: 'playplay',
name: '好玩'
}, {
id: 'apple',
name: 'Apple'
}, {
id: 'jobs',
name: '酷工作'
}, {
id: 'deals',
name: '交易'
}, {
id: 'city',
name: '城市'
}, {
id: 'qna',
name: '问与答'
}
],
// go
[],
]
// 当前设置的存储key提示可通过桌面设置不同参数来保存多个设置
let _md5 = this.md5(module.filename)
this.CACHE_KEY = `cache_${_md5}`
// 获取设置
// 格式type@name比如 go@create、api@hot、tab@all
this.SETTINGS = this.settings['node'] || 'tab@all'
// 注册操作菜单
this.registerAction("节点设置", this.actionSettings)
}
// 渲染组件
async render () {
// 加载节点列表
await this._loadNodes()
const data = await this.getData()
console.log(data)
if (this.widgetFamily === 'medium') {
return await this.renderMedium(data)
} else if (this.widgetFamily === 'large') {
return await this.renderLarge(data)
} else {
return await this.renderSmall(data)
}
}
async renderSmall (data) {
let w = new ListWidget()
let topic = data[0]
w.url = this.actionUrl('open-url', topic['url'])
w = await this.renderHeader(w, this.logo, this.name + ' / ' + this.SETTINGS.split('@')[0], Color.white())
let content = w.addText(topic['title'])
content.font = Font.lightSystemFont(16)
content.textColor = Color.white()
content.lineLimit = 3
w.backgroundImage = await this.shadowImage(await this.getImageByUrl(topic['member']['avatar_large'].replace('mini', 'large')))
w.addSpacer()
let footer = w.addText(`@${topic['member']['username']} / ${topic['node']['title']}`)
footer.font = Font.lightSystemFont(10)
footer.textColor = Color.white()
footer.textOpacity = 0.5
footer.lineLimit = 1
return w
}
// 中尺寸组件
async renderMedium (data) {
let w = new ListWidget()
// w.addSpacer(10)
// 设置名称
let tmp = this.SETTINGS.split('@')
let tid = tmp[0] === 'api' ? 0 : (tmp[0] === 'tab' ? 1 : 2)
let current = ''
this.API[tid].map(a => {
if (a['id'] === tmp[1]) current = a['name']
})
await this.renderHeader(w, this.logo, this.name + ' / ' + current)
w.addSpacer()
let body = w.addStack()
let bodyleft= body.addStack()
bodyleft.layoutVertically()
for (let i = 0; i < 2; i ++) {
bodyleft = await this.renderCell(bodyleft, data[i])
bodyleft.addSpacer()
}
// body.addSpacer()
w.url = this.actionUrl("settings")
return w
}
// 大尺寸组件
async renderLarge (data) {
let w = new ListWidget()
// w.addSpacer(10)
// 设置名称
let tmp = this.SETTINGS.split('@')
let tid = tmp[0] === 'api' ? 0 : (tmp[0] === 'tab' ? 1 : 2)
let current = ''
this.API[tid].map(a => {
if (a['id'] === tmp[1]) current = a['name']
})
await this.renderHeader(w, this.logo, this.name + ' / ' + current)
w.addSpacer()
let body = w.addStack()
let bodyleft= body.addStack()
bodyleft.layoutVertically()
for (let i = 0; i < 5; i ++) {
bodyleft = await this.renderCell(bodyleft, data[i])
bodyleft.addSpacer()
}
// body.addSpacer()
// w.addSpacer()
w.url = this.actionUrl("settings")
return w
}
async renderCell (widget, topic) {
let body = widget.addStack()
body.url = this.actionUrl('open-url', topic['url'])
let left = body.addStack()
let avatar = left.addImage(await this.getImageByUrl(topic['member']['avatar_large'].replace('mini', 'large')))
avatar.imageSize = new Size(35, 35)
avatar.cornerRadius = 5
body.addSpacer(10)
let right = body.addStack()
right.layoutVertically()
let content = right.addText(topic['title'])
content.font = Font.lightSystemFont(14)
content.lineLimit = 1
right.addSpacer(5)
let info = right.addText(`@${topic['member']['username']} / ${topic['node']['title']}`)
info.font = Font.lightSystemFont(10)
info.textOpacity = 0.6
info.lineLimit = 2
widget.addSpacer()
return widget
}
async getData () {
// 解析设置,判断类型,获取对应数据
const tmp = this.SETTINGS.split('@')
switch (tmp[0]) {
case 'tab':
return await this.getDataForTab(tmp[1])
case 'go':
return await this.getDataForGo(tmp[1])
case 'api':
return await this.getDataForApi(tmp[1])
}
}
/**
* 获取首页tab数据
* @param {string} tab tab首页名称
*/
async getDataForTab (tab = 'all') {
let url = `https://www.v2ex.com/?tab=${tab}`
let html = await this.fetchAPI(url, false)
// 解析html
let tmp = html.split(`<div id="Wrapper">`)[1].split(`<div class="inner" style="text-align: right;">`)[0]
let arr = tmp.split('<div class="cell item">')
arr.shift()
let datas = []
for (let i = 0; i < arr.length; i ++) {
let t = arr[i]
let title = t.split(`class="topic-link">`)[1].split('</a')[0]
let avatar = t.split(`<img src="`)[1].split('"')[0]
let node = t.split(`<a class="node"`)[1].split('</a>')[0].split('>')[1]
let user = t.split(`<a class="node" href="`)[1].split('</strong>')[0].split('<strong>')[1].split('>')[1].split('</')[0]
let link = t.split(`<span class="item_title">`)[1].split('class="')[0].split('"')[1]
datas.push({
title,
url: `https://www.v2ex.com${link}`,
member: {
username: user,
'avatar_large': avatar
},
node: {
title: node
}
})
}
return datas
}
async getDataForApi (api) {
return await this.httpGet(`https://www.v2ex.com/api/topics/${api}.json`)
}
/**
* 加载数据
* 节点列表
*/
async getDataForGo (arg = 'create') {
let url = `https://www.v2ex.com/go/${arg}`
let html = await this.fetchAPI(url, false)
// 解析html
let tmp = html.split(`<div id="Wrapper">`)[1].split(`<div class="sidebar_units">`)[0]
let arr = tmp.split(`<table cellpadding="0" cellspacing="0" border="0" width="100%">`)
let node_title = html.split('<title>')[1].split('</')[0]
arr.shift()
arr.pop()
let datas = []
for (let i = 0; i < arr.length; i ++) {
let t = arr[i]
let title = t.split(`class="topic-link">`)[1].split('</a')[0]
let avatar = t.split(`<img src="`)[1].split('"')[0]
let user = t.split(`class="small fade"><strong>`)[1].split('</')[0]
let link = t.split(`<span class="item_title"><a href="`)[1].split('"')[0]
datas.push({
title,
url: `https://www.v2ex.com${link}`,
member: {
username: user,
'avatar_large': avatar
},
node: {
title: node_title
}
})
}
return datas
}
// http.get
async fetchAPI (api, json = true) {
let data = null
try {
let req = new Request(api)
req.headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/85.0.4183.102'
}
data = await (json ? req.loadJSON() : req.loadString())
} catch (e) {}
// 判断数据是否为空(加载失败)
if (!data) {
// 判断是否有缓存
if (Keychain.contains(this.CACHE_KEY)) {
let cache = Keychain.get(this.CACHE_KEY)
return json ? JSON.parse(cache) : cache
} else {
// 刷新
return null
}
}
// 存储缓存
Keychain.set(this.CACHE_KEY, json ? JSON.stringify(data) : data)
return data
}
// 加载节点列表
async _loadNodes () {
let s2 = await this.httpGet("https://www.v2ex.com/api/nodes/s2.json")
// 排序通过topic
let nodes = s2.sort((a,b) => b['topics']-a['topics'])
this.API[2] = nodes.map(n => ({
id: n['id'],
name: n['text'],
topic: n['topic']
}))
return this.API[2]
}
async actionOpenUrl (url) {
Safari.openInApp(url, false)
}
async actionSettings () {
const tmp = this.SETTINGS.split('@')
const a = new Alert()
a.title = "内容设置"
a.message = "设置组件展示的内容来自哪里"
a.addAction((tmp[0]==='api'?'✅ ':'')+"API接口")
a.addAction((tmp[0]==='tab'?'✅ ':'')+"首页目录")
a.addAction((tmp[0]==='go'?'✅ ':'')+"指定节点")
a.addCancelAction("取消设置")
const i = await a.presentSheet()
if (i === -1) return
const table = new UITable()
// 如果是节点,则先远程获取
if (i === 2 && this.API[2].length === 0) {
await this._loadNodes()
}
this.API[i].map(t => {
const r = new UITableRow()
r.addText((tmp[1]===t.id?'✅ ':'')+t['name'])
r.onSelect = (n) => {
// 保存设置
let _t = 'api';
_t = i === 1 ? 'tab' : _t;
_t = i === 2 ? 'go' : _t;
let v = `${_t}@${t['id']}`
this.SETTINGS = v
this.settings['node'] = v
this.saveSettings()
}
table.addRow(r)
})
table.present(false)
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,135 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: apple-alt;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
/**
* 传递给组件的参数可以是桌面 Parameter 数据也可以是外部如 URLScheme 等传递的数据
* @param {string} arg 自定义参数
*/
constructor (arg) {
super(arg)
this.name = 'iOS 限免'
this.logo = 'https://api.kzddck.com/script/freeapp.png'
this.desc = 'AppStore 每日限免App速递'
this.registerAction("关于插件", this.aboutHandler)
}
/**
* 渲染函数函数名固定
* 可以根据 this.widgetFamily 来判断小组件尺寸以返回不同大小的内容
*/
async render () {
const data = await this.getData()
console.log('data=')
console.log(data)
switch (this.widgetFamily) {
case 'large':
return await this.renderLarge(data)
case 'medium':
return await this.renderMedium(data)
default:
return await this.renderSmall(data)
}
}
/**
* 渲染小尺寸组件
*/
async renderSmall (data) {
let w = new ListWidget()
this.renderHeader(w, this.logo, this.name, Color.white())
// w.addSpacer(10)
const name = w.addText(data['name'].trim())
name.font = Font.boldSystemFont(18)
name.lineLimit = 2
w.addSpacer(10)
const c = w.addText(data['class'].trim())
c.font = Font.lightSystemFont(12)
w.addSpacer()
const price = w.addText(data['price'])
price.font = Font.lightSystemFont(12)
price.textOpacity = 0.6
w.backgroundImage = await this.shadowImage(await this.getImageByUrl(data['img']))
w.url = data['url']
name.textColor = c.textColor = price.textColor = Color.white()
return w
}
/**
* 渲染中尺寸组件
*/
async renderMedium (data, lineLimit = 3) {
let w = new ListWidget()
const body = w.addStack()
const leftBox = body.addStack()
leftBox.layoutVertically()
const icon = leftBox.addImage(await this.getImageByUrl(data['img']))
icon.imageSize = new Size(60, 60)
icon.cornerRadius = 5
leftBox.addSpacer(10)
const price = leftBox.addText(data['price'])
price.font = Font.lightSystemFont(10)
price.textOpacity = 0.8
body.addSpacer(10)
const rightBox = body.addStack()
rightBox.layoutVertically()
const title = rightBox.addText(data['name'])
title.font = Font.boldSystemFont(18)
rightBox.addSpacer(5)
const c = rightBox.addText(data['class'])
c.font = Font.lightSystemFont(12)
c.textOpacity = 0.8
rightBox.addSpacer(5)
const desc = rightBox.addText(data['content'])
desc.lineLimit = lineLimit
desc.font = Font.lightSystemFont(14)
w.url = data['url']
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge (data) {
return await this.renderMedium(data, 15)
}
/**
* 获取数据函数函数名可不固定
*/
async getData () {
return await this.httpGet("https://api.kzddck.com/script/free.json")
}
async aboutHandler () {
const a = new Alert()
a.title = "关于"
a.message = "本插件数据来自接口https://api.kzddck.com/script/free.json\n感谢群友 @你很闹i 分享"
a.addCancelAction("了解")
return await a.presentAlert()
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,163 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: purple; icon-glyph: comment-alt;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const {
Base
} = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
constructor(arg) {
super(arg)
this.name = '一言'
this.desc = '在茫茫句海中寻找能感动你的句子'
this.settings = this.getSettings(true, true)
if (this.settings && this.settings['arg']) {
this.arg = this.settings['arg']
}
console.log('arg=' + this.arg)
// 注册设置
this.registerAction('插件设置', this.actionSetting)
}
async render() {
let w = new ListWidget()
await this.renderHeader(
w,
'https://txc.gtimg.com/data/285778/2020/1012/f9cf50f08ebb8bd391a7118c8348f5d8.png',
'一言'
)
let data = await this._getData()
let content = w.addText(data['hitokoto'])
content.font = Font.lightSystemFont(16)
w.addSpacer()
let footer = w.addText(data['from'])
footer.font = Font.lightSystemFont(12)
footer.textOpacity = 0.5
footer.rightAlignText()
footer.lineLimit = 1
w.url = this.actionUrl("menus", JSON.stringify(data));
return w
}
async _getData() {
let args = 'abcdefghijk'
const types = this.arg.split('')
.filter(c => args.indexOf(c) > -1)
.map(c => `c=${c}`)
.join('&') || 'c=k'
let api = `https://v1.hitokoto.cn/?${types}&encode=json`
return await this.httpGet(api)
}
async actionSetting() {
let a = new Alert()
a.title = "插件设置"
a.message = "桌面组件的个性化设置"
a.addAction("句子类型")
a.addCancelAction("取消设置")
let id = await a.presentSheet()
if (id === 0) {
return await this.actionSetting1()
}
}
/**
* 句子类型设置
*/
async actionSetting1() {
console.warn('setting--->' + this.arg)
// 设置句子类型
// 1. 获取本地存储(如果有)
let caches = {}
if (this.arg) {
this.arg.split('').map(a => {
caches[a] = true
})
}
let a1 = new Alert()
let keys = [
['a', '动画'],
['b', '漫画'],
['c', '游戏'],
['d', '文学'],
['e', '原创'],
['g', '其他'],
['h', '影视'],
['i', '诗词'],
['k', '哲学'],
['j', '网易云'],
['f', '来自网络'],
]
a1.title = "句子类型"
a1.message = "桌面组件显示的语句内容类型"
keys.map(k => {
let _id = k[0]
let _name = k[1]
if (caches[_id]) {
_name = `${_name}`
}
a1.addAction(_name)
})
a1.addCancelAction("完成设置")
let id1 = await a1.presentSheet()
if (id1 === -1) return this.saveSettings()
console.log(id1)
let arg = keys[id1]
// 本地存储
if (caches[arg[0]]) {
// 已经有了,那么就取消
caches[arg[0]] = false
} else {
caches[arg[0]] = true
}
// 重新获取设置
let _caches = []
for (let k in caches) {
if (caches[k]) {
_caches.push(k)
}
}
this.arg = _caches.join('');
this.settings["arg"] = this.arg;
// this.saveSettings(false);
// console.log('save-setting:' + this.arg)
// Keychain.set(this.SETTING_KEY, this.arg)
return await this.actionSetting1()
}
// 用户点击组件,触发的 action
async actionMenus(content) {
// this.settings = this.getSettings()
const data = JSON.parse(content)
const alert = new Alert()
alert.title = "一言"
alert.message = data['hitokoto']
alert.addAction("复制内容")
alert.addAction("内容设置")
alert.addAction("关于一言")
alert.addCancelAction("取消操作")
const idx = await alert.presentSheet()
if (idx === 0) {
Pasteboard.copyString(data['hitokoto'] + "\n" + "—— " + data['from'])
} else if (idx === 1) {
return await this.actionSetting1()
} else if (idx === 2) {
Safari.openInApp('https://hitokoto.cn/about', false)
}
}
}
// @组件代码结束
const {
Testing
} = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,244 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: hand-holding-usd;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
/**
* 传递给组件的参数可以是桌面 Parameter 数据也可以是外部如 URLScheme 等传递的数据
* @param {string} arg 自定义参数
*/
constructor (arg) {
super(arg)
this.name = '京东白条'
this.desc = '显示京东白条账号额度和还款数据'
this.logo = 'https://m.jr.jd.com/statics/logo.jpg'
this.registerAction("登录京东", this.actionLogin)
}
/**
* 渲染函数函数名固定
* 可以根据 this.widgetFamily 来判断小组件尺寸以返回不同大小的内容
*/
async render () {
const data = await this.getData()
try {
if (data.resultCode !== 0) {
return this.renderFail(data['resultMsg'], true);
}
if (!data.resultData.data['quota'] || !data.resultData.data['bill']) {
return this.renderFail("数据获取失败,请联系反馈更新")
}
} catch (e) {
return this.renderFail("数据解析失败")
}
switch (this.widgetFamily) {
case 'large':
return await this.renderLarge(data['resultData']['data'])
case 'medium':
return await this.renderMedium(data['resultData']['data'])
default:
return await this.renderSmall(data['resultData']['data'])
}
}
async renderFail (msg, login = false) {
const w = new ListWidget()
w.addText("⚠️")
w.addSpacer(10)
const t = w.addText(msg)
t.textColor = Color.red()
t.font = Font.boldSystemFont(14)
w.url = login ? this.actionUrl('login') : this.actionUrl()
return w
}
/**
* 渲染小尺寸组件
*/
async renderSmall (data) {
let w = new ListWidget()
w.url = this.actionUrl('open-url')
await this.renderHeader(w, this.logo, this.name)
const bg = new LinearGradient()
bg.locations = [0, 1]
bg.colors = [
new Color('#f35942', 1),
new Color('#e92d1d', 1)
]
w.backgroundGradient = bg
// 判断参数如果传递1则显示待还否则显示额度
let info = {}
if (this.arg === "1") {
info = {
title: data['bill']['title'],
data: data['bill']['amount'],
desc: data['bill']['buttonName']
}
} else {
info = {
title: '可用额度',
data: data['quota']['quotaLeft'],
desc: '总额度:' + data['quota']['quotaAll']
}
}
const box = w.addStack()
const body = box.addStack()
body.layoutVertically()
const title = body.addText(info.title)
title.font = Font.boldSystemFont(16)
body.addSpacer(10)
const num = body.addText(info.data)
num.font = Font.systemFont(24)
body.addSpacer()
const desc = body.addText(info.desc)
desc.font = Font.lightSystemFont(12)
desc.textOpacity = 0.8
desc.lineLimit = 1
box.addSpacer()
return w
}
/**
* 渲染中尺寸组件
*/
async renderMedium (data) {
let w = new ListWidget()
w.url = this.actionUrl('open-url')
// const bg = new LinearGradient()
// bg.locations = [0, 1]
// bg.colors = [
// new Color('#f35942', 1),
// new Color('#e92d1d', 1)
// ]
// w.backgroundGradient = bg
w.backgroundImage = await this.getImageByUrl('https://txc.gtimg.com/data/287371/2020/1124/30e1524a9288442bec9243c9afa40e90.png')
await this.renderHeader(w, this.logo, this.name, Color.white())
const VIEW_TOP = w.addStack()
VIEW_TOP.addSpacer(24)
const TOP_LEFT = VIEW_TOP.addStack()
TOP_LEFT.layoutVertically()
const t11 = TOP_LEFT.addText("可用额度")
t11.font = Font.boldSystemFont(16)
TOP_LEFT.addSpacer(10)
const t12 = TOP_LEFT.addText(data['quota']['quotaLeft'])
t12.font = Font.systemFont(24)
TOP_LEFT.addSpacer()
const t13 = TOP_LEFT.addText("总额度:" + data['quota']['quotaAll'])
t13.font = Font.lightSystemFont(12)
t13.textOpacity = 0.8
VIEW_TOP.addSpacer()
const TOP_RIGHT = VIEW_TOP.addStack()
TOP_RIGHT.layoutVertically()
const t21 = TOP_RIGHT.addText(data['bill']['title'])
t21.font = Font.boldSystemFont(16)
TOP_RIGHT.addSpacer(10)
const t22 = TOP_RIGHT.addText(data['bill']['amount'])
t22.font = Font.systemFont(24)
TOP_RIGHT.addSpacer()
const t23 = TOP_RIGHT.addText(data['bill']['buttonName'])
t23.font = Font.lightSystemFont(12)
t23.textOpacity = 0.8
;[t11, t12, t13, t21, t22, t23].map(t => t.textColor = Color.white())
VIEW_TOP.addSpacer(20)
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge (data) {
return await this.renderFail("暂只支持中尺寸小组件")
}
async getData () {
const pt_key = this.settings['pt_key']
const req = new Request("https://ms.jr.jd.com/gw/generic/bt/h5/m/firstScreenNew")
req.method = "POST"
req.body = 'reqData={"clientType":"ios","clientVersion":"13.2.3","deviceId":"","environment":"3"}'
req.headers = {
Cookie: 'pt_key=' + pt_key
}
const res = await req.loadJSON()
return res
}
async actionLogin () {
const webView = new WebView()
webView.loadURL('https://mcr.jd.com/credit_home/pages/index.html?btPageType=BT&channelName=024')
// 循环获取cookie
const tm = new Timer()
tm.timeInterval = 1000
tm.repeats = true
tm.schedule(async () => {
const req = new Request("https://ms.jr.jd.com/gw/generic/bt/h5/m/firstScreenNew")
req.method = "POST"
req.body = 'reqData={"clientType":"ios","clientVersion":"13.2.3","deviceId":"","environment":"3"}'
const res = await req.loadJSON()
const cookies = req.response.cookies
cookies.map(cookie => {
if (cookie['name'] === 'pt_key') {
// 存储,并通知成功
this.notify("登录成功", "登录凭证已保存!可以关闭当前登录页面了!")
tm.invalidate()
this.settings['pt_key'] = cookie['value']
this.saveSettings(false)
return
}
})
})
await webView.present(true)
tm.invalidate()
}
async actionOpenUrl () {
Safari.openInApp('https://mcr.jd.com/credit_home/pages/index.html?btPageType=BT', false)
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,342 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: green; icon-glyph: battery-half;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
/**
* 传递给组件的参数可以是桌面 Parameter 数据也可以是外部如 URLScheme 等传递的数据
* @param {string} arg 自定义参数
*/
constructor (arg) {
super(arg)
this.name = '人生电量'
this.desc = '预计一下余生还剩多少电量'
this.logo = 'https://txc.gtimg.com/data/287371/2020/1105/a8d2e9e19644b244b7a2307bdf2609c0.png'
this.registerAction("设置信息", this.actionSettings)
this.registerAction("透明背景", this.actionSettings3)
this.BG_FILE = this.getBackgroundImage()
if (this.BG_FILE) this.registerAction("移除背景", this.actionSettings4)
}
/**
* 渲染函数函数名固定
* 可以根据 this.widgetFamily 来判断小组件尺寸以返回不同大小的内容
*/
async render () {
if (!this.settings || !this.settings['name'] || !this.settings['date'] || !this.settings['gender']) {
return await this.renderConfigure()
}
switch (this.widgetFamily) {
case 'large':
return await this.renderLarge()
case 'medium':
return await this.renderMedium()
default:
return await this.renderSmall()
}
}
/**
* 手工绘制电量图标
* @param {int} num 0-100 电量
*/
async renderBattery (stack, num = 100, size = 'small') {
const SIZES = {
small: {
width: 40,
height: 20,
borderWidth: 3,
cornerRadius: 3,
rightWidth: 2,
rightHeight: 8,
spacer: 3
},
medium: {
width: 80,
height: 40,
borderWidth: 5,
cornerRadius: 10,
rightWidth: 5,
rightHeight: 15,
spacer: 5
},
large: {}
}
const SIZE = SIZES[size]
// 电池颜色
let color = new Color("#CCCCCC", 1)
if (num < 40) color = Color.yellow()
if (num > 80) color = Color.green()
const box = stack.addStack()
box.centerAlignContent()
const boxLeft = box.addStack()
boxLeft.size = new Size(SIZE['width'], SIZE['height'])
boxLeft.borderColor = new Color('#CCCCCC', 0.8)
boxLeft.borderWidth = SIZE['borderWidth']
boxLeft.cornerRadius = SIZE['cornerRadius']
// 中间电量
// 根据电量计算电量矩形的长总长80-边距10
// 算法70/100 * 电量
const BATTERY_WIDTH = parseInt((SIZE['width'] - (SIZE['spacer']*2)) / 100 * num)
boxLeft.addSpacer(SIZE['spacer'])
boxLeft.setPadding(SIZE['spacer'], 0, SIZE['spacer'], 0)
const boxCenter = boxLeft.addStack()
boxCenter.backgroundColor = color
boxCenter.size = new Size(BATTERY_WIDTH, SIZE['height'] - SIZE['spacer']*2)
boxCenter.cornerRadius = SIZE['cornerRadius'] / 2
boxLeft.addSpacer((SIZE['width'] - SIZE['spacer']*2) - BATTERY_WIDTH + SIZE['spacer'])
box.addSpacer(2)
const boxRight = box.addStack()
boxRight.backgroundColor = new Color('#CCCCCC', 0.8)
boxRight.cornerRadius = 5
boxRight.size = new Size(SIZE['rightWidth'], SIZE['rightHeight'])
return box
}
// 提示配置
async renderConfigure () {
const w = new ListWidget()
w.addText("请点击组件进行设置信息")
w.url = this.actionUrl("settings")
return w
}
// 获取电量值
getPricNum () {
// 电量
// 男7578预计寿命
const SM = this.settings['gender'] === '男' ? 75 : 78
// 1. 已经过了多少天
const DAY_TO_NOW = Math.floor((+new Date() - (+new Date(this.settings['date']))) / (24*60*60*1000))
// 2. 百分比
const PRIC_NUM = parseFloat(1-(DAY_TO_NOW / (75*365))).toFixed(2) * 100
return PRIC_NUM
}
/**
* 渲染小尺寸组件
*/
async renderSmall () {
let w = new ListWidget()
// 名称
await this.renderHeader(w, this.logo, this.name, this.BG_FILE ? Color.white() : null)
const PRIC_NUM = this.getPricNum()
const battery = w.addStack()
battery.addSpacer()
await this.renderBattery(battery, PRIC_NUM)
battery.addSpacer()
w.addSpacer(5)
const num = w.addText(` ${PRIC_NUM} %`)
num.centerAlignText()
num.font = Font.systemFont(36)
// 生日
w.addSpacer()
const _date = new DateFormatter()
_date.dateFormat = "yyyy/MM/dd"
const date = w.addText(this.settings['name'] + ' @ ' + _date.string(new Date(this.settings['date'])))
date.font = Font.lightSystemFont(10)
date.textOpacity = 0.8
date.centerAlignText()
if (this.BG_FILE) {
w.backgroundImage = this.BG_FILE
num.textColor = date.textColor = Color.white()
}
w.url = this.actionUrl("settings")
return w
}
/**
* 渲染中尺寸组件
*/
async renderMedium () {
let w = new ListWidget()
await this.renderHeader(w, this.logo, this.name, this.BG_FILE ? Color.white() : null)
w.addSpacer()
const name = w.addText(this.settings['name'])
name.centerAlignText()
name.font = Font.systemFont(14)
name.textOpacity = 0.8
w.addSpacer(10)
const box = w.addStack()
box.centerAlignContent()
box.addSpacer()
// 中间电量
const PRIC_NUM = this.getPricNum()
const num = box.addText(`${PRIC_NUM} %`)
num.font = Font.boldSystemFont(34)
box.addSpacer(10)
await this.renderBattery(box, PRIC_NUM, 'medium')
box.addSpacer()
w.addSpacer()
w.addSpacer(5)
const _date = new DateFormatter()
_date.dateFormat = "yyyy / MM / dd"
const date = w.addText(_date.string(new Date(this.settings['date'])))
date.font = Font.lightSystemFont(12)
date.textOpacity = 0.8
date.rightAlignText()
if (this.BG_FILE) {
w.backgroundImage = this.BG_FILE
name.textColor = num.textColor = date.textColor = Color.white()
}
w.url = this.actionUrl("settings")
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge () {
return await this.renderMedium()
}
/**
* 获取数据函数函数名可不固定
*/
async getData () {
return false
}
/**
* 自定义注册点击事件 actionUrl 生成一个触发链接点击后会执行下方对应的 action
* @param {string} url 打开的链接
*/
async actionOpenUrl (url) {
Safari.openInApp(url, false)
}
async actionSettings () {
const a = new Alert()
a.title = "设置信息"
a.message = "配置您的信息,以便小组件进行计算展示"
const menus = ['输入名称', '选择生日', '选择性别'];
;[{
name:'name',
text: '输入名称'
}, {
name: 'date',
text: '选择生日'
}, {
name: 'gender',
text: '选择性别'
}].map(item => {
a.addAction((this.settings[item.name] ? '✅ ' : '❌ ') + item.text)
})
a.addCancelAction('取消设置')
const id = await a.presentSheet()
if (id === -1) return
await this['actionSettings' + id]()
}
// 设置名称
async actionSettings0 () {
const a = new Alert()
a.title = "输入名称"
a.message = "请输入小组件显示的用户名称"
a.addTextField("名称", this.settings['name'])
a.addAction("确定")
a.addCancelAction("取消")
const id = await a.presentAlert()
if (id === -1) return await this.actionSettings()
const n = a.textFieldValue(0)
if (!n) return await this.actionSettings0()
this.settings['name'] = n
this.saveSettings()
return await this.actionSettings()
}
// 选择生日
async actionSettings1 () {
const dp = new DatePicker()
if (this.settings['date']) {
dp.initialDate = new Date(this.settings['date'])
}
let date
try {
date = await dp.pickDate()
} catch (e) {
return await this.actionSettings()
}
this.settings['date'] = date
this.saveSettings()
return await this.actionSettings()
}
// 选择性别
async actionSettings2 () {
const a = new Alert()
a.title = "选择性别"
a.message = "性别可用于预计寿命"
const genders = ['男', '女']
genders.map(n => {
a.addAction((this.settings['gender'] === n ? '✅ ' : '') + n)
})
a.addCancelAction('取消选择')
const i = await a.presentSheet()
if (i !== -1) {
this.settings['gender'] = genders[i]
this.saveSettings()
}
return await this.actionSettings()
}
// 透明背景
async actionSettings3 () {
const img = await this.getWidgetScreenShot()
if (!img) return
this.setBackgroundImage(img)
}
// 移除背景
async actionSettings4 () {
this.setBackgroundImage(null)
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,106 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: orange; icon-glyph: comments;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const {
Base
} = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
/**
* 传递给组件的参数可以是桌面 Parameter 数据也可以是外部如 URLScheme 等传递的数据
* @param {string} arg 自定义参数
*/
constructor(arg) {
super(arg)
this.name = '示例小组件'
this.desc = '「小件件」—— 原创精美实用小组件'
}
/**
* 渲染函数函数名固定
* 可以根据 this.widgetFamily 来判断小组件尺寸以返回不同大小的内容
*/
async render() {
const data = await this.getData()
switch (this.widgetFamily) {
case 'large':
return await this.renderLarge(data)
case 'medium':
return await this.renderMedium(data)
default:
return await this.renderSmall(data)
}
}
/**
* 渲染小尺寸组件
*/
async renderSmall(data) {
let w = new ListWidget()
await this.renderHeader(w, data['logo'], data['title'])
const t = w.addText(data['content'])
t.font = Font.lightSystemFont(16)
w.addSpacer()
w.url = this.actionUrl('open-url', data['url'])
return w
}
/**
* 渲染中尺寸组件
*/
async renderMedium(data, num = 3) {
let w = new ListWidget()
await this.renderHeader(w, data['logo'], data['title'])
data['data'].slice(0, num).map(d => {
const cell = w.addStack()
cell.centerAlignContent()
const cell_box = cell.addStack()
cell_box.size = new Size(3, 15)
cell_box.backgroundColor = new Color('#ff837a', 0.6)
cell.addSpacer(10)
const cell_text = cell.addText(d['title'])
cell_text.font = Font.lightSystemFont(16)
cell.url = this.actionUrl("open-url", d['url'])
cell.addSpacer()
w.addSpacer(10)
})
w.addSpacer()
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge(data) {
return await this.renderMedium(data, 10)
}
/**
* 获取数据函数函数名可不固定
*/
async getData() {
const api = 'https://x.im3x.cn/v1/test-api.json'
return await this.httpGet(api, true, false)
}
/**
* 自定义注册点击事件 actionUrl 生成一个触发链接点击后会执行下方对应的 action
* @param {string} url 打开的链接
*/
async actionOpenUrl(url) {
Safari.openInApp(url, false)
}
}
// @组件代码结束
const {
Testing
} = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,154 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: orange; icon-glyph: comments;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
constructor (arg) {
super(arg)
this.name = '微博热榜'
this.desc = '实时刷新微博热搜榜事件'
// 注册设置
this.registerAction('插件设置', this.actionSetting.bind(this))
}
async render () {
if (this.widgetFamily === 'medium') {
return await this.renderMedium()
} else if (this.widgetFamily === 'large') {
return await this.renderLarge()
} else {
return await this.renderSmall()
}
}
/**
* 渲染小尺寸组件
*/
async renderSmall () {
let res = await this.httpGet('https://m.weibo.cn/api/container/getIndex?containerid=106003%26filter_type%3Drealtimehot')
let data = res['data']['cards'][0]['card_group']
// 去除第一条
data.shift()
let topic = data[0]
console.log(topic)
// 显示数据
let w = new ListWidget()
w = await this.renderHeader(w, 'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2225458401,2104443747&fm=26&gp=0.jpg', '微博热搜')
let body = w.addStack()
let txt = body.addText(topic['desc'])
body.addSpacer()
txt.leftAlignText()
txt.font = Font.lightSystemFont(14)
w.addSpacer()
let footer = w.addStack()
footer.centerAlignContent()
let img = footer.addImage(await this.getImageByUrl(topic['pic']))
img.imageSize = new Size(18, 18)
footer.addSpacer(5)
if (topic['icon']) {
let hot = footer.addImage(await this.getImageByUrl(topic['icon']))
hot.imageSize = new Size(18, 18)
footer.addSpacer(5)
}
let num = footer.addText(String(topic['desc_extr']))
num.font = Font.lightSystemFont(10)
num.textOpacity = 0.5
w.url = this.actionUrl('open-url', topic['scheme'])
return w
}
/**
* 渲染中尺寸组件
*/
async renderMedium (count = 4) {
let res = await this.httpGet('https://m.weibo.cn/api/container/getIndex?containerid=106003%26filter_type%3Drealtimehot')
let data = res['data']['cards'][0]['card_group']
// 去除第一条
data.shift()
// 显示数据
let w = new ListWidget()
w = await this.renderHeader(w, 'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2225458401,2104443747&fm=26&gp=0.jpg', '微博热搜')
// 布局:一行一个,左边顺序排序,中间标题,后边热/新
const body = w.addStack()
const bodyLeft = body.addStack()
bodyLeft.layoutVertically()
for (let i = 0; i < count; i ++) {
let topic = data[i];
let dom = bodyLeft.addStack()
dom.centerAlignContent()
let pic = dom.addImage(await this.getImageByUrl(topic['pic']))
pic.imageSize = new Size(18, 18)
dom.addSpacer(5)
let title = dom.addText(topic['desc'])
title.lineLimit = 1
title.font = Font.lightSystemFont(14)
dom.addSpacer(5)
if (topic['icon']) {
let iconDom = dom.addStack()
let icon = iconDom.addImage(await this.getImageByUrl(topic['icon']))
icon.imageSize = new Size(18, 18)
}
dom.addSpacer()
let extr = dom.addText(String(topic['desc_extr']))
extr.font = Font.lightSystemFont(12)
extr.textOpacity = 0.6
dom.url = this.actionUrl('open-url', topic['scheme'])
bodyLeft.addSpacer(5)
}
body.addSpacer()
w.url = this.actionUrl("setting")
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge () {
return await this.renderMedium(11)
}
async actionSetting () {
const settings = this.getSettings()
const arg = settings["type"] || "1"
let a = new Alert()
a.title="打开方式"
a.message="点击小组件浏览热点的方式"
a.addAction((arg==="0"?"✅ ":"")+"微博客户端")
a.addAction((arg==="1"?"✅ ":"")+"自带浏览器")
a.addCancelAction("取消设置")
let i = await a.presentSheet()
if (i===-1) return
this.settings["type"] = String(i)
this.saveSettings()
}
async actionOpenUrl (url) {
const settings = this.getSettings()
if (settings['type']==="1"){
Safari.openInApp(url, false)
} else {
let k = decodeURIComponent(url).split('q=')[1].split('&')[0]
Safari.open('sinaweibo://searchall?q=' + encodeURIComponent(k))
}
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,138 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: fire;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
/**
* 传递给组件的参数可以是桌面 Parameter 数据也可以是外部如 URLScheme 等传递的数据
* @param {string} arg 自定义参数
*/
constructor (arg) {
super(arg)
this.name = '百度热榜'
this.logo = 'https://www.baidu.com/cache/icon/favicon.ico'
this.desc = '百度搜索风云榜,实时更新网络热点'
}
/**
* 渲染函数函数名固定
* 可以根据 this.widgetFamily 来判断小组件尺寸以返回不同大小的内容
*/
async render () {
const data = await this.getData()
switch (this.widgetFamily) {
case 'large':
return await this.renderLarge(data)
case 'medium':
return await this.renderMedium(data)
default:
return await this.renderSmall(data)
}
}
/**
* 渲染小尺寸组件
*/
async renderSmall (data) {
let w = new ListWidget()
await this.renderHeader(w, this.logo, this.name)
const t = w.addText(data['hotsearch'][0]['pure_title'])
t.font = Font.lightSystemFont(16)
w.addSpacer()
w.url = this.actionUrl('open-url', decodeURIComponent(data['hotsearch'][0]['linkurl']))
return w
}
/**
* 渲染中尺寸组件
*/
async renderMedium (data, num = 4) {
let w = new ListWidget()
await this.renderHeader(w, this.logo, this.name)
data['hotsearch'].slice(0, num).map((d, i) => {
const cell = w.addStack()
cell.centerAlignContent()
const idx = cell.addText(String(i+1))
idx.font = Font.boldSystemFont(14)
if (i === 0) {
idx.textColor = new Color('#fe2d46', 1)
} else if (i === 1) {
idx.textColor = new Color('#ff6600', 1)
} else if (i === 2) {
idx.textColor = new Color('#faa90e', 1)
} else {
idx.textColor = new Color('#9195a3', 1)
}
cell.addSpacer(10)
let _title = d['pure_title']
_title = _title.replace(/&quot;/g, '"')
const cell_text = cell.addText(_title)
cell_text.font = Font.lightSystemFont(14)
cell_text.lineLimit = 1
let _url = decodeURIComponent(d['linkurl'])
_url = _url.replace("://www.", "://m.")
cell.url = this.actionUrl("open-url", _url)
cell.addSpacer()
w.addSpacer()
})
// w.addSpacer()
// let lbg = new LinearGradient()
// lbg.locations = [0, 1]
// lbg.colors = [
// Color.dynamic(new Color('#cfd9df', 1), new Color('#09203f', 1)),
// Color.dynamic(new Color('#e2ebf0', 1), new Color('#537895', 1))
// ]
// w.backgroundGradient = lbg
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge (data) {
return await this.renderMedium(data, 11)
}
/**
* 获取数据函数函数名可不固定
*/
async getData () {
const req = new Request("https://www.baidu.com/")
req.method = "GET"
req.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.55"
}
const res = await req.loadString()
// console.log(res)
const tmp = res.split(`<textarea id="hotsearch_data" style="display:none;">`)[1].split(`</textarea>`)[0]
console.log(tmp)
const data = eval(`(${tmp})`)
console.log(data)
// const data = JSON.parse(tmp)
// console.log(data['hotsearch'].length)
return data
}
/**
* 自定义注册点击事件 actionUrl 生成一个触发链接点击后会执行下方对应的 action
* @param {string} url 打开的链接
*/
async actionOpenUrl (url) {
Safari.openInApp(url, false)
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

View File

@ -0,0 +1,120 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: orange; icon-glyph: comments;
//
// iOS 桌面组件脚本 @「小件件」
// 开发说明:请从 Widget 类开始编写,注释请勿修改
// https://x.im3x.cn
//
// 添加require是为了vscode中可以正确引入包以获得自动补全等功能
if (typeof require === 'undefined') require = importModule
const { Base } = require("./「小件件」开发环境")
// @组件代码开始
class Widget extends Base {
url = 'https://www.youxi369.com/news/2254_4.html'
/**
* 传递给组件的参数可以是桌面 Parameter 数据也可以是外部如 URLScheme 等传递的数据
* @param {string} arg 自定义参数
*/
constructor (arg) {
super(arg)
this.name = '蚂蚁庄园'
this.desc = '今天的题会做么?'
}
/**
* 渲染函数函数名固定
* 可以根据 this.widgetFamily 来判断小组件尺寸以返回不同大小的内容
*/
async render () {
const data = await this.getData()
switch (this.widgetFamily) {
case 'large':
return await this.renderLarge(data)
case 'medium':
return await this.renderMedium(data)
default:
return await this.renderMedium(data)
}
}
/**
* 渲染中尺寸组件
*/
async renderMedium (data, num = 4, title = false) {
let w = new ListWidget()
// await this.renderHeader(w, data['logo'], data['title'])
data.slice(0, num * 2).map((d, idx) => {
if (!title && idx % 2 === 0) return;
const cell = w.addStack()
cell.centerAlignContent()
const cell_text = cell.addText(d)
cell_text.font = Font.lightSystemFont(16)
cell.addSpacer()
w.addSpacer(10)
})
w.url = this.actionUrl(this.url)
w.addSpacer()
return w
}
/**
* 渲染大尺寸组件
*/
async renderLarge (data) {
return await this.renderMedium(data, 5, true)
}
/**
* 获取数据函数函数名可不固定
*/
async getData () {
const html = await this.fetchAPI(this.url, false);
const tmp = html.split(`<p style="display:none">mryt</p>`)[1].split(`<div class="more-strategy"><i></i></div>`)[0];
const arr = tmp.split(`</span></p>`).slice(0, 20);
const result = [];
for (const answer of arr) {
// console.log(`====answer==`, answer);
if (!answer) continue;
const text = answer.replace(/<p>/, '').replace(/\&nbsp;/gi, ' ').replace(/<span style="[^"]+">/gi, '').replace(/小鸡宝宝考考你[,]?/, '').trim();
const [ title, an ] = text.split('答案:');
if (!title) continue;
const [ day, t] = title.split('月')[1].split('');
result.push(t);
result.push(`答案:${an} (${day})`);
}
return result;
}
// http.get
async fetchAPI (api, json = true) {
let data = null
try {
let req = new Request(api)
req.headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/85.0.4183.102'
}
data = await (json ? req.loadJSON() : req.loadString())
} catch (e) {}
// 判断数据是否为空(加载失败)
if (!data) {
return null
}
return data
}
/**
* 自定义注册点击事件 actionUrl 生成一个触发链接点击后会执行下方对应的 action
* @param {string} url 打开的链接
*/
async actionOpenUrl (url) {
Safari.openInApp(url, false)
}
}
// @组件代码结束
const { Testing } = require("./「小件件」开发环境")
await Testing(Widget)

138
app.js Normal file
View File

@ -0,0 +1,138 @@
const fs = require('fs')
const os = require('os')
const path = require('path')
const express = require('express')
const child_process = require('child_process')
const multer = require('multer')
const bodyParser = require('body-parser')
const chalk = require('chalk')
///////////////////////// Config配置
const HTTP_PORT = 5566
const WORK_DIR = path.dirname(__filename)
const SCRIPTS_DIR = path.join(WORK_DIR, "Scripts")
/////////////////////////
const app = express()
const upload = multer({
dest: os.tmpdir()
})
app.use(upload.any())
app.use(bodyParser.urlencoded({
extended: false
}))
app.use(bodyParser.json())
/// 模版渲染
app.get("/", (req, res) => {
let html = fs.readFileSync(path.join(WORK_DIR, "template/guide.html")).toString()
let js = fs.readFileSync(path.join(WORK_DIR, "install-runtime.js")).toString()
html = html.replace("@@code@@", js)
res.send(html)
})
app.get('/ping', (req, res) => {
console.log('[-] ping..')
setTimeout(() => {
res.send("pong").end()
}, 1000)
})
let FILE_DATE = null
app.get('/sync', (req, res) => {
// console.log('[-] 等待同步到手机..')
const {
name
} = req.query
const WIDGET_FILE = path.join(SCRIPTS_DIR, name + '.js')
if (!fs.existsSync(WIDGET_FILE)) return res.send("nofile").end()
setTimeout(() => {
// 判断文件时间
const _time = fs.statSync(WIDGET_FILE).mtimeMs
if (_time === FILE_DATE) {
res.send("no").end()
return
// return console.log("[!] 文件没有更改,不同步")
}
// 同步
res.sendFile(WIDGET_FILE)
console.log('[+] 同步到手机完毕')
FILE_DATE = _time
}, 1000)
})
app.post("/sync", (req, res) => {
if (req.files.length !== 1) return res.send("no")
console.log('[+] Scriptalbe App 已连接')
const _file = req.files[0]
const FILE_NAME = _file['originalname'] + '.js'
const WIDGET_FILE = path.join(SCRIPTS_DIR, FILE_NAME)
fs.renameSync(_file['path'], WIDGET_FILE)
res.send("ok")
console.log(`[*] 小组件源码(${_file['originalname']})已同步,请打开编辑`)
FILE_DATE = fs.statSync(WIDGET_FILE).mtimeMs
// 尝试打开
let cmd = `code "${WIDGET_FILE}"`
if (os.platform() === "win32") {
cmd = `cmd.exe /c ${cmd}`
} else if (os.platform() === "linux") {
let shell = process.env["SHELL"]
cmd = `${shell} -c ${cmd}`
} else {
cmd = `"/Applications/Visual Studio Code.app/Contents/MacOS/Electron" "${WIDGET_FILE}"`
}
child_process.execSync(cmd)
})
// 远程 console调试中把调试输出内容传送到服务端控制台输出
app.post('/console', (req, res) => {
const {
t,
data
} = req.body
const _time = new Date().toLocaleString().split(' ')[1]
switch (t) {
case 'warn':
console.warn(`[console.warn / ${_time}]`, typeof data === 'string' ? data : '')
if (typeof data === 'object') console.warn(data)
break
case 'error':
console.error(`[console.error / ${_time}]`, typeof data === 'string' ? data : '')
if (typeof data === 'object') console.error(data)
break
default:
console.log(`[console.log / ${_time}]`, typeof data === 'string' ? data : '')
if (typeof data === 'object') console.log(data)
}
res.send("ok")
})
// 获取PCIP地址
function getIPAdress() {
var interfaces = os.networkInterfaces();
for (var devName in interfaces) {
var iface = interfaces[devName];
for (var i = 0; i < iface.length; i++) {
var alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
}
function main() {
const _ip = getIPAdress()
const _host = `http://${_ip}:${HTTP_PORT}`
console.log(chalk.blue('[*] 「小件件」开发服务运行中'))
console.log(chalk.blue(`[-] 地址:${_host}`))
console.log(chalk.blue(`[-] 如果你的手机还没有配置开发环境,请手机 Safari 访问上述地址,查看引导`))
console.log(chalk.blue('[+] 如果你的手机已经安装好环境和小组件模板,请在 Scriptable 里点击小组件模板->远程开发,服务器地址输入:', _ip))
app.listen(HTTP_PORT)
}
main();

46
encode.js Normal file
View File

@ -0,0 +1,46 @@
// 加密压缩打包后的小组件代码
// 方便隐藏敏感信息,减少组件体积和保护小组件不被随意修改
// 用法:
// node encode.js Dist/你的小组件.js
const process = require('process')
const os = require('os')
const fs = require('fs')
const path = require('path')
var JB = require('javascript-obfuscator');
if (process.argv.length !== 3) {
console.log('[!] 用法node encode.js Dist/「小件件」xxx.js')
process.exit(0)
}
const file_name = process.argv[2]
const out_name = file_name.replace(".js", ".enc.js")
// 读取源文件
const widget_file = fs.readFileSync(path.join(__dirname, file_name))
let widget_code = widget_file.toString("utf-8")
widget_code = widget_code.split("await Running(Widget)")[0];
var result = JB.obfuscate(widget_code.toString("utf-8"), {
"rotateStringArray": true,
"selfDefending": true,
"stringArray": true,
splitStringsChunkLength: 100,
"stringArrayEncoding": ["rc4", "base64"]
}).getObfuscatedCode()
let result_header = widget_code.split("// icon-color:")[0]
result_header += "// icon-color:"
result_header += widget_code.split("// icon-color:")[1].split("\n")[0]
result_header += "\n// " + file_name
result_header += "\n// https://github.com/im3x/Scriptables"
let result_code = `${result_header}\n${result};await Running(Widget);`
fs.writeFileSync(path.join(__dirname, out_name), result_code);
console.log("[*] 文件已压缩混淆至:", out_name)

8
install-runtime.js Normal file
View File

@ -0,0 +1,8 @@
const FILE_MGR = FileManager[module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']();
await Promise.all(['「小件件」开发环境.js', '「源码」小组件示例.js'].map(async js => {
const REQ = new Request(`https://gitee.com/im3x/Scriptables/raw/v2-dev/Scripts/${encodeURIComponent(js)}`);
const RES = await REQ.load();
FILE_MGR.write(FILE_MGR.joinPath(FILE_MGR.documentsDirectory(), js), RES);
}));
FILE_MGR.remove(module.filename);
Safari.open("scriptable:///open?scriptName="+encodeURIComponent('「源码」小组件示例'));

9
jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"checkJs": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

45
pack.js Normal file
View File

@ -0,0 +1,45 @@
/**
* 打包成单独小组件
* 用法
* node pack.js Scripts/源码小组件示例.js
* 将会在`Dist`目录生成小件件小组件示例.js 文件这个文件可以发送给用户单独使用
*/
const process = require('process')
const os = require('os')
const fs = require('fs')
const path = require('path')
if (process.argv.length !== 3) {
console.log('[!] 用法node pack Scripts/「源码」xxx.js')
process.exit(0)
}
const SAVE_PATH = path.join(__dirname, "Dist")
const file_name = process.argv[2]
const out_name = file_name.replace("「源码」", "「小件件」").replace("Scripts", "Dist")
// 创建目录
if (!fs.existsSync(SAVE_PATH)) {
fs.mkdirSync(SAVE_PATH)
}
// 组合文件
const runtime_file = fs.readFileSync(path.join(__dirname, "Scripts", "「小件件」开发环境.js"))
const runtime_code = runtime_file.toString("utf-8").split("// @running.end")[0]
const widget_file = fs.readFileSync(path.join(__dirname, file_name))
const widget_code = widget_file.toString("utf-8");
const widget_class = widget_code.split("// @组件代码开始")[1].split("// @组件代码结束")[0]
const widget_header = widget_code.split('// icon-color:')[1].split('\n')[0];
let result_code = `// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color:${widget_header}
${runtime_code}
${widget_class}
await Running(Widget)`
// 写入文件
fs.writeFileSync(path.join(__dirname, out_name), result_code)
console.log('[*] 文件已经保存到:' + out_name)

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "t-iosapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "node app"
},
"author": "taolin",
"license": "ISC",
"dependencies": {
"@types/scriptable-ios": "^1.6.1",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"javascript-obfuscator": "^2.9.4",
"multer": "^1.4.2",
"chalk": "4.1.2"
}
}

62
template/guide.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发环境指导</title>
</head>
<body>
<h1>开发环境安装</h1>
<button id="copy" onclick="copy()">👇 1.点击复制下方代码</button>
<textarea id="js" readonly>@@code@@</textarea>
<p>打开 Scriptable点击 ➕,粘贴,运行 ▶️</p>
<a id="open" href="scriptable:///add?scriptName=hello">👉 2. 点击打开 Scriptable</a>
</body>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#copy,
#open {
width: 200px;
height: 50px;
background-color: green;
color: #FFF;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 14px;
}
#open {
background-color: darkred;
}
#js {
width: 200px;
height: 100px;
margin: 20px;
z-index: -1;
}
</style>
<script>
var copyOK = 0;
function copy() {
document.getElementById("js").select();
document.execCommand("Copy");
document.getElementById("copy").innerText = "复制成功!" + (copyOK > 0 ? '+' + copyOK : '')
copyOK++;
document.getElementById("open").focus()
}
</script>
</html>

1274
yarn.lock Normal file

File diff suppressed because it is too large Load Diff