// ==UserScript==
// @name Kemer Enhance
// @name:zh-TW Kemer 增強
// @name:zh-CN Kemer 增强
// @name:ja Kemer 強化
// @name:ko Kemer 강화
// @name:ru Kemer Улучшение
// @name:en Kemer Enhance
// @version 2025.10.13-Beta
// @author Canaan HS
// @description 美化介面與操作增強,增加額外功能,提供更好的使用體驗
// @description:zh-TW 美化介面與操作增強,增加額外功能,提供更好的使用體驗
// @description:zh-CN 美化界面与操作增强,增加额外功能,提供更好的使用体验
// @description:ja インターフェースを美化し操作を強化、追加機能により、より良い使用体験を提供します
// @description:ko 인터페이스를 미화하고 조작을 강화하며, 추가 기능을 통해 더 나은 사용 경험을 제공합니다
// @description:ru Улучшение интерфейса и функций управления, добавление дополнительных возможностей для лучшего опыта использования
// @description:en Beautify interface and enhance operations, add extra features, and provide a better user experience
// @connect *
// @match *://kemono.cr/*
// @match *://coomer.st/*
// @match *://nekohouse.su/*
// @license MPL-2.0
// @namespace https://greasyfork.org/users/989635
// @supportURL https://github.com/Canaan-HS/MonkeyScript/issues
// @icon https://cdn-icons-png.flaticon.com/512/2566/2566449.png
// @resource pako https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js
// @require https://update.greasyfork.org/scripts/487608/1676101/SyntaxLite_min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/preact/10.27.1/preact.umd.min.js
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_getResourceText
// @grant window.onurlchange
// @grant GM_registerMenuCommand
// @grant GM_addValueChangeListener
// @run-at document-body
// ==/UserScript==
(async function () {
/* Data type checks are removed in user configuration; providing incorrect input may cause it to break */
const User_Config = {
Global: {
BlockAds: true, // 阻擋廣告
CacheFetch: true, // 緩存 Fetch 請求 (僅限 JSON)
DeleteNotice: true, // 刪除上方公告
SidebarCollapse: true, // 側邊攔摺疊
KeyScroll: { mode: 1, enable: true }, // 上下鍵觸發自動滾動 [mode: 1 = 動畫偵滾動, mode: 2 = 間隔滾動] (選擇對於自己較順暢的)
TextToLink: { // 連結的 (文本 -> 超連結)
enable: true,
newtab: true, // 新選項卡開啟
newtab_active: false, // 切換焦點到新選項卡
newtab_insert: true, // 選項卡插入到當前選項卡的正後方
},
BetterPostCard: { // 修復名稱|自訂名稱|外部TAG跳轉|快速預覽內容
enable: true,
newtab: true,
newtab_active: true,
newtab_insert: true,
previewAbove: true, // 快速預覽展示於帖子上方
},
},
Preview: {
CardZoom: { mode: 3, enable: true }, // 縮放預覽卡大小 [mode: 1 = 卡片放大 , 2 = 卡片放大 + 懸浮縮放, 3 = 卡片放大 + 自動縮放]
CardText: { mode: 2, enable: true }, // 預覽卡文字效果 [mode: 1 = 隱藏文字 , 2 = 淡化文字]
BetterThumbnail: true, // 替換成內頁縮圖 (nekohouse 不支援)
QuickPostToggle: true, // 快速切換帖子 (僅支援 nekohouse)
NewTabOpens: { // 預覽頁面的帖子都以新分頁開啟
enable: true,
newtab_active: false,
newtab_insert: true,
},
},
Content: {
ExtraButton: true, // 額外的下方按鈕
LinkBeautify: true, // 下載連結美化, 當出現 (browse »), 滑鼠懸浮會直接顯示內容, 並移除多餘的字串
CommentFormat: true, // 評論區重新排版
VideoBeautify: { mode: 1, enable: true }, // 影片美化 [mode: 1 = 複製下載節點 , 2 = 移動下載節點]
OriginalImage: { // 自動原圖 [mode: 1 = 快速自動 , 2 = 慢速自動 , 3 = 觀察後觸發]
mode: 1,
enable: true,
experiment: false, // 實驗性替換方式
}
}
};
const Parame = {
Url: Lib.$url,
DB: await Lib.openDB("KemerEnhanceDB", 1, GM_getResourceText("pako")),
OriginalApi: `https://${Lib.$domain}/data`,
ThumbnailApi: `https://${Lib.$domain}/thumbnail/data`,
SaveKey: {
Img: "ImgStyle",
Lang: "Language",
Menu: "MenuPoint"
},
Artists: new RegExp(".+(?<!favorites)\\/artists.*"),
Links: /.+\/user\/[^\/]+\/links.*/,
Recommended: /.+\/user\/[^\/]+\/recommended.*/,
FavoritesArtists: /.+\/favorites\/artists.*/,
Posts: new RegExp(".+(?<!favorites)\\/posts.*"),
User: /.+\/user\/[^\/]+(\?.*)?$/,
FavorPosts: /.+\/favorites\/posts.*/,
Dms: /.+\/dms(\?.*)?$/,
Announcement: /.+\/user\/[^\/]+\/announcements.*/,
Content: /.+\/user\/.+\/post\/.+$/,
Registered: new Set(),
SupportImg: new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp", "avif", "heic", "svg"]),
VideoType: new Set(["mp4", "avi", "mkv", "mov", "flv", "wmv", "webm", "mpg", "mpeg", "m4v", "ogv", "3gp", "asf", "ts", "vob", "rm", "rmvb", "m2ts", "f4v", "mts", "mpe", "mpv", "m2v", "m4a", "bdmv", "ifo", "r3d", "braw", "cine", "qt", "f4p", "swf", "mng", "gifv", "yuv", "roq", "nsv", "amv", "svi", "mod", "mxf", "ogg"])
};
const Page = {
isContent: () => Parame.Content.test(Parame.Url),
isAnnouncement: () => Parame.Announcement.test(Parame.Url) || Parame.Dms.test(Parame.Url),
isSearch: () => Parame.Artists.test(Parame.Url) || Parame.Links.test(Parame.Url) || Parame.Recommended.test(Parame.Url) || Parame.FavoritesArtists.test(Parame.Url),
isPreview: () => Parame.Posts.test(Parame.Url) || Parame.User.test(Parame.Url) || Parame.FavorPosts.test(Parame.Url),
isNeko: Lib.$domain.startsWith("nekohouse")
};
const Load = (() => {
const color = {
kemono: "#e8a17d !important",
coomer: "#99ddff !important",
nekohouse: "#bb91ff !important"
}[Lib.$domain.split(".")[0]];
const userSet = {
menuSet: () => Lib.getV(Parame.SaveKey.Menu, {
Top: "10vh",
Left: "10vw"
}),
imgSet: () => Lib.getV(Parame.SaveKey.Img, {
Width: "auto",
Height: "auto",
Spacing: "0px",
MaxWidth: "100%"
})
};
return {
...userSet,
color: color
};
})();
async function SidebarCollapse() {
if (Lib.platform === "Mobile") return;
Lib.addStyle(`
.global-sidebar {
opacity: 0;
height: 100%;
width: 10rem;
display: flex;
position: fixed;
padding: 0.5em 0;
transition: 0.8s;
background: #282a2e;
flex-direction: column;
transform: translateX(-9rem);
}
.global-sidebar:hover {opacity: 1; transform: translateX(0rem);}
.content-wrapper.shifted {transition: 0.7s; margin-left: 0rem;}
.global-sidebar:hover + .content-wrapper.shifted {margin-left: 10rem;}
`, "Collapse-Effects", false);
}
async function DeleteNotice() {
Lib.waitEl("#announcement-banner", null, {
throttle: 50,
timeout: 10
}).then(announcement => announcement.remove());
}
async function KeyScroll({
mode
}) {
if (Lib.platform === "Mobile" || Parame.Registered.has("KeyScroll")) return;
const Scroll_Requ = {
Scroll_Pixels: 2,
Scroll_Interval: 800
};
const UP_ScrollSpeed = Scroll_Requ.Scroll_Pixels * -1;
let Scroll, Up_scroll = false, Down_scroll = false;
const [TopDetected, BottomDetected] = [Lib.$throttle(() => {
Up_scroll = Lib.sY == 0 ? false : true;
}, 600), Lib.$throttle(() => {
Down_scroll = Lib.sY + Lib.iH >= Lib.html.scrollHeight ? false : true;
}, 600)];
switch (mode) {
case 2:
Scroll = Move => {
const Interval = setInterval(() => {
if (!Up_scroll && !Down_scroll) {
clearInterval(Interval);
}
if (Up_scroll && Move < 0) {
window.scrollBy(0, Move);
TopDetected();
} else if (Down_scroll && Move > 0) {
window.scrollBy(0, Move);
BottomDetected();
}
}, Scroll_Requ.Scroll_Interval);
};
default:
Scroll = Move => {
if (Up_scroll && Move < 0) {
window.scrollBy(0, Move);
TopDetected();
requestAnimationFrame(() => Scroll(Move));
} else if (Down_scroll && Move > 0) {
window.scrollBy(0, Move);
BottomDetected();
requestAnimationFrame(() => Scroll(Move));
}
};
}
Lib.onEvent(window, "keydown", Lib.$throttle(event => {
const key = event.key;
if (key == "ArrowUp") {
event.stopImmediatePropagation();
event.preventDefault();
if (Up_scroll) {
Up_scroll = false;
} else if (!Up_scroll || Down_scroll) {
Down_scroll = false;
Up_scroll = true;
Scroll(UP_ScrollSpeed);
}
} else if (key == "ArrowDown") {
event.stopImmediatePropagation();
event.preventDefault();
if (Down_scroll) {
Down_scroll = false;
} else if (Up_scroll || !Down_scroll) {
Up_scroll = false;
Down_scroll = true;
Scroll(Scroll_Requ.Scroll_Pixels);
}
}
}, 100), {
capture: true
});
Parame.Registered.add("KeyScroll");
}
async function BlockAds() {
if (Page.isNeko) return;
const cookieString = Lib.cookie();
const required = ["ts_popunder", "ts_popunder-cnt"];
const hasCookies = required.every(name => new RegExp(`(?:^|;\\s*)${name}=`).test(cookieString));
if (!hasCookies) {
const now = new Date();
now.setFullYear(now.getFullYear() + 1);
const expires = now.toUTCString();
const cookies = {
[required[0]]: now,
[required[1]]: 1
};
for (const [key, value] of Object.entries(cookies)) {
Lib.cookie(`${key}=${value}; domain=.${Lib.$domain}; path=/; expires=${expires};`);
}
}
if (Parame.Registered.has("BlockAds")) return;
Lib.addStyle(`
.root--ujvuu, [id^="ts_ad_native_"], [id^="ts_ad_video_"] {display: none !important}
`, "Ad-blocking-style");
const domains = new Set(["go.mnaspm.com", "go.reebr.com", "creative.reebr.com", "tsyndicate.com", "tsvideo.sacdnssedge.com"]);
const originalRequest = unsafeWindow.XMLHttpRequest;
unsafeWindow.XMLHttpRequest = new Proxy(originalRequest, {
construct: function (target, args) {
const xhr = new target(...args);
return new Proxy(xhr, {
get: function (target2, prop, receiver) {
if (prop === "open") {
return function (method, url) {
try {
if (typeof url !== "string" || url.endsWith(".m3u8")) return;
if ((url.startsWith("http") || url.startsWith("//")) && domains.has(new URL(url).host)) return;
} catch { }
return target2[prop].apply(target2, arguments);
};
}
return Reflect.get(target2, prop, receiver);
}
});
}
});
Parame.Registered.add("BlockAds");
}
async function CacheFetch() {
if (Page.isNeko || Parame.Registered.has("CacheFetch")) return;
const cacheKey = "fetch_cache_data";
const cache = await Parame.DB.get(cacheKey, new Map());
const saveCache = Lib.$debounce(() => {
Parame.DB.set(cacheKey, cache, {
expireStr: "5m"
});
}, 1e3);
const originalFetch = {
Sandbox: window.fetch,
Window: unsafeWindow.fetch
};
window.fetch = (...args) => fetchWrapper(originalFetch.Sandbox, ...args);
unsafeWindow.fetch = (...args) => fetchWrapper(originalFetch.Window, ...args);
async function fetchWrapper(windowContext, ...args) {
const input = args[0];
const options = args[1] || {};
if (!input) return windowContext(...args);
const url = typeof input === "string" ? input : input.url;
const method = options.method || (typeof input === "object" ? input.method : "GET") || "GET";
if (method.toUpperCase() !== "GET" || options.headers?.["X-Bypass-CacheFetch"] || url.endsWith("random")) {
return windowContext(...args);
}
if (cache.has(url)) {
const cached = cache.get(url);
return new Response(cached.body, {
status: cached.status,
headers: cached.headers
});
}
try {
const response = await windowContext(...args);
if (response.status === 200 && (url.includes("api") || url.includes("default_config"))) {
(async () => {
try {
const responseClone = response.clone();
const bodyText = await responseClone.text();
if (bodyText) {
const headersObject = {};
responseClone.headers.forEach((value, key) => {
headersObject[key] = value;
});
cache.set(url, {
body: bodyText,
status: responseClone.status,
headers: headersObject
});
saveCache();
}
} catch { }
})();
}
return response;
} catch (error) {
throw error;
}
}
Parame.Registered.add("CacheFetch");
}
function megaUtils(urlRegex) {
const megaPDecoder = (() => {
const encoder = new TextEncoder();
const ITER = 1e5;
const urlBase64ToBase64 = s => s.replace(/-/g, "+").replace(/_/g, "/").replace(/,/g, "");
function base64ToBytes(b64) {
try {
const raw = atob(b64);
const n = raw.length;
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) out[i] = raw.charCodeAt(i);
return out;
} catch (e) {
return null;
}
}
function bytesToBase64Url(bytes) {
let bin = "";
for (let i = 0, L = bytes.length; i < L; i++) bin += String.fromCharCode(bytes[i]);
let b64 = btoa(bin);
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function equalBytesConstTime(a, b) {
if (!a || !b || a.length !== b.length) return false;
let r = 0;
for (let i = 0, L = a.length; i < L; i++) r |= a[i] ^ b[i];
return r === 0;
}
function xorInto(a, b) {
const n = a.length;
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) out[i] = a[i] ^ b[i];
return out;
}
async function importPwKey(password) {
return crypto.subtle.importKey("raw", encoder.encode(password), {
name: "PBKDF2"
}, false, ["deriveBits"]);
}
async function deriveDK(pwKey, salt) {
const bits = await crypto.subtle.deriveBits({
name: "PBKDF2",
salt: salt,
iterations: ITER,
hash: "SHA-512"
}, pwKey, 512);
return new Uint8Array(bits);
}
async function importMacKey(raw) {
return crypto.subtle.importKey("raw", raw, {
name: "HMAC",
hash: "SHA-256"
}, false, ["sign"]);
}
return async (pFragmentOrFull, password) => {
try {
if (!pFragmentOrFull || !password) return pFragmentOrFull;
let s = String(pFragmentOrFull);
const idx = s.indexOf("#P!");
if (idx >= 0) s = s.slice(idx + 3);
if (s.toUpperCase().startsWith("P!")) s = s.slice(2);
let b64 = urlBase64ToBase64(s);
const mod = b64.length % 4;
if (mod !== 0) b64 += "=".repeat(4 - mod);
const data = base64ToBytes(b64);
if (!data || data.length < 1 + 1 + 6 + 32 + 32) {
return pFragmentOrFull;
}
const algorithm = data[0];
const type = data[1];
const publicHandle = data.subarray(2, 8);
const salt = data.subarray(8, 40);
const macTag = data.subarray(data.length - 32);
const encryptedKey = data.subarray(40, data.length - 32);
const keyLen = encryptedKey.length;
const pwKey = await importPwKey(password);
const dk = await deriveDK(pwKey, salt);
if (dk.length < 64 || dk.length < 32 + 32) {
return pFragmentOrFull;
}
const xorKey = dk.subarray(0, keyLen);
const macKey = dk.subarray(32, 64);
const recoveredKey = xorInto(encryptedKey, xorKey);
const msgLen = 1 + 1 + publicHandle.length + salt.length + encryptedKey.length;
const msg = new Uint8Array(msgLen);
let off = 0;
msg[off++] = algorithm;
msg[off++] = type;
msg.set(publicHandle, off);
off += publicHandle.length;
msg.set(salt, off);
off += salt.length;
msg.set(encryptedKey, off);
const macCryptoKey = await importMacKey(macKey);
const macBuffer = await crypto.subtle.sign("HMAC", macCryptoKey, msg);
const mac = new Uint8Array(macBuffer);
if (!equalBytesConstTime(mac, macTag)) {
return pFragmentOrFull;
}
const handleB64Url = bytesToBase64Url(publicHandle);
const keyB64Url = bytesToBase64Url(recoveredKey);
const fileType = type === 0 ? "folder" : "file";
return `https://mega.nz/${fileType}/${handleB64Url}#${keyB64Url}`;
} catch (e) {
return pFragmentOrFull;
}
};
})();
const getDecryptedUrl = async (url, password) => await megaPDecoder(url, password);
const passwordCleaner = text => text.match(/^(Password|Pass|Key)\s*:?\s*(.*)$/i)?.[2]?.trim() ?? "";
const extractRegex = /(https?:\/\/mega\.nz\/#P![A-Za-z0-9_-]+).*?(?:Password|Pass|Key)\b[\s:]*(?:<[^>]+>)?([\p{L}\p{N}\p{P}_-]+)(?:<[^>]+>)?/gisu;
function extractPasswords(data) {
const result = {};
if (typeof data === "string") {
let match;
while ((match = extractRegex.exec(data)) !== null) {
result[match[1]] = match[2]?.trim() ?? "";
}
}
return result;
}
function parsePassword(href, text) {
let state = false;
if (!text) return {
state: state,
href: href
};
const lowerText = text.toLowerCase();
if (text.startsWith("#")) {
state = true;
href += text;
} else if (/^[A-Za-z0-9_!F-]{16,43}$/.test(text)) {
state = true;
href += "#" + text;
} else if (lowerText.startsWith("pass") || lowerText.startsWith("key")) {
const key = passwordCleaner(text);
if (key) {
state = true;
href += "#" + key;
}
}
return {
state: state,
href: href.match(urlRegex)?.[0] ?? href
};
}
async function getPassword(node, href) {
let state;
const nextNode = node.nextSibling;
if (nextNode) {
if (nextNode.nodeType === Node.TEXT_NODE) {
({
state,
href
} = parsePassword(href, nextNode.$text()));
if (state) nextNode?.remove();
} else if (nextNode.nodeType === Node.ELEMENT_NODE) {
const nodeText = [...nextNode.childNodes].find(node2 => node2.nodeType === Node.TEXT_NODE)?.$text() ?? "";
({
state,
href
} = parsePassword(href, nodeText));
}
}
return href;
}
return {
getPassword: getPassword,
getDecryptedUrl: getDecryptedUrl,
extractPasswords: extractPasswords
};
}
const TextToLinkFactory = () => {
let mega;
const exclusionRegex = /onfanbokkusuokibalab\.net/;
const urlRegex = /(?:(?:https?|ftp|mailto|file|data|blob|ws|wss|ed2k|thunder):\/\/|(?:[-\w]+\.)+[a-zA-Z]{2,}(?:\/|$)|\w+@[-\w]+\.[a-zA-Z]{2,})[^\s]*?(?=[{}「」『』【】\[\]()()<>、"',。!?;:…—~~]|$|\s)/gi;
const exclusionTags = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "SVG", "CANVAS", "IFRAME", "AUDIO", "VIDEO", "EMBED", "OBJECT", "SOURCE", "TRACK", "CODE", "KBD", "SAMP", "TEMPLATE", "SLOT", "PARAM", "META", "LINK", "IMG", "PICTURE", "FIGURE", "FIGCAPTION", "MATH", "PORTAL", "METER", "PROGRESS", "OUTPUT", "TEXTAREA", "SELECT", "OPTION", "DATALIST", "FIELDSET", "LEGEND", "MAP", "AREA"]);
const urlMatch = str => {
urlRegex.lastIndex = 0;
return urlRegex.test(str);
};
const uriFormat1 = /^[a-zA-Z][\w+.-]*:\/\//;
const uriFormat2 = /^[a-zA-Z][\w+.-]*:/;
const uriFormat3 = /^([\w-]+\.)+[a-z]{2,}(\/|$)/i;
const uriFormat4 = /^\/\//;
const protocolParse = uri => {
if (uriFormat1.test(uri) || uriFormat2.test(uri)) return uri;
if (uriFormat3.test(uri)) return "https://" + uri;
if (uriFormat4.test(uri)) return "https:" + uri;
return uri;
};
const jumpTrigger = async (root, {
newtab,
newtab_active,
newtab_insert
}) => {
const [active, insert] = [newtab_active, newtab_insert];
Lib.onEvent(root, "click", event => {
const target = event.target.closest("a:not(.fileThumb)");
if (!target || target.$hAttr("download")) return;
event.preventDefault();
!newtab ? location.assign(target.href) : GM_openInTab(target.href, {
active: active,
insert: insert
});
}, {
capture: true
});
};
const getTextNodeMap = root => {
const nodes = new Map();
const tree = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: node2 => {
const parentElement = node2.parentElement;
if (!parentElement || exclusionTags.has(parentElement.tagName)) return NodeFilter.FILTER_REJECT;
const content = node2.$text();
if (!content || exclusionRegex.test(content)) return NodeFilter.FILTER_REJECT;
return content === "(frame embed)" || urlMatch(content) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});
let node, parent, pack;
while (node = tree.nextNode()) {
parent = node.parentElement;
pack = nodes.get(parent);
if (pack === void 0) {
pack = [];
nodes.set(parent, pack);
}
pack.push(node);
}
return nodes;
};
async function parseModify(container, father, text, textNode = null, complex = false) {
let modifyUrl, passwordDict = {};
if (text === "(frame embed)") {
const a = father.closest("a");
if (!a) return;
const href = a.href;
if (!href) return;
if (href.includes("mega.nz/#P!")) {
mega ??= megaUtils(urlRegex);
passwordDict = mega.extractPasswords(container.$oHtml());
}
if (passwordDict[href]) modifyUrl = await mega.getDecryptedUrl(href, passwordDict[href]);
if (modifyUrl && modifyUrl !== href) {
a.href = modifyUrl;
a.$text(modifyUrl);
} else {
a.$text(href);
}
} else if (complex) {
textNode.replaceWith(Lib.createDomFragment(text.replace(urlRegex, url => {
const decode = decodeURIComponent(url).trim();
return `<a href="${protocolParse(decode)}" rel="noopener noreferrer">${decode}</a>`;
})));
} else {
if (text.match(urlRegex).length === 0) return;
if (text.includes("mega.nz/#P!")) {
mega ??= megaUtils(urlRegex);
passwordDict = mega.extractPasswords(text);
}
let url, index, lastIndex = 0;
const segments = [];
for (const match of text.matchAll(urlRegex)) {
url = match[0];
index = match.index;
if (index > lastIndex) segments.push(text.slice(lastIndex, index));
modifyUrl = decodeURIComponent(url).trim();
if (passwordDict[url]) modifyUrl = await mega.getDecryptedUrl(url, passwordDict[url]);
segments.push(`<a href="${protocolParse(modifyUrl)}" rel="noopener noreferrer">${modifyUrl}</a>`);
lastIndex = index + url.length;
}
if (lastIndex < text.length) {
segments.push(text.slice(lastIndex));
}
father.tagName === "A" ? father.replaceWith(Lib.createDomFragment(segments.join(""))) : father.$iHtml(segments.join(""));
}
}
return {
async TextToLink(config) {
if (!Page.isContent() && !Page.isAnnouncement()) return;
if (Page.isContent()) {
Lib.waitEl(".post__body, .scrape__body", null).then(async body => {
let [article, content] = [body.$q("article"), body.$q(".post__content, .scrape__content")];
if (article) {
jumpTrigger(content, config);
let span;
for (span of article.$qa("span.choice-text")) {
parseModify(article, span, span.$text());
}
} else if (content) {
jumpTrigger(content, config);
let parentNode, text, textNode, data, dataLength;
for ([parentNode, data] of getTextNodeMap(content).entries()) {
dataLength = data.length;
for (textNode of data) {
text = textNode.$text();
if (text.startsWith("https://mega.nz")) {
mega ??= megaUtils(urlRegex);
text = await mega.getPassword(parentNode, text);
}
parseModify(content, parentNode, text, textNode, dataLength > 1);
}
}
} else {
const attachments = body.$q(".post__attachments, .scrape__attachments");
attachments && jumpTrigger(attachments, config);
}
});
} else if (Page.isAnnouncement()) {
Lib.waitEl(".card-list__items pre", null, {
raf: true
}).then(() => {
const items = Lib.$q(".card-list__items");
jumpTrigger(items, config);
let parentNode, textNode, data, dataLength;
for ([parentNode, data] of getTextNodeMap(items).entries()) {
dataLength = data.length;
for (textNode of data) {
parseModify(items, parentNode, textNode.$text(), textNode, dataLength > 1);
}
}
});
}
}
};
};
const Fetch = (() => {
const responseRule = {
text: res => res.text(),
json: res => res.json(),
blob: res => res.blob(),
arrayBuffer: res => res.arrayBuffer(),
formData: res => res.formData(),
document: async res => {
res = await res.text();
return Lib.domParse(res);
}
};
const fetchRecord = {};
const abort = url => {
fetchRecord[url]?.abort();
delete fetchRecord[url];
};
async function send(url, callback, {
responseType = "json",
headers = {
Accept: "text/css"
}
} = {}) {
try {
fetchRecord[url]?.abort();
} catch { }
const controller = new AbortController();
fetchRecord[url] = controller;
return new Promise((resolve, reject) => {
fetch(url, {
headers: headers,
signal: controller.signal
}).then(async response => {
if (!response.ok) {
const text = await response.text();
throw new Error(`
Fetch failed
url: ${response.url}
status: ${response.status}
statusText: ${text}`);
}
try {
return await responseRule[responseType](response);
} catch { }
}).then(res => {
resolve(res);
callback?.(res);
}).catch(error => {
reject(error);
Lib.log(error).error;
}).finally(() => {
delete fetchRecord[url];
});
});
}
return {
abort: abort,
send: send
};
})();
const BetterPostCardFactory = async () => {
const oldKey = "fix_record_v2";
const recordKey = "better_post_record";
const oldRecord = Lib.getLocal(oldKey);
if (oldRecord instanceof Array) {
const r = await Parame.Parame.DB.set(recordKey, new Map(oldRecord));
r === recordKey && Lib.delLocal(oldKey);
}
let recordCache;
const fixCache = new Map();
const init = async () => {
recordCache = await getRecord();
};
const getRecord = async () => await Parame.DB.get(recordKey, new Map());
const saveRecord = async save => {
await Parame.DB.set(recordKey, new Map([...await getRecord(), ...save]));
fixCache.clear();
};
const saveWork = Lib.$debounce(() => saveRecord(fixCache), 1e3);
const fixRequest = async (url, headers = {}) => {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: headers,
responseType: "json",
onload: response => resolve(response),
onerror: () => resolve(),
ontimeout: () => resolve()
});
});
};
const replaceUrlTail = (url, tail) => {
const uri = new URL(url);
uri.pathname = tail;
url = uri.href;
return url;
};
const uriFormat1 = /\/([^\/]+)\/(?:user|server|creator|fanclubs)\/([^\/?]+)/;
const uriFormat2 = /\/([^\/]+)\/([^\/]+)$/;
const uriFormat3 = /^https?:\/\/([^.]+)\.([^.]+)\./;
const specialServer = {
x: "twitter",
maker_id: "dlsite"
};
const supportServer = /Gumroad|Patreon|Fantia|Pixiv|Fanbox|CandFans|Twitter|Boosty|OnlyFans|Fansly|SubscribeStar|DLsite/i;
const parseUrlInfo = uri => {
uri = uri.match(uriFormat1) || uri.match(uriFormat2) || uri.match(uriFormat3);
if (!uri) return;
return uri.splice(1).reduce((acc, str) => {
if (supportServer.test(str)) {
const cleanStr = str.replace(/\/?(www\.|\.com|\.to|\.jp|\.net|\.adult|user\?u=)/g, "");
acc.server = specialServer[cleanStr] ?? cleanStr;
} else {
acc.user = str;
}
return acc;
}, {});
};
const getPixivName = async id => {
const response = await fixRequest(`https://www.pixiv.net/ajax/user/${id}?full=1&lang=ja`, {
referer: "https://www.pixiv.net/"
});
if (response.status === 200) {
const user = response.response;
let user_name = user.body.name;
user_name = user_name.replace(/(c\d+)?([日月火水木金土]曜日?|[123123一二三]日目?)[東南西北]..?\d+\w?/i, "");
user_name = user_name.replace(/[@@]?(fanbox|fantia|skeb|ファンボ|リクエスト|お?仕事|新刊|単行本|同人誌)+(.*(更新|募集|公開|開設|開始|発売|販売|委託|休止|停止)+中?[!!]?$|$)/gi, "");
user_name = user_name.replace(/\(\)|()|「」|【】|[@@__]+$/g, "").trim();
return user_name;
} else return;
};
const getCandfansName = async id => {
const response = await fixRequest(`https://candfans.jp/api/contents/get-timeline?user_id=${id}&record=1`);
if (response.status === 200) {
const user = response.response.data[0];
const user_code = user?.user_code || "";
const username = user?.username || "";
return [user_code, username];
} else return;
};
const candfansPageAdapt = (oldId, newId, oldUrl, oldName, newName) => {
if (Page.isSearch()) {
oldId = newId || oldId;
} else {
oldUrl = newId ? replaceUrlTail(oldUrl, newId) : oldUrl;
}
oldName = newName || oldName;
return [oldId, oldUrl, oldName];
};
const supportFixName = new Set(["pixiv", "fanbox", "candfans"]);
const supportFixTag = {
ID: /Gumroad|Patreon|Fantia|Pixiv|Fanbox|CandFans/gi,
NAME: /Twitter|Boosty|OnlyFans|Fansly|SubscribeStar|DLsite/gi,
Fantia: "https://fantia.jp/fanclubs/{id}/posts",
FantiaPost: "https://fantia.jp/posts/{id}",
Patreon: "https://www.patreon.com/user?u={id}",
PatreonPost: "https://www.patreon.com/posts/{id}",
DLsite: "https://www.dlsite.com/maniax/circle/profile/=/maker_id/{name}.html",
DLsitePost: "https://www.dlsite.com/maniax/work/=/product_id/{name}.html",
CandFans: "https://candfans.jp/{id}",
CandFansPost: "https://candfans.jp/posts/comment/show/{id}",
Gumroad: "https://gumroad.com/{id}",
Pixiv: "https://www.pixiv.net/users/{id}/artworks",
Fanbox: "https://www.pixiv.net/fanbox/creator/{id}",
Boosty: "https://boosty.to/{name}",
SubscribeStar: "https://subscribestar.adult/{name}",
Twitter: "https://x.com/{name}",
OnlyFans: "https://onlyfans.com/{name}",
Fansly: "https://fansly.com/{name}/posts"
};
async function fixUpdateUi(mainUrl, otherUrl, user, nameEl, tagEl, showText, appendTag) {
nameEl.$sAttr("style", "display: none;");
if (nameEl.previousElementSibling?.tagName !== "FIX_WRAPPER") {
nameEl.$iAdjacent(`
<fix_wrapper>
<fix_name jump="${mainUrl}">${showText.trim()}</fix_name>
<fix_edit id="${user}">Edit</fix_edit>
</fix_wrapper>
`, "beforebegin");
}
const [tag_text, support_id, support_name] = [tagEl.$text(), supportFixTag.ID, supportFixTag.NAME];
if (!tag_text) return;
const [mark, matchId] = support_id.test(tag_text) ? ["{id}", support_id] : support_name.test(tag_text) ? ["{name}", support_name] : ["", null];
if (!mark) return;
tagEl.$iHtml(tag_text.replace(matchId, tag => {
let supported = false;
const supportFormat = appendTag ? (supported = supportFixTag[`${tag}${appendTag}`],
supported ? (user = parseUrlInfo(otherUrl).user, supported) : supportFixTag[tag]) : supportFixTag[tag];
return `<fix_tag jump="${supportFormat.replace(mark, user)}">${tag}</fix_tag>`;
}));
}
async function fixTrigger(data) {
let {
mainUrl,
otherUrl,
server,
user,
nameEl,
tagEl,
appendTag
} = data;
let recordName = recordCache?.get(user);
if (recordName) {
if (server === "candfans") {
[user, mainUrl, recordName] = candfansPageAdapt(user, recordName[0], mainUrl, nameEl.$text(), recordName[1]);
}
fixUpdateUi(mainUrl, otherUrl, user, nameEl, tagEl, recordName, appendTag);
} else {
if (supportFixName.has(server)) {
if (server === "candfans") {
const [user_code, username] = await getCandfansName(user) ?? nameEl.$text();
if (user_code && username) fixCache.set(user, [user_code, username]);
[user, mainUrl, recordName] = candfansPageAdapt(user, user_code, mainUrl, nameEl.$text(), username);
fixUpdateUi(mainUrl, otherUrl, user, nameEl, tagEl, username, appendTag);
} else {
const username = await getPixivName(user) ?? nameEl.$text();
fixUpdateUi(mainUrl, otherUrl, user, nameEl, tagEl, username, appendTag);
fixCache.set(user, username);
}
saveWork();
} else {
fixUpdateUi(mainUrl, otherUrl, user, nameEl, tagEl, nameEl.$text(), appendTag);
}
}
}
async function searchFix(items) {
items.$sAttr("fix", true);
const url = items.href;
const img = items.$q("img");
const {
server,
user
} = parseUrlInfo(url);
img.$sAttr("jump", url);
fixTrigger({
mainUrl: url,
otherUrl: "",
server: server,
user: user,
nameEl: items.$q(".user-card__name"),
tagEl: items.$q(".user-card__service"),
appendTag: ""
});
}
async function otherFix(artist, tag = "", mainUrl = null, otherUrl = null, reTag = "fix_view") {
try {
const parent = artist.parentElement;
const url = mainUrl ?? parent.href;
const {
server,
user
} = parseUrlInfo(url);
await fixTrigger({
mainUrl: url,
otherUrl: otherUrl,
server: server,
user: user,
nameEl: artist,
tagEl: tag,
appendTag: otherUrl ? "Post" : ""
});
parent.replaceWith(Lib.createElement(reTag, {
innerHTML: parent.$iHtml()
}));
} catch { }
}
async function dynamicFix(element) {
Lib.$observer(element, async () => {
recordCache = await getRecord();
const checkFix = !Parame.FavoritesArtists.test(Parame.Url);
for (const items of element.$qa(`a${checkFix ? ":not([fix])" : ""}`)) {
searchFix(items);
}
}, {
mark: "dynamic-fix",
subtree: false,
debounce: 50
});
}
await init();
const color = Load.color;
const loadStyle = async () => {
Lib.addStyle(`
a {
user-drag: none;
-webkit-user-drag: none;
}
/* 搜尋頁面的樣式 */
fix_tag:hover { color: ${color}; }
.card-list__items a:not(article a) {
cursor: default;
}
.fancy-image__image, fix_name, fix_tag, fix_edit {
cursor: pointer;
}
.user-card__info {
display: flex;
flex-direction: column;
align-items: flex-start;
}
fix_name {
color: #fff;
font-size: 28px;
font-weight: 500;
max-width: 320px;
overflow: hidden;
display: block;
padding: .25rem .1rem;
border-radius: .25rem;
white-space: nowrap;
text-overflow: ellipsis;
}
fix_edit {
top: 85px;
right: 8%;
color: #fff;
display: none;
z-index: 9999;
font-size: 1.1rem;
font-weight: 700;
position: absolute;
background: #666;
white-space: nowrap;
padding: .25rem .5rem;
border-radius: .25rem;
transform: translateY(-100%);
}
.edit_textarea {
color: #fff;
display: block;
font-size: 30px;
padding: 6px 1px;
line-height: 5vh;
text-align: center;
}
.user-card:hover fix_edit {
display: block;
}
.user-card:hover fix_name {
background-color: ${color};
}
.edit_textarea ~ fix_name,
.edit_textarea ~ fix_edit {
display: none !important;
}
/* 預覽頁面的樣式 */
fix_view {
display: flex;
flex-flow: wrap;
align-items: center;
}
fix_view fix_name {
font-size: 2rem;
font-weight: 700;
padding: .25rem 3rem;
border-radius: .25rem;
transition: background-color 0.3s ease;
}
fix_view fix_edit {
top: 65px;
right: 5%;
transform: translateY(-80%);
}
fix_view:hover fix_name {
background-color: ${color};
}
fix_view:hover fix_edit {
display: block;
}
/* 內容頁面的樣式 */
fix_cont {
display: flex;
height: 5rem;
width: 15rem;
align-items: center;
justify-content: center;
}
fix_cont fix_wrapper {
position: relative;
display: inline-block;
margin-top: 1.5rem;
}
fix_cont fix_name {
color: ${color};
font-size: 1.8rem;
display: inline-block;
}
fix_cont fix_edit {
top: 2.2rem;
right: -4.2rem;
position: absolute;
}
fix_cont fix_wrapper::after {
content: "";
position: absolute;
width: 1.2rem;
height: 100%;
}
fix_cont fix_wrapper:hover fix_name {
background-color: #fff;
}
fix_cont fix_wrapper:hover fix_edit {
display: block;
}
.post-show-box {
z-index: 9999;
cursor: pointer;
position: absolute;
padding: 8px 4px;
max-width: 120%;
min-width: 80px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
border-radius: 5px;
background: #1d1f20ff;
border: 1px solid #fff;
}
.post-show-box[preview="above"] {
bottom: 85%;
}
.post-show-box[preview="below"] {
top: 85%;
}
.post-show-box::-webkit-scrollbar {
display: none;
}
.post-show-box img {
height: 23vh;
margin: 0 .3rem;
min-width: 55%;
border: 1px solid #fff;
}
.fancy-image__image {
z-index: 1;
position: relative;
}
.fancy-image__picture:before {
content: "";
z-index: 0;
bottom: 10%;
width: 100px;
height: 115px;
position: absolute;
}
`, "Better-Post-Card-Effects", false);
};
return {
async BetterPostCard({
newtab,
newtab_active,
newtab_insert,
previewAbove
}) {
loadStyle();
const [active, insert] = [newtab_active, newtab_insert];
Lib.onEvent(Lib.body, "click", event => {
const target = event.target;
const tagName = target.tagName;
if (tagName === "TEXTAREA") {
event.preventDefault();
event.stopImmediatePropagation();
} else if (tagName === "FIX_EDIT") {
event.preventDefault();
event.stopImmediatePropagation();
Lib.$q(".edit_textarea")?.remove();
const display = target.previousElementSibling;
const text = Lib.createElement(display, "textarea", {
class: "edit_textarea",
style: `height: ${display.scrollHeight + 10}px;`
}, "beforebegin");
const original_name = display.$text();
text.value = original_name.trim();
text.scrollTop = 0;
setTimeout(() => {
text.focus();
setTimeout(() => {
text.on("blur", () => {
const change_name = text.value.trim();
if (!change_name) display.$text(original_name); else if (change_name !== original_name) {
display.$text(change_name);
saveRecord(new Map([[target.id, change_name]]));
}
text.remove();
}, {
once: true,
passive: true
});
}, 50);
}, 300);
} else if (newtab && Lib.platform !== "Mobile" && (tagName === "FIX_NAME" || tagName === "FIX_TAG" || tagName === "PICTURE" || target.matches(".fancy-image__image, .post-show-box, .post-show-box img")) || tagName === "FIX_TAG" || tagName === "FIX_NAME" && (Page.isPreview() || Page.isContent()) || Page.isContent() && target.matches(".fancy-image__image")) {
event.preventDefault();
event.stopImmediatePropagation();
const url = target.$gAttr("jump");
if (url) {
newtab || tagName === "FIX_TAG" || tagName === "FIX_NAME" && Page.isPreview() ? GM_openInTab(url, {
active: active,
insert: insert
}) : location.assign(url);
} else if (tagName === "IMG" || tagName === "PICTURE") {
const href = target.closest("a").href;
newtab && !Page.isContent() ? GM_openInTab(href, {
active: active,
insert: insert
}) : location.assign(href);
}
}
}, {
capture: true,
mark: "BetterPostCard"
});
if (Lib.platform === "Desktop") {
let currentBox, currentTarget;
Lib.onEvent(Lib.body, "mouseover", Lib.$debounce(event => {
let target = event.target;
const tagName = target.tagName;
if (tagName === "IMG" && target.$hAttr("jump")) {
currentTarget = target.parentElement;
currentBox = target.previousElementSibling;
} else if (tagName === "PICTURE") {
currentTarget = target;
currentBox = target.$q(".post-show-box");
target = target.$q("img");
} else return;
if (!currentBox && target) {
currentBox = Lib.createElement(target, "div", {
text: "Loading...",
style: "display: none;",
class: "post-show-box",
attr: {
preview: previewAbove ? "above" : "below"
},
on: {
wheel: event2 => {
event2.preventDefault();
event2.currentTarget.scrollLeft += event2.deltaY;
}
}
}, "beforebegin");
const url = target.$gAttr("jump");
if (url && !url.includes("discord")) {
const uri = new URL(url);
const api = Page.isNeko ? url : `${uri.origin}/api/v1${uri.pathname}/posts`;
Fetch.send(api, null, {
responseType: Page.isNeko ? "document" : "json"
}).then(data => {
if (Page.isNeko) data = data.$qa(".post-card__image");
currentBox.$text("");
const srcBox = new Set();
for (const post of data) {
let src = "";
if (Page.isNeko) src = post.src ?? ""; else {
for (const {
path
} of [post.file, ...post?.attachments || []]) {
if (!path) continue;
const isImg = Parame.SupportImg.has(path.split(".")[1]);
if (!isImg) continue;
src = Parame.ThumbnailApi + path;
break;
}
}
if (!src) continue;
srcBox.add(src);
}
if (srcBox.size === 0) currentBox.$text("No Image"); else {
currentBox.$iAdjacent([...srcBox].map((src, index) => `<img src="${src}" loading="lazy" number="${index + 1}">`).join(""));
srcBox.clear();
}
});
} else currentBox.$text("Not Supported");
}
currentBox?.$sAttr("style", "display: block;");
}, 300), {
passive: true,
mark: "PostShow"
});
Lib.onEvent(Lib.body, "mouseout", event => {
if (!currentTarget) return;
if (currentTarget.contains(event.relatedTarget)) return;
currentTarget = null;
currentBox?.$sAttr("style", "display: none;");
}, {
passive: true,
mark: "PostHide"
});
}
if (Page.isSearch()) {
Lib.waitEl(".card-list__items", null, {
raf: true,
timeout: 10
}).then(card_items => {
if (Parame.Links.test(Parame.Url) || Parame.Recommended.test(Parame.Url)) {
const artist = Lib.$q("span[itemprop='name']");
artist && otherFix(artist);
}
dynamicFix(card_items);
card_items.$sAttr("fix-trigger", true);
});
} else if (Page.isContent()) {
Lib.waitEl(["h1 span:nth-child(2)", ".post__user-name, .scrape__user-name"], null, {
raf: true,
timeout: 10
}).then(([title, artist]) => {
otherFix(artist, title, artist.href, Lib.url, "fix_cont");
});
} else {
Lib.waitEl("span[itemprop='name']", null, {
raf: true,
timeout: 3
}).then(artist => {
otherFix(artist);
});
}
}
};
};
const globalLoader = {
SidebarCollapse: SidebarCollapse,
DeleteNotice: DeleteNotice,
KeyScroll: KeyScroll,
BlockAds: BlockAds,
CacheFetch: CacheFetch,
async TextToLink(...args) {
const value = TextToLinkFactory().TextToLink;
Object.defineProperty(this, value.name, {
value: value,
writable: false
});
value(...args);
},
async BetterPostCard(...args) {
const func = await BetterPostCardFactory();
const value = func.BetterPostCard;
Object.defineProperty(this, value.name, {
value: value,
writable: false
});
value(...args);
}
};
async function NewTabOpens({
newtab_active,
newtab_insert
}) {
const [active, insert] = [newtab_active, newtab_insert];
Lib.onEvent(Lib.body, "click", event => {
const target = event.target.closest("article a");
target && (event.preventDefault(), event.stopImmediatePropagation(),
GM_openInTab(target.href, {
active: active,
insert: insert
}));
}, {
capture: true,
mark: "NewTabOpens"
});
}
async function CardText({
mode
}) {
if (Lib.platform === "Mobile") return;
switch (mode) {
case 2:
Lib.addStyle(`
.post-card__header, .post-card__footer {
opacity: 0.4 !important;
transition: opacity 0.3s;
}
a:hover .post-card__header,
a:hover .post-card__footer {
opacity: 1 !important;
}
`, "CardText-Effects-2", false);
break;
default:
Lib.addStyle(`
.post-card__header {
opacity: 0;
z-index: 1;
padding: 5px;
pointer-events: none;
transform: translateY(-6vh);
transition: transform 0.4s, opacity 0.6s;
}
.post-card__footer {
opacity: 0;
z-index: 1;
padding: 5px;
pointer-events: none;
transform: translateY(6vh);
transition: transform 0.4s, opacity 0.6s;
}
a:hover .post-card__header,
a:hover .post-card__footer {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
`, "CardText-Effects", false);
}
}
async function CardZoom({
mode
}) {
let paddingBottom, rowGap, height;
switch (mode) {
case 2:
Lib.addStyle(`
.post-card a:hover {
z-index: 9999;
overflow: auto;
max-height: 90vh;
min-height: 100%;
height: max-content;
background: #000;
border: 1px solid #fff6;
transform: scale(1.1) translateY(0);
}
.post-card a::-webkit-scrollbar {
display: none;
}
.post-card a:hover .post-card__image-container {
position: relative;
}
`, "CardZoom-Effects-2", false);
break;
case 3:
[paddingBottom, rowGap, height] = Page.isNeko ? ["0", "0", "57"] : ["7", "5.8", "50"];
Lib.addStyle(`
.card-list--legacy { padding-bottom: ${paddingBottom}em }
.card-list--legacy .card-list__items {
row-gap: ${rowGap}em;
column-gap: 3em;
}
.post-card a {
width: 20em;
height: ${height}vh;
}
.post-card__image-container img { object-fit: contain }
`, "CardZoom-Effects-3", false);
}
Lib.addStyle(`
.card-list--legacy * {
font-size: 20px !important;
font-weight: 600 !important;
--card-size: 350px !important;
}
.post-card a {
background: #000;
overflow: hidden;
border-radius: 8px;
border: 3px solid #fff6;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
`, "CardZoom-Effects", false);
}
async function QuickPostToggle() {
if (!Page.isNeko || Parame.Registered.has("QuickPostToggle")) return;
Lib.waitEl("menu", null, {
all: true,
timeout: 5
}).then(menu => {
Parame.Registered.add("QuickPostToggle");
function Rendering({
href,
className,
textContent,
style
}) {
return preact.h("a", {
href: href,
className: className,
style: style
}, preact.h("b", null, textContent));
}
const pageContentCache = new Map();
const MAX_CACHE_SIZE = 30;
function cleanupCache() {
if (pageContentCache.size >= MAX_CACHE_SIZE) {
const firstKey = pageContentCache.keys().next().value;
pageContentCache.delete(firstKey);
}
}
async function fetchPage(url, abortSignal) {
if (pageContentCache.has(url)) {
const cachedContent = pageContentCache.get(url);
pageContentCache.delete(url);
pageContentCache.set(url, cachedContent);
const clonedContent = cachedContent.cloneNode(true);
Lib.$q(".card-list--legacy").replaceChildren(...clonedContent.childNodes);
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const request = GM_xmlhttpRequest({
method: "GET",
url: url,
onload: response => {
if (abortSignal?.aborted) return reject(new Error("Aborted"));
if (response.status !== 200) return reject(new Error("Server error"));
const newContent = response.responseXML.$q(".card-list--legacy");
cleanupCache();
const contentToCache = newContent.cloneNode(true);
pageContentCache.set(url, contentToCache);
Lib.$q(".card-list--legacy").replaceWith(newContent);
resolve();
},
onerror: () => reject(new Error("Network error"))
});
if (abortSignal) {
abortSignal.addEventListener("abort", () => {
request.abort?.();
reject(new Error("Aborted"));
});
}
});
}
const totalPages = Math.ceil(+menu[0].previousElementSibling.$text().split("of")[1].trim() / 50);
const pageLinks = [Parame.Url, ...Array(totalPages - 1).fill().map((_, i) => `${Parame.Url}?o=${(i + 1) * 50}`)];
const MAX_VISIBLE = 11;
const hasScrolling = totalPages > 11;
let buttonCache = null;
let pageButtonIndexMap = null;
let visibleRangeCache = {
page: -1,
range: null
};
function getVisibleRange(currentPage) {
if (visibleRangeCache.page === currentPage) {
return visibleRangeCache.range;
}
let range;
if (!hasScrolling) {
range = {
start: 1,
end: totalPages
};
} else {
let start = 1;
if (currentPage >= MAX_VISIBLE && totalPages > MAX_VISIBLE) {
start = currentPage - MAX_VISIBLE + 2;
}
range = {
start: start,
end: Math.min(totalPages, start + MAX_VISIBLE - 1)
};
}
visibleRangeCache = {
page: currentPage,
range: range
};
return range;
}
function createButton(text, page, isDisabled = false, isCurrent = false, isHidden = false) {
return preact.h(Rendering, {
href: isDisabled ? void 0 : pageLinks[page - 1],
textContent: text,
className: `${isDisabled ? "pagination-button-disabled" : ""} ${isCurrent ? "pagination-button-current" : ""}`.trim(),
style: isHidden ? {
display: "none"
} : void 0
});
}
function createPaginationElements(currentPage = 1) {
const {
start,
end
} = getVisibleRange(currentPage);
const elements2 = [];
if (hasScrolling) {
elements2.push(createButton("<<", 1, currentPage === 1));
}
elements2.push(createButton("<", currentPage - 1, currentPage === 1));
pageLinks.forEach((link, index) => {
const pageNum = index + 1;
const isVisible = pageNum >= start && pageNum <= end;
const isCurrent = pageNum === currentPage;
elements2.push(createButton(pageNum, pageNum, isCurrent, isCurrent, !isVisible));
});
elements2.push(createButton(">", currentPage + 1, currentPage === totalPages));
if (hasScrolling) {
elements2.push(createButton(">>", totalPages, currentPage === totalPages));
}
return elements2;
}
function initializeButtonCache() {
const menu1Buttons = menu[0].$qa("a");
const menu2Buttons = menu[1].$qa("a");
const navOffset = hasScrolling ? 2 : 1;
buttonCache = {
menu1: {
all: menu1Buttons,
nav: {
first: hasScrolling ? menu1Buttons[0] : null,
prev: menu1Buttons[hasScrolling ? 1 : 0],
next: menu1Buttons[menu1Buttons.length - (hasScrolling ? 2 : 1)],
last: hasScrolling ? menu1Buttons[menu1Buttons.length - 1] : null
},
pages: menu1Buttons.slice(navOffset, menu1Buttons.length - navOffset)
},
menu2: {
all: menu2Buttons,
nav: {
first: hasScrolling ? menu2Buttons[0] : null,
prev: menu2Buttons[hasScrolling ? 1 : 0],
next: menu2Buttons[menu2Buttons.length - (hasScrolling ? 2 : 1)],
last: hasScrolling ? menu2Buttons[menu2Buttons.length - 1] : null
},
pages: menu2Buttons.slice(navOffset, menu2Buttons.length - navOffset)
}
};
pageButtonIndexMap = new Map();
buttonCache.menu1.pages.forEach((btn, index) => {
const pageNum = index + 1;
pageButtonIndexMap.set(pageNum, index);
});
}
function updateNavigationButtons(menuData, targetPage) {
const isFirstPage = targetPage === 1;
const isLastPage = targetPage === totalPages;
const {
nav
} = menuData;
const navUpdates = [];
if (hasScrolling) {
navUpdates.push([nav.first, isFirstPage, pageLinks[0]], [nav.prev, isFirstPage, pageLinks[targetPage - 2]], [nav.next, isLastPage, pageLinks[targetPage]], [nav.last, isLastPage, pageLinks[totalPages - 1]]);
} else {
navUpdates.push([nav.prev, isFirstPage, pageLinks[targetPage - 2]], [nav.next, isLastPage, pageLinks[targetPage]]);
}
navUpdates.forEach(([btn, isDisabled, href]) => {
btn.$toggleClass("pagination-button-disabled", isDisabled);
if (isDisabled) {
btn.$dAttr("href");
} else {
btn.href = href;
}
});
}
function updatePageButtons(menuData, targetPage, visibleRange) {
const {
start,
end
} = visibleRange;
const {
pages
} = menuData;
const currentActiveBtn = pages.find(btn => btn.classList.contains("pagination-button-current"));
if (currentActiveBtn) {
currentActiveBtn.$delClass("pagination-button-current", "pagination-button-disabled");
}
const startIndex = Math.max(0, start - 1);
const endIndex = Math.min(pages.length - 1, end - 1);
for (let i = 0; i < startIndex; i++) {
pages[i].style.display = "none";
}
for (let i = endIndex + 1; i < pages.length; i++) {
pages[i].style.display = "none";
}
for (let i = startIndex; i <= endIndex; i++) {
const btn = pages[i];
const pageNum = i + 1;
btn.style.display = "";
if (pageNum === targetPage) {
btn.$addClass("pagination-button-current", "pagination-button-disabled");
}
}
}
function updatePagination(targetPage) {
const visibleRange = getVisibleRange(targetPage);
updateNavigationButtons(buttonCache.menu1, targetPage);
updateNavigationButtons(buttonCache.menu2, targetPage);
updatePageButtons(buttonCache.menu1, targetPage, visibleRange);
updatePageButtons(buttonCache.menu2, targetPage, visibleRange);
}
const navigationActions = {
"<<": () => 1,
">>": () => totalPages,
"<": current => current > 1 ? current - 1 : null,
">": current => current < totalPages ? current + 1 : null
};
function parseTargetPage(clickText, currentPage) {
const clickedNum = parseInt(clickText);
if (!isNaN(clickedNum)) return clickedNum;
const action = navigationActions[clickText];
return action ? action(currentPage) : null;
}
const elements = createPaginationElements(1);
const [fragment1, fragment2] = [Lib.createFragment, Lib.createFragment];
preact.render([...elements], fragment1);
preact.render([...elements], fragment2);
menu[0].replaceChildren(fragment1);
menu[0].$sAttr("QuickPostToggle", "true");
requestAnimationFrame(() => {
menu[1].replaceChildren(fragment2);
menu[1].$sAttr("QuickPostToggle", "true");
initializeButtonCache();
});
let isLoading = false;
let abortController = null;
Lib.onEvent("section", "click", async event => {
const target = event.target.closest("menu a:not(.pagination-button-disabled)");
if (!target || isLoading) return;
event.preventDefault();
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
const currentActiveBtn = target.closest("menu").$q(".pagination-button-current");
const currentPage = parseInt(currentActiveBtn.$text());
const targetPage = parseTargetPage(target.$text(), currentPage);
if (!targetPage || targetPage === currentPage) return;
isLoading = true;
try {
await Promise.all([fetchPage(pageLinks[targetPage - 1], abortController.signal), new Promise(resolve => {
updatePagination(targetPage);
resolve();
})]);
target.closest("#paginator-bottom") && menu[0].scrollIntoView();
history.pushState(null, null, pageLinks[targetPage - 1]);
} catch (error) {
if (error.message !== "Aborted") {
Lib.log("Page fetch failed:", error).error;
}
} finally {
isLoading = false;
abortController = null;
}
}, {
capture: true,
mark: "QuickPostToggle"
});
});
}
const BetterThumbnailFactory = () => {
const imgReload = (img, thumbnailSrc, retry) => {
if (!img.isConnected) return;
if (!retry) {
img.src = thumbnailSrc;
return;
}
const src = img.src;
img.onload = function () {
img.onload = img.onerror = null;
};
img.onerror = function () {
img.onload = img.onerror = null;
img.src = thumbnailSrc;
setTimeout(() => {
imgReload(img, thumbnailSrc, retry - 1);
}, 2e3);
};
img.src = src;
};
const changeSrc = (img, thumbnailSrc, src) => {
if (!img.isConnected) return;
img.loading = "lazy";
img.onerror = function () {
img.onerror = null;
imgReload(this, thumbnailSrc, 10);
};
img.src = src;
};
return {
async BetterThumbnail() {
if (Page.isNeko) return;
Lib.waitEl("article.post-card", null, {
raf: true,
all: true,
timeout: 5
}).then(postCard => {
const uri = new URL(Parame.Url);
if (uri.searchParams.get("q") === "") uri.searchParams.delete("q");
if (Parame.User.test(Parame.Url)) {
uri.pathname += "/posts";
} else if (Parame.FavorPosts.test(Parame.Url)) {
uri.pathname = uri.pathname.replace("/posts", "");
uri.searchParams.set("type", "post");
}
const postData = [...postCard].reduce((acc, card) => {
const id = card.$gAttr("data-id");
if (id) acc[id] = {
img: card.$q("img"),
footer: card.$q("time").nextElementSibling
};
return acc;
}, {});
const api = `${uri.origin}/api/v1${uri.pathname}${uri.search}`;
Fetch.send(api, data => {
if (Lib.$type(data) === "Object") data = data?.posts || [];
for (const post of data) {
const {
img,
footer
} = postData[post?.id] || {};
if (!img && !footer) continue;
let replaced = false;
const src = img?.src;
const attachments = post.attachments || [];
const record = new Set();
const count = [post.file, ...attachments].reduce((count2, attach, index) => {
const path = attach.path || "";
if (record.has(path)) return count2;
const ext = path.split(".").at(-1).toLowerCase();
if (!ext) return count2;
const isImg = Parame.SupportImg.has(ext);
if (isImg) count2.image = (count2.image ?? 0) + 1; else if (Parame.VideoType.has(ext)) count2.video = (count2.video ?? 0) + 1; else count2.file = (count2.file ?? 0) + 1;
if (src && !replaced && index > 0 && isImg) {
replaced = true;
changeSrc(img, src, Parame.ThumbnailApi + path);
}
record.add(path);
return count2;
}, {});
if (footer && !Lib.isEmpty(count)) {
const {
image,
video,
file
} = count;
const parts = [];
if (image) parts.push(`${image} images`);
if (video) parts.push(`${video} videos`);
if (file) parts.push(`${file} files`);
const showText = parts.join(" | ");
if (showText) footer.$text(showText);
}
}
});
});
}
};
};
const previewLoader = {
NewTabOpens: NewTabOpens,
CardText: CardText,
CardZoom: CardZoom,
async BetterThumbnail(...args) {
const value = BetterThumbnailFactory().BetterThumbnail;
Object.defineProperty(this, value.name, {
value: value,
writable: false
});
value(...args);
},
QuickPostToggle: QuickPostToggle
};
const LinkBeautifyFactory = () => {
const showBrowse = (browse, retry = 3) => {
if (!retry) return;
browse.style.position = "relative";
browse.$q("View")?.remove();
Fetch.send(browse.href?.replace("posts/archives", "api/v1/file"), json => {
const password = json.password;
browse.$iAdjacent(`
<view>
${password ? `password: ${password}<br>` : ""}
${json.file_list.map(file => `${file}<br>`).join("")}
</view>`);
}).catch(() => {
setTimeout(() => showBrowse(browse, retry - 1), 1e3);
});
};
return {
async LinkBeautify() {
Lib.addStyle(`
View {
top: -10px;
z-index: 1;
padding: 10%;
display: none;
overflow: auto;
color: #f2f2f2;
font-size: 14px;
max-height: 50vh;
font-weight: 600;
text-align: center;
position: absolute;
white-space: nowrap;
border-radius: .5rem;
left: calc(100% + 10px);
border: 1px solid #737373;
background-color: #3b3e44;
}
a:hover View { display: block }
.post__attachment .fancy-link::after {
content: "";
position: absolute;
height: 100%;
padding: .4rem;
}
.post__attachment-link:not([beautify]) { display: none !important; }
`, "Link-Effects", false);
Lib.waitEl(".post__attachment-link, .scrape__attachment-link", null, {
raf: true,
all: true,
timeout: 5
}).then(post => {
for (const link of post) {
if (!Page.isNeko && link.$gAttr("beautify")) {
link.remove();
continue;
}
const text = link.$text().replace("Download ", "");
if (Page.isNeko) {
link.$text(text);
link.$sAttr("download", text);
} else {
link.$iAdjacent(`<a class="${link.$gAttr("class")}" href="${link.href}" download="${text}" beautify="true">${text}</a>`, "beforebegin");
}
const browse = link.nextElementSibling;
if (!browse || browse.$text() !== "browse »") continue;
showBrowse(browse);
}
});
}
};
};
async function VideoBeautify({
mode
}) {
if (Page.isNeko) {
Lib.waitEl(".scrape__files video", null, {
raf: true,
all: true,
timeout: 5
}).then(video => {
video.forEach(media => media.$sAttr("preload", "metadata"));
});
} else {
Lib.waitEl("ul[style*='text-align: center; list-style-type: none;'] li:not([id])", null, {
raf: true,
all: true,
timeout: 5
}).then(parents => {
Lib.waitEl(".post__attachment-link, .scrape__attachment-link", null, {
raf: true,
all: true,
timeout: 5
}).then(post => {
Lib.addStyle(`
.fluid_video_wrapper {
height: 50% !important;
width: 65% !important;
border-radius: 8px !important;
}
`, "Video-Effects", false);
const move = mode === 2;
const linkBox = Object.fromEntries([...post].map(a => {
const data = [a.download?.trim(), a];
return data;
}));
for (const li of parents) {
const waitLoad = new MutationObserver(Lib.$debounce(() => {
waitLoad.disconnect();
let [video, summary] = [li.$q("video"), li.$q("summary")];
if (!video || !summary) return;
video.$sAttr("loop", true);
video.$sAttr("preload", "metadata");
const link = linkBox[summary.$text()];
if (!link) return;
move && link.parentElement.remove();
let element = link.$copy();
element.$sAttr("beautify", true);
element.$text(element.$text().replace("Download", ""));
summary.$text("");
summary.appendChild(element);
}, 100));
waitLoad.observe(li, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
li.$sAttr("Video-Beautify", true);
}
});
});
}
}
const OriginalImageFactory = () => {
const linkQuery = Page.isNeko ? "div" : "a";
const safeGetSrc = element => element?.src || element?.$gAttr("src");
const safeGetHref = element => element?.href || element?.$gAttr("href");
const loadFailedClick = () => {
Lib.onE(".post__files, .scrape__files", "click", event => {
const target = event.target;
const isImg = target.matches("img");
if (isImg && target.alt === "Loading Failed") {
target.onload = null;
target.$dAttr("src");
target.onload = function () {
cleanMark(target);
};
target.src = target.$gAttr("data-fsrc");
}
}, {
capture: true,
passive: true
});
};
const cleanMark = img => {
img.onload = img.onerror = null;
img.$dAttr("alt");
img.$dAttr("data-tsrc");
img.$dAttr("data-fsrc");
img.$delClass("Image-loading-indicator");
};
const imgReload = (img, retry) => {
if (!img.isConnected) return;
if (!retry) {
img.alt = "Loading Failed";
img.src = img.$gAttr("data-tsrc");
return;
}
img.$dAttr("src");
img.onload = function () {
cleanMark(img);
};
img.onerror = function () {
img.onload = img.onerror = null;
setTimeout(() => {
imgReload(img, retry - 1);
}, 1e4);
};
img.alt = "Reload";
img.src = img.$gAttr("data-fsrc");
};
async function imgRequest(container, url, result) {
const indicator = Lib.createElement(container, "div", {
class: "progress-indicator"
});
let blob = null;
try {
if (false); else {
for (let i = 0; i < 5; i++) {
try {
blob = await new Promise((resolve, reject) => {
let timeout = null;
const request = GM_xmlhttpRequest({
url: url,
method: "GET",
responseType: "blob",
onload: res => {
clearTimeout(timeout);
return res.status === 200 ? resolve(res.response) : reject(res);
},
onerror: reject,
onprogress: progress => {
timer();
if (progress.lengthComputable && indicator.isConnected) {
const percent = (progress.loaded / progress.total * 100).toFixed(1);
indicator.$text(`${percent}%`);
}
}
});
function timer() {
clearTimeout(timeout);
timeout = setTimeout(() => {
request.abort();
reject();
}, 15e3);
}
});
break;
} catch (error) {
if (i < 4) await new Promise(res => setTimeout(res, 300));
}
}
}
if (blob && blob.size > 0) {
result(URL.createObjectURL(blob));
} else {
result(Parame.Url);
}
} catch (error) {
result(Parame.Url);
} finally {
indicator?.remove();
}
}
return {
async OriginalImage({
mode,
experiment
}) {
Lib.waitEl(".post__thumbnail, .scrape__thumbnail", null, {
raf: true,
all: true,
timeout: 5
}).then(thumbnail => {
let token = 0, timer = null;
function imgRendering({
root,
index,
thumbUrl,
newUrl,
oldUrl,
mode: mode2
}) {
if (!root.isConnected) return;
++index;
++token;
const tagName = oldUrl ? "rc" : "div";
const oldSrc = oldUrl ? `src="${oldUrl}"` : "";
const container = Lib.createDomFragment(`
<${tagName} id="IMG-${index}" ${oldSrc}>
<img src="${newUrl}" class="Image-loading-indicator Image-style" data-tsrc="${thumbUrl}" data-fsrc="${newUrl}">
</${tagName}>
`);
const img = container.querySelector("img");
timer = setTimeout(() => {
--token;
}, 1e4);
img.onload = function () {
clearTimeout(timer);
--token;
cleanMark(img);
mode2 === "slow" && slowAutoLoad(index);
};
if (mode2 === "fast") {
img.onerror = function () {
--token;
img.onload = img.onerror = null;
imgReload(img, 7);
};
}
root.replaceWith(container);
}
async function imgLoad(root, index, mode2 = "fast") {
if (!root.isConnected) return;
root.$dAttr("class");
const a = root.$q(linkQuery);
const safeHref = safeGetHref(a);
const img = root.$q("img");
const safeSrc = safeGetSrc(img);
if (!a && img) {
img.$addClass("Image-style");
return;
}
const replaceRoot = Page.isNeko ? root : a;
if (experiment) {
img.$addClass("Image-loading-indicator-experiment");
imgRequest(root, safeHref, href => {
imgRendering({
root: replaceRoot,
index: index,
thumbUrl: safeSrc,
newUrl: href,
oldUrl: safeHref,
mode: mode2
});
});
} else {
imgRendering({
root: replaceRoot,
index: index,
thumbUrl: safeSrc,
newUrl: safeHref,
mode: mode2
});
}
}
async function fastAutoLoad() {
loadFailedClick();
for (const [index, root] of [...thumbnail].entries()) {
while (token >= 7) {
await Lib.sleep(700);
}
imgLoad(root, index);
}
}
async function slowAutoLoad(index) {
if (index === thumbnail.length) return;
const root = thumbnail[index];
imgLoad(root, index, "slow");
}
let observer;
function observeLoad() {
loadFailedClick();
return new IntersectionObserver(observed => {
observed.forEach(entry => {
if (entry.isIntersecting) {
const root = entry.target;
observer.unobserve(root);
imgLoad(root, root.dataset.index);
}
});
}, {
threshold: .4
});
}
switch (mode) {
case 2:
slowAutoLoad(0);
break;
case 3:
observer?.disconnect();
observer = observeLoad();
thumbnail.forEach((root, index) => {
root.dataset.index = index;
observer.observe(root);
});
break;
default:
fastAutoLoad();
}
});
}
};
};
const ExtraButtonFactory = () => {
const loadStyle = () => {
Lib.addStyle(`
#main section {
width: 100%;
}
`, "Post-Extra", false);
};
const getNextPage = (url, oldMain, retry = 5) => {
if (!retry) return;
Fetch.send(url, null, {
responseType: "document"
}).then(dom => {
const main = dom.$q("main");
if (!main) return;
oldMain.replaceWith(main);
Lib.$q("header")?.scrollIntoView();
history.pushState(null, null, url);
Lib.title(dom.title);
}).catch(() => {
setTimeout(() => getNextPage(url, oldMain), 1e3);
});
};
return {
async ExtraButton() {
Lib.waitEl("h2.site-section__subheading", null, {
raf: true,
timeout: 5
}).then(comments => {
loadStyle();
Lib.$q(".post__nav-link.prev, .scrape__nav-link.prev");
const nextBtn = Lib.$q(".post__nav-link.next, .scrape__nav-link.next");
let toTopBtn, newNextBtn;
if (!Lib.$q("#to-top-svg")) {
const header = Lib.$q("header");
toTopBtn = Lib.createElement(comments, "span", {
id: "to-top-svg",
innerHTML: `
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512" style="margin-left: 10px;cursor: pointer;">
<style>svg{fill: ${Load.color}}</style>
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM135.1 217.4l107.1-99.9c3.8-3.5 8.7-5.5 13.8-5.5s10.1 2 13.8 5.5l107.1 99.9c4.5 4.2 7.1 10.1 7.1 16.3c0 12.3-10 22.3-22.3 22.3H304v96c0 17.7-14.3 32-32 32H240c-17.7 0-32-14.3-32-32V256H150.3C138 256 128 246 128 233.7c0-6.2 2.6-12.1 7.1-16.3z"></path>
</svg>`,
on: {
click: () => header?.scrollIntoView()
}
});
}
if (nextBtn && !Lib.$q("#next-btn")) {
const newBtn = nextBtn.$copy(true);
newBtn.style = `color: ${Load.color};`;
newBtn.$sAttr("jump", nextBtn.href);
newBtn.$dAttr("href");
newNextBtn = Lib.createElement(comments, "span", {
id: "next-btn",
style: "float: right; cursor: pointer;",
on: {
click: {
listen: () => {
if (Page.isNeko) {
newBtn.disabled = true;
getNextPage(newBtn.$gAttr("jump"), Lib.$q("main"));
} else {
toTopBtn?.remove();
newNextBtn.remove();
nextBtn.click();
}
},
add: {
once: true
}
}
}
});
newNextBtn.appendChild(newBtn);
}
});
}
};
};
async function CommentFormat() {
Lib.addStyle(`
.post__comments,
.scrape__comments {
display: flex;
flex-wrap: wrap;
}
.post__comments > *:last-child,
.scrape__comments > *:last-child {
margin-bottom: 0.5rem;
}
.comment {
margin: 0.5rem;
max-width: 25rem;
border-radius: 10px;
flex-basis: calc(35%);
word-break: break-all;
border: 0.125em solid var(--colour1-secondary);
}
`, "Comment-Effects", false);
}
const contentLoader = {
VideoBeautify: VideoBeautify,
async LinkBeautify(...args) {
const value = LinkBeautifyFactory().LinkBeautify;
Object.defineProperty(this, value.name, {
value: value,
writable: false
});
value(...args);
},
async OriginalImage(...args) {
const value = OriginalImageFactory().OriginalImage;
Object.defineProperty(this, value.name, {
value: value,
writable: false
});
value(...args);
},
async ExtraButton(...args) {
const value = ExtraButtonFactory().ExtraButton;
Object.defineProperty(this, value.name, {
value: value,
writable: false
});
value(...args);
},
CommentFormat: CommentFormat
};
const dict = {
Traditional: {},
Simplified: {
"📝 設置選單": "📝 设置菜单",
"設置菜單": "设置菜单",
"圖像設置": "图像设置",
"讀取設定": "加载设置",
"關閉離開": "关闭",
"保存應用": "保存并应用",
"語言": "语言",
"英文": "英语",
"繁體": "繁体中文",
"簡體": "简体中文",
"日文": "日语",
"韓文": "韩语",
"俄語": "俄语",
"圖片高度": "图片高度",
"圖片寬度": "图片宽度",
"圖片最大寬度": "图片最大宽度",
"圖片間隔高度": "图片间距"
},
Japan: {
"📝 設置選單": "📝 設定メニュー",
"設置菜單": "設定メニュー",
"圖像設置": "画像設定",
"讀取設定": "設定を読み込む",
"關閉離開": "閉じる",
"保存應用": "保存して適用",
"語言": "言語",
"英文": "英語",
"繁體": "繁体字中国語",
"簡體": "簡体字中国語",
"日文": "日本語",
"韓文": "韓国語",
"俄語": "ロシア語",
"圖片高度": "画像の高さ",
"圖片寬度": "画像の幅",
"圖片最大寬度": "画像の最大幅",
"圖片間隔高度": "画像の間隔"
},
Korea: {
"📝 設置選單": "📝 설정 메뉴",
"設置菜單": "설정 메뉴",
"圖像設置": "이미지 설정",
"讀取設定": "설정 불러오기",
"關閉離開": "닫기",
"保存應用": "저장 및 적용",
"語言": "언어",
"英文": "영어",
"繁體": "번체 중국어",
"簡體": "간체 중국어",
"日文": "일본어",
"韓文": "한국어",
"俄語": "러시아어",
"圖片高度": "이미지 높이",
"圖片寬度": "이미지 너비",
"圖片最大寬度": "이미지 최대 너비",
"圖片間隔高度": "이미지 간격"
},
Russia: {
"📝 設置選單": "📝 Меню настроек",
"設置菜單": "Меню настроек",
"圖像設置": "Настройки изображений",
"讀取設定": "Загрузить настройки",
"關閉離開": "Закрыть",
"保存應用": "Сохранить и применить",
"語言": "Язык",
"英文": "Английский",
"繁體": "Традиционный китайский",
"簡體": "Упрощенный китайский",
"日文": "Японский",
"韓文": "Корейский",
"俄語": "Русский",
"圖片高度": "Высота изображения",
"圖片寬度": "Ширина изображения",
"圖片最大寬度": "Максимальная ширина",
"圖片間隔高度": "Интервал между изображениями"
},
English: {
"📝 設置選單": "📝 Settings Menu",
"設置菜單": "Settings Menu",
"圖像設置": "Image Settings",
"讀取設定": "Load Settings",
"關閉離開": "Close & Exit",
"保存應用": "Save & Apply",
"語言": "Language",
"英文": "English",
"繁體": "Traditional Chinese",
"簡體": "Simplified Chinese",
"日文": "Japanese",
"韓文": "Korean",
"俄語": "Russian",
"圖片高度": "Image Height",
"圖片寬度": "Image Width",
"圖片最大寬度": "Max Image Width",
"圖片間隔高度": "Image Spacing"
}
};
function getLanguage() {
const Log = Lib.getV(Parame.SaveKey.Lang, navigator.language);
const ML = Lib.translMatcher(dict, Log);
return {
Log: Log,
Transl: str => ML[str] ?? str
};
}
const MenuFactory = (() => {
let imgRule, menuRule;
const importantStyle = (element, property, value) => {
requestAnimationFrame(() => {
element.style.setProperty(property, value, "important");
});
};
const normalStyle = (element, property, value) => {
requestAnimationFrame(() => {
element.style[property] = value;
});
};
const stylePointer = {
Top: value => normalStyle(menuRule[1], "top", value),
Left: value => normalStyle(menuRule[1], "left", value),
Width: value => importantStyle(imgRule[1], "width", value),
Height: value => importantStyle(imgRule[1], "height", value),
MaxWidth: value => importantStyle(imgRule[1], "max-width", value),
Spacing: value => importantStyle(imgRule[1], "margin", `${value} auto`)
};
async function postViewInit() {
if (Parame.Registered.has("PostViewInit")) return;
const set = Load.imgSet();
Lib.addStyle(`
.post__files > div,
.scrape__files > div {
position: relative;
}
.Image-style, figure img {
display: block;
will-change: transform;
width: ${set.Width} !important;
height: ${set.Height} !important;
margin: ${set.Spacing} auto !important;
max-width: ${set.MaxWidth} !important;
}
.Image-loading-indicator {
min-width: 50vW;
min-height: 50vh;
object-fit: contain;
border: 2px solid #fafafa;
}
.Image-loading-indicator-experiment {
border: 3px solid #00ff7e;
}
.Image-loading-indicator[alt] {
border: 2px solid #e43a3aff;
}
.Image-loading-indicator:hover {
cursor: pointer;
}
.progress-indicator {
top: 5px;
left: 5px;
colo: #fff;
font-size: 14px;
padding: 3px 6px;
position: absolute;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.3);
}
`, "Image-Custom-Style", false);
imgRule = Lib.$q("#Image-Custom-Style")?.sheet.cssRules;
Lib.storageListen(Object.values(Parame.SaveKey), call => {
if (call.far) {
if (typeof call.nv === "string") {
menuInit();
} else {
for (const [key, value] of Object.entries(call.nv)) {
stylePointer[key](value);
}
}
}
});
Parame.Registered.add("PostViewInit");
}
async function draggable(element) {
let isDragging = false;
let startX, startY, initialLeft, initialTop;
const nonDraggableTags = new Set(["SELECT", "BUTTON", "INPUT", "TEXTAREA", "A"]);
const handleMouseMove = e => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = `${initialLeft + dx}px`;
element.style.top = `${initialTop + dy}px`;
};
const handleMouseUp = () => {
if (!isDragging) return;
isDragging = false;
element.style.cursor = "auto";
document.body.style.removeProperty("user-select");
Lib.offEvent(document, "mousemove");
Lib.offEvent(document, "mouseup");
};
const handleMouseDown = e => {
if (nonDraggableTags.has(e.target.tagName)) return;
e.preventDefault();
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const style = window.getComputedStyle(element);
initialLeft = parseFloat(style.left) || 0;
initialTop = parseFloat(style.top) || 0;
element.style.cursor = "grabbing";
document.body.style.userSelect = "none";
Lib.onEvent(document, "mousemove", handleMouseMove);
Lib.onEvent(document, "mouseup", handleMouseUp);
};
Lib.onEvent(element, "mousedown", handleMouseDown);
}
async function menuInit(callback = null) {
const {
Log,
Transl
} = getLanguage();
callback?.({
Log: Log,
Transl: Transl
});
Lib.regMenu({
[Transl("📝 設置選單")]: () => createMenu(Log, Transl)
});
}
const menuScript = `
<script id="menu-script">
function check(value) {
return value.toString().length > 4 || value > 1000
? 1000 : value < 0 ? "" : value;
}
<\/script>
`;
const getImgOptions = (title, key) => `
<div>
<h2 class="narrative">${title}:</h2>
<p>
<input type="number" data-key="${key}" class="Image-input-settings" oninput="value = check(value)">
<select data-key="${key}" class="Image-input-settings" style="margin-left: 1rem;">
<option value="px" selected>px</option>
<option value="%">%</option>
<option value="rem">rem</option>
<option value="vh">vh</option>
<option value="vw">vw</option>
<option value="auto">auto</option>
</select>
</p>
</div>
`;
function createMenu(Log, Transl) {
const shadowID = "shadow";
if (Lib.$q(`#${shadowID}`)) return;
const imgSet = Load.imgSet();
const imgSetData = [["圖片高度", "Height", imgSet.Height], ["圖片寬度", "Width", imgSet.Width], ["圖片最大寬度", "MaxWidth", imgSet.MaxWidth], ["圖片間隔高度", "Spacing", imgSet.Spacing]];
let analyze, img_set, img_input, img_select, set_value, save_cache = {};
const shadow = Lib.createElement(Lib.body, "div", {
id: shadowID
});
const shadowRoot = shadow.attachShadow({
mode: "open"
});
const menuSet = Load.menuSet();
const menuStyle = `
<style id="menu-style">
.modal-background {
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
z-index: 9999;
overflow: auto;
position: fixed;
pointer-events: none;
}
/* 模態介面 */
.modal-interface {
top: ${menuSet.Top};
left: ${menuSet.Left};
margin: 0;
display: flex;
overflow: auto;
position: fixed;
border-radius: 5px;
pointer-events: auto;
background-color: #2C2E3E;
border: 3px solid #EE2B47;
}
/* 設定介面 */
#image-settings-show {
width: 0;
height: 0;
opacity: 0;
padding: 10px;
overflow: hidden;
transition: opacity 0.8s, height 0.8s, width 0.8s;
}
/* 模態內容盒 */
.modal-box {
padding: 0.5rem;
height: 50vh;
width: 32vw;
}
/* 菜單框架 */
.menu {
width: 5.5vw;
overflow: auto;
text-align: center;
vertical-align: top;
border-radius: 2px;
border: 2px solid #F6F6F6;
}
/* 菜單文字標題 */
.menu-text {
color: #EE2B47;
cursor: default;
padding: 0.2rem;
margin: 0.3rem;
margin-bottom: 1.5rem;
white-space: nowrap;
border-radius: 10px;
border: 4px solid #f05d73;
background-color: #1f202c;
}
/* 菜單選項按鈕 */
.menu-options {
cursor: pointer;
font-size: 1.4rem;
color: #F6F6F6;
font-weight: bold;
border-radius: 5px;
margin-bottom: 1.2rem;
border: 5px inset #EE2B47;
background-color: #6e7292;
transition: color 0.8s, background-color 0.8s;
}
.menu-options:hover {
color: #EE2B47;
background-color: #F6F6F6;
}
.menu-options:disabled {
color: #6e7292;
cursor: default;
background-color: #c5c5c5;
border: 5px inset #faa5b2;
}
/* 設置內容框架 */
.content {
height: 48vh;
width: 28vw;
overflow: auto;
padding: 0px 1rem;
border-radius: 2px;
vertical-align: top;
border-top: 2px solid #F6F6F6;
border-right: 2px solid #F6F6F6;
}
.narrative { color: #EE2B47; }
.Image-input-settings {
width: 8rem;
color: #F6F6F6;
text-align: center;
font-size: 1.5rem;
border-radius: 15px;
border: 3px inset #EE2B47;
background-color: #202127;
}
.Image-input-settings:disabled {
border: 3px inset #faa5b2;
background-color: #5a5a5a;
}
/* 底部按鈕框架 */
.button-area {
display: flex;
padding: 0.3rem;
border-left: none;
border-radius: 2px;
border: 2px solid #F6F6F6;
justify-content: space-between;
}
.button-area select {
color: #F6F6F6;
margin-right: 1.5rem;
border: 3px inset #EE2B47;
background-color: #6e7292;
}
/* 底部選項 */
.button-options {
color: #F6F6F6;
cursor: pointer;
font-size: 0.8rem;
font-weight: bold;
border-radius: 10px;
white-space: nowrap;
background-color: #6e7292;
border: 3px inset #EE2B47;
transition: color 0.5s, background-color 0.5s;
}
.button-options:hover {
color: #EE2B47;
background-color: #F6F6F6;
}
.button-space { margin: 0 0.6rem; }
.toggle-menu {
width: 0;
height: 0;
padding: 0;
margin: 0;
}
/* 整體框線 */
table, td {
margin: 0px;
padding: 0px;
overflow: auto;
border-spacing: 0px;
}
.modal-background p {
display: flex;
flex-wrap: nowrap;
}
option { color: #F6F6F6; }
ul {
list-style: none;
padding: 0px;
margin: 0px;
}
</style>
`;
shadowRoot.$safeiHtml(`
${menuStyle}
${menuScript}
<div class="modal-background">
<div class="modal-interface">
<table class="modal-box">
<tr>
<td class="menu">
<h2 class="menu-text">${Transl("設置菜單")}</h2>
<ul>
<li>
<a class="toggle-menu">
<button class="menu-options" id="image-settings">${Transl("圖像設置")}</button>
</a>
<li>
<li>
<a class="toggle-menu">
<button class="menu-options" disabled>null</button>
</a>
<li>
</ul>
</td>
<td>
<table>
<tr>
<td class="content" id="set-content">
<div id="image-settings-show"></div>
</td>
</tr>
<tr>
<td class="button-area">
<select id="language">
<option value="" disabled selected>${Transl("語言")}</option>
<option value="en-US">${Transl("英文")}</option>
<option value="ru">${Transl("俄語")}</option>
<option value="zh-TW">${Transl("繁體")}</option>
<option value="zh-CN">${Transl("簡體")}</option>
<option value="ja">${Transl("日文")}</option>
<option value="ko">${Transl("韓文")}</option>
</select>
<button id="readsettings" class="button-options" disabled>${Transl("讀取設定")}</button>
<span class="button-space"></span>
<button id="closure" class="button-options">${Transl("關閉離開")}</button>
<span class="button-space"></span>
<button id="application" class="button-options">${Transl("保存應用")}</button>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</div>
`);
const languageEl = shadowRoot.querySelector("#language");
const readsetEl = shadowRoot.querySelector("#readsettings");
const interfaceEl = shadowRoot.querySelector(".modal-interface");
const imageSetEl = shadowRoot.querySelector("#image-settings-show");
languageEl.value = Log ?? "en-US";
draggable(interfaceEl);
menuRule = shadowRoot.querySelector("#menu-style")?.sheet?.cssRules;
const menuRequ = {
menuClose() {
shadow.remove();
},
menuSave() {
const styles = getComputedStyle(interfaceEl);
Lib.setV(Parame.SaveKey.Menu, {
Top: styles.top,
Left: styles.left
});
},
imgSave() {
img_set = imageSetEl.querySelectorAll("p");
if (img_set.length === 0) return;
imgSetData.forEach(([title, key, set], index) => {
img_input = img_set[index].querySelector("input");
img_select = img_set[index].querySelector("select");
const inputVal = img_input.value;
const selectVal = img_select.value;
set_value = selectVal === "auto" ? "auto" : inputVal === "" ? set : `${inputVal}${selectVal}`;
save_cache[img_input.$gAttr("data-key")] = set_value;
});
Lib.setV(Parame.SaveKey.Img, save_cache);
},
async imgSettings() {
let running = false;
const handle = event => {
if (running) return;
running = true;
const target = event.target;
if (!target) {
running = false;
return;
}
const key = target.$gAttr("data-key");
const value = target?.value;
if (isNaN(value)) {
const input = target.previousElementSibling;
if (value === "auto") {
input.disabled = true;
stylePointer[key](value);
} else {
input.disabled = false;
stylePointer[key](`${input.value}${value}`);
}
} else {
const select = target.nextElementSibling;
stylePointer[key](`${value}${select.value}`);
}
setTimeout(() => running = false, 100);
};
Lib.onEvent(imageSetEl, "input", handle);
Lib.onEvent(imageSetEl, "change", handle);
}
};
Lib.onE(languageEl, "change", event => {
event.stopImmediatePropagation();
const value = event.currentTarget.value;
Lib.setV(Parame.SaveKey.Lang, value);
menuRequ.menuSave();
menuRequ.menuClose();
menuInit(Updata => {
createMenu(Updata.Log, Updata.Transl);
});
});
Lib.onE(interfaceEl, "click", event => {
const target = event.target;
const id = target?.id;
if (!id) return;
if (id === "image-settings") {
const imgsetCss = menuRule[2].style;
if (imgsetCss.opacity === "0") {
let dom = "";
imgSetData.forEach(([title, key]) => {
dom += getImgOptions(Transl(title), key) + "\n";
});
imageSetEl.insertAdjacentHTML("beforeend", dom);
Object.assign(imgsetCss, {
width: "auto",
height: "auto",
opacity: "1"
});
target.disabled = true;
readsetEl.disabled = false;
menuRequ.imgSettings();
}
} else if (id === "readsettings") {
img_set = imageSetEl.querySelectorAll("p");
if (img_set.length === 0) return;
imgSetData.forEach(([title, key, set], index) => {
img_input = img_set[index].querySelector("input");
img_select = img_set[index].querySelector("select");
if (set === "auto") {
img_input.disabled = true;
img_select.value = set;
} else {
analyze = set?.match(/^(\d+)(\D+)$/);
if (!analyze) return;
img_input.value = analyze[1];
img_select.value = analyze[2];
}
});
} else if (id === "application") {
menuRequ.imgSave();
menuRequ.menuSave();
menuRequ.menuClose();
} else if (id === "closure") {
menuRequ.menuClose();
}
});
}
return {
menuInit: menuInit,
postViewInit: postViewInit
};
})();
function Main() {
const Enhance = (() => {
const runningOrder = {
Global: ["BlockAds", "CacheFetch", "SidebarCollapse", "DeleteNotice", "TextToLink", "BetterPostCard", "KeyScroll"],
Preview: ["CardText", "CardZoom", "NewTabOpens", "QuickPostToggle", "BetterThumbnail"],
Content: ["LinkBeautify", "VideoBeautify", "OriginalImage", "ExtraButton", "CommentFormat"]
};
const loadFunc = {
Global: globalLoader,
Preview: previewLoader,
Content: contentLoader
};
async function call(page, config = User_Config[page]) {
const func = loadFunc[page];
for (const ord of runningOrder[page]) {
let userConfig = config[ord];
if (!userConfig) continue;
if (typeof userConfig !== "object") {
userConfig = {
enable: true
};
} else if (!userConfig.enable) continue;
func[ord]?.(userConfig);
}
}
return {
async run() {
call("Global");
if (Page.isPreview()) call("Preview"); else if (Page.isContent()) {
MenuFactory.postViewInit();
call("Content");
MenuFactory.menuInit();
}
}
};
})();
Enhance.run();
{
const waitDom = new MutationObserver(() => {
waitDom.disconnect();
Enhance.run();
});
Lib.onUrlChange(change => {
Parame.Url = change.url;
waitDom.observe(document, {
attributes: true,
childList: true,
subtree: true,
characterData: true
});
Lib.body.$sAttr("Enhance", true);
});
}
}
Main();
})();