import Qmsg from "qmsg"; import { DOMUtils, httpx, log, pops, utils } from "../env.base"; import { PanelUISize } from "../setting/panel-ui-size"; import { CommonUtil } from "./CommonUtil"; import type { RuleSubscribeOption } from "./RulePanelView"; import { StorageUtils } from "./StorageUtils"; type RuleSubscribeConstructOption = { /** * 存储的主键名 */ STORAGE_API_KEY: string; /** * 存储的键名 */ STORAGE_KEY: string; }; class RuleSubscribe< T extends { /** 唯一键 */ uuid: string; /** 订阅的uuid */ subscribeUUID: string | null; /** 是否启用 */ enable: boolean; }, > { option; storageApi; constructor(option: RuleSubscribeConstructOption) { this.option = option; this.storageApi = new StorageUtils(option.STORAGE_API_KEY); } /** * 获取所有订阅 */ getAllSubscribe() { const allSubscribe = this.storageApi.get[]>(this.option.STORAGE_KEY, []); return allSubscribe; } /** * 获取所有订阅内的所有的规则 * @param [filterUnEnable=false] 是否过滤掉未启用的规则(包括订阅) */ getAllSubscribeRule(filterUnEnable = false) { const allSubscribe = this.getAllSubscribe(); const allSubscribeRule: T[] = []; for (let index = 0; index < allSubscribe.length; index++) { const subscribeItem = allSubscribe[index]; if (filterUnEnable && !subscribeItem.data.enable) { // 未启用 continue; } for (let subscribeIndex = 0; subscribeIndex < subscribeItem.subscribeData.ruleData.length; subscribeIndex++) { const subscribeRuleData = subscribeItem.subscribeData.ruleData[subscribeIndex]; if (filterUnEnable && !subscribeRuleData.enable) { // 未启用 continue; } // 赋值订阅的uuid subscribeRuleData.subscribeUUID = subscribeItem.uuid; allSubscribeRule.push(subscribeRuleData); } } return allSubscribeRule; } /** * 获取某个订阅 * @param subscribeUUID 订阅的uuid */ getSubscribe(subscribeUUID: string) { const findValue = this.getAllSubscribe().find((rule) => rule.uuid == subscribeUUID); return findValue; } /** * 获取某个订阅的规则 * @param subscribeUUID 订阅的uuid * @param uuid 规则的uuid */ getSubscribeRule(subscribeUUID: string, uuid: string) { const findSubscribe = this.getSubscribe(subscribeUUID); if (findSubscribe) { const findRule = findSubscribe.subscribeData.ruleData.find((rule) => rule.uuid === uuid); return findRule; } } /** * 删除所有订阅 */ deleteAllSubscribe() { this.storageApi.delete(this.option.STORAGE_KEY); } /** * 删除某个订阅 * @param config 配置/uuid */ deleteSubscribe(config: RuleSubscribeOption | string) { const uuid = typeof config === "string" ? config : config.uuid; const allSubscribe = this.getAllSubscribe(); const findIndex = allSubscribe.findIndex((subscribeItem) => subscribeItem.uuid === uuid); if (findIndex !== -1) { allSubscribe.splice(findIndex, 1); this.storageApi.set(this.option.STORAGE_KEY, allSubscribe); } return findIndex !== -1; } /** * 清空某个订阅内的规则 */ clearSubscribe(config: RuleSubscribeOption | string) { const uuid = typeof config === "string" ? config : config.uuid; const allSubscribe = this.getAllSubscribe(); const findIndex = allSubscribe.findIndex((subscribeItem) => subscribeItem.uuid === uuid); if (findIndex !== -1) { allSubscribe[findIndex].subscribeData.ruleData = []; this.storageApi.set(this.option.STORAGE_KEY, allSubscribe); return true; } else { return false; } } /** * 新增某个订阅 */ addSubscribe(subscribe: RuleSubscribeOption) { let flag = false; const allSubscribe = this.getAllSubscribe(); const findIndex = allSubscribe.findIndex((subscribeItem) => subscribeItem.uuid === subscribe.uuid); if (findIndex === -1) { // 不存在 allSubscribe.push(subscribe); flag = true; } else { // 存在相同uuid } if (flag) { this.storageApi.set(this.option.STORAGE_KEY, allSubscribe); } return flag; } /** * 更新某个订阅 */ updateSubscribe(subscribe: RuleSubscribeOption) { let flag = false; const allSubscribe = this.getAllSubscribe(); const findIndex = allSubscribe.findIndex((subscribeItem) => subscribeItem.uuid === subscribe.uuid); if (findIndex !== -1) { // 存在相同uuid,更新数据 allSubscribe[findIndex] = subscribe; flag = true; } else { // 不存在 } if (flag) { this.storageApi.set(this.option.STORAGE_KEY, allSubscribe); } return flag; } /** * 更新某个订阅内的某个规则 */ updateSubscribeRule(subscribeUUID: string, rule: RuleSubscribeOption["subscribeData"]["ruleData"]["0"]) { let flag = false; const allSubscribe = this.getAllSubscribe(); const targetSubscribe = allSubscribe.find((subscribeItem) => subscribeItem.uuid === subscribeUUID); if (targetSubscribe) { // 找到目标订阅 const findRuleIndex = targetSubscribe.subscribeData.ruleData.findIndex((ruleItem) => ruleItem.uuid === rule.uuid); if (findRuleIndex !== -1) { // 找到目标规则 targetSubscribe.subscribeData.ruleData[findRuleIndex] = rule; flag = true; } } if (flag) { this.storageApi.set(this.option.STORAGE_KEY, allSubscribe); } return true; } /** * 删除某个订阅内的某个规则 * @param subscribeUUID 订阅的uuid * @param rule 规则 */ deleteSubscribeRule(subscribeUUID: string, rule: RuleSubscribeOption["subscribeData"]["ruleData"]["0"]) { let flag = false; const allSubscribe = this.getAllSubscribe(); const findIndex = allSubscribe.findIndex((subscribeItem) => subscribeItem.uuid === subscribeUUID); if (findIndex !== -1) { const targetSubscribe = allSubscribe[findIndex]; const findRuleIndex = targetSubscribe.subscribeData.ruleData.findIndex((ruleItem) => ruleItem.uuid === rule.uuid); if (findRuleIndex !== -1) { allSubscribe[findIndex].subscribeData.ruleData.splice(findRuleIndex, 1); this.storageApi.set(this.option.STORAGE_KEY, allSubscribe); flag = true; } } return flag; } /** * 获取订阅链接的数据信息 * @param url 订阅链接 */ async getSubscribeInfo(url: string) { const response = await httpx.get(url, { allowInterceptConfig: false, timeout: 10000, headers: { "User-Agent": utils.getRandomPCUA(), }, }); if (!response.status) { log.error(response); return { data: null, msg: "获取订阅信息失败", }; } const subscribeParsedData = utils.toJSON["subscribeData"]>(response.data.responseText); if ( typeof subscribeParsedData.title === "string" && typeof subscribeParsedData.version === "number" && typeof subscribeParsedData.lastModified === "number" && Array.isArray(subscribeParsedData.ruleData) ) { /** 用于存储的数据 */ const subscribeInfo: RuleSubscribeOption = { uuid: utils.generateUUID(), subscribeData: subscribeParsedData, data: { enable: true, url: url, latestUpdateTime: Date.now(), updateFailedTime: null, }, }; return { data: subscribeInfo, msg: "", }; } else { log.error(subscribeParsedData); return { data: null, msg: "订阅链接的内容格式不正确", }; } } /** * 更新所有订阅 */ async updateAllSubscribe() { const allSubscribe = this.getAllSubscribe(); for (let index = 0; index < allSubscribe.length; index++) { const subscribeItem = allSubscribe[index]; if (!subscribeItem.data.enable) { // 未启用,不更新 continue; } if ( typeof subscribeItem.data.updateFailedTime === "number" && utils.formatTime(subscribeItem.data.updateFailedTime, "yyyyMMdd") === utils.formatTime(Date.now(), "yyyyMMdd") ) { // 今天更新失败,今天不再更新 continue; } if ( typeof subscribeItem.data.latestUpdateTime === "number" && utils.formatTime(Date.now(), "yyyyMMdd") === utils.formatTime(subscribeItem.data.latestUpdateTime, "yyyyMMdd") ) { // 今天已更新 continue; } const requestSubscribeInfo = await this.getSubscribeInfo(subscribeItem.data.url); let updateFlag = false; if (requestSubscribeInfo.data) { const subscribeNewItem = requestSubscribeInfo.data; subscribeNewItem.uuid = subscribeItem.uuid; subscribeNewItem.data = subscribeItem.data; subscribeNewItem.data.latestUpdateTime = Date.now(); const title = subscribeNewItem.data.title || subscribeNewItem.subscribeData.title || subscribeNewItem.data.url; subscribeItem.data.updateFailedTime = null; updateFlag = this.updateSubscribe(subscribeNewItem); if (updateFlag) { log.success(`更新订阅成功:${title}`); } else { log.error(`更新订阅失败:${title}`, subscribeItem); } } else { log.error("更新订阅失败:" + requestSubscribeInfo.msg, subscribeItem); } if (!updateFlag) { subscribeItem.data.updateFailedTime = Date.now(); // 更新失败,设置失败时间 this.updateSubscribe(subscribeItem); } } } /** * 导入订阅 * @param importEndCallBack 导入完毕后的回调 */ importSubscribe(importEndCallBack?: () => void) { const $alert = pops.alert({ title: { text: "请选择导入方式", position: "center", }, content: { text: /*html*/ `
本地导入
网络导入
剪贴板导入
`, html: true, }, btn: { ok: { enable: false }, close: { enable: true, callback(details) { details.close(); }, }, }, drag: true, mask: { enable: true, }, width: PanelUISize.info.width, height: PanelUISize.info.height, style: /*css*/ ` .btn-control{ display: inline-block; margin: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .btn-control:hover{ color: #409eff; border-color: #c6e2ff; background-color: #ecf5ff; } `, }); /** 本地导入 */ const $local = $alert.$shadowRoot.querySelector(".btn-control[data-mode='local']")!; /** 网络导入 */ const $network = $alert.$shadowRoot.querySelector(".btn-control[data-mode='network']")!; /** 剪贴板导入 */ const $clipboard = $alert.$shadowRoot.querySelector(".btn-control[data-mode='clipboard']")!; /** * 将获取到的规则更新至存储 */ const updateRuleToStorage = async (data: RuleSubscribeOption[]) => { let allData = this.getAllSubscribe(); const addNewData: typeof allData = []; const repeatData: { index: number; data: (typeof allData)["0"]; }[] = []; for (let index = 0; index < data.length; index++) { const dataItem = data[index]; const findIndex = allData.findIndex((it) => it.uuid === dataItem.uuid); if (findIndex !== -1) { // 存在相同的uuid的规则 repeatData.push({ index: findIndex, data: dataItem, }); } else { // 追加 addNewData.push(dataItem); } } await new Promise((resolve) => { const confirmResult = globalThis.confirm(`存在相同的uuid的规则 ${repeatData.length}条,是否进行覆盖?`); if (confirmResult) { // 覆盖 for (const repeatDataItem of repeatData) { allData[repeatDataItem.index] = repeatDataItem.data; } } resolve(true); }); allData = allData.concat(addNewData); this.storageApi.set(this.option.STORAGE_KEY, allData); Qmsg.success(`共 ${data.length} 条订阅,新增 ${addNewData.length} 条`); importEndCallBack?.(); }; /** * @param subscribeText 订阅文件文本 */ const importFile = (subscribeText: string) => { return new Promise(async (resolve) => { const data = utils.toJSON>(subscribeText); if (!Array.isArray(data)) { log.error(data); Qmsg.error("导入失败,格式不符合(不是数组)", { consoleLogContent: true, }); resolve(false); return; } if (!data.length) { Qmsg.error("导入失败,解析出的数据为空", { consoleLogContent: true, }); resolve(false); return; } const demoFirst: RuleSubscribeOption = data[0]; if ( !( typeof demoFirst.data === "object" && demoFirst.data != null && typeof demoFirst.subscribeData === "object" && demoFirst.subscribeData != null && typeof demoFirst.uuid === "string" ) ) { Qmsg.error("导入失败,解析的格式不符合", { consoleLogContent: true, }); resolve(false); return; } await updateRuleToStorage(data); resolve(true); }); }; // 本地导入 DOMUtils.on($local, "click", (event) => { DOMUtils.preventEvent(event); $alert.close(); const $input = DOMUtils.createElement("input", { type: "file", accept: ".json", }); DOMUtils.on($input, ["propertychange", "input"], () => { if (!$input.files?.length) { return; } const uploadFile = $input.files![0]; const fileReader = new FileReader(); fileReader.onload = () => { importFile(fileReader.result as string); }; fileReader.readAsText(uploadFile, "UTF-8"); }); $input.click(); }); // 网络导入 DOMUtils.on($network, "click", (event) => { DOMUtils.preventEvent(event); $alert.close(); const $prompt = pops.prompt({ title: { text: "网络导入", position: "center", }, content: { text: "", placeholder: "请填写URL", focus: true, }, btn: { close: { enable: true, callback(details) { details.close(); }, }, ok: { text: "导入", callback: async (eventDetails) => { let url = eventDetails.text; if (utils.isNull(url)) { Qmsg.error("请填入完整的url"); return; } let $loading = Qmsg.loading("正在获取配置..."); let response = await httpx.get(url, { allowInterceptConfig: false, }); $loading.close(); if (!response.status) { log.error(response); Qmsg.error("获取配置失败", { consoleLogContent: true }); return; } let flag = await importFile(response.data.responseText); if (!flag) { return; } eventDetails.close(); }, }, cancel: { enable: false, }, }, drag: true, mask: { enable: true, }, width: PanelUISize.info.width, height: "auto", }); const $promptInput = $prompt.$shadowRoot.querySelector("input")!; const $promptOk = $prompt.$shadowRoot.querySelector(".pops-prompt-btn-ok")!; DOMUtils.on($promptInput, ["input", "propertychange"], () => { const value = DOMUtils.val($promptInput); if (value === "") { DOMUtils.attr($promptOk, "disabled", "true"); } else { DOMUtils.removeAttr($promptOk, "disabled"); } }); DOMUtils.onKeyboard($promptInput, "keydown", (keyName, keyValue, otherCodeList) => { if (keyName === "Enter" && otherCodeList.length === 0) { const value = DOMUtils.val($promptInput); if (value !== "") { DOMUtils.emit($promptOk, "click"); } } }); DOMUtils.emit($promptInput, "input"); }); // 剪贴板导入 DOMUtils.on($clipboard, "click", async (event) => { DOMUtils.preventEvent(event); $alert.close(); const clipboardText = await CommonUtil.getClipboardText(); if (clipboardText.trim() === "") { Qmsg.warning("获取到的剪贴板内容为空"); return; } const flag = await importFile(clipboardText); if (!flag) { return; } }); } /** * 导出订阅 */ exportSubscribe(fileName = "rule.json") { const $alert = pops.alert({ title: { text: "请选择导出方式", position: "center", }, content: { text: /*html*/ `
导出订阅
`, html: true, }, btn: { ok: { enable: false }, close: { enable: true, callback(details) { details.close(); }, }, }, drag: true, mask: { enable: true, }, width: PanelUISize.info.width, height: PanelUISize.info.height, style: /*css*/ ` .btn-control{ display: inline-block; margin: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .btn-control:hover{ color: #409eff; border-color: #c6e2ff; background-color: #ecf5ff; } `, }); /** 仅导出规则 */ const $onlyExportRuleList = $alert.$shadowRoot.querySelector( ".btn-control[data-mode='only-export-rule-list']" )!; /** * 导出文件 */ const exportFile = (__fileName__: string, __data__: any) => { const blob = new Blob([JSON.stringify(__data__, null, 4)]); const blobUrl = window.URL.createObjectURL(blob); const $a = document.createElement("a"); $a.href = blobUrl; $a.download = __fileName__; $a.click(); setTimeout(() => { window.URL.revokeObjectURL(blobUrl); }, 1500); }; // 仅导出订阅 DOMUtils.on($onlyExportRuleList, "click", (event) => { DOMUtils.preventEvent(event); try { let allRule = this.getAllSubscribe(); if (allRule.length === 0) { Qmsg.warning("订阅为空,无需导出"); return; } exportFile(fileName, allRule); $alert.close(); } catch (error: any) { Qmsg.error(error.toString(), { consoleLogContent: true }); } }); } } export { RuleSubscribe, type RuleSubscribeConstructOption };