Pixiv收藏夹自动标签

自动为Pixiv收藏夹内图片打上已有的标签,并可以搜索收藏夹

目前为 2022-10-22 提交的版本。查看 最新版本

// ==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/[email protected]/dist/css/bootstrap.min.css
// @resource     bootstrapJS https://cdn.jsdelivr.net/npm/[email protected]/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

// ==/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 = `
      <div class="text-center text-black-50 fw-bold py-4 ${textColor}" style="white-space: pre-wrap; font-size: 2rem" id="no_result">
        暂无结果 / No Result
      </div>
    `;
  }
  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 = `
       <div class="mb-1 position-relative">
         <img src=${work.url} alt="square" class="rounded-3 img-fluid" />
         <div class="position-absolute w-100" style="pointer-events: none; top: 0;">
           <div class="p-1 d-flex">
             <div class="rate-icon d-none">R-18</div>
             <div class="ms-auto page-icon d-none">
               <span class="gbNjFx">
                 <svg viewBox="0 0 9 10" width="9" height="9" class="sc-14heosd-1 fArvVr">
                   <path d="M8,3 C8.55228475,3 9,3.44771525 9,4 L9,9 C9,9.55228475 8.55228475,10 8,10 L3,10 C2.44771525,10 2,9.55228475 2,9 L6,9 C7.1045695,9 8,8.1045695 8,7 L8,3 Z M1,1 L6,1    C6.55228475,1 7,1.44771525 7,2 L7,7 C7,7.55228475 6.55228475,8 6,8 L1,8 C0.44771525,8    0,7.55228475 0,7 L0,2 C0,1.44771525 0.44771525,1 1,1 Z" transform=""></path>
                 </svg>
               </span>
               <span class="ms-1 page-count">${work["pageCount"]}</span>
             </div>
           </div>
         </div>
       </div>
       <div class="mb-1" style="font-size: 10px; color: rgb(61, 118, 153); pointer-events: none">
         ${tagsString}
       </div>
       <div class="mb-1">
         <a href=${"/artworks/" + work.id}
          target="_blank" rel="noreferrer"
          style="font-weight: bold; color: ${textColor};">
          ${work.title}
          </a>
       </div>
       <div class="mb-4">
        <a href=${"/users/" + work["userId"]}  target="_blank" rel="noreferrer"
          style="rgba(0, 0, 0, 0.64)">
          <img
            src=${profile} alt="profile" class="rounded-circle"
            style="width: 24px; height: 24px; margin-right: 4px"
          />
          ${work["userName"]}
        </a>
       </div>
      `;
  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 = `
    <div class="p-2">
      <div class="images my-2" style="min-height: calc(100vh - 4.5rem);"></div>
      <button class="btn btn-outline-secondary w-100 d-none" id="gallery_all">查看全部 / See All</button>
      <button class="btn p-0 opacity-50 position-fixed" style="left: 2rem; top: 2rem;" id="gallery_exit">
        <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
          <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
        </svg>
      </button>
      <a class="position-fixed" style="left: 2rem; top: 7rem;" target="_blank" rel="noreferrer" id="gallery_link">
        <button class="btn p-0 opacity-50">
          <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-arrow-up-right" viewBox="0 0 16 16">
            <path fill-rule="evenodd" d="M14 2.5a.5.5 0 0 0-.5-.5h-6a.5.5 0 0 0 0 1h4.793L2.146 13.146a.5.5 0 0 0 .708.708L13 3.707V8.5a.5.5 0 0 0 1 0v-6z"/>
          </svg>
        </button>
      </a>
      <button class="btn p-0 opacity-50 position-fixed" style="left: 2rem; top: 50vh; transform: translateY(-50%);" id="gallery_left">
        <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-chevron-double-left" viewBox="0 0 16 16">
          <path fill-rule="evenodd" d="M8.354 1.646a.5.5 0 0 1 0 .708L2.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
          <path fill-rule="evenodd" d="M12.354 1.646a.5.5 0 0 1 0 .708L6.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
        </svg>
      </button>
      <button class="btn p-0 opacity-50 position-fixed" style="right: 2rem; top: 50vh; transform: translateY(-50%);" id="gallery_right">
        <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-chevron-double-right" viewBox="0 0 16 16">
          <path fill-rule="evenodd" d="M3.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L9.293 8 3.646 2.354a.5.5 0 0 1 0-.708z"/>
          <path fill-rule="evenodd" d="M7.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L13.293 8 7.646 2.354a.5.5 0 0 1 0-.708z"/>
        </svg>
      </button>
    </div>
  `;
  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 = `
      <div class="text-center">
        <img class="gallery-image mb-2" src=${masterUrl} alt="master">
      </div>
    `;
    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 = `<img class="gallery-image mb-2" src=${url} alt="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;
}

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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pin" viewBox="0 0 16 16">
    <path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5c0 .276-.224 1.5-.5 1.5s-.5-1.224-.5-1.5V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A5.921 5.921 0 0 1 5 6.708V2.277a2.77 2.77 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354zm1.58 1.408-.002-.001.002.001zm-.002-.001.002.001A.5.5 0 0 1 6 2v5a.5.5 0 0 1-.276.447h-.002l-.012.007-.054.03a4.922 4.922 0 0 0-.827.58c-.318.278-.585.596-.725.936h7.792c-.14-.34-.407-.658-.725-.936a4.915 4.915 0 0 0-.881-.61l-.012-.006h-.002A.5.5 0 0 1 10 7V2a.5.5 0 0 1 .295-.458 1.775 1.775 0 0 0 .351-.271c.08-.08.155-.17.214-.271H5.14c.06.1.133.191.214.271a1.78 1.78 0 0 0 .37.282z"/>
  </svg>`;
  const svgUnpin = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pin-angle" viewBox="0 0 16 16">
    <path id="svg_unpin" d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146zm.122 2.112v-.002.002zm0-.002v.002a.5.5 0 0 1-.122.51L6.293 6.878a.5.5 0 0 1-.511.12H5.78l-.014-.004a4.507 4.507 0 0 0-.288-.076 4.922 4.922 0 0 0-.765-.116c-.422-.028-.836.008-1.175.15l5.51 5.509c.141-.34.177-.753.149-1.175a4.924 4.924 0 0 0-.192-1.054l-.004-.013v-.001a.5.5 0 0 1 .12-.512l3.536-3.535a.5.5 0 0 1 .532-.115l.096.022c.087.017.208.034.344.034.114 0 .23-.011.343-.04L9.927 2.028c-.029.113-.04.23-.04.343a1.779 1.779 0 0 0 .062.46z"/>
  </svg>`;
  const svgClose = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
  <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
</svg>`;
  const defaultPinConfig = backdropConfig ? svgPin : svgUnpin;

  // label
  const labelModal = document.createElement("div");
  labelModal.className = "modal fade";
  labelModal.id = "label_modal";
  labelModal.tabIndex = -1;
  labelModal.innerHTML = `
    <div class="modal-dialog modal-lg ${bgColor} ${textColor}" style="pointer-events: initial">
      <div class="modal-header">
        <h5 class="modal-title">自动添加标签 / Label Bookmarks</h5>
        <button class="btn btn-close btn-close-empty ms-auto" id="label_pin">${defaultPinConfig}</button>
        <button class="btn btn-close btn-close-empty ms-3" data-bs-dismiss="modal">${svgClose}</button>
      </div>
      <form class="modal-body p-4" id="label_form">
        <div class="text-center mb-4">
          <div>如果对以下配置有疑惑,请参考
            <a href="https://greasyfork.org/zh-CN/scripts/423823-pixiv%E6%94%B6%E8%97%8F%E5%A4%B9%E8%87%AA%E5%8A%A8%E6%A0%87%E7%AD%BE?locale_override=1" style="text-decoration: underline"
              target="_blank" rel="noreferrer">文档</a>
          </div>
          <div>Please refer to the
            <a href="https://greasyfork.org/en/scripts/423823-pixiv%E6%94%B6%E8%97%8F%E5%A4%B9%E8%87%AA%E5%8A%A8%E6%A0%87%E7%AD%BE" style="text-decoration: underline"
              target="_blank" rel="noreferrer">document</a> if confused.
          </div>
        </div>
        <div class="mb-4">
          <button type="button" class="mb-3 btn p-0" data-bs-toggle="collapse" data-bs-target="#synonym_content">
            &#9658; 同义词词典 / Synonym Dict
          </button>
          <div class="collapse show px-3" id="synonym_content">
            <div class="mb-4">
              <button type="button" class="btn p-0 fw-light" data-bs-toggle="collapse"
                data-bs-target="#load_synonym_dict">&#9658; 加载词典 / Load Dict</button>
              <div class="pt-3 collapse" id="load_synonym_dict">
                <input class="form-control border mb-3" type="file" accept="application/json" id="synonym_dict_input"/>
                <div class="mb-3 d-flex">
                  <div class="fw-light me-auto">
                    如果词典意外丢失,请点击右侧按钮获取历史版本的词典用于恢复,并上报该BUG
                    <br />
                    If the dictionary accidentally got lost, click the right button to download the history version for restoring, as well as reporting the bug ASAP.
                  </div>
                  <button type="button" class="btn btn-outline-primary ms-3" id="label_restore_dict">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
                      <path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
                      <path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
                    </svg>
                  </button>
                </div>
              </div>
            </div>
            <div class="mb-4">
              <button type="button" class="btn p-0 fw-light" data-bs-toggle="collapse"
                data-bs-target="#edit_synonym_dict">&#9658; 编辑词典 / Edit Dict</button>
              <div class="pt-3 collapse" id="edit_synonym_dict">
                <label class="form-label fw-light" for="target_tag">目标标签(用户标签) / Target Tag (User Tag)</label>
                <input class="form-control mb-3" type="text" id="target_tag" placeholder="eg: 新世紀エヴァンゲリオン" />
                <label class="form-label fw-light" for="tag_alias">同义词(作品标签,空格/回车分割,不区分大小写) / Alias (From Artwork, Space/Line Delimited, Case-Insensitive)</label>
                <textarea class="form-control mb-3" rows="2" id="tag_alias" style="min-height: initial" placeholder="eg: エヴァンゲリオン evangelion eva"></textarea>
                <div class="mb-3" style="display: none" >
                  <div class="mb-2">备选同义词 / Suggested Alias</div>
                  <div class="ms-3" id="label_suggestion"></div>
                </div>
                <div class="d-flex mb-3" id="synonym_buttons">
                  <button type="button" class="btn btn-outline-primary me-auto" title="保存至本地\nSave to Local Disk">导出词典 / Export Dict</button>
                  <button type="button" class="btn btn-outline-primary me-3" title="加载已有标签的同义词\nLoad Alias of Existing User Tag">加载标签 / Load Tag</button>
                  <button type="button" class="btn btn-outline-primary" title="保存结果至词典,同义词为空时将删除该项\nUpdate dict. User tag will be removed if alias is empty">更新标签 / Update Tag</button>
                </div>
              </div>
            </div>
            <div class="mb-4">
              <button type="button" class="btn p-0 fw-light" data-bs-toggle="collapse"
                data-bs-target="#preview_synonym_dict">&#9658; 预览词典 / Preview Dict</button>
              <div class="pt-3 collapse show" id="preview_synonym_dict">
                <div class="mb-2 position-relative">
                  <input type="text" class="form-control mb-2" id="synonym_filter" placeholder="筛选 / Filter" />
                  <button type="button" class="position-absolute btn btn-close end-0 top-50 translate-middle" id="clear_synonym_filter"/>
                </div>
                <div id="synonym_preview" class="border py-1 px-3" style="white-space: pre-wrap; min-height: 100px; max-height: 30vh; overflow-y: scroll"></div>
              </div>
            </div>
          </div>
        </div>
        <div class="mb-4">
          <button type="button" class="btn p-0 mb-3" data-bs-toggle="collapse" data-bs-target="#label_tag_query">
            &#9658; 自动标签范围 / Auto Labeling For
          </button>
          <select id="label_tag_query"
            class="form-select select-custom-tags px-3 collapse show ${bgColor}">
            <option value="未分類">未分类作品 / Uncategorized Only</option>
            <option value="">全部作品 / All Works</option>
          </select>
        </div>
        <div class="mb-4">
          <button type="button" class="btn p-0 mb-3" data-bs-toggle="collapse" data-bs-target="#advanced_label">&#9658; 高级设置 / Advanced</button>
          <div class="px-3 mb-4 collapse" id="advanced_label">
            <div class="mb-3">
              <label class="form-label fw-light" for="label_exclusion">
                忽略以下标签(空格/回车分割,区分大小写)
                <br />
                Ignore following tags (delimited by line/space, case-sensitive)
              </label>
              <textarea id="label_exclusion" class="form-control" rows="2" id="tag_alias" style="min-height: initial" placeholder="オリジナル"></textarea>
            </div>
            <div class="mb-3">
              <label class="form-label fw-light" for="label_add_first">
                无匹配时是否自动添加首个未被忽略的标签
                <br />
                Whether the first tag (not ignored) will be added if there is not any match
              </label>
              <select id="label_add_first" class="form-select ${bgColor}">
                <option value="false">否 / No</option>
                <option value="true">是 / Yes</option>
              </select>
            </div>
            <div class="mb-3">
              <label class="form-label fw-light" for="label_r18">
                是否为非全年龄作品标记#R-18标签
                <br />
                Whether NSFW works will be labeled as #R-18
              </label>
              <select id="label_r18" class="form-select ${bgColor}">
                <option value="true">标记 / Yes</option>
                <option value="false">忽略 / No</option>
              </select>
            </div>
          </div>
        </div>
        <div class="my-5">
          <label class="form-label mb-2 fw-light">执行进度 / Progress</label>
          <div class="mb-3" id="label_prompt"></div>
          <div class="progress" id="progress" style="min-height: 1rem">
            <div style="width: 0" class="progress-bar progress-bar-striped"
             id="progress_bar" role="progressbar"></div>
          </div>
        </div>
        <button class="d-none" id="start_label_button"></button>
      </form>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">关闭 / Close</button>
        <button type="button" class="btn btn-outline-danger me-3" style="white-space: nowrap"
          id="footer_stop_button">停止 / Stop
        </button>
        <button type="button" class="btn btn-outline-primary ms-auto"
          onclick="window.location.reload();">刷新 / Refresh</button>
        <button type="button" class="btn btn-outline-primary ms-3" style="white-space: nowrap"
          id="footer_label_button">开始 / Start
        </button>
      </div>
    </div>
  `;
  // 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 = `
    <div class="modal-dialog modal-xl d-flex flex-column ${bgColor} ${textColor}" style="pointer-events: initial">
      <div class="gallery overflow-auto no-scroll flex-grow-1 d-none"></div>
      <div class="modal-header">
        <h5 class="modal-title">搜索图片标签 / Search Bookmarks</h5>
        <button class="btn btn-close btn-close-empty ms-auto" id="search_pin">${defaultPinConfig}</button>
        <button class="btn btn-close btn-close-empty ms-3" data-bs-dismiss="modal">${svgClose}</button>
      </div>
      <form class="modal-body flex-grow-1 d-flex flex-column p-4" id="search_form">
          <div class="mb-4">
            <div class="mb-3">
              <label class="form-label" for="search_value">
                输入要搜索的关键字,使用空格分隔,在关键字前加<strong>感叹号</strong>来排除该关键字。将会结合用户设置的同义词词典,
                在收藏的图片中寻找标签匹配的图片展示在下方。当收藏时间跨度较大时,使用自定义标签缩小范围以加速搜索。
                <br />
                点击输入框右侧的切换按钮切换至高级搜索模式,此时可以限制该关键词的搜索范围,单个输入框只接受一个关键词。
                <br />
                Enter keywords seperated by spaces to launch a search. Add a <strong>Exclamation Mark</strong>
                before any keyword to exclude it. The search process will use your synonym dictionary to look up the tags
                of your bookmarked images. Use custom tag to narrow the search if images come from a wide time range.
                <br />
                Clicking the button on the right will toggle to advanced search mode, where you are able to choose
                the search fields of each keyword. Note that in this mode only single keyword is accepted for each input box.
              </label>
              <div id="basic_search_field"></div>
              <div class="d-none" id="advanced_search_fields"></div>
              <div class="mt-3" style="display: none">
                <div class="mb-2">您是否想要搜索 / Are you looking for:</div>
                <div class="ms-3" id="search_suggestion"></div>
              </div>
            </div>
            <button class="btn p-0 mb-3"
              data-bs-toggle="collapse" data-bs-target="#advanced_search"
              type="button" id="advanced_search_controller">&#9658; 高级设置 / Advanced</button>
            <div class="mb-3 px-3 collapse" id="advanced_search">
              <div class="mb-2">
                <label class="form-label fw-light">标签匹配模式 / Match Pattern</label>
                <select class="form-select ${bgColor}" id="search_exact_match">
                  <option value="fuzzy">模糊匹配 / Fuzzy Match</option>
                  <option value="exact">精确匹配 / Exact Match</option>
                </select>
              </div>
              <div class="mb-2">
                <label class="form-label fw-light">搜索标签数量匹配 / Search Tags Length Match</label>
                <select class="form-select ${bgColor}" id="search_length_match">
                  <option value="false">不需匹配 / Not Needed</option>
                  <option value="true">需要匹配 / Must Match</option>
                </select>
              </div>
              <div class="mb-2">
                <label class="form-label fw-light">自定义标签用于缩小搜索范围 / Custom Tag to Narrow the Search</label>
                <select class="form-select select-custom-tags ${bgColor}" id="search_select_tag">
                  <option value="">所有收藏 / All Works</option>
                  <option value="未分類">未分类作品 / Uncategorized Works</option>
                </select>
              </div>
              <div class="mb-2">
                <label class="form-label fw-light">作品公开类型 / Publication Type</label>
                <select class="form-select ${bgColor}" id="search_publication">
                  <option value="show">公开收藏 / Public</option>
                  <option value="hide">私密收藏 / Private</option>
                </select>
              </div>
            </div>
          </div>
          <div class="flex-grow-1">
            <div class="position-absolute start-50 top-50 spinner-border text-secondary" style="display: none" id="spinner">
            </div>
            <div class="row" id="search_results"></div>
            <div class="mb-2 text-end" id="search_prompt"></div>
            <button class="btn btn-outline-secondary w-100 py-1" style="display: none;" id="search_more">继续搜索 / Search More</button>
          </div>
      </form>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">关闭 / Close</button>
        <button type="button" class="btn btn-outline-primary ms-auto" style="white-space: nowrap"
          id="footer_search_button">搜索 / Search</button>
      </div>
    </div>
  `;
  // 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 = `
    <div class="modal-dialog modal-xl d-flex flex-column ${bgColor} ${textColor}" style="pointer-events: initial">
      <div class="gallery overflow-auto no-scroll flex-grow-1 d-none"></div>
      <div class="modal-header">
        <h5 class="modal-title">展示随机图片 / Display Shuffled Images</h5>
        <button class="btn btn-close btn-close-empty ms-auto" id="generator_pin">${defaultPinConfig}</button>
        <button class="btn btn-close btn-close-empty ms-3" data-bs-dismiss="modal">${svgClose}</button>
      </div>
      <div class="modal-body flex-grow-1 d-flex flex-column p-4 no-scroll">
        <div class="mb-4 flex-grow-1 d-none" id="generator_display">
          <div class="position-relative mb-4" style="height: 3rem" id="generator_spinner"><div class="position-absolute start-50 top-50 spinner-border text-secondary"></div></div>
          <div class="row" id="generator_results"></div>
          <button class="btn btn-outline-secondary w-100 py-1 d-none" id="generator_more">显示更多 / More</button>
          <div class="mt-3 text-end" id="generator_prompt"></div>
          <hr class="mt-4" />
        </div>
        <div>
          <label class="form-label mb-3">
            选择已收藏的标签,填写批量大小等参数后,点击<strong>生成</strong>按钮生成数组随机图片。
            点击<strong>保存至新标签</strong>可以按批次保存至临时标签,随后可以修改标签名称。
            <br />
            点击缩略图进入鉴赏模式,可以使用上下左右方向键切换浏览的图片,点击左上角X按钮回到原模式。
            <br />
            本功能可以用来随机浏览收藏的图片,或是将较大的收藏标签切分为数个较小的收藏标签。本页面仍在开发阶段,欢迎留言。
            <br />
            Select a tag you have bookmarked and set batch size and other configs before generating.
            Click on <strong>Generate</strong> to get batches of shuffled images. Click on <strong>Save to Tag</strong>
            will add a temperate tag to each batch that you can edit later.
            <br />
            Click on the thumbnail to enter gallery mode, where you can use four arrow key to switch between pages. Click the X on the top left to exit gallery mode.
            <br />
            This function is used to view random images, or slice a big tag into smaller ones. This page is in development and suggestions are welcomed.
          </label>
          <form class="mb-3" id="generator_form">
            <select class="mb-3 form-select select-custom-tags flex-grow-1 ${bgColor}" id="generator_select_tag">
              <option value="未分類">未分类作品 / Uncategorized Works</option>
              <option value="">所有收藏 / All Works</option>
            </select>
            <div class="row">
              <div class="col-4">
                <label for="generator_form_num" class="form-label fw-light">批量大小 / Batch Size</label>
                <input type="text" class="form-control me-3" value="100" id="generator_form_num" />
              </div>
              <div class="col-4">
                <label for="generator_form_publication" class="form-label fw-light">作品公开类型 / Publication Type</label>
                <select class="form-select ${bgColor}" id="generator_form_publication">
                  <option value="show">公开收藏 / Public</option>
                  <option value="hide">私密收藏 / Private</option>
                </select>
              </div>
              <div class="col-4">
                <label for="generator_form_restriction" class="form-label fw-light">作品限制类型 / Restriction Type</label>
                <select class="form-select ${bgColor}" id="generator_form_restriction">
                  <option value="all">全部作品 / All</option>
                  <option value="sfw">全年龄 / SFW</option>
                  <option value="nsfw">非全年龄 / NSFW</option>
                </select>
              </div>
            </div>
            <div class="mt-3 d-flex d-none" id="generator_form_buttons">
              <button type="button" class="btn btn-outline-primary me-auto">清除搜索 / Clear</button>
              <button type="button" class="btn btn-outline-primary">保存至标签 / Save to Tag</button>
              <button class="btn btn-outline-primary d-none">生成 / Generate</button>
            </div>
          </form>
          <div class="my-3 fw-bold text-center d-none" id="generator_save_tag_prompt"></div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary me-auto" data-bs-dismiss="modal">关闭 / Close</button>
        <button type="button" class="btn btn-outline-danger me-3" id="generator_footer_stop">停止 / Stop</button>
        <button type="button" class="btn btn-outline-primary" id="generator_footer_button">加载 / Load</button>
      </div>
    </div>
  `;
  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 = `
    <div class="modal-dialog modal-lg ${bgColor} ${textColor}" style="pointer-events: initial">
      <div class="modal-header">
        <h5 class="modal-title">其他功能 / Other Functions</h5>
        <button class="btn btn-close btn-close-empty ms-auto" data-bs-dismiss="modal">${svgClose}</button>
      </div>
      <div class="modal-body p-4">
        <label class="form-label mb-4" for="feature_select">
          本页面用于放置一些较为零散独立的功能。
          <br />
          This page contains some scattered and independent functions.
        </label>
        <div class="mb-4">
          <div class="row mb-3">
            <div class="col-6">
              <label for="feature_form_publication" class="form-label fw-light">作品公开类型 / Publication Type</label>
              <select class="form-select ${bgColor}" id="feature_form_publication">
                  <option value="show">公开收藏 / Public</option>
                  <option value="hide">私密收藏 / Private</option>
               </select>
            </div>
            <div class="col-6">
              <label for="feature_select_tag" class="form-label fw-light">作品标签 / Tag</label>
              <select class="col-6 form-select select-custom-tags flex-grow-1 ${bgColor}" id="feature_select_tag">
                <option value="">所有收藏 / All Works</option>
              </select>
            </div>
          </div>
          <div class="" id="feature_buttons">
            <div class="mb-2">
              <button class="btn btn-outline-danger me-3">删除该标签 / Delete This Tag</button>
              <button class="btn btn-outline-danger me-auto">清除作品标签 / Clear Work Tags</button>
            </div>
            <div>
              <button class="btn btn-outline-secondary me-3">更改作品公开类型 / Toggle Publication Type</button>
            </div>
          </div>
        </div>
        <div class="fw-bold text-center mt-4 d-none" id="feature_prompt"></div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary me-auto" data-bs-dismiss="modal">关闭 / Close</button>
        <button type="button" class="btn btn-outline-danger" onclick="window.runFlag = false">停止 / Stop</button>
      </div>
    </div>
  `;
  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 = `
    <div class="modal-dialog modal-dialog-centered" style="pointer-events: initial">
      <div class="modal-body py-4 px-5 ${bgColor} ${textColor}">
        <div class="fs-5 mb-4 text-center">
          <div class="mt-4 mb-3">正在处理 / Working on</div>
          <div id="progress_modal_prompt"></div>
        </div>
        <div class="progress my-4" id="progress_modal_progress" style="min-height: 1rem">
          <div style="width: 0" class="progress-bar progress-bar-striped"
           id="progress_modal_progress_bar" role="progressbar"></div>
        </div>
        <div class="my-4 text-center">
          <button class="btn btn-danger" id="stop_remove_tag_button">停止 / Stop</button>
        </div>
      </div>
    </div>
  `;
  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 = `
        <button class="label-button ${textColor} ${fClass}" data-bs-toggle="modal" data-bs-target="#feature_modal" id="feature_modal_button"/>
        <button class="label-button ${textColor} ${gClass}" data-bs-toggle="modal" data-bs-target="#generator_modal" id="generator_modal_button"/>
        <button class="label-button ${textColor}" data-bs-toggle="modal" data-bs-target="#search_modal" id="search_modal_button"/>
        <button class="label-button ${textColor}" data-bs-toggle="modal" data-bs-target="#label_modal" id="label_modal_button"/>
      `;

  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 = `<div aria-disabled="true" class="sc-4a5gah-0 ${clearTagsThemeClass}">
            <div class="sc-4a5gah-1 kHyYuA">
              ${clearTagsText}
            </div>
          </div>`;
  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 = `
    <div style="margin-right: 4px">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
        <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
        <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
      </svg>
    </div>
    <div class="fw-bold ${textColor}" id="remove_tag_prompt"></div>
  `;
  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"] || "<span>🈳</span>"
        } - ${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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-toggles" viewBox="0 0 16 16">
      <path d="M4.5 9a3.5 3.5 0 1 0 0 7h7a3.5 3.5 0 1 0 0-7h-7zm7 6a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm-7-14a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zm2.45 0A3.49 3.49 0 0 1 8 3.5 3.49 3.49 0 0 1 6.95 6h4.55a2.5 2.5 0 0 0 0-5H6.95zM4.5 0h7a3.5 3.5 0 1 1 0 7h-7a3.5 3.5 0 1 1 0-7z"/>
    </svg>`;
    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 = `<input type="text" class="advanced_search_field form-control" required>`;
    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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-toggles" viewBox="0 0 16 16">
        <path d="M4.5 9a3.5 3.5 0 1 0 0 7h7a3.5 3.5 0 1 0 0-7h-7zm7 6a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm-7-14a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zm2.45 0A3.49 3.49 0 0 1 8 3.5 3.49 3.49 0 0 1 6.95 6h4.55a2.5 2.5 0 0 0 0-5H6.95zM4.5 0h7a3.5 3.5 0 1 1 0 7h-7a3.5 3.5 0 1 1 0-7z"/>
      </svg>`;
      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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
        <path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
      </svg>`;
      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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dash-lg" viewBox="0 0 16 16">
        <path fill-rule="evenodd" d="M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8Z"/>
      </svg>`;
      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 = `<div class="form-check">
        <input type="checkbox" class="form-check-input" id=${id} checked="true">
        <label class="form-check-label" for=${id}>${name}</label>
      </div>`;
      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);
})();