UNPKG

@yiero/gmlib

Version:

GM Lib for Tampermonkey

736 lines (735 loc) 20.4 kB
/* * @module : @yiero/gmlib * @author : Yiero * @version : 0.1.23 * @description : GM Lib for Tampermonkey * @keywords : tampermonkey, lib, scriptcat, utils * @license : MIT * @repository : git+https://github.com/AliubYiero/GmLib.git */ var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const environmentTest = () => { return GM_info.scriptHandler; }; function getCookie(content, key) { const isTextCookie = [/^\w+=[^=;]+$/, /^\w+=[^=;]+;/].some((reg) => reg.test(content)); if (isTextCookie) { if (!key) { return Promise.reject(new Error(`请输入需要获取的具体 Cookie 的键名.`)); } const cookieList = content.split(/;\s?/).map((cookie) => cookie.split("=")); const cookieMap = new Map(cookieList); const cookieValue = cookieMap.get(key); if (!cookieValue) { return Promise.reject(new Error("获取 Cookie 失败, key 不存在.")); } return Promise.resolve(cookieValue); } return new Promise((resolve, reject) => { const env = environmentTest(); if (env !== "ScriptCat") { reject(`当前脚本不支持 ${env} 环境, 仅支持 ScriptCat .`); } GM_cookie("list", { domain: content }, (cookieList) => { if (!cookieList) { reject(new Error("获取 Cookie 失败, 该域名下没有 cookie. ")); return; } if (!key) { resolve(cookieList); } const userIdCookie = cookieList.find( (cookie) => cookie.name === key ); if (!userIdCookie) { reject(new Error("获取 Cookie 失败, key 不存在. ")); return; } resolve(userIdCookie.value); }); }); } const parseResponseText = (responseText) => { try { return JSON.parse(responseText); } catch (e) { try { const domParser = new DOMParser(); return domParser.parseFromString(responseText, "text/html"); } catch (e2) { return responseText; } } }; function gmRequest(param1, method, body, GMXmlHttpRequestConfig) { const unifiedParameters = () => { if (typeof param1 !== "string") { return { url: param1.url, method: param1.method || "GET", param: param1.method === "POST" ? param1.data : void 0, GMXmlHttpRequestConfig: param1 }; } return { url: param1, method: method || "GET", param: body, GMXmlHttpRequestConfig: GMXmlHttpRequestConfig || {} }; }; const params = unifiedParameters(); if (params.method === "GET" && params.param && typeof params.param === "object") { params.url = `${params.url}?${new URLSearchParams(params.param).toString()}`; } if (params.method === "POST" && params.param) { params.GMXmlHttpRequestConfig.data = JSON.stringify(params.param); } return new Promise((resolve, reject) => { const newGMXmlHttpRequestConfig = { // 默认20s的超时等待 timeout: 2e4, // 请求地址, 请求方法和请求返回 url: params.url, method: params.method, onload(response) { resolve(parseResponseText(response.responseText)); }, onerror(error) { reject(error); }, ontimeout() { reject(new Error("Request timed out")); }, headers: { "Content-Type": "application/json" }, // 用户自定义的油猴配置项 ...params.GMXmlHttpRequestConfig }; GM_xmlhttpRequest(newGMXmlHttpRequestConfig); }); } const hookXhr = (hookUrl, callback) => { const xhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function() { const xhr = this; if (hookUrl(arguments[1])) { const getter = Object.getOwnPropertyDescriptor( XMLHttpRequest.prototype, "responseText" ).get; Object.defineProperty(xhr, "responseText", { get: () => { const responseText = getter.call(xhr); return callback(parseResponseText(responseText), arguments[1]) || responseText; } }); } return xhrOpen.apply(xhr, arguments); }; }; const gmDownload = (url, filename, details = {}) => { return new Promise((resolve, reject) => { const abortHandle = GM_download({ url, name: filename, ...details, onload() { details.onload && details.onload(); resolve(true); }, onerror(err) { details.onerror && details.onerror(err); reject(err.error); }, ontimeout() { details.ontimeout && details.ontimeout(); reject("time_out"); }, onprogress(response) { details.onprogress && details.onprogress(response, abortHandle); } }); }); }; gmDownload.blob = async (blob, filename, details = {}) => { const url = URL.createObjectURL(blob); return gmDownload(url, filename, details).then((res) => { URL.revokeObjectURL(url); return res; }); }; gmDownload.text = (content, filename, mimeType = "text/plain", details = {}) => { const blob = new Blob([content], { type: mimeType }); return gmDownload.blob(blob, filename, details); }; function scroll(targetElement, container = window, scrollPercent = 0.5) { if (!targetElement || typeof targetElement === "number") { scrollPercent = targetElement || 0.5; const yOffset2 = Math.round(document.body.clientHeight * scrollPercent); window.scrollTo({ top: yOffset2, behavior: "smooth" }); return; } let containerTop = 0; let containerHeight = document.body.clientHeight; if (container.getBoundingClientRect) { const rect = container.getBoundingClientRect(); containerTop = rect.top; containerHeight = rect.height; } const { top: targetTop } = targetElement.getBoundingClientRect(); const yOffset = targetTop - containerTop - Math.round(containerHeight * scrollPercent); container.scrollBy({ top: yOffset, behavior: "smooth" }); } const returnElement = (selector, options, resolve, reject) => { setTimeout(() => { const element = options.parent.querySelector(selector); if (!element) { reject(new Error("Void Element")); return; } resolve(element); }, options.delayPerSecond * 1e3); }; const getElementByTimer = (selector, options, resolve, reject) => { const intervalDelay = 100; let intervalCounter = 0; const maxIntervalCounter = Math.ceil(options.timeoutPerSecond * 1e3 / intervalDelay); const timer = window.setInterval(() => { if (++intervalCounter > maxIntervalCounter) { clearInterval(timer); returnElement(selector, options, resolve, reject); return; } const element = options.parent.querySelector(selector); if (element) { clearInterval(timer); returnElement(selector, options, resolve, reject); } }, intervalDelay); }; const getElementByMutationObserver = (selector, options, resolve, reject) => { const timer = options.timeoutPerSecond && window.setTimeout(() => { observer.disconnect(); returnElement(selector, options, resolve, reject); }, options.timeoutPerSecond * 1e3); const observeElementCallback = (mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((addNode) => { if (addNode.nodeType !== Node.ELEMENT_NODE) { return; } const addedElement = addNode; const element = addedElement.matches(selector) ? addedElement : addedElement.querySelector(selector); if (element) { timer && clearTimeout(timer); returnElement(selector, options, resolve, reject); } }); }); }; const observer = new MutationObserver(observeElementCallback); observer.observe(options.parent, { subtree: true, childList: true }); return true; }; function elementWaiter(selector, options) { const elementWaiterOptions = { parent: document, timeoutPerSecond: 20, delayPerSecond: 0.5, ...options }; return new Promise((resolve, reject) => { const targetElement = elementWaiterOptions.parent.querySelector(selector); if (targetElement) { returnElement(selector, elementWaiterOptions, resolve, reject); return; } if (MutationObserver) { getElementByMutationObserver(selector, elementWaiterOptions, resolve, reject); return; } getElementByTimer(selector, elementWaiterOptions, resolve, reject); }); } let messageContainer = null; const messageTypes = { success: { backgroundColor: "#f0f9eb", borderColor: "#e1f3d8", textColor: "#67c23a", icon: "✓" }, warning: { backgroundColor: "#fdf6ec", borderColor: "#faecd8", textColor: "#e6a23c", icon: "⚠" }, error: { backgroundColor: "#fef0f0", borderColor: "#fde2e2", textColor: "#f56c6c", icon: "✕" }, info: { backgroundColor: "#edf2fc", borderColor: "#e4e7ed", textColor: "#909399", icon: "i" } }; const messagePositions = { "top": { top: "20px" }, "top-left": { top: "20px", left: "20px" }, "top-right": { top: "20px", right: "20px" }, "left": { left: "20px" }, "right": { right: "20px" }, "bottom": { bottom: "20px" }, "bottom-left": { bottom: "20px", left: "20px" }, "bottom-right": { bottom: "20px", right: "20px" } }; function createMessageContainer() { if (!messageContainer) { messageContainer = document.createElement("div"); messageContainer.setAttribute("style", ` position: fixed; z-index: 9999999999; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; display: flex; justify-content: center; align-items: center; width: 100vw; `); document.body.appendChild(messageContainer); } return messageContainer; } function Message(options) { const detail = { type: "info", duration: 3e3, position: "top", message: "" }; if (typeof options === "string") { detail.message = options; } else { Object.assign(detail, options); } messageContainer = createMessageContainer(); const messageEl = document.createElement("div"); const typeConfig = messageTypes[detail.type] || messageTypes.info; messageEl.setAttribute("style", ` position: absolute; min-width: 300px; max-width: 500px; padding: 15px 20px; border-radius: 8px; transform: translateY(-20px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); background-color: ${typeConfig.backgroundColor}; border: 1px solid ${typeConfig.borderColor}; color: ${typeConfig.textColor}; display: flex; align-items: center; transition: all 0.3s ease; opacity: 0; pointer-events: auto; cursor: pointer; ${Object.entries(messagePositions[detail.position || "top"]).map(([k, v]) => `${k}: ${v};`).join(" ")} `); const iconEl = document.createElement("span"); iconEl.setAttribute("style", ` display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-right: 12px; font-size: 16px; font-weight: bold; `); iconEl.textContent = typeConfig.icon; messageEl.appendChild(iconEl); const contentEl = document.createElement("span"); contentEl.setAttribute("style", ` flex: 1; font-size: 14px; line-height: 1.5; `); contentEl.textContent = detail.message; messageEl.appendChild(contentEl); messageContainer.appendChild(messageEl); setTimeout(() => { messageEl.style.opacity = "1"; messageEl.style.transform = "translateY(0)"; }, 10); let timer = setTimeout(() => { closeMessage(messageEl); }, detail.duration); messageEl.addEventListener("click", () => { clearTimeout(timer); closeMessage(messageEl); }); } function closeMessage(element) { element.style.opacity = "0"; element.style.transform = "translateY(-20px)"; setTimeout(() => { if (element.parentNode) { element.parentNode.removeChild(element); } }, 300); } Message.success = (message, options) => Message({ ...options, message, type: "success" }); Message.warning = (message, options) => Message({ ...options, message, type: "warning" }); Message.error = (message, options) => Message({ ...options, message, type: "error" }); Message.info = (message, options) => Message({ ...options, message, type: "info" }); const _gmMenuCommand = class _gmMenuCommand { constructor() { } /** * 获取一个菜单按钮 */ static get(title) { const commandButton = this.list.find((commandButton2) => commandButton2.title === title); if (!commandButton) { throw new Error("菜单按钮不存在"); } return commandButton; } /** * 创建一个带有状态的菜单按钮 */ static createToggle(details) { this.create(details.active.title, () => { this.toggleActive(details.active.title); this.toggleActive(details.inactive.title); details.active.onClick(); this.render(); }, true).create(details.inactive.title, () => { this.toggleActive(details.active.title); this.toggleActive(details.inactive.title); details.inactive.onClick(); this.render(); }, false); return _gmMenuCommand; } /** * 手动激活一个菜单按钮 */ static click(title) { const commandButton = this.get(title); commandButton.onClick(); return _gmMenuCommand; } /** * 创建一个菜单按钮 */ static create(title, onClick, isActive = true) { if (this.list.some((commandButton) => commandButton.title === title)) { throw new Error("菜单按钮已存在"); } this.list.push({ title, onClick, isActive, id: 0 }); return _gmMenuCommand; } /** * 删除一个菜单按钮 */ static remove(title) { this.list = this.list.filter((commandButton) => commandButton.title !== title); return _gmMenuCommand; } /** * 修改两个菜单按钮的顺序 */ static swap(title1, title2) { const index1 = this.list.findIndex((commandButton) => commandButton.title === title1); const index2 = this.list.findIndex((commandButton) => commandButton.title === title2); if (index1 === -1 || index2 === -1) { throw new Error("菜单按钮不存在"); } [this.list[index1], this.list[index2]] = [this.list[index2], this.list[index1]]; return _gmMenuCommand; } /** * 修改一个菜单按钮 */ static modify(title, details) { const commandButton = this.get(title); details.onClick && (commandButton.onClick = details.onClick); details.isActive && (commandButton.isActive = details.isActive); return _gmMenuCommand; } /** * 切换菜单按钮激活状态 */ static toggleActive(title) { const commandButton = this.get(title); commandButton.isActive = !commandButton.isActive; return _gmMenuCommand; } /** * 渲染所有激活的菜单按钮 */ static render() { this.list.forEach((commandButton) => { GM_unregisterMenuCommand(commandButton.id); if (commandButton.isActive) { commandButton.id = GM_registerMenuCommand(commandButton.title, commandButton.onClick); } }); } }; /** * 菜单按钮列表 * */ __publicField(_gmMenuCommand, "list", []); let gmMenuCommand = _gmMenuCommand; const isIframe = () => { return Boolean( window.frameElement && window.frameElement.tagName === "IFRAME" || window !== window.top ); }; const uiImporter = (htmlContent, cssContent, options) => { const { isAppendCssToDocument = true, isAppendHtmlToDocument = true, appendHtmlContainer = window.document.body, isFilterScriptNode = true } = options || {}; const styleNode = cssContent && isAppendCssToDocument && GM_addStyle(cssContent) || void 0; const domParser = new DOMParser(); const uiDoc = domParser.parseFromString(htmlContent, "text/html"); const documentFragment = new DocumentFragment(); let nodeList = Array.from(uiDoc.body.children); if (isFilterScriptNode) { nodeList = nodeList.filter((node) => node.nodeName !== "SCRIPT"); } documentFragment.append(...nodeList); isAppendHtmlToDocument && appendHtmlContainer.append(documentFragment); return { styleNode, appendNodeList: nodeList }; }; class GmStorage { constructor(key, defaultValue) { __publicField(this, "listenerId", 0); this.key = key; this.defaultValue = defaultValue; this.key = key; this.defaultValue = defaultValue; } /** * 获取当前存储的值 * * @alias get() */ get value() { return this.get(); } /** * 获取当前存储的值 */ get() { return GM_getValue(this.key, this.defaultValue); } /** * 给当前存储设置一个新值 */ set(value) { return GM_setValue(this.key, value); } /** * 移除当前键 */ remove() { GM_deleteValue(this.key); } /** * 监听元素更新, 同时只能存在 1 个监听器 */ updateListener(callback) { this.removeListener(); this.listenerId = GM_addValueChangeListener(this.key, (key, oldValue, newValue, remote) => { callback({ key, oldValue, newValue, remote }); }); } /** * 移除元素更新回调 */ removeListener() { GM_removeValueChangeListener(this.listenerId); } } class GmArrayStorage extends GmStorage { constructor(key, defaultValue = []) { super(key, defaultValue); this.key = key; this.defaultValue = defaultValue; this.checkIsArray(defaultValue); } /** * 获取数组长度 */ get length() { return this.value.length; } /** * 获取数组最后一个项 */ get lastItem() { const list = this.value; if (!list.length) { list.push(...this.defaultValue); } return list[list.length - 1]; } /** * 设置值, 有类型检查 */ set(value) { this.checkIsArray(value); super.set(value); } /** * 基于索引修改数组项 */ modify(value, index) { const list = this.value; list[index] = value; this.set(list); } /** * 清空储存, 将其变更为默认值 */ reset() { this.set(this.defaultValue); } /** * 基于索引删除数组项 */ delete(index) { this.filter((_, i) => i !== index); } /** * 向数组的最后添加项 */ push(value) { const list = this.value; list.push(value); this.set(list); } /** * 删除数组的最后一个元素 */ pop() { const list = this.value; list.pop(); this.set(list); } /** * 向数组的最开始添加项 */ unshift(value) { const list = this.value; list.unshift(value); this.set(list); } /** * 删除数组的第一个元素 */ shift() { const list = this.value; list.shift(); this.set(list); } /** * 遍历数组 */ forEach(callback) { this.value.forEach(callback); } /** * 覆盖数组 */ map(callback) { const list = this.value; const newList = list.map(callback); this.set(newList); } /** * 过滤数组 */ filter(callback) { const list = this.value; const newList = list.filter(callback); this.set(newList); } /** * 校验输入的值是否为数组 * * @throws TypeError */ checkIsArray(value) { if (!Array.isArray(value)) { throw new TypeError("Init Default Value Cannot Be NonArray"); } } } export { GmArrayStorage, GmStorage, Message, elementWaiter, environmentTest, getCookie, gmDownload, gmMenuCommand, gmRequest, hookXhr, isIframe, scroll, uiImporter };