您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a few quality of life improvements on SankakuChan: 'Set Parent', image scaling, duplicate tagging/flagging, muting videos. Fully configurable using localStorage.
当前为
// ==UserScript== // @name SankakuAddon // @namespace SankakuAddon // @description Adds a few quality of life improvements on SankakuChan: 'Set Parent', 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.91 // @grant none // ==/UserScript== var version = "v0.91" //TODO check if compatible with chrome //TODO add option to disable flash video scaling //TODO always load config from local storage or use some kind of eventlistener for config changes //TODO do more sanity checks on duplicate tag (e.g. only allow saving when parent id is set) //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 are not muted (unfixable) /***************************/ /* configuration functions */ /***************************/ var default_config = { scroll_to_image:true, scale:true, scale_mode:0, //hardcoded exception in save/load_config scale_on_resize:false, video_mute:true, video_controls:true, setparent_deletepotentialduplicate:false, hide_headerlogo:false }; var config = JSON.parse(JSON.stringify(default_config)); var use_local_storage = (typeof(Storage) !== "undefined"); function save_config() { for (var key in config) { var cfg_key = "config." + 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") { config[key] = cfg_elem.value; } else { config[key] = cfg_elem.checked; } } if (use_local_storage) { localStorage.setItem("addon_config", JSON.stringify(config)); } else { notice("addon: couldn't save settings to local storage. check permissions."); } } function load_config() { if (use_local_storage) { var value = localStorage.getItem("addon_config"); if (value) config = JSON.parse(value); } for (var key in config) { var cfg_key = "config." + key; var cfg_elem = document.getElementById(cfg_key); if (cfg_elem === null) { notice("addon error: couldn't find config element " + cfg_key + "!"); return; } cfg_elem.addEventListener("input", function(e) { save_config(); hide_headerlogo(config.hide_headerlogo); }); if (key === "scale_mode") { cfg_elem.value = config[key]; } else { cfg_elem.checked = config[key]; } } } function reset_config() { config = JSON.parse(JSON.stringify(default_config)); if (use_local_storage) { localStorage.clear(); //WARNING: might break other scripts } load_config(); } 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(nodeID, nodeModifier) { 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 == nodeID) { nodeModifier(node); observer.disconnect(); return; } } } }); observer.observe(document, {childList: true, subtree: true}); } /*********************/ /* general functions */ /*********************/ function hide_headerlogo(bool) { document.getElementById("headerlogo").style.display = (bool ? "none" : ""); } /***********************************************/ /* 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 */ /***********************/ 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 parent_id = document.getElementById("post_parent_id"); if (parent_id) { parent_id = parent_id.value; } //TODO check if parent_id was actually saved, not just typed if (!parent_id || parent_id.length === 0) { notice("addon: no parent id set!"); return false; } var old_tags = get_old_tags_array(); if (old_tags.indexOf("legitimate_variation") != -1) { notice("addon: tagged as legitimate_variation, are you sure it is a duplicate?"); return false; } if (old_tags.indexOf("duplicate") == -1) { notice("addon: not tagged as 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); } } }); } var img_aspect_ratio = null; //save image aspect ratio instead of recalculating //stretch image/video/flash function scale_image(mode) { mode = Number(mode) if (mode === NaN) { notice("addon error: scaling mode wasn't a number?"); return; } var img = document.getElementById("image"); //image or video if (!img) { //flash video img = document.getElementById("non-image-content"); if (!img) return; img = img.getElementsByTagName("embed")[0]; if (!img) return; } if (img_aspect_ratio === null) img_aspect_ratio = img.width / img.height; //reset scaling if (mode === -1) { img.style.width = null; img.style.height = null; return; } //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) { img.style.width = Math.floor(img_rect_w) + "px"; img.style.height = Math.floor(img_rect_w / img_aspect_ratio) + "px"; //vertical } else if (mode === 2) { img.style.width = Math.floor(img_rect_h * img_aspect_ratio) + "px"; img.style.height = Math.floor(img_rect_h) + "px"; } } 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.lastIndexOf("/post/similar", 0) === 0) { // 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.lastIndexOf("/post/show/", 0) === 0) { /* mutes all videos in document var mute_videos = function() { var videos = document.querySelectorAll("video"); for (var i = 0; i < videos.length; i++) { if (config.video_mute) videos[i].muted = true; if (config.video_controls) videos[i].controls = true; } }; */ 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() { //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: 170px; width: 400px; height: 225px; 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='config.scroll_to_image' type='checkbox'>" + "<span>Scroll to image when opening post</span><br>" + "</span>" + "<span>" + "<input id='config.scale' type='checkbox'>" + "<span>Scale image when opening post</span><br>" + "</span>" + "<span>" + "<input id='config.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 mode: </span>" + "<select id='config.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='config.video_mute' type='checkbox'>" + "<span>Mute videos*</span><br>" + "</span>" + "<span>" + "<input id='config.video_controls' type='checkbox'>" + "<span>Show video controls*</span><br>" + "</span>" + "<span>" + "<input id='config.setparent_deletepotentialduplicate' type='checkbox'>" + "<span>Delete potential_duplicate tag when using \"Set Parent\"</span><br>" + "</span>" + "<span>" + "<input id='config.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); document.getElementById("config_close").onclick = function() { show_config_overlay(false); return false; }; document.getElementById("config_reset").onclick = function() { reset_config(); hide_headerlogo(config.hide_headerlogo); //TODO automatically update return false; }; //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() { load_config(); 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(); hide_headerlogo(config.hide_headerlogo); //TODO automatically update /*************************************/ /* main page / visually similar page */ /*************************************/ if (location.pathname === "/" || location.pathname.lastIndexOf("/post/similar", 0) === 0) { 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.lastIndexOf("/post/show/", 0) === 0) { if (config.scale) scale_image(config.scale_mode); if (config.scroll_to_image) scroll_to_image(); window.addEventListener("resize", function(e) { if (config.scale && config.scale_on_resize) scale_image(config.scale_mode); }); var post_id = document.getElementById("hidden_post_id").innerHTML; if (post_id % 1 !== 0) { notice("addon error: hidden_post_id not found! Addon actions disabled."); return; } var li = document.getElementById("add-to-pool"); if (li === null) { notice("addon error: couldn't find \"add-to-pool\" element! Addon actions disabled."); return; } //add actions 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"); 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 === null) { notice("addon error: couldn't find \"edit_form\" element! Tag buttons disabled."); return; } var button_place = edit_form.children[1].children[0].children[0].children[0]; //TODO add 'clear parent id' button 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);