372 lines
10 KiB
JavaScript
372 lines
10 KiB
JavaScript
|
// 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)
|