@yiero/gmlib
Version:
GM Lib for Tampermonkey
736 lines (735 loc) • 20.4 kB
JavaScript
/*
* @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
};