Skip to content

Instantly share code, notes, and snippets.

@notzheng
Last active April 5, 2023 15:17
Show Gist options
  • Save notzheng/c797bdacf6ef8708cccd7b2ef2260d3e to your computer and use it in GitHub Desktop.
Save notzheng/c797bdacf6ef8708cccd7b2ef2260d3e to your computer and use it in GitHub Desktop.
✈️机场剩余流量显示 Widget (Scriptable App)

Untitled 001

使用

  1. 下载 Scriptable https://scriptable.app/ , macOS 从此处下载 https://scriptable.app/mac-beta/
  2. 打开 Scriptable App ,新建脚本,输入 TrafficDisplay.js 中的内容
  3. 从机场复制 Shadowsocks 订阅地址 ,将脚本内容中 “<API_URL>” 更换为复制的 URL,更改 BUY_DAY 为你服务开始的日期(每月的几号),SERVICE_NAME 可以填写为服务提供商名称
  4. 在 iOS 桌面新建小组件(macOS 为通知栏),选择 Scriptable App,尺寸选择 small,选择第二步新建的脚本即可

关于重置日期

目前仅支持按月重置

由于订阅链接没有返回重置时间,脚本就按照每月几号计算的,如果过了几个月发现有误差,把脚本中 BUY_DAY 改成你最新一次重置邮件的日期即可(如 7.27,填 27 即可)

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: bell;
// 提供商名称
const SERVICE_NAME = "<SERVICE_NAME>";
// 提供商 shadowsocks 订阅地址
const API_URL = "<API_URL>";
// 购买日期
const BUY_DAY = 21;
const widget = await buildWidget();
if (config.runsInWidget) {
Script.setWidget(widget);
} else {
widget.presentSmall();
}
Script.complete();
function formatBytes(bytes, decimals = 2, split = false) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const number = String(parseFloat((bytes / Math.pow(k, i)).toFixed(dm)));
const unit = sizes[i];
if (split) {
const [int, decimal] = number.split(".");
return [int, decimal, unit];
}
return number + unit;
}
async function fetchTrafficInfo() {
const request = new Request(API_URL);
request.method = "HEAD";
request.headers = {
"user-agent": "Quantumult%20X",
};
await request.load();
const infoString = request.response.headers["subscription-userinfo"];
const { upload, download, total, expire } = infoString
.split(";")
.reduce((map, item) => {
const [key, value] = item.trim().split("=");
map[key] = Number(value);
return map;
}, {});
const used = upload + download;
const remaining = total - used;
//const df = new DateFormatter();
// df.dateFormat = "yyyy-mm-dd";
// info.expire = df.string(new Date(info.expire * 1000));
return {
used,
remaining,
total,
expireAt: expire,
};
}
function computeResetDaysFromNow() {
const today = new Date();
const day = today.getDate();
if (day > 0 && day < BUY_DAY) {
return BUY_DAY - day;
}
return (
new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate() -
today.getDate() +
BUY_DAY
);
}
async function main() {
try {
const info = await fetchTrafficInfo();
console.log(info);
} catch (err) {
console.log(err);
}
}
async function buildWidget() {
const info = await fetchTrafficInfo();
const widget = new ListWidget();
let gradient = new LinearGradient();
gradient.locations = [0, 1];
gradient.colors = [new Color("141414"), new Color("13233F")];
widget.backgroundGradient = gradient;
/** header */
const headerStack = widget.addStack();
addSFSymbol(headerStack, "cube.transparent", {
color: Color.white(),
opacity: 0.7,
});
headerStack.addSpacer(4);
addText(headerStack, SERVICE_NAME, {
opacity: 0.7,
font: Font.mediumSystemFont(13),
});
widget.addSpacer();
/** body */
const [remainingInt, remainingDecimal, remainingUnit] = formatBytes(
info.remaining,
2,
true
);
const remainingStack = widget.addStack();
remainingStack.addSpacer();
addText(remainingStack, remainingInt + "." + remainingDecimal, {
font: Font.boldSystemFont(32),
});
// addText(remainingStack, remainingDecimal, {
// font: Font.boldSystemFont(24),
// });
remainingStack.addSpacer();
remainingStack.bottomAlignContent();
const descStack = widget.addStack();
descStack.addSpacer();
addText(descStack, `${remainingUnit} 剩余`, {
opacity: 0.7,
font: Font.mediumSystemFont(10),
});
descStack.addSpacer();
widget.addSpacer(4);
widget.addSpacer(4);
const progressBarStack = widget.addStack();
// progressBarStack.addSpacer();
addImage(progressBarStack, createProgressImage(info.total, info.used), {
size: new Size(125, 5),
});
// progressBarStack.addSpacer();
const progressLabelStack = widget.addStack();
addText(progressLabelStack, formatBytes(info.used), {
opacity: 0.7,
font: Font.mediumSystemFont(10),
});
widget.addSpacer(2);
progressLabelStack.addSpacer();
addText(progressLabelStack, formatBytes(info.total), {
opacity: 0.7,
font: Font.mediumSystemFont(10),
});
widget.addSpacer();
const footerStack = widget.addStack();
addText(footerStack, `距下次重置:${computeResetDaysFromNow()}天`, {
opacity: 0.7,
font: Font.mediumSystemFont(12),
});
return widget;
}
function addText(target, text, { color = Color.white(), font, opacity } = {}) {
const textElement = target.addText(text);
if (opacity) {
textElement.textOpacity = opacity;
}
if (font) {
textElement.font = font;
}
textElement.textColor = color;
return textElement;
}
function addImage(target, image, { size, tintColor, opacity }) {
const imageElement = target.addImage(image);
if (size) {
imageElement.imageSize = size;
}
if (tintColor) {
imageElement.tintColor = tintColor;
}
if (opacity) {
imageElement.imageOpacity = opacity;
}
return imageElement;
}
function addSFSymbol(
target,
name,
{ size = new Size(16, 16), color = Color.white(), opacity } = {}
) {
const font = Font.systemFont(22);
const sfSymbol = SFSymbol.named(name);
sfSymbol.applyFont(font);
addImage(target, sfSymbol.image, {
tintColor: color,
size,
opacity,
});
}
function createProgressImage(total, process, { size = new Size(125, 5) } = {}) {
const context = new DrawContext();
(context.size = size), (context.opaque = false);
context.respectScreenScale = true;
context.setFillColor(new Color("#48484b"));
const path = new Path();
path.addRoundedRect(new Rect(0, 0, size.width, size.height), 3, 2);
context.addPath(path);
context.fillPath();
context.setFillColor(Color.blue());
const path1 = new Path();
path1.addRoundedRect(
new Rect(0, 0, (size.width * process) / total, size.height),
3,
2
);
context.addPath(path1);
context.fillPath();
return context.getImage();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment