2 Star 2 Fork 3

胡春东 CTO/QjlRAPV06FFj

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
MyBmwEncryption.js 53.01 KB
一键复制 编辑 原始数据 按行查看 历史
胡春东 CTO 提交于 2022-05-07 21:05 +08:00 . 优化提示
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: light-brown; icon-glyph: magic;
///<reference path="../index.d.ts" />
const VERSION = '202205072125'
const DEV = (Device.screenSize().width === 1920)
const AMAP_KEY = ''
const MyLog = (content) => {
console.log(content)
}
const JSEncrypt = importModule(
DEV ? 'modules/BmwEncrypt' : 'modules/_BmwEncrypt')
let defaultData = {
username: '', // 手机号码不带86
password: '', // 密码
invite_code: '',// 邀请码
custom_name: '',
custom_car_image: '',
vin: '',
darkBackground: '',//'#404040,#ffffff'
lightBackground: '',//'#404040,#ffffff'
darkFgColor: '', // 深色前景色 '#404040,#ffffff'
lightFgColor: '', // 浅色前景色 '#404040,#ffffff'
oilTotal: '',
}
const DeviceSize = {
'428x926': { small: [170, 170], medium: [360, 170], large: [364, 382] },
'414x896': { small: [169, 169], medium: [360, 169], large: [360, 379] },
'414x736': { small: [159, 159], medium: [348, 159], large: [348, 357] },
'390x844': { small: [158, 158], medium: [338, 158], large: [338, 354] },
'375x812': { small: [155, 155], medium: [329, 155], large: [329, 345] },
'375x667': { small: [148, 148], medium: [321, 148], large: [321, 324] },
'360x780': { small: [155, 155], medium: [329, 155], large: [329, 345] },
'320x568': { small: [141, 141], medium: [292, 141], large: [292, 311] },
}
const MY_BMW_REFRESH_TOKEN = 'HUCD_MY_BMW_REFRESH_TOKEN'
const MY_BMW_TOKEN = 'HUCD_MY_BMW_TOKEN'
const MY_BMW_UPDATE_AT = 'HUCD_MY_BMW_UPDATE_AT'
const MY_BMW_CHECKIN_AT = 'HUCD_MY_BMW_CHECKIN_AT'
const MY_BMW_AGREE = 'HUCD_MY_BMW_AGREE'
// js 工具类统一放这里
const Utils = {
async notify (title, body, url, opts = {}) {
let n = new Notification()
n = Object.assign(n, opts)
n.title = title
n.body = body
if (url) n.openURL = url
return await n.schedule()
},
/**
* md5 加密字符串
* @param {string} str 要加密成md5的数据
*/
md5 (str) {
function d (n, t) {
var r = (65535 & n) + (65535 & t)
return (n >> 16) + (t >> 16) + (r >> 16) << 16 | 65535 & r
}
function f (n, t, r, e, o, u) {
return d((c = d(d(t, n), d(e, u))) << (f = o) | c >>> 32 - f, r)
var c, f
}
function l (n, t, r, e, o, u, c) { return f(t & r | ~t & e, n, t, o, u, c) }
function v (n, t, r, e, o, u, c) { return f(t & e | r & ~e, n, t, o, u, c) }
function g (n, t, r, e, o, u, c) { return f(t ^ r ^ e, n, t, o, u, c) }
function m (n, t, r, e, o, u, c) { return f(r ^ (t | ~e), n, t, o, u, c) }
function i (n, t) {
var r, e, o, u
n[t >> 5] |= 128 << t % 32, n[14 + (t + 64 >>> 9 << 4)] = t
for (var c = 1732584193, f = -271733879, i = -1732584194, a = 271733878, h = 0; h <
n.length; h += 16) c = l(r = c, e = f, o = i, u = a, n[h], 7,
-680876936), a = l(a, c, f, i, n[h + 1], 12, -389564586), i = l(i, a, c,
f, n[h + 2], 17, 606105819), f = l(f, i, a, c, n[h + 3], 22,
-1044525330), c = l(c, f, i, a, n[h + 4], 7, -176418897), a = l(a, c, f,
i, n[h + 5], 12, 1200080426), i = l(i, a, c, f, n[h + 6], 17,
-1473231341), f = l(f, i, a, c, n[h + 7], 22, -45705983), c = l(c, f, i,
a, n[h + 8], 7, 1770035416), a = l(a, c, f, i, n[h + 9], 12,
-1958414417), i = l(i, a, c, f, n[h + 10], 17, -42063), f = l(f, i, a,
c, n[h + 11], 22, -1990404162), c = l(c, f, i, a, n[h + 12], 7,
1804603682), a = l(a, c, f, i, n[h + 13], 12, -40341101), i = l(i, a, c,
f, n[h + 14], 17, -1502002290), c = v(c,
f = l(f, i, a, c, n[h + 15], 22, 1236535329), i, a, n[h + 1], 5,
-165796510), a = v(a, c, f, i, n[h + 6], 9, -1069501632), i = v(i, a, c,
f, n[h + 11], 14, 643717713), f = v(f, i, a, c, n[h], 20,
-373897302), c = v(c, f, i, a, n[h + 5], 5, -701558691), a = v(a, c, f,
i, n[h + 10], 9, 38016083), i = v(i, a, c, f, n[h + 15], 14,
-660478335), f = v(f, i, a, c, n[h + 4], 20, -405537848), c = v(c, f, i,
a, n[h + 9], 5, 568446438), a = v(a, c, f, i, n[h + 14], 9,
-1019803690), i = v(i, a, c, f, n[h + 3], 14, -187363961), f = v(f, i,
a, c, n[h + 8], 20, 1163531501), c = v(c, f, i, a, n[h + 13], 5,
-1444681467), a = v(a, c, f, i, n[h + 2], 9, -51403784), i = v(i, a, c,
f, n[h + 7], 14, 1735328473), c = g(c,
f = v(f, i, a, c, n[h + 12], 20, -1926607734), i, a, n[h + 5], 4,
-378558), a = g(a, c, f, i, n[h + 8], 11, -2022574463), i = g(i, a, c,
f, n[h + 11], 16, 1839030562), f = g(f, i, a, c, n[h + 14], 23,
-35309556), c = g(c, f, i, a, n[h + 1], 4, -1530992060), a = g(a, c, f,
i, n[h + 4], 11, 1272893353), i = g(i, a, c, f, n[h + 7], 16,
-155497632), f = g(f, i, a, c, n[h + 10], 23, -1094730640), c = g(c, f,
i, a, n[h + 13], 4, 681279174), a = g(a, c, f, i, n[h], 11,
-358537222), i = g(i, a, c, f, n[h + 3], 16, -722521979), f = g(f, i, a,
c, n[h + 6], 23, 76029189), c = g(c, f, i, a, n[h + 9], 4,
-640364487), a = g(a, c, f, i, n[h + 12], 11, -421815835), i = g(i, a,
c, f, n[h + 15], 16, 530742520), c = m(c,
f = g(f, i, a, c, n[h + 2], 23, -995338651), i, a, n[h], 6,
-198630844), a = m(a, c, f, i, n[h + 7], 10, 1126891415), i = m(i, a, c,
f, n[h + 14], 15, -1416354905), f = m(f, i, a, c, n[h + 5], 21,
-57434055), c = m(c, f, i, a, n[h + 12], 6, 1700485571), a = m(a, c, f,
i, n[h + 3], 10, -1894986606), i = m(i, a, c, f, n[h + 10], 15,
-1051523), f = m(f, i, a, c, n[h + 1], 21, -2054922799), c = m(c, f, i,
a, n[h + 8], 6, 1873313359), a = m(a, c, f, i, n[h + 15], 10,
-30611744), i = m(i, a, c, f, n[h + 6], 15, -1560198380), f = m(f, i, a,
c, n[h + 13], 21, 1309151649), c = m(c, f, i, a, n[h + 4], 6,
-145523070), a = m(a, c, f, i, n[h + 11], 10, -1120210379), i = m(i, a,
c, f, n[h + 2], 15, 718787259), f = m(f, i, a, c, n[h + 9], 21,
-343485551), c = d(c, r), f = d(f, e), i = d(i, o), a = d(a, u)
return [c, f, i, a]
}
function a (n) {
for (var t = '', r = 32 * n.length, e = 0; e <
r; e += 8) t += String.fromCharCode(n[e >> 5] >>> e % 32 & 255)
return t
}
function h (n) {
var t = []
for (t[(n.length >> 2) - 1] = void 0, e = 0; e <
t.length; e += 1) t[e] = 0
for (var r = 8 * n.length, e = 0; e < r; e += 8) t[e >> 5] |= (255 &
n.charCodeAt(e / 8)) << e % 32
return t
}
function e (n) {
for (var t, r = '0123456789abcdef', e = '', o = 0; o <
n.length; o += 1) t = n.charCodeAt(o), e += r.charAt(t >>> 4 & 15) +
r.charAt(15 & t)
return e
}
function r (n) { return unescape(encodeURIComponent(n)) }
function o (n) {
return a(i(h(t = r(n)), 8 * t.length))
var t
}
function u (n, t) {
return function (n, t) {
var r, e, o = h(n), u = [], c = []
for (u[15] = c[15] = void 0, 16 < o.length &&
(o = i(o, 8 * n.length)), r = 0; r < 16; r += 1) u[r] = 909522486 ^
o[r], c[r] = 1549556828 ^ o[r]
return e = i(u.concat(h(t)), 512 + 8 * t.length), a(i(c.concat(e), 640))
}(r(n), r(t))
}
function t (n, t, r) {
return t ? r ? u(t, n) : e(u(t, n)) : r ? o(n) : e(o(n))
}
return t(str)
},
/**
* 获取远程图片内容
* @param {string} url 图片地址
* @param {bool} useCache 是否使用缓存(请求失败时获取本地缓存)
*/
async getImageByUrl (url, useCache = true) {
const cacheKey = Utils.md5(url)
const cacheFile = FileManager.local().
joinPath(FileManager.local().temporaryDirectory(), cacheKey)
// 判断是否有缓存
if (useCache && FileManager.local().fileExists(cacheFile)) {
return Image.fromFile(cacheFile)
}
try {
const req = new Request(url)
const img = await req.loadImage()
// 存储到缓存
FileManager.local().writeImage(cacheFile, img)
return img
} catch (e) {
// 没有缓存+失败情况下,返回自定义的绘制图片(红色背景)
let ctx = new DrawContext()
ctx.size = new Size(100, 100)
ctx.setFillColor(Color.red())
ctx.fillRect(new Rect(0, 0, 100, 100))
return await ctx.getImage()
}
},
timeFormat (fmt, ts = null) {
const date = ts ? new Date(ts) : new Date()
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
'S': date.getMilliseconds(),
}
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length))
for (let k in o)
if (new RegExp('(' + k + ')').test(fmt))
fmt = fmt.replace(RegExp.$1,
RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(
('' + o[k]).length))
return fmt
},
}
// 接口相关类全部放这里
const BMWApi = {
getOilPercent (data) {
if (data.status.fuelIndicators.length === 1 &&
data.status.fuelIndicators[0].showsBar) {
return data.status.fuelIndicators[0].mainBarValue
} else {
let current = data.status.fuelIndicators[0].levelValue
let all = defaultData.oilTotal === '' ? 65 : parseInt(
defaultData.oilTotal)
if (defaultData.oilTotal === '') {
console.log('非id7车辆需要设置油箱容积,默认容积65L')
}
return parseInt(current * 100 / all)
}
},
async getNonce () {
const req = new Request(
'http://yymm.huchundong.com:7000/bimmer/getNonce')
req.headers = {
'Content-Type': 'application/json',
}
MyLog('[0+]获取随机秘钥')
let p = {
'mobile': defaultData.username.length === 13
? defaultData.username
: '86' + defaultData.username,
'verify': 'BMW-LINKER偷的一手好代码',
}
req.method = 'POST'
req.body = JSON.stringify(p)
const res = await req.loadJSON()
if (res.code === 200) {
console.log(res)
return res.data
} else {
console.log(res)
MyLog('[0-]获取随机秘钥失败')
App.error = res.message ? res.message : '检查账号配置'
return null
}
},
async getPublicKey () {
const req = new Request(
'https://myprofile.bmw.com.cn/eadrax-coas/v1/cop/publickey')
req.headers = {
'user-agent': 'Dart/2.13 (dart:io)',
'x-user-agent': 'ios(15.4.1);bmw;2.3.0(13603)',
'accept-language': 'zh_CN',
'host': 'myprofile.bmw.com.cn',
'x-cluster-use-mock': 'never',
'24-hour-format': 'true',
}
MyLog('[0+]获取加密公钥')
const res = await req.loadJSON()
if (res.code === 200) {
let pk = res.data.value.split('\r\n')[1]
return pk
} else {
MyLog('[0-]加密公钥获取失败')
return null
}
},
async getAccessToken (force) {
if (force === undefined) {
force = false
}
let accessToken = ''
if (Keychain.contains(MY_BMW_UPDATE_AT)) {
let lastUpdate = parseInt(Keychain.get(MY_BMW_UPDATE_AT))
MyLog(Utils.timeFormat('yyyy-MM-dd HH:mm:ss', lastUpdate))
if (!force && lastUpdate > ((new Date).valueOf() - 1000 * 60 * 30)) {
console.warn('[-] token有效')
if (Keychain.contains(MY_BMW_TOKEN)) {
accessToken = Keychain.get(MY_BMW_TOKEN)
}
} else {
if (Keychain.contains(MY_BMW_REFRESH_TOKEN)) {
let refresh_token = Keychain.get(MY_BMW_REFRESH_TOKEN)
accessToken = await this.refreshToken(refresh_token)
}
}
}
if (accessToken === '' || DEV) {
MyLog('token 无效或不存在,重新获取token')
const loginRes = await this.myLogin()
if (loginRes !== null) {
const { access_token, refresh_token, token_type } = loginRes
accessToken = access_token
Keychain.set(MY_BMW_UPDATE_AT, String((new Date).valueOf()))
Keychain.set(MY_BMW_TOKEN, accessToken)
Keychain.set(MY_BMW_REFRESH_TOKEN, refresh_token)
} else {
accessToken = ''
}
}
return accessToken
},
async refreshToken (refresh_token) {
MyLog('token 过期,但是有刷新token')
const req = new Request(
'https://myprofile.bmw.com.cn/eadrax-coas/v1/oauth/token')
req.headers = {
'user-agent': 'Dart/2.13 (dart:io)',
'x-user-agent': 'ios(15.4.1);bmw;2.3.0(13603)',
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJhYzRkZTdmYi00ZTg3LTQ3MjctOTNkNS1jMDY1MzMwYmI3ZTgkMSRBJDE2Mjc0ODA0NTExNjciLCJuYmYiOjE2Mjc0ODA0NTEsImV4cCI6MTYyNzQ4Mzc1MSwiaWF0IjoxNjI3NDgwNDUxfQ.bX_KZbSzVYnM9ht8S9Cu__Kawg6XsEcpn-qA7YRi4GA',
'content-type': 'application/json; charset=utf-8',
'accept-language': 'zh-CN',
'host': 'myprofile.bmw.com.cn',
'x-cluster-use-mock': 'never',
'24-hour-format': 'true',
}
req.method = 'POST'
req.body = `grant_type=refresh_token&refresh_token=${refresh_token}`
const res = await req.loadJSON()
if (res.access_token !== undefined) {
const { access_token, refresh_token } = res
Keychain.set(MY_BMW_UPDATE_AT, String((new Date).valueOf()))
Keychain.set(MY_BMW_TOKEN, access_token)
Keychain.set(MY_BMW_REFRESH_TOKEN, refresh_token)
return access_token
} else {
return ''
}
},
async myLogin () {
MyLog('获取登录凭证')
let pk = await this.getPublicKey()
console.log('获取nonce')
let nonce = await this.getNonce()
if (nonce === null) {
return null
}
console.log('计算密码' + pk)
const encrypt = new JSEncrypt.JSEncrypt()
let encryptPass = ''
encrypt.setPublicKey(pk)
encryptPass = encrypt.encrypt(defaultData.password)
console.log('获取actoken')
const req = new Request(
'https://myprofile.bmw.com.cn/eadrax-coas/v2/login/pwd')
req.headers = {
'user-agent': 'Dart/2.13 (dart:io)',
'x-user-agent': 'ios(15.4.1);bmw;2.3.0(13603)',
'content-type': 'application/json; charset=utf-8',
'accept-language': 'zh-CN',
'host': 'myprofile.bmw.com.cn',
'x-cluster-use-mock': 'never',
'24-hour-format': 'true',
'x-login-nonce': nonce,
}
req.method = 'POST'
let p = {
'mobile': '86' + defaultData.username,
'password': encryptPass,
'deviceId': '6D3EB088-09A4-4B7E-A408-12B35275B946',
}
req.body = JSON.stringify(p)
const res = await req.loadJSON()
if (res.code === 200) {
const { access_token, refresh_token, token_type, usid, cid } = res.data
return { access_token, refresh_token, token_type, usid, cid }
} else {
console.log(res)
App.error = res.description
return null
}
},
async getVIN (access_token) {
MyLog('开始获取车辆信息')
const req = new Request(
`https://myprofile.bmw.com.cn/eadrax-vcs/v1/vehicles?apptimezone=480&appDateTime=${(new Date()).valueOf()}`)
req.headers = {
'user-agent': 'Dart/2.13 (dart:io)',
'x-user-agent': 'ios(15.4.1);bmw;2.3.0(13603)',
'authorization': 'Bearer ' + access_token,
'content-type': 'application/json; charset=utf-8',
'accept-language': 'zh-CN',
'host': 'myprofile.bmw.com.cn',
'x-cluster-use-mock': 'never',
'24-hour-format': 'true',
}
const res = await req.loadJSON()
console.log(defaultData.vin)
let vin = defaultData.vin
if (res instanceof Array) {
MyLog('车辆信息获取成功')
console.log(res)
if (vin.length > 0) {
let vinCurrent = vin.toUpperCase()
let findVin = res.findIndex(p => p.vin === vinCurrent)
if (findVin < 0) {
findVin = 0
} else {
console.log('找到目标车辆')
}
return res[findVin]
} else {
return res[0]
}
} else {
MyLog(res)
App.error = res.description
return null
}
},
async checkInDaily (access_token) {
let today = Utils.timeFormat('yyyyMMdd')
let hasCheckIn = false
if (Keychain.contains(MY_BMW_CHECKIN_AT)) {
const lastCheckIn = Keychain.get(MY_BMW_CHECKIN_AT)
if (lastCheckIn === today) {
hasCheckIn = true
MyLog('已经签到过了')
} else {
let hour = new Date().getHours()
if (hour < 8) {
hasCheckIn = true
MyLog('8点后开始签到')
} else if (hour > 8 && hour < 18) {
if (Math.random() > 0.5) {
hasCheckIn = true
MyLog('50%概率本次不签到')
}
}
}
}
if (!hasCheckIn) {
MyLog('开始签到')
const req = new Request(
'https://myprofile.bmw.com.cn/cis/eadrax-community/private-api/v1/mine/check-in')
req.headers = {
'user-agent': 'Dart/2.13 (dart:io)',
'x-user-agent': 'ios(15.4.1);bmw;2.3.0(13603)',
'authorization': 'Bearer ' + access_token,
'content-type': 'application/json; charset=utf-8',
'accept-language': 'zh-CN',
'host': 'myprofile.bmw.com.cn',
'x-cluster-use-mock': 'never',
'24-hour-format': 'true',
}
req.method = 'POST'
req.body = JSON.stringify({ 'signDate': null })
const res = await req.loadJSON()
Keychain.set(MY_BMW_CHECKIN_AT, today)
MyLog(res)
const n = new Notification()
n.title = 'MyBMW签到'
n.body = res.message + (res.businessCode || '')
n.schedule()
// todo 判断真实的签到成功,如果失败,需要提醒一下用户,今天还没有签到,需要人工签到,或者等待下一轮循环,如果是用户的验证信息过期,方便纠错
}
},
async getBMWLogo () {
/*let logoImage = await this.getImageByUrl(
'https://z3.ax1x.com/2021/10/25/54cQGd.png')
return logoImage*/
let logoBase64 = 'data:image/png;base64,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7tvs8/lmf/LJJzc0NDTcVFZWVjZ58mSrtLSU7Ozswe4dbKRa/SwS7VMGvGMlHo9PMlgUcnq9J/n7o8xu5ksQ+DUSD15CpsNgCJE89NBDjB49mt27dxc7jjPT6/X+JCcn55tFRUVZJSUljBo1isxAgOaoxcw1O9nf+qWCSAdCh/0lstQ1QNpLXTKWbGO8H5p+rgPak7Ec9doFCxZgkpqpCOeuCFlqFpHE/E5XVq5cyciRI9X777+f09XVVW7b9vlut7vc4/GM8ft8wYg7K/eZprG9LXFPu4JOZGf6AXIx4gOUcBT004Mb9SSGeTJJ6lqEhke/Bkg0drlc2LadTJP7KcIOO6kLCs899xxer1dt3brVl5WZ6e/Q/swXD50ZP2QHupUUM3oV6DRT2n7BH4cmV+nz+dZEo9GBaXLHWNCLkJ+qENLhbQgrZMiuqwy1GB4hWusrEWLXSGQQ5wO9x9P7uOHZ5AY2QoEfjmwgpgJ1WuuPB8vJP1XgkQG9EmGJjUL4SguBw/0NWr90eWOECHILZBxCL/meUqoB+DAUCumvixGS5vy1wFMG/O8QFmnzQB474H0BY4QuhFlRQN/9HT+wNRQKRU+3EZKi/QKEMZaPUGHuAlLS5dPKP81L8pCIehsSH9YhXMIdQ3F760SAG95wORLtr0L4gauRletQOjqlnYAnBcbZCFN7FLIjW47MtXb46gNkUm4fRMhRVUiB4zOEy/wiEE1Xj0EV45MCzRSEp3sZUoZ/C+Hhvaq1blVKDbkhkoDnIncF5iCBWSHXbZZord9WSg3KGwd9GjF37txEbj+oa3MnCXyga3NPI27f4XK5ePzxxwfV/0ldnDRzcBzCMJ2NuGLyxclNCCenCcM+SWWQpJFOXJychgTeaRx9cfIFxN1P7cXJfhRWyFXXq+j/6uyHyIg1IBXZGH2UHD99p1OjEY86G6lUj6LvDHMn5uqs6e/0XZ09VqqqqlBK4ThODkK8vAqh351J/5enEzfFvBz/8nQbR1+e3mpZVpvWmmNvpZx2AyRLklckrs9Ppu/6fCkDX5/fyzHX55VSLVrrr+T6/P8DAGZddp9W0hYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMTAtMjhUMDA6MDA6MzUrMDg6MDDA/zZXAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTEwLTI4VDAwOjAwOjM1KzA4OjAwsaKO6wAAACB0RVh0c29mdHdhcmUAaHR0cHM6Ly9pbWFnZW1hZ2ljay5vcme8zx2dAAAAGHRFWHRUaHVtYjo6RG9jdW1lbnQ6OlBhZ2VzADGn/7svAAAAGHRFWHRUaHVtYjo6SW1hZ2U6OkhlaWdodAA2MDB63r21AAAAF3RFWHRUaHVtYjo6SW1hZ2U6OldpZHRoADYwMOkv7egAAAAZdEVYdFRodW1iOjpNaW1ldHlwZQBpbWFnZS9wbmc/slZOAAAAF3RFWHRUaHVtYjo6TVRpbWUAMTYzNTM1MDQzNcyCpWkAAAATdEVYdFRodW1iOjpTaXplADQ2Mzc3QkJOQPy9AAAARnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vYXBwL3RtcC9pbWFnZWxjL2ltZ3ZpZXcyXzlfMTYzMTc1NzUzMTk4NTEzNTJfNzhfWzBdsLVeAwAAAABJRU5ErkJggg=='
let imageDataString = logoBase64.slice(22)
let imageData = Data.fromBase64String(imageDataString)
let imageFromData = Image.fromData(imageData)
return imageFromData
},
/**
* 获取渲染需要的数据
*/
async getVinData () {
const cacheKey = Utils.md5('BMWApi.getVinData')
/*if (DEV && Keychain.contains(cacheKey)) {
console.log('测试阶段直接读取缓存')
return JSON.parse(Keychain.get(cacheKey))
}*/
let accessToken = await this.getAccessToken()
if (accessToken === '') {
// todo 返回具体什么情况到前端
return null
}
const data = await this.getVIN(accessToken)
// 存储缓存
Keychain.set(cacheKey, JSON.stringify(data))
await this.checkInDaily(accessToken)
return data
},
/**
* 获取远程图片内容
* @param {string} url 图片地址
* @param {bool} useCache 是否使用缓存(请求失败时获取本地缓存)
*/
async getBmwImage (url, useCache = true) {
const cacheKey = Utils.md5(url)
const cacheFile = FileManager.local().
joinPath(FileManager.local().temporaryDirectory(), cacheKey)
// 判断是否有缓存
if (useCache && FileManager.local().fileExists(cacheFile)) {
return Image.fromFile(cacheFile)
}
try {
let access_token = ''
if (Keychain.contains(MY_BMW_TOKEN)) {
access_token = Keychain.get(MY_BMW_TOKEN)
} else {
throw new Error('没有token')
}
const req = new Request(url)
req.method = 'GET'
req.headers = {
'user-agent': 'Dart/2.13 (dart:io)',
'x-user-agent': 'ios(15.4.1);bmw;2.3.0(13603)',
'authorization': 'Bearer ' + access_token,
'accept-language': 'zh-CN',
'host': 'myprofile.bmw.com.cn',
'x-cluster-use-mock': 'never',
'24-hour-format': 'true',
}
const img = await req.loadImage()
// 存储到缓存
FileManager.local().writeImage(cacheFile, img)
return img
} catch (e) {
// 没有缓存+失败情况下,返回自定义的绘制图片(红色背景)
let ctx = new DrawContext()
ctx.size = new Size(100, 100)
ctx.setFillColor(Color.red())
ctx.fillRect(new Rect(0, 0, 100, 100))
return await ctx.getImage()
}
},
}
// 界面辅助类放这里
const UIUtils = {
/**
* @description Provide a font based on the input.
* @param {*} fontName
* @param {*} fontSize
*/
provideFont (fontName, fontSize) {
const fontGenerator = {
'ultralight': function () {
return Font.ultraLightSystemFont(fontSize)
},
'light': function () { return Font.lightSystemFont(fontSize) },
'regular': function () { return Font.regularSystemFont(fontSize) },
'medium': function () { return Font.mediumSystemFont(fontSize) },
'semibold': function () { return Font.semiboldSystemFont(fontSize) },
'bold': function () { return Font.boldSystemFont(fontSize) },
'heavy': function () { return Font.heavySystemFont(fontSize) },
'black': function () { return Font.blackSystemFont(fontSize) },
'italic': function () { return Font.italicSystemFont(fontSize) },
}
const systemFont = fontGenerator[fontName]
if (systemFont) { return systemFont() }
return new Font(fontName, fontSize)
},
getBack () {
const bgColor = new LinearGradient()
let bgArr = ['#ffffff', '#ffffff', '#1c1c1c', '#1c1c1c']
if (defaultData.lightBackground.length === 15) {
let arr = defaultData.lightBackground.split(',')
bgArr[0] = arr[0]
bgArr[1] = arr[1]
}
if (defaultData.darkBackground.length === 15) {
let arr = defaultData.darkBackground.split(',')
bgArr[2] = arr[0]
bgArr[3] = arr[1]
}
let startColor = Color.dynamic(new Color(bgArr[0]), new Color(bgArr[2]))
let endColor = Color.dynamic(new Color(bgArr[1]), new Color(bgArr[3]))
bgColor.colors = [startColor, endColor]
bgColor.locations = [0.0, 1.0]
return bgColor
},
getFgColor () {
// 浅色模式 主色,深色模式主色,浅色模式次色,深色模式次色
const defaultColor = ['#404040', '#ffffff', '#404040', '#ffffff']
if (defaultData.lightFgColor.length > 0) {
let arr = defaultData.lightFgColor.split(',') // 分割后是主色和次色
defaultColor[0] = arr[0]
if (arr.length == 2) {
defaultColor[1] = arr[1]
}
}
if (defaultData.darkFgColor.length > 0) {
let arr = defaultData.darkFgColor.split(',') // 分割后是主色和次色
defaultColor[2] = arr[0]
if (arr.length == 2) {
defaultColor[3] = arr[1]
}
}
let fgColor = Color.dynamic(new Color(defaultColor[0]),
new Color(defaultColor[1]))
let fgColorAccent = Color.dynamic(new Color(defaultColor[2]),
new Color(defaultColor[3]))
return { fgColor, fgColorAccent, defaultColor }
},
creatProgress (percent, width, fgH, bgH, fgColor, bgColor) {
const context = new DrawContext()
context.size = new Size(width, fgH > bgH ? fgH : bgH)
context.opaque = false
context.respectScreenScale = true
context.setFillColor(bgColor)
const path = new Path()
path.addRoundedRect(new Rect(0, (fgH - bgH) / 2, width, bgH), bgH / 2,
bgH / 2)
context.addPath(path)
context.fillPath()
context.setFillColor(fgColor)
const path1 = new Path()
path1.addRoundedRect(new Rect(0, 0, width * percent, fgH), fgH / 2, fgH / 2)
context.addPath(path1)
context.fillPath()
return context.getImage()
},
}
const App = {
name: 'MyBMW',
desc: 'MyBMW小部件',
SETTING_KEY: '',
FILE_MGR: null,
// 本地,用于存储图片等
FILE_MGR_LOCAL: null,
settings: {},
en: 'UserConfig',
NEW_SETTING_KEY: '',
widgetFamily: '',
error: '',
async setWidgetUserConfig () {
const b = new Alert()
b.title = '免责声明'
b.message = `
此脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
由于此脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
请勿将此脚本用于任何商业或非法目的,若违反规定请自行对此负责。
此脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
如果任何单位或个人认为此脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此脚本。
所有直接或间接使用、查看此脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此脚本,即视为您已接受此免责声明。
`
b.addAction('同意')
b.addCancelAction('不同意')
const idb = await b.presentAlert()
if (idb === -1) {
MyLog('不同意')
Keychain.set(MY_BMW_AGREE, 'false')
return
} else {
Keychain.set(MY_BMW_AGREE, 'true')
}
const a = new Alert()
a.title = 'MyBMW'
a.message = '配置MyBMW账号密码'
a.addTextField('账号(不要86)', defaultData.username)
a.addSecureTextField('密码', defaultData.password)
a.addTextField('邀请码', defaultData.invite_code)
a.addTextField('车架号(土豪选填)', defaultData.vin)
a.addAction('确定')
a.addCancelAction('取消')
const id = await a.presentAlert()
if (id === -1) return
defaultData.username = a.textFieldValue(0)
defaultData.password = a.textFieldValue(1)
defaultData.invite_code = a.textFieldValue(2)
defaultData.vin = a.textFieldValue(3)
// 保存到本地
App.settings[App.en] = defaultData
App.clearKeychain()
App.saveSettings()
},
async setWidgetStyleConfig () {
const a = new Alert()
a.title = 'MyBMW'
a.message = '风格自定义配置,背景色15个字符,比如:#ffffff,#ffffff'
a.addTextField('自定义车名', defaultData.custom_name)
a.addTextField('自定义车辆图', defaultData.custom_car_image)
a.addTextField('浅色模式背景颜色', defaultData.lightBackground)
a.addTextField('深色模式背景颜色', defaultData.darkBackground)
a.addTextField('浅色模式前景色', defaultData.lightFgColor)
a.addTextField('深色模式前景色', defaultData.darkFgColor)
a.addTextField('油箱容积(L)', defaultData.oilTotal)
a.addAction('确定')
a.addCancelAction('取消')
const id = await a.presentAlert()
if (id === -1) return
defaultData.custom_name = a.textFieldValue(0)
defaultData.custom_car_image = a.textFieldValue(1)
defaultData.lightBackground = a.textFieldValue(2)
defaultData.darkBackground = a.textFieldValue(3)
defaultData.lightFgColor = a.textFieldValue(4)
defaultData.darkFgColor = a.textFieldValue(5)
defaultData.oilTotal = a.textFieldValue(6)
// 保存到本地
App.settings[App.en] = defaultData
App.saveSettings()
},
/**
* 获取当前插件的设置
* @param {boolean} json 是否为json格式
*/
getSettings (json = true) {
let res = json ? {} : ''
let cache = ''
if (Keychain.contains(this.NEW_SETTING_KEY)) {
cache = Keychain.get(this.NEW_SETTING_KEY)
} else if (Keychain.contains(this.SETTING_KEY)) {
cache = Keychain.get(this.SETTING_KEY)
}
if (json) {
try {
res = JSON.parse(cache)
} catch (e) { }
} else {
res = cache
}
return res
},
/**
* 存储当前设置
* @param {bool} notify 是否通知提示
*/
saveSettings (notify = true) {
let res = (typeof this.settings === 'object') ? JSON.stringify(
this.settings) : String(this.settings)
Keychain.set(this.NEW_SETTING_KEY, res)
if (notify) Utils.notify('设置成功', '桌面组件稍后将自动刷新')
},
clearKeychain () {
if (Keychain.contains(MY_BMW_REFRESH_TOKEN)) {
Keychain.remove(MY_BMW_REFRESH_TOKEN)
}
if (Keychain.contains(MY_BMW_TOKEN)) {
Keychain.remove(MY_BMW_TOKEN)
}
if (Keychain.contains(MY_BMW_UPDATE_AT)) {
Keychain.remove(MY_BMW_UPDATE_AT)
}
console.log('更新配置,移除token')
},
init (widgetFamily = config.widgetFamily) {
// 组件大小:small,medium,large
this.widgetFamily = widgetFamily
// 系统设置的key,这里分为三个类型:
// 1. 全局
// 2. 不同尺寸的小组件
// 3. 不同尺寸+小组件自定义的参数
// 当没有key2时,获取key1,没有key1获取全局key的设置
// this.SETTING_KEY = this.md5(Script.name()+'@'+this.widgetFamily+"@"+this.arg)
// this.SETTING_KEY1 = this.md5(Script.name()+'@'+this.widgetFamily)
this.SETTING_KEY = Utils.md5(Script.name())
this.NEW_SETTING_KEY = Utils.md5(module.filename)
// 文件管理器
// 提示:缓存数据不要用这个操作,这个是操作源码目录的,缓存建议存放在local temp目录中
this.FILE_MGR = FileManager[module.filename.includes('Documents/iCloud~')
? 'iCloud'
: 'local']()
// 本地,用于存储图片等
this.FILE_MGR_LOCAL = FileManager.local()
// // 插件设置
this.settings = this.getSettings()
defaultData = { ...defaultData, ...this.settings[this.en] }
this.error = ''
},
}
/**
* 渲染函数
* 可以根据 widgetFamily 来判断小组件尺寸,以返回不同大小的内容
*/
render = async () => {
if (defaultData.username === '' || defaultData.password === '') {
App.error = '请先配置用户'
return await renderError(App.error)
}
let size = Device.screenSize()
console.log(size)
const data = await BMWApi.getVinData()
if (data === null) {
return await renderError(App.error)
}
data.size = DeviceSize[size.width + 'x' + size.height] ||
DeviceSize['375x812']
MyLog('开始渲染')
switch (App.widgetFamily) {
case 'large':
return await renderLarge(data)
case 'medium':
return await renderMedium(data)
default:
return await renderSmall(data)
}
}
const renderSmall = async (data) => {
let w = new ListWidget()
const width = data.size.small[0]
const height = data.size.small[1]
let fontColor = UIUtils.getFgColor().fgColor
w.backgroundGradient = UIUtils.getBack()
w.setPadding(0, 0, 0, 0)
//data.status.fuelIndicators[0].rangeValue = '50'
//data.status.fuelIndicators[0].levelValue = '10'
const {
levelValue,
levelUnits,
rangeValue,
rangeUnits,
} = data.status.fuelIndicators[0]
const topBox = w.addStack()
// 横向布局
topBox.layoutHorizontally()
topBox.setPadding(12, 12, 4, 12)
const topLeftBox = topBox.addStack()
topLeftBox.size = new Size(width - 40, 0)
topLeftBox.setPadding(0, 0, 0, 0)
topLeftBox.bottomAlignContent()
const topRightBox = topBox.addStack()
topRightBox.setPadding(0, 0, 0, 0)
let logoImage = topRightBox.addImage(await BMWApi.getBMWLogo())
logoImage.imageSize = new Size(16, 16)
const rangeBox = topLeftBox.addStack()
rangeBox.bottomAlignContent()
const rangeValueTxt = rangeBox.addText(rangeValue)
rangeValueTxt.font = UIUtils.provideFont('EuphemiaUCAS-Bold', 26)
rangeValueTxt.textColor = fontColor
rangeValueTxt.leftAlignText()
rangeValueTxt.minimumScaleFactor = 0.8
const rangeUnitsBox = rangeBox.addStack()
rangeUnitsBox.setPadding(0, 1, 2, 0)
const rangeUnitsTxt = rangeUnitsBox.addText(rangeUnits)
rangeUnitsTxt.font = UIUtils.provideFont('EuphemiaUCAS', 12)
rangeUnitsTxt.textColor = fontColor
topLeftBox.addSpacer(4)
const levelBox = topLeftBox.addStack()
levelBox.setPadding(0, 0, 1.5, 0)
levelBox.bottomAlignContent()
const levelValueTxt = levelBox.addText(levelValue)
levelValueTxt.font = UIUtils.provideFont('EuphemiaUCAS-Bold', 15)
levelValueTxt.minimumScaleFactor = 0.8
levelValueTxt.textColor = fontColor
levelValueTxt.rightAlignText()
const levelUnitsBox = levelBox.addStack()
levelUnitsBox.setPadding(0, 0, 1, 0)
const levelUnitsTxt = levelUnitsBox.addText(levelUnits)
levelUnitsTxt.font = UIUtils.provideFont('EuphemiaUCAS', 10)
levelUnitsTxt.textColor = fontColor
levelUnitsTxt.rightAlignText()
topLeftBox.addSpacer()
const carNameBox = w.addStack()
carNameBox.setPadding(0, 12, 0, 0)
let carName = `${data.bodyType} ${data.model}`
if (defaultData.custom_name.length > 0) {
carName = defaultData.custom_name
}
const carNameTxt = carNameBox.addText(carName)
carNameTxt.font = UIUtils.provideFont('EuphemiaUCAS-Bold', 16)
carNameTxt.minimumScaleFactor = 0.7
carNameTxt.textColor = fontColor
const parentBox = w.addStack()
parentBox.setPadding(2, 12, 0, 0)
const carStatusBox = parentBox.addStack()
carStatusBox.setPadding(3, 3, 3, 3)
carStatusBox.layoutHorizontally()
carStatusBox.centerAlignContent()
carStatusBox.cornerRadius = 4
carStatusBox.backgroundColor = Color.dynamic(new Color('#e4e4e4'),
new Color('#030303'))
const clockBox = carStatusBox.addStack()
clockBox.size = new Size(11, 11)
const clockImage = clockBox.addImage(
await Utils.getImageByUrl('https://z3.ax1x.com/2021/11/02/IPIGEF.png'))
clockImage.size = new Size(11, 11)
clockImage.tintColor = Color.dynamic(
new Color(UIUtils.getFgColor().defaultColor[0]),
new Color(UIUtils.getFgColor().defaultColor[1]))
carStatusBox.addSpacer(2)
const updateTxt = carStatusBox.addText(
`${data.status.timestampMessage.replace('已从车辆更新', '').
replace('2022/', '')}`)
updateTxt.font = UIUtils.provideFont('EuphemiaUCAS', 10)
updateTxt.textColor = fontColor
updateTxt.textOpacity = 0.7
carStatusBox.addSpacer(4)
const carStatusTxt = carStatusBox.addText(
`${data.status.doorsGeneralState}`)
carStatusTxt.font = UIUtils.provideFont('EuphemiaUCAS', 10)
carStatusTxt.textColor = fontColor
carStatusTxt.textOpacity = 0.8
carStatusBox.addSpacer(4)
const bottomBox = w.addStack()
bottomBox.setPadding(8, 12, 0, 0)
bottomBox.addSpacer()
const carImageBox = bottomBox.addStack()
carImageBox.setPadding(0, 0, 0, 0)
carImageBox.centerAlignContent()
let imageCar
let carImageUrl = `https://myprofile.bmw.com.cn/eadrax-ics/v3/presentation/vehicles/${data.vin}/images?carView=VehicleStatus`
if (defaultData.custom_car_image.length > 0) {
imageCar = await Utils.getImageByUrl(defaultData.custom_car_image)
} else {
imageCar = await BMWApi.getBmwImage(carImageUrl)
}
let carImage = carImageBox.addImage(imageCar)
w.addSpacer()
const SHORTCUTNAME = 'MyBMW'
const BASEURL = 'shortcuts://run-shortcut?name='
w.url = 'de.bmw.connected.mobile20.cn.Share-Ext.Destination://' //BASEURL + encodeURI(SHORTCUTNAME)
return w
}
const renderMedium = async (data) => {
const {
levelValue,
levelUnits,
rangeValue,
rangeUnits,
} = data.status.fuelIndicators[0]
const padding = 11
let w = new ListWidget()
const width = data.size.medium[0]
const height = data.size.medium[1]
let fontColor = UIUtils.getFgColor().fgColor
w.backgroundGradient = UIUtils.getBack()
w.setPadding(padding, padding, padding, padding)
const mainBox = w.addStack()
mainBox.layoutHorizontally()
const leftBox = mainBox.addStack()
leftBox.size = new Size(width / 2 - padding, height - padding * 2)
//leftBox.backgroundColor = Color.red()
mainBox.addSpacer(10)
const rightBox = mainBox.addStack()
rightBox.size = new Size(width / 2 - padding - 10, height - padding * 2)
rightBox.layoutVertically()
//rightBox.backgroundColor = Color.green()
const carBox = leftBox.addStack()
carBox.setPadding(8, 8, 8, 8)
carBox.cornerRadius = 12
carBox.backgroundColor = Color.dynamic(new Color('#ffffff'),
new Color('#2c2c2c'))
let imageCar
let carImageUrl = `https://myprofile.bmw.com.cn/eadrax-ics/v3/presentation/vehicles/${data.vin}/images?carView=VehicleStatus`
if (defaultData.custom_car_image.length > 0) {
imageCar = await Utils.getImageByUrl(defaultData.custom_car_image)
} else {
imageCar = await BMWApi.getBmwImage(carImageUrl)
}
carBox.layoutVertically()
const carImageBox = carBox.addStack()
carImageBox.addSpacer(null)
let carImage = carImageBox.addImage(imageCar)
carImageBox.addSpacer(null)
carBox.addSpacer(null)
let carName = `${data.brand} ${data.bodyType} ${data.model}`
if (defaultData.custom_name.length > 0) {
carName = defaultData.custom_name
}
const carNameText = carBox.addText(carName)
carNameText.font = UIUtils.provideFont('EuphemiaUCAS-Bold', 20)
carNameText.textColor = fontColor
carNameText.textOpacity = 1
carNameText.minimumScaleFactor = 0.7
carBox.addSpacer(null)
const infoBox = carBox.addStack()
infoBox.centerAlignContent()
infoBox.setPadding(4, 6, 4, 6)
infoBox.cornerRadius = 6
infoBox.backgroundColor = Color.dynamic(new Color('#e4e4e4'),
new Color('#030303'))
const clockBox = infoBox.addStack()
clockBox.size = new Size(13, 13)
const clockImage = clockBox.addImage(
await Utils.getImageByUrl('https://z3.ax1x.com/2021/11/02/IPIGEF.png'))
clockImage.size = new Size(13, 13)
clockImage.tintColor = Color.dynamic(
new Color(UIUtils.getFgColor().defaultColor[0]),
new Color(UIUtils.getFgColor().defaultColor[1]))
infoBox.addSpacer(4)
const updateTimeTxt = infoBox.addText(
`${data.status.timestampMessage.replace('已从车辆更新', '').
replace('2022/', '')}`)
updateTimeTxt.font = UIUtils.provideFont('regular', 12)
updateTimeTxt.textColor = fontColor
updateTimeTxt.textOpacity = 0.8
updateTimeTxt.minimumScaleFactor = 0.8
infoBox.addSpacer(null)
const statusTxt = infoBox.addText(`${data.status.doorsGeneralState}`)
statusTxt.font = UIUtils.provideFont('regular', 13)
statusTxt.textColor = fontColor
statusTxt.textOpacity = 0.8
statusTxt.minimumScaleFactor = 0.8
// 不用canvas,手工画进度条试试
const oilBox = rightBox.addStack()
oilBox.layoutVertically()
const oilInfoBox = oilBox.addStack()
oilBox.addSpacer(6)
oilInfoBox.centerAlignContent()
const oilIconBox = oilInfoBox.addStack()
oilIconBox.size = new Size(16, 16)
const oilIcon = oilIconBox.addImage(
await Utils.getImageByUrl('https://z3.ax1x.com/2021/11/02/IPHyLt.png'))
oilIcon.size = new Size(16, 16)
let fuelPercentage = BMWApi.getOilPercent(data)
if (fuelPercentage < 15 || data.properties.combustionRange.distance.value <
100 || DEV) {
oilIcon.tintColor = Color.red()
} else {
oilIcon.tintColor = Color.dynamic(
new Color(UIUtils.getFgColor().defaultColor[0]),
new Color(UIUtils.getFgColor().defaultColor[1]))
}
oilInfoBox.addSpacer(4)
const oilPercentTxt = oilInfoBox.addText(`${fuelPercentage}%`)
oilPercentTxt.font = UIUtils.provideFont('regular', 16)
oilPercentTxt.textColor = fontColor
oilPercentTxt.textOpacity = 1
oilInfoBox.addSpacer(null)
if (!data.properties.areDoorsClosed || DEV) {
const doorIcon = await Utils.getImageByUrl(
'https://z3.ax1x.com/2021/11/02/IPb71H.png')
const doorBox = oilInfoBox.addStack()
doorBox.size = new Size(16, 16)
doorBox.addImage(doorIcon).tintColor = Color.red()
}
if (!data.properties.areWindowsClosed || DEV) {
oilInfoBox.addSpacer(8)
const windowIcon = await Utils.getImageByUrl(
'https://z3.ax1x.com/2021/11/02/IPbT9e.png')
const windowBox = oilInfoBox.addStack()
windowBox.size = new Size(16, 16)
windowBox.addImage(windowIcon).tintColor = Color.red()
}
oilInfoBox.addSpacer(8)
const logoBox = oilInfoBox.addStack()
logoBox.size = new Size(16, 16)
let logoImage = logoBox.addImage(await BMWApi.getBMWLogo())
logoImage.imageSize = new Size(16, 16)
const processBarBox = oilBox.addStack()
const allLength = width / 2 - padding - 10
processBarBox.size = new Size(allLength, 0)
const remainOilProcessBox = processBarBox.addStack()
remainOilProcessBox.backgroundColor = Color.dynamic(
new Color(UIUtils.getFgColor().defaultColor[0]),
new Color(UIUtils.getFgColor().defaultColor[1]))
if (fuelPercentage < 15 || DEV) {
remainOilProcessBox.backgroundColor = Color.red()
}
remainOilProcessBox.size = new Size(allLength * (fuelPercentage / 100), 10)
remainOilProcessBox.cornerRadius = 5
processBarBox.addSpacer(null)
// let processFg = Color.dynamic(new Color(UIUtils.getFgColor().defaultColor[0]), new Color(UIUtils.getFgColor().defaultColor[1]))
// if (fuelPercentage < 15 || DEV) {
// processFg = Color.red()
// }
// let processBg = Color.dynamic(new Color('#000000', 0.5), new Color('#ffffff', 0.5))
// const processImage = await UIUtils.creatProgress(fuelPercentage / 100,
// allLength, 8, 5, processFg, processBg)
// processBarBox.addImage(processImage)
processBarBox.backgroundColor = Color.dynamic(new Color('#000000', 0.1),
new Color('#ffffff', 0.1))
processBarBox.cornerRadius = 5
rightBox.addSpacer(null)
const rangeBox = rightBox.addStack()
rangeBox.layoutHorizontally()
rangeBox.bottomAlignContent()
rangeBox.addSpacer(null)
const rangeIconBox = rangeBox.addStack()
rangeIconBox.centerAlignContent()
rangeIconBox.size = new Size(22, 26)
rangeIconBox.setPadding(0, 0, 6, 2)
const rangeIcon = rangeIconBox.addImage(
await Utils.getImageByUrl('https://z3.ax1x.com/2021/11/02/IPbt6s.png'))
rangeIcon.size = new Size(20, 20)
rangeIcon.tintColor = Color.dynamic(
new Color(UIUtils.getFgColor().defaultColor[0]),
new Color(UIUtils.getFgColor().defaultColor[1]))
const rangeValueBox = rangeBox.addStack()
//rangeValueBox.backgroundColor = Color.red()
const rangeValueTxt = rangeValueBox.addText(rangeValue)
rangeValueTxt.font = UIUtils.provideFont('black', 35)
rangeValueTxt.textColor = fontColor
rangeValueTxt.leftAlignText()
rangeValueTxt.minimumScaleFactor = 0.8
rangeBox.addSpacer(4)
const rangeUnitsBox = rangeBox.addStack()
rangeUnitsBox.setPadding(0, 0, 4, 0)
//rangeUnitsBox.backgroundColor = Color.green()
const rangeUnitsTxt = rangeUnitsBox.addText(rangeUnits)
rangeUnitsTxt.font = UIUtils.provideFont('black', 20)
rangeUnitsTxt.textColor = fontColor
rangeUnitsTxt.leftAlignText()
rangeUnitsTxt.minimumScaleFactor = 0.8
rangeBox.addSpacer(null)
rightBox.addSpacer(null)
const otherInfoBox = rightBox.addStack()
otherInfoBox.setPadding(10, 8, 10, 8)
otherInfoBox.cornerRadius = 12
otherInfoBox.backgroundColor = Color.dynamic(new Color('#ffffff'),
new Color('#2c2c2c'))
otherInfoBox.addSpacer(null)
const allMileageTxt = otherInfoBox.addText(
`总里程: ${data.status.currentMileage.mileage}${data.status.currentMileage.units}`)
allMileageTxt.font = UIUtils.provideFont('medium', 15)
allMileageTxt.textColor = fontColor
allMileageTxt.leftAlignText()
allMileageTxt.minimumScaleFactor = 0.8
otherInfoBox.addSpacer(null)
// 进度条测试
w.url = 'de.bmw.connected.mobile20.cn.Share-Ext.Destination://' //BASEURL + encodeURI(SHORTCUTNAME)
return w
}
const renderLarge = async (data) => {
let w = new ListWidget()
const width = data.size.large[0]
const height = data.size.large[1]
let fontColor = Color.dynamic(new Color('#404040'), Color.white())
//w.backgroundGradient = UIUtils.getBack()
w.setPadding(0, 0, 0, 0)
const padding = 16
const {
levelValue,
levelUnits,
rangeValue,
rangeUnits,
} = data.status.fuelIndicators[0]
const { longitude, latitude } = data.properties.vehicleLocation.coordinates
let markerUrl = 'https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png'
let marker = `markers=-1,${markerUrl},0:${longitude},${latitude}`
let zoom = 17
let mapWidth = width// parseInt(width / height * 1024)
let mapHeight = height// 1024
const url = `https://restapi.amap.com/v3/staticmap?${marker}&zoom=${zoom}&traffic=1&scale=2&size=${mapWidth}*${mapHeight}&key=${AMAP_KEY}&location=${longitude},${latitude}`
let img = await Utils.getImageByUrl(url)
console.log(url)
console.log(img.size)
const SHORTCUTNAME = 'MyBMW'
const BASEURL = 'shortcuts://run-shortcut?name='
w.url = 'de.bmw.connected.mobile20.cn.Share-Ext.Destination://' //BASEURL + encodeURI(SHORTCUTNAME)
w.backgroundImage = img
return w
}
const renderError = async (errMsg) => {
let w = new ListWidget()
w.backgroundGradient = UIUtils.getBack()
const padding = 16
w.setPadding(padding, padding, padding, padding)
const errText = w.addStack().addText(errMsg)
const helpText = w.addStack().addText(`登录秘钥失效,请联系QQ:1265988200`)
let fontColor = Color.dynamic(new Color('#404040'), Color.white())
errText.font = UIUtils.provideFont('EuphemiaUCAS-Bold', 16)
errText.textColor = fontColor
helpText.font = UIUtils.provideFont('EuphemiaUCAS-Bold', 12)
helpText.textColor = fontColor
w.url = 'de.bmw.connected.mobile20.cn.Share-Ext.Destination://' //BASEURL + encodeURI(SHORTCUTNAME)
return w
}
const Running = async () => {
App.init()
// 判断hash是否和当前设备匹配
if (config.runsInWidget) {
const W = await render()
Script.setWidget(W)
Script.complete()
} else {
if (DEV) {
App.widgetFamily = 'large'
w = await render()
await w.presentLarge()
return
}
let { act, data, __arg, __size } = args.queryParameters
const _actions = [
// 预览组件
async () => {
let a = new Alert()
a.title = '预览组件'
a.message = '测试桌面组件在各种尺寸下的显示效果'
a.addAction('小尺寸 Small')
a.addAction('中尺寸 Medium')
a.addAction('大尺寸 Large')
a.addAction('全部 All')
a.addCancelAction('取消操作')
const funcs = []
let i = await a.presentSheet()
if (i === -1) return
let w
switch (i) {
case 0:
App.widgetFamily = 'small'
w = await render()
await w.presentSmall()
break
case 1:
App.widgetFamily = 'medium'
w = await render()
await w.presentMedium()
break
case 2:
App.widgetFamily = 'large'
w = await render()
await w.presentLarge()
break
case 3:
App.widgetFamily = 'small'
w = await render()
await w.presentSmall()
App.widgetFamily = 'medium'
w = await render()
await w.presentMedium()
App.widgetFamily = 'large'
w = await render()
await w.presentLarge()
break
default:
const func = funcs[i - 4]
if (func) await func()
break
}
return i
},
async () => {
Safari.openInApp('https://support.qq.com/product/362419', false)
},
// 设置用户名
App.setWidgetUserConfig,
// 设置风格
App.setWidgetStyleConfig,
]
const alert = new Alert()
alert.title = App.name
alert.message = App.desc
alert.addAction('预览组件')
alert.addAction('查看文档和帮助')
alert.addAction('账号密码设置')
alert.addAction('组件自定义设置')
alert.addCancelAction('取消操作')
const idx = await alert.presentSheet()
if (_actions[idx]) {
const func = _actions[idx]
await func()
}
}
}
await Running()
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/huchundong/qjl-rapv06-ffj.git
[email protected]:huchundong/qjl-rapv06-ffj.git
huchundong
qjl-rapv06-ffj
QjlRAPV06FFj
master

搜索帮助