// ==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 https://chan.sankakucomplex.com/*
// @include https://idol.sankakucomplex.com/*
// @run-at document-start
// @version 0.99.14
// @grant GM.openInTab
// @grant unsafeWindow
// ==/UserScript==
(function(unsafeWindow) {
"use strict";
const VERSION = "v0.99.14";
/*****************/
/* compatibility */
/*****************/
let is_greasemonkey4 = false; //script breaking changes (see TODO)
let is_monkey = false; //Tampermonkey, Violentmonkey, Greasemonkey (all seem to support 'GM.' functions)
if (typeof GM !== "undefined" && typeof GM.info === "object") {
is_monkey = true;
is_greasemonkey4 = (GM.info.scriptHandler === "Greasemonkey");
}
function console_log(...args) {
if (typeof window.console === "undefined") return;
const msgs = args.map(a => (typeof a === "object" ? JSON.parse(JSON.stringify(a)) : a));
window.console.log(...msgs);
}
function open_in_tab(url) {
if (is_monkey) GM.openInTab(url, false);
else window.open(url); //requires popup permission
}
function show_notice(msg) {
if (unsafeWindow.notice) unsafeWindow.notice(msg);
console_log(msg);
}
/***************************/
/* configuration functions */
/***************************/
const 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: 50,
video_controls: true,
show_speaker_icon: true,
show_animated_icon: true,
setparent_deletepotentialduplicate: false,
editform_deleteuselesstags: false,
hide_headerlogo: false,
tag_search_buttons: true,
tag_menu: true,
tag_menu_scale: "30%",
tag_menu_layout: 0,
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,
};
const USE_LOCAL_STORAGE = (typeof(Storage) !== "undefined");
const KEY_PREFIX = "config."; //used to avoid conflicts in localStorage
let config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); //load default
let dom_content_loaded = false;
//config entries that need to have specific types
const CONFIG_TYPES = {
scale_mode: Number,
tag_menu_layout: Number
}
//correct the type of a config entry
function fix_config_entry(key, value) {
const fixer = CONFIG_TYPES[key];
if (fixer !== undefined) {
return fixer(value);
}
return value;
}
function save_config(key, value, warn = true) {
value = fix_config_entry(key, value);
if (!USE_LOCAL_STORAGE) {
if (warn) {
show_notice("addon: couldn't save setting \"" + key + " = " + value + "\" to local storage. check permissions.");
}
return;
}
const cfg_key = KEY_PREFIX + key;
try {
localStorage.setItem(cfg_key, JSON.stringify(value));
} catch (error) {
show_notice("addon: couldn't save setting \"" + key + " = " + value + "\" to local storage, check the console.");
console_log("addon error: ", error);
}
}
function load_config() {
for (const key in config) {
if (config.hasOwnProperty(key)) {
let value = config[key]; //default already loaded
if (USE_LOCAL_STORAGE) {
const cfg_key = KEY_PREFIX + key;
const stored_value = localStorage.getItem(cfg_key);
if (stored_value !== null) {
value = JSON.parse(stored_value);
}
}
config_changed(key, value); //fire regardless
}
}
}
function local_storage_changed(e) {
if (e.key.startsWith(KEY_PREFIX)) {
const key = e.key.substring(KEY_PREFIX.length);
const 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] = fix_config_entry(key, value);
if (key === "scale_on_resize") {
if (value) add_scale_on_resize_listener();
else remove_scale_on_resize_listener();
}
//UI hasn't loaded yet
if (!dom_content_loaded) {
return;
}
update_config_dialog_by_key(key);
if (key === "hide_headerlogo") {
update_headerlogo();
}
}
function reset_config() {
//clear local storage
if (USE_LOCAL_STORAGE) {
for (const key in config) {
if (config.hasOwnProperty(key)) {
const cfg_key = KEY_PREFIX + key;
localStorage.removeItem(cfg_key);
}
}
}
for (const key in config) {
if (config.hasOwnProperty(key)) {
config_changed(key, DEFAULT_CONFIG[key]);
}
}
}
//template for the config dialog
const CONFIG_TEMPLATE = {
"scroll_to_image": {type: "checkbox", desc: "Scroll to image/video when opening post"},
"scroll_to_image_center": {type: "checkbox", desc: "Scroll to center of image/video, else scroll to top"},
"scale_image": {type: "checkbox", desc: "Scale image/video when opening post"},
"scale_only_downscale": {type: "checkbox", desc: "Only downscale"},
"scale_flash": {type: "checkbox", desc: "Also scale flash videos"},
"scale_on_resize": {type: "checkbox", desc: "Scale image on window resize", title: "This uses the 'scale image mode' setting, so it doesn't work well when using the manual scaling actions."},
"scale_mode": {type: "select", desc: "Scale image/video mode: ", options: {0: "Fit to window", 1: "Fit horizontally", 2: "Fit vertically"}},
"video_pause": {type: "checkbox", desc: "Pause (non-flash) videos*"},
"video_mute": {type: "checkbox", desc: "Mute (non-flash) videos*"},
"set_video_volume": {type: "checkbox", desc: "Set (non-flash) video volume to: "},
"video_controls": {type: "checkbox", desc: "Show video controls*"},
"tag_search_buttons": {type: "checkbox", desc: "Show + - tag search buttons*"},
"show_speaker_icon": {type: "checkbox", desc: "Show 🔊 icon on thumbnail if it has audio*"},
"show_animated_icon": {type: "checkbox", desc: "Show ⏩ icon on thumbnail if it is animated (🔊 overrides ⏩)*"},
"setparent_deletepotentialduplicate": {type: "checkbox", desc: "Delete potential_duplicate tag when using \"Set Parent\""},
"editform_deleteuselesstags": {type: "checkbox", desc: "\"Save changes\" button deletes useless_tags tag (if there have been changes)"},
"hide_headerlogo": {type: "checkbox", desc: "Hide header logo"},
"sankaku_channel_dark_compatibility": {type: "checkbox", desc: "Galinoa's Sankaku Channel Dark compatibilty*"},
"tag_menu": {type: "checkbox", desc: "Activate tag menu*:"},
"common_tags_json": {type: "text", desc: " Common tags list (JSON format)*"},
"tag_menu_layout": {type: "select", desc: "Tag menu layout: ", options: {0: "Normal", 1: "Vertically compact"}},
};
//whether a config element's value are accessed via '.value' (or otherwise '.checked')
function is_value_element(key) {
if (key === "video_volume") return true; //"video_volume" is hardcoded in add_config_dialog()
if (key === "tag_menu_scale") return true; //doesn't exist as an element, but it would be '.value' type
const type = CONFIG_TEMPLATE[key]["type"];
return (type === "select" || type === "text");
}
//calls f(cfg_elem, key, get_value) for each existing config element
function foreach_config_element(f) {
for (const key in config) {
if (config.hasOwnProperty(key)) {
const cfg_key = KEY_PREFIX + key;
const cfg_elem = document.getElementById(cfg_key);
if (cfg_elem === null) continue;
(function(cfg_elem) {
if (is_value_element(key)) {
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) {
const cfg_key = KEY_PREFIX + key;
const cfg_elem = document.getElementById(cfg_key);
if (cfg_elem !== null) {
if (is_value_element(key)) {
cfg_elem.value = config[key];
} else {
cfg_elem.checked = config[key];
}
}
}
function update_config_dialog() {
for (const 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 ? "table" : "none");
}
/********************/
/* helper functions */
/********************/
function get_scrollbar_width() { //from Stack Overflow
const outer = document.createElement("DIV");
outer.style.visibility = "hidden";
outer.style.width = "100px";
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = "scroll"; //force scrollbars
const inner = document.createElement("DIV");
inner.style.width = "100%";
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
function get_original_background_color() {
//the background-color style gets changed through the (site)script, but we need the original one
//there has to be a better way than this, right?
const current = window.getComputedStyle(document.body).getPropertyValue("background-color");
document.body.style.backgroundColor = "";
const original = window.getComputedStyle(document.body).getPropertyValue("background-color");
document.body.style.backgroundColor = current;
return original;
}
//"rgb(r,g,b)" -> [int(r), int(g), int(b)]
function rgb_to_array(rgb) {
const arr = rgb.substring(rgb.indexOf("(") + 1, rgb.lastIndexOf(")")).split(/,\s*/);
for (let i = 0; i < arr.length; i++) {
arr[i] = parseInt(arr[i]);
}
return arr;
}
function rgb_array_is_dark(rgb_array) {
let avg = 0;
for (let i = 0; i < rgb_array.length; i++) {
avg += rgb_array[i];
}
avg /= rgb_array.length;
return avg <= 128;
}
function rgb_array_shift(rgb, shift) {
const shifted = [];
for (let 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] + ")";
}
function is_darkmode() {
const theme = unsafeWindow.Cookie.get("theme");
if (theme !== "" && Number(theme) !== 0) {
return true;
}
//fallback
const rgb = rgb_to_array(get_original_background_color());
return rgb_array_is_dark(rgb);
}
//helper function to adjust background colors based on light or dark mode
function shifted_backgroundColor(shift) {
const rgb = rgb_to_array(get_original_background_color());
const shifted_rgb = rgb_array_shift(rgb, (is_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) {
const observer = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
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 */
/*********************/
let header_offset_height = null; //distance needed to scroll if headerlogo is hidden/shown
function hide_headerlogo(hide) {
const headerlogo = document.getElementById("headerlogo");
if (headerlogo === null) return;
const 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() {
const cfg_dialog = document.createElement("DIV");
cfg_dialog.id = "cfg_dialog";
cfg_dialog.style.display = "none"; //show_config_dialog() switches this with "table" so that centeringDiv works
cfg_dialog.style.border = "1px solid " + shifted_backgroundColor(32);
cfg_dialog.style.top = "50%";
cfg_dialog.style.transform = "translateY(-50%)";
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 = get_original_background_color();
cfg_dialog.style.zIndex = "10002";
const centeringDiv = document.createElement("DIV");
centeringDiv.style.display = "table-cell";
centeringDiv.style.verticalAlign = "middle";
cfg_dialog.appendChild(centeringDiv);
const innerDiv = document.createElement("DIV");
innerDiv.style.margin = "12px";
innerDiv.id = "cfg_dialog_inner";
centeringDiv.appendChild(innerDiv);
//generate the content of the config menu
let innerDivHTML = "<div style='font-weight: bold;'>Sankaku Addon " + VERSION + "</div>"
+ "<hr style='margin-top: 0; margin-bottom: 2px; border:1px solid " + shifted_backgroundColor(32) + ";'>";
//parse the config_template
for (const [key, value] of Object.entries(CONFIG_TEMPLATE)) {
const type = value.type;
const generate_span = function (value) {
return "<span style='vertical-align: middle;" + (value.title ? "cursor:help; text-decoration: underline dashed;" : "") + "' "
+ (value.title ? "title=\"" + value.title + "\"" : "") + " >" + value.desc + "</span>";
}
innerDivHTML += "<div>"
switch (type) {
case "checkbox":
innerDivHTML += "<input id='" + KEY_PREFIX + key + "' type='checkbox' style='vertical-align: middle;'>";
innerDivHTML += generate_span(value);
//hardcode 'video_volume' element:
innerDivHTML += (key === "set_video_volume" ? "<input id='" + KEY_PREFIX + "video_volume' type='number' min='0' max='100' size='4'>%" : "");
break;
case "select":
innerDivHTML += generate_span(value);
innerDivHTML += "<select id='" + KEY_PREFIX + key + "'>";
for (const [k, v] of Object.entries(value.options))
innerDivHTML += "<option value=" + k + ">" + v + "</option>";
innerDivHTML += "</select>";
break;
case "text":
innerDivHTML += "<input id='" + KEY_PREFIX + key + "' style='vertical-align: middle;'>";
innerDivHTML += generate_span(value);
break;
}
innerDivHTML += "</div>";
}
innerDivHTML += "<div>";
innerDivHTML += "<button id='config_close' style='cursor: pointer;'>Close</button>";
innerDivHTML += "<button id='config_reset' style='cursor: pointer;'>Reset settings</button>";
innerDivHTML += "</div>";
innerDivHTML += "<div> *requires a page reload.</div>";
innerDiv.innerHTML = innerDivHTML;
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() {
const navbar = document.getElementById("navbar");
if (navbar === null) {
show_notice("addon error: couldn't find \"navbar\" element! Config dialog disabled.");
return;
}
navbar.style.whiteSpace = "nowrap"; //hack to fit config button
const 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%";
const li = document.createElement("LI");
li.className = "lang-select"; //match style of top bar
li.appendChild(a);
navbar.appendChild(li);
}
function add_tag_search_buttons() {
const tagsidebar = document.getElementById("tag-sidebar");
if (tagsidebar === null) return;
const items = tagsidebar.getElementsByTagName("LI");
for (const item of items) {
const taglink = item.getElementsByTagName("A")[0];
const tagname = taglink.innerHTML.replace(/ /g, "_"); // " " -> "_" hopefully this is the only edgecase
//generates onclick events
const tag_search_button_func = function (tagname) {
return function () {
const search_field = document.getElementById("tags");
const search_tags = search_field.value.trim().split(/\s+/);
const tag_index = search_tags.indexOf(tagname);
//add tag 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;
};
};
{
const a = document.createElement("A");
a.href = "#";
a.innerHTML = "+";
a.onclick = tag_search_button_func(tagname);
item.insertBefore(a, taglink);
item.insertBefore(document.createTextNode(" "), taglink);
}
{
const a = document.createElement("A");
a.href = "#";
a.innerHTML = "-";
a.onclick = tag_search_button_func("-" + tagname);
item.insertBefore(a, taglink);
item.insertBefore(document.createTextNode(" "), taglink);
}
}
}
function add_speaker_icons(root) {
if (root === null) return;
const elems = root.getElementsByTagName("SPAN");
for (const elem of elems) {
if (elem.classList.contains("thumb")) {
add_speaker_icon(elem);
}
}
}
function add_speaker_icon(thumb_span) {
const img = thumb_span.querySelector(".preview");
if (img === null) return;
const a = thumb_span.getElementsByTagName("A");
if (a.length === 0) return;
const icon = document.createElement("SPAN");
const 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;
node.controls = config.video_controls;
}
/***********************************************/
/* main page / visually similar page functions */
/***********************************************/
let mode_dropdown = null;
let added_mode_options = false;
let call_postmodemenu_change = false;
function add_mode_options(dropdown) {
if (added_mode_options) return;
added_mode_options = true;
mode_dropdown = dropdown;
//override change event
mode_dropdown.removeAttribute("onchange");
mode_dropdown.onchange = PostModeMenu_change_override;
if (!is_greasemonkey4) {
if (mode_dropdown.options.namedItem("choose-parent") === null) {
const option = document.createElement("option");
option.text = "Choose Parent";
option.value = "choose-parent";
mode_dropdown.add(option);
}
if (mode_dropdown.options.namedItem("set-parent") === null) {
const 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_init_workaround(); //guarantee that 'mode' variable correctly changes to new modes when loading page
}
}
function PostModeMenu_init_workaround() {
//issue: new post modes can reset on page load if they were added too late
//reason: on page load, PostModeMenu.init reads the "mode" cookie, tries to set mode_dropdown.value, then
//calls PostModeMenu.change, which sets the cookie to mode_dropdown.value.
//so if the new modes aren't added yet, mode_dropdown.value and the "mode" cookie will both reset
//solution: safe mode in a separate 'backup' cookie and set the "mode" cookie and mode_dropdown after new modes were added
const mode = unsafeWindow.Cookie.get("addon_mode");
if (mode !== "") {
unsafeWindow.Cookie.put("mode", mode, 7);
mode_dropdown.value = mode;
}
PostModeMenu_change_override();
}
function PostModeMenu_change_override() {
if (!added_mode_options) return;
const s = mode_dropdown.value;
unsafeWindow.PostModeMenu.change();
unsafeWindow.Cookie.put("addon_mode", s, 7); //set 'backup' cookie
const darkmode = is_darkmode();
if (s === "add-fav") {
//FFFFAA, original. darkmode: luminance 40
document.body.style.backgroundColor = (darkmode ? "#505000" : "#FFA");
} else if (s === "remove-fav") {
//FFFFAA -> FFEEAA, slightly more orange. darkmode: luminance 40
document.body.style.backgroundColor = (darkmode ? "#504000" : "#FEA");
} else if (s === "apply-tag-script") {
//AA33AA -> FFDDFF, weaken color intensity. darkmode: luminance 40
document.body.style.backgroundColor = (darkmode ? "#500050" : "#FDF");
} else if (s === "approve") {
//2266AA -> FFDDFF, increase contrast to unapproved posts. darkmode: TODO
document.body.style.backgroundColor = "#FDF";
} else if (s === "choose-parent") {
document.body.style.backgroundColor = (darkmode ? "#464600" : "#FFD");
} else if (s === "set-parent") {
if (unsafeWindow.Cookie.get("chosen-parent") === "") {
show_notice("addon: Choose parent first!");
mode_dropdown.value = "choose-parent";
PostModeMenu_change_override();
} else {
document.body.style.backgroundColor = (darkmode ? "#005050" : "#DFF");
}
}
}
let PostModeMenu_click_original = null;
function PostModeMenu_click_override(post_id) {
if (!added_mode_options) return false;
if (PostModeMenu_click_original(post_id)) {
return true; //view mode, let it click
}
const s = mode_dropdown.value;
if (s === "choose-parent") {
unsafeWindow.Cookie.put("chosen-parent", post_id);
mode_dropdown.value = "set-parent";
PostModeMenu_change_override();
} else if (s === "set-parent") {
const parent_id = unsafeWindow.Cookie.get("chosen-parent");
unsafeWindow.TagScript.run(post_id, "parent:" + parent_id + (config.setparent_deletepotentialduplicate ? " -potential_duplicate" : ""));
}
return false;
}
/***********************/
/* post page functions */
/***********************/
let post_parent_id = null; //input elem
//original post/parent ids
let post_id = null;
let parent_id = null;
let image_data = null;
let resize_timer;
let tag_update_timer;
let tags_changed = false;
//set by find_actions_list():
let actions_ul = null;
let found_delete_action = false;
let 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) {
const tag_menu = document.getElementById("tag_menu");
if (tag_menu === null) return;
const yFromBottom = window.innerHeight - e.clientY;
let 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, false);
}
}
function add_tag_menu() {
if (document.getElementById("post_tags") === null) return; //not logged in
const 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 = get_original_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 = "<span style='width: calc(100% - 2px); height: 100%; overflow: auto;'>" + "<span id='common_tags'></span></span>" + "current tags:" + "<span id='current_tags'></span></div>";
const 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
const create_tag_menu_button = function(id, text) {
const 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;
};
const tag_menu_close = create_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);
const tag_menu_open = create_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);
const tag_menu_save = create_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() {
if (tags_submit_listener()) {
document.getElementById("edit-form").submit();
}
return false;
};
tag_menu.appendChild(tag_menu_save);
}
function update_tag_menu() {
if (document.getElementById("post_tags") === null) return; //not logged in
const common_tags_elem = document.getElementById("common_tags");
const current_tags_elem = document.getElementById("current_tags");
//tag menu disabled
if (common_tags_elem === null || current_tags_elem === null)
return;
if (config.tag_menu_layout === 1) {
common_tags_elem.style.display = "grid";
common_tags_elem.style.gridTemplateColumns = "fit-content(5%) auto";
} else {
common_tags_elem.style.display = "";
}
const create_tag_button = function(tag) {
const 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;
return a;
};
const wrap_in_div = function(el, margin) {
const div = document.createElement("DIV");
div.style.margin = margin;
div.style.float = "left";
div.appendChild(el);
return div;
};
const create_top_level_div = function(margin = "3px") {
const div = document.createElement("DIV");
div.style.margin = margin;
return div;
};
const create_tag_list = function() {
const div = document.createElement("DIV");
div.style.display = "flex";
div.style.flexWrap = "wrap";
div.style.alignContent = "flex-start";
div.style.alignItems = "flex-start";
div.style.margin = "0";
div.style.padding = "0";
return div;
};
//generate tag button list for current tags
const current_tags_flex = create_tag_list();
current_tags_flex.style.marginBottom = "3px";
const current_tags = get_tags_array();
for (const current_tag of current_tags) {
let div = create_top_level_div();
div.appendChild(create_tag_button(current_tag));
current_tags_flex.appendChild(div);
}
//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_flex);
//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" ]} ]
let tag_data;
try {
tag_data = JSON.parse(config.common_tags_json);
} catch (error) {
show_notice("addon error: \"common tags\" JSON syntax error");
return;
}
if (!Array.isArray(tag_data)) {
show_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 (let k = 0; k < tag_data.length; k++) {
const list_flex = create_tag_list();
const list_name = tag_data[k].name;
const list_tags = tag_data[k].tags;
if (!Array.isArray(list_tags)) {
show_notice("addon error: a \"common tags\" object needs to have a \"tags\" array");
return;
}
const TAGS_TYPES = {
LIST: "list", //e.g. "tag1 tag2"
GROUP: "group", //e.g. ["tag1 tag2"]
TABLE: "table" //e.g. [["tag1 tag2"], ["tag3 tag4"]]
};
const group_style = function(el) {
//red in darkmode needs more contrast
const rgb = rgb_to_array(get_original_background_color());
if (is_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 (const list_tag of list_tags) {
const is_array = Array.isArray(list_tag);
//find tags_type
let tags_type;
if (is_array) {
if (list_tag.length === 0) {
show_notice("addon error: \"common tags\" \"tags\" array contains an empty array");
return;
}
//check what the array consists of
let all_arrays = true;
let no_arrays = true;
for (let i = 0; i < list_tag.length; i++) {
if (!Array.isArray(list_tag[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 {
show_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) {
const tags_table = [];
for (let j = 0; j < list_tag.length; j++) {
if (list_tag[j].length !== 1) {
show_notice("addon error: \"common tags\" \"tags\" array contains a table entry with not exactly 1 tags string");
return;
}
tags_table.push(list_tag[j][0].trim().split(/\s+/));
}
const table_height = tags_table.length;
let table_width = 0;
for (let row = 0; row < tags_table.length; row++) {
table_width = Math.max(table_width, tags_table[row].length);
}
//div (flexbox)><div><table><tr><td><div (button)>
const table = document.createElement("TABLE");
table.style.display = "inline-block";
group_style(table);
table.style.marginBottom = "0";
for (let row = 0; row < table_height; row++) {
const tr = document.createElement("TR");
for (let col = 0; col < table_width; col++) {
const td = document.createElement("TD");
td.style.border = "none";
td.style.padding = "0";
if (tags_table[row][col])
td.appendChild(wrap_in_div(create_tag_button(tags_table[row][col]), "1px"));
tr.appendChild(td);
}
table.appendChild(tr);
}
let div = create_top_level_div("0 3px 0 3px");
div.appendChild(table);
list_flex.appendChild(div);
} else if (tags_type === TAGS_TYPES.GROUP) {
if (list_tag.length !== 1) {
show_notice("addon error: \"common tags\" \"tags\" array contains a group with not exactly 1 tags string");
return;
}
let tags = list_tag[0].trim().split(/\s+/);
//<div (flexbox)><div><div (button)>
const group_div = document.createElement("DIV");
group_div.style.display = "inline-block";
group_style(group_div);
for (const tag of tags) {
group_div.appendChild(wrap_in_div(create_tag_button(tag), "3px"));
}
let div = create_top_level_div("0 3px 0 3px");
div.appendChild(group_div);
list_flex.appendChild(div);
} else /* if (tags_type === tag_types.LIST) */ {
//<div (flexbox)><div><div (button)>
let tags = list_tag.trim().split(/\s+/);
for (const tag of tags) {
const div = create_top_level_div("4px 3px 2px 3px");
div.appendChild(wrap_in_div(create_tag_button(tag)));
list_flex.appendChild(div);
}
}
}
const span = document.createElement("SPAN");
span.innerHTML = (list_name ? list_name + ":" : "");
span.style.paddingTop = "2px";
if (list_name) span.style.marginLeft = "2px";
if (list_name && config.tag_menu_layout === 1) {
const add_top_border = function(el) {
el.style.borderTopWidth = "1px";
el.style.borderTopStyle = "solid";
el.style.borderTopColor = shifted_backgroundColor(32);
};
add_top_border(span);
add_top_border(list_flex);
}
common_tags_elem.appendChild(span);
common_tags_elem.appendChild(list_flex);
}
}
function show_tag_menu(bool) {
document.getElementById("tag_menu").style.display = (bool ? "" : "none");
document.getElementById("tag_menu_open").style.display = (!bool ? "" : "none");
}
function add_tags_change_listener() {
const post_tags_area = document.getElementById("post_tags");
if (post_tags_area === null) return; //not logged in
post_tags_area.addEventListener("input", function() {
tags_changed = true;
clearTimeout(tag_update_timer);
tag_update_timer = setTimeout(function() {
updated_tags();
}, 500);
});
}
//also used for 'tag_menu_save' button
function tags_submit_listener() {
delete_useless_tags_tag();
return true; //actually submit?
}
function add_tags_submit_listener() {
const edit_form = document.getElementById("edit-form");
if (edit_form === null) return; //not logged in
edit_form.addEventListener("submit", function(e) {
if (!tags_submit_listener()) {
e.preventDefault();
}
});
}
function find_actions_list() {
const li = document.getElementById("add-to-pool");
if (li === null) return;
actions_ul = li.parentElement;
const action_links = actions_ul.getElementsByTagName("A");
for (const action_link of action_links) {
if (action_link.innerHTML === "Delete") {
found_delete_action = true;
break;
}
}
}
function add_addon_actions() {
if (actions_ul === null) return;
const separator = document.createElement("H5");
separator.innerHTML = "Addon actions";
const newli = document.createElement("LI");
newli.appendChild(separator);
actions_ul.appendChild(newli);
const add_action = function(func, name, id) {
const a = document.createElement("A");
a.href = "#";
a.onclick = function() {func(); return false;};
a.innerHTML = name;
const 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) {
show_notice("addon error: couldn't find post id?! 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() {
const edit_form = document.getElementById("edit-form");
if (edit_form === null) return; //not logged in
const button_place = edit_form.children[1].children[0].children[0].children[0];
button_place.style.whiteSpace = "nowrap";
{
const 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);
}
{
const 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);
}
{
const 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);
}
{
const el = document.createElement("BUTTON");
el.id = "tag_dup_button";
el.style.margin = "0 3px";
button_place.appendChild(el);
}
{
const el = document.createElement("BUTTON");
el.id = "tag_var_button";
el.style.margin = "0 3px";
button_place.appendChild(el);
}
{
const el = document.createElement("BUTTON");
el.id = "tag_pot_button";
el.style.margin = "0 3px";
button_place.appendChild(el);
}
}
function update_tag_buttons() {
const taglist = document.getElementById("post_tags");
const dup_button = document.getElementById("tag_dup_button");
const var_button = document.getElementById("tag_var_button");
const pot_button = document.getElementById("tag_pot_button");
if (taglist === null || dup_button === null || var_button === null || pot_button === null)
return;
const 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) {
const tags = get_tags_array();
if ((tag === "duplicate" && tags.indexOf("legitimate_variation") !== -1) || (tag === "legitimate_variation" && tags.indexOf("duplicate") !== -1)) {
show_notice("addon: cannot tag as duplicate and legitimate_variation at the same time.");
return;
}
if (tags.indexOf(tag) !== -1) {
show_notice("addon: tag already present.");
return;
}
document.getElementById("post_tags").value += " " + tag;
tags_changed = true;
updated_tags();
}
function remove_tag(tag) {
const tags = get_tags_array();
for (let i = 0; i < tags.length; i++) {
if (tags[i] === tag) {
tags[i] = "";
}
}
document.getElementById("post_tags").value = tags.join(" ").trim();
tags_changed = true;
updated_tags();
}
function delete_useless_tags_tag() {
if (tags_changed && config.editform_deleteuselesstags) {
remove_tag("useless_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;
tags_changed = false;
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 (is_greasemonkey4) {
show_notice("addon error: 'Flag duplicate' not yet supported in Greasemonkey");
return;
}
if (parent_id === null) {
show_notice("addon: user not logged in");
return;
}
const current_parent_id = post_parent_id.value;
if (current_parent_id !== parent_id) {
show_notice("addon: parent id was changed but not saved!");
return;
}
if (!current_parent_id || current_parent_id.length === 0) {
show_notice("addon: no parent id set!");
return;
}
const tags = get_tags_array();
const old_tags = get_old_tags_array();
if (tags.indexOf("duplicate") !== -1 && old_tags.indexOf("duplicate") === -1) {
show_notice("addon: duplicate tag set but not saved!");
return;
}
if (old_tags.indexOf("duplicate") === -1) {
show_notice("addon: not tagged as duplicate!");
return;
}
if (old_tags.indexOf("legitimate_variation") !== -1) {
show_notice("addon: tagged as legitimate_variation, are you sure it is a duplicate?");
return;
}
const reason = window.prompt("Why should this post be reconsidered for moderation?", "duplicate of " + parent_id + reason_suffix);
if (reason === null) {
return;
}
//TODO will not work on Greasemonkey at all
new unsafeWindow.Ajax.Request("/post/flag.json", {
parameters: {
"id": id,
"reason": reason
},
onComplete: function(response) {
const resp = response.responseJSON;
if (resp.success) {
show_notice("Post was resent to moderation queue");
} else {
show_notice("Error: " + resp.reason);
}
}
});
}
function read_image_data() {
const data = {
img_elem: null, //image or video
non_img_elem: null, emb_elem: null, //flash or unknown
is_flash: null,
width: null,
height: null,
aspect_ratio: null,
current_height: null //store current height separately, because flash is a bitch
};
let img = document.getElementById("image");
if (img !== null) {
//image or video
data.img_elem = img;
data.is_flash = false;
//workaround for Galinoa's Sankaku Channel Dark: don't read .width/.height attributes but read "Details"
let res = null;
const lowres = document.getElementById("lowres");
if (lowres !== null) {
res = lowres.innerHTML.split("x"); //parse "<width>x<height>"
} else {
const highres = document.getElementById("highres");
if (highres !== null) {
res = highres.innerHTML.split(" ")[0].split("x"); //parse "<width>x<height> (<file size>)"
}
}
if (res === null) {
show_notice("addon error: image/video resolution not in \"Details\"?! Disabled scaling.");
return null;
}
data.width = res[0];
data.height = res[1];
} else {
//flash or unknown
data.non_img_elem = document.getElementById("non-image-content");
if (!data.non_img_elem) return null;
data.img_elem = data.non_img_elem.getElementsByTagName("OBJECT")[0];
data.emb_elem = data.non_img_elem.getElementsByTagName("EMBED")[0];
data.is_flash = (data.img_elem !== null && data.emb_elem !== null);
img = data.img_elem; //object contains width/height
data.width = img.width;
data.height = img.height;
}
data.aspect_ratio = data.width / data.height;
data.current_height = data.height;
return data;
}
//stretch image/video/flash, requires data from read_image_data()
function scale_image(mode, always_scale) {
let img_rect_w, img_rect_h;
let new_width, new_height;
//read_image_data() failed
if (image_data === null)
return;
if (!always_scale && (!config.scale_flash && image_data.is_flash))
return;
//reset image size
if (mode === -1) {
if (!image_data.is_flash) {
image_data.img_elem.style.width = null;
image_data.img_elem.style.height = null;
} else {
image_data.img_elem.width = image_data.width;
image_data.img_elem.height = image_data.height;
image_data.emb_elem.width = image_data.width;
image_data.emb_elem.height = image_data.height;
}
image_data.current_height = image_data.height;
//workaround for Galinoa's Sankaku Channel Dark
if (config.sankaku_channel_dark_compatibility) {
image_data.img_elem.style.paddingLeft = "";
note_fix();
}
return;
}
let left_side;
//workaround for Galinoa's Sankaku Channel Dark
//problem: seems to only work for bigger windows
if (config.sankaku_channel_dark_compatibility) {
const sidebar = document.getElementsByClassName("sidebar")[0];
left_side = (sidebar.getBoundingClientRect().right + 12);
image_data.img_elem.style.paddingLeft = left_side + "px"; //don't hide behind sidebar
} else {
left_side = image_data.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);
const img_rect_aspect_ratio = img_rect_w / img_rect_h;
//fit into window
if (mode === 0) {
mode = (image_data.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 / image_data.aspect_ratio);
//vertical
} else if (mode === 2) {
new_width = Math.floor(img_rect_h * image_data.aspect_ratio);
new_height = Math.floor(img_rect_h);
}
if (!always_scale && (config.scale_only_downscale && (new_width > image_data.width || new_height > image_data.height)))
return;
const set_dimensions = function(obj, new_width, new_height) {
obj.width = new_width + "px";
obj.height = new_height + "px";
};
if (image_data.is_flash) {
set_dimensions(image_data.img_elem, new_width, new_height);
set_dimensions(image_data.emb_elem, new_width, new_height);
} else {
set_dimensions(image_data.img_elem.style, new_width, new_height);
}
image_data.current_height = new_height;
}
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 (image_data === null) return;
const absolute_img_top = (image_data.is_flash ? image_data.non_img_elem : image_data.img_elem).getBoundingClientRect().top + window.pageYOffset;
if (to_center) {
const top_of_centered_rect = absolute_img_top - (window.innerHeight - image_data.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() {
const note_container = document.getElementById("note-container");
if (note_container !== null && image_data !== null) {
note_container.style.marginLeft = ((window.innerWidth - image_data.img_elem.clientWidth) / 2 - 8) + "px";
}
}
/******************/
/* document-start */
/******************/
load_config();
/*************************************/
/* main page / visually similar page */
/*************************************/
//skip language codes in pathnames like "/jp/post/show"
let 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
const observer = register_observer(function(node) {
return (node.value === "apply-tag-script");
}, function(node, observer) {
observer.disconnect();
add_mode_options(node.parentNode);
return false;
});
const dropdown = document.getElementById("mode");
if (dropdown !== null) {
for (const child of dropdown.children) {
if (child.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
const observer = register_observer(function(node) {return node.id === "image"; }, function(node, observer) { configure_video(node); return true; });
const 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
const 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);
const 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 (!is_greasemonkey4) {
PostModeMenu_click_original = unsafeWindow.PostModeMenu.click;
//TODO will not work on Greasemonkey (need to replace click events just like with the mode change event)
unsafeWindow.PostModeMenu.click = PostModeMenu_click_override;
}
if (added_mode_options) {
//add_mode_options() was called early, as it should
PostModeMenu_init_workaround(); //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) {
if (mode_dropdown === null) return;
if (e.ctrlKey || e.altKey || e.shiftKey) return;
if (e.target === mode_dropdown) {
e.preventDefault(); //e.g. 'v' would otherwise change to 'View Posts'
} else {
const tag = e.target.tagName.toLowerCase();
if (tag === "input" || tag === "textarea" || tag === "select") {
return;
}
}
switch (e.key) {
case "v":
if (is_greasemonkey4) {
show_notice("addon error: 'Set Parent' not yet supported in Greasemonkey");
return;
}
mode_dropdown.value = "set-parent";
break;
case "c":
if (is_greasemonkey4) {
show_notice("addon error: 'Choose Parent' not yet supported in Greasemonkey");
return;
}
mode_dropdown.value = "choose-parent";
break;
case "q":
mode_dropdown.value = "rating-q";
break;
case "s":
mode_dropdown.value = "rating-s";
break;
case "e":
mode_dropdown.value = "rating-e";
break;
}
PostModeMenu_change_override();
}, true);
/*************/
/* post page */
/*************/
} else if (pathname.startsWith("/post/show/")) {
const hidden_post_id_el = document.getElementById("hidden_post_id");
if (hidden_post_id_el !== null) {
post_id = hidden_post_id_el.innerHTML;
} else {
post_id = pathname.substring(pathname.lastIndexOf("/") + 1);
}
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(); //initialize tag menu/buttons
if (config.tag_search_buttons) add_tag_search_buttons();
image_data = 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) {
const 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) {
show_notice("addon error: couldn't find post id?!");
} else {
open_in_tab(location.origin + "/post/similar?id=" + post_id);
}
break;
case "d": //d(elete)
if (post_id === null) {
show_notice("addon error: couldn't find post id?!");
} else if (!found_delete_action) {
show_notice("addon error: Delete action not found, no permission?");
} else {
open_in_tab(location.origin + "/post/delete/" + post_id);
}
break;
}
}, true);
if (config.scale_on_resize) add_scale_on_resize_listener();
add_tags_change_listener();
add_tags_submit_listener();
/*************/
/* wiki page */
/*************/
} else if (pathname.startsWith("/wiki/show")) {
try {
//add a "⚙" link to the edit tag page
const h2 = document.getElementsByClassName("title")[0];
const tag = new URL(window.location.href).searchParams.get("title");
const wiki_edit_link = document.createElement("A");
wiki_edit_link.href = "/tag/edit?name=" + tag;
wiki_edit_link.innerHTML = "⚙";
wiki_edit_link.title = "Edit Tag";
h2.appendChild(wiki_edit_link);
} catch (error) {
show_notice("addon error: couldn't add \"⚙\" tag page link, check console");
console_log(error);
}
}
}
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
init();
} else {
document.addEventListener("DOMContentLoaded", init, false);
}
})(typeof unsafeWindow !== "undefined" ? unsafeWindow : window);