[fix]页面创建
This commit is contained in:
commit
efe5165c9e
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
Widget.js
|
||||
.DS_File
|
||||
Dist/
|
42
README.md
Normal file
42
README.md
Normal 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`
|
||||
|
||||
|
||||
|
1092
Scripts/「小件件」开发环境.js
Normal file
1092
Scripts/「小件件」开发环境.js
Normal file
File diff suppressed because one or more lines are too long
372
Scripts/「源码」V2EX 社区.js
Normal file
372
Scripts/「源码」V2EX 社区.js
Normal 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)
|
135
Scripts/「源码」iOS限免.js
Normal file
135
Scripts/「源码」iOS限免.js
Normal 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)
|
163
Scripts/「源码」一言.js
Normal file
163
Scripts/「源码」一言.js
Normal 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)
|
244
Scripts/「源码」京东白条数据.js
Normal file
244
Scripts/「源码」京东白条数据.js
Normal 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)
|
342
Scripts/「源码」人生电量.js
Normal file
342
Scripts/「源码」人生电量.js
Normal 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 () {
|
||||
// 电量
|
||||
// 男:75,女:78(预计寿命
|
||||
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)
|
106
Scripts/「源码」小组件示例.js
Normal file
106
Scripts/「源码」小组件示例.js
Normal 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)
|
154
Scripts/「源码」微博热搜.js
Normal file
154
Scripts/「源码」微博热搜.js
Normal 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)
|
138
Scripts/「源码」百度热榜.js
Normal file
138
Scripts/「源码」百度热榜.js
Normal 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(/"/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)
|
120
Scripts/「源码」蚂蚁庄园.js
Normal file
120
Scripts/「源码」蚂蚁庄园.js
Normal 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(/\ /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
138
app.js
Normal 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
46
encode.js
Normal 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
8
install-runtime.js
Normal 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
9
jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
45
pack.js
Normal file
45
pack.js
Normal 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
19
package.json
Normal 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
62
template/guide.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user