SankakuAddon

Adds a few quality of life improvements on Sankaku Channel: Automatic image scaling, muting videos, speaker icons on loud videos, + - tag search buttons, a tag menu which allows for tagging by clicking, 'Choose/Set Parent' modes, duplicate tagging/flagging. Fully configurable using localStorage.

От 22.11.2020. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name        SankakuAddon
// @namespace   SankakuAddon
// @description Adds a few quality of life improvements on Sankaku Channel: Automatic image scaling, muting videos, speaker icons on loud videos, + - tag search buttons, a tag menu which allows for tagging by clicking, 'Choose/Set Parent' modes, duplicate tagging/flagging. Fully configurable using localStorage.
// @include     http://chan.sankakucomplex.com/*
// @include     https://chan.sankakucomplex.com/*
// @include     http://idol.sankakucomplex.com/*
// @include     https://idol.sankakucomplex.com/*
// @run-at      document-start
// @version     0.99.06
// @grant       none
// ==/UserScript==


"use strict";
var version = "v0.99.06";

if (!String.prototype.startsWith) {
  String.prototype.startsWith = function(searchString, position) {
    return this.substr(position || 0, searchString.length) === searchString;
  };
}


/***************************/
/* configuration functions */
/***************************/

var default_config = {
  scroll_to_image:true,
  scale_image:true, //and video
  scale_only_downscale:false,
  scale_flash:false,
  scale_mode:0,
  scale_on_resize:false,
  scroll_to_image_center:true,
  video_pause:false,
  video_mute:true,
  set_video_volume:false,
  video_volume:100,
  video_controls:true,
  show_speaker_icon:true,
  show_animated_icon:true,
  setparent_deletepotentialduplicate:false,
  hide_headerlogo:false,
  tag_search_buttons:true,
  tag_menu:true,
  tag_menu_scale:"30%",
  common_tags_json:"[ {\"name\":\"test tags\", \"tags\":[\"tag1 tag2\", [\"grouped_tag1 grouped_tag2\"], \"tag3 tag4\"] }, { \"tags\":[ \"next_line tag5 tag6\", [\"grouped_tag3 grouped_tag4\"] , \"tag7 tag8\"] }, {\"name\":\"another category\", \"tags\":[\"t1 t2 t3\"]} ]",
  sankaku_channel_dark_compatibility:false
};
var config_value_types = ["scale_mode", "common_tags_json", "video_volume", "tag_menu_scale"];


var use_local_storage = (typeof(Storage) !== "undefined");
var key_prefix = "config.";

var config = JSON.parse(JSON.stringify(default_config));
var dom_content_loaded = false;

function save_config(key, value) {
  if (!use_local_storage) {
    notice("addon: couldn't save setting \"" + key + " = " + value + "\" to local storage. check permissions.");
    return;
  }

  var cfg_key = key_prefix + key;
  try {
    localStorage.setItem(cfg_key, JSON.stringify(value));
  } catch (error) {
    console.log("SankakuAddon: ", error);
    notice("addon: couldn't save setting \"" + key + " = " + value + "\" to local storage, check the console.");
  }
}

function load_config() {
  for (var key in config) {
    if (config.hasOwnProperty(key)) {
      var value = config[key]; //default already loaded

      if (use_local_storage) {
        var cfg_key = key_prefix + key;
        var stored_value = localStorage.getItem(cfg_key);
        if (stored_value) value = JSON.parse(stored_value);
      }

      config_changed(key, value); //fire regardless
    }
  }
}

function local_storage_changed(e) {
  if (e.key.startsWith(key_prefix)) {
    var key = e.key.substring(key_prefix.length);
    var value = JSON.parse(e.newValue);

    config_changed(key, value);
  }
}

//event that is fired whenever a setting changes in the config dialog or the local storage
function config_changed(key, value) {
  config[key] = value;

  if (!dom_content_loaded) return; //UI hasn't loaded yet
  update_config_dialog_by_key(key);
  if (key === "hide_headerlogo") {
    update_headerlogo();
  } else if (key === "scale_on_resize") {
    if (value) add_scale_on_resize_listener();
    else       remove_scale_on_resize_listener();
  }
}

function reset_config() {
  //clear local storage
  if (use_local_storage) {
    for (var key in config) {
      if (config.hasOwnProperty(key)) {
        var cfg_key = key_prefix + key;
        localStorage.removeItem(cfg_key);
      }
    }
  }

  for (var key in config) {
    if (config.hasOwnProperty(key)) {
      config_changed(key, default_config[key]);
    }
  }
}

//calls f(cfg_elem, key, get_value) for each existing config element
function foreach_config_element(f) {
  for (var key in config) {
    if (config.hasOwnProperty(key)) {
      var cfg_key = key_prefix + key;
      var cfg_elem = document.getElementById(cfg_key);
      if (cfg_elem === null) continue;

      (function(cfg_elem) {
        if (config_value_types.indexOf(key) !== -1) {
          f(cfg_elem, key, function() { return cfg_elem.value; });
        } else {
          f(cfg_elem, key, function() { return cfg_elem.checked; });
        }
      })(cfg_elem);
    }
  }
}

function update_config_dialog_by_key(key) {
  var cfg_key = key_prefix + key;
  var cfg_elem = document.getElementById(cfg_key);
  if (cfg_elem !== null) {
    if (config_value_types.indexOf(key) !== -1) {
      cfg_elem.value   = config[key];
    } else {
      cfg_elem.checked = config[key];
    }
  }
}

function update_config_dialog() {
  for (var key in config) {
    if (config.hasOwnProperty(key)) {
      update_config_dialog_by_key(key);
    }
  }
}

function update_headerlogo() {
  hide_headerlogo(config.hide_headerlogo);
}

function show_config_dialog(bool) {
  document.getElementById("cfg_dialog").style.display = (bool ? "" : "none");
}


/********************/
/* helper functions */
/********************/

function get_scrollbar_width() { //from Stack Overflow
  var outer = document.createElement("DIV");
  outer.style.visibility = "hidden";
  outer.style.width = "100px";
  document.body.appendChild(outer);

  var widthNoScroll = outer.offsetWidth;

  outer.style.overflow = "scroll"; //force scrollbars

  var inner = document.createElement("DIV");
  inner.style.width = "100%";
  outer.appendChild(inner);

  var widthWithScroll = inner.offsetWidth;

  outer.parentNode.removeChild(outer);

  return widthNoScroll - widthWithScroll;
}

//"rgb(r,g,b)" -> [int(r), int(g), int(b)]
function rgb_to_array(rgb) {
  var arr = rgb.substring(rgb.indexOf('(') + 1, rgb.lastIndexOf(')')).split(/,\s*/);
  for (var i = 0; i < arr.length; i++)
    arr[i] = parseInt(arr[i]);
  return arr;
}

function rgb_array_is_dark(rgb_array) {
  var avg = 0;
  for (var i = 0; i < rgb_array.length; i++)
    avg += rgb_array[i];
  avg /= rgb_array.length;
  return avg <= 128;
}

function rgb_array_shift(rgb, shift) {
  var shifted = [];
  for (var i = 0; i < 3; i++)
    shifted[i] = Math.min(Math.max(rgb[i] + shift, 0), 255);
  return shifted;
}

//[r, g, b] -> "rgb(r,g,b)"
function rgb_array_to_rgb(rgb) {
  if (rgb.length === 3)
    return "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] +")";
  return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + rgb[3] +")";
}

//inefficient helper
function is_darkmode() {
    var rgb = rgb_to_array(window.getComputedStyle(document.body, null).getPropertyValue("background-color"));
    return rgb_array_is_dark(rgb);
}

//helper function to adjust background colors based on light or dark mode
function shifted_backgroundColor(shift) {
  var rgb = rgb_to_array(window.getComputedStyle(document.body, null).getPropertyValue("background-color"));
  var darkmode = rgb_array_is_dark(rgb);
  var shifted_rgb = rgb_array_shift(rgb, (darkmode ? 1 : -1) * shift);
  return rgb_array_to_rgb(shifted_rgb);
}

//helper function to modify nodes on creation
function register_observer(node_predicate, node_modifier) {
  var observer = new MutationObserver(function(mutations) {
    for (var i = 0; i < mutations.length; i++) {
      var added_nodes = mutations[i].addedNodes;
      for (var j = 0; j < added_nodes.length; j++) {
        var node = added_nodes[j];
        if (node_predicate(node)) {
          if (node_modifier(node, observer)) { //are we done?
            observer.disconnect();
            return;
          }
        }
      }
    }
  });
  observer.observe(document, {childList: true, subtree: true});
  return observer;
}


/*********************/
/* general functions */
/*********************/

var header_offset_height = null; //distance needed to scroll if headerlogo is hidden/shown


function hide_headerlogo(hide) {
  var headerlogo = document.getElementById("headerlogo");
  if (headerlogo === null) return;

  var visible = (headerlogo.style.display !== "none");

  headerlogo.style.display = (hide ? "none" : "");

  //scroll if needed
  if (visible === !!hide) window.scrollBy(0, (visible && hide ? -1 : 1) * header_offset_height);
}

function add_config_dialog() {
  var cfg_dialog = document.createElement("div");
  cfg_dialog.id = "cfg_dialog";
  cfg_dialog.style.display = "none";
  cfg_dialog.style.border = "1px solid " + shifted_backgroundColor(32);
  cfg_dialog.style.top = "50%";
  cfg_dialog.style.transform = "translateY(-50%)";
  cfg_dialog.style.width = "450px";
  cfg_dialog.style.height = "470px";
  cfg_dialog.style.position = "fixed";
  cfg_dialog.style.left = "0";
  cfg_dialog.style.right = "0";
  cfg_dialog.style.margin = "0 auto";
  cfg_dialog.style.overflow = "auto";
  cfg_dialog.style.backgroundColor = window.getComputedStyle(document.body, null).getPropertyValue("background-color");
  cfg_dialog.style.zIndex = "10002";

  cfg_dialog.innerHTML = ""
    + "<div style='display: table; position: absolute; height: 100%; width: 100%'>"
    + "<div style='display: table-cell; vertical-align: middle'>"
    + "<div style='padding: 8px; margin-left: auto; margin-right: auto;'>"
    +   "<b>Sankaku Addon "+version+"</b><br>"
    +   "<hr style='margin-top: 0; margin-bottom: 2px; border:1px solid " + shifted_backgroundColor(32) + ";'>"
    +   "<span>"
    +     "<input id='" + key_prefix + "scroll_to_image' type='checkbox'>"
    +     "<span>Scroll to image/video when opening post</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "scroll_to_image_center' type='checkbox'>"
    +     "<span>Scroll to center of image/video, else scroll to top</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "scale_image' type='checkbox'>"
    +     "<span>Scale image/video when opening post</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "scale_only_downscale' type='checkbox'>"
    +     "<span>Only downscale</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "scale_flash' type='checkbox'>"
    +     "<span>Also scale flash videos</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "scale_on_resize' type='checkbox'>"
    +     "<span title=\"This uses the 'scale image mode' setting, so it doesn't work well when using the manual scaling actions.\" style='cursor:help'>Scale image on window resize</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<span>Scale image/video mode: </span>"
    +     "<select id='" + key_prefix + "scale_mode'>"
    +       "<option value=0>Fit to window</option>"
    +       "<option value=1>Fit horizontally</option>"
    +       "<option value=2>Fit vertically</option>"
    +     "</select>"
    +     "<br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "video_pause' type='checkbox'>"
    +     "<span>Pause (non-flash) videos*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "video_mute' type='checkbox'>"
    +     "<span>Mute (non-flash) videos*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "set_video_volume' type='checkbox'>"
    +     "<span>Set (non-flash) video volume to: </span>"
    +     "<input id='" + key_prefix + "video_volume' type='number' min='0' max='100' size='4'>%"
    +     "<br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "video_controls' type='checkbox'>"
    +     "<span>Show video controls*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "tag_search_buttons' type='checkbox'>"
    +     "<span>Show + - tag search buttons*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "show_speaker_icon' type='checkbox'>"
    +     "<span>Show 🔊 icon on thumbnail if it has audio*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "show_animated_icon' type='checkbox'>"
    +     "<span>Show ⏩ icon on thumbnail if it is animated (🔊 overrides ⏩)*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "setparent_deletepotentialduplicate' type='checkbox'>"
    +     "<span>Delete potential_duplicate tag when using \"Set Parent\"</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "hide_headerlogo' type='checkbox'>"
    +     "<span>Hide header logo</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "sankaku_channel_dark_compatibility' type='checkbox'>"
    +     "<span>Galinoa's Sankaku Channel Dark compatibilty*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "tag_menu' type='checkbox'>"
    +     "<span>Activate tag menu*:</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "common_tags_json'>"
    +     "<span> Common tags list (JSON format)*</span><br>"
    +   "</span>"
    +   "<button id='config_close' style='cursor: pointer'>Close</button>"
    +   "<button id='config_reset' style='cursor: pointer'>Reset settings</button>"
    +   "<span>&nbsp;*requires a page reload.</span>"
    + "</div>"
    + "";
  document.body.appendChild(cfg_dialog);

  //add events
  document.getElementById("config_close").onclick = function() { show_config_dialog(false); return false; };
  document.getElementById("config_reset").onclick = function() { reset_config(); return false; };
  foreach_config_element(function(cfg_elem, key, get_value) {
    cfg_elem.addEventListener("change", function() {
      config_changed(key, get_value());
      save_config(key, get_value());
    });
  });
}

function add_config_button() {
  var navbar = document.getElementById("navbar");
  if (navbar === null) {
    notice("addon error: couldn't find \"navbar\" element! Config dialog disabled.");
    return;
  }

  navbar.style.whiteSpace = "nowrap"; //hack to fit config button

  var a = document.createElement("A");
  a.href = "#";
  a.onclick = function() { show_config_dialog(true); return false; };
  a.innerHTML = "<span style='font-size: 110%'>⚙</span> Addon config";
  a.style.fontSize = "120%";

  var li = document.createElement("LI");
  li.className = "lang-select"; //match style of top bar
  li.appendChild(a);
  navbar.appendChild(li);
}

function add_tag_search_buttons() {
  var tagsidebar = document.getElementById("tag-sidebar");
  if (tagsidebar === null) return;

  var items = tagsidebar.getElementsByTagName("LI");
  for (var i = 0; i < items.length; i++) {
    var taglink = items[i].getElementsByTagName("A")[0];
    var tagname = taglink.innerHTML.replace(/ /g, "_"); // " " -> "_" hopefully this is the only edgecase

    //generates onclick events
    var tag_search_button_func = function(tagname) {
      return function() {
        var search_field = document.getElementById("tags");

        var search_tags = search_field.value.trim().split(/\s+/);
        var tag_index = search_tags.indexOf(tagname);

        //add if missing, remove if existing
        if (tag_index === -1) {
          search_field.value = (search_field.value + " " + tagname).trim();
        } else {
          search_tags.splice(tag_index, 1);
          search_field.value = search_tags.join(" ");
        }

        search_field.focus({preventScroll: true});

        return false;
      };
    };

    var a = document.createElement("A");
    a.href = "#";
    a.innerHTML = "+";
    a.onclick = tag_search_button_func(tagname);

    items[i].insertBefore(a, taglink);
    items[i].insertBefore(document.createTextNode(" "), taglink);

    a = document.createElement("A");
    a.href = "#";
    a.innerHTML = "-";
    a.onclick = tag_search_button_func("-" + tagname);

    items[i].insertBefore(a, taglink);
    items[i].insertBefore(document.createTextNode(" "), taglink);
  }
}

function add_speaker_icons(root) {
  if (root === null) return;

  var elems = root.getElementsByTagName("SPAN");
  for (var i = 0; i < elems.length; i++) {
    if (elems[i].classList.contains("thumb")) {
      add_speaker_icon(elems[i]);
    }
  }
}

function add_speaker_icon(thumb_span) {
  var img = thumb_span.querySelector(".preview");
  if (img === null) return;
  var a = thumb_span.getElementsByTagName("A");
  if (a.length === 0) return;

  var icon = document.createElement("SPAN");
  var tags = img.title.trim().split(/\s+/);

  if (config.show_speaker_icon && (tags.indexOf("has_audio") !== -1)) {
    icon.innerHTML = "🔊";
  } else if (config.show_animated_icon && (tags.indexOf("animated") !== -1 || tags.indexOf("video") !== -1)) {
    icon.innerHTML = "⏩";
  } else {
    return;
  }

  icon.className = "speaker_icon";
  icon.style.color = "#666";
  icon.style.position = "absolute";
  icon.style.top   = "2px"; //account for border
  icon.style.right = "2px";
  icon.style.fontSize = "200%";
  icon.style.textShadow = "-1px 0 white, 0 1px white, 1px 0 white, 0 -1px white";
  icon.style.transform = "translateX(50%) translateY(-50%)";

  a[0].style.display = "inline-block"; //makes the element fit its content
  a[0].style.position = "relative";
  a[0].appendChild(icon);
}

function add_speaker_icons_observer(predicate) {
  if (config.show_speaker_icon || config.show_animated_icon) { //don't hog CPU when disabled, but requires page reload to activate
    //this might observe recommendations too early, so add missing thumbnail icons in DOMContentLoaded
    register_observer(predicate, function (node, observer) {
      add_speaker_icons(node);
      return false; //listen forever
    });
  }
}

function configure_video(node) {
  if (node === null || node.nodeType !== Node.ELEMENT_NODE || node.tagName !== "VIDEO") return;

  if (config.video_pause)      node.pause();
  if (config.set_video_volume) node.volume = config.video_volume / 100.0;
  if (config.video_mute)       node.muted = true;
  if (config.video_controls)   node.controls = true;
}


/***********************************************/
/* main page / visually similar page functions */
/***********************************************/

var added_mode_options = false;
var call_postmodemenu_change = false;
function add_mode_options(mode_dropdown) {
  if (added_mode_options) return;
  added_mode_options = true;

  var option;
  if (mode_dropdown.options.namedItem("choose-parent") === null) {
    option = document.createElement("option");
    option.text = "Choose Parent";
    option.value = "choose-parent";
    mode_dropdown.add(option);
  }
  if (mode_dropdown.options.namedItem("set-parent") === null) {
    option = document.createElement("option");
    option.text = "Set Parent";
    option.value = "set-parent";
    mode_dropdown.add(option);
  }

  //add_mode_options() was called late
  if (call_postmodemenu_change) {
    PostModeMenu.change(); //guarantee that 'mode' variable correctly changes to new modes when loading page
  }
}


/***********************/
/* post page functions */
/***********************/

var post_parent_id = null; //input elem
//original post/parent ids
var post_id = null;
var parent_id = null;
//original image size
var img_elem = null; //image or video
var non_img_elem = null, emb_elem = null; //flash or unknown
var img_is_flash;
var img_width = null;
var img_height = null;
var img_aspect_ratio = null;
var img_current_height = null; //store current height separately, because flash is a bitch
var resize_timer;
var tag_update_timer;
//set by find_actions_list():
var actions_ul = null;
var found_delete_action = false;
var mouse_moved = false; //for tag_menu_scaler

function tag_menu_scaler_mousedown(e) {
  e.preventDefault();
  mouse_moved = false;
  window.addEventListener("mousemove", tag_menu_scaler_mousemove);
  window.addEventListener("mouseup",   tag_menu_scaler_mouseup);
}

function tag_menu_scaler_mousemove(e) {
  e.preventDefault();
  mouse_moved = true;
  set_tag_menu_scale(e, false);
}

function tag_menu_scaler_mouseup(e) {
  e.preventDefault();
  if (mouse_moved)
    set_tag_menu_scale(e, true);

  window.removeEventListener("mousemove", tag_menu_scaler_mousemove);
  window.removeEventListener("mouseup",   tag_menu_scaler_mouseup);
}

function set_tag_menu_scale(e, save) {
  var tag_menu = document.getElementById("tag_menu");
  if (tag_menu === null) return;

  var yFromBottom = window.innerHeight - e.clientY;
  var yPercentfromBottom = (100.0 * (yFromBottom / window.innerHeight));
  yPercentfromBottom = Math.min(Math.max(yPercentfromBottom, 5), 95) + "%";

  tag_menu.style.height = yPercentfromBottom;

  if (save)
    save_config("tag_menu_scale", yPercentfromBottom);
}

function add_tag_menu() {
  if (document.getElementById("post_tags") === null) return; //not logged in

  var tag_menu = document.createElement("DIV");
  tag_menu.id = "tag_menu";
  tag_menu.style.display = "none";
  tag_menu.style.width  = "100%";
  tag_menu.style.height = config.tag_menu_scale;
  tag_menu.style.position = "fixed";
  tag_menu.style.bottom = "0";
  tag_menu.style.overflow = "auto";
  tag_menu.style.backgroundColor = window.getComputedStyle(document.body, null).getPropertyValue("background-color");
  tag_menu.style.zIndex = "10001";
  document.body.appendChild(tag_menu);

  //the inner div ensures tag_menu_close button doesn't scroll with the content
  tag_menu.innerHTML = "<div style='width: calc(100% - 2px); height: 100%; overflow: auto; margin-left: 2px'>" + "<span id='common_tags'></span>" + "<br>current tags:<br>" + "<span id='current_tags'></span></div>";

  var tag_menu_scaler = document.createElement("DIV");
  tag_menu_scaler.id = "tag_menu_scaler";
  tag_menu_scaler.style.width  = "100%";
  tag_menu_scaler.style.height = "6px";
  tag_menu_scaler.style.backgroundColor = shifted_backgroundColor(32);
  tag_menu_scaler.style.position = "absolute";
  tag_menu_scaler.style.top = "0";
  tag_menu_scaler.style.cursor = "ns-resize";
  tag_menu_scaler.style.zIndex = "10000";
  tag_menu_scaler.addEventListener("mousedown", tag_menu_scaler_mousedown);
  tag_menu.appendChild(tag_menu_scaler);
  tag_menu.style.paddingTop = tag_menu_scaler.style.height; //since tag_menu_scaler floats above the tags

  var generate_tag_menu_button = function(id, text) {
    var button = document.createElement("DIV");
    button.id = id;
    button.style.border = "1px solid " + shifted_backgroundColor(32);
    button.style.width  = "24px";
    button.style.height = "24px";
    button.style.position = "absolute";
    button.style.textAlign = "center";
    button.style.cursor = "pointer";
    button.style.backgroundColor = shifted_backgroundColor(16);
    button.innerHTML = "<span style='width: 100%; display: block; position: absolute; top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%);'>" + text + "</span>";
    button.style.zIndex = "10001";
    return button;
  };

  var tag_menu_close = generate_tag_menu_button("tag_menu_close", "X");
  tag_menu_close.style.right = "0";
  tag_menu_close.style.top = "0";
  tag_menu_close.onclick = function() { show_tag_menu(false); return false; };
  tag_menu.appendChild(tag_menu_close);

  var tag_menu_open = generate_tag_menu_button("tag_menu_open", "«");
  tag_menu_open.style.position = "fixed";
  tag_menu_open.style.right = "0";
  tag_menu_open.style.bottom = "0";
  tag_menu_open.onclick = function() { show_tag_menu(true); update_tag_menu(); return false; };
  document.body.appendChild(tag_menu_open);

  var tag_menu_save = generate_tag_menu_button("tag_menu_save", "Save changes");
  tag_menu_save.style.right = "36px";
  tag_menu_save.style.width  = "140px";
  tag_menu_save.style.top = "0";
  tag_menu_save.style.fontWeight = "bold";
  tag_menu_save.onclick = function() {
    var form = document.getElementById("edit-form");
    if (form !== null)
      form.submit();
    return false;
  };
  tag_menu.appendChild(tag_menu_save);
}

function update_tag_menu() {
  if (document.getElementById("post_tags") === null) return; //not logged in

  var common_tags_elem  = document.getElementById("common_tags");
  var current_tags_elem = document.getElementById("current_tags");

  //tag menu disabled
  if (common_tags_elem === null || current_tags_elem === null)
    return;

  var generate_tag_button = function(tag, margin = "3px") {
    var a = document.createElement("A");
    a.href = "#";
    a.style.paddingLeft  = "5px";
    a.style.paddingRight = "5px";
    a.style.borderStyle = "solid";
    a.style.borderWidth = "1px";
    a.style.backgroundColor = (is_darkmode() ? "#000" : "#FFF"); //more contrast for tag buttons
    a.className = (tag_is_present(tag) ? "" : "tag_nonexistent");

    a.onclick = function(tag, a) {
      return function() {
        if (tag_is_present(tag)) {
          remove_tag(tag);
          a.className = "tag_nonexistent";
        } else {
          add_tag(tag);
          a.className = "";
        }
        return false;
      };
    }(tag, a);
    a.innerHTML = tag;

    var div = document.createElement("DIV");
    div.style.margin = margin;
    div.style.float = "left";
    div.appendChild(a);
    return div;
  };

  var generate_top_level_li = function(common_margin = "3px") {
    var li = document.createElement("LI");
    li.style.marginLeft   = common_margin;
    li.style.marginTop    = common_margin;
    li.style.marginRight  = common_margin;
    li.style.marginBottom = common_margin;
    li.style.float = "left";
    return li;
  };


  var create_tag_list = function() {
    var ul = document.createElement("UL");
    ul.style.listStyleType = "none";
    ul.style.margin  = "0";
    ul.style.padding = "0";
    ul.style.display = "inline-block";
    return ul;
  };

  //generate tag button list for current tags
  var current_tags_ul = create_tag_list();
  var current_tags = get_tags_array();
  for (var i = 0; i < current_tags.length; i++) {
    var li = generate_top_level_li();
    li.appendChild(generate_tag_button(current_tags[i], "0"));
    current_tags_ul.appendChild(li);
  }

  //replace current list with new one
  while (current_tags_elem.hasChildNodes()) {
    current_tags_elem.removeChild(current_tags_elem.lastChild);
  }
  current_tags_elem.appendChild(current_tags_ul);

  //now add common tags
  //common_tags_json should be an array of objects with an optional string "name" field and an array "tags" field,
  //where the "tags" array can contain strings (space separated tags), arrays containing one string (representing a group)
  //or arrays of array containing one string (representing a table)
  //ex. [ { "name":"common tags", "tags":[ "tag1 tag2", ["grouped_tag1 grouped_tag2"] , "tag3 tag4"] }, { "name":"uncommon tags", "tags":[ "t1 t2 t3" ]} ]
  var tag_data;
  try {
    tag_data = JSON.parse(config.common_tags_json);
  } catch (error) {
    notice("addon error: \"common tags\" JSON syntax error");
    return;
  }

  if (!Array.isArray(tag_data)) {
    notice("addon error: \"common tags\" needs to be an array of objects");
    return;
  }

  while (common_tags_elem.hasChildNodes()) {
    common_tags_elem.removeChild(common_tags_elem.lastChild);
  }

  for (var k = 0; k < tag_data.length; k++) {
    var list_ul = create_tag_list();
    var list_name = tag_data[k].name;
    var list_tags = tag_data[k].tags;

    if (!Array.isArray(list_tags)) {
      notice("addon error: a \"common tags\" object needs to have a \"tags\" array");
      return;
    }

    var TAGS_TYPES = {
      LIST:  'list',  //e.g. "tag1 tag2"
      GROUP: 'group', //e.g. ["tag1 tag2"]
      TABLE: 'table'  //e.g. [["tag1 tag2"], ["tag3 tag4"]]
    };

    var group_style = function(el) {
      //red in darkmode needs more contrast
      var rgb = rgb_to_array(window.getComputedStyle(document.body, null).getPropertyValue("background-color"));
      var darkmode = rgb_array_is_dark(rgb);
      if (darkmode) {
        el.style.border = "1px solid " + rgb_array_to_rgb(rgb_array_shift(rgb, 96));
        el.style.backgroundColor = rgb_array_to_rgb(rgb_array_shift(rgb, 64));
      } else {
        el.style.border = "1px solid " + rgb_array_to_rgb(rgb_array_shift(rgb, -64));
        el.style.backgroundColor = rgb_array_to_rgb(rgb_array_shift(rgb, -32));
      }
    }

    for (var t = 0; t < list_tags.length; t++) {
      var is_array = Array.isArray(list_tags[t]);

      //find tags_type
      var tags_type;
      if (is_array) {
        if (list_tags[t].length === 0) {
          notice("addon error: \"common tags\" \"tags\" array contains an empty array");
          return;
        }

        //check what the array consists of
        var all_arrays = true;
        var no_arrays = true;
        for (var i = 0; i < list_tags[t].length; i++) {
          if (!Array.isArray(list_tags[t][i])) {
            all_arrays = false;
          } else {
            no_arrays = false;
          }
        }

        if (all_arrays) {
          tags_type = TAGS_TYPES.TABLE;
        } else if (no_arrays) {
          tags_type = TAGS_TYPES.GROUP;
        } else {
          notice("addon error: \"common tags\" \"tags\" array contains an array which is neither a group nor a table");
          return;
        }
      } else {
        tags_type = TAGS_TYPES.LIST;
      }

      if (tags_type === TAGS_TYPES.TABLE) {
        var tags_table = [];
        for (var j = 0; j < list_tags[t].length; j++) {
          if (list_tags[t][j].length !== 1) {
            notice("addon error: \"common tags\" \"tags\" array contains a table entry with not exactly 1 tags string");
            return;
          }

          tags_table.push(list_tags[t][j][0].trim().split(/\s+/));
        }

        var table_height = tags_table.length;
        var table_width = 0;
        for (var row = 0; row < tags_table.length; row++)
          table_width = Math.max(table_width, tags_table[row].length);

        //<ul><li><table><tr><td><div (button)>
        var table = document.createElement("TABLE");
        group_style(table);
        table.style.marginBottom = "0";
        for (var row = 0; row < table_height; row++) {
          var tr = document.createElement("TR");
          for (var col = 0; col < table_width; col++) {
            var td = document.createElement("TD");
            td.style.border = "none";
            td.style.padding = "0";
            if (tags_table[row][col])
              td.appendChild(generate_tag_button(tags_table[row][col], "2px"));
            tr.appendChild(td);
          }
          table.appendChild(tr);
        }

        var li = generate_top_level_li();
        li.appendChild(table);
        list_ul.appendChild(li);
      } else if (tags_type === TAGS_TYPES.GROUP) {
        if (list_tags[t].length !== 1) {
          notice("addon error: \"common tags\" \"tags\" array contains a group with not exactly 1 tags string");
          return;
        }

        var tags = list_tags[t][0].trim().split(/\s+/);

        //<ul><li><div><div (button)>
        var div = document.createElement("DIV");
        div.style.display = "inline-block";
        group_style(div);

        for (var i = 0; i < tags.length; i++)
          div.appendChild(generate_tag_button(tags[i]));

        var li = generate_top_level_li();
        li.appendChild(div);
        list_ul.appendChild(li);
      } else /* if (tags_type === tag_types.LIST) */ {
        //<ul><li><div (button)>
        var tags = list_tags[t].trim().split(/\s+/);
        for (var i = 0; i < tags.length; i++) {
          var li = generate_top_level_li();
          li.appendChild(generate_tag_button(tags[i], "3px 0 0 0"));
          list_ul.appendChild(li);
        }
      }
    }

    var span = document.createElement("SPAN");
    span.innerHTML = (k !== 0 ? "<br>" : "") + (list_name ? list_name + ":<br>" : "");
    common_tags_elem.appendChild(span);
    common_tags_elem.appendChild(list_ul);
  }

}

function show_tag_menu(bool) {
  document.getElementById("tag_menu").style.display = (bool ? "" : "none");
  document.getElementById("tag_menu_open").style.display = (!bool ? "" : "none");
}

function add_tag_menu_change_listener() {
  var post_tags_area = document.getElementById("post_tags");
  if (post_tags_area !== null) {
    post_tags_area.addEventListener("change", function() {
      clearTimeout(tag_update_timer);
      tag_update_timer = setTimeout(function() {
        updated_tags();
      }, 500);
    });
  }
}

function find_actions_list() {
  var li = document.getElementById("add-to-pool");
  if (li === null) return;

  actions_ul = li.parentElement;

  var action_links = actions_ul.getElementsByTagName("A");
  for (var i = 0; i < action_links.length; i++) {
    if (action_links[i].innerHTML === "Delete") {
      found_delete_action = true;
      break;
    }
  }
}

function add_addon_actions() {
  if (actions_ul === null) return;

  var separator = document.createElement("H5");
  separator.innerHTML = "Addon actions";
  var newli = document.createElement("LI");
  newli.appendChild(separator);
  actions_ul.appendChild(newli);

  var add_action = function(func, name, id) {
    var a = document.createElement("A");
    a.href = "#";
    a.onclick = function() {func(); return false;};
    a.innerHTML = name;

    var newli = document.createElement("LI");
    newli.id = id;
    newli.appendChild(a);
    actions_ul.appendChild(newli);
  };

  add_action(function() { scale_image( 0, true); scroll_to_image(config.scroll_to_image_center); }, "Fit image",              "scale-image-fit");
  add_action(function() { scale_image( 1, true); scroll_to_image(config.scroll_to_image_center); }, "Fit image (Horizontal)", "scale-image-hor");
  add_action(function() { scale_image( 2, true); scroll_to_image(config.scroll_to_image_center); }, "Fit image (Vertical)",   "scale-image-ver");
  add_action(function() { scale_image(-1, true); scroll_to_image(config.scroll_to_image_center); }, "Reset image size",       "reset-image");

  if (parent_id === null) return; //not logged in

  if (post_id === null) {
    notice("addon error: couldn't find \"hidden_post_id\" element! Flag duplicate feature disabled.");
    return;
  }

  add_action(function() { flag_duplicate(post_id, "");                       }, "Flag duplicate",              "flag-duplicate");
  add_action(function() { flag_duplicate(post_id, ", visually identical");   }, "Flag duplicate (identical)",  "flag-duplicate-identical");
  add_action(function() { flag_duplicate(post_id, " with worse quality");    }, "Flag duplicate (quality)",    "flag-duplicate-quality");
  add_action(function() { flag_duplicate(post_id, " with worse resolution"); }, "Flag duplicate (resolution)", "flag-duplicate-resolution");
}

function add_tag_buttons() {
  var edit_form = document.getElementById("edit-form");
  if (edit_form === null) return; //not logged in

  var button_place = edit_form.children[1].children[0].children[0].children[0];

  button_place.setAttribute("nowrap", "nowrap"); //hack to keep buttons from wrapping (not HTML5 conform, should use CSS)

  var el = document.createElement("BUTTON");
  el.id = "clear_parent_id_button";
  el.style.margin = "0 3px 0 6px";
  el.innerHTML = "Clear";
  el.onclick = function() { post_parent_id.clear(); return false;};
  post_parent_id.parentNode.appendChild(el);

  el = document.createElement("BUTTON");
  el.id = "reset_parent_id_button";
  el.style.margin = "0 3px";
  el.innerHTML = "Reset";
  el.onclick = function() { reset_parent_id(); return false;};
  post_parent_id.parentNode.appendChild(el);

  el = document.createElement("BUTTON");
  el.id = "tag_reset_button";
  el.style.margin = "0 3px 0 6px";
  el.innerHTML = "Reset";
  el.onclick = function() { reset_tags(); return false; };
  button_place.appendChild(el);

  el = document.createElement("BUTTON");
  el.id = "tag_dup_button";
  el.style.margin = "0 3px";
  button_place.appendChild(el);

  el = document.createElement("BUTTON");
  el.id = "tag_var_button";
  el.style.margin = "0 3px";
  button_place.appendChild(el);

  el = document.createElement("BUTTON");
  el.id = "tag_pot_button";
  el.style.margin = "0 3px";
  button_place.appendChild(el);
}

function update_tag_buttons() {
  var taglist = document.getElementById("post_tags");
  var dup_button = document.getElementById("tag_dup_button");
  var var_button = document.getElementById("tag_var_button");
  var pot_button = document.getElementById("tag_pot_button");

  if (taglist === null || dup_button === null || var_button === null || pot_button === null)
    return;

  var tags = get_tags_array();

  if (tags.indexOf("duplicate") === -1) {
    dup_button.onclick = function() {add_tag("duplicate"); return false;};
    dup_button.innerHTML = "Tag duplicate";
  } else {
    dup_button.onclick = function() {remove_tag("duplicate"); return false;};
    dup_button.innerHTML = "Untag duplicate";
  }

  if (tags.indexOf("legitimate_variation") === -1) {
    var_button.onclick = function() {add_tag("legitimate_variation"); return false;};
    var_button.innerHTML = "Tag legitimate_variation";
  } else {
    var_button.onclick = function() {remove_tag("legitimate_variation"); return false;};
    var_button.innerHTML = "Untag legitimate_variation";
  }

  pot_button.innerHTML = "Untag potential_duplicate";
  if (tags.indexOf("potential_duplicate") === -1) {
    pot_button.disabled = true;
  } else {
    pot_button.onclick = function() {remove_tag("potential_duplicate"); return false;};
    pot_button.disabled = false;
  }
}

function reset_parent_id() {
  post_parent_id.value = parent_id;
}

function get_old_tags_array() {
  return document.getElementById("post_old_tags").value.trim().split(/\s+/);
}

function get_tags_array() {
  return document.getElementById("post_tags").value.trim().split(/\s+/);
}

function add_tag(tag) {
  var tags = get_tags_array();

  if ((tag === "duplicate" && tags.indexOf("legitimate_variation") !== -1) || (tag === "legitimate_variation" && tags.indexOf("duplicate") !== -1)) {
    notice("addon: cannot tag as duplicate and legitimate_variation at the same time.");
    return;
  }

  if (tags.indexOf(tag) !== -1) {
    notice("addon: tag already present.");
    return;
  }

  document.getElementById("post_tags").value += " " + tag;

  updated_tags();
}

function remove_tag(tag) {
  var tags = get_tags_array();

  for (var i = 0; i < tags.length; i++) {
    if (tags[i] === tag) {
      tags[i] = "";
    }
  }

  document.getElementById("post_tags").value = tags.join(" ").trim();

  updated_tags();
}

function tag_is_present(tag) {
   return get_tags_array().indexOf(tag) !== -1;
}

function reset_tags() {
  document.getElementById("post_tags").value = document.getElementById("post_old_tags").value;
  updated_tags();
}

//event that gets called whenever post_tags changes
function updated_tags() {
  update_tag_buttons();
  update_tag_menu();
}



//flag option with default text
function flag_duplicate(id, reason_suffix) {
  if (parent_id === null) {
    notice("addon: user not logged in");
    return false;
  }

  var current_parent_id = post_parent_id.value;
  if (current_parent_id !== parent_id) {
    notice("addon: parent id was changed but not saved!");
    return false;
  }

  if (!current_parent_id || current_parent_id.length === 0) {
    notice("addon: no parent id set!");
    return false;
  }

  var tags = get_tags_array();
  var old_tags = get_old_tags_array();
  if (tags.indexOf("duplicate") !== -1 && old_tags.indexOf("duplicate") === -1) {
    notice("addon: duplicate tag set but not saved!");
    return false;
  }
  if (old_tags.indexOf("duplicate") === -1) {
    notice("addon: not tagged as duplicate!");
    return false;
  }

  if (old_tags.indexOf("legitimate_variation") !== -1) {
    notice("addon: tagged as legitimate_variation, are you sure it is a duplicate?");
    return false;
  }

  var reason = prompt("Why should this post be reconsidered for moderation?", "duplicate of " + parent_id + reason_suffix);
  if (reason === null) {
    return false;
  }
  new Ajax.Request("/post/flag.json", {
    parameters: {
      "id": id,
      "reason": reason
    },
    onComplete: function(response) {
      var resp = response.responseJSON;
      if (resp.success) {
        notice("Post was resent to moderation queue");
      } else {
        notice("Error: " + resp.reason);
      }
    }
  });
}


function read_image_data() { //TODO should probably be OOP
  var img = document.getElementById("image"); //image or video
  if (img !== null) {
    img_elem = img;
    img_is_flash = false;

    //workaround for Galinoa's Sankaku Channel Dark: don't read .width/.height attributes but read "Details"
    var res = null;
    var lowres = document.getElementById("lowres");
    if (lowres !== null) {
      res = lowres.innerHTML.split("x"); //parse "<width>x<height>"
    } else {
      var highres = document.getElementById("highres");
      if (highres !== null) {
        res = highres.innerHTML.split(" ")[0].split("x"); //parse "<width>x<height> (<file size>)"
      }
    }

    if (res === null) {
      notice("addon error: image/video resolution not in \"Details\"?! Disabled scaling.");
      return;
    }

    if (img_width  === null) img_width  = res[0];
    if (img_height === null) img_height = res[1];
  } else {
    non_img_elem = document.getElementById("non-image-content");
    if (!non_img_elem) return;
    img_elem = non_img_elem.getElementsByTagName("OBJECT")[0];
    emb_elem = non_img_elem.getElementsByTagName("EMBED")[0];
    img_is_flash = (img_elem !== null && emb_elem !== null);

    img = img_elem; //object contains width/height

    //save original image size
    if (img_width  === null) img_width  = img.width;
    if (img_height === null) img_height = img.height;
  }

  img_aspect_ratio = img_width / img_height;
  img_current_height = img_height;
}

//stretch image/video/flash, requires data from read_image_data()
function scale_image(mode, always_scale) {
  var img_rect_w, img_rect_h;
  var new_width, new_height;

try {
  mode = Number(mode);
  if (isNaN(mode)) {
    notice("addon error: scaling mode wasn't a number?");
    return;
  }

  if (img_width === null || img_height === null) //read_image_data() failed
    return;

  if (!always_scale && (!config.scale_flash && img_is_flash))
    return;

  //reset image size
  if (mode === -1) {
    if (!img_is_flash) {
      img_elem.style.width  = null;
      img_elem.style.height = null;
    } else {
      img_elem.width  = img_width;
      img_elem.height = img_height;
      emb_elem.width  = img_width;
      emb_elem.height = img_height;
    }
    img_current_height = img_height;

    //workaround for Galinoa's Sankaku Channel Dark
    if (config.sankaku_channel_dark_compatibility) {
      img_elem.style.paddingLeft = "";
      note_fix();
    }

    return;
  }

  var left_side;

  //workaround for Galinoa's Sankaku Channel Dark
  //problem: seems to only work for bigger windows
  if (config.sankaku_channel_dark_compatibility) {
    var sidebar = document.getElementsByClassName("sidebar")[0];
    left_side = (sidebar.getBoundingClientRect().right + 12);
    img_elem.style.paddingLeft = left_side + "px"; //don't hide behind sidebar
  } else {
    left_side = img_elem.getBoundingClientRect().left;
  }

  //target rect
  img_rect_w = Math.max(window.innerWidth - left_side - get_scrollbar_width() - 1, 1);
  img_rect_h = Math.max(window.innerHeight - 1, 1);
  var img_rect_aspect_ratio = img_rect_w / img_rect_h;

  //fit into window
  if (mode === 0) {
    mode = (img_aspect_ratio > img_rect_aspect_ratio ? 1 : 2);
  }

  //horizontal
  if (mode === 1) {
    new_width  = Math.floor(img_rect_w);
    new_height = Math.floor(img_rect_w / img_aspect_ratio);
  //vertical
  } else if (mode === 2) {
    new_width  = Math.floor(img_rect_h * img_aspect_ratio);
    new_height = Math.floor(img_rect_h);
  }

  if (!always_scale && (config.scale_only_downscale && (new_width > img_width || new_height > img_height)))
    return;


  var set_dimensions = function(obj, new_width, new_height) {
    obj.width  = new_width  + "px";
    obj.height = new_height + "px";
  };

  if (img_is_flash) {
    set_dimensions(img_elem, new_width, new_height);
    set_dimensions(emb_elem, new_width, new_height);
  } else {
    set_dimensions(img_elem.style, new_width, new_height);
  }

  img_current_height = new_height;
} catch(error) {
  alert("addon error: scaling failed! please report this error to the author: img rect (" + img_width + ", " + img_height + ") to rect (" + img_rect_w + ", " + img_rect_h + "), new size (" + new_width + ", " + new_height + "), exception " + error);
}
}

function scale_on_resize_helper() {
  clearTimeout(resize_timer);
  resize_timer = setTimeout(function() {
    if (config.scale_on_resize) scale_image(config.scale_mode, false);
  }, 100);
}

function add_scale_on_resize_listener() {
  window.addEventListener("resize", scale_on_resize_helper);
}

function remove_scale_on_resize_listener() {
  window.removeEventListener("resize", scale_on_resize_helper);
}

function scroll_to_image(to_center) {
  if (!img_elem) return;
  var absolute_img_top = (img_is_flash ? non_img_elem : img_elem).getBoundingClientRect().top + window.pageYOffset;
  if (to_center) {
    var top_of_centered_rect = absolute_img_top - (window.innerHeight - img_current_height) / 2;
    window.scrollTo(0, top_of_centered_rect);
  } else {
    window.scrollTo(0, absolute_img_top);
  }
}

//simple note fix for Galinoa's Sankaku Channel Dark (only for default image size)
function note_fix() {
  var note_container = document.getElementById("note-container");
  if (note_container !== null) {
    note_container.style.marginLeft = ((window.innerWidth - img_elem.clientWidth) / 2 - 8) + "px";
  }
}


/******************/
/* document-start */
/******************/

load_config();

/*************************************/
/* main page / visually similar page */
/*************************************/

//skip language codes in pathnames like "/jp/post/show"
var pathname = location.pathname;
if (pathname.indexOf('/', 1) === 3)
  pathname = pathname.substring(3);

if (pathname === "/" || pathname.startsWith("/post/similar")) {

  //try to add new modes right after the "Apply tag script" mode is added to prevent it being reset to "View posts" on page reloads
  //it's possible we are too late to observe its construction, so look for it afterwards immediately
  var observer = register_observer(function(node) {
    return (node.value === "apply-tag-script");
  }, function(node, observer) {
    observer.disconnect();
    add_mode_options(node.parentNode);
    return false;
  });

  var dropdown = document.getElementById("mode");
  if (dropdown !== null) {
    var children = dropdown.childNodes;
    for (var i = 0; i < children.length; i++) {
      if (children[i].value === "apply-tag-script") { //it's already there
        observer.disconnect(); //stop looking for it
        add_mode_options(dropdown);
      }
    }
  }

  //add thumbnail icons for dynamically loaded posts (from auto paging)
  if (config.show_speaker_icon || config.show_animated_icon) {
    add_speaker_icons_observer(function (node) { return (node.classList != null && node.classList.contains("content-page")); });
  }


/*************/
/* post page */
/*************/

} else if (pathname.startsWith("/post/show/")) {

  //mute/pause videos
  var observer = register_observer(function(node) {return node.id === "image"; }, function(node, observer) { configure_video(node); return true; });
  var video = document.getElementById("image");
  if (video !== null) {
    observer.disconnect();
    configure_video(video);
  }

  add_speaker_icons_observer(function (node) { return (node.id === "recommendations"); });

/*************/
/* user page */
/*************/

} else if (pathname.startsWith("/user/show/")) {

  add_speaker_icons_observer(function (node) { return (node.id === "recommendations"); });

}




/******************/
/* content-loaded */
/******************/

function init() {
  dom_content_loaded = true;

  //sitefix for flagged posts not always showing red border
  //problem: "flagged" style is defined before "has-parent" and "has-children" CSS styles, so these two take priority
  //fix: just add another copy of the "flagged" style at the end
  var sheet = document.createElement("style");
  sheet.innerHTML  = "img.has-children {padding:0px;border:2px solid #A7DF38;} img.has-parent {padding:0px;border:2px solid #CCCC00;} img.flagged {padding: 0px; border: 2px solid #F00;}";
  sheet.innerHTML += " a.tag_nonexistent { color: #E00; }"; //custom style for tag menu
  document.body.appendChild(sheet);

  var headerlogo = document.getElementById("headerlogo");
  if (headerlogo !== null) {
    header_offset_height = headerlogo.offsetHeight;
    update_headerlogo();
  }

  add_config_dialog();
  add_config_button();
  update_config_dialog();

  //listen for config changes in other windows
  window.addEventListener("storage", local_storage_changed);

  if (config.show_speaker_icon || config.show_animated_icon) add_speaker_icons(document);


  /*************************************/
  /* main page / visually similar page */
  /*************************************/

  if (pathname === "/" || pathname.startsWith("/post/similar")) {

    if (config.tag_search_buttons) add_tag_search_buttons();

    if (!PostModeMenu.old_change) PostModeMenu.old_change = PostModeMenu.change;
    if (!PostModeMenu.old_click)  PostModeMenu.old_click  = PostModeMenu.click;

    //add change events
    PostModeMenu.change = function() {
      var s = $F("mode");

      PostModeMenu.old_change();

      if (s === "remove-fav") {
        document.body.setStyle({
          backgroundColor: "#FEA" //slightly more orange
        });
      } else if (s === "apply-tag-script") {
        document.body.setStyle({
          backgroundColor: "#FDF" //weaken color intensity
        });
      } else if (s === "approve") {
        document.body.setStyle({
          backgroundColor: "#DEF" //weaken color intensity
        });
      } else if (s === "choose-parent") {
        document.body.setStyle({
          backgroundColor: "#FFD"
        });
      } else if (s === "set-parent") {
        if (Cookie.get("chosen-parent") === null) {
          notice("addon: Choose parent first!");
          $("mode").value = "choose-parent";
          PostModeMenu.change();
        } else {
          document.body.setStyle({
            backgroundColor: "#DFF"
          });
        }
      }
    };

    //add click events
    PostModeMenu.click = function(post_id) {

      if (PostModeMenu.old_click(post_id)) {
        return true;
      }

      var s = $("mode");

      if (s.value === "choose-parent") {
        Cookie.put("chosen-parent", post_id);
        $("mode").value = "set-parent";
        PostModeMenu.change();
      } else if (s.value === "set-parent") {
        var parent_id = Cookie.get("chosen-parent");
        TagScript.run(post_id, "parent:" + parent_id + (config.setparent_deletepotentialduplicate ? " -potential_duplicate" : ""));
      }
      return false;
    };

    if (added_mode_options) {
      //add_mode_options() was called early, as it should
      PostModeMenu.change(); //guarantee that 'mode' variable correctly changes to new modes when loading page
    } else {
      //if not, catch up on it later
      call_postmodemenu_change = true;
    }

    document.addEventListener("keydown", function(e) {
      var tag = e.target.tagName.toLowerCase();
      if (tag === "input" || tag === "textarea" || tag === "select") return;
      var mode = document.getElementById("mode");
      if (mode === null) return;

      if (e.ctrlKey || e.altKey || e.shiftKey) return;

      switch (e.key) {
        case "v":
          mode.value = "set-parent";
          break;
        case "c":
          mode.value = "choose-parent";
          break;
        case "q":
          mode.value = "rating-q";
          break;
        case "s":
          mode.value = "rating-s";
          break;
        case "e":
          mode.value = "rating-e";
          break;
      }

      PostModeMenu.change();
    }, true);


  /*************/
  /* post page */
  /*************/

  } else if (pathname.startsWith("/post/show/")) {

    var hidden_post_id_el = document.getElementById("hidden_post_id");
    if (hidden_post_id_el !== null) {
      post_id = hidden_post_id_el.innerHTML;
    }

    post_parent_id = document.getElementById("post_parent_id");
    if (post_parent_id !== null) {
      parent_id = post_parent_id.value;
    }

    find_actions_list();
    add_addon_actions();
    add_tag_buttons();
    if (config.tag_menu) add_tag_menu();
    updated_tags();
    if (config.tag_search_buttons) add_tag_search_buttons();

    read_image_data();
    if (config.scale_image) scale_image(config.scale_mode, false);
    if (config.scroll_to_image) scroll_to_image(config.scroll_to_image_center);
    if (config.sankaku_channel_dark_compatibility) note_fix();

    document.addEventListener("keydown", function(e) {
      var tag = e.target.tagName.toLowerCase();
      if (tag === "input" || tag === "textarea" || tag === "select") return;

      if (e.ctrlKey || e.altKey || e.shiftKey) return;

      switch (e.key) {
        case "r": //r(eset)
          scale_image(-1, true);
          scroll_to_image(config.scroll_to_image_center);
          break;
        case "f": //f(it)
          scale_image(0, true);
          scroll_to_image(config.scroll_to_image_center);
          break;
        case "g": //g
          scale_image(1, true);
          scroll_to_image(config.scroll_to_image_center);
          break;
        case "h": //h
          scale_image(2, true);
          scroll_to_image(config.scroll_to_image_center);
          break;
        case "s": //s(imilar)
          if (post_id === null) {
            notice("addon error: couldn't find \"hidden_post_id\" element!")
          } else {
            window.open("/post/similar?id=" + post_id);
          }
          break;
        case "d": //d(elete)
          if (post_id === null) {
            notice("addon error: couldn't find \"hidden_post_id\" element!")
          } else if (!found_delete_action) {
            notice("addon error: Delete action not found, no permission?");
          } else {
            window.open("/post/delete/" + post_id);
          }
          break;
      }
    }, true);

    if (config.scale_on_resize) add_scale_on_resize_listener();

    add_tag_menu_change_listener();


  /**************/
  /* pool index */
  /**************/

  } else if (pathname.startsWith("/pool/index")) {

    //sitefix to show pool links even if missing english translation (they could not be clicked on otherwise)
    var pool_entries = document.getElementById("pool-index").getElementsByTagName("TABLE")[0].getElementsByTagName("TBODY")[0].getElementsByTagName("TR");

    for (var i = 0; i < pool_entries.length; i++) {
      var pool_name = pool_entries[i].getElementsByTagName("TD")[0].getElementsByTagName("A")[0];
      if (pool_name.innerHTML.trim().length === 0) pool_name.innerHTML = "&lt;missing English translation&gt;";
    }

  } else if (pathname.startsWith("/wiki/show")) {
    var h2 = document.getElementsByClassName("title")[0];
    var wiki_edit_link = document.createElement("A");
    var tag = new URL(window.location.href).searchParams.get("title");
    wiki_edit_link.href = "/tag/edit?name=" + tag;
    wiki_edit_link.innerHTML = "⚙";
    wiki_edit_link.title = "Edit Tag";
    h2.appendChild(wiki_edit_link);
  }

}

if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
  init();
} else {
  document.addEventListener("DOMContentLoaded", init, false);
}