此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/476017/1357292/userscripts-core-library.js
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name userscripts-core-library
// @version 0.3.0
// @author lucianjp
// @description Core library to handle webpages dom with userscripts from document-start
// ==/UserScript==
// https://gf.qytechs.cn/scripts/476017-userscripts-core-library/code/userscripts-core-library.js
//polyfills
if (typeof GM == 'undefined') {
this.GM = {};
}
class UserJsCore {
constructor() {
throw new Error('UserJsCore cannot be instantiated.');
}
static ready = (callback) =>
document.readyState !== "loading"
? callback()
: document.addEventListener("DOMContentLoaded", callback);
static addStyle = (aCss) => {
let head = document.getElementsByTagName("head")[0];
if (!head) {
console.error("Head element not found. Cannot add style.");
return null;
}
let style = document.createElement("style");
style.setAttribute("type", "text/css");
style.textContent = aCss;
head.appendChild(style);
return style;
};
static observe = (observableCollection, continuous = false) => {
const observables = Array.from(observableCollection.entries()).filter(
([_, observable]) => observable instanceof UserJsCore.ObservableAll || !observable.currentValue
);
const observer = new MutationObserver(function (mutations) {
for (var i = mutations.length - 1; i >= 0; i--) {
const mutation = mutations[i];
const addedNodesLength = mutation.addedNodes.length;
if (addedNodesLength > 0) {
for (var j = addedNodesLength - 1; j >= 0; j--) {
const $node = mutation.addedNodes[j];
if ($node && $node.nodeType === 1) {
let observablesLength = observables.length;
for (let k = observablesLength - 1; k >= 0; k--) {
const [_, observable] = observables[k];
if (observable.test($node)) {
if(observable instanceof UserJsCore.Observable) {
observable.set($node);
const last = observables.pop();
if (k < observablesLength - 1) observables[k] = last;
observablesLength = observablesLength - 1;
}
if(observable instanceof UserJsCore.ObservableAll){
observable.currentValue.includes($node) || observable.add($node);
}
break;
}
}
}
}
if (observables.length === 0 && !continuous) {
observer.disconnect();
return;
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
if (!continuous) UserJsCore.ready(() => observer.disconnect());
return observer;
};
static Observable = class {
constructor(lookup, test) {
this.value = undefined;
this.callbacks = [];
this.lookup = lookup;
this.test = test;
if (typeof lookup === "function") {
this.value = lookup();
}
}
set(newValue) {
this.value = newValue;
this.executeCallbacks(this.value);
}
then(callback) {
if (typeof callback === "function") {
this.callbacks.push(callback);
if (this.value) callback(this.value);
}
return this;
}
executeCallbacks(value) {
this.callbacks.forEach((callback) => callback(value));
}
get currentValue() {
return this.value;
}
};
static ObservableAll = class {
constructor(lookup, test) {
this.values = [];
this.callbacks = [];
this.lookup = lookup;
this.test = test;
if (typeof lookup === "function") {
this.values = [...lookup()];
}
}
add(newValue) {
this.values.push(newValue);
this.executeCallbacks(newValue);
}
then(callback) {
if (typeof callback === "function") {
this.callbacks.push(callback);
if (this.values.length > 0)
this.values.forEach((value) => callback(value));
}
return this;
}
executeCallbacks(value) {
this.callbacks.forEach((callback) => callback(value));
}
get currentValue() {
return this.values;
}
}
static ObservableCollection = class extends Map {
constructor() {
super();
}
add(name, observable) {
this.set(name, observable);
return observable;
}
}
static Config = class {
static #config;
static #isInitializedPromise;
constructor() {
throw new Error('Config cannot be instantiated.');
}
static async init(defaultConfig = {}) {
if (!this.#isInitializedPromise) {
this.#isInitializedPromise = (async () => {
if (!this.#config) {
const storedConfig = await GM.getValue('config', {});
this.#config = { ...defaultConfig, ...storedConfig };
}
})();
}
await this.#isInitializedPromise;
return this; // Return the class instance after initialization
}
static get(key) {
if (!this.#isInitializedPromise) {
throw new Error('Config has not been initialized. Call init() first.');
}
return this.#config[key];
}
static set(key, value) {
if (!this.#isInitializedPromise) {
throw new Error('Config has not been initialized. Call init() first.');
}
this.#config[key] = value;
GM.setValue('config', this.#config);
}
}
static Feature = class {
constructor(id, name, action) {
this._id = id;
this._name = name;
if (this.enabled == null) {
this.enabled = true;
}
if (this._enabled) {
try{
action();
console.groupCollapsed(name)
console.log(`${name} started`)
} catch (error){
console.group(name)
console.error(error);
}
console.groupEnd();
}
}
set id(id) {
this._id = id;
}
get id() {
return this._id;
}
set name(name) {
this._name = name;
}
get name() {
return this._name;
}
set enabled(enabled) {
this._enabled = enabled;
UserJsCore.Config.set(`feature_${this._id}`, this._enabled);
}
get enabled() {
return this._enabled || (this._enabled = UserJsCore.Config.get(`feature_${this._id}`));
}
get displayName() {
return `${this._enabled ? "Disable" : "Enable"} ${this._name}`;
}
toggle() {
this.enabled = !this.enabled;
}
}
static Menu = class {
static #menuIds = [];
static #features;
static #notification;
static initialize(features, notificationChange) {
if(GM.registerMenuCommand === undefined){
throw new Error("UserJsCore.Menu needs the GM.registerMenuCommand granted");
}
if(GM.unregisterMenuCommand === undefined){
throw new Error("UserJsCore.Menu needs the GM.unregisterMenuCommand granted");
}
this.#features = Object.values(features);
this.#notification = notificationChange;
this.#generateMenu();
}
static #generateMenu() {
if (this.#menuIds.length > 0 && this.#notification) {
this.#notification();
}
this.#menuIds.forEach((id) => GM.unregisterMenuCommand(id));
for (const feature of this.#features) {
this.#menuIds.push(
GM.registerMenuCommand(feature.displayName, () => {
feature.toggle();
this.#generateMenu();
})
);
}
}
}
static AsyncQueue = class {
constructor(concurrentLimit = 6) {
this.concurrentLimit = concurrentLimit;
this.runningCount = 0;
this.queue = [];
this.isPaused = false;
}
async enqueueAsync(func, priority = 0) {
return new Promise((resolve, reject) => {
const taskId = Symbol(); // Generate a unique ID for each task
const task = {
id: taskId,
func,
priority,
resolve,
reject,
};
const execute = async (task) => {
if (this.isPaused) {
this.queue.unshift(task);
this.logQueueStatus();
return;
}
this.runningCount++;
this.logQueueStatus();
try {
const result = await task.func();
task.resolve(result);
} catch (error) {
task.reject(error);
} finally {
this.runningCount--;
if (this.queue.length > 0) {
//this.queue.sort((a, b) => b.priority - a.priority);
const nextTask = this.queue.shift();
execute(nextTask);
}
this.logQueueStatus();
}
};
this.logQueueStatus();
if (this.runningCount < this.concurrentLimit) {
execute(task);
} else {
this.queue.push(task);
//this.queue.sort((a, b) => b.priority - a.priority);
}
});
}
cancelTask(taskId) {
const index = this.queue.findIndex((task) => task.id === taskId);
if (index !== -1) {
const [canceledTask] = this.queue.splice(index, 1);
canceledTask.reject(new Error('Task canceled'));
}
}
logQueueStatus() {
//console.log(`Running: ${this.runningCount}, Queued: ${this.queue.length}`);
}
clearQueue() {
this.queue.forEach((task) => task.reject(new Error('Queue cleared')));
this.queue = [];
}
pause() {
this.isPaused = true;
this.logQueueStatus();
}
resume() {
this.isPaused = false;
if (this.queue.length > 0) {
this.queue.sort((a, b) => b.priority - a.priority);
const nextTask = this.queue.shift();
this.enqueueAsync(nextTask.func, nextTask.priority);
}
this.logQueueStatus();
}
}
static Cache = class {
constructor(props = {}) {
this.version = props.version ?? 1;
this.name = props.dbName ?? window.location.origin;
this.storeName = props.storeName ?? 'cache';
this.db = null;
this.concurrentRequests = props.concurrentRequests ?? 6;
this.queue = new UserJsCore.AsyncQueue(this.concurrentRequests);
}
init() {
if(GM.xmlHttpRequest === undefined){
throw new Error("UserJsCore.Cache needs the GM.xmlHttpRequest granted");
}
return new Promise(resolve => {
if(this.db) resolve(this);
const request = indexedDB.open(this.name, this.version);
request.onupgradeneeded = event => {
event.target.result.createObjectStore(this.storeName);
};
request.onsuccess = () => {
this.db = request.result;
this.db.onerror = () => {
console.error('Error creating/accessing db');
};
if (this.db.setVersion && this.db.version !== this.version) {
const version = this.db.setVersion(this.version);
version.onsuccess = () => {
this.db.createObjectStore(this.storeName);
resolve(this);
};
} else {
resolve(this);
}
};
});
}
putImage(key, url) {
return this.queue.enqueueAsync(async () => {
if (!this.db) {
throw new Error('DB not initialized. Call the init method');
}
try {
const blob = await new Promise((resolve, reject) => {
console.log(`requesting : ${url}`)
GM.xmlHttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: (event) => resolve(event.response),
onerror: (e) => reject(e),
});
});
// Check if the blob is a valid image
if (!(blob instanceof Blob) || blob.type.indexOf('image') === -1) {
throw new Error('The response does not contain a valid image.');
}
const transaction = this.db.transaction(this.storeName, 'readwrite');
transaction.objectStore(this.storeName).put(blob, key);
return URL.createObjectURL(blob);
} catch (error) {
console.error(error);
throw error;
}
});
}
getImage(key) {
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('DB not initialized. Call the init method');
}
const transaction = this.db.transaction(this.storeName, 'readonly');
const request = transaction.objectStore(this.storeName).get(key);
request.onsuccess = event => {
const result = event?.target?.result;
if(result)
resolve(URL.createObjectURL(result));
else
resolve();
};
request.onerror = (event) => {
const error = event?.target?.error;
reject(error);
};
});
}
clear() {
return new Promise(resolve => {
if (!this.db)
return reject('DB not initialized. Call the init method');
const transaction = this.db.transaction(this.storeName, "readwrite");
const request = transaction.objectStore(this.storeName).clear();
request.onsuccess = () => {
resolve();
};
});
}
}
};