Danbooru (etc...) userscripts

Posted under General

Is there a CSS that removes the downvote button? I've no need for a function that is both completely useless to me and has been used by other users as a way to "attack" other uploaders.

It also serves as a safety measure so that I don't accidentally press the downvote button (which fortunately hasn't happened yet) when browsing on mobile.

Is there a userscript or CSS that adds a tagbox (with a fully functioning tag autocomplete dropdown menu) into the main upload page without anything uploaded, similarly to the old upload page?

Jisadel said:

Is there a userscript or CSS that adds a tagbox (with a fully functioning tag autocomplete dropdown menu) into the main upload page without anything uploaded, similarly to the old upload page?

Here you go:

Show
// ==UserScript==
// @name        Danbooru - add tag box on /uploads/new
// @version     0.1.1
// @author      imapiekindaguy
// @match       *://*.donmai.us/uploads/new
// @grant       GM_addStyle
// @run-at      document-idle
// ==/UserScript==

GM_addStyle(`
div#c-uploads div#a-new textarea {
  width: 100%;
  max-width: 100%;
  height: 20vh;
}
`);

document.querySelector("div#a-new").insertAdjacentHTML("beforeend", `
<div>
  <div class="flex justify-between">
    <label for="post_tag_string">Tags</label>

    <span data-tag-counter="" data-for="#post_tag_string" class="text-muted text-sm">
      <span class="tag-count"></span>
    </span>
  </div>

  <div class="input text optional post_tag_string"><textarea data-autocomplete="tag-edit" data-shortcut="e" class="text optional ui-autocomplete-input" name="post[tag_string]" id="post_tag_string" autocomplete="off" title="Shortcut is e"></textarea></div>
</div>`
);

Danbooru.Autocomplete.initialize_all();

Updated by ygmdd

imapiekindaguy said:

Here you go:

Show
// ==UserScript==
// @name        Danbooru - add tag box on /uploads/new
// @version     0.1.0
// @author      imapiekindaguy
// @match       *://*.donmai.us/uploads/new
// @grant       GM_addStyle
// @run-at      document-idle
// ==/UserScript==

GM_addStyle(`
div#c-uploads div#a-new textarea {
  width: 100%;
  max-width: 100%;
  height: 20vh;
}
`);

document.querySelector("div#a-new").insertAdjacentHTML("beforeend", `
<div>
  <div class="flex justify-between">
    <label for="post_tag_string">Tags</label>

    <span data-tag-counter="" data-for="#post_tag_string" class="text-muted text-sm">
      <span class="tag-count"></span>
    </span>
  </div>

  <div class="input text optional post_tag_string"><textarea data-autocomplete="tag-edit" data-shortcut="e" class="text optional ui-autocomplete-input" name="post[tag_string]" id="post_tag_string" autocomplete="off" title="Shortcut is e"></textarea></div>
</div>`
);

Although a tag box is added, the autocomplete doesn't seem to function on the added tag box. If there's no way to make it work, then it's alright.

Jisadel said:

Although a tag box is added, the autocomplete doesn't seem to function on the added tag box. If there's no way to make it work, then it's alright.

Oh, sorry about that. Add the line Danbooru.Autocomplete.initialize_all(); to the end of the script.

I was wondering why it just seemed to work for me; I have another script that was enabling autocomplete.

imapiekindaguy said:

Oh, sorry about that. Add the line Danbooru.Autocomplete.initialize_all(); to the end of the script.

I was wondering why it just seemed to work for me; I have another script that was enabling autocomplete.

Nice, the script works! Thank you!

I just finished up with a 'smart canvas' userscript, available here. There's a few images there to show it better, but basically it makes a wrapper for (full size) image posts to ensure they fit in your browser window, which means it'll scale down oversized images just as much as needed to fit, and clicking once will zoom in to 1x scale, clicking again resets scale to initial. It also allows you to pan around, and zoom manually (up to 3x), while retaining image-drag functionality at the initial zoom scale (for my fellow click-and-drag savers).

The inspiration was to essentially have a more seamless version of Eza's Image Glutton, bringing sensible zoom/panning into the post page directly. It'd be great to hear feedback, and I'm open to feature or option requests (I tried to make sure all significant variables are already possible to change as constants at the top of the script, but could make a custom interface for tweaking if enough people are interested).

A heads up: deleted uploads percentage (whether you're using the original script or my edited version from forum #310718) was broken due to 1376f7d. To fix it, all you need to do is change Uploads to Posts in the script. I've edited the aforementioned forum post as well if you just want to turn your brain off and copy-paste the entire thing over your current version.

Sticky Search Box

Makes the search box stick to the top of the page.

I only recently realized this is actually a small feature from Danbooru EX, though with some improvements:

  • Better experience on mobile devices.
  • Fixed the autocomplete dropdown menu.
Show
// ==UserScript==
// @name        Sticky Search Box
// @namespace   https://danbooru.donmai.us/forum_topics/8502
// @match       *://*.donmai.us/*
// @grant       none
// @version     1.0
// @author      Sibyl
// @description Makes the search box stick to the top of the page.
// ==/UserScript==

const useAcrylicBackground = true;

const acrylicConfig = {
  b: "backdrop-filter:blur(10px);",
  bg: "background-color:rgba(255,255,255,.8);", // Body Background Color
  rbg: "background-color:rgba(225,232,255,.8);", // Responsive Menu Background Color
  rbgc: "background-color:rgba(244,246,255,.4);", // Responsive Menu Background Color Current
  sbg: "background-color:rgba(244,246,255,.8);", // Subnav Menu Background Color
  d: {
    // Dark Mode
    bg: "background-color:rgba(30,30,44,.8);",
    rbg: "background-color:rgba(44,45,63,.8);",
    rbgc: "background-color:rgba(44,45,63,.4);",
    sbg: "background-color:rgba(44,45,63,.8);"
  }
};

const searchBox = document.getElementById("search-box");
if (searchBox) {
  let searchForm = document.getElementById("search-box-form"),
    input = document.getElementById("tags"),
    header = document.getElementById("top"),
    div = document.createElement("div");
  div.id = "search-header";
  document.body.insertBefore(div, header);
  div.appendChild(searchForm);
  document.getElementById("app-name").remove();
  document.querySelector('#post-sections a[href="#search-box"]')?.remove();
  searchBox.remove();
  setTimeout(() => $(input).autocomplete("option", "appendTo", "#search-header"), 0);
  const style = document.createElement("style");
  document.head.appendChild(style);
  const bgConfig = !useAcrylicBackground
    ? "}#search-header{background-color:var(--body-background-color)}"
    : `#top{background-color:transparent}#main-menu{${acrylicConfig.b};${acrylicConfig.rbg}}#main-menu .current{${acrylicConfig.rbgc}}#subnav-menu{${acrylicConfig.b};${acrylicConfig.sbg}}}#search-header{${acrylicConfig.b};${acrylicConfig.bg}}@media (prefers-color-scheme:dark){#search-header{${acrylicConfig.d.bg}}}@media screen and (max-width:660px) and (prefers-color-scheme:dark){#main-menu{${acrylicConfig.d.rbg}}#main-menu .current{${acrylicConfig.d.rbgc}}#subnav-menu{${acrylicConfig.d.sbg}}}`;
  style.innerHTML =
    "body{height:unset}#search-header{position:sticky;top:0;z-index:2;}#search-box-form{min-width:180px;width:50vw;margin:0 30px;padding:.5rem 0}#search-box-form input{height:26px}#app-name-header{display:none}#notice{top:calc(1rem + 26px)}#main-menu a{outline-offset:-1px}header#top,header#top>nav{margin-top:0!important}@media screen and (max-width:660px){header#top{z-index:2;position:sticky;top:calc(26px + 1.5rem)}header#top>div{display:block;margin:0}#app-name-header{display:block;position:fixed;top:.3rem;left:.5rem}header#top>div>a{position:fixed;top:.7rem;right:.5rem}#search-box-form{width:70vw;margin:0 auto;padding:.75rem 0}#search-box-form input#tags{min-width:180px}#search-header .ui-menu{width:70vw!important}#notice{top:calc(1.5rem + 26px)}" +
    bgConfig;
  document.getElementById("app-logo").addEventListener("click", e => {
    e.preventDefault();
    e.currentTarget.blur();
    window.scrollTo({ top: 0, behavior: "smooth" });
  });
  input.addEventListener("keydown", function (event) {
    if (event.altKey && event.key === "Enter") {
      event.preventDefault();
      const query = encodeURIComponent(this.value.trim());
      if (query) {
        const searchUrl = `/posts?tags=${query}`;
        window.open(searchUrl, "_blank");
      }
    }
  });
}

Updated by Sibyl

// ==UserScript==
// @name Toggle Blacklist
// @namespace topic #8502
// @match *://*.donmai.us/*
// @grant none
// @version 1.0
// @run-at document-end
// ==/UserScript==const hotKey = "alt+b"; // Custom hotkey
Danbooru.Utility.keydown(hotKey, "toggle_blacklist", ()=> {
document.querySelector("#blacklist-box>div>label>input").click();
});

See commit #9cdf821

Updated by Sibyl

Sibyl said:

// ==UserScript==
// @name        Toggle Blacklist
// @namespace   https://danbooru.donmai.us/forum_topics/8502
// @match       *://*.donmai.us/*
// @grant       none
// @version     1.0
// @run-at      document-end
// ==/UserScript==

const hotKey = "alt+b"; // Custom hotkey
Danbooru.Utility.keydown(hotKey, "toggle_blacklist", ()=>{
  const a = document.querySelector("#disable-all-blacklists");
  if (a.style.display === "none") a.nextElementSibling.click();
  else a.click();
});

Sweet, how do I change the hotkey? I trried editing the code but it didn't work. alt+b opens up the bookmark menu on firefox

๐Ÿ“Š Post Source Report

Counts and visualizes the sources of a user's uploads on their profile page.

If you know of any additional sites worth including, feel free to DM me.

You can also customize the tracked sources by editing the code at the beginning of the script.

Show
// ==UserScript==
// @name        Source Report
// @namespace   https://danbooru.donmai.us/forum_topics/8502
// @match       *://*.donmai.us/users/*
// @match       *://*.donmai.us/profile
// @version     1.23
// @author      Sibyl
// @description Counts and visualizes the sources of a user's uploads.
// @resource    echarts.pieonly.build https://paste.ee/r/tjrVA697/0
// @grant       GM_getResourceText
// @run-at      document-end
// ==/UserScript==

const sourceType = [
  {
    name: "Pixiv",
    search: "pixiv:any"
  },
  {
    name: "Twitter",
    search: "~source:*://x.com/ ~source:*://twitter.com/ ~source:*://pbs.twimg.com/"
  },
  {
    name: "Tumblr",
    search: "source:*://*.tumblr.com/"
  },
  {
    name: "Lofter",
    search: "source:*://*.lofter.com/"
  },
  {
    name: "DeviantArt",
    search: "~source:*://*.deviantart.com ~source:*://deviantart.com ~source:*://*.deviantart.net/ ~source:*://images-wixmp-*.wixmp.com/"
  },
  {
    name: "yande.re",
    search: "~source:*://yande.re ~source:*://files.yande.re"
  },
  {
    name: "Niconico",
    search: "~source:*://*.nicovideo.jp ~source:*://*.nicoseiga.jp"
  },
  {
    name: "FC2",
    search: "source:*://*.fc2.com"
  },
  {
    name: "E-hentai",
    search: "~source:*://exhentai.org/ ~source:*://e-hentai.org/ ~source:*://*.hath.network/"
  },
  {
    name: "Weibo",
    search: "~source:*://*.weibo.com/ ~source:*://weibo.com/ ~source:*://m.weibo.cn ~source:*.sinaimg.cn/"
  },
  {
    name: "Fanbox",
    search: "source:*://*.fanbox.cc"
  },
  {
    name: "Fantia",
    search: "~source:*://*.fantia.jp ~source:*://fantia.jp/"
  },
  {
    name: "Unknown",
    search: '~source:"" ~source_request'
  },
  {
    name: "Artstation",
    search: "~source:*://artstation.com ~source:*://*.artstation.com"
  },
  {
    name: "Bilibili",
    search: "~source:*://*.bilibili.com/ ~source:*://*.hdslb.com/"
  },
  {
    name: "Bluesky",
    search: "source:https://bsky.app/"
  },
  {
    name: "๐Ÿ‡ฐ๐Ÿ‡ทSNS",
    search: "~source:*dcinside ~source:*://*.naver.com ~source:*://arca.live"
  }
];

const sourceReport = {
  widthQuery: window.matchMedia("(max-width: 660px)"),
  darkQuery: window.matchMedia("(prefers-color-scheme: dark)"),
  baseRichStyle: {
    fontSize: 18,
    fontWeight: "bold"
  },
  get config() {
    const textColor = this.darkQuery.matches ? "#D1D1DA" : "#000";
    const secondaryColor = this.darkQuery.matches ? "#B9B8CE" : "#666";
    const backgroundColor = this.darkQuery.matches ? "#1E1E2C" : "#FFF";
    return {
      backgroundColor,
      title: {
        text: `{${this.userLevel}|${this.userName.replace(/_/g, " ")}}'s Source Report`,
        subtext: this.chartSubtext,
        left: "center",
        textStyle: {
          ...this.baseRichStyle,
          color: textColor,
          rich: {
            admin: { color: this.darkQuery.matches ? "#FF8A8B" : "#ED2426", ...this.baseRichStyle },
            moderator: { color: this.darkQuery.matches ? "#35C64A" : "#00AB2C", ...this.baseRichStyle },
            builder: { color: this.darkQuery.matches ? "#C797FF" : "#A800AA", ...this.baseRichStyle },
            platinum: { color: this.darkQuery.matches ? "#ABABBC" : "#777892", ...this.baseRichStyle },
            gold: { color: this.darkQuery.matches ? "#EAD084" : "#FD9200", ...this.baseRichStyle },
            member: { color: this.darkQuery.matches ? "#009BE6" : "#0075F8", ...this.baseRichStyle }
          }
        },
        subtextStyle: { color: secondaryColor }
      },
      tooltip: {
        trigger: "item"
      },
      legend: {
        orient: "horizontal",
        left: this.widthQuery.matches ? "left" : "center",
        top: "bottom",
        textStyle: { color: textColor }
      },
      series: [
        {
          type: "pie",
          radius: "50%",
          center: ["50%", "50%"],
          data: this.chartData,
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: "rgba(0, 0, 0, 0.5)"
            }
          },
          label: { color: this.darkQuery.matches ? "#D1D1DA" : "#000" }
        }
      ],
      toolbox: {
        show: true,
        orient: "vertical",
        left: "right",
        top: this.widthQuery.matches ? "65%" : "center",
        iconStyle: { borderColor: secondaryColor },
        emphasis: { iconStyle: { borderColor: this.darkQuery.matches ? "#009BE6" : "#3E98C5" } },
        feature: {
          dataView: { show: true },
          saveAsImage: { show: true }
        }
      }
    };
  },
  chartData: null,
  chartInstance: null,
  loadEcharts() {
    const lib = GM_getResourceText("echarts.pieonly.build");
    new Function(lib)();
    if (!this.chartData) this.fetchButton.textContent = "Counting...";
    this.chartInstance = echarts.init(this.chart);
    window.addEventListener("resize", this.chartInstance.resize);
    this.widthQuery.addEventListener("change", () => {
      this.chartInstance.setOption({ legend: this.config.legend, toolbox: this.config.toolbox });
    });
    this.darkQuery.addEventListener("change", () => {
      this.chartInstance.setOption(this.config);
    });
  },
  createModal() {
    this.container = document.createElement("div");
    this.container.style.position = "fixed";
    this.container.style.top = "0";
    this.container.style.left = "0";
    this.container.style.width = "100vw";
    this.container.style.height = "100vh";
    this.container.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
    this.container.style.zIndex = "9999";
    this.container.hidden = true;
    this.container.addEventListener("click", e => {
      if (e.target === this.container) {
        this.container.hidden = true;
      }
    });
    const shadowHost = document.createElement("div");
    const shadow = shadowHost.attachShadow({ mode: "open" });
    const style = `<style>button:disabled,button:not(:disabled):hover{background-color:var(--form-button-hover-background)}:root{font-size:87.5%;line-height:1.25em}h1{line-height:1.5em;margin:0;color:var(--header-color)}.count input:focus-visible{outline-offset:-2px;outline:2px solid var(--focus-ring-color);border-color:transparent}.content,.modal{max-height:90vh;color:var(--text-color);font-family:var(--body-font)}#chart,.modal{padding:10px}.modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--body-background-color);border-radius:4px;width:auto;box-shadow:0 0 20px rgba(0,0,0,.3)}.content{overflow-y:auto;overscroll-behavior-y:contain;scrollbar-width:thin;display:flex;flex-direction:column;flex-wrap:nowrap;align-items:flex-start;gap:10px}.options{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;width:100%;align-items:flex-end}button{cursor:pointer;margin-bottom:.5em;padding:.15rem 1em;border-radius:3px;border:1px solid var(--form-button-border-color);color:var(--form-button-text-color);background-color:var(--form-button-background)}button:not(:disabled):hover{box-shadow:0 0 2px var(--form-button-hover-box-shadow-color)}button:disabled{cursor:wait;color:var(--form-button-disabled-text-color)}.count{display:table;border-spacing:0 0.5em;border-collapse:separate}.count>div{display:table-row}.count label{display:table-cell;text-align:right;padding-right:.5em;font-weight:700}.count input{display:table-cell;width:100%;text-align:left;color:var(--form-input-text-color);border:1px solid var(--form-input-border-color);background-color:var(--form-input-background);font:var(--body-font)}#chart{border:1px solid #D1D1DA;border-radius:4px;align-self:center;width:min(calc(90vw - 24px),480px);height:min(60vh,320px)}@media (prefers-color-scheme:dark){.modal{border:1px solid #444}#chart{border-color:#444}}</style>`;

    shadow.innerHTML =
      style +
      '<div class="modal"><div class="content"><h1>Source Report</h1><div class="options"><div class="count"><div class="count_from"><label for="count_from">From</label> <input type="date" id="count_from"></div><div class="count_to"><label for="count_to">To</label> <input type="date" id="count_to"></div></div><button disabled>Fetching...</button></div><div id="chart"></div></div></div>';

    this.container.appendChild(shadowHost);
    document.body.appendChild(this.container);
    const countFrom = shadow.querySelector("#count_from");
    const countTo = shadow.querySelector("#count_to");
    Object.defineProperty(this, "from", {
      get() {
        return countFrom.value;
      }
    });
    Object.defineProperty(this, "to", {
      get() {
        return countTo.value;
      }
    });
    this.fetchButton = shadow.querySelector("button");
    this.chart = shadow.getElementById("chart");
    this.fetchButton.onclick = e => {
      e.preventDefault();
      this.load();
    };
  },
  showModal() {
    if (!this.chartInstance) {
      this.loadEcharts();
    }
    this.container.hidden = false;
    if (!this.chartData) this.load();
  },
  async fetchCounts(tags = "") {
    let date = "";
    if (this.from || this.to) date = ` date:${this.from}..${this.to}`;
    tags = `user:${this.userName} ${tags}${date}`;
    return new Promise(resolve => {
      fetch(`/counts/posts.json?tags=${encodeURIComponent(tags)}`)
        .then(resp => resp.json())
        .then(data => resolve(data))
        .catch(error => {
          Danbooru.error(`Failed to fetch counts: ${error}`);
          console.error(tags);
          resolve({ counts: { posts: 0 } });
        });
    });
  },
  fetchAll() {
    const promises = sourceType.map(type => {
      return this.fetchCounts(type.search).then(({ counts }) => {
        return { name: type.name, value: counts.posts };
      });
    });
    return Promise.all(promises);
  },
  load() {
    if (!this.chartInstance) this.fetchButton.textContent = "Loading ECharts...";
    else this.fetchButton.textContent = "Counting...";
    this.fetchButton.disabled = true;
    let subtextSuffix = this.from || this.to ? ` (${this.from} โ€“ ${this.to})` : "";
    this.fetchCounts().then(json => {
      this.all = json.counts.posts;
      this.chartSubtext = `Total: ${this.all} posts${subtextSuffix}`;
      this.fetchAll().then(allCounts => {
        allCounts.sort((a, b) => b.value - a.value);
        let restCounts = this.all;
        allCounts = allCounts.filter(counts => {
          const show = counts.value / this.all > 0.02;
          if (show) restCounts -= counts.value;
          return show;
        });
        if (restCounts) allCounts.push({ name: "Others", value: restCounts });
        this.chartData = allCounts;
        if (this.chartInstance) this.render();
        this.fetchButton.textContent = "Count";
        this.fetchButton.disabled = false;
      });
    });
  },
  render() {
    this.chartInstance.setOption(this.config);
    setTimeout(this.chartInstance.resize, 1000);
  },
  init() {
    const changesReport = document.querySelector('[href^="/post_versions"][href$="&search%5Bversion%5D=1&type=current"]');
    if (changesReport) {
      sourceReport.createModal();
      const nameEl = document.querySelector("#a-show>div>h1>a[data-user-name]");
      this.userName = nameEl.dataset.userName;
      const level = nameEl.dataset.userLevel;
      this.userLevel = level > 49 ? "admin" : level > 39 ? "moderator" : level > 31 ? "builder" : level > 30 ? "platinum" : level > 29 ? "gold" : "member";
      const a = document.createElement("a");
      a.href = "";
      a.textContent = "source report";
      changesReport.after(" | ", a);
      a.onclick = e => {
        e.preventDefault();
        this.showModal();
      };
    }
  }
};

const controller = document.body.dataset?.controller,
  action = document.body.dataset?.action;
if (controller === "users" && action === "show") {
  sourceReport.init();
}

Updated by Sibyl

Strikethrough for banned artists

Commit d38c451
* Limit custom CSS to 40k characters long.

Due to upcoming limitations on the number of characters allowed in custom CSS user settings, embedding banned artist data (over 90 KB) into custom CSS is no longer viable.

Since there's no way to target all banned artists using simple CSS selectors, we still need to fetch the data manually.

The updated script now stores the banned artist data in localStorage instead of custom CSS user settings. As a result, Iโ€™ve moved the updated code here from the custom CSS thread (forum #363012).

Show
// ==UserScript==
// @name          Strikethrough for banned artists
// @namespace     https://danbooru.donmai.us/forum_topics/8502
// @match         *://*.donmai.us/*
// @exclude-match *://cdn.donmai.us/*
// @version       1.1
// @author        Sibyl
// @description   Add strikethrough for banned artists.
// @grant         GM_registerMenuCommand
// @grant         GM_unregisterMenuCommand
// ==/UserScript==

// Set your preferences here
const preference = {
  updateInterval: 7 * 24 * 60 * 60 * 1000, // 7 days, in milliseconds
  sidebarQuestionMark: true,
  dtextTag: true, // [[artist_name]]
  dtextId: true // artist #1234
};
// Set your preferences โ†‘โ†‘โ†‘

const config = JSON.parse(localStorage.getItem("s4ba_config")) || {
  lastUpdate: 0,
  autoUpdate: true
};

const saveConfig = () => localStorage.setItem("s4ba_config", JSON.stringify(config));

GM_registerMenuCommand("๐Ÿ—‘๏ธ Remove local CSS", () => {
  localStorage.removeItem("s4ba_css");
  applyLocalCss();
  Danbooru.Utility.notice("Local CSS removed.");
});

GM_registerMenuCommand("โœ๏ธ Manually fetch artists info", updateCustomCss);

const nextUpdateStr = () => {
  if (!config.autoUpdate) return "disabled";
  else {
    if (config.lastUpdate === 0) return `(Next: ${new Date(Date.now()).toLocaleString()})`;
    else return `(Next: ${new Date(config.lastUpdate + preference.updateInterval).toLocaleString()})`;
  }
};
let menuId = GM_registerMenuCommand((config.autoUpdate ? "โœ”๏ธ" : "โŒ") + " Auto update " + nextUpdateStr(), function toggleAutoUpdate() {
  GM_unregisterMenuCommand(menuId);
  config.autoUpdate = !config.autoUpdate;
  Danbooru.Utility.notice("Auto update banned artists info " + (config.autoUpdate ? "enabled. " + nextUpdateStr() : "disabled"));
  const prefix = config.autoUpdate ? "โœ”๏ธ" : "โŒ";
  menuId = GM_registerMenuCommand(prefix + " Auto Update " + nextUpdateStr(), toggleAutoUpdate);
  saveConfig();
});

const version = typeof GM_info === undefined ? null : GM_info?.script?.version;
if (version && config.version !== version) {
  config.version = version;
  saveConfig();
  updateCustomCss();
} else if (config.autoUpdate && Date.now() - config.lastUpdate > preference.updateInterval) updateCustomCss();

async function updateCustomCss() {
  Danbooru.notice("Fetching banned artists info...");
  const data = await fetchBannedArtistData();
  if (data.length) {
    const newCss = generateCss(data);
    localStorage.setItem("s4ba_css", newCss);
    applyLocalCss();
    Danbooru.Utility.notice(`CSS updated successfully. ${data.length} banned artists found.`);
    config.lastUpdate = Date.now();
    saveConfig();
  }
}

function applyLocalCss() {
  let css = localStorage.getItem("s4ba_css");
  let style = document.getElementById("s4ba-style");
  if (style) style.remove();
  if (css) {
    style = document.createElement("style");
    style.id = "s4ba-style";
    style.textContent = css;
    document.head.appendChild(style);
  }
}

async function fetchBannedArtistData() {
  let page = 1;
  let allData = [];
  while (true) {
    const resp = await fetch("/artists.json?search[is_banned]=true&search[order]=created_at&only=id,name&limit=200&page=" + page);
    if (!resp.ok) {
      const msg = `Failed to get artist info: ${resp.status}`;
      Danbooru.Utility.error(msg);
      throw new Error(msg);
    }
    const data = await resp.json();
    if (!Array.isArray(data)) {
      const msg = "Failed to get artist info: Expected an array in response";
      Danbooru.Utility.error(msg);
      throw new Error(msg);
    }
    allData.push(...data);
    if (data.length < 200) break;
    else page++;
  }
  return allData;
}

const sidebarSelector = name => {
  return preference.sidebarQuestionMark ? `.tag-type-1 [href*='=${name}&']` : `.tag-type-1 [href*='tags=${name}&']`;
};
const dtextTagSelector = name => `.tag-type-1[href$='=${name}']`;
const dtextIdSelector = id => `[href='/artists/${id}']`;

function generateCss(data) {
  data.forEach(i => {
    i.name = escape(i.name);
  });
  let sel = ["", "", ""];
  data.forEach(i => {
    sel[0] += sidebarSelector(i.name) + ",";
    if (preference.dtextTag) sel[1] += dtextTagSelector(i.name) + ",";
    if (preference.dtextId) sel[2] += dtextIdSelector(i.id) + ",";
  });
  return sel.join("").slice(0, -1) + "{text-decoration:line-through}";
}

applyLocalCss();
1 8 9 10 11 12 13