@nsnanocat/util
Version:
Pure JS's util module for well-known iOS network tools
186 lines (182 loc) • 6.3 kB
JavaScript
import { $app } from "./app.mjs";
import { Console } from "../polyfill/Console.mjs";
/**
* 通知内容扩展参数。
* Extended notification content options.
*
* @typedef {object|string|number|boolean} NotificationContent
* @property {string} [open] 打开链接 / Open URL.
* @property {string} ["open-url"] 打开链接 (QuanX) / Open URL (QuanX).
* @property {string} [url] 打开链接 / Open URL.
* @property {string} [openUrl] 打开链接 (Loon/Shadowrocket) / Open URL (Loon/Shadowrocket).
* @property {string} [copy] 复制文本 / Copy text.
* @property {string} ["update-pasteboard"] 复制文本 (QuanX) / Copy text (QuanX).
* @property {string} [updatePasteboard] 复制文本 / Copy text.
* @property {string} [media] 媒体 URL 或 Base64 / Media URL or Base64.
* @property {string} ["media-url"] 媒体 URL / Media URL.
* @property {string} [mediaUrl] 媒体 URL / Media URL.
* @property {string} [mime] Base64 媒体 MIME / MIME type for Base64 media.
* @property {number} ["auto-dismiss"] 自动消失秒数 / Auto dismiss seconds.
* @property {string} [sound] 提示音 / Notification sound.
*/
/**
* 发送系统通知并按平台适配参数格式。
* Send system notification with platform-specific payload mapping.
*
* 说明:
* Notes:
* - iOS App 平台调用 `$notification.post` 或 `$notify`
* - iOS app platforms call `$notification.post` or `$notify`
* - Worker 不支持 iOS 通知接口,仅输出日志
* - Worker does not support iOS notification APIs; it logs only
* - Node.js 不支持 iOS 通知接口,仅输出日志
* - Node.js does not support iOS notification APIs; it logs only
*
* @param {string} [title=`ℹ️ ${$app} 通知`] 标题 / Title.
* @param {string} [subtitle=""] 副标题 / Subtitle.
* @param {string} [body=""] 内容 / Message body.
* @param {NotificationContent} [content={}] 扩展参数 / Extended content options.
* @returns {void}
*/
export function notification(title = `ℹ️ ${$app} 通知`, subtitle = "", body = "", content = {}) {
const mutableContent = MutableContent(content);
switch ($app) {
case "Surge":
case "Loon":
case "Stash":
case "Egern":
case "Shadowrocket":
default:
$notification.post(title, subtitle, body, mutableContent);
break;
case "Quantumult X":
$notify(title, subtitle, body, mutableContent);
break;
case "Worker":
case "Node.js":
break;
}
Console.group("📣 系统通知");
Console.log(title, subtitle, body, JSON.stringify(mutableContent, null, 2));
Console.groupEnd();
}
/**
* 将统一通知参数转换为平台可识别字段。
* Convert normalized content into platform-recognized fields.
*
* @private
* @param {NotificationContent} content 通知扩展参数 / Extended content options.
* @returns {Record<string, any>}
*/
const MutableContent = content => {
const mutableContent = {};
switch (typeof content) {
case undefined:
break;
case "string":
case "number":
case "boolean":
switch ($app) {
case "Surge":
case "Stash":
case "Egern":
default:
mutableContent.url = content;
break;
case "Loon":
case "Shadowrocket":
mutableContent.openUrl = content;
break;
case "Quantumult X":
mutableContent["open-url"] = content;
break;
case "Worker":
case "Node.js":
break;
}
break;
case "object": {
const openUrl = content.open || content["open-url"] || content.url || content.openUrl;
const copyUrl = content.copy || content["update-pasteboard"] || content.updatePasteboard;
const mediaUrl = content.media || content["media-url"] || content.mediaUrl;
switch ($app) {
case "Surge":
case "Stash":
case "Egern":
case "Shadowrocket":
default: {
if (openUrl) {
mutableContent.action = "open-url";
mutableContent.url = openUrl;
}
if (copyUrl) {
mutableContent.action = "clipboard";
mutableContent.text = copyUrl;
}
if (mediaUrl) {
switch (true) {
case mediaUrl.startsWith("http"): // http 开头的网络地址
mutableContent["media-url"] = mediaUrl;
break;
case mediaUrl.startsWith("data:"): {
// data 开头的 Base64 编码
// data:image/png;base64,iVBORw0KGgo...
const base64RegExp = /^data:(?<MIME>\w+\/\w+);base64,(?<Base64>.+)/;
const { MIME, Base64 } = mediaUrl.match(base64RegExp).groups;
mutableContent["media-base64"] = Base64;
mutableContent["media-base64-mime"] = content.mime || MIME;
break;
}
default: {
mutableContent["media-base64"] = mediaUrl;
// https://stackoverflow.com/questions/57976898/how-to-get-mime-type-from-base-64-string
switch (true) {
case mediaUrl.startsWith("CiVQREYt"):
case mediaUrl.startsWith("JVBERi0"):
mutableContent["media-base64-mime"] = "application/pdf";
break;
case mediaUrl.startsWith("R0lGODdh"):
case mediaUrl.startsWith("R0lGODlh"):
mutableContent["media-base64-mime"] = "image/gif";
break;
case mediaUrl.startsWith("iVBORw0KGgo"):
mutableContent["media-base64-mime"] = "image/png";
break;
case mediaUrl.startsWith("/9j/"):
mutableContent["media-base64-mime"] = "image/jpg";
break;
case mediaUrl.startsWith("Qk02U"):
mutableContent["media-base64-mime"] = "image/bmp";
break;
}
break;
}
}
}
if (content["auto-dismiss"]) mutableContent["auto-dismiss"] = content["auto-dismiss"];
if (content.sound) mutableContent.sound = content.sound;
break;
}
case "Loon": {
if (openUrl) mutableContent.openUrl = openUrl;
if (mediaUrl?.startsWith("http")) mutableContent.mediaUrl = mediaUrl;
break;
}
case "Quantumult X": {
if (openUrl) mutableContent["open-url"] = openUrl;
if (mediaUrl?.startsWith("http")) mutableContent["media-url"] = mediaUrl;
if (copyUrl) mutableContent["update-pasteboard"] = copyUrl;
break;
}
case "Worker":
case "Node.js":
break;
}
break;
}
default:
Console.error(`不支持的通知参数类型: ${typeof content}`, "");
break;
}
return mutableContent;
};