Pixiv Previewer L

Original project: https://github.com/Ocrosoft/PixivPreviewer.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name                Pixiv Previewer L
// @namespace           https://github.com/LolipopJ/PixivPreviewer
// @version             1.4.3-20260407
// @description         Original project: https://github.com/Ocrosoft/PixivPreviewer.
// @author              Ocrosoft, LolipopJ
// @license             GPL-3.0
// @supportURL          https://github.com/LolipopJ/PixivPreviewer
// @match               *://www.pixiv.net/*
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_registerMenuCommand
// @grant               GM_unregisterMenuCommand
// @grant               GM.xmlHttpRequest
// @icon                https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://www.pixiv.net
// @icon64              https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=64&url=https://www.pixiv.net
// @require             https://update.greasyfork.org/scripts/515994/1478507/gh_2215_make_GM_xhr_more_parallel_again.js
// @require             https://code.jquery.com/jquery-4.0.0.min.js
// @require             https://code.jquery.com/jquery-migrate-4.0.2.min.js
// @run-at              document-end
// ==/UserScript==

//#region src/enums/index.ts
let LogLevel = /* @__PURE__ */ function(LogLevel) {
	LogLevel[LogLevel["None"] = 0] = "None";
	LogLevel[LogLevel["Error"] = 1] = "Error";
	LogLevel[LogLevel["Warning"] = 2] = "Warning";
	LogLevel[LogLevel["Info"] = 3] = "Info";
	LogLevel[LogLevel["Elements"] = 4] = "Elements";
	return LogLevel;
}({});
let IllustType = /* @__PURE__ */ function(IllustType) {
	/** 插画 */
	IllustType[IllustType["ILLUST"] = 0] = "ILLUST";
	/** 漫画 */
	IllustType[IllustType["MANGA"] = 1] = "MANGA";
	/** 动图 */
	IllustType[IllustType["UGOIRA"] = 2] = "UGOIRA";
	return IllustType;
}({});
let AiType = /* @__PURE__ */ function(AiType) {
	/** 非 AI 生成 */
	AiType[AiType["NONE_AI"] = 1] = "NONE_AI";
	/** AI 生成 */
	AiType[AiType["AI"] = 2] = "AI";
	return AiType;
}({});
let PageType = /* @__PURE__ */ function(PageType) {
	PageType[PageType["Search"] = 0] = "Search";
	PageType[PageType["BookMarkNew"] = 1] = "BookMarkNew";
	PageType[PageType["Discovery"] = 2] = "Discovery";
	PageType[PageType["Member"] = 3] = "Member";
	PageType[PageType["Home"] = 4] = "Home";
	PageType[PageType["Ranking"] = 5] = "Ranking";
	PageType[PageType["NewIllust"] = 6] = "NewIllust";
	PageType[PageType["R18"] = 7] = "R18";
	PageType[PageType["Stacc"] = 8] = "Stacc";
	PageType[PageType["Artwork"] = 9] = "Artwork";
	PageType[PageType["NovelSearch"] = 10] = "NovelSearch";
	PageType[PageType["SearchTop"] = 11] = "SearchTop";
	return PageType;
}({});
/** 插画(漫画)作品排序类型 */
let IllustSortType = /* @__PURE__ */ function(IllustSortType) {
	/** @link https://www.pixiv.net/tags/%E5%A4%A9%E7%AB%A5%E3%82%A2%E3%83%AA%E3%82%B9/artworks */
	IllustSortType[IllustSortType["TAG_ARTWORK"] = 0] = "TAG_ARTWORK";
	/** @link https://www.pixiv.net/tags/%E5%A4%A9%E7%AB%A5%E3%82%A2%E3%83%AA%E3%82%B9/illustrations */
	IllustSortType[IllustSortType["TAG_ILLUST"] = 1] = "TAG_ILLUST";
	/** @link https://www.pixiv.net/tags/%E5%A4%A9%E7%AB%A5%E3%82%A2%E3%83%AA%E3%82%B9/manga */
	IllustSortType[IllustSortType["TAG_MANGA"] = 2] = "TAG_MANGA";
	/** @link https://www.pixiv.net/search?q=%E5%A4%A9%E7%AB%A5%E3%82%A2%E3%83%AA%E3%82%B9&s_mode=tag&type=illust_ugoira */
	IllustSortType[IllustSortType["SEARCH_ILLUST"] = 3] = "SEARCH_ILLUST";
	/** @link https://www.pixiv.net/search?q=%E5%A4%A9%E7%AB%A5%E3%82%A2%E3%83%AA%E3%82%B9&s_mode=tag&type=manga */
	IllustSortType[IllustSortType["SEARCH_MANGA"] = 4] = "SEARCH_MANGA";
	/** @link https://www.pixiv.net/bookmark_new_illust.php */
	IllustSortType[IllustSortType["BOOKMARK_NEW"] = 5] = "BOOKMARK_NEW";
	/** @link https://www.pixiv.net/bookmark_new_illust_r18.php */
	IllustSortType[IllustSortType["BOOKMARK_NEW_R18"] = 6] = "BOOKMARK_NEW_R18";
	/** @link https://www.pixiv.net/users/333556/artworks */
	IllustSortType[IllustSortType["USER_ARTWORK"] = 7] = "USER_ARTWORK";
	/** @link https://www.pixiv.net/users/333556/illustrations */
	IllustSortType[IllustSortType["USER_ILLUST"] = 8] = "USER_ILLUST";
	/** @link https://www.pixiv.net/users/49906039/manga */
	IllustSortType[IllustSortType["USER_MANGA"] = 9] = "USER_MANGA";
	/** @link https://www.pixiv.net/users/17435436/bookmarks/artworks */
	IllustSortType[IllustSortType["USER_BOOKMARK"] = 10] = "USER_BOOKMARK";
	return IllustSortType;
}({});
/** 作品排序顺序 */
let IllustSortOrder = /* @__PURE__ */ function(IllustSortOrder) {
	/** 按收藏数 */
	IllustSortOrder[IllustSortOrder["BY_BOOKMARK_COUNT"] = 0] = "BY_BOOKMARK_COUNT";
	/** 按发布日期 */
	IllustSortOrder[IllustSortOrder["BY_DATE"] = 1] = "BY_DATE";
	return IllustSortOrder;
}({});

//#endregion
//#region src/constants/index.ts
/** 版本号,发生改变时将会弹窗 */
const g_version = "1.4.3";
/** 默认设置 */
const g_defaultSettings = {
	enablePreview: true,
	enableAnimePreview: true,
	previewDelay: 300,
	pageCount: 2,
	favFilter: 500,
	orderType: IllustSortOrder.BY_BOOKMARK_COUNT,
	aiFilter: false,
	aiAssistedFilter: false,
	hideFavorite: true,
	hideByTag: false,
	hideByTagList: "",
	linkBlank: true,
	version: g_version
};
/** 加载中占位图片 */
const g_loadingImage = "https://pp-1252089172.cos.ap-chengdu.myqcloud.com/loading.gif";
/** 工具栏 ID */
const TOOLBAR_ID = "pp-toolbar";
/** 排序按钮 ID */
const SORT_BUTTON_ID = "pp-sort";
/** 排序事件名称 */
const SORT_EVENT_NAME = "PIXIV_PREVIEWER_RUN_SORT";
/** 下一页按钮 ID */
const SORT_NEXT_PAGE_BUTTON_ID = "pp-sort-next-page";
/** 下一页事件名称 */
const SORT_NEXT_PAGE_EVENT_NAME = "PIXIV_PREVIEWER_JUMP_TO_NEXT_PAGE";
/** 隐藏已收藏作品按钮 */
const HIDE_FAVORITES_BUTTON_ID = "pp-hide-favorites";
/** AI 辅助标签列表,全小写 */
const AI_ASSISTED_TAGS = [
	"aiイラスト",
	"ai-generated",
	"ai-assisted",
	"ai-shoujo",
	"ai生成",
	"ai輔助",
	"ai辅助",
	"ai加筆",
	"ai加笔"
];

//#endregion
//#region src/utils/logger.ts
var ILog = class {
	prefix = "%c Pixiv Preview";
	v(...values) {
		console.log(this.prefix + " [VERBOSE] ", "color:#333 ;background-color: #fff", ...values);
	}
	i(...infos) {
		console.info(this.prefix + " [INFO] ", "color:#333 ;background-color: #fff;", ...infos);
	}
	w(...warnings) {
		console.warn(this.prefix + " [WARNING] ", "color:#111 ;background-color:#ffa500;", ...warnings);
	}
	e(...errors) {
		console.error(this.prefix + " [ERROR] ", "color:#111 ;background-color:#ff0000;", ...errors);
	}
	d(...data) {
		console.log(this.prefix + " [DATA] ", "color:#333 ;background-color: #fff;", ...data);
	}
};
const iLog = new ILog();
function DoLog(level = LogLevel.Info, ...msgOrElement) {
	switch (level) {
		case LogLevel.Error:
			iLog.e(...msgOrElement);
			break;
		case LogLevel.Warning:
			iLog.w(...msgOrElement);
			break;
		case LogLevel.Info:
			iLog.i(...msgOrElement);
			break;
		case LogLevel.Elements:
		case LogLevel.None:
		default: iLog.v(...msgOrElement);
	}
}

//#endregion
//#region src/databases/index.ts
const INDEX_DB_NAME = "PIXIV_PREVIEWER_L";
const INDEX_DB_VERSION = 1;
const ILLUSTRATION_DETAILS_CACHE_TABLE_KEY = "illustrationDetailsCache";
/** 缓存过期时间 */
const ILLUSTRATION_DETAILS_CACHE_TIME = 1e3 * 60 * 60 * 6;
/** 新作品发布初期不添加缓存 */
const NEW_ILLUSTRATION_NOT_CACHE_TIME = 1e3 * 60 * 60 * 1;
const request$1 = indexedDB.open(INDEX_DB_NAME, INDEX_DB_VERSION);
let db;
request$1.onupgradeneeded = (event) => {
	event.target.result.createObjectStore(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY, { keyPath: "id" });
};
request$1.onsuccess = (event) => {
	db = event.target.result;
	console.log("Open IndexedDB successfully:", db);
	deleteExpiredIllustrationDetails();
};
request$1.onerror = (event) => {
	iLog.e(`An error occurred while requesting IndexedDB`, event);
};
const cacheIllustrationDetails = (illustrations, now = /* @__PURE__ */ new Date()) => {
	return new Promise(() => {
		const cachedIllustrationDetailsObjectStore = db.transaction(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY, "readwrite").objectStore(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY);
		illustrations.forEach((illustration) => {
			const uploadTimestamp = illustration.uploadTimestamp * 1e3;
			if (now.getTime() - uploadTimestamp > NEW_ILLUSTRATION_NOT_CACHE_TIME) {
				const illustrationDetails = {
					...illustration,
					cacheDate: now
				};
				const addCachedIllustrationDetailsRequest = cachedIllustrationDetailsObjectStore.put(illustrationDetails);
				addCachedIllustrationDetailsRequest.onerror = (event) => {
					iLog.e(`An error occurred while caching illustration details`, event);
				};
			}
		});
	});
};
const getCachedIllustrationDetails = (id, now = /* @__PURE__ */ new Date()) => {
	return new Promise((resolve) => {
		const cachedIllustrationDetailsObjectStore = db.transaction(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY, "readwrite").objectStore(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY);
		const getCachedIllustrationDetailsRequest = cachedIllustrationDetailsObjectStore.get(id);
		getCachedIllustrationDetailsRequest.onsuccess = (event) => {
			const illustrationDetails = event.target.result;
			if (illustrationDetails) {
				const { cacheDate } = illustrationDetails;
				if (now.getTime() - cacheDate.getTime() <= ILLUSTRATION_DETAILS_CACHE_TIME) resolve(illustrationDetails);
				else cachedIllustrationDetailsObjectStore.delete(id).onerror = (event) => {
					iLog.e(`An error occurred while deleting outdated illustration details`, event);
				};
			}
			resolve(null);
		};
		getCachedIllustrationDetailsRequest.onerror = (event) => {
			iLog.e(`An error occurred while getting cached illustration details`, event);
			resolve(null);
		};
	});
};
const deleteCachedIllustrationDetails = (ids) => {
	return new Promise((resolve) => {
		const cachedIllustrationDetailsObjectStore = db.transaction(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY, "readwrite").objectStore(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY);
		for (const id of ids) {
			const deleteCachedIllustrationDetailsRequest = cachedIllustrationDetailsObjectStore.delete(id);
			deleteCachedIllustrationDetailsRequest.onsuccess = () => {
				resolve();
			};
			deleteCachedIllustrationDetailsRequest.onerror = (event) => {
				iLog.w(`An error occurred while deleting cached details of illustration ${id}`, event);
				resolve();
			};
		}
	});
};
/** 移除过期的缓存 */
function deleteExpiredIllustrationDetails() {
	return new Promise((resolve) => {
		const now = (/* @__PURE__ */ new Date()).getTime();
		const cachedIllustrationDetailsObjectStore = db.transaction(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY, "readwrite").objectStore(ILLUSTRATION_DETAILS_CACHE_TABLE_KEY);
		const getAllRequest = cachedIllustrationDetailsObjectStore.getAll();
		getAllRequest.onsuccess = (event) => {
			event.target.result.forEach((entry) => {
				if (now - entry.cacheDate.getTime() > ILLUSTRATION_DETAILS_CACHE_TIME) cachedIllustrationDetailsObjectStore.delete(entry.id);
			});
			resolve();
		};
	});
}

//#endregion
//#region src/features/hide-favorites.ts
let isHidden = false;
const hideFavorites = () => {
	$("svg").filter(function() {
		return $(this).css("color") === "rgb(255, 64, 96)";
	}).each(function() {
		const listItem = $(this).closest("li, div.col-span-2");
		listItem.hide();
		listItem.attr("data-pp-fav-hidden", "true");
	});
	isHidden = true;
};

//#endregion
//#region src/icons/download.svg
var download_default = "<svg t=\"1742281193586\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"\n  p-id=\"24408\" width=\"10\" height=\"10\">\n  <path\n    d=\"M1024 896v128H0v-320h128v192h768v-192h128v192zM576 554.688L810.688 320 896 405.312l-384 384-384-384L213.312 320 448 554.688V0h128v554.688z\"\n    fill=\"#ffffff\" p-id=\"24409\"></path>\n</svg>";

//#endregion
//#region src/icons/loading.svg
var loading_default = "<svg t=\"1742282291278\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"\n  p-id=\"38665\" width=\"48\" height=\"48\">\n  <path\n    d=\"M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3 0.1 19.9-16 36-35.9 36z\"\n    p-id=\"38666\" fill=\"#1296db\"></path>\n</svg>";

//#endregion
//#region src/icons/page.svg
var page_default = "<svg viewBox=\"0 0 10 10\" width=\"10\" height=\"10\">\n  <path\n    d=\"M 8 3 C 8.55228 3 9 3.44772 9 4 L 9 9 C 9 9.55228 8.55228 10 8 10 L 3 10 C 2.44772 10 2 9.55228 2 9 L 6 9 C 7.10457 9 8 8.10457 8 7 L 8 3 Z M 1 1 L 6 1 C 6.55228 1 7 1.44772 7 2 L 7 7 C 7 7.55228 6.55228 8 6 8 L 1 8 C 0.447715 8 0 7.55228 0 7 L 0 2 C 0 1.44772 0.447715 1 1 1 Z\"\n    fill=\"#ffffff\"></path>\n</svg>";

//#endregion
//#region src/utils/utils.ts
const pause = (ms) => {
	return new Promise((resolve) => setTimeout(resolve, ms));
};
const convertObjectKeysFromSnakeToCamel = (obj) => {
	function snakeToCamel(snake) {
		return snake.replace(/_([a-z])/g, (result) => result[1].toUpperCase());
	}
	const newResponse = {};
	for (const key in obj) newResponse[snakeToCamel(key)] = obj[key];
	return newResponse;
};

//#endregion
//#region src/services/request.ts
const xmlHttpRequest = window.GM.xmlHttpRequest;
const request = (options) => {
	const { headers, ...restOptions } = options;
	return xmlHttpRequest({
		responseType: "json",
		...restOptions,
		headers: {
			referer: "https://www.pixiv.net/",
			...headers
		}
	});
};
const requestWithRetry = async (options) => {
	const { retryDelay = 1e4, maxRetryTimes = Infinity, onRetry, ...restOptions } = options;
	let response;
	let retryTimes = 0;
	while (retryTimes < maxRetryTimes) {
		response = await request(restOptions);
		if (response.status === 200) {
			if (!response.response.error) return response;
		}
		retryTimes += 1;
		onRetry?.(response, retryTimes);
		await pause(retryDelay);
	}
	throw new Error(`Request for ${restOptions.url} failed: ${response.responseText}`);
};

//#endregion
//#region src/services/download.ts
const downloadFile = (url, filename, options = {}) => {
	const { onload, onerror, ...restOptions } = options;
	request({
		...restOptions,
		url,
		method: "GET",
		responseType: "blob",
		onload: function(resp) {
			onload?.call(this, resp);
			const blob = new Blob([resp.response], { type: resp.responseType });
			const blobUrl = URL.createObjectURL(blob);
			const a = document.createElement("a");
			a.href = blobUrl;
			a.download = filename;
			document.body.appendChild(a);
			a.click();
			document.body.removeChild(a);
			URL.revokeObjectURL(blobUrl);
		},
		onerror: function(resp) {
			onerror?.call(this, resp);
			iLog.e(`Download ${filename} from ${url} failed: ${resp.responseText}`);
		}
	});
};

//#endregion
//#region src/services/illustration.ts
/** 从 IndexedDB 或接口获取指定作品的详细信息 */
const getIllustrationDetailsWithCache = async (id, retry = false) => {
	let illustDetails = await getCachedIllustrationDetails(id);
	if (illustDetails) iLog.d(`Use cached details of illustration ${id}`, illustDetails);
	else {
		const requestUrl = `/touch/ajax/illust/details?illust_id=${id}`;
		const getIllustDetailsRes = retry ? await requestWithRetry({
			url: requestUrl,
			onRetry: (response, retryTimes) => {
				iLog.w(`Get illustration details via api \`${requestUrl}\` failed:`, response, `${retryTimes} times retrying...`);
			}
		}) : await request({ url: requestUrl });
		if (getIllustDetailsRes.status === 200) {
			illustDetails = convertObjectKeysFromSnakeToCamel(getIllustDetailsRes.response.body.illust_details);
			cacheIllustrationDetails([illustDetails]);
		} else illustDetails = null;
	}
	return illustDetails;
};
/** 获取用户发布的所有作品,按照发布时间倒叙 */
const getUserIllustrations = async (userId) => {
	const responseData = (await request({ url: `/ajax/user/${userId}/profile/all?sensitiveFilterMode=userSetting&lang=zh` })).response.body;
	const illusts = Object.keys(responseData.illusts).reverse();
	const manga = Object.keys(responseData.manga).reverse();
	return {
		illusts,
		manga,
		artworks: [...illusts, ...manga].sort((a, b) => Number(b) - Number(a))
	};
};
/** 从 Session Storage 或接口获取指定用户的作品列表 */
const getUserIllustrationsWithCache = async (userId, { onRequesting } = {}) => {
	let userIllustrations = {
		illusts: [],
		manga: [],
		artworks: []
	};
	const userIllustrationsCacheKey = `PIXIV_PREVIEWER_CACHED_ARTWORKS_OF_USER_${userId}`;
	try {
		const userIllustrationsCacheString = sessionStorage.getItem(userIllustrationsCacheKey);
		if (!userIllustrationsCacheString) throw new Error("Illustrations cache not existed.");
		userIllustrations = JSON.parse(userIllustrationsCacheString);
	} catch (error) {
		iLog.i(`Get illustrations of current user from session storage failed, re-getting...`, error);
		onRequesting?.();
		userIllustrations = await getUserIllustrations(userId);
		sessionStorage.setItem(userIllustrationsCacheKey, JSON.stringify(userIllustrations));
	}
	return userIllustrations;
};

//#endregion
//#region src/services/preview.ts
/** 下载作品 */
const downloadIllust = ({ url, filename, options = {} }) => {
	downloadFile(url, filename, {
		...options,
		onerror: function(resp) {
			options.onerror?.call(this, resp);
			window.open(url, "__blank");
		}
	});
};
/** 获取图片分页信息和访问链接 */
const getIllustPagesRequestUrl = (id) => {
	return `/ajax/illust/${id}/pages`;
};
/** 获取动图下载链接的链接 */
const getUgoiraMetadataRequestUrl = (id) => {
	return `/ajax/illust/${id}/ugoira_meta`;
};

//#endregion
//#region src/utils/debounce.ts
function debounce(func, delay = 100) {
	let timeout = null;
	return function(...args) {
		if (timeout) clearTimeout(timeout);
		timeout = setTimeout(() => {
			func(...args);
		}, delay);
	};
}

//#endregion
//#region src/utils/event.ts
const stopEventPropagation = (event) => {
	event.stopPropagation();
};

//#endregion
//#region src/utils/illustration.ts
const checkIsR18 = (tags) => {
	const R18_TAGS = ["r-18", "r18"];
	for (const tag of tags) if (R18_TAGS.includes(tag.toLowerCase())) return true;
	return false;
};
const checkIsUgoira = (illustType) => {
	return illustType === IllustType.UGOIRA;
};
const checkIsAiGenerated = (aiType) => {
	return aiType === AiType.AI;
};
const checkIsAiAssisted = (tags) => {
	for (const tag of tags) if (AI_ASSISTED_TAGS.includes(tag.toLowerCase())) return true;
	return false;
};

//#endregion
//#region src/utils/mouse-monitor.ts
var MouseMonitor = class {
	/** 鼠标相对网页的位置 */
	mousePos = [0, 0];
	/** 鼠标相对视窗的绝对位置 */
	mouseAbsPos = [0, 0];
	constructor() {
		document.addEventListener("mousemove", (mouseMoveEvent) => {
			this.mousePos = [mouseMoveEvent.pageX, mouseMoveEvent.pageY];
			this.mouseAbsPos = [mouseMoveEvent.clientX, mouseMoveEvent.clientY];
		});
	}
};
const mouseMonitor = new MouseMonitor();

//#endregion
//#region src/utils/ugoira-player.ts
function ZipImagePlayer(options) {
	this.op = options;
	this._URL = window.URL || window.webkitURL || window.MozURL || window.MSURL;
	this._Blob = window.Blob || window.WebKitBlob || window.MozBlob || window.MSBlob;
	this._BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
	this._Uint8Array = window.Uint8Array || window.WebKitUint8Array || window.MozUint8Array || window.MSUint8Array;
	this._DataView = window.DataView || window.WebKitDataView || window.MozDataView || window.MSDataView;
	this._ArrayBuffer = window.ArrayBuffer || window.WebKitArrayBuffer || window.MozArrayBuffer || window.MSArrayBuffer;
	this._maxLoadAhead = 0;
	if (!this._URL) {
		this._debugLog("No URL support! Will use slower data: URLs.");
		this._maxLoadAhead = 10;
	}
	if (!this._Blob) this._error("No Blob support");
	if (!this._Uint8Array) this._error("No Uint8Array support");
	if (!this._DataView) this._error("No DataView support");
	if (!this._ArrayBuffer) this._error("No ArrayBuffer support");
	this._isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor") > 0;
	this._loadingState = 0;
	this._dead = false;
	this._context = options.canvas.getContext("2d");
	this._files = {};
	this._frameCount = this.op.metadata.frames.length;
	this._debugLog("Frame count: " + this._frameCount);
	this._frame = 0;
	this._loadFrame = 0;
	this._frameImages = [];
	this._paused = false;
	this._loadTimer = null;
	this._startLoad();
	if (this.op.autoStart) this.play();
	else this._paused = true;
}
ZipImagePlayer.prototype = {
	_trailerBytes: 3e4,
	_failed: false,
	_mkerr: function(msg) {
		const _this = this;
		return function() {
			_this._error(msg);
		};
	},
	_error: function(msg) {
		this._failed = true;
		throw Error("ZipImagePlayer error: " + msg);
	},
	_debugLog: function(msg) {
		if (this.op.debug) console.log(msg);
	},
	_load: function(offset, length, callback) {
		const _this = this;
		const xhr = new XMLHttpRequest();
		xhr.addEventListener("load", function() {
			if (_this._dead) return;
			_this._debugLog("Load: " + offset + " " + length + " status=" + xhr.status);
			if (xhr.status == 200) {
				_this._debugLog("Range disabled or unsupported, complete load");
				offset = 0;
				length = xhr.response.byteLength;
				_this._len = length;
				_this._buf = xhr.response;
				_this._bytes = new _this._Uint8Array(_this._buf);
			} else {
				if (xhr.status != 206) _this._error("Unexpected HTTP status " + xhr.status);
				if (xhr.response.byteLength != length) _this._error("Unexpected length " + xhr.response.byteLength + " (expected " + length + ")");
				_this._bytes.set(new _this._Uint8Array(xhr.response), offset);
			}
			if (callback) callback.apply(_this, [offset, length]);
		}, false);
		xhr.addEventListener("error", this._mkerr("Fetch failed"), false);
		xhr.open("GET", this.op.source);
		xhr.responseType = "arraybuffer";
		if (offset != null && length != null) {
			const end = offset + length;
			xhr.setRequestHeader("Range", "bytes=" + offset + "-" + (end - 1));
			if (this._isSafari) {
				xhr.setRequestHeader("Cache-control", "no-cache");
				xhr.setRequestHeader("If-None-Match", Math.random().toString());
			}
		}
		xhr.send();
	},
	_startLoad: function() {
		const _this = this;
		if (!this.op.source) {
			this._loadNextFrame();
			return;
		}
		$.ajax({
			url: this.op.source,
			type: "HEAD"
		}).done(function(data, status, xhr) {
			if (_this._dead) return;
			_this._pHead = 0;
			_this._pNextHead = 0;
			_this._pFetch = 0;
			const len = parseInt(String(xhr.getResponseHeader("Content-Length")));
			if (!len) {
				_this._debugLog("HEAD request failed: invalid file length.");
				_this._debugLog("Falling back to full file mode.");
				_this._load(null, null, function(off, len) {
					_this._pTail = 0;
					_this._pHead = len;
					_this._findCentralDirectory();
				});
				return;
			}
			_this._debugLog("Len: " + len);
			_this._len = len;
			_this._buf = new _this._ArrayBuffer(len);
			_this._bytes = new _this._Uint8Array(_this._buf);
			let off = len - _this._trailerBytes;
			if (off < 0) off = 0;
			_this._pTail = len;
			_this._load(off, len - off, function(off) {
				_this._pTail = off;
				_this._findCentralDirectory();
			});
		}).fail(this._mkerr("Length fetch failed"));
	},
	_findCentralDirectory: function() {
		const dv = new this._DataView(this._buf, this._len - 22, 22);
		if (dv.getUint32(0, true) != 101010256) this._error("End of Central Directory signature not found");
		const cd_count = dv.getUint16(10, true);
		const cd_size = dv.getUint32(12, true);
		const cd_off = dv.getUint32(16, true);
		if (cd_off < this._pTail) this._load(cd_off, this._pTail - cd_off, function() {
			this._pTail = cd_off;
			this._readCentralDirectory(cd_off, cd_size, cd_count);
		});
		else this._readCentralDirectory(cd_off, cd_size, cd_count);
	},
	_readCentralDirectory: function(offset, size, count) {
		const dv = new this._DataView(this._buf, offset, size);
		let p = 0;
		for (let i = 0; i < count; i++) {
			if (dv.getUint32(p, true) != 33639248) this._error("Invalid Central Directory signature");
			const compMethod = dv.getUint16(p + 10, true);
			const uncompSize = dv.getUint32(p + 24, true);
			const nameLen = dv.getUint16(p + 28, true);
			const extraLen = dv.getUint16(p + 30, true);
			const cmtLen = dv.getUint16(p + 32, true);
			const off = dv.getUint32(p + 42, true);
			if (compMethod != 0) this._error("Unsupported compression method");
			p += 46;
			const nameView = new this._Uint8Array(this._buf, offset + p, nameLen);
			let name = "";
			for (let j = 0; j < nameLen; j++) name += String.fromCharCode(nameView[j]);
			p += nameLen + extraLen + cmtLen;
			this._files[name] = {
				off,
				len: uncompSize
			};
		}
		if (this._pHead >= this._pTail) {
			this._pHead = this._len;
			$(this).triggerHandler("loadProgress", [this._pHead / this._len]);
			this._loadNextFrame();
		} else {
			this._loadNextChunk();
			this._loadNextChunk();
		}
	},
	_loadNextChunk: function() {
		if (this._pFetch >= this._pTail) return;
		const off = this._pFetch;
		let len = this.op.chunkSize;
		if (this._pFetch + len > this._pTail) len = this._pTail - this._pFetch;
		this._pFetch += len;
		this._load(off, len, function() {
			if (off == this._pHead) {
				if (this._pNextHead) {
					this._pHead = this._pNextHead;
					this._pNextHead = 0;
				} else this._pHead = off + len;
				if (this._pHead >= this._pTail) this._pHead = this._len;
				$(this).triggerHandler("loadProgress", [this._pHead / this._len]);
				if (!this._loadTimer) this._loadNextFrame();
			} else this._pNextHead = off + len;
			this._loadNextChunk();
		});
	},
	_fileDataStart: function(offset) {
		const dv = new DataView(this._buf, offset, 30);
		const nameLen = dv.getUint16(26, true);
		const extraLen = dv.getUint16(28, true);
		return offset + 30 + nameLen + extraLen;
	},
	_isFileAvailable: function(name) {
		const info = this._files[name];
		if (!info) this._error("File " + name + " not found in ZIP");
		if (this._pHead < info.off + 30) return false;
		return this._pHead >= this._fileDataStart(info.off) + info.len;
	},
	_loadNextFrame: function() {
		if (this._dead) return;
		const frame = this._loadFrame;
		if (frame >= this._frameCount) return;
		const meta = this.op.metadata.frames[frame];
		if (!this.op.source) {
			this._loadFrame += 1;
			this._loadImage(frame, meta.file, false);
			return;
		}
		if (!this._isFileAvailable(meta.file)) return;
		this._loadFrame += 1;
		const off = this._fileDataStart(this._files[meta.file].off);
		const end = off + this._files[meta.file].len;
		let url;
		const mime_type = this.op.metadata.mime_type || "image/png";
		if (this._URL) {
			let slice;
			if (!this._buf.slice) {
				slice = new this._ArrayBuffer(this._files[meta.file].len);
				new this._Uint8Array(slice).set(this._bytes.subarray(off, end));
			} else slice = this._buf.slice(off, end);
			let blob;
			try {
				blob = new this._Blob([slice], { type: mime_type });
			} catch (err) {
				this._debugLog("Blob constructor failed. Trying BlobBuilder... (" + err.message + ")");
				const bb = new this._BlobBuilder();
				bb.append(slice);
				blob = bb.getBlob();
			}
			url = this._URL.createObjectURL(blob);
			this._loadImage(frame, url, true);
		} else {
			url = "data:" + mime_type + ";base64," + base64ArrayBuffer(this._buf, off, end - off);
			this._loadImage(frame, url, false);
		}
	},
	_loadImage: function(frame, url, isBlob) {
		const _this = this;
		const image = new Image();
		const meta = this.op.metadata.frames[frame];
		image.addEventListener("load", function() {
			_this._debugLog("Loaded " + meta.file + " to frame " + frame);
			if (isBlob) _this._URL.revokeObjectURL(url);
			if (_this._dead) return;
			_this._frameImages[frame] = image;
			$(_this).triggerHandler("frameLoaded", frame);
			if (_this._loadingState == 0) _this._displayFrame.apply(_this);
			if (frame >= _this._frameCount - 1) {
				_this._setLoadingState(2);
				_this._buf = null;
				_this._bytes = null;
			} else if (!_this._maxLoadAhead || frame - _this._frame < _this._maxLoadAhead) _this._loadNextFrame();
			else if (!_this._loadTimer) _this._loadTimer = setTimeout(function() {
				_this._loadTimer = null;
				_this._loadNextFrame();
			}, 200);
		});
		image.src = url;
	},
	_setLoadingState: function(state) {
		if (this._loadingState != state) {
			this._loadingState = state;
			$(this).triggerHandler("loadingStateChanged", [state]);
		}
	},
	_displayFrame: function() {
		if (this._dead) return;
		const _this = this;
		const meta = this.op.metadata.frames[this._frame];
		this._debugLog("Displaying frame: " + this._frame + " " + meta.file);
		const image = this._frameImages[this._frame];
		if (!image) {
			this._debugLog("Image not available!");
			this._setLoadingState(0);
			return;
		}
		if (this._loadingState != 2) this._setLoadingState(1);
		if (this.op.autosize) {
			if (this._context.canvas.width != image.width || this._context.canvas.height != image.height) {
				this._context.canvas.width = image.width;
				this._context.canvas.height = image.height;
			}
		}
		this._context.clearRect(0, 0, this.op.canvas.width, this.op.canvas.height);
		this._context.drawImage(image, 0, 0);
		$(this).triggerHandler("frame", this._frame);
		if (!this._paused) this._timer = setTimeout(function() {
			_this._timer = null;
			_this._nextFrame.apply(_this);
		}, meta.delay);
	},
	_nextFrame: function() {
		if (this._frame >= this._frameCount - 1) if (this.op.loop) this._frame = 0;
		else {
			this.pause();
			return;
		}
		else this._frame += 1;
		this._displayFrame();
	},
	play: function() {
		if (this._dead) return;
		if (this._paused) {
			$(this).triggerHandler("play", [this._frame]);
			this._paused = false;
			this._displayFrame();
		}
	},
	pause: function() {
		if (this._dead) return;
		if (!this._paused) {
			if (this._timer) clearTimeout(this._timer);
			this._paused = true;
			$(this).triggerHandler("pause", [this._frame]);
		}
	},
	rewind: function() {
		if (this._dead) return;
		this._frame = 0;
		if (this._timer) clearTimeout(this._timer);
		this._displayFrame();
	},
	stop: function() {
		this._debugLog("Stopped!");
		this._dead = true;
		if (this._timer) clearTimeout(this._timer);
		if (this._loadTimer) clearTimeout(this._loadTimer);
		this._frameImages = null;
		this._buf = null;
		this._bytes = null;
		$(this).triggerHandler("stop");
	},
	getCurrentFrame: function() {
		return this._frame;
	},
	getLoadedFrames: function() {
		return this._frameImages.length;
	},
	getFrameCount: function() {
		return this._frameCount;
	},
	hasError: function() {
		return this._failed;
	}
};
function base64ArrayBuffer(arrayBuffer, off, byteLength) {
	let base64 = "";
	const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	const bytes = new Uint8Array(arrayBuffer);
	const byteRemainder = byteLength % 3;
	const mainLength = off + byteLength - byteRemainder;
	let a, b, c, d;
	let chunk;
	for (let i = off; i < mainLength; i = i + 3) {
		chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
		a = (chunk & 16515072) >> 18;
		b = (chunk & 258048) >> 12;
		c = (chunk & 4032) >> 6;
		d = chunk & 63;
		base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
	}
	if (byteRemainder == 1) {
		chunk = bytes[mainLength];
		a = (chunk & 252) >> 2;
		b = (chunk & 3) << 4;
		base64 += encodings[a] + encodings[b] + "==";
	} else if (byteRemainder == 2) {
		chunk = bytes[mainLength] << 8 | bytes[mainLength + 1];
		a = (chunk & 64512) >> 10;
		b = (chunk & 1008) >> 4;
		c = (chunk & 15) << 2;
		base64 += encodings[a] + encodings[b] + encodings[c] + "=";
	}
	return base64;
}

//#endregion
//#region src/features/preview.ts
let isInitialized$1 = false;
const loadIllustPreview = (options) => {
	if (isInitialized$1) return;
	const { previewDelay, enableAnimePreview, linkBlank } = options;
	const mouseHoverDebounceWait = previewDelay / 5;
	const mouseHoverPreviewWait = previewDelay - mouseHoverDebounceWait;
	/**
	* 获取作品的元数据信息
	* @param target 查找的 JQuery 对象
	* @returns 作品的元数据
	*/
	const getIllustMetadata = (target) => {
		const imgLink = target.closest("a");
		if (!imgLink.length) return null;
		const illustHrefMatch = imgLink.attr("href")?.match(/\/artworks\/(\d+)(#(\d+))?/);
		if (!illustHrefMatch) return null;
		return {
			illustId: illustHrefMatch[1],
			previewPage: Number(illustHrefMatch[3] ?? 1),
			illustType: imgLink.children("div:first").find("svg:first").length || imgLink.hasClass("ugoku-illust") ? IllustType.UGOIRA : IllustType.ILLUST,
			illustLinkDom: imgLink
		};
	};
	/**
	* 获取作品访问链接并在前端显示预览
	* @param target 作品的元数据
	*/
	const previewIllust = (() => {
		const previewedIllust = new PreviewedIllust();
		let currentHoveredIllustId = "";
		let getIllustPagesRequest = $.ajax();
		const getIllustPagesCache = {};
		const getUgoiraMetadataCache = {};
		return ({ target, illustId, previewPage = 1, illustType }) => {
			getIllustPagesRequest.abort();
			currentHoveredIllustId = illustId;
			if (illustType === IllustType.UGOIRA && !enableAnimePreview) {
				iLog.i("动图预览已禁用,跳过");
				return;
			}
			if ([IllustType.ILLUST, IllustType.MANGA].includes(illustType)) {
				if (getIllustPagesCache[illustId]) {
					previewedIllust.setImage({
						illustId,
						illustElement: target,
						previewPage,
						...getIllustPagesCache[illustId]
					});
					return;
				}
				getIllustPagesRequest = $.ajax(getIllustPagesRequestUrl(illustId), {
					method: "GET",
					success: (data) => {
						if (data.error) {
							iLog.e(`An error occurred while requesting preview urls of illust ${illustId}: ${data.message}`);
							return;
						}
						const urls = data.body.map((item) => item.urls);
						const regularUrls = urls.map((url) => url.regular);
						const originalUrls = urls.map((url) => url.original);
						getIllustPagesCache[illustId] = {
							regularUrls,
							originalUrls
						};
						if (currentHoveredIllustId !== illustId) return;
						previewedIllust.setImage({
							illustId,
							illustElement: target,
							previewPage,
							regularUrls,
							originalUrls
						});
					},
					error: (err) => {
						iLog.e(`An error occurred while requesting preview urls of illust ${illustId}: ${err}`);
					}
				});
			} else if (illustType === IllustType.UGOIRA) {
				if (getUgoiraMetadataCache[illustId]) {
					previewedIllust.setUgoira({
						illustId,
						illustElement: target,
						...getUgoiraMetadataCache[illustId]
					});
					return;
				}
				getIllustPagesRequest = $.ajax(getUgoiraMetadataRequestUrl(illustId), {
					method: "GET",
					success: (data) => {
						if (data.error) {
							iLog.e(`An error occurred while requesting metadata of ugoira ${illustId}: ${data.message}`);
							return;
						}
						getUgoiraMetadataCache[illustId] = data.body;
						if (currentHoveredIllustId !== illustId) return;
						const { src, originalSrc, mime_type, frames } = data.body;
						previewedIllust.setUgoira({
							illustId,
							illustElement: target,
							src,
							originalSrc,
							mime_type,
							frames
						});
					},
					error: (err) => {
						iLog.e(`An error occurred while requesting metadata of ugoira ${illustId}: ${err.responseText}`);
					}
				});
			} else {
				iLog.e("Unknown illust type.");
				return;
			}
		};
	})();
	const onMouseOverIllust = (target) => {
		const { illustId, previewPage, illustType, illustLinkDom } = getIllustMetadata(target) || {};
		if (illustId === void 0 || illustType === void 0) return;
		if (linkBlank) {
			illustLinkDom.attr({
				target: "_blank",
				rel: "external"
			});
			illustLinkDom.off("click", stopEventPropagation);
			illustLinkDom.on("click", stopEventPropagation);
		}
		const previewIllustTimeout = setTimeout(() => {
			previewIllust({
				target,
				illustId,
				previewPage,
				illustType
			});
		}, mouseHoverPreviewWait);
		const onMouseMove = (mouseMoveEvent) => {
			if (mouseMoveEvent.ctrlKey || mouseMoveEvent.metaKey) {
				clearTimeout(previewIllustTimeout);
				target.off("mousemove", onMouseMove);
			}
		};
		target.on("mousemove", onMouseMove);
		const onMouseOut = () => {
			clearTimeout(previewIllustTimeout);
			target.off("mouseout", onMouseOut);
		};
		target.on("mouseout", onMouseOut);
	};
	const onMouseMoveDocument = (() => {
		const debouncedOnMouseOverIllust = debounce(onMouseOverIllust, mouseHoverDebounceWait);
		let prevTarget = null;
		return (mouseMoveEvent) => {
			if (mouseMoveEvent.ctrlKey || mouseMoveEvent.metaKey) return;
			if (mouseMoveEvent.target === prevTarget) return;
			prevTarget = mouseMoveEvent.target;
			debouncedOnMouseOverIllust($(mouseMoveEvent.target));
		};
	})();
	$(document).on("mousemove", onMouseMoveDocument);
	(function inactiveUnexpectedDoms() {
		const styleRules = $("<style>").prop("type", "text/css");
		styleRules.append(`
@keyframes pp-spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}`);
		styleRules.append(`
._layout-thumbnail img + div {
  pointer-events: none;
}`);
		styleRules.appendTo("head");
	})();
	isInitialized$1 = true;
};
const DETAIL_BADGE_CSS = {
	height: "20px",
	"border-radius": "12px",
	color: "rgb(245, 245, 245)",
	background: "rgba(0, 0, 0, 0.32)",
	"font-size": "12px",
	"line-height": "1",
	"font-weight": "bold",
	padding: "3px 6px",
	display: "flex",
	"align-items": "center",
	gap: "4px"
};
var PreviewedIllust = class {
	/** 当前正在预览的作品的 ID */
	illustId = "";
	/** 当前正在预览的作品的详细信息 */
	illustDetails = null;
	/** 当前正在预览的作品 DOM 元素 */
	illustElement = $();
	/** 当前预览的作品是否加载完毕 */
	illustLoaded = false;
	/** 图片的链接 */
	regularUrls = [];
	/** 图片的原图链接 */
	originalUrls = [];
	/** 当前预览图片的页数 */
	currentPage = 1;
	/** 当前预览图片的总页数 */
	pageCount = 1;
	/** 预览图片或动图容器 DOM */
	previewWrapperElement = $();
	/** 预览容器顶部栏 DOM */
	previewWrapperHeader = $();
	/** 当前预览作品的元数据 */
	illustMeta = $();
	/** 当前预览的是第几张图片标记 DOM */
	pageCountElement = $();
	pageCountText = $();
	/** 下载原图按钮 DOM */
	downloadOriginalElement = $();
	/** 预览图片或动图加载状态 DOM */
	previewLoadingElement = $();
	/** 当前预览的图片或动图 DOM */
	previewImageElement = $();
	/** 预加载图片的列表 */
	#images = [];
	/** 下载按钮重置定时器 */
	#downloadResetTimeout = null;
	/** 保存的鼠标位置 */
	#prevMousePos = [0, 0];
	/** 当前预览图片的实际尺寸 */
	#currentIllustSize = [0, 0];
	/** 当前预览的动图播放器 */
	#currentUgoiraPlayer;
	constructor() {
		this.reset();
	}
	/** 初始化预览组件 */
	reset() {
		this.illustId = "";
		this.illustDetails = null;
		this.illustElement = $();
		this.illustLoaded = false;
		this.regularUrls = [];
		this.originalUrls = [];
		this.currentPage = 1;
		this.pageCount = 1;
		this.previewWrapperElement?.remove();
		this.previewWrapperElement = $(document.createElement("div")).attr({ id: "pp-wrapper" }).css({
			position: "fixed",
			"z-index": "999999",
			border: `${2}px solid rgb(0, 150, 250)`,
			"border-radius": `${8}px`,
			background: "rgba(31, 31, 31, 0.8)",
			"backdrop-filter": "blur(4px)",
			"text-align": "center",
			"pointer-events": "none"
		}).hide().appendTo($("body"));
		this.previewWrapperHeader = $(document.createElement("div")).css({
			position: "absolute",
			top: "0px",
			left: "0px",
			right: "0px",
			padding: "5px",
			display: "flex",
			gap: "5px",
			"align-items": "center"
		}).hide().appendTo(this.previewWrapperElement);
		this.illustMeta = $(document.createElement("div")).css({
			display: "flex",
			gap: "5px",
			"align-items": "center",
			"margin-right": "auto"
		}).appendTo(this.previewWrapperHeader);
		this.pageCountText = $(document.createElement("span")).text("1/1");
		this.pageCountElement = $(document.createElement("div")).css({
			height: "20px",
			"border-radius": "12px",
			color: "white",
			background: "rgba(0, 0, 0, 0.32)",
			"font-size": "12px",
			"line-height": "1",
			"font-weight": "bold",
			padding: "3px 6px",
			cursor: "pointer",
			display: "flex",
			"align-items": "center",
			gap: "4px"
		}).append(page_default).append(this.pageCountText).hide().appendTo(this.previewWrapperHeader);
		this.downloadOriginalElement = $(document.createElement("a")).css({
			height: "20px",
			"border-radius": "12px",
			color: "white",
			background: "rgba(0, 0, 0, 0.32)",
			"font-size": "12px",
			"line-height": "1",
			"font-weight": "bold",
			padding: "3px 6px",
			cursor: "pointer",
			display: "flex",
			"align-items": "center",
			gap: "4px"
		}).append(`${download_default}<span>原图</span>`).appendTo(this.previewWrapperHeader);
		this.previewLoadingElement = $(loading_default).css({
			padding: "12px",
			animation: "pp-spin 1s linear infinite"
		}).appendTo(this.previewWrapperElement);
		this.previewImageElement = $(new Image()).css({ "border-radius": `${8}px` }).hide().appendTo(this.previewWrapperElement);
		this.#images.forEach((image) => {
			if (image) image.src = "";
		});
		this.#images = [];
		this.#prevMousePos = [0, 0];
		this.#currentIllustSize = [0, 0];
		this.#currentUgoiraPlayer?.stop();
		if (this.#downloadResetTimeout !== null) {
			clearTimeout(this.#downloadResetTimeout);
			this.#downloadResetTimeout = null;
		}
		this.unbindPreviewImageEvents();
		this.unbindUgoiraPreviewEvents();
	}
	/** 初始化预览容器,默认显示第一张图片 */
	setImage({ illustId, illustElement, previewPage = 1, regularUrls, originalUrls }) {
		this.reset();
		this.initPreviewWrapper();
		this.illustId = illustId;
		this.illustElement = illustElement;
		this.regularUrls = regularUrls;
		this.originalUrls = originalUrls;
		this.currentPage = previewPage;
		this.pageCount = regularUrls.length;
		this.preloadImages();
		this.bindPreviewImageEvents();
		this.updatePreviewImage();
		this.showIllustrationDetails();
	}
	bindPreviewImageEvents() {
		this.previewImageElement.on("load", this.onImageLoad);
		this.previewImageElement.on("click", this.onPreviewImageMouseClick);
		this.downloadOriginalElement.on("click", this.onDownloadImage);
		$(document).on("keydown", this.onCtrlKeyDown);
		$(document).on("keyup", this.onCtrlKeyUp);
		$(document).on("wheel", this.onPreviewImageMouseWheel);
		$(document).on("keydown", this.onPreviewImageKeyDown);
		$(document).on("mousemove", this.onMouseMove);
		window.addEventListener("wheel", this.preventPageZoom, { passive: false });
	}
	unbindPreviewImageEvents() {
		this.previewImageElement.off();
		this.downloadOriginalElement.off();
		$(document).off("keydown", this.onCtrlKeyDown);
		$(document).off("keyup", this.onCtrlKeyUp);
		$(document).off("wheel", this.onPreviewImageMouseWheel);
		$(document).off("keydown", this.onPreviewImageKeyDown);
		$(document).off("mousemove", this.onMouseMove);
		window.removeEventListener("wheel", this.preventPageZoom);
	}
	/** 显示 pageIndex 指向的图片 */
	updatePreviewImage(page = this.currentPage) {
		const currentImageUrl = this.regularUrls[page - 1];
		this.previewImageElement.attr("src", currentImageUrl);
		this.pageCountText.text(`${page}/${this.pageCount}`);
	}
	onImageLoad = () => {
		this.illustLoaded = true;
		this.previewLoadingElement.hide();
		this.previewImageElement.show();
		this.previewWrapperHeader.show();
		if (this.pageCount > 1) this.pageCountElement.show();
		this.previewImageElement.css({
			width: "",
			height: ""
		});
		this.#currentIllustSize = [this.previewImageElement.width() ?? 0, this.previewImageElement.height() ?? 0];
		this.adjustPreviewWrapper({ baseOnMousePos: false });
	};
	nextPage() {
		if (this.currentPage < this.pageCount) this.currentPage += 1;
		else this.currentPage = 1;
		this.resetDownloadButton();
		this.updatePreviewImage();
		this.preloadImages();
	}
	prevPage() {
		if (this.currentPage > 1) this.currentPage -= 1;
		else this.currentPage = this.pageCount;
		this.resetDownloadButton();
		this.updatePreviewImage();
	}
	resetDownloadButton() {
		if (this.#downloadResetTimeout !== null) {
			clearTimeout(this.#downloadResetTimeout);
			this.#downloadResetTimeout = null;
		}
		this.downloadOriginalElement.find("span").text("原图");
		this.downloadOriginalElement.css({
			pointerEvents: "",
			backgroundImage: "",
			backgroundSize: "",
			backgroundRepeat: ""
		});
	}
	preloadImages(from = this.currentPage - 1, to = this.currentPage - 1 + 5) {
		if (!this.#images.length) this.#images = new Array(this.regularUrls.length);
		for (let i = from; i < to && i < this.regularUrls.length; i += 1) {
			const preloadImage = new Image();
			preloadImage.src = this.regularUrls[i];
			this.#images[i] = preloadImage;
		}
	}
	onPreviewImageMouseClick = () => {
		this.nextPage();
	};
	onPreviewImageMouseWheel = (mouseWheelEvent) => {
		if (mouseWheelEvent.ctrlKey || mouseWheelEvent.metaKey) {
			mouseWheelEvent.preventDefault();
			if (mouseWheelEvent.originalEvent.deltaY > 0) this.nextPage();
			else this.prevPage();
		}
	};
	onPreviewImageKeyDown = (keyDownEvent) => {
		if (keyDownEvent.ctrlKey || keyDownEvent.metaKey) {
			keyDownEvent.preventDefault();
			switch (keyDownEvent.key) {
				case "ArrowUp":
				case "ArrowRight":
					this.nextPage();
					break;
				case "ArrowDown":
				case "ArrowLeft":
					this.prevPage();
					break;
			}
		}
	};
	onDownloadImage = (onClickEvent) => {
		onClickEvent.preventDefault();
		const downloadPage = this.currentPage;
		const currentImageOriginalUrl = this.originalUrls[downloadPage - 1];
		const currentImageFilename = currentImageOriginalUrl.split("/").pop() || "illust.jpg";
		const textSpan = this.downloadOriginalElement.find("span");
		const originalText = textSpan.text();
		this.downloadOriginalElement.css({
			pointerEvents: "none",
			backgroundImage: "linear-gradient(to right, rgba(0,150,250,0.28), rgba(0,150,250,0.28))",
			backgroundSize: "0% 100%",
			backgroundRepeat: "no-repeat"
		});
		textSpan.text("下载中");
		downloadIllust({
			url: currentImageOriginalUrl,
			filename: currentImageFilename,
			options: {
				onprogress: (ev) => {
					if (this.currentPage !== downloadPage) return;
					try {
						const loaded = ev.loaded ?? 0;
						const total = ev.total ?? 0;
						let percent = 0;
						if (total && total > 0) percent = Math.min(100, Math.round(loaded / total * 100));
						this.downloadOriginalElement.css({ backgroundSize: `${percent}% 100%` });
					} catch (e) {
						console.error(`An error occurred in download progress callback: ${e}`);
					}
				},
				onload: () => {
					if (this.currentPage !== downloadPage) return;
					textSpan.text("已下载");
					this.downloadOriginalElement.css({
						backgroundImage: "",
						backgroundSize: "",
						pointerEvents: ""
					});
					this.#downloadResetTimeout = setTimeout(() => {
						textSpan.text(originalText);
						this.#downloadResetTimeout = null;
					}, 3e3);
				}
			}
		});
	};
	setUgoira({ illustId, illustElement, src, mime_type, frames }) {
		this.reset();
		this.initPreviewWrapper();
		this.illustId = illustId;
		this.illustElement = illustElement;
		illustElement.siblings("svg").css({ "pointer-events": "none" });
		this.#currentUgoiraPlayer = this.createUgoiraPlayer({
			source: src,
			metadata: {
				mime_type,
				frames
			}
		});
		this.bindUgoiraPreviewEvents();
		this.showIllustrationDetails();
	}
	createUgoiraPlayer(options) {
		const canvas = document.createElement("canvas");
		const p = new ZipImagePlayer({
			canvas,
			chunkSize: 3e5,
			loop: true,
			autoStart: true,
			debug: false,
			...options
		});
		p.canvas = canvas;
		return p;
	}
	bindUgoiraPreviewEvents() {
		$(this.#currentUgoiraPlayer).on("frameLoaded", this.onUgoiraFrameLoaded);
		$(document).on("mousemove", this.onMouseMove);
		$(document).on("keydown", this.onCtrlKeyDown);
		$(document).on("keyup", this.onCtrlKeyUp);
	}
	unbindUgoiraPreviewEvents() {
		$(this.#currentUgoiraPlayer).off();
		$(document).off("mousemove", this.onMouseMove);
		$(document).off("keydown", this.onCtrlKeyDown);
		$(document).off("keyup", this.onCtrlKeyUp);
	}
	onUgoiraFrameLoaded = (ev, frame) => {
		if (frame !== 0) return;
		this.illustLoaded = true;
		this.previewLoadingElement.hide();
		const canvas = $(this.#currentUgoiraPlayer.canvas);
		this.previewImageElement.after(canvas);
		this.previewImageElement.remove();
		this.previewImageElement = canvas;
		const ugoiraOriginWidth = ev.currentTarget._frameImages[0].width;
		const ugoiraOriginHeight = ev.currentTarget._frameImages[0].height;
		this.#currentIllustSize = [ugoiraOriginWidth, ugoiraOriginHeight];
		this.previewImageElement.attr({
			width: ugoiraOriginWidth,
			height: ugoiraOriginHeight
		});
		this.adjustPreviewWrapper({ baseOnMousePos: false });
	};
	async showIllustrationDetails() {
		const illustrationDetails = await getIllustrationDetailsWithCache(this.illustId);
		if (illustrationDetails && illustrationDetails.id === this.illustId) {
			this.illustMeta.empty();
			const { aiType, bookmarkId, bookmarkUserTotal, tags } = illustrationDetails;
			const isR18 = checkIsR18(tags);
			const isAi = checkIsAiGenerated(aiType);
			const isAiAssisted = checkIsAiAssisted(tags);
			const illustrationDetailsElements = [];
			if (isR18) illustrationDetailsElements.push($(document.createElement("div")).css({
				...DETAIL_BADGE_CSS,
				background: "rgb(255, 64, 96)"
			}).text("R-18"));
			if (isAi) illustrationDetailsElements.push($(document.createElement("div")).css({
				...DETAIL_BADGE_CSS,
				background: "rgb(29, 78, 216)"
			}).text("AI 生成"));
			else if (isAiAssisted) illustrationDetailsElements.push($(document.createElement("div")).css({
				...DETAIL_BADGE_CSS,
				background: "rgb(109, 40, 217)"
			}).text("AI 辅助"));
			illustrationDetailsElements.push($(document.createElement("div")).css({
				...DETAIL_BADGE_CSS,
				background: bookmarkUserTotal > 5e4 ? "rgb(159, 18, 57)" : bookmarkUserTotal > 1e4 ? "rgb(220, 38, 38)" : bookmarkUserTotal > 5e3 ? "rgb(29, 78, 216)" : bookmarkUserTotal > 1e3 ? "rgb(21, 128, 61)" : "rgb(71, 85, 105)"
			}).text(`${bookmarkId ? "❤️" : "❤"} ${bookmarkUserTotal}`));
			this.illustMeta.append(illustrationDetailsElements);
		}
	}
	/** 初始化显示预览容器 */
	initPreviewWrapper() {
		this.previewWrapperElement.show();
		this.previewLoadingElement.show();
		this.adjustPreviewWrapper({ baseOnMousePos: true });
	}
	/** 阻止页面缩放事件 */
	preventPageZoom = (mouseWheelEvent) => {
		if (mouseWheelEvent.ctrlKey || mouseWheelEvent.metaKey) mouseWheelEvent.preventDefault();
	};
	/**
	* 按下 Ctrl 或 Meta 键时,预览容器接收鼠标事件
	* @param keyDownEvent
	*/
	onCtrlKeyDown = (keyDownEvent) => {
		if (keyDownEvent.key === "Control" || keyDownEvent.key === "Meta") this.previewWrapperElement.css({ "pointer-events": "auto" });
	};
	/**
	* 松开 Ctrl 或 Meta 键时,鼠标事件穿透预览容器,避免鼠标快速移动时预览窗口闪烁
	* @param keyUpEvent
	*/
	onCtrlKeyUp = (keyUpEvent) => {
		if (keyUpEvent.key === "Control" || keyUpEvent.key === "Meta") this.previewWrapperElement.css({ "pointer-events": "none" });
	};
	/**
	* 根据鼠标移动调整预览容器位置与显隐
	* @param mouseMoveEvent
	*/
	onMouseMove = (mouseMoveEvent) => {
		if (mouseMoveEvent.ctrlKey || mouseMoveEvent.metaKey) return;
		if ($(mouseMoveEvent.target).is(this.illustElement)) this.adjustPreviewWrapper({ baseOnMousePos: true });
		else this.reset();
	};
	/**
	* 调整预览容器的位置与大小
	* @param `baseOnMousePos` 是否根据当前鼠标所在位置调整
	* @param `illustSize` 作品的实际大小
	*/
	adjustPreviewWrapper({ baseOnMousePos = true } = {}) {
		const [mousePosX, mousePosY] = baseOnMousePos ? mouseMonitor.mouseAbsPos : this.#prevMousePos;
		this.#prevMousePos = [mousePosX, mousePosY];
		const [illustWidth, illustHeight] = this.#currentIllustSize;
		const screenWidth = document.documentElement.clientWidth;
		const screenHeight = document.documentElement.clientHeight;
		const DIST = 20;
		if (!illustWidth || !illustHeight) {
			const defaultPos = {
				left: `${mousePosX + DIST}px`,
				top: `${mousePosY}px`
			};
			this.previewWrapperElement.css(defaultPos);
			this.previewImageElement.css({
				width: "",
				height: ""
			});
			return;
		}
		const candidates = [
			{
				side: "left",
				availW: Math.max(0, mousePosX - DIST),
				availH: screenHeight
			},
			{
				side: "right",
				availW: Math.max(0, screenWidth - mousePosX - DIST),
				availH: screenHeight
			},
			{
				side: "top",
				availW: screenWidth,
				availH: Math.max(0, mousePosY - DIST)
			},
			{
				side: "bottom",
				availW: screenWidth,
				availH: Math.max(0, screenHeight - mousePosY - DIST)
			}
		];
		let best = null;
		for (const c of candidates) {
			let scale = 1;
			if (this.illustLoaded) {
				const sx = c.availW / illustWidth;
				const sy = c.availH / illustHeight;
				scale = Number(Math.min(sx, sy).toFixed(3));
			}
			const fitW = Math.max(0, Math.floor(illustWidth * scale));
			const fitH = Math.max(0, Math.floor(illustHeight * scale));
			const area = fitW * fitH;
			if (!best || area > best.area) best = {
				side: c.side,
				fitW,
				fitH,
				area
			};
		}
		const previewImageFitWidth = best?.fitW ?? 0;
		const previewImageFitHeight = best?.fitH ?? 0;
		const clamp = (v, lo, hi) => Math.max(lo, Math.min(v, hi));
		const side = best?.side ?? "bottom";
		const isHorizontal = side === "left" || side === "right";
		const anchorX = isHorizontal ? side === "right" ? mousePosX + DIST : mousePosX - DIST - previewImageFitWidth : Math.floor(mousePosX - previewImageFitWidth / 2);
		const anchorY = isHorizontal ? Math.floor(mousePosY - previewImageFitHeight / 2) : side === "bottom" ? mousePosY + DIST : mousePosY - DIST - previewImageFitHeight;
		const left = clamp(anchorX, 0, Math.max(0, screenWidth - previewImageFitWidth));
		const top = clamp(anchorY, 0, Math.max(0, screenHeight - previewImageFitHeight));
		this.previewWrapperElement.css({
			left: `${left}px`,
			top: `${top}px`,
			right: "",
			bottom: ""
		});
		this.previewImageElement.css({
			width: `${previewImageFitWidth}px`,
			height: `${previewImageFitHeight}px`
		});
	}
};

//#endregion
//#region src/i18n/index.ts
const Texts = {
	install_title: "欢迎使用 Pixiv Previewer (LolipopJ Edition) v",
	upgrade_body: `<div>
  <p style="line-height: 1.6;">
    本脚本基于
    <a
      style="color: skyblue"
      href="https://greasyfork.org/zh-CN/scripts/30766-pixiv-previewer"
      target="_blank"
      >Pixiv Previewer</a
    >
    二次开发,旨在满足开发者自己需要的能力。如果您有不错的想法或建议,请前往原脚本的
    <a
      style="color: skyblue"
      href="https://greasyfork.org/zh-CN/scripts/30766-pixiv-previewer/feedback"
      target="_blank"
      >Greasy Fork 反馈页面</a
    >或开启一个新的
    <a
      style="color: skyblue"
      href="https://github.com/Ocrosoft/PixivPreviewer/issues"
      target="_blank"
      >Github 议题</a
    >!
  </p>
</div>
`,
	setting_language: "语言",
	setting_preview: "预览",
	setting_animePreview: "动图预览",
	setting_sort: "搜索页自动排序",
	setting_anime: "动图下载(动图预览及详情页生效)",
	setting_origin: "预览时优先显示原图(慢)",
	setting_previewDelay: "延迟显示预览图(毫秒)",
	setting_previewByKey: "使用按键控制预览图展示(Ctrl)",
	setting_previewByKeyHelp: "开启后鼠标移动到图片上不再展示预览图,按下Ctrl键才展示,同时“延迟显示预览”设置项不生效。",
	setting_maxPage: "每次排序时统计的最大页数",
	setting_hideWork: "隐藏收藏数少于设定值的作品",
	setting_sortOrderByBookmark: "按照收藏数排序作品",
	setting_hideAiWork: "排序时隐藏 AI 生成作品",
	setting_hideAiAssistedWork: "排序时隐藏 AI 辅助作品",
	setting_hideFav: "排序时隐藏已收藏的作品",
	setting_hideFollowed: "排序时隐藏已关注画师作品",
	setting_hideByTag: "排序时隐藏指定标签的作品",
	setting_hideByTagPlaceholder: "输入标签名,多个标签用','分隔",
	setting_clearFollowingCache: "清除缓存",
	setting_clearFollowingCacheHelp: "关注画师信息会在本地保存一天,如果希望立即更新,请点击清除缓存",
	setting_followingCacheCleared: "已清除缓存,请刷新页面。",
	setting_blank: "使用新标签页打开作品详情页",
	setting_turnPage: "使用键盘←→进行翻页(排序后的搜索页)",
	setting_save: "保存设置",
	setting_reset: "重置脚本",
	setting_resetHint: "这会删除所有设置,相当于重新安装脚本,确定要重置吗?",
	setting_novelSort: "小说排序",
	setting_novelMaxPage: "小说排序时统计的最大页数",
	setting_novelHideWork: "隐藏收藏数少于设定值的作品",
	setting_novelHideFav: "排序时隐藏已收藏的作品",
	sort_noWork: "没有可以显示的作品(隐藏了 %1 个作品)",
	sort_getWorks: "正在获取第 %1/%2 页作品",
	sort_getBookmarkCount: "获取收藏数:%1/%2",
	sort_getPublicFollowing: "获取公开关注画师",
	sort_getPrivateFollowing: "获取私有关注画师",
	sort_filtering: "过滤%1收藏量低于%2的作品",
	sort_filteringHideFavorite: "已收藏和",
	sort_fullSizeThumb: "全尺寸缩略图(搜索页、用户页)",
	label_sort: "排序",
	label_sorting: "排序中",
	label_nextPage: "下一页",
	label_hideFav: "过滤收藏"
};

//#endregion
//#region src/icons/heart.svg
var heart_default = "<svg viewBox=\"0 0 32 32\" width=\"32\" height=\"32\">\n  <path d=\"\nM21,5.5 C24.8659932,5.5 28,8.63400675 28,12.5 C28,18.2694439 24.2975093,23.1517313 17.2206059,27.1100183\nC16.4622493,27.5342993 15.5379984,27.5343235 14.779626,27.110148 C7.70250208,23.1517462 4,18.2694529 4,12.5\nC4,8.63400691 7.13400681,5.5 11,5.5 C12.829814,5.5 14.6210123,6.4144028 16,7.8282366\nC17.3789877,6.4144028 19.170186,5.5 21,5.5 Z\"></path>\n  <path d=\"M16,11.3317089 C15.0857201,9.28334665 13.0491506,7.5 11,7.5\nC8.23857625,7.5 6,9.73857647 6,12.5 C6,17.4386065 9.2519779,21.7268174 15.7559337,25.3646328\nC15.9076021,25.4494645 16.092439,25.4494644 16.2441073,25.3646326 C22.7480325,21.7268037 26,17.4385986 26,12.5\nC26,9.73857625 23.7614237,7.5 21,7.5 C18.9508494,7.5 16.9142799,9.28334665 16,11.3317089 Z\" style=\"fill: #fafafa;\">\n  </path>\n</svg>";

//#endregion
//#region src/icons/heart-filled.svg
var heart_filled_default = "<svg viewBox=\"0 0 32 32\" width=\"32\" height=\"32\">\n  <path d=\"\nM21,5.5 C24.8659932,5.5 28,8.63400675 28,12.5 C28,18.2694439 24.2975093,23.1517313 17.2206059,27.1100183\nC16.4622493,27.5342993 15.5379984,27.5343235 14.779626,27.110148 C7.70250208,23.1517462 4,18.2694529 4,12.5\nC4,8.63400691 7.13400681,5.5 11,5.5 C12.829814,5.5 14.6210123,6.4144028 16,7.8282366\nC17.3789877,6.4144028 19.170186,5.5 21,5.5 Z\"></path>\n  <path d=\"M16,11.3317089 C15.0857201,9.28334665 13.0491506,7.5 11,7.5\nC8.23857625,7.5 6,9.73857647 6,12.5 C6,17.4386065 9.2519779,21.7268174 15.7559337,25.3646328\nC15.9076021,25.4494645 16.092439,25.4494644 16.2441073,25.3646326 C22.7480325,21.7268037 26,17.4385986 26,12.5\nC26,9.73857625 23.7614237,7.5 21,7.5 C18.9508494,7.5 16.9142799,9.28334665 16,11.3317089 Z\" style=\"fill: #dc2626;\">\n  </path>\n</svg>";

//#endregion
//#region src/icons/play.svg
var play_default = "<svg viewBox=\"0 0 24 24\"\n  style=\"width: 48px; height: 48px; stroke: none; line-height: 0; font-size: 0px; vertical-align: middle;\">\n  <circle cx=\"12\" cy=\"12\" r=\"10\" style=\"fill: rgba(0, 0, 0, 0.32);\"></circle>\n  <path d=\"M9,8.74841664 L9,15.2515834 C9,15.8038681 9.44771525,16.2515834 10,16.2515834\nC10.1782928,16.2515834 10.3533435,16.2039156 10.5070201,16.1135176 L16.0347118,12.8619342\nC16.510745,12.5819147 16.6696454,11.969013 16.3896259,11.4929799\nC16.3034179,11.3464262 16.1812655,11.2242738 16.0347118,11.1380658 L10.5070201,7.88648243\nC10.030987,7.60646294 9.41808527,7.76536339 9.13806578,8.24139652\nC9.04766776,8.39507316 9,8.57012386 9,8.74841664 Z\" style=\"fill: rgb(245, 245, 245);\"></path>\n</svg>";

//#endregion
//#region src/utils/promise.ts
const execLimitConcurrentPromises = async (promises, limit = 48) => {
	const results = [];
	let index = 0;
	const executeNext = async () => {
		if (index >= promises.length) return Promise.resolve();
		const currentIndex = index++;
		results[currentIndex] = await promises[currentIndex]();
		return await executeNext();
	};
	const initialPromises = Array.from({ length: Math.min(limit, promises.length) }, () => executeNext());
	await Promise.all(initialPromises);
	return results;
};

//#endregion
//#region src/features/sort.ts
const BOOKMARK_USER_PAGE_ILLUSTRATION_LIST_SELECTOR = "ul.sc-e83d358-1.gIHHFW";
const USER_TYPE_ARTWORKS_PER_PAGE = 48;
let isInitialized = false;
const loadIllustSort = (options) => {
	if (isInitialized) return;
	const { pageCount: optionPageCount, favFilter: optionFavFilter, orderType = IllustSortOrder.BY_BOOKMARK_COUNT, hideFavorite = false, hideByTag = false, hideByTagList: hideByTagListString, aiFilter = false, aiAssistedFilter = false } = options;
	let pageCount = Number(optionPageCount), favFilter = Number(optionFavFilter);
	if (pageCount <= 0) pageCount = g_defaultSettings.pageCount;
	if (favFilter < 0) favFilter = g_defaultSettings.favFilter;
	const hideByTagList = hideByTagListString.split(",").map((tag) => tag.trim().toLowerCase()).filter((tag) => !!tag);
	if (aiAssistedFilter) hideByTagList.push(...AI_ASSISTED_TAGS);
	class IllustSorter {
		type;
		illustrations;
		sorting = false;
		nextSortPage;
		listElement = $();
		progressElement = $();
		progressText = $();
		sortButtonElement = $(`#${SORT_BUTTON_ID}`);
		reset({ type }) {
			try {
				this.type = type;
				this.illustrations = [];
				this.sorting = false;
				this.nextSortPage = void 0;
				this.listElement = getIllustrationsListDom(type);
				this.progressElement?.remove();
				this.progressElement = $(document.createElement("div")).attr({ id: "pp-sort-progress" }).css({
					width: "100%",
					display: "flex",
					"flex-direction": "column",
					"align-items": "center",
					"justify-content": "center",
					gap: "6px"
				}).append($(new Image(96, 96)).attr({
					id: "sort-progress__loading",
					src: g_loadingImage
				}).css({ "border-radius": "50%" })).prependTo(this.listElement).hide();
				this.progressText = $(document.createElement("div")).attr({ id: "pp-sort-progress__text" }).css({
					"text-align": "center",
					"font-size": "16px",
					"font-weight": "bold",
					color: "initial"
				}).appendTo(this.progressElement);
				this.sortButtonElement.text(Texts.label_sort);
			} catch (error) {
				iLog.e(`An error occurred while resetting sorter:`, error);
				throw new Error(String(error), { cause: error });
			}
		}
		async sort({ type, api, searchParams }) {
			this.sorting = true;
			iLog.i("Start to sort illustrations.");
			this.sortButtonElement.text(Texts.label_sorting);
			try {
				let illustrations = [];
				const startPage = Number(searchParams.get("p") ?? 1);
				this.nextSortPage = startPage + pageCount;
				for (let page = startPage; page < startPage + pageCount; page += 1) {
					searchParams.set("p", String(page));
					if ([
						IllustSortType.USER_ARTWORK,
						IllustSortType.USER_ILLUST,
						IllustSortType.USER_MANGA
					].includes(type)) {
						searchParams.set("is_first_page", page > 1 ? "0" : "1");
						searchParams.delete("ids[]");
						const userIllustrations = await getUserIllustrationsWithCache(searchParams.get("user_id"), { onRequesting: () => this.setProgress(`Getting illustrations of current user...`) });
						const fromIndex = (page - 1) * USER_TYPE_ARTWORKS_PER_PAGE;
						const toIndex = page * USER_TYPE_ARTWORKS_PER_PAGE;
						switch (type) {
							case IllustSortType.USER_ARTWORK:
								userIllustrations.artworks.slice(fromIndex, toIndex).forEach((id) => searchParams.append("ids[]", id));
								break;
							case IllustSortType.USER_ILLUST:
								userIllustrations.illusts.slice(fromIndex, toIndex).forEach((id) => searchParams.append("ids[]", id));
								break;
							case IllustSortType.USER_MANGA:
								userIllustrations.manga.slice(fromIndex, toIndex).forEach((id) => searchParams.append("ids[]", id));
								break;
						}
					} else if ([IllustSortType.USER_BOOKMARK].includes(type)) searchParams.set("offset", String((page - 1) * USER_TYPE_ARTWORKS_PER_PAGE));
					this.setProgress(`Getting illustration list of page ${page} ...`);
					const requestUrl = `${api}?${searchParams}`;
					const extractedIllustrations = getIllustrationsFromResponse(type, (await requestWithRetry({
						url: requestUrl,
						onRetry: (response, retryTimes) => {
							iLog.w(`Get illustration list through \`${requestUrl}\` failed:`, response, `${retryTimes} times retrying...`);
							this.setProgress(`Retry to get illustration list of page ${page} (${retryTimes} times)...`);
						}
					})).response);
					illustrations = illustrations.concat(extractedIllustrations);
				}
				const getDetailedIllustrationPromises = [];
				for (let i = 0; i < illustrations.length; i += 1) {
					const illustration = illustrations[i];
					const illustrationId = illustration.id;
					const illustrationAuthorId = illustration.userId;
					if (String(illustrationAuthorId) === "0") continue;
					getDetailedIllustrationPromises.push(async () => {
						this.setProgress(`Getting details of ${i + 1}/${illustrations.length} illustration...`);
						const illustrationDetails = await getIllustrationDetailsWithCache(illustrationId, true);
						return {
							...illustration,
							bookmarkUserTotal: illustrationDetails?.bookmarkUserTotal ?? -1
						};
					});
				}
				const detailedIllustrations = await execLimitConcurrentPromises(getDetailedIllustrationPromises);
				iLog.d("Queried detailed illustrations:", detailedIllustrations);
				this.setProgress("Filtering illustrations...");
				const filteredIllustrations = detailedIllustrations.filter((illustration) => {
					if (hideFavorite && illustration.bookmarkData) return false;
					if (aiFilter && illustration.aiType === AiType.AI) return false;
					if ((hideByTag || aiAssistedFilter) && hideByTagList.length) {
						for (const tag of illustration.tags) if (hideByTagList.includes(tag.toLowerCase())) return false;
					}
					return illustration.bookmarkUserTotal >= favFilter;
				});
				this.setProgress("Sorting filtered illustrations...");
				const sortedIllustrations = orderType === IllustSortOrder.BY_BOOKMARK_COUNT ? filteredIllustrations.sort((a, b) => b.bookmarkUserTotal - a.bookmarkUserTotal) : filteredIllustrations;
				iLog.d("Filtered and sorted illustrations:", sortedIllustrations);
				iLog.i("Sort illustrations successfully.");
				this.illustrations = sortedIllustrations;
				this.showIllustrations();
			} catch (error) {
				iLog.e("Sort illustrations failed:", error);
			}
			this.hideProgress();
			this.sorting = false;
			this.sortButtonElement.text(Texts.label_sort);
		}
		setProgress(text) {
			this.progressText.text(text);
			this.progressElement.show();
		}
		hideProgress() {
			this.progressText.text("");
			this.progressElement.hide();
		}
		showIllustrations() {
			const fragment = document.createDocumentFragment();
			for (const { aiType, alt, bookmarkData, bookmarkUserTotal, id, illustType, pageCount, profileImageUrl, tags, title, url, userId, userName } of this.illustrations) {
				const isR18 = checkIsR18(tags);
				const isUgoira = checkIsUgoira(illustType);
				const isAi = checkIsAiGenerated(aiType);
				const isAiAssisted = checkIsAiAssisted(tags);
				const listItem = document.createElement("li");
				listItem.className = "col-span-2";
				const container = document.createElement("div");
				container.style = "width: 184px;";
				const illustrationAnchor = document.createElement("a");
				illustrationAnchor.setAttribute("data-gtm-value", id);
				illustrationAnchor.setAttribute("data-gtm-user-id", userId);
				illustrationAnchor.href = `/artworks/${id}`;
				illustrationAnchor.target = "_blank";
				illustrationAnchor.rel = "external";
				illustrationAnchor.style = "display: block; position: relative; width: 184px;";
				const illustrationImageWrapper = document.createElement("div");
				illustrationImageWrapper.style = "position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;";
				const illustrationImage = document.createElement("img");
				illustrationImage.src = url;
				illustrationImage.alt = alt;
				illustrationImage.style = "object-fit: cover; object-position: center center; width: 100%; height: 100%; border-radius: 4px; background-color: rgb(31, 31, 31);";
				const ugoriaSvg = document.createElement("div");
				ugoriaSvg.style = "position: absolute;";
				ugoriaSvg.innerHTML = play_default;
				const illustrationMeta = document.createElement("div");
				illustrationMeta.style = "position: absolute; top: 0px; left: 0px; right: 0px; display: flex; align-items: flex-start; padding: 4px 4px 0; pointer-events: none; font-size: 10px;";
				illustrationMeta.innerHTML = `
          ${isR18 ? "<div style=\"padding: 0px 4px; border-radius: 4px; color: rgb(245, 245, 245); background: rgb(255, 64, 96); font-weight: bold; line-height: 16px; user-select: none;\">R-18</div>" : ""}
          ${isAi ? "<div style=\"padding: 0px 4px; border-radius: 4px; color: rgb(245, 245, 245); background: rgb(29, 78, 216); font-weight: bold; line-height: 16px; user-select: none;\">AI 生成</div>" : isAiAssisted ? "<div style=\"padding: 0px 4px; border-radius: 4px; color: rgb(245, 245, 245); background: rgb(109, 40, 217); font-weight: bold; line-height: 16px; user-select: none;\">AI 辅助</div>" : ""}
          ${pageCount > 1 ? `
                <div style="margin-left: auto;">
                  <div style="display: flex; justify-content: center; align-items: center; height: 20px; min-width: 20px; color: rgb(245, 245, 245); font-weight: bold; padding: 0px 6px; background: rgba(0, 0, 0, 0.32); border-radius: 10px; line-height: 10px;">
                    ${page_default}
                    <span>${pageCount}</span>
                  </div>
                </div>` : ""}
        `;
				const illustrationToolbar = document.createElement("div");
				illustrationToolbar.style = "position: absolute; top: 154px; left: 0px; right: 0px; display: flex; align-items: center; padding: 0 4px 4px; pointer-events: none; font-size: 12px;";
				illustrationToolbar.innerHTML = `
          <div style="padding: 0px 4px; border-radius: 4px; color: rgb(245, 245, 245); background: ${bookmarkUserTotal > 5e4 ? "rgb(159, 18, 57)" : bookmarkUserTotal > 1e4 ? "rgb(220, 38, 38)" : bookmarkUserTotal > 5e3 ? "rgb(29, 78, 216)" : bookmarkUserTotal > 1e3 ? "rgb(21, 128, 61)" : "rgb(71, 85, 105)"}; font-weight: bold; line-height: 16px; user-select: none;">❤ ${bookmarkUserTotal}</div>
          <div style="margin-left: auto; display: none;">${bookmarkData ? heart_filled_default : heart_default}</div>
        `;
				const illustrationTitle = document.createElement("div");
				illustrationTitle.innerHTML = title;
				illustrationTitle.style = "margin-top: 4px; max-width: 100%; overflow: hidden; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; line-height: 22px; font-size: 14px; font-weight: bold; color: rgb(245, 245, 245); transition: color 0.2s;";
				const illustrationAuthor = document.createElement("a");
				illustrationAuthor.setAttribute("data-gtm-value", userId);
				illustrationAuthor.href = `/users/${userId}`;
				illustrationAuthor.target = "_blank";
				illustrationAuthor.rel = "external";
				illustrationAuthor.style = "display: flex; align-items: center; margin-top: 4px;";
				illustrationAuthor.innerHTML = `
          <img src="${profileImageUrl}" alt="${userName}" style="object-fit: cover; object-position: center top; width: 24px; height: 24px; border-radius: 50%; margin-right: 4px;">
          <span style="min-width: 0px; line-height: 22px; font-size: 14px; color: rgb(214, 214, 214); text-decoration: none; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">${userName}</span>
        `;
				illustrationImageWrapper.appendChild(illustrationImage);
				if (isUgoira) illustrationImageWrapper.appendChild(ugoriaSvg);
				illustrationAnchor.appendChild(illustrationImageWrapper);
				illustrationAnchor.appendChild(illustrationMeta);
				illustrationAnchor.appendChild(illustrationToolbar);
				illustrationAnchor.appendChild(illustrationTitle);
				container.appendChild(illustrationAnchor);
				container.appendChild(illustrationAuthor);
				listItem.appendChild(container);
				fragment.appendChild(listItem);
			}
			if ([
				IllustSortType.BOOKMARK_NEW,
				IllustSortType.BOOKMARK_NEW_R18,
				IllustSortType.USER_ARTWORK,
				IllustSortType.USER_ILLUST,
				IllustSortType.USER_MANGA,
				IllustSortType.USER_BOOKMARK
			].includes(this.type)) this.listElement.css({ gap: "24px" });
			this.listElement.children().remove();
			this.listElement.append(fragment);
		}
	}
	const illustSorter = new IllustSorter();
	window.addEventListener(SORT_EVENT_NAME, () => {
		if (illustSorter.sorting) {
			iLog.w("Current is in sorting progress.");
			return;
		}
		const url = new URL(location.href);
		const { type, api, searchParams: defaultSearchParams } = getSortOptionsFromUrl(url);
		if (type === void 0) {
			iLog.w("Current page doesn't support sorting illustrations.");
			return;
		}
		const mergedSearchParams = new URLSearchParams(defaultSearchParams);
		url.searchParams.forEach((value, key) => {
			mergedSearchParams.set(key, value);
		});
		illustSorter.reset({ type });
		illustSorter.sort({
			type,
			api,
			searchParams: mergedSearchParams
		});
	});
	window.addEventListener(SORT_NEXT_PAGE_EVENT_NAME, () => {
		const { origin, pathname, searchParams } = new URL(location.href);
		let nextPage = Number(searchParams.get("p") ?? 1) + 1;
		if (illustSorter.listElement?.length && illustSorter.nextSortPage) {
			iLog.i("Illustrations in current page are sorted, jump to next available page...");
			nextPage = illustSorter.nextSortPage;
		}
		searchParams.set("p", String(nextPage));
		location.href = `${origin}${pathname}?${searchParams}`;
	});
	isInitialized = true;
};
/** 获取作品节点 li 的父节点 ul */
function getIllustrationsListDom(type) {
	let dom;
	if ([
		IllustSortType.TAG_ARTWORK,
		IllustSortType.TAG_ILLUST,
		IllustSortType.TAG_MANGA,
		IllustSortType.SEARCH_ILLUST,
		IllustSortType.SEARCH_MANGA
	].includes(type)) {
		dom = $("div[data-ga4-label=\"works_content\"]").children("div").last();
		if (!dom.length) dom = $("section").find("ul").last();
	} else if ([
		IllustSortType.BOOKMARK_NEW,
		IllustSortType.BOOKMARK_NEW_R18,
		IllustSortType.USER_BOOKMARK
	].includes(type)) {
		dom = $(BOOKMARK_USER_PAGE_ILLUSTRATION_LIST_SELECTOR);
		if (!dom.length) dom = $("section").find("ul").last();
	} else if ([
		IllustSortType.USER_ARTWORK,
		IllustSortType.USER_ILLUST,
		IllustSortType.USER_MANGA
	].includes(type)) {
		dom = $(BOOKMARK_USER_PAGE_ILLUSTRATION_LIST_SELECTOR);
		if (!dom.length) dom = $(".__top_side_menu_body").find("ul").last();
	}
	if (dom.length) return dom;
	else throw new Error(`Illustrations list DOM not found in current page: ${location.href}. Please create a new issue here: https://github.com/LolipopJ/PixivPreviewer/issues`);
}
/** 根据当前路由获取接口参数 */
function getSortOptionsFromUrl(url) {
	const { pathname, searchParams } = url;
	let type;
	let api;
	let defaultSearchParams;
	let match;
	if (match = pathname.match(/\/tags\/(.+)\/(artworks|illustrations|manga)$/)) {
		const tagName = match[1];
		switch (match[2]) {
			case "artworks":
				type = IllustSortType.TAG_ARTWORK;
				api = `/ajax/search/artworks/${tagName}`;
				defaultSearchParams = `word=${tagName}&order=date_d&mode=all&p=1&csw=0&s_mode=s_tag_full&type=all&lang=zh`;
				break;
			case "illustrations":
				type = IllustSortType.TAG_ILLUST;
				api = `/ajax/search/illustrations/${tagName}`;
				defaultSearchParams = `word=${tagName}&order=date_d&mode=all&p=1&csw=0&s_mode=s_tag_full&type=illust_and_ugoira&lang=zh`;
				break;
			case "manga":
				type = IllustSortType.TAG_MANGA;
				api = `/ajax/search/manga/${tagName}`;
				defaultSearchParams = `word=${tagName}&order=date_d&mode=all&p=1&csw=0&s_mode=s_tag_full&type=manga&lang=zh`;
				break;
		}
	} else if (match = pathname.match(/\/search/)) {
		const tagName = searchParams.get("q");
		switch (searchParams.get("type")) {
			case "illust_ugoira":
				type = IllustSortType.SEARCH_ILLUST;
				api = `/ajax/search/illustrations/${tagName}`;
				defaultSearchParams = `word=${tagName}&order=date_d&mode=all&p=1&csw=0&s_mode=s_tag_full&type=illust_and_ugoira&lang=zh`;
				break;
			case "manga":
				type = IllustSortType.SEARCH_MANGA;
				api = `/ajax/search/manga/${tagName}`;
				defaultSearchParams = `word=${tagName}&order=date_d&mode=all&p=1&csw=0&s_mode=s_tag_full&type=manga&lang=zh`;
				break;
		}
	} else if (match = pathname.match(/\/bookmark_new_illust(_r18)?\.php$/)) {
		const isR18 = !!match[1];
		api = "/ajax/follow_latest/illust";
		if (isR18) {
			type = IllustSortType.BOOKMARK_NEW;
			defaultSearchParams = "mode=r18&lang=zh";
		} else {
			type = IllustSortType.BOOKMARK_NEW_R18;
			defaultSearchParams = "mode=all&lang=zh";
		}
	} else if (match = pathname.match(/\/users\/(\d+)\/bookmarks\/artworks$/)) {
		const userId = match[1];
		type = IllustSortType.USER_BOOKMARK;
		api = `/ajax/user/${userId}/illusts/bookmarks`;
		defaultSearchParams = `tag=&offset=0&limit=${USER_TYPE_ARTWORKS_PER_PAGE}&rest=show&lang=zh`;
	} else if (match = pathname.match(/\/users\/(\d+)\/(artworks|illustrations|manga)$/)) {
		const userId = match[1];
		const filterType = match[2];
		api = `/ajax/user/${userId}/profile/illusts`;
		switch (filterType) {
			case "artworks":
				type = IllustSortType.USER_ARTWORK;
				defaultSearchParams = `work_category=illustManga&is_first_page=1&sensitiveFilterMode=userSetting&user_id=${userId}&lang=zh`;
				break;
			case "illustrations":
				type = IllustSortType.USER_ILLUST;
				defaultSearchParams = `work_category=illust&is_first_page=1&sensitiveFilterMode=userSetting&user_id=${userId}&lang=zh`;
				break;
			case "manga":
				type = IllustSortType.USER_MANGA;
				defaultSearchParams = `work_category=manga&is_first_page=1&sensitiveFilterMode=userSetting&user_id=${userId}&lang=zh`;
				break;
		}
	}
	return {
		type,
		api,
		searchParams: new URLSearchParams(defaultSearchParams)
	};
}
/** 从响应值里提取作品数据列表 */
function getIllustrationsFromResponse(type, response) {
	if (type === IllustSortType.TAG_ARTWORK) return response.body.illustManga.data ?? [];
	else if (type === IllustSortType.TAG_ILLUST || type === IllustSortType.SEARCH_ILLUST) return response.body.illust.data ?? [];
	else if (type === IllustSortType.TAG_MANGA || type === IllustSortType.SEARCH_MANGA) return response.body.manga.data ?? [];
	else if ([IllustSortType.BOOKMARK_NEW, IllustSortType.BOOKMARK_NEW_R18].includes(type)) return response.body.thumbnails.illust ?? [];
	else if ([
		IllustSortType.USER_ARTWORK,
		IllustSortType.USER_ILLUST,
		IllustSortType.USER_MANGA,
		IllustSortType.USER_BOOKMARK
	].includes(type)) return Object.values(response.body.works);
	return [];
}

//#endregion
//#region src/utils/setting.ts
const SETTINGS_KEY = "PIXIV_PREVIEWER_L_SETTINGS";
const toggleSettingBooleanValue = (key) => {
	const settings = getSettings();
	const newValue = !Boolean(settings[key] ?? g_defaultSettings[key]);
	GM_setValue(SETTINGS_KEY, {
		...settings,
		[key]: newValue
	});
};
const setSettingStringValue = (key, label, { parseValue = (v) => v, onSet }) => {
	const settings = getSettings();
	const currentValue = settings[key] ?? g_defaultSettings[key];
	const newValue = prompt(label, String(currentValue));
	if (newValue !== null) {
		const savedValue = parseValue(newValue);
		GM_setValue(SETTINGS_KEY, {
			...settings,
			[key]: savedValue
		});
		onSet?.(savedValue);
	}
};
const setSettingValue = (key, value) => {
	const settings = getSettings();
	const newValue = value ?? g_defaultSettings[key];
	GM_setValue(SETTINGS_KEY, {
		...settings,
		[key]: newValue
	});
};
const getSettings = () => {
	return GM_getValue(SETTINGS_KEY) ?? g_defaultSettings;
};
const resetSettings = () => {
	GM_setValue(SETTINGS_KEY, g_defaultSettings);
};

//#endregion
//#region src/index.ts
let g_csrfToken = "";
let g_pageType;
let g_settings;
const Pages = {
	[PageType.Search]: {
		PageTypeString: "SearchPage",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/tags\/.+\/(artworks|illustrations|manga)/.test(url) || /^https?:\/\/www.pixiv.net(\/en)?\/search/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.BookMarkNew]: {
		PageTypeString: "BookMarkNewPage",
		CheckUrl: function(url) {
			return /^https:\/\/www.pixiv.net(\/en)?\/bookmark_new_illust(_r18)?.php.*/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.Discovery]: {
		PageTypeString: "DiscoveryPage",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/discovery.*/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.Member]: {
		PageTypeString: "MemberPage/MemberIllustPage/MemberBookMark",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/users\/\d+/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.Home]: {
		PageTypeString: "HomePage",
		CheckUrl: function(url) {
			return /https?:\/\/www.pixiv.net(\/en)?\/?$/.test(url) || /https?:\/\/www.pixiv.net(\/en)?\/illustration\/?$/.test(url) || /https?:\/\/www.pixiv.net(\/en)?\/manga\/?$/.test(url) || /https?:\/\/www.pixiv.net(\/en)?\/cate_r18\.php$/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.Ranking]: {
		PageTypeString: "RankingPage",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/ranking.php.*/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.NewIllust]: {
		PageTypeString: "NewIllustPage",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/new_illust.php.*/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.R18]: {
		PageTypeString: "R18Page",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/cate_r18.php.*/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.Stacc]: {
		PageTypeString: "StaccPage",
		CheckUrl: function(url) {
			return /^https:\/\/www.pixiv.net(\/en)?\/stacc.*/.test(url);
		},
		GetToolBar: function() {
			return getToolbarOld();
		}
	},
	[PageType.Artwork]: {
		PageTypeString: "ArtworkPage",
		CheckUrl: function(url) {
			return /^https:\/\/www.pixiv.net(\/en)?\/artworks\/.*/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.NovelSearch]: {
		PageTypeString: "NovelSearchPage",
		CheckUrl: function(url) {
			return /^https:\/\/www.pixiv.net(\/en)?\/tags\/.*\/novels/.test(url);
		},
		GetToolBar: getToolbar
	},
	[PageType.SearchTop]: {
		PageTypeString: "SearchTopPage",
		CheckUrl: function(url) {
			return /^https?:\/\/www.pixiv.net(\/en)?\/tags\/[^/*]/.test(url);
		},
		GetToolBar: getToolbar
	}
};
function getToolbar() {
	const toolbar = $(`#${TOOLBAR_ID}`);
	if (toolbar.length > 0) return toolbar.get(0);
	$("body").append(`<div id="${TOOLBAR_ID}" style="position: fixed; right: 28px; bottom: 96px;"></div>`);
	return $(`#${TOOLBAR_ID}`).get(0);
}
function getToolbarOld() {
	return $("._toolmenu").get(0);
}
function showSearchLinksForDeletedArtworks() {
	const searchEngines = [
		{
			name: "Google",
			url: "https://www.google.com/search?q="
		},
		{
			name: "Bing",
			url: "https://www.bing.com/search?q="
		},
		{
			name: "Baidu",
			url: "https://www.baidu.com/s?wd="
		}
	];
	document.querySelectorAll("span[to]").forEach((span) => {
		const artworkPath = span.getAttribute("to");
		if (span.textContent.trim() === "-----" && artworkPath.startsWith("/artworks/")) {
			const keyword = `pixiv "${artworkPath.slice(10)}"`;
			const container = document.createElement("span");
			container.className = span.className;
			searchEngines.forEach((engine, i) => {
				const link = document.createElement("a");
				link.href = engine.url + encodeURIComponent(keyword);
				link.textContent = engine.name;
				link.target = "_blank";
				container.appendChild(link);
				if (i < searchEngines.length - 1) container.appendChild(document.createTextNode(" | "));
			});
			span.parentNode.replaceChild(container, span);
		}
	});
}
let menuIds = [];
const registerSettingsMenu = () => {
	const settings = getSettings();
	for (const menuId of menuIds) GM_unregisterMenuCommand(menuId);
	menuIds = [];
	menuIds.push(GM_registerMenuCommand(`🖼️ 插画作品预览 ${settings.enablePreview ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("enablePreview");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🎦 动图作品预览 ${settings.enableAnimePreview ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("enableAnimePreview");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🕗 延迟 ${settings.previewDelay} 毫秒显示预览图`, () => {
		setSettingStringValue("previewDelay", "延迟显示预览图时间(毫秒)", {
			parseValue: (newValue) => Number(newValue) || g_defaultSettings.previewDelay,
			onSet: () => registerSettingsMenu()
		});
	}), GM_registerMenuCommand(`📚️ 每次排序 ${settings.pageCount} 页`, () => {
		setSettingStringValue("pageCount", "每次排序的页数", {
			parseValue: (newValue) => Number(newValue) || g_defaultSettings.pageCount,
			onSet: () => registerSettingsMenu()
		});
	}), GM_registerMenuCommand(`👨‍👩‍👧 排序隐藏收藏数少于 ${settings.favFilter} 的作品`, () => {
		setSettingStringValue("favFilter", "排序隐藏少于设定收藏数的作品", {
			parseValue: (newValue) => Number(newValue) || g_defaultSettings.favFilter,
			onSet: () => registerSettingsMenu()
		});
	}), GM_registerMenuCommand(`🎨 按照 ${settings.orderType === IllustSortOrder.BY_BOOKMARK_COUNT ? "作品收藏数" : "作品发布时间"} 排序作品`, () => {
		setSettingValue("orderType", settings.orderType === IllustSortOrder.BY_BOOKMARK_COUNT ? IllustSortOrder.BY_DATE : IllustSortOrder.BY_BOOKMARK_COUNT);
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🤖 排序过滤 AI 生成作品 ${settings.aiFilter ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("aiFilter");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🦾 排序过滤 AI 辅助(加笔)作品 ${settings.aiAssistedFilter ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("aiAssistedFilter");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`❤️ 排序过滤已收藏作品 ${settings.hideFavorite ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("hideFavorite");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🔖 排序过滤包含指定标签的作品 ${settings.hideByTag ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("hideByTag");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🔖 排序过滤的标签:${settings.hideByTagList}`, () => {
		setSettingStringValue("hideByTagList", "过滤的标签列表,使用`,`分隔不同标签", { onSet: () => registerSettingsMenu() });
	}), GM_registerMenuCommand(`📑 在新标签页打开作品 ${settings.linkBlank ? "✅" : "❌"}`, () => {
		toggleSettingBooleanValue("linkBlank");
		registerSettingsMenu();
	}), GM_registerMenuCommand(`🔁 重置设置`, () => {
		if (confirm("您确定要重置所有设置到脚本的默认值吗?")) {
			resetSettings();
			location.reload();
		}
	}));
	return settings;
};
const ShowUpgradeMessage = () => {
	$("#pp-bg").remove();
	const bg = $("<div id=\"pp-bg\"></div>").css({
		position: "fixed",
		"z-index": 9999,
		"background-color": "rgba(0, 0, 0, 0.8)",
		inset: "0px"
	});
	$("body").append(bg);
	bg.get(0).innerHTML = "<img id=\"pps-close\" src=\"https://pp-1252089172.cos.ap-chengdu.myqcloud.com/Close.png\"style=\"position: absolute; right: 35px; top: 20px; width: 32px; height: 32px; cursor: pointer;\"><div style=\"position: absolute; width: 40%; left: 30%; top: 25%; font-size: 25px; font-weight: bold; text-align: center; color: white;\">" + Texts.install_title + g_version + "</div><br><div style=\"position: absolute; left: 50%; top: 35%; font-size: 20px; color: white; transform: translate(-50%,0); height: 50%; overflow: auto;\">" + Texts.upgrade_body + "</div>";
	$("#pps-close").on("click", () => {
		setSettingValue("version", g_version);
		$("#pp-bg").remove();
	});
};
const initializePixivPreviewer = () => {
	try {
		g_settings = registerSettingsMenu();
		iLog.i("Start to initialize Pixiv Previewer with global settings:", g_settings);
		if (g_settings.version !== "1.4.3") ShowUpgradeMessage();
		if (g_settings.enablePreview) loadIllustPreview(g_settings);
		$.get(location.href, function(data) {
			const matched = data.match(/token\\":\\"([a-z0-9]{32})/);
			if (matched.length > 0) {
				g_csrfToken = matched[1];
				DoLog(LogLevel.Info, "Got g_csrfToken: " + g_csrfToken);
				loadIllustSort({
					...g_settings,
					csrfToken: g_csrfToken
				});
			} else DoLog(LogLevel.Error, "Can not get g_csrfToken, sort function is disabled.");
		});
		for (let i = 0; i < Object.keys(Pages).length; i++) if (Pages[i].CheckUrl(location.href)) {
			g_pageType = i;
			break;
		}
		if (g_pageType !== void 0) DoLog(LogLevel.Info, "Current page is " + Pages[g_pageType].PageTypeString);
		else {
			DoLog(LogLevel.Info, "Unsupported page.");
			return;
		}
		if (g_pageType === PageType.Member) showSearchLinksForDeletedArtworks();
		else if (g_pageType === PageType.Artwork) {
			const artworkId = window.location.pathname.match(/\/artworks\/(\d+)/)?.[1];
			if (artworkId) setTimeout(() => {
				deleteCachedIllustrationDetails([artworkId]);
			});
		}
		const toolBar = Pages[g_pageType].GetToolBar();
		if (toolBar) DoLog(LogLevel.Elements, toolBar);
		else {
			DoLog(LogLevel.Warning, "Get toolbar failed.");
			return;
		}
		if (!$(`#${"pp-sort"}`).length) {
			const newListItem = document.createElement("div");
			newListItem.title = "Sort artworks";
			newListItem.innerHTML = "";
			const newButton = document.createElement("button");
			newButton.id = SORT_BUTTON_ID;
			newButton.style.cssText = "box-sizing: border-box; background-color: rgba(0,0,0,0.32); color: #fff; margin-top: 5px; opacity: 0.8; cursor: pointer; border: none; padding: 0px; border-radius: 24px; width: 48px; height: 48px; font-size: 12px; font-weight: bold;";
			newButton.innerHTML = Texts.label_sort;
			newListItem.appendChild(newButton);
			toolBar.appendChild(newListItem);
			$(newButton).on("click", () => {
				const sortEvent = new Event(SORT_EVENT_NAME);
				window.dispatchEvent(sortEvent);
			});
		}
		if (!$(`#${"pp-sort-next-page"}`).length) {
			const newListItem = document.createElement("div");
			newListItem.title = "Jump to next page";
			newListItem.innerHTML = "";
			const newButton = document.createElement("button");
			newButton.id = SORT_NEXT_PAGE_BUTTON_ID;
			newButton.style.cssText = "box-sizing: border-box; background-color: rgba(0,0,0,0.32); color: #fff; margin-top: 5px; opacity: 0.8; cursor: pointer; border: none; padding: 0px; border-radius: 24px; width: 48px; height: 48px; font-size: 12px; font-weight: bold;";
			newButton.innerHTML = Texts.label_nextPage;
			newListItem.appendChild(newButton);
			toolBar.appendChild(newListItem);
			$(newButton).on("click", () => {
				const sortEvent = new Event(SORT_NEXT_PAGE_EVENT_NAME);
				window.dispatchEvent(sortEvent);
			});
		}
		if (!$(`#${"pp-hide-favorites"}`).length) {
			const newListItem = document.createElement("div");
			newListItem.title = "Hide favorite illustrations";
			newListItem.innerHTML = "";
			const newButton = document.createElement("button");
			newButton.id = HIDE_FAVORITES_BUTTON_ID;
			newButton.style.cssText = "box-sizing: border-box; background-color: rgba(0,0,0,0.32); color: #fff; margin-top: 5px; opacity: 0.8; cursor: pointer; border: none; padding: 0px; border-radius: 24px; width: 48px; height: 48px; font-size: 12px; font-weight: bold;";
			newButton.innerHTML = Texts.label_hideFav;
			newListItem.appendChild(newButton);
			toolBar.appendChild(newListItem);
			$(newButton).on("click", () => {
				hideFavorites();
			});
		}
	} catch (e) {
		DoLog(LogLevel.Error, "An error occurred while initializing:", e);
	}
};
window.addEventListener("DOMContentLoaded", () => {
	setTimeout(initializePixivPreviewer, 1e3);
});

//#endregion