// ==UserScript== // @name Pixiv收藏夹自动标签 // @name:en Label Pixiv Bookmarks // @namespace http://tampermonkey.net/ // @version 5.18 // @description 自动为Pixiv收藏夹内图片打上已有的标签,并可以搜索收藏夹 // @description:en Automatically add existing labels for images in the bookmarks, and users are able to search the bookmarks // @author philimao // @match https://www.pixiv.net/*users/* // @icon https://www.google.com/s2/favicons?domain=pixiv.net // @resource bootstrapCSS https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css // @resource bootstrapJS https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js // @grant unsafeWindow // @grant GM_getResourceURL // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/423823/Pixiv%E6%94%B6%E8%97%8F%E5%A4%B9%E8%87%AA%E5%8A%A8%E6%A0%87%E7%AD%BE.user.js // @updateURL https://update.greasyfork.icu/scripts/423823/Pixiv%E6%94%B6%E8%97%8F%E5%A4%B9%E8%87%AA%E5%8A%A8%E6%A0%87%E7%AD%BE.meta.js // ==/UserScript== const version = "5.18"; const latest = `♢ 处理Pixiv组件类名更新 ♢ Update constants due to change of element class names ♢ 并非所有功能都已恢复,仅验证了设置标签功能 ♢ Note that not all functions have been restored. Only labeling function has been validated.`; let uid, token, lang, userTags, userTagDict, synonymDict, pageInfo, theme, showWorkTags, generator, // workType, feature, turboMode, cachedBookmarks = {}, DEBUG; // noinspection TypeScriptUMDGlobal,JSUnresolvedVariable let unsafeWindow_ = unsafeWindow, GM_getValue_ = GM_getValue, GM_setValue_ = GM_setValue, GM_addStyle_ = GM_addStyle, GM_getResourceURL_ = GM_getResourceURL, GM_registerMenuCommand_ = GM_registerMenuCommand; // selectors const BANNER = ".sc-8bf48ebe-0"; const THEME_CONTAINER = "html"; const WORK_SECTION = "section.sc-3d8ed48f-0"; // 作品section,从works->pagination const WORK_CONTAINER = "ul.sc-7d21cb21-1.jELUak"; // 仅包含作品 const PAGE_BODY = ".sc-2b45994f-0.cUskQy"; // 自主页、收藏起下方 const EDIT_BUTTON_CONTAINER = ".sc-9d335d39-6.cfUrtF"; // 管理收藏按钮父容器,包含左侧作品文字 const REMOVE_BOOKMARK_CONTAINER = ".sc-231887f1-4.kvBpUA"; const WORK_NUM = ".sc-b5e6ab10-0.hfQbJx"; const ADD_TAGS_MODAL_ENTRY = ".bbTNLI"; // 原生添加标签窗口中标签按钮 const ALL_TAGS_BUTTON = ".jkGZFM"; // 标签切换窗口触发按钮 const ALL_TAGS_CONTAINER = ".hpRxDJ"; // 标签按钮容器 const ALL_TAGS_MODAL = ".ggMyQW"; // 原生标签切换窗口 const ALL_TAGS_MODAL_CONTAINER = ".gOPhqx"; // 原生标签切换窗口中标签按钮容器 function getCharacterName(tag) { return tag.split("(")[0]; } function getWorkTitle(tag) { return (tag.split("(")[1] || "").split(")")[0]; } function stringIncludes(s1, s2) { const isString = (s) => typeof s === "string" || s instanceof String; if (!isString(s1) || !isString(s2)) throw new Error("Argument is not a string"); return s1.includes(s2); } function arrayIncludes(array, element, func1, func2, fuzzy) { if (!Array.isArray(array)) throw new TypeError("First argument is not an array"); let array1 = func1 ? array.map(func1) : array; let array2 = Array.isArray(element) ? element : [element]; array2 = func2 ? array2.map(func2) : array2; const el = [...array1, ...array2].find((i) => !i.toUpperCase); if (el) { console.log(el, array, element); throw new TypeError( `Element ${el.toString()} does not have method toUpperCase`, ); } array1 = array1.map((i) => i.toUpperCase()); array2 = array2.map((i) => i.toUpperCase()); if (fuzzy) return array2.every((i2) => array1.some((i1) => stringIncludes(i1, i2))); else return array2.every((i) => array1.includes(i)); } function isEqualObject(obj1, obj2) { if (typeof obj1 !== "object") return obj1 === obj2; return ( typeof obj1 === typeof obj2 && Object.keys(obj1).every((key, i) => key === Object.keys(obj2)[i]) && Object.values(obj1).every((value, i) => isEqualObject(value, Object.values(obj2)[i]), ) ); } function delay(ms) { return new Promise((res) => setTimeout(res, ms)); } function chunkArray(arr, chunkSize) { return Array.from({ length: Math.ceil(arr.length / chunkSize) }, (_, index) => arr.slice(index * chunkSize, index * chunkSize + chunkSize), ); } function getValue(name, defaultValue) { return GM_getValue_(name, defaultValue); } function setValue(name, value) { if (name === "synonymDict" && (!value || !Object.keys(value).length)) return; GM_setValue_(name, value); // backup let valueArray = JSON.parse(window.localStorage.getItem(name)); if (!valueArray) valueArray = []; // save the dict by date if (name === "synonymDict") { const date = new Date().toLocaleDateString(); // not update if of same value if (valueArray.length) { if ( !valueArray.find( (el) => JSON.stringify(el.value) === JSON.stringify(value), ) ) { const lastElem = valueArray[valueArray.length - 1]; if (lastElem.date === date) { // append only for (let key of Object.keys(value)) { if (lastElem.value[key]) { // previous key lastElem.value[key] = Array.from( new Set(lastElem["value"][key].concat(value[key])), ); } else { // new key lastElem.value[key] = value[key]; } } valueArray.pop(); valueArray.push(lastElem); } else { if (valueArray.length > 30) valueArray.shift(); valueArray.push({ date, value }); } window.localStorage.setItem(name, JSON.stringify(valueArray)); } else { // same value, pass } } else { // empty array valueArray.push({ date, value }); window.localStorage.setItem(name, JSON.stringify(valueArray)); } } else { if (valueArray.length > 30) valueArray.shift(); valueArray.push(value); window.localStorage.setItem(name, JSON.stringify(valueArray)); } } function addStyle(style) { GM_addStyle_(style); } // merge all previous dict and return function restoreSynonymDict() { const value = window.localStorage.getItem("synonymDict"); if (!value) return {}; const dictArray = JSON.parse(value); const newDict = {}; for (let elem of dictArray) { const dict = elem.value; // merge all history value for the key Object.keys(dict).forEach((key) => { if (newDict[key]) newDict[key] = Array.from(new Set(newDict[key].concat(dict[key]))); else newDict[key] = dict[key]; }); } const a = document.createElement("a"); a.href = URL.createObjectURL( new Blob([JSON.stringify([newDict].concat(dictArray))], { type: "application/json", }), ); a.setAttribute( "download", `synonym_dict_restored_${new Date().toLocaleDateString()}.json`, ); a.click(); } function sortByParody(array) { const sortFunc = (a, b) => { let reg = /^[a-zA-Z0-9]/; if (reg.test(a) && !reg.test(b)) return -1; else if (!reg.test(a) && reg.test(b)) return 1; else return a.localeCompare(b, "zh"); }; const withParody = array.filter((key) => key.includes("(")); const withoutParody = array.filter((key) => !key.includes("(")); withoutParody.sort(sortFunc); withParody.sort(sortFunc); withParody.sort((a, b) => sortFunc(a.split("(")[1], b.split("(")[1])); return withoutParody.concat(withParody); } function loadResources() { function cssElement(url) { const link = document.createElement("link"); link.id = "bootstrapCSS"; link.href = url; link.rel = "stylesheet"; link.type = "text/css"; return link; } function jsElement(url) { const script = document.createElement("script"); script.id = "bootstrapJS"; script.src = url; return script; } document.head.appendChild(cssElement(GM_getResourceURL_("bootstrapCSS"))); document.head.appendChild(jsElement(GM_getResourceURL_("bootstrapJS"))); // overwrite bootstrap global box-sizing style const style = document.createElement("style"); style.id = "LB_overwrite"; style.innerHTML = "*,::after,::before { box-sizing: content-box; } .btn,.form-control,.form-select,.row>* { box-sizing: border-box; } body { background: initial; } a {color: inherit; text-decoration: none} .collapse.show {visibility: visible}"; document.head.appendChild(style); if (DEBUG) console.log("[Label Bookmarks] Stylesheet Loaded"); } const bookmarkBatchSize = 100; async function fetchBookmarks(uid, tagToQuery, offset, publicationType) { const bookmarksRaw = await fetch( `/ajax/user/${uid}` + `/illusts/bookmarks?tag=${tagToQuery}` + `&offset=${offset}&limit=${bookmarkBatchSize}&rest=${publicationType}`, ); if (!turboMode) await delay(500); const bookmarksRes = await bookmarksRaw.json(); if (!bookmarksRaw.ok || bookmarksRes.error === true) { return alert( `获取用户收藏夹列表失败\nFail to fetch user bookmarks\n` + decodeURI(bookmarksRes.message), ); } else return bookmarksRes.body; } async function fetchAllBookmarksByTag( tag, publicationType, progressBar, max = 100, ) { let total = 65535, offset = 0, totalWorks = []; try { while (offset < total && window.runFlag) { if (turboMode) { const fetchPromises = []; const bookmarksBatch = []; const batchSize = 10; for (let i = 0; i < batchSize && offset < total; i++) { bookmarksBatch.push( fetchBookmarks(uid, tag, offset, publicationType), ); offset += max; } const batchResults = await Promise.all(bookmarksBatch); for (const bookmarks of batchResults) { total = bookmarks.total; for (const work of bookmarks["works"]) { const fetchedWork = { ...work, associatedTags: bookmarks["bookmarkTags"][work["bookmarkData"]["id"]] || [], }; totalWorks.push(fetchedWork); fetchPromises.push(fetchedWork); } } await Promise.all(fetchPromises); await delay(500); } else { const bookmarks = await fetchBookmarks( uid, tag, offset, publicationType, ); total = bookmarks.total; const works = bookmarks["works"]; works.forEach( (w) => (w.associatedTags = bookmarks["bookmarkTags"][w["bookmarkData"]["id"]] || []), ); totalWorks.push(...works); offset = totalWorks.length; } if (progressBar) { progressBar.innerText = totalWorks.length + "/" + total; const ratio = ((totalWorks.length / total) * max).toFixed(2); progressBar.style.width = ratio + "%"; } } } catch (err) { window.alert( `获取收藏夹时发生错误,请截图到GitHub反馈\nAn error was caught during fetching bookmarks. You might report it on GitHub\n${err.name}: ${err.message}\n${err.stack}`, ); console.log(err); } finally { if (progressBar) { progressBar.innerText = total + "/" + total; progressBar.style.width = "100%"; } } return totalWorks; } async function addBookmark(illust_id, restrict, tags) { const resRaw = await fetch("/ajax/illusts/bookmarks/add", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ illust_id, restrict, comment: "", tags, }), method: "POST", }); await delay(500); return resRaw; } async function removeBookmark(bookmarkIds, progressBar) { async function run(ids) { await fetch("/ajax/illusts/bookmarks/remove", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ bookmarkIds: ids }), method: "POST", }); await delay(500); } const num = Math.ceil(bookmarkIds.length / bookmarkBatchSize); for (let i of [...Array(num).keys()]) { if (!window.runFlag) break; const ids = bookmarkIds.filter( (_, j) => j >= i * bookmarkBatchSize && j < (i + 1) * bookmarkBatchSize, ); await run(ids); if (progressBar) { const offset = i * bookmarkBatchSize; progressBar.innerText = offset + "/" + bookmarkIds.length; const ratio = ((offset / bookmarkIds.length) * 100).toFixed(2); progressBar.style.width = ratio + "%"; } } if (progressBar) { progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; progressBar.style.width = "100%"; } } async function updateBookmarkTags( bookmarkIds, addTags, removeTags, progressBar, ) { if (!bookmarkIds?.length) throw new TypeError("BookmarkIds is undefined or empty array"); if (!Array.isArray(addTags) && !Array.isArray(removeTags)) throw new TypeError("Either addTags or removeTags should be valid array"); async function fetchRequest(url, data) { return await fetch(url, { method: "POST", headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify(data), }); } async function run(ids) { if (turboMode) { const requests = []; if (addTags && addTags.length) { const addTagsChunks = chunkArray(addTags, bookmarkBatchSize); for (const tagsChunk of addTagsChunks) { requests.push( fetchRequest("/ajax/illusts/bookmarks/add_tags", { tags: tagsChunk, bookmarkIds: ids, }), ); } } if (removeTags && removeTags.length) { const removeTagsChunks = chunkArray(removeTags, bookmarkBatchSize); for (const tagsChunk of removeTagsChunks) { requests.push( fetchRequest("/ajax/illusts/bookmarks/remove_tags", { removeTags: tagsChunk, bookmarkIds: ids, }), ); } } if (requests.length > 1) await Promise.all(requests); await delay(500); } else { if (addTags && addTags.length) { await fetchRequest("/ajax/illusts/bookmarks/add_tags", { tags: addTags, bookmarkIds: ids, }); await delay(500); } if (removeTags && removeTags.length) { await fetchRequest("/ajax/illusts/bookmarks/remove_tags", { removeTags, bookmarkIds: ids, }); await delay(500); } } } let i = 0; for (const ids of chunkArray(bookmarkIds, bookmarkBatchSize)) { if (!window.runFlag) break; await run(ids); if (progressBar) { i++; const offset = Math.min(i * bookmarkBatchSize, bookmarkIds.length); progressBar.innerText = offset + "/" + bookmarkIds.length; const ratio = ((offset / bookmarkIds.length) * 100).toFixed(2); progressBar.style.width = ratio + "%"; } } if (progressBar) { progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; progressBar.style.width = "100%"; } } async function updateBookmarkRestrict( bookmarkIds, bookmarkRestrict, progressBar, ) { if (!bookmarkIds?.length) throw new TypeError("BookmarkIds is undefined or empty array"); if (!["public", "private"].includes(bookmarkRestrict)) throw new TypeError("Bookmark restrict should be public or private"); async function run(ids) { await fetch("/ajax/illusts/bookmarks/edit_restrict", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ bookmarkIds: ids, bookmarkRestrict }), method: "POST", }); await delay(500); } const num = Math.ceil(bookmarkIds.length / bookmarkBatchSize); for (let i of [...Array(num).keys()]) { if (!window.runFlag) break; const ids = bookmarkIds.filter( (_, j) => j >= i * bookmarkBatchSize && j < (i + 1) * bookmarkBatchSize, ); await run(ids); if (progressBar) { const offset = i * bookmarkBatchSize; progressBar.innerText = offset + "/" + bookmarkIds.length; const ratio = ((offset / bookmarkIds.length) * 100).toFixed(2); progressBar.style.width = ratio + "%"; } } if (progressBar) { progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; progressBar.style.width = "100%"; } } async function clearBookmarkTags(works) { if (!works?.length) { return alert( `没有获取到收藏夹内容,操作中断,请检查选项下是否有作品\nFetching bookmark information failed. Abort operation. Please check the existence of works with the configuration`, ); } if ( !window.confirm( `确定要删除所选作品的标签吗?(作品的收藏状态不会改变)\nThe tags of work(s) you've selected will be removed (become uncategorized). Is this okay?`, ) ) return; window.runFlag = true; const modal = document.querySelector("#progress_modal"); // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; let instance = bootstrap_.Modal.getInstance(modal); if (!instance) instance = new bootstrap_.Modal(modal); instance.show(); const prompt = document.querySelector("#progress_modal_prompt"); const progressBar = document.querySelector("#progress_modal_progress_bar"); const tagPool = Array.from( new Set(works.reduce((a, b) => [...a, ...b.associatedTags], [])), ); const workLength = works.length; const tagPoolSize = tagPool.length; if (DEBUG) console.log(works, tagPool); if (workLength > tagPoolSize) { for (let index = 1; index <= tagPoolSize; index++) { if (!window.runFlag) break; const tag = tagPool[index - 1]; const ids = works .filter((w) => w.associatedTags.includes(tag)) .map((w) => w.bookmarkId || w["bookmarkData"]["id"]); if (DEBUG) console.log("Clearing", tag, ids); progressBar.innerText = index + "/" + tagPoolSize; const ratio = ((index / tagPoolSize) * 100).toFixed(2); progressBar.style.width = ratio + "%"; prompt.innerText = `正在清除标签... / Clearing bookmark tags`; await updateBookmarkTags(ids, null, [tag]); } } else { for (let index = 1; index <= workLength; index++) { if (!window.runFlag) break; const work = works[index - 1]; const url = "https://www.pixiv.net/artworks/" + work.id; console.log(index, work.title, work.id, url); if (DEBUG) console.log(work); progressBar.innerText = index + "/" + workLength; const ratio = ((index / workLength) * 100).toFixed(2); progressBar.style.width = ratio + "%"; prompt.innerText = work.alt + "\n" + work.associatedTags.join(" "); await updateBookmarkTags( [work.bookmarkId || work["bookmarkData"]["id"]], undefined, work.associatedTags, ); } } if (window.runFlag) prompt.innerText = `标签删除完成!\nFinish Tag Clearing!`; else prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; setTimeout(() => { instance.hide(); if (window.runFlag && !DEBUG) window.location.reload(); }, 1000); } async function handleClearBookmarkTags(evt) { evt.preventDefault(); const selected = [ ...document.querySelectorAll("label>div[aria-disabled='true']"), ]; if (!selected.length) return; const works = selected .map((el) => { const middleChild = Object.values( el.parentNode.parentNode.parentNode.parentNode, )[0]["child"]; const work = middleChild["memoizedProps"]["work"]; work.associatedTags = middleChild["child"]["memoizedProps"]["associatedTags"] || []; work.bookmarkId = middleChild["memoizedProps"]["bookmarkId"]; return work; }) .filter((work) => work.associatedTags.length); await clearBookmarkTags(works); } async function deleteTag(tag, publicationType) { if (!tag) return alert( `请选择需要删除的标签\nPlease select the tag you would like to delete`, ); if ( tag === "未分類" || !window.confirm( `确定要删除所选的标签 ${tag} 吗?(作品的收藏状态不会改变)\nThe tag ${tag} will be removed and works of ${tag} will keep bookmarked. Is this okay?`, ) ) return; window.runFlag = true; const modal = document.querySelector("#progress_modal"); // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; let instance = bootstrap_.Modal.getInstance(modal); if (!instance) instance = new bootstrap_.Modal(modal); await instance.show(); const prompt = document.querySelector("#progress_modal_prompt"); const progressBar = document.querySelector("#progress_modal_progress_bar"); const totalBookmarks = await fetchAllBookmarksByTag( tag, publicationType, progressBar, 90, ); console.log(totalBookmarks); if (window.runFlag) { prompt.innerText = `标签${tag}删除中...\nDeleting Tag ${tag}`; progressBar.style.width = "90%"; } else { prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; progressBar.style.width = "100%"; return; } const ids = totalBookmarks.map((work) => work["bookmarkData"]["id"]); await updateBookmarkTags(ids, undefined, [tag]); progressBar.style.width = "100%"; if (window.runFlag) prompt.innerText = `标签${tag}删除完成!\nTag ${tag} Removed!`; else prompt.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; setTimeout(() => { instance.hide(); if (window.runFlag && !DEBUG) window.location.href = `https://www.pixiv.net/users/${uid}/bookmarks/artworks?rest=${publicationType}`; }, 1000); } async function handleDeleteTag(evt) { evt.preventDefault(); const { tag, restrict } = await updateWorkInfo(); await deleteTag(tag, restrict ? "hide" : "show"); } async function handleLabel(evt) { evt.preventDefault(); const addFirst = document.querySelector("#label_add_first").value; const addAllTags = document.querySelector("#label_add_all_tags").value; const tagToQuery = document.querySelector("#label_tag_query").value; const publicationType = (await updateWorkInfo())["restrict"] ? "hide" : "show"; const labelR18 = document.querySelector("#label_r18").value; const labelSafe = document.querySelector("#label_safe").value; const labelAI = document.querySelector("#label_ai").value; const labelAuthor = document.querySelector("#label_author").value; const labelStrict = document.querySelector("#label_strict").value; const exclusion = document .querySelector("#label_exclusion") .value.split(/[\s\n]/) .filter((t) => t); console.log("Label Configuration:"); console.log( `addFirst: ${addFirst === "true"}; addAllTags: ${ addAllTags === "true" }; tagToQuery: ${tagToQuery}; labelR18: ${ labelR18 === "true" }; labelSafe: ${labelSafe}; labelAI: ${labelAI}; labelAuthor: ${ labelAuthor === "true" }; publicationType: ${publicationType}; exclusion: ${exclusion.join(",")}`, ); if ( addAllTags === "true" && !window.confirm(`作品自带的所有标签都会被优先加入用户标签,这将导致大量的标签被添加,是否确定? All tags that come with the work will be first added to your user tags, which can be large. Is this okay?`) ) return; window.runFlag = true; const promptBottom = document.querySelector("#label_prompt"); promptBottom.innerText = "处理中,请勿关闭窗口\nProcessing. Please do not close the window."; const objDiv = document.querySelector("#label_form"); objDiv.scrollTop = objDiv.scrollHeight; // fetch bookmarks let total, // total bookmarks of specific tag index = 0, // counter of do-while loop offset = 0; // as uncategorized ones will decrease, offset means num of images updated "successfully" // update progress bar const progressBar = document.querySelector("#progress_bar"); progressBar.style.width = "0"; const intervalId = setInterval(() => { if (total) { progressBar.innerText = index + "/" + total; const ratio = ((index / total) * 100).toFixed(2); progressBar.style.width = ratio + "%"; if (!window.runFlag || index === total) { console.log("Progress bar stops updating"); clearInterval(intervalId); } } }, 1000); do { const realOffset = tagToQuery === "未分類" ? offset : index; const bookmarks = await fetchBookmarks( uid, tagToQuery, realOffset, publicationType, ); if (DEBUG) console.log("Bookmarks", bookmarks); if (!total) total = bookmarks.total; for (let work of bookmarks["works"]) { const url = "https://www.pixiv.net/artworks/" + work.id; if (DEBUG) console.log(index, work.title, work.id, url); index++; // ---- means unavailable, hidden or deleted by author if (work.title === "-----") { offset++; continue; } const workTags = work["tags"]; let intersection = []; // add all work tags, and replace those which are defined in synonym dict if (addAllTags === "true") intersection = workTags.map( (workTag) => Object.keys(synonymDict).find((userTag) => synonymDict[userTag].includes(workTag), ) || workTag, ); if (labelAuthor === "true") workTags.push(work["userName"], work["userId"]); intersection = intersection.concat( [...userTags, ...Object.keys(synonymDict)].filter((userTag) => { // if work tags includes this user tag if ( arrayIncludes(workTags, userTag) || // full tag arrayIncludes(workTags, userTag, getWorkTitle) || // work title (arrayIncludes(workTags, userTag, null, getCharacterName) && // char name (!labelStrict || arrayIncludes(workTags, userTag, null, getWorkTitle))) // not strict or includes work title ) return true; // if work tags match a user alias (exact match) return ( synonymDict[userTag] && synonymDict[userTag].find( (alias) => arrayIncludes(workTags, alias) || arrayIncludes(workTags, alias, getWorkTitle) || // work title (arrayIncludes(workTags, alias, null, getCharacterName) && (!labelStrict || arrayIncludes(workTags, alias, null, getWorkTitle))), ) ); }), ); // if workTags match some alias, add it to the intersection (exact match, with or without work title) intersection = intersection.concat( Object.keys(synonymDict).filter((aliasName) => { if (!synonymDict[aliasName]) { console.log(aliasName, synonymDict[aliasName]); throw new Error("Empty value in synonym dictionary"); } if ( workTags.some( (workTag) => arrayIncludes( synonymDict[aliasName].concat(aliasName), workTag, null, getWorkTitle, ) || arrayIncludes( synonymDict[aliasName].concat(aliasName), workTag, null, getCharacterName, ), ) ) return true; }), ); if (work["xRestrict"] && labelR18 === "true") intersection.push("R-18"); if (!work["xRestrict"] && labelSafe === "true") intersection.push("SFW"); if (work["aiType"] === 2 && labelAI === "true") intersection.push("AI"); // remove duplicate and exclusion intersection = Array.from(new Set(intersection)).filter( (t) => !exclusion.includes(t), ); const bookmarkId = work["bookmarkData"]["id"]; const prevTags = bookmarks["bookmarkTags"][bookmarkId] || []; if (!intersection.length && !prevTags.length) { if (addFirst === "true") { const first = workTags .filter( (tag) => !exclusion.includes(tag) && tag.length <= 20 && !tag.includes("入り"), ) .slice(0, 1); // Can be changed if you want to add more than 1 tag from the same work if (first) { intersection.push(...first); userTags.push(...first); } } } const addTags = intersection.filter((tag) => !prevTags.includes(tag)); // for uncategorized if (!intersection.length) { offset++; } if (addTags.length) { if (!DEBUG) console.log(index, work.title, work.id, url); console.log("\tprevTags:", prevTags); console.log("\tintersection:", intersection); console.log("\taddTags:", addTags); } else continue; promptBottom.innerText = `处理中,请勿关闭窗口 / Processing. Please do not close the window.\n${work.alt}`; await updateBookmarkTags([bookmarkId], addTags); if (!window.runFlag) { promptBottom.innerText = "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; index = total; break; } } } while (index < total); if (total === 0) { promptBottom.innerText = `指定分类下暂无符合要求的作品,请关闭窗口 Works needed to be labeled not found. Please close the window. `; } else if (window.runFlag) { promptBottom.innerText = `自动添加标签已完成,请关闭窗口并刷新网页 Auto labeling finished successfully. Please close the window and refresh. `; } window.runFlag = false; } let prevSearch, searchBatch, searchResults, searchOffset, totalBookmarks; async function handleSearch(evt) { evt.preventDefault(); let searchMode = 0; let searchString = document.querySelector("#search_value")?.value; if ( document.querySelector("#basic_search_field").className.includes("d-none") ) { searchMode = 1; searchString = [...document.querySelectorAll(".advanced_search_field")] .map((el) => el.value.split(" ")[0]) .join(" "); } searchString = searchString.replace(/!/g, "!").trim(); const searchStringArray = searchString.split(" "); let searchConfigs = Array(searchStringArray.length).fill(Array(4).fill(true)); if (searchMode) { const advanced = document.querySelector("#advanced_search_fields"); const configContainers = [...advanced.querySelectorAll(".row")]; searchConfigs = configContainers.map((el) => [...el.querySelectorAll("input")].map((i) => i.checked), ); } const matchPattern = document.querySelector("#search_exact_match").value; const tagsLengthMatch = document.querySelector("#search_length_match").value === "true"; const tagToQuery = document.querySelector("#search_select_tag").value; const publicationType = document.querySelector("#search_publication").value; const newSearch = { searchString, searchConfigs, matchPattern, tagsLengthMatch, tagToQuery, publicationType, }; // initialize new search window.runFlag = true; const resultsDiv = document.querySelector("#search_results"); const noResult = document.querySelector("#no_result"); if (noResult) resultsDiv.removeChild(noResult); if (!prevSearch || !isEqualObject(prevSearch, newSearch)) { prevSearch = newSearch; searchResults = []; searchOffset = 0; totalBookmarks = 0; searchBatch = 200; document.querySelector("#search_prompt").innerText = ""; while (resultsDiv.firstChild) { resultsDiv.removeChild(resultsDiv.firstChild); } clearTimeout(timeout); document.querySelector("#search_suggestion").parentElement.style.display = "none"; } else { searchBatch += 200; } if (searchOffset && searchOffset === totalBookmarks) { window.runFlag = false; return alert(` 已经完成所选标签下所有收藏的搜索! All Bookmarks Of Selected Tag Have Been Searched! `); } const spinner = document.querySelector("#spinner"); spinner.style.display = "block"; // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; const collapseIns = bootstrap_.Collapse.getInstance( document.querySelector("#advanced_search"), ); if (collapseIns) collapseIns.hide(); let includeArray = searchStringArray.filter( (el) => el.length && !el.includes("!"), ); let excludeArray = searchStringArray .filter((el) => el.length && el.includes("!")) .map((el) => el.slice(1)); console.log("Search Configuration:", searchConfigs); console.log( `matchPattern: ${matchPattern}; tagsLengthMatch: ${tagsLengthMatch}; tagToQuery: ${tagToQuery}; publicationType: ${publicationType}`, ); console.log("includeArray:", includeArray, "excludeArray", excludeArray); const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; const searchPrompt = document.querySelector("#search_prompt"); let index = 0; // index for current search batch do { const bookmarks = await fetchBookmarks( uid, tagToQuery, searchOffset, publicationType, ); searchPrompt.innerText = ` 当前搜索进度 / Searched:${searchOffset} / ${totalBookmarks} `; if (DEBUG) console.log(bookmarks); if (!totalBookmarks) { totalBookmarks = bookmarks.total; } for (let work of bookmarks["works"]) { if (DEBUG) { console.log(searchOffset, work.title, work.id); console.log(work["tags"]); } index++; searchOffset++; if (work.title === "-----") continue; const bookmarkTags = bookmarks["bookmarkTags"][work["bookmarkData"]["id"]] || []; // empty if uncategorized work.bookmarkTags = bookmarkTags; const workTags = work["tags"]; const ifInclude = (keyword) => { // especially, R-18 tag is labelled in work if (["R-18", "r-18", "R18", "r18"].includes(keyword)) return work["xRestrict"]; const index = searchStringArray.findIndex((kw) => kw.includes(keyword)); const config = searchConfigs[index]; if (DEBUG) console.log(keyword, config); // convert input keyword to a user tag // keywords from user input, alias from dict // keyword: 新世纪福音战士 // alias: EVA eva const el = Object.keys(synonymDict) .map((key) => [key.split("(")[0], key]) // [char name, full key] .find( (el) => stringIncludes(el[0], keyword) || // input match char name stringIncludes(el[1], keyword) || // input match full name arrayIncludes(synonymDict[el[1]], keyword) || // input match any alias (matchPattern === "fuzzy" && (stringIncludes(el[1], keyword) || arrayIncludes( synonymDict[el[1]], keyword, null, null, true, ))), ); const keywordArray = [keyword]; if (el) { keywordArray.push(...el); keywordArray.push(...synonymDict[el[1]]); } if ( keywordArray.some( (kw) => (config[0] && stringIncludes(work.title, kw)) || (config[1] && stringIncludes(work["userName"], kw)) || (config[2] && arrayIncludes(workTags, kw)) || (config[3] && arrayIncludes(bookmarkTags, kw)), ) ) return true; if (matchPattern === "exact") return false; return keywordArray.some( (kw) => (config[2] && arrayIncludes(workTags, kw, null, null, true)) || (config[3] && arrayIncludes(bookmarkTags, kw, null, null, true)), ); }; if ( (!tagsLengthMatch || includeArray.length === bookmarkTags.length) && includeArray.every(ifInclude) && !excludeArray.some(ifInclude) ) { searchResults.push(work); displayWork(work, resultsDiv, textColor); } } } while (searchOffset < totalBookmarks && index < searchBatch); if (totalBookmarks === 0) document.querySelector("#search_prompt").innerText = "无结果 / No Result"; else document.querySelector("#search_prompt").innerText = ` 当前搜索进度 / Searched:${searchOffset} / ${totalBookmarks} `; if (searchOffset < totalBookmarks) document.querySelector("#search_more").style.display = "block"; else document.querySelector("#search_more").style.display = "none"; if (!searchResults.length) { resultsDiv.innerHTML = `
暂无结果 / No Result
`; } spinner.style.display = "none"; console.log(searchResults); window.runFlag = false; } function displayWork(work, resultsDiv, textColor) { const tagsString = work.tags .slice(0, 6) .map((i) => "#" + i) .join(" "); const container = document.createElement("div"); const profile = work["profileImageUrl"] || "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; container.className = "col-4 col-lg-3 col-xl-2 p-1"; container.innerHTML = `
square
R-18
${work["pageCount"]}
${tagsString}
${work.title}
profile ${work["userName"]}
`; if (work["xRestrict"]) container.querySelector(".rate-icon").classList.remove("d-none"); if (work["pageCount"] > 1) container.querySelector(".page-icon").classList.remove("d-none"); container.firstElementChild.addEventListener("click", (evt) => galleryMode(evt, work), ); resultsDiv.appendChild(container); } function galleryMode(evt, work) { if (DEBUG) console.log(work); const modal = evt.composedPath().find((el) => el.id.includes("modal")); const scrollTop = modal.scrollTop; const dialog = evt .composedPath() .find((el) => el.className.includes("modal-dialog")); dialog.classList.add("modal-fullscreen"); const title = dialog.querySelector(".modal-header"); const body = dialog.querySelector(".modal-body"); const footer = dialog.querySelector(".modal-footer"); const gallery = modal.querySelector(".gallery"); const works = modal.id === "search_modal" ? searchResults : generatedResults; let index = works.findIndex((w) => w === work); const host = "https://i.pximg.net/img-master"; gallery.innerHTML = `
`; const imageContainer = gallery.querySelector(".images"); const all = gallery.querySelector("#gallery_all"); let pageIndex = 0, pageLoaded = false, masterUrl; function updateWork(work) { masterUrl = work.url.includes("limit_unknown") ? work.url : host + work.url .match(/\/img\/.*/)[0] .replace(/_(custom|square)1200/, "_master1200"); imageContainer.innerHTML = `
master
`; gallery.querySelector("#gallery_link").href = "/artworks/" + work.id; pageIndex = 0; pageLoaded = false; if (work["pageCount"] > 1) all.classList.remove("d-none"); else all.classList.add("d-none"); } updateWork(work); function loadAll() { const work = works[index]; [...Array(work["pageCount"] - 1).keys()].forEach((i) => { const p = i + 1; const url = masterUrl.replace("_p0_", `_p${p}_`); const div = document.createElement("div"); div.className = "text-center"; div.innerHTML = `page${p}`; imageContainer.appendChild(div); }); all.classList.add("d-none"); pageLoaded = true; } all.addEventListener("click", loadAll); gallery.querySelector("#gallery_exit").addEventListener("click", () => { dialog.classList.remove("modal-fullscreen"); gallery.classList.add("d-none"); title.classList.remove("d-none"); body.classList.remove("d-none"); footer.classList.remove("d-none"); modal.removeEventListener("keyup", fnKey); modal.scrollTo({ top: scrollTop, behavior: "smooth" }); }); function preFetch(work) { const url = work.url.includes("limit_unknown") ? work.url : host + work.url .match(/\/img\/.*/)[0] .replace(/_(custom|square)1200/, "_master1200"); const img = new Image(); img.src = url; } function prev() { if (works[index - 1]) { index--; updateWork(works[index]); if (works[index - 1]) preFetch(works[index - 1]); } } function next() { if (works[index + 1]) { index++; updateWork(works[index]); if (works[index + 1]) preFetch(works[index + 1]); } } function up() { if (!pageLoaded) return; const scrollY = gallery.scrollTop; pageIndex = Math.max(0, pageIndex - 1); const elemTop = imageContainer.children[pageIndex].getBoundingClientRect().top; gallery.scrollTo({ top: scrollY + elemTop }); } function down() { if (!pageLoaded) return loadAll(); const scrollY = gallery.scrollTop; pageIndex = Math.min(pageIndex + 1, works[index]["pageCount"] - 1); const elemTop = imageContainer.children[pageIndex].getBoundingClientRect().top; gallery.scrollTo({ top: scrollY + elemTop }); } function fnKey(evt) { if (evt.key === "ArrowLeft") prev(); else if (evt.key === "ArrowRight") next(); else if (evt.key === "ArrowUp") up(); else if (evt.key === "ArrowDown") down(); } gallery.querySelector("#gallery_left").addEventListener("click", prev); gallery.querySelector("#gallery_right").addEventListener("click", next); modal.addEventListener("keyup", fnKey); gallery.classList.remove("d-none"); title.classList.add("d-none"); body.classList.add("d-none"); footer.classList.add("d-none"); } let prevTag, prevRestriction, totalAvailable, generatorBookmarks, generatedResults, generatorDisplayLimit, generatorBatchNum; async function handleGenerate(evt) { evt.preventDefault(); const tag = document.querySelector("#generator_select_tag").value; const batchSize = Math.max( 0, parseInt(document.querySelector("#generator_form_num").value) || 100, ); const publicationType = document.querySelector( "#generator_form_publication", ).value; const restriction = document.querySelector( "#generator_form_restriction", ).value; console.log(tag, batchSize, publicationType, restriction); if ( !tag && !confirm( `加载全部收藏夹需要较长时间,是否确认操作?\nLoad the whole bookmark will take quite long time to process. Is this okay?`, ) ) return; const resultsDiv = document.querySelector("#generator_results"); while (resultsDiv.firstChild) { resultsDiv.removeChild(resultsDiv.firstChild); } const display = document.querySelector("#generator_display"); display.classList.remove("d-none"); const prompt = document.querySelector("#generator_save_tag_prompt"); if (prevTag !== tag || !generatorBookmarks?.length) { prevTag = tag; prevRestriction = null; generatorDisplayLimit = 12; generatorBookmarks = []; generatorBatchNum = -1; let offset = 0, total = 0; window.runFlag = true; prompt.classList.remove("d-none"); prompt.innerText = "正在加载收藏夹信息,点击停止可中断运行 / Loading bookmarks, Click stop to abort"; do { if (!window.runFlag) break; const bookmarks = await fetchBookmarks(uid, tag, offset, publicationType); if (!total) { total = bookmarks.total; prompt.innerText = `正在加载收藏夹信息(${total}),点击停止可中断运行 / Loading bookmarks (${total}), Click stop to abort`; } generatorBookmarks.push(...bookmarks["works"]); offset = generatorBookmarks.length; } while (offset < total); prompt.classList.add("d-none"); window.runFlag = false; shuffle(generatorBookmarks); } if (prevRestriction !== restriction) { prevRestriction = restriction; generatorBatchNum = -1; if (restriction !== "all") { generatorBookmarks.forEach((w) => { w.used = !!( (restriction === "sfw" && w["xRestrict"]) || (restriction === "nsfw" && !w["xRestrict"]) ); }); } totalAvailable = generatorBookmarks.filter((w) => !w.used).length; document.querySelector("#generator_spinner").classList.add("d-none"); console.log(generatorBookmarks); } if (!totalAvailable) { display.classList.add("d-none"); prompt.innerText = "图片加载失败 / Image Loading Failed"; return; } document.querySelector("#generator_form_buttons").classList.remove("d-none"); let availableBookmarks = generatorBookmarks.filter((w) => !w.used); if (generatorBookmarks.length && !availableBookmarks.length) { generatorBatchNum = -1; generatorBookmarks.forEach((w) => { if ( restriction === "all" || (restriction === "sfw" && !w["xRestrict"]) || (restriction === "nsfw" && w["xRestrict"]) ) w.used = false; }); availableBookmarks = generatorBookmarks.filter((w) => !w.used); } generatorBatchNum++; const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; generatedResults = availableBookmarks.slice(0, batchSize); generatedResults.forEach((w) => (w.used = true)); generatedResults .filter((_, i) => i < generatorDisplayLimit) .forEach((w) => displayWork(w, resultsDiv, textColor)); if (generatedResults.length > generatorDisplayLimit) { document.querySelector("#generator_more").classList.remove("d-none"); } document.querySelector("#generator_prompt").innerText = `当前批次 / Batch Num: ${generatorBatchNum} | 当前展示 / Display: ${generatedResults.length} / ${totalAvailable}`; } function shuffle(array) { let currentIndex = array.length, randomIndex; // while there remain elements to shuffle. while (currentIndex !== 0) { // pick a remaining element. randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; // swap it with the current element. [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex], ]; } return array; } const hold = false; function createModalElements() { // noinspection TypeScriptUMDGlobal const bootstrap_ = bootstrap; const bgColor = theme ? "bg-white" : "bg-dark"; const textColor = theme ? "text-lp-dark" : "text-lp-light"; addStyle(` .text-lp-dark { color: rgb(31, 31, 31); } .text-lp-light { color: rgb(245, 245, 245); } .label-button.text-lp-dark, .label-button.text-lp-light { color: rgb(133, 133, 133); } .label-button.text-lp-dark:hover { color: rgb(31, 31, 31); } .label-button.text-lp-light:hover { color: rgb(245, 245, 245); } .icon-invert { filter: invert(1); } .bg-dark button, .form-control, .form-control:focus, .form-select { color: inherit; background: inherit; } .modal::-webkit-scrollbar, .no-scroll::-webkit-scrollbar { display: none; /* Chrome */ } .modal, .no-scroll { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } .btn-close-empty { background: none; height: initial; width: initial; } .gallery-image { max-height: calc(100vh - 5rem); } .rate-icon { padding: 0px 6px; border-radius: 3px; color: rgb(245, 245, 245); background: rgb(255, 64, 96); font-weight: bold; font-size: 10px; line-height: 16px; user-select: none; height: 16px; } .page-icon { display: flex; -webkit-box-pack: center; justify-content: center; -webkit-box-align: center; align-items: center; flex: 0 0 auto; box-sizing: border-box; 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; font-size: 10px; line-height: 10px; } .page-icon:first-child { display: inline-flex; vertical-align: top; -webkit-box-align: center; align-items: center; height: 10px; } #gallery_link:hover { color: var(--bs-btn-hover-color); } `); const backdropConfig = getValue("backdropConfig", "false") === "true"; const svgPin = ` `; const svgUnpin = ` `; const svgClose = ` `; const defaultPinConfig = backdropConfig ? svgPin : svgUnpin; const showLatest = getValue("version") !== version ? "show" : ""; const lastBackupDictTime = getValue("lastBackupDict", ""); let lastBackupDict = ""; if (lastBackupDictTime) { if (lang.includes("zh")) { lastBackupDict = `
最后备份:${new Date( parseInt(lastBackupDictTime), ).toLocaleDateString("zh-CN")}
`; } else { lastBackupDict = `
Last Backup: ${new Date( parseInt(lastBackupDictTime), ).toLocaleDateString("en-US")}
`; } } // label const labelModal = document.createElement("div"); labelModal.className = "modal fade"; labelModal.id = "label_modal"; labelModal.tabIndex = -1; labelModal.innerHTML = ` `; // backdrop pin labelModal.setAttribute( "data-bs-backdrop", backdropConfig ? "static" : "true", ); const labelPinButton = labelModal.querySelector("#label_pin"); labelPinButton.addEventListener("click", () => { const ins = bootstrap_.Modal.getOrCreateInstance(labelModal); const backdrop = ins["_config"]["backdrop"] === "static"; if (backdrop) { ins["_config"]["backdrop"] = true; setValue("backdropConfig", "false"); labelPinButton.innerHTML = svgUnpin; } else { ins["_config"]["backdrop"] = "static"; setValue("backdropConfig", "true"); labelPinButton.innerHTML = svgPin; } }); // latest labelModal .querySelector("button#toggle_latest") .addEventListener("click", () => { setValue("version", version); }); // search const searchModal = document.createElement("div"); searchModal.className = "modal fade"; searchModal.id = "search_modal"; searchModal.tabIndex = -1; searchModal.innerHTML = ` `; // backdrop pin searchModal.setAttribute( "data-bs-backdrop", backdropConfig ? "static" : "true", ); const searchPinButton = searchModal.querySelector("#search_pin"); searchPinButton.addEventListener("click", () => { const ins = bootstrap_.Modal.getOrCreateInstance(searchModal); const backdrop = ins["_config"]["backdrop"] === "static"; if (backdrop) { ins["_config"]["backdrop"] = true; setValue("backdropConfig", "false"); searchPinButton.innerHTML = svgUnpin; } else { ins["_config"]["backdrop"] = "static"; setValue("backdropConfig", "true"); searchPinButton.innerHTML = svgPin; } }); const generatorModal = document.createElement("div"); generatorModal.className = "modal fade"; generatorModal.id = "generator_modal"; generatorModal.tabIndex = -1; generatorModal.innerHTML = ` `; generatorModal.setAttribute( "data-bs-backdrop", backdropConfig ? "static" : "true", ); const generatorPin = generatorModal.querySelector("#generator_pin"); generatorPin.addEventListener("click", () => { const ins = bootstrap_.Modal.getOrCreateInstance(generatorModal); const backdrop = ins["_config"]["backdrop"] === "static"; if (backdrop) { ins["_config"]["backdrop"] = true; setValue("backdropConfig", "false"); generatorPin.innerHTML = svgUnpin; } else { ins["_config"]["backdrop"] = "static"; setValue("backdropConfig", "true"); generatorPin.innerHTML = svgPin; } }); const generatorButtons = generatorModal .querySelector("#generator_form_buttons") .querySelectorAll("button"); generatorButtons[0].addEventListener("click", () => { generatedResults = null; generatorBookmarks.forEach((w) => (w.used = true)); generatorDisplayLimit = 12; document.querySelector("#generator_form_buttons").classList.add("d-none"); document.querySelector("#generator_display").classList.add("d-none"); document.querySelector("#generator_spinner").classList.add("d-none"); document.querySelector("#generator_more").classList.add("d-none"); const resultsDiv = document.querySelector("#generator_results"); while (resultsDiv.firstChild) { resultsDiv.removeChild(resultsDiv.firstChild); } }); generatorButtons[1].addEventListener("click", async () => { window.runFlag = true; const tag = document.querySelector("#generator_select_tag").value; const restriction = document.querySelector( "#generator_form_restriction", ).value; const availableBookmarks = generatorBookmarks.filter( (w) => restriction === "all" || (restriction === "sfw" && !w["xRestrict"]) || (restriction === "nsfw" && w["xRestrict"]), ); const batchSize = Math.max( 0, parseInt(document.querySelector("#generator_form_num").value) || 100, ); const batchNum = Math.ceil(availableBookmarks.length / batchSize); const prompt = document.querySelector("#generator_save_tag_prompt"); prompt.classList.remove("d-none"); for (let index of [...Array(batchNum).keys()]) { if (!window.runFlag) break; const addTag = `S_${index}_${tag}`.slice(0, 30); prompt.innerText = `正在保存至 ${addTag} / Saving to ${addTag}`; const ids = availableBookmarks .slice(index * batchSize, (index + 1) * batchSize) .map((w) => w["bookmarkData"]["id"]); // console.log(addTag, ids); await updateBookmarkTags(ids, [addTag]); } window.runFlag = false; prompt.classList.add("d-none"); }); generatorModal .querySelector("#generator_footer_button") .addEventListener("click", () => generatorButtons[2].click()); generatorModal .querySelector("#generator_footer_stop") .addEventListener("click", () => { window.runFlag = false; document .querySelector("#generator_save_tag_prompt") .classList.add("d-none"); }); generatorModal .querySelector("#generator_more") .addEventListener("click", (evt) => { const resultsDiv = document.querySelector("#generator_results"); const s = resultsDiv.childElementCount; const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; generatorDisplayLimit += 108; if (generatorDisplayLimit >= generatedResults.length) { evt.target.classList.add("d-none"); } generatedResults .filter((_, i) => i >= s && i < generatorDisplayLimit) .forEach((w) => displayWork(w, resultsDiv, textColor)); }); const tagSelectionDialog = getValue("tagSelectionDialog", "false") === "true"; /* eslint-disable indent */ const featureModal = document.createElement("div"); featureModal.className = "modal fade"; featureModal.id = "feature_modal"; featureModal.tabIndex = -1; featureModal.innerHTML = ` `; featureModal .querySelector("#feature_footer_stop_button") .addEventListener("click", () => (window.runFlag = false)); /* eslint-disable indent */ const featurePrompt = featureModal.querySelector("#feature_prompt"); const featureProgress = featureModal.querySelector("#feature_modal_progress"); const featureProgressBar = featureModal.querySelector( "#feature_modal_progress_bar", ); // tag related const featurePublicationType = featureModal.querySelector( "#feature_form_publication", ); const featureTag = featureModal.querySelector("#feature_select_tag"); const featureTagButtons = featureModal .querySelector("#feature_tag_buttons") .querySelectorAll("button"); async function featureFetchWorks(tag, publicationType, progressBar) { if (window.runFlag === false) return; window.runFlag = true; const tag_ = tag || featureTag.value; const publicationType_ = publicationType || featurePublicationType.value; if (tag_ === "" && cachedBookmarks[publicationType_]) return cachedBookmarks[publicationType_]; const progressBar_ = progressBar || featureProgressBar; featurePrompt.innerText = "正在获取收藏夹信息 / Fetching bookmark information"; featurePrompt.classList.remove("d-none"); if (!progressBar) featureProgress.classList.remove("d-none"); const totalWorks = await fetchAllBookmarksByTag( tag_, publicationType_, progressBar_, ); if (DEBUG) console.log(totalWorks); if (tag_ === "" && totalWorks) cachedBookmarks[publicationType_] = totalWorks; return totalWorks || []; } // toggle publication type featureTagButtons[0].addEventListener("click", () => featureFetchWorks().then(async (works) => { if (!works?.length) { return alert( `没有获取到收藏夹内容,操作中断,请检查选项下是否有作品\nFetching bookmark information failed. Abort operation. Please check the existence of works with the configuration`, ); } const tag = featureTag.value; const publicationType = featurePublicationType.value; const restrict = publicationType === "show" ? "private" : "public"; if ( !window.confirm(`标签【${tag || "所有作品"}】下所有【${ publicationType === "show" ? "公开" : "非公开" }】作品(共${works.length}项)将会被移动至【${ publicationType === "show" ? "非公开" : "公开" }】类型,是否确认操作? All works of tag ${tag || "All Works"} and type ${ publicationType === "show" ? "PUBLIC" : "PRIVATE" } (${ works.length } in total) will be set as ${restrict.toUpperCase()}. Is this Okay?`) ) return; const instance = bootstrap_.Modal.getOrCreateInstance(progressModal); instance.show(); await updateBookmarkRestrict( works.map((w) => w["bookmarkData"]["id"]), restrict, progressBar, ); setTimeout(() => { instance.hide(); if (window.runFlag && !hold) window.location.reload(); }, 1000); }), ); // delete tag featureTagButtons[1].addEventListener("click", async () => { const publicationType = featurePublicationType.value; const tag = featureTag.value; await deleteTag(tag, publicationType); }); // clear tag featureTagButtons[2].addEventListener("click", () => featureFetchWorks().then(clearBookmarkTags), ); // rename tag featureTagButtons[3].addEventListener("click", async () => { const tag = featureTag.value; let newName = featureModal.querySelector( "input#feature_new_tag_name", ).value; newName = newName.split(" ")[0].replace("(", "(").replace(")", ")"); if (!tag || tag === "未分類") return window.alert(`无效的标签名\nInvalid tag name`); if (!newName) return window.alert(`新标签名不可以为空!\nEmpty New Tag Name!`); const type = featurePublicationType.value === "show" ? "public" : "private"; if (userTagDict[type].find((e) => e.tag === newName)) if ( !window.confirm( `将会合并标签【${tag}】至【${newName}】,是否继续?\nWill merge tag ${tag} into ${newName}. Is this Okay?`, ) ) return; if ( !window.confirm(`是否将标签【${tag}】重命名为【${newName}】?\n与之关联的作品标签将被更新,该操作将同时影响公开和非公开收藏 Tag ${tag} will be renamed to ${newName}.\n All related works (both public and private) will be updated. Is this okay?`) ) return; const updateDict = featureModal.querySelector( "#feature_tag_update_dict", ).checked; if (updateDict && synonymDict[tag]) { const value = synonymDict[tag]; delete synonymDict[tag]; synonymDict[newName] = value; const newDict = {}; for (let key of sortByParody(Object.keys(synonymDict))) { newDict[key] = synonymDict[key]; } synonymDict = newDict; setValue("synonymDict", synonymDict); } const startTime = Date.now(); featurePrompt.innerText = "更新中 / Updating"; featurePrompt.classList.remove("d-none"); featureProgress.classList.remove("d-none"); const id = setInterval(() => { fetch( `https://www.pixiv.net/ajax/illusts/bookmarks/rename_tag_progress?lang=${lang}`, ) .then((resRaw) => resRaw.json()) .then((res) => { if (res.body["isInProgress"]) { const estimate = res.body["estimatedSeconds"]; const elapsed = (Date.now() - startTime) / 1000; const ratio = Math.min((elapsed / (elapsed + estimate)) * 100, 100).toFixed(2) + "%"; featureProgressBar.innerText = ratio; featureProgressBar.style.width = ratio; } else { clearInterval(id); featureProgressBar.innerText = "100%"; featureProgressBar.style.width = "100%"; featurePrompt.innerText = "更新成功 / Update Successfully"; setTimeout(() => { if (!hold) window.location.reload(); }, 1000); } }); }, 1000); await fetch("https://www.pixiv.net/ajax/illusts/bookmarks/rename_tag", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify({ newTagName: newName, oldTagName: tag }), method: "POST", }); }); // batch removing invalid bookmarks const batchRemoveButton = featureModal .querySelector("#feature_batch_remove_invalid_buttons") .querySelector("button"); batchRemoveButton.addEventListener("click", async () => { const display = featureModal.querySelector( "#feature_batch_remove_invalid_display", ); featureProgress.classList.remove("d-none"); const invalidShow = ( await featureFetchWorks("", "show", featureProgressBar) ).filter((w) => w.title === "-----"); if (DEBUG) console.log("invalidShow", invalidShow); const invalidHide = ( await featureFetchWorks("", "hide", featureProgressBar) ).filter((w) => w.title === "-----"); if (DEBUG) console.log("invalidHide", invalidHide); if (window.runFlag) { featurePrompt.classList.add("d-none"); featureProgress.classList.add("d-none"); if (invalidShow.length || invalidHide.length) { display.innerHTML = `
` + [...invalidShow, ...invalidHide] .map((w) => { const { id, associatedTags, restrict, xRestrict } = w; const l = lang.includes("zh") ? 0 : 1; const info = [ ["作品ID:", "ID: ", id], [ "用户标签:", "User Tags: ", (associatedTags || []).join(", "), ], [ "公开类型:", "Publication: ", restrict ? ["非公开", "hide"][l] : ["公开", "show"][l], ], ["限制分类:", "Restrict: ", xRestrict ? "R-18" : "SFW"], ]; return `
${info .map((i) => `${i[l] + i[2]}`) .join("
")}
`; }) .join("") + `
`; const buttonContainer = document.createElement("div"); buttonContainer.className = "d-flex mt-3"; const labelButton = document.createElement("button"); labelButton.className = "btn btn-outline-primary"; labelButton.innerText = "标记失效 / Label As Invalid"; labelButton.addEventListener("click", async (evt) => { evt.preventDefault(); if ( !window.confirm( `是否确认批量为失效作品添加"INVALID"标签\nInvalid works (deleted/private) will be labelled as INVALID. Is this okay?`, ) ) return; window.runFlag = true; const bookmarkIds = [...invalidShow, ...invalidHide] .filter((w) => !w.associatedTags.includes("INVALID")) .map((w) => w["bookmarkData"]["id"]); featureProgress.classList.remove("d-none"); featurePrompt.classList.remove("d-none"); featurePrompt.innerText = "添加标签中,请稍后 / Labeling invalid bookmarks"; await updateBookmarkTags( bookmarkIds, ["INVALID"], null, featureProgressBar, ); featurePrompt.innerText = "标记完成,即将刷新页面 / Updated. The page is going to reload."; setTimeout(() => { if (window.runFlag && !hold) window.location.reload(); }, 1000); }); const removeButton = document.createElement("button"); removeButton.className = "btn btn-outline-danger ms-auto"; removeButton.innerText = "确认删除 / Confirm Removing"; removeButton.addEventListener("click", async (evt) => { evt.preventDefault(); if ( !window.confirm( `是否确认批量删除失效作品\nInvalid works (deleted/private) will be removed. Is this okay?`, ) ) return; window.runFlag = true; const bookmarkIds = [...invalidShow, ...invalidHide].map( (w) => w["bookmarkData"]["id"], ); featureProgress.classList.remove("d-none"); featurePrompt.classList.remove("d-none"); featurePrompt.innerText = "删除中,请稍后 / Removing invalid bookmarks"; await removeBookmark(bookmarkIds, featureProgressBar); featurePrompt.innerText = "已删除,即将刷新页面 / Removed. The page is going to reload."; setTimeout(() => { if (window.runFlag && !hold) window.location.reload(); }, 1000); }); buttonContainer.appendChild(labelButton); buttonContainer.appendChild(removeButton); display.appendChild(buttonContainer); } else { display.innerText = "未检测到失效作品 / No invalid works detected"; } display.className = "mt-3"; } else { featurePrompt.innerText = "操作中断 / Operation Aborted"; } delete window.runFlag; }); // bookmarks related const featureBookmarkButtons = featureModal .querySelector("#feature_bookmark_buttons") .querySelectorAll("button"); // backup featureBookmarkButtons[0].addEventListener("click", async () => { featureProgress.classList.remove("d-none"); const show = await featureFetchWorks("", "show", featureProgressBar); const hide = await featureFetchWorks("", "hide", featureProgressBar); if (window.runFlag) { const bookmarks = { show, hide }; const a = document.createElement("a"); a.href = URL.createObjectURL( new Blob([JSON.stringify(bookmarks)], { type: "application/json" }), ); a.setAttribute( "download", `label_pixiv_bookmarks_backup_${new Date().toLocaleDateString()}.json`, ); a.click(); featurePrompt.innerText = "备份成功 / Backup successfully"; featureProgress.classList.add("d-none"); } else { featurePrompt.innerText = "操作中断 / Operation Aborted"; } delete window.runFlag; }); // lookup invalid const featureBookmarkDisplay = featureModal.querySelector( "#feature_bookmark_display", ); featureBookmarkButtons[1].addEventListener("click", async () => { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.addEventListener("change", async (evt) => { const reader = new FileReader(); reader.onload = async (evt) => { let json = {}; const invalidArray = []; async function run(type) { const col = await featureFetchWorks("", type, featureProgressBar); if (!window.runFlag) return; for (let work of col.filter((w) => w.title === "-----")) { const jsonWork = json[type].find( (w) => w.id.toString() === work.id.toString(), ); invalidArray.push(jsonWork || work); if (DEBUG) console.log(jsonWork); } } try { eval("json = " + evt.target.result.toString()); if (!json["show"]) return alert( "请检查是否加载了正确的收藏夹备份\nPlease check if the backup file is correct", ); if (DEBUG) console.log(json); featureProgress.classList.remove("d-none"); await run("show"); await run("hide"); if (invalidArray.length) { featureBookmarkDisplay.innerHTML = `
` + invalidArray .map((w) => { const { id, title, tags, userId, userName, alt, associatedTags, restrict, xRestrict, } = w; const l = lang.includes("zh") ? 0 : 1; const info = [ ["", "", alt], ["作品ID:", "ID: ", id], ["作品名称:", "Title: ", title], ["用户名称:", "User: ", userName + " - " + userId], ["作品标签:", "Tags: ", (tags || []).join(", ")], [ "用户标签:", "User Tags: ", (associatedTags || []).join(", "), ], [ "公开类型:", "Publication: ", restrict ? ["非公开", "hide"][l] : ["公开", "show"][l], ], ["限制分类:", "Restrict: ", xRestrict ? "R-18" : "SFW"], ]; return `
${info .map((i) => `${i[l] + i[2]}`) .join("
")}
`; }) .join("") + `
`; } else { featureBookmarkDisplay.innerText = "未检测到失效作品 / No invalid works detected"; } featureBookmarkDisplay.className = "mt-3"; } catch (err) { alert("无法加载收藏夹 / Fail to load bookmarks\n" + err); console.log(err); } finally { featurePrompt.classList.add("d-none"); featureProgress.classList.add("d-none"); delete window.runFlag; } }; reader.readAsText(evt.target.files[0]); }); input.click(); }); // import bookmarks const importBookmarkButtons = featureModal .querySelector("#feature_import_bookmark") .querySelectorAll("button"); importBookmarkButtons[0].addEventListener("click", async () => { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.addEventListener("change", (evt) => { const reader = new FileReader(); reader.onload = async (evt) => { let json = {}; try { eval("json = " + evt.target.result.toString()); if (!json["show"]) return alert( "请检查是否加载了正确的收藏夹备份\nPlease check if the backup file is correct", ); window.bookmarkImport = json; const selectTag = featureModal.querySelector( "#feature_import_bookmark_tag", ); while (selectTag.firstChild) { selectTag.removeChild(selectTag.firstChild); } const tagShow = json["show"] .map((w) => w.associatedTags || []) .reduce((a, b) => [...new Set(a.concat(b))], []); const tagHide = json["hide"] .map((w) => w.associatedTags || []) .reduce((a, b) => [...new Set(a.concat(b))], []); console.log("tagShow", tagShow); console.log("tagHide", tagHide); const tagAll = sortByParody([...new Set(tagShow.concat(tagHide))]); console.log("tagAll", tagAll); const optionAll = document.createElement("option"); optionAll.value = ""; optionAll.innerText = `所有收藏 / All Works (${json["show"].length}, ${json["hide"].length})`; const optionUncat = document.createElement("option"); optionUncat.value = "未分類"; const uncatS = json["show"].filter( (w) => !(w.associatedTags || []).length, ).length; const uncatH = json["hide"].filter( (w) => !(w.associatedTags || []).length, ).length; optionUncat.innerText = `未分类作品 / Uncategorized Works (${uncatS}, ${uncatH})`; selectTag.appendChild(optionAll); selectTag.appendChild(optionUncat); tagAll.forEach((t) => { const option = document.createElement("option"); option.value = t; const s = json["show"].filter((w) => (w.associatedTags || []).includes(t), ).length; const h = json["hide"].filter((w) => (w.associatedTags || []).includes(t), ).length; option.innerText = `${t} (${s}, ${h})`; selectTag.appendChild(option); }); featureModal .querySelector("#feature_import_bookmark_hide") .classList.remove("d-none"); } catch (err) { alert("无法加载收藏夹 / Fail to load bookmarks\n" + err); console.log(err); } }; reader.readAsText(evt.target.files[0]); }); input.click(); }); importBookmarkButtons[1].addEventListener("click", async () => { if (!window.bookmarkImport?.["show"]) return alert("加载收藏夹备份失败!\nFail to load backup bookmarks"); const json = window.bookmarkImport; const pub = featureModal.querySelector( "#feature_import_bookmark_publication", ).value; const tag = featureModal.querySelector( "#feature_import_bookmark_tag", ).value; const mode = featureModal.querySelector( "#feature_import_bookmark_mode", ).value; const importWorks = json[pub].filter((w) => { if (tag === "") return true; else if (tag === "未分類") return !w.associatedTags?.length; else return w.associatedTags?.includes(tag); }); importWorks.reverse(); featureProgress.classList.remove("d-none"); const existWorks = await featureFetchWorks(tag, pub, featureProgressBar); featurePrompt.classList.remove("d-none"); const errorList = []; window.runFlag = true; for (let i = 0; i < importWorks.length; i++) { const w = importWorks[i]; if (!window.runFlag) break; let { id, title, restrict, associatedTags, alt } = w; if (title === "-----") { errorList.push({ message: "The creator has limited who can view this content", ...w, }); continue; } if (!associatedTags) associatedTags = []; const ew = existWorks.find((ew) => ew.id === id); if (ew) { // note that when work does not have target tag but is in exist bookmarked works, skip will not take effect if (mode === "skip") continue; const diff = (ew.associatedTags || []).filter( (t) => !associatedTags.includes(t), ); associatedTags = associatedTags.filter( (t) => !(ew.associatedTags || []).includes(t), ); if (!associatedTags) continue; if (mode === "merge") await updateBookmarkTags([ew["bookmarkData"]["id"]], associatedTags); else if (mode === "override") await updateBookmarkTags( [ew["bookmarkData"]["id"]], associatedTags, diff, ); } else { const resRaw = await addBookmark(id, restrict, associatedTags); if (!resRaw.ok) { const res = await resRaw.json(); errorList.push({ ...res, ...w }); } } featurePrompt.innerText = alt; featureProgressBar.innerText = i + "/" + importWorks.length; const ratio = ((i / importWorks.length) * 100).toFixed(2); featureProgressBar.style.width = ratio + "%"; } if (!window.runFlag) { featurePrompt.innerText = "操作中断 / Operation Aborted"; } else { featurePrompt.innerText = "导入成功 / Import successfully"; featureProgress.classList.add("d-none"); } if (errorList.length) { console.log(errorList); featurePrompt.innerText = "部分导入成功 / Import Partially Successful"; featureBookmarkDisplay.classList.remove("d-none"); featureBookmarkDisplay.innerText = errorList .map((w) => { const { id, title, tags, userId, userName, alt, associatedTags, xRestrict, message, } = w; return `${alt}\ntitle: ${title}\nid: ${id}\nuser: ${userName} - ${userId}\ntags: ${( tags || [] ).join(", ")}\nuserTags: ${(associatedTags || []).join( ", ", )}\nrestrict: ${xRestrict ? "R-18" : "SFW"}\nmessage: ${message}`; }) .join("\n\n"); } }); // switch dialog const switchDialogButtons = featureModal .querySelector("#feature_switch_tag_dialog") .querySelectorAll("button"); // dialog style switchDialogButtons[0].addEventListener("click", () => { const tagSelectionDialog = getValue("tagSelectionDialog", "false"); if (tagSelectionDialog === "false") setValue("tagSelectionDialog", "true"); else setValue("tagSelectionDialog", "false"); window.location.reload(); }); // turbo mode switchDialogButtons[1].addEventListener("click", () => { if (turboMode) setValue("turboMode", "false"); else setValue("turboMode", "true"); window.location.reload(); }); // all tags selection modal const c_ = ALL_TAGS_CONTAINER.slice(1); const allTagsModal = document.createElement("div"); allTagsModal.className = "modal fade"; allTagsModal.id = "all_tags_modal"; allTagsModal.tabIndex = -1; allTagsModal.innerHTML = ` `; const parodyContainer = allTagsModal.querySelector(ALL_TAGS_CONTAINER); const characterContainer = [ ...allTagsModal.querySelectorAll(ALL_TAGS_CONTAINER), ][1]; userTags.forEach((tag) => { const d = document.createElement("div"); d.className = "sc-1jxp5wn-2 cdeTmC"; d.innerHTML = `
#${tag}
`; d.addEventListener("click", async () => { try { const c0 = document.querySelector(ALL_TAGS_MODAL_CONTAINER); const c1 = c0.lastElementChild; let lastScrollTop = -1; let targetDiv; let i = 0; while ( c1.scrollTop !== lastScrollTop && !targetDiv && i < userTags.length ) { targetDiv = [...c1.firstElementChild.children].find((el) => el.textContent.includes(tag), ); if (!targetDiv) { c1.scrollTop = parseInt( c1.firstElementChild.lastElementChild.style.top, ); if ("onscrollend" in window) await new Promise((r) => c1.addEventListener("scrollend", () => r(), { once: true }), ); else { let j = 0, lastText = c1.firstElementChild.lastElementChild.textContent; while ( j < 10 && lastText === c1.firstElementChild.lastElementChild.textContent ) { console.log("wait"); await new Promise((r) => setTimeout(r, 100)); j++; } } } i++; } if (targetDiv) { targetDiv.firstElementChild.click(); allTagsModal.querySelector("button.btn-close").click(); } } catch (err) { window.alert(`${err.name}: ${err.message}\n${err.stack}`); } }); if (tag.includes("(")) characterContainer.appendChild(d); else parodyContainer.appendChild(d); }); const progressModal = document.createElement("div"); progressModal.className = "modal fade"; progressModal.id = "progress_modal"; progressModal.setAttribute("data-bs-backdrop", "static"); progressModal.tabIndex = -1; progressModal.innerHTML = ` `; const progressBar = progressModal.querySelector( "#progress_modal_progress_bar", ); const body = document.querySelector("body"); body.appendChild(labelModal); body.appendChild(searchModal); body.appendChild(generatorModal); body.appendChild(featureModal); body.appendChild(progressModal); body.appendChild(allTagsModal); } async function fetchUserTags() { const tagsRaw = await fetch( `/ajax/user/${uid}/illusts/bookmark/tags?lang=${lang}`, ); const tagsObj = await tagsRaw.json(); if (tagsObj.error === true) return alert( `获取tags失败 Fail to fetch user tags` + "\n" + decodeURI(tagsObj.message), ); userTagDict = tagsObj.body; const userTagsSet = new Set(); const addTag2Set = (tag) => { try { userTagsSet.add(decodeURI(tag)); } catch (err) { userTagsSet.add(tag); if (err.message !== "URI malformed") { console.log("[Label Pixiv] Error!"); console.log(err.name, err.message); console.log(err.stack); window.alert(`加载标签%{tag}时出现错误,请按F12打开控制台截图错误信息并反馈至GitHub。 Error loading tag ${tag}. Please press F12 and take a screenshot of the error message in the console and report it to GitHub.`); } } }; for (let obj of userTagDict.public) { addTag2Set(obj.tag); } for (let obj of userTagDict["private"]) { addTag2Set(obj.tag); } userTagsSet.delete("未分類"); return sortByParody(Array.from(userTagsSet)); } async function fetchTokenPolyfill() { // get token const userRaw = await fetch( "/bookmark_add.php?type=illust&illust_id=83540927", ); if (!userRaw.ok) { console.log(`获取身份信息失败 Fail to fetch user information`); throw new Error(); } const userRes = await userRaw.text(); const tokenPos = userRes.indexOf("pixiv.context.token"); const tokenEnd = userRes.indexOf(";", tokenPos); return userRes.slice(tokenPos, tokenEnd).split('"')[1]; } async function updateWorkInfo(bookmarkTags) { const el = await waitForDom(WORK_SECTION); let workInfo = {}; for (let i = 0; i < 100; i++) { workInfo = Object.values(el)[0]["memoizedProps"]["children"][2]["props"]; if (Object.keys(workInfo).length) break; else await delay(200); } if (bookmarkTags) { [...el.querySelectorAll("li")].forEach((li, i) => { workInfo["works"][i].associatedTags = Object.values(li)[0].child.child["memoizedProps"].associatedTags; }); } const page = window.location.search.match(/p=(\d+)/)?.[1] || 1; workInfo.page = parseInt(page); return workInfo; } async function initializeVariables() { async function polyfill() { try { const dataLayer = unsafeWindow_["dataLayer"][0]; uid = dataLayer["user_id"]; lang = dataLayer["lang"]; token = await fetchTokenPolyfill(); pageInfo.userId = window.location.href.match(/users\/(\d+)/)?.[1]; pageInfo.client = { userId: uid, lang, token }; } catch (err) { console.log(err); console.log("[Label Bookmarks] Initializing Failed"); } } try { if (DEBUG) console.log("[Label Bookmarks] Initializing Variables"); pageInfo = Object.values(document.querySelector(BANNER))[0]["return"][ "return" ]["memoizedProps"]; if (DEBUG) console.log("[Label Bookmarks] Page Info", pageInfo); uid = pageInfo["client"]["userId"]; token = pageInfo["client"]["token"]; lang = pageInfo["client"]["lang"]; if (!uid || !token || !lang) await polyfill(); } catch (err) { console.log(err); await polyfill(); } userTags = await fetchUserTags(); // workType = Object.values(document.querySelector(".sc-1x9383j-0"))[0].child["memoizedProps"]["workType"]; // switch between default and dark theme const themeDiv = document.querySelector(THEME_CONTAINER); theme = themeDiv.getAttribute("data-theme") === "light"; new MutationObserver(() => { theme = themeDiv.getAttribute("data-theme") === "light"; const prevBgColor = theme ? "bg-dark" : "bg-white"; const bgColor = theme ? "bg-white" : "bg-dark"; const prevTextColor = theme ? "text-lp-light" : "text-lp-dark"; const textColor = theme ? "text-lp-dark" : "text-lp-light"; [...document.querySelectorAll(".bg-dark, .bg-white")].forEach((el) => { el.classList.replace(prevBgColor, bgColor); }); [...document.querySelectorAll(".text-lp-dark, .text-lp-light")].forEach( (el) => { el.classList.replace(prevTextColor, textColor); }, ); const prevClearTag = theme ? "dydUg" : "jbzOgz"; const clearTag = theme ? "jbzOgz" : "dydUg"; const clearTagsButton = document.querySelector("#clear_tags_button"); if (clearTagsButton) clearTagsButton.children[0].classList.replace(prevClearTag, clearTag); }).observe(themeDiv, { attributes: true }); synonymDict = getValue("synonymDict", {}); if (Object.keys(synonymDict).length) { // remove empty values on load, which could be caused by unexpected interruption for (let key of Object.keys(synonymDict)) { if (!synonymDict[key]) delete synonymDict[key]; } setValue("synonymDict", synonymDict); } if (DEBUG) console.log("[Label Bookmarks] Initialized"); } const maxRetries = 100; async function waitForDom(selector, container) { let dom; for (let i = 0; i < maxRetries; i++) { dom = (container || document).querySelector(selector); if (dom) return dom; await delay(200); } throw new ReferenceError( `[Label Bookmarks] Dom element ${selector} not loaded in given time`, ); } async function injectElements() { if (DEBUG) console.log("[Label Bookmarks] Start Injecting"); const textColor = theme ? "text-lp-dark" : "text-lp-light"; const pageBody = document.querySelector(PAGE_BODY); const root = document.querySelector("nav"); if (!root) console.log("[Label Bookmarks] Navbar Not Found"); root.classList.add("d-flex"); const buttonContainer = document.createElement("span"); buttonContainer.className = "flex-grow-1 justify-content-end d-flex"; buttonContainer.id = "label_bookmarks_buttons"; const gClass = generator ? "" : "d-none"; const fClass = feature ? "" : "d-none"; buttonContainer.innerHTML = `