// ==UserScript==
// @name SankakuAddon
// @namespace SankakuAddon
// @description Adds a few quality of life improvements on Sankaku Channel: 'Set Parent' mode, 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> *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);