Soybooru Ext

Add autocomplete to the Soybooru upload form

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Soybooru Ext
// @namespace   https://github.com/thoughever
// @match       https://booru.soy/*
// @match       http://booru.soy/*
// @match       https://www.booru.soy/*
// @match       http://www.booru.soy/*
// @grant       GM_setClipboard
// @version     0.1.2
// @author      thoughever
// @license     MIT
// @description Add autocomplete to the Soybooru upload form
// ==/UserScript==

(function () {

  "use strict";

  /* Global settings */
  const g_autocomplete_query_delay_ms = 300;

  /* Utils */
  function add_page_styles(styles) {
    let tag_styles = document.createElement('style');
    tag_styles.type = 'text/css';
    tag_styles.innerHTML = styles;
    let head = document.getElementsByTagName('head')[0];
    head.appendChild(tag_styles);
  }

  function insert_after(reference_elem, new_elem) {
      reference_elem.parentNode.insertBefore(new_elem, reference_elem.nextSibling);
  }

  function reverse_str(str){
      return [...str].reverse().join("");
  }

  function insert_mid_str(str, i,  j, insert_str) {
    return `${str.slice(0, i)}${insert_str}${str.slice(j, str.length)}`;
  }

  function legacy_copy_to_clipboard(text) {
    let textarea = document.createElement("textarea");
    textarea.classList.add("shimmieext-legacy-clipboard-textarea");
    textarea.value = text;

    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();
    try {
      document.execCommand("copy");
    } catch (e) {}
    document.body.removeChild(textarea);
  }

  function copy_to_clipboard(text) {
    try {
      GM_setClipboard(text);
    } catch(e) {
      if(navigator.clipboard) {
        navigator.clipboard.writeText(text);
      } else {
        legacy_copy_to_clipboard(text);
      }
    }
  }

  const KEYCODE_TAB = 9;
  const KEYCODE_ENTER = 13;
  const KEYCODE_ARROW_LEFT = 37;
  const KEYCODE_ARROW_UP = 38;
  const KEYCODE_ARROW_RIGHT = 39;
  const KEYCODE_ARROW_DOWN = 40;

  /* Autocomplete elements */
  class AutocompleteDropdown {
    constructor() {
      this.node = document.createElement("ul");
      this.node.classList.add("shimmieext-autocomplete");
      this.node.tabIndex = "-1";
      let _this = this;
      this.node.addEventListener("keydown", function(e) {
        let key_code = e.keyCode || e.which;
        switch(key_code) {
          case KEYCODE_ARROW_UP:
            _this.focus_prev();
            e.preventDefault();
            break;
          case KEYCODE_ARROW_DOWN:
            _this.focus_next();
            e.preventDefault();
            break;
          case KEYCODE_ARROW_RIGHT:
          case KEYCODE_TAB:
            _this.select_focused();
            e.preventDefault();
        }
      }, false);
      this.items = [];
    }

    focus_prev() {
      let prev_li = document.activeElement.previousSibling;
      if(prev_li) {
        prev_li.focus();
      }
    }

    focus_next() {
      let next_li = document.activeElement.nextSibling;
      if(next_li) {
        next_li.focus();
      }
    }

    set_focus(i) {
      let li = this.items[i];
      if(li) {
        li.focus();
      }
    }

    select_focused() {
      let focused_elem = document.activeElement;
      if(this.items.includes(focused_elem)) {
        focused_elem.click();
      }
    }

    select(i) {
      let li = this.items[i];
      if(li) {
        li.click();
      }
    }

    clear() {
      this.node.replaceChildren();
      this.items = [];
    }

    show() {
      this.node.classList.remove("shimmieext-hidden");
    }

    hide() {
      this.node.classList.add("shimmieext-hidden");
    }

    _add_li(li) {
      this.node.appendChild(li);
      this.items.push(li);
    }

    add_item(tag, count, on_select) {
      let new_li = document.createElement("li");
      new_li.classList.add("shimmieext-autocomplete");
      new_li.innerHTML = `${tag} (${count})`;
      new_li.tabIndex = "0";

      let _this = this;
      let hide_select = function() {
        _this.hide();
        on_select(tag);
      };
      new_li.addEventListener("click", function() {
        hide_select();
      }, false);
      new_li.addEventListener("keydown", function(e) {
        let key_code = e.keyCode || e.which;
        if(key_code === KEYCODE_ENTER) {
          hide_select();
          e.preventDefault();
        }
      }, false);

      this._add_li(new_li);
    }
  }

  class AutocompleteField {
    constructor(elem_input, delay_query, shimmie_api) {
      this.root = elem_input;
      this.delay = delay_query;
      this.api = shimmie_api;
      this.timer = undefined;

      const _this = this;
      this.root.addEventListener("input", function(e) {
        let text = _this.root.value;
        if(text) {
          // Reset delay if another key pressed before finished
          _this.clear_timer();
          _this.timer = setTimeout(function() {
            let cursor_i = e.target.selectionStart;
            // If at end of input or space in front
            let cursor_at_end = text[cursor_i] === undefined;
            if(cursor_at_end || text[cursor_i] === " ") {
              // Get word behind cursor
              let i = cursor_i - 1;
              let c = text[i];
              let word_behind = "";
              while(c !== undefined && c !== " ") {
                word_behind += c;
                i--;
                c = text[i];
              }
              let word_start_i = i + 1;
              if(word_behind) {
                word_behind = reverse_str(word_behind);
                _this.api_get_autocomplete(word_behind, function(res_text) {
                  _this.populate_dropdown(JSON.parse(res_text), word_start_i, cursor_i, cursor_at_end);
                });
              }
            }
          }, _this.delay);
        }
      }, false);

      this.root.addEventListener("keydown", function(e) {
        let key_code = e.keyCode || e.which;
        switch(key_code) {
          case KEYCODE_TAB:
          case KEYCODE_ARROW_DOWN:
          case KEYCODE_ENTER:
            _this.dropdown.set_focus(0);
            _this.clear_timer();
            e.preventDefault();
            break;
        }
      }, false);

      this.dropdown = new AutocompleteDropdown();
      this.dropdown.hide();
      insert_after(this.root, this.dropdown.node);
    }

    clear_timer() {
      if(this.timer) {
        clearTimeout(this.timer);
      }
    }

    populate_dropdown(res_json, insert_start_i, insert_end_i, add_space) {
      this.dropdown.clear();
      let _this = this;
      for (const [k, v] of Object.entries(res_json)) {
        this.dropdown.add_item(k, v, function(selected_tag) {
          _this.root.value = insert_mid_str(_this.root.value, insert_start_i, insert_end_i, selected_tag);
          if(add_space) {
            _this.root.value += " ";
          }
          _this.root.focus();
        });
      }
      this.dropdown.show();
    }

    api_get_autocomplete(query_text, on_load) {
      const xhttp = new XMLHttpRequest();
      const _this = this;
      xhttp.onload = function() {
        on_load(this.responseText);
      };
      xhttp.open("GET", `${_this.api}/api/internal/autocomplete?s=${query_text}`, true);
      xhttp.send();
    }
  }

  /* Upload form autocomplete */
  function page_upload(shimmie_api) {
    let upload_form = document.getElementById("file_upload");
    // Prevent form submit on enter press
    upload_form.addEventListener("keydown", function(e) {
      let key_code = e.keyCode || e.which;
      if(key_code === KEYCODE_ENTER){
        e.preventDefault();
      }
    }, false);

    // Add enter press submit back to post button
    let upload_button = document.getElementById("uploadbutton");
    upload_button.addEventListener("keydown", function(e) {
      let key_code = e.keyCode || e.which;
      if(key_code === KEYCODE_ENTER){
        upload_form.submit();
      }
    }, false);

    let autocomplete_fields = [];
    let autocomplete_inputs = upload_form.getElementsByClassName('autocomplete_tags');
    for (const input_node of autocomplete_inputs) {
      autocomplete_fields.push(new AutocompleteField(input_node, g_autocomplete_query_delay_ms, shimmie_api));
    }

    // Close dropdown on click anywhere else
    document.body.addEventListener("click", function() {
      for (const field of autocomplete_fields) {
        field.dropdown.hide();
      }
    }, false);
  }

  /* Single page copy tags and autocomplete tag editor */
  function page_single_image(shimmie_api) {
    let tag_editor_input = document.getElementById("tag_editor");
    let tag_editor_autocomplete_field = new AutocompleteField(tag_editor_input, g_autocomplete_query_delay_ms, shimmie_api);

    let tags = tag_editor_input.value;
    let info_table = document.querySelector(".image_info tbody");

    let new_tr = document.createElement("tr");
    let new_td = document.createElement("td");
    let new_button = document.createElement("input");

    new_td.colSpan = "4";

    new_button.value = "Copy Tags";
    new_button.type = "button";
    new_button.classList.add("shimmieext-button-copy-tags");
    new_button.addEventListener("click", function() {
      copy_to_clipboard(tags);
    }, false);

    new_td.appendChild(new_button);
    new_tr.appendChild(new_td);
    info_table.appendChild(new_tr);
  }

  function main() {
    let url = window.location.href.split('/');
    let shimmie_api = `${url[0]}//${url[2]}`;

    let page;
    if(url[3] === "post" && url[4] === "view" || url[3] === "random_image" && url[4] === "view") {
      page = page_single_image;
    } else if(url[3] === "upload") {
      page = page_upload;
    }

    if(page) {
      add_page_styles("ul.shimmieext-autocomplete {list-style: none; padding: 2px; margin: 0; display: block; outline: none; color: #444444; border: 1px solid #dddddd; z-index: 100;\
font-size: 1.1em; font-family: Helvetica,Arial,sans-serif; text-align: left; background-color: #ffffff; position: absolute; cursor: pointer;}\
li.shimmieext-autocomplete {list-style: none;}\
li.shimmieext-autocomplete:hover, li.shimmieext-autocomplete:focus {font-weight: bold; background-color: #0a78eb; color: #ffffff; }\
.shimmieext-hidden {display: none !important;}\
.shimmieext-button-copy-tags {cursor: pointer;} .shimmieext-button-copy-tags:active  {background-color:#E9E9ED;}\
.shimmieext-legacy-clipboard-textarea {top: 0; left: 0; position: fixed;}");

      page(shimmie_api);
    }
  }

  main();

})();