// ==UserScript==
// @name Pixiv收藏夹自动标签
// @name:en Label Pixiv Bookmarks
// @namespace http://tampermonkey.net/
// @version 5.2
// @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 none
// ==/UserScript==
let uid,
token,
lang,
userTags,
userTagDict,
synonymDict,
pageInfo,
theme,
generator,
feature;
// 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;
Array.prototype.toUpperCase = function () {
return this.map((i) => i.toUpperCase());
};
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 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; }";
document.head.appendChild(style);
}
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}`
);
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 = [];
do {
if (!window.runFlag) break;
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 = offset + "/" + total;
const ratio = ((offset / total) * max).toFixed(2);
progressBar.style.width = ratio + "%";
}
} while (offset < total);
return totalWorks;
}
async function updateBookmarkTags(bookmarkIds, addTags, removeTags) {
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 run(ids) {
if (addTags && addTags.length) {
await fetch("/ajax/illusts/bookmarks/add_tags", {
headers: {
accept: "application/json",
"content-type": "application/json; charset=utf-8",
"x-csrf-token": token,
},
body: JSON.stringify({ tags: addTags, bookmarkIds: ids }),
method: "POST",
});
await delay(500);
}
if (removeTags && removeTags.length) {
await fetch("/ajax/illusts/bookmarks/remove_tags", {
headers: {
accept: "application/json",
"content-type": "application/json; charset=utf-8",
"x-csrf-token": token,
},
body: JSON.stringify({ removeTags, 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);
}
}
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).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["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");
}
const DEBUG = true;
async function handleLabel(evt) {
evt.preventDefault();
const addFirst = document.querySelector("#label_add_first").value;
const tagToQuery = document.querySelector("#label_tag_query").value;
const publicationType = (await updateWorkInfo())["restrict"]
? "hide"
: "show";
const labelR18 = document.querySelector("#label_r18").value;
const exclusion = document
.querySelector("#label_exclusion")
.value.split(/[\s\n]/)
.filter((t) => t);
console.log("Label Configuration:");
console.log(
`addFirst: ${addFirst === "true"}; tagToQuery: ${tagToQuery}; labelR18: ${
labelR18 === "true"
}; publicationType: ${publicationType}; exclusion: ${exclusion.join(",")}`
);
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 "successfully" updated
// 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 = userTags.filter((userTag) => {
// if work tags includes this user tag
if (
workTags.toUpperCase().includes(userTag.toUpperCase()) || // full tag
workTags
.toUpperCase()
.includes(userTag.toUpperCase().split("(")[0]) || // char name
workTags.some(
(workTag) =>
(workTag.toUpperCase().split("(")[1] || "").split(")")[0] ===
userTag
) // parody name
)
return true;
// if work tags match a user alias (exact match)
return (
synonymDict[userTag] &&
synonymDict[userTag].find(
(alias) =>
workTags.toUpperCase().includes(alias.toUpperCase()) ||
workTags
.toUpperCase()
.includes(alias.toUpperCase().split("(")[0]) ||
workTags.some(
(workTag) =>
(workTag.toUpperCase().split("(")[1] || "").split(")")[0] ===
alias
) // parody name
)
);
});
// if workTags match some alias, add it to the intersection (exact match, with or without parody name)
intersection = intersection.concat(
workTags
.map((workTag) => {
for (let aliasName of Object.keys(synonymDict)) {
if (
aliasName.toUpperCase() === workTag.toUpperCase() ||
synonymDict[aliasName]
.toUpperCase()
.includes(workTag.toUpperCase()) ||
synonymDict[aliasName]
.toUpperCase()
.includes(workTag.split("(")[0].toUpperCase())
)
return aliasName;
}
})
.filter((i) => i)
);
if (work["xRestrict"] && labelR18 === "true") intersection.push("R-18");
// 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.find((tag) => !exclusion.includes(tag));
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
const workTags = work["tags"];
const ifInclude = (keyword) => {
// especially, R-18 tag is labelled in work
if (["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) =>
el[0].toUpperCase() === keyword.toUpperCase() || // input match char name
el[1].toUpperCase() === keyword.toUpperCase() || // input match full name
synonymDict[el[1]]
.toUpperCase()
.includes(keyword.toUpperCase()) || // input match any alias
(matchPattern === "fuzzy" &&
(el[1].toUpperCase().includes(keyword.toUpperCase()) ||
synonymDict[el[1]]
.toUpperCase()
.some((alias) => alias.includes(keyword.toUpperCase()))))
);
const keywordArray = [keyword];
if (el) {
keywordArray.push(...el);
keywordArray.push(...synonymDict[el[1]]);
}
if (
keywordArray.some(
(kw) =>
(config[0] &&
work.title.toUpperCase().includes(kw.toUpperCase())) ||
(config[1] &&
work["userName"].toUpperCase().includes(kw.toUpperCase())) ||
(config[2] &&
workTags.toUpperCase().includes(kw.toUpperCase())) ||
(config[3] &&
bookmarkTags.toUpperCase().includes(kw.toUpperCase()))
)
)
return true;
if (matchPattern === "exact") return false;
return keywordArray.some(
(kw) =>
(config[2] &&
workTags
.toUpperCase()
.some((tag) => tag.includes(kw.toUpperCase()))) ||
(config[3] &&
bookmarkTags
.toUpperCase()
.some((tag) => tag.includes(kw.toUpperCase())))
);
};
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 = `
${tagsString}
`;
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) {
const modal = evt.path.find((el) => el.id.includes("modal"));
const scrollTop = modal.scrollTop;
const dialog = evt.path.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 = `
`;
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 = `
`;
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;
}
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);
}
.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;
// 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;
}
});
// 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 featureModal = document.createElement("div");
featureModal.className = "modal fade";
featureModal.id = "feature_modal";
featureModal.tabIndex = -1;
featureModal.innerHTML = `
`;
const featurePrompt = featureModal.querySelector("#feature_prompt");
const featurePublicationType = featureModal.querySelector(
"#feature_form_publication"
);
const featureTag = featureModal.querySelector("#feature_select_tag");
const featureButtons = featureModal
.querySelector("#feature_buttons")
.querySelectorAll("button");
featureButtons[0].addEventListener("click", async () => {
const publicationType = featurePublicationType.value;
const tag = featureTag.value;
await deleteTag(tag, publicationType);
});
async function featureFetchWorks() {
const tag = featureTag.value;
const publicationType = featurePublicationType.value;
window.runFlag = true;
featurePrompt.innerText =
"正在获取收藏夹信息 / Fetching bookmark information";
featurePrompt.classList.remove("d-none");
const totalWorks = await fetchAllBookmarksByTag(tag, publicationType);
featurePrompt.classList.add("d-none");
console.log(totalWorks);
return totalWorks;
}
featureButtons[1].addEventListener("click", () =>
featureFetchWorks().then(clearBookmarkTags)
);
featureButtons[2].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 && !DEBUG) window.location.reload();
}, 1000);
})
);
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);
}
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();
for (let obj of userTagDict.public) {
userTagsSet.add(decodeURI(obj.tag));
}
for (let obj of userTagDict["private"]) {
userTagsSet.add(decodeURI(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() {
const el = await waitForDom("section.sc-jgyytr-0.buukZm");
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 (DEBUG) console.log(workInfo);
return workInfo;
}
async function initializeVariables() {
try {
pageInfo = Object.values(document.querySelector(".sc-x1dm5r-0"))[0][
"return"
]["return"]["memoizedProps"];
if (DEBUG) console.log(pageInfo);
uid = pageInfo["client"]["userId"];
token = pageInfo["client"]["token"];
lang = pageInfo["client"]["lang"];
} catch (err) {
console.log(err);
const dataLayer = unsafeWindow_["dataLayer"][0];
uid = dataLayer["user_id"];
lang = dataLayer["lang"];
token = await fetchTokenPolyfill();
}
userTags = await fetchUserTags();
// switch between default and dark theme
const themeDiv = document.querySelector(".charcoal-token");
theme = themeDiv.getAttribute("data-theme") === "default";
new MutationObserver(() => {
theme = themeDiv.getAttribute("data-theme") === "default";
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) setValue("synonymDict", synonymDict);
}
const maxRetries = 100;
async function waitForDom(selector) {
let dom;
for (let i = 0; i < maxRetries; i++) {
dom = document.querySelector(selector);
if (dom) return dom;
await delay(500);
}
throw new ReferenceError(
`[Label Bookmarks] Dom element ${selector} not loaded in given time`
);
}
async function injectElements() {
const textColor = theme ? "text-lp-dark" : "text-lp-light";
const pageBody = document.querySelector(".sc-12rgki1-0.jMEnyM");
const root = document.querySelector("nav");
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 = `
`;
const clearTagsThemeClass = theme ? "jbzOgz" : "dydUg";
const clearTagsText = lang.includes("zh") ? "清除标签" : "Clear Tags";
const clearTagsButton = document.createElement("div");
clearTagsButton.id = "clear_tags_button";
clearTagsButton.className = "sc-1ij5ui8-0 QihHO sc-13ywrd6-7 tPCje";
clearTagsButton.setAttribute("aria-disabled", "true");
clearTagsButton.setAttribute("role", "button");
clearTagsButton.innerHTML = ``;
clearTagsButton.addEventListener("click", handleClearBookmarkTags);
const removeTagButton = document.createElement("div");
removeTagButton.id = "remove_tag_button";
removeTagButton.style.display = "none";
removeTagButton.style.marginRight = "16px";
removeTagButton.style.marginBottom = "12px";
removeTagButton.style.color = "rgba(0, 0, 0, 0.64)";
removeTagButton.style.cursor = "pointer";
removeTagButton.innerHTML = `
`;
removeTagButton.addEventListener("click", handleDeleteTag);
async function injection(_, injectionObserver) {
if (_) console.log(_);
if (pageInfo["userId"] !== uid) return;
if (injectionObserver) injectionObserver.disconnect();
console.log("[Label Bookmarks] Try Injecting");
const workInfo = await updateWorkInfo();
if (!workInfo["works"]) {
if (injectionObserver)
injectionObserver.observe(pageBody, { childList: true });
return console.log("[Label Bookmarks] Abort Injection");
}
if (DEBUG) {
console.log("user tags", userTags, userTagDict);
console.log("dict:", synonymDict);
}
root.appendChild(buttonContainer);
setElementProperties();
setSynonymEventListener();
setAdvancedSearch();
const editButtonContainer = await waitForDom(".sc-1dg0za1-6.fElfQf");
if (editButtonContainer) {
editButtonContainer.style.justifyContent = "initial";
editButtonContainer.firstElementChild.style.marginRight = "auto";
editButtonContainer.insertBefore(
removeTagButton,
editButtonContainer.lastChild
);
let removeBookmarkContainerObserver;
const editButtonObserver = new MutationObserver(
async (MutationRecord) => {
if (DEBUG) console.log("on edit mode change");
const { tag } = await updateWorkInfo();
if (!MutationRecord[0].addedNodes.length) {
// open edit mode
const removeBookmarkContainer = document.querySelector(
"div.sc-13ywrd6-4.cXBjgZ"
);
removeBookmarkContainer.appendChild(clearTagsButton);
removeBookmarkContainerObserver = new MutationObserver(() => {
const value =
removeBookmarkContainer.children[2].getAttribute(
"aria-disabled"
);
clearTagsButton.setAttribute("aria-disabled", value);
clearTagsButton.children[0].setAttribute("aria-disabled", value);
});
removeBookmarkContainerObserver.observe(
removeBookmarkContainer.children[2],
{ attributes: true }
);
if (tag && tag !== "未分類") {
document.querySelector("#remove_tag_prompt").innerText =
lang.includes("zh") ? "删除标签 " + tag : "Delete Tag " + tag;
removeTagButton.style.display = "flex";
}
} else {
// exit edit mode
removeTagButton.style.display = "none";
if (removeBookmarkContainerObserver)
removeBookmarkContainerObserver.disconnect();
clearTagsButton.setAttribute("aria-disabled", "true");
clearTagsButton.children[0].setAttribute("aria-disabled", "true");
}
}
);
editButtonObserver.observe(editButtonContainer, {
childList: true,
});
}
let lastTag = workInfo.tag;
const tagsContainer = await waitForDom(".sc-1jxp5wn-1");
new MutationObserver(async () => {
const workInfo = await updateWorkInfo();
if (lastTag !== workInfo.tag) {
lastTag = workInfo.tag;
const removeTagButton = document.querySelector("#remove_tag_button");
if (!workInfo.tag || workInfo.tag === "未分類") {
if (removeTagButton && removeTagButton.style.display === "flex") {
removeTagButton.style.display = "none";
}
} else {
if (
workInfo["editMode"] &&
removeTagButton &&
removeTagButton.style.display === "none"
) {
removeTagButton.style.display = "flex";
}
const removeTagButtonPrompt =
document.querySelector("#remove_tag_prompt");
if (removeTagButtonPrompt)
removeTagButtonPrompt.innerText = lang.includes("zh")
? "删除标签 " + workInfo.tag
: "Delete Tag " + workInfo.tag;
}
}
if (DEBUG) console.log("Current Tag:", workInfo.tag);
}).observe(tagsContainer, {
subtree: true,
childList: true,
});
const toUncategorized = document.querySelector(".sc-1mr081w-0");
if (toUncategorized) {
toUncategorized.style.cursor = "pointer";
toUncategorized.onclick = () =>
(window.location.href = `https://www.pixiv.net/users/${uid}/bookmarks/artworks/未分類`);
}
console.log("[Label Bookmarks] Injected");
window.addEventListener(
"popstate",
() => {
if (window.location.href.match(/\/users\/\d+\/bookmarks\/artworks/))
delay(1000)
.then(() => waitForDom(".sc-1jxp5wn-1"))
.then(createModalElements)
.then(injectElements);
},
{ once: true }
);
return true;
}
if (!(await injection())) {
const pageObserver = new MutationObserver(injection);
pageObserver.observe(pageBody, { childList: true });
}
}
let timeout = null,
prevKeyword = null;
async function updateSuggestion(
evt,
suggestionEl,
searchDict,
handleClickCandidateButton
) {
clearTimeout(timeout);
const keywordsArray = evt.target.value.split(" ");
const keyword = keywordsArray[keywordsArray.length - 1]
.replace(/^!/, "")
.replace(/^!/, "");
if (
window.runFlag ||
!keyword ||
!keyword.length ||
keyword === " " ||
keyword === prevKeyword
)
return;
timeout = setTimeout(async () => {
suggestionEl.parentElement.style.display = "none";
prevKeyword = keyword;
setTimeout(() => (prevKeyword = null), 3000);
while (suggestionEl.firstElementChild) {
suggestionEl.removeChild(suggestionEl.firstElementChild);
}
if (keyword.toUpperCase() === "R-18") return;
let candidates = [];
if (searchDict) {
let dictKeys = Object.keys(synonymDict).filter((el) =>
el.toUpperCase().includes(keyword.toUpperCase())
);
if (dictKeys.length)
candidates = dictKeys.map((dictKey) => ({
tag_name: synonymDict[dictKey][0],
tag_translation: dictKey,
}));
if (!candidates.length) {
dictKeys = Object.keys(synonymDict).filter((key) =>
synonymDict[key]
.toUpperCase()
.map((i) => i.split("(")[0])
.includes(keyword.split("(")[0].toUpperCase())
);
if (dictKeys.length)
candidates = dictKeys.map((dictKey) => ({
tag_name: synonymDict[dictKey][0],
tag_translation: dictKey,
}));
}
}
if (!candidates.length) {
const resRaw = await fetch(
`/rpc/cps.php?keyword=${encodeURI(keyword)}&lang=${lang}`
);
const res = await resRaw.json();
candidates = res["candidates"].filter((i) => i["tag_name"] !== keyword);
}
if (candidates.length) {
for (let candidate of candidates.filter((_, i) => i < 5)) {
const candidateButton = document.createElement("button");
candidateButton.type = "button";
candidateButton.className = "btn p-0 mb-1 d-block";
candidateButton.innerHTML = `${
candidate["tag_translation"] || "🈳"
} - ${candidate["tag_name"]}`;
handleClickCandidateButton(candidate, candidateButton);
suggestionEl.appendChild(candidateButton);
}
} else {
const noCandidate = document.createElement("div");
noCandidate.innerText = "无备选 / No Suggestion";
suggestionEl.appendChild(noCandidate);
}
suggestionEl.parentElement.style.display = "block";
}, 500);
}
function setElementProperties() {
// label buttons
const labelButton = document.querySelector("#label_modal_button");
const searchButton = document.querySelector("#search_modal_button");
const generatorButton = document.querySelector("#generator_modal_button");
const featureButton = document.querySelector("#feature_modal_button");
if (lang.includes("zh")) {
labelButton.innerText = "添加标签";
searchButton.innerText = "搜索图片";
generatorButton.innerText = "随机图片";
featureButton.innerText = "其他功能";
} else {
labelButton.innerText = "Label";
searchButton.innerText = "Search";
generatorButton.innerText = "Shuffle";
featureButton.innerText = "Function";
}
addStyle(
`.label-button {
padding: 0 24px;
background: transparent;
font-size: 16px;
font-weight: 700;
border-top: 4px solid rgba(0, 150, 250, 0);
border-bottom: none;
border-left: none;
border-right: none;
line-height: 24px;
background: transparent;
transition: color 0.4s ease 0s, border 0.4s ease 0s;
}
.label-button:hover {
border-top: 4px solid rgb(0, 150, 250);
}`
);
// append user tags options
const customSelects = [...document.querySelectorAll(".select-custom-tags")];
customSelects.forEach((el) => {
const uncat = el.querySelector("option[value='未分類']");
if (uncat) {
const t = "未分類";
const pb = userTagDict.public.find((e) => e.tag === t)?.["cnt"] || 0;
const pr = userTagDict["private"].find((e) => e.tag === t)?.["cnt"] || 0;
uncat.innerText = `未分类作品 / Uncategorized Works (${pb}, ${pr})`;
}
userTags.forEach((tag) => {
const option = document.createElement("option");
option.value = tag;
const pb = userTagDict.public.find((e) => e.tag === tag)?.["cnt"] || 0;
const pr =
userTagDict["private"].find((e) => e.tag === tag)?.["cnt"] || 0;
option.innerText = tag + ` (${pb}, ${pr})`;
el.appendChild(option);
});
});
// label bookmark form
const labelForm = document.querySelector("#label_form");
labelForm.onsubmit = handleLabel;
const footerLabel = document.querySelector("#footer_label_button");
const startLabel = document.querySelector("#start_label_button");
footerLabel.onclick = () => startLabel.click();
const stopButton = document.querySelector("#footer_stop_button");
stopButton.onclick = () => (window.runFlag = false);
// default value
const addFirst = document.querySelector("#label_add_first");
addFirst.value = getValue("addFirst", "false");
addFirst.onchange = () => setValue("addFirst", addFirst.value);
const tagToQuery = document.querySelector("#label_tag_query");
const tag = getValue("tagToQuery", "未分類");
if (userTags.includes(tag)) tagToQuery.value = tag;
// in case that tag has been deleted
else tagToQuery.value = "未分類";
tagToQuery.onchange = () => setValue("tagToQuery", tagToQuery.value);
const labelR18 = document.querySelector("#label_r18");
labelR18.value = getValue("labelR18", "true");
labelR18.onchange = () => setValue("labelR18", labelR18.value);
const exclusion = document.querySelector("#label_exclusion");
exclusion.value = getValue("exclusion", "");
exclusion.onchange = () => setValue("exclusion", exclusion.value);
// search bookmark form
const searchForm = document.querySelector("#search_form");
searchForm.onsubmit = handleSearch;
const searchMore = document.querySelector("#search_more");
const footerSearch = document.querySelector("#footer_search_button");
footerSearch.onclick = () => searchMore.click();
// generator form
const generatorForm = document.querySelector("#generator_form");
generatorForm.onsubmit = handleGenerate;
document
.querySelector("#stop_remove_tag_button")
.addEventListener("click", () => (window.runFlag = false));
}
function setSynonymEventListener() {
const targetTag = document.querySelector("#target_tag");
const alias = document.querySelector("#tag_alias");
const preview = document.querySelector("#synonym_preview");
const buttons = document
.querySelector("#synonym_buttons")
.querySelectorAll("button");
const lineHeight = parseInt(getComputedStyle(preview).lineHeight);
const labelSuggestion = document.querySelector("#label_suggestion");
targetTag.addEventListener("keyup", (evt) => {
updateSuggestion(
evt,
labelSuggestion,
false,
(candidate, candidateButton) =>
candidateButton.addEventListener("click", () => {
alias.value = alias.value + " " + candidate["tag_name"];
})
).catch(console.log);
});
targetTag.addEventListener("keyup", (evt) => {
// scroll to modified entry
const lines = preview.innerText.split("\n");
let lineNum = lines.findIndex((line) => line.includes(evt.target.value));
if (lineNum < 0) return;
if (lines[lineNum].startsWith("\t")) lineNum--;
if (lineHeight * lineNum) preview.scrollTop = lineHeight * lineNum;
});
targetTag.addEventListener("blur", (evt) => {
if (Object.keys(synonymDict).includes(evt.target.value)) {
const value = synonymDict[evt.target.value];
if (value.length > 4) alias.value = value.join("\n");
else alias.value = value.join(" ");
}
});
// update preview
function updatePreview(synonymDict) {
let synonymString = "";
for (let key of Object.keys(synonymDict)) {
let value = synonymDict[key];
if (value.length > 4) value = value.join("\n\t");
else value = value.join(" ");
synonymString += key + "\n\t" + value + "\n\n";
}
preview.innerText = synonymString;
}
updatePreview(synonymDict);
// on json file load
document
.querySelector("#synonym_dict_input")
.addEventListener("change", (evt) => {
const reader = new FileReader();
reader.onload = (evt) => {
try {
let json = {};
eval("json = " + evt.target.result.toString());
if (Array.isArray(json)) synonymDict = json[0];
else synonymDict = json;
setValue("synonymDict", synonymDict);
updatePreview(synonymDict);
} catch (err) {
alert("无法加载词典 / Fail to load dictionary\n" + err);
}
};
reader.readAsText(evt.target.files[0]);
});
// export dict
buttons[0].addEventListener("click", (evt) => {
evt.preventDefault();
const a = document.createElement("a");
a.href = URL.createObjectURL(
new Blob([JSON.stringify(synonymDict)], {
type: "application/json",
})
);
a.setAttribute(
"download",
`label_pixiv_bookmarks_synonym_dict_${new Date().toLocaleDateString()}.json`
);
a.click();
});
// load alias
buttons[1].addEventListener("click", (evt) => {
evt.preventDefault();
labelSuggestion.parentElement.style.display = "none";
const targetValue = targetTag.value;
for (let key of Object.keys(synonymDict)) {
if (key === targetValue) {
alias.value = synonymDict[key].join(" ");
updatePreview(synonymDict);
}
}
});
// update the alias array
buttons[2].addEventListener("click", (evt) => {
evt.preventDefault();
labelSuggestion.parentElement.style.display = "none";
const targetValue = targetTag.value
.split(" ")[0]
.replace("(", "(")
.replace(")", ")");
// navigator.clipboard.writeText(targetValue).catch(console.log);
const aliasValue = alias.value;
if (aliasValue === "") {
// delete
if (
synonymDict[targetValue] &&
window.confirm(
`将会删除 ${targetValue},请确认\nWill remove ${targetValue}. Is this okay?`
)
) {
delete synonymDict[targetValue];
}
} else {
const value = aliasValue
.split(/[\s\r\n]/)
.filter((i) => i)
.map((i) => i.trim());
if (synonymDict[targetValue]) {
synonymDict[targetValue] = value; // update
} else {
synonymDict[targetValue] = value; // add and sort
const newDict = {};
for (let key of sortByParody(Object.keys(synonymDict))) {
newDict[key] = synonymDict[key];
}
synonymDict = newDict;
}
}
targetTag.value = "";
alias.value = "";
setValue("synonymDict", synonymDict);
updatePreview(synonymDict);
});
// filter
document
.querySelector("input#synonym_filter")
.addEventListener("input", (evt) => {
const filter = evt.target.value;
if (filter.length) {
if (filter === " ") return;
const filteredKeys = Object.keys(synonymDict).filter(
(key) =>
key.toUpperCase().includes(filter.toUpperCase()) ||
synonymDict[key]
.join(",")
.toUpperCase()
.includes(filter.toUpperCase())
);
const newDict = {};
for (let key of filteredKeys) {
newDict[key] = synonymDict[key];
}
updatePreview(newDict);
} else {
updatePreview(synonymDict);
}
});
// clear
document
.querySelector("button#clear_synonym_filter")
.addEventListener("click", () => {
document.querySelector("input#synonym_filter").value = "";
updatePreview(synonymDict);
});
// restore
document
.querySelector("button#label_restore_dict")
.addEventListener("click", restoreSynonymDict);
if (DEBUG) console.log("[Label Bookmarks] Synonym Dictionary Ready");
}
function setAdvancedSearch() {
function generatePlaceholder() {
const synonymDictKeys = Object.keys(synonymDict);
return synonymDictKeys.length
? "eg: " +
synonymDictKeys[
Math.floor(Math.random() * synonymDictKeys.length)
].split("(")[0]
: "";
}
function generateBasicField() {
const fieldContainer = document.createElement("div");
fieldContainer.className = "d-flex";
const searchInput = document.createElement("input");
searchInput.className = "form-control";
searchInput.required = true;
searchInput.id = "search_value";
searchInput.setAttribute("placeholder", generatePlaceholder());
// search with suggestion
const searchSuggestion = document.querySelector("#search_suggestion");
searchInput.addEventListener("keyup", (evt) => {
updateSuggestion(
evt,
searchSuggestion,
true,
(candidate, candidateButton) =>
candidateButton.addEventListener("click", () => {
const keywordsArray = searchInput.value.split(" ");
const keyword = keywordsArray[keywordsArray.length - 1];
let newKeyword = candidate["tag_name"];
if (keyword.match(/^!/) || keyword.match(/^!/))
newKeyword = "!" + newKeyword;
keywordsArray.splice(keywordsArray.length - 1, 1, newKeyword);
searchInput.value = keywordsArray.join(" ");
})
).catch(console.log);
});
const toggleBasic = document.createElement("button");
toggleBasic.style.border = "1px solid #ced4da";
toggleBasic.className = "btn btn-outline-secondary ms-2";
toggleBasic.type = "button";
toggleBasic.addEventListener("click", () => {
basic.classList.add("d-none");
basic.removeChild(basic.firstChild);
advanced.appendChild(generateAdvancedField(0));
advanced.classList.remove("d-none");
});
toggleBasic.innerHTML = ``;
fieldContainer.appendChild(searchInput);
fieldContainer.appendChild(toggleBasic);
return fieldContainer;
}
function generateAdvancedField(index) {
const fieldContainer = document.createElement("div");
fieldContainer.className = "mb-3";
const inputContainer = document.createElement("div");
inputContainer.className = "d-flex mb-2";
inputContainer.innerHTML = ``;
if (!index) {
inputContainer.firstElementChild.setAttribute(
"placeholder",
generatePlaceholder()
);
const toggleAdvanced = document.createElement("button");
toggleAdvanced.style.border = "1px solid #ced4da";
toggleAdvanced.className = "btn btn-outline-secondary ms-2";
toggleAdvanced.type = "button";
toggleAdvanced.addEventListener("click", () => {
const basic = document.querySelector("#basic_search_field");
const advanced = document.querySelector("#advanced_search_fields");
basic.appendChild(generateBasicField());
basic.classList.remove("d-none");
advanced.classList.add("d-none");
while (advanced.firstChild) {
advanced.removeChild(advanced.firstChild);
}
});
toggleAdvanced.innerHTML = ``;
const addFieldButton = document.createElement("button");
addFieldButton.style.border = "1px solid #ced4da";
addFieldButton.className = "btn btn-outline-secondary ms-2";
addFieldButton.type = "button";
addFieldButton.addEventListener("click", () => {
const advanced = document.querySelector("#advanced_search_fields");
advanced.appendChild(generateAdvancedField(advanced.childElementCount));
});
addFieldButton.innerHTML = ``;
inputContainer.appendChild(toggleAdvanced);
inputContainer.appendChild(addFieldButton);
} else {
const removeFieldButton = document.createElement("button");
removeFieldButton.style.border = "1px solid #ced4da";
removeFieldButton.className = "btn btn-outline-secondary ms-2";
removeFieldButton.type = "button";
removeFieldButton.addEventListener("click", () => {
const advanced = document.querySelector("#advanced_search_fields");
advanced.removeChild(fieldContainer);
});
removeFieldButton.innerHTML = ``;
inputContainer.appendChild(removeFieldButton);
}
const configContainer = document.createElement("div");
configContainer.className = "row";
[
"标题/Title",
"作者/Author",
"作品标签/Work Tags",
"用户标签/Bookmark Tags",
].forEach((name) => {
const id = name.split("/")[0] + index;
const container = document.createElement("div");
container.className = "col-3";
container.innerHTML = `
`;
configContainer.appendChild(container);
});
fieldContainer.appendChild(inputContainer);
fieldContainer.appendChild(configContainer);
return fieldContainer;
}
const basic = document.querySelector("#basic_search_field");
const advanced = document.querySelector("#advanced_search_fields");
basic.appendChild(generateBasicField());
}
function registerMenu() {
generator = getValue("showGenerator", "false") === "true";
if (generator)
GM_registerMenuCommand_("关闭随机图片 / Disable Shuffled Images", () => {
setValue("showGenerator", "false");
window.location.reload();
});
else
GM_registerMenuCommand_("启用随机图片 / Enable Shuffled Images", () => {
setValue("showGenerator", "true");
window.location.reload();
});
feature = getValue("showFeature", "false") === "true";
if (feature)
GM_registerMenuCommand_(
"关闭其他功能 / Disable Additional Functions",
() => {
setValue("showFeature", "false");
window.location.reload();
}
);
else
GM_registerMenuCommand_(
"显示其他功能 / Enable Additional Functions",
() => {
setValue("showFeature", "true");
window.location.reload();
}
);
}
(function () {
"use strict";
loadResources();
registerMenu();
waitForDom("nav")
.then(initializeVariables)
.then(createModalElements)
.then(injectElements);
})();