SankakuAddon

Adds a few quality of life improvements on Sankaku Channel: 'Set Parent', automatic image scaling, duplicate tagging/flagging, muting videos. Fully configurable using localStorage.

Tính đến 16-10-2016. Xem phiên bản mới nhất.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        SankakuAddon
// @namespace   SankakuAddon
// @description Adds a few quality of life improvements on Sankaku Channel: 'Set Parent', automatic image scaling, duplicate tagging/flagging, muting videos. 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.95
// @grant       none
// ==/UserScript==

var version = "v0.95";

//IDEA hotkeys for quick access of different modes

//known bugs/limitations:
//#1 new modes are sometimes not at the last place in the dropdown menu
//#2 image scaling doesn't scale translation boxes
//flash videos cannot be muted


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

const default_config = {
  scroll_to_image:true,
  scale_image:true, //and video
  scale_flash:false,
  scale_mode:0, //hardcoded exception
  scale_on_resize:false,
  video_mute:true,
  video_controls:true,
  setparent_deletepotentialduplicate:false,
  hide_headerlogo:false
};

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

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


//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 (key === "scale_mode") { //hardcoded exception
          f(cfg_elem, key, function() { return cfg_elem.value; });
        } else {
          f(cfg_elem, key, function() { return cfg_elem.checked; });
        }
      })(cfg_elem);
    }
  }
}

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;
  localStorage.setItem(cfg_key, JSON.stringify(value));
}

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;

  //update config dialog
  var cfg_key = key_prefix + key;
  var cfg_elem = document.getElementById(cfg_key);
  if (cfg_elem === null) {
    notice("addon error: couldn't find config element " + cfg_key + "!");
    return;
  }

  if (key === "scale_mode") { //hardcoded exception
    cfg_elem.value = config[key];
  } else {
    cfg_elem.checked = config[key];
  }

  //currently the only dynamic change
  if (key === "hide_headerlogo") hide_headerlogo(value);
}

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]);
    }
  }
}

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


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

function getScrollbarWidth() { //from Stack Overflow
  var outer = document.createElement("div");
  outer.style.visibility = "hidden";
  outer.style.width = "100px";
  outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps

  document.body.appendChild(outer);

  var widthNoScroll = outer.offsetWidth;
  // force scrollbars
  outer.style.overflow = "scroll";

  // add innerdiv
  var inner = document.createElement("div");
  inner.style.width = "100%";
  outer.appendChild(inner);

  var widthWithScroll = inner.offsetWidth;

  // remove divs
  outer.parentNode.removeChild(outer);

  return widthNoScroll - widthWithScroll;
}

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


/*********************/
/* 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");
  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);
}


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

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


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

//original post/parent ids
var post_id = null;
var parent_id = null;
//original image size
var img_width = null;
var img_height = null;
var img_aspect_ratio = null;
var resize_timer;

function reset_parent_id() {
  document.getElementById("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;

  update_tag_buttons();
}

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(" ");

  update_tag_buttons();
}

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

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 || !dup_button || !var_button || !pot_button) {
    notice("addon error: couldn't find tag buttons.");
    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;
  }
}


//flag option with default text
function flag_duplicate(id, reason_suffix) {
  var current_parent_id = document.getElementById("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);
      }
    }
  });
}


//stretch image/video/flash
function scale_image(mode) {
  mode = Number(mode);
  if (isNaN(mode)) {
    notice("addon error: scaling mode wasn't a number?");
    return;
  }

  var flash = false;
  var obj, emb;

  var img = document.getElementById("image"); //image or video
  if (!img) {
    var non_image_content = document.getElementById("non-image-content");
    img = non_image_content.getElementsByTagName("object")[0];
    emb = non_image_content.getElementsByTagName("embed")[0];
    flash = img && emb;

    if (!flash || !config.scale_flash) return;
  }

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

  //reset image size
  if (mode === -1) {
    if (!flash) {
      img.style.width  = null;
      img.style.height = null;
    } else {
      img.width  = img_width;
      img.height = img_height;
      emb.width  = img_width;
      emb.height = img_height;
    }
    return;
  }

  var scale = function(obj) {
    //target rect
    var img_rect_w = Math.max(window.innerWidth - img.getBoundingClientRect().left - getScrollbarWidth() - 1, 1);
    var 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) {
      obj.width  = Math.floor(img_rect_w) + "px";
      obj.height = Math.floor(img_rect_w / img_aspect_ratio) + "px";

      //vertical
    } else if (mode === 2) {
      obj.width  = Math.floor(img_rect_h * img_aspect_ratio) + "px";
      obj.height = Math.floor(img_rect_h) + "px";
    }
  };

  if (!flash) {
    scale(img.style);
  } else {
    scale(img);
    scale(emb);
  }
}

function scroll_to_image() {
  var img = document.getElementById("image");
  if (!img) img = document.getElementById("non-image-content");
  if (!img) return;
  img.scrollIntoView();
}


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


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

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

  // add new modes right when dropdown is added to prevent it being reset to "view post" on page reloads
  register_observer("mode", function(node) { add_mode_options(node); });


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

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

  var mute_video = function(node) {
    if (node.nodeType !== Node.ELEMENT_NODE) return;
    if (node.tagName !== "VIDEO") return;

    if (config.video_mute)     node.muted = true;
    if (config.video_controls) node.controls = true;
  };

  register_observer("image", function(node) {
    mute_video(node);
  });

}




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

document.addEventListener("DOMContentLoaded", function() {

  header_offset_height = document.getElementById("headerlogo").offsetHeight;


  //fix 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;}";
  document.body.appendChild(sheet);


  //add config overlay
  var cfg_overlay = document.createElement("div");
  cfg_overlay.style = "display:none; border:1px solid #DDD; top: 300px; width: 400px; height: 260px; position:fixed; left: 0; right: 0; margin: 0 auto; overflow: auto; background-color: white; z-index: 10001;";
  cfg_overlay.id = "cfg_overlay";
  cfg_overlay.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 #DDD;'>"
    +   "<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 + "scale_image' type='checkbox'>"
    +     "<span>Scale image/video when opening post</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_mute' type='checkbox'>"
    +     "<span>Mute (non-flash) videos*</span><br>"
    +   "</span>"
    +   "<span>"
    +     "<input id='" + key_prefix + "video_controls' type='checkbox'>"
    +     "<span>Show video controls*</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>"
    +   "<button id='config_close'>Close</button>"
    +   "<button id='config_reset'>Reset settings</button>"
    +   "<span>&nbsp;*requires a page reload.</span>"
    + "</div>"
    + "";
  document.body.appendChild(cfg_overlay);

  //add events
  document.getElementById("config_close").onclick = function() { show_config_overlay(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());
    });
  });

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

  //add config button to navbar
  var navbar = document.getElementById("navbar");
  if (navbar === null) {
    notice("addon error: couldn't find \"navbar\" element! Config dialog disabled.");
  } else {
    navbar.style.whiteSpace = "nowrap"; //hack to fit config button

    var a = document.createElement("A");
    a.href = "#";
    a.onclick = function() { show_config_overlay(true); return false; };
    a.innerHTML = "Addon config";
    a.style.fontSize = "110%";

    var newli = document.createElement("li");
    newli.className = "lang-select"; //
    newli.appendChild(a);
    navbar.appendChild(newli);
  }

  load_config();


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

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

    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 == "apply-tag-script") {
        document.body.setStyle({
          backgroundColor: "#FDF" //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;
    };

    //guarantee that 'mode' variable correctly changes to new modes when loading page
    PostModeMenu.change();


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

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

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

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


    scale_image(config.scale_mode);
    if (config.scroll_to_image) scroll_to_image();

    window.addEventListener("resize", function() {
      clearTimeout(resize_timer);
      resize_timer = setTimeout(function() {
        if (config.scale_on_resize) scale_image(config.scale_mode);
      }, 100);
    });


    //add actions
    var li = document.getElementById("add-to-pool");
    if (li === null) {
      notice("addon error: couldn't find \"add-to-pool\" element! Addon actions disabled.");
      return;
    }

    if (document.getElementById("flag-duplicate") === null) {
      var actions_ul = li.parentElement;

      var seperator = document.createElement("H5");
      seperator.innerHTML = "Addon actions";
      var newli = document.createElement("li");
      newli.appendChild(seperator);
      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); scroll_to_image(); },  "Fit image",              "scale-image-fit");
      add_action(function() { scale_image(1); scroll_to_image(); },  "Fit image (Horizontal)", "scale-image-hor");
      add_action(function() { scale_image(2); scroll_to_image(); },  "Fit image (Vertical)",   "scale-image-ver");
      add_action(function() { scale_image(-1); scroll_to_image(); }, "Reset image size",       "reset-image");

      if (post_id !== null) {
        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");
      }
    }


    //add tag buttons
    var edit_form = document.getElementById("edit-form");
    if (edit_form) {

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

      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);

      update_tag_buttons();
    }

  }

}, false);