// ==UserScript==
// @name SpookyX
// @description Enhances functionality of FoolFuuka boards. Developed further for more comfortable ghost-posting on the moe archives.
// @author Fiddlekins
// @version 32.50
// @namespace
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include http://**
// @include https://**
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @include
// @require
// @require
// @require
// @grant none
// @updateURL
// @downloadURL
// @icon
// ==/UserScript==
if (GM_info === undefined) {
var GM_info = {script: {version: '32.50'}};
}
var settings = {
"UserSettings": {
"inlineImages": {
"name": "Inline Images",
"description": "Load full-size images in the thread, enable click to expand",
"type": "checkbox",
"value": true,
"suboptions": {
"inlineVideos": {
"name": "Inline Videos",
"description": "Replace thumbnails of natively posted videos with the videos themselves",
"type": "checkbox",
"value": true,
"suboptions": {
"firefoxCompatibility": {
"name": "Firefox Compatibility Mode",
"description": "Turn this on to allow you to use the controls on an expanded video without collapsing it",
"type": "checkbox",
"value": false
}
}
},
"delayedLoad": {
"name": "Delayed Load",
"description": "Fullsize images are not automatically retrieved and used to replace the thumbnails. Instead this occurs on an individual basis when the thumbnails are clicked on",
"type": "checkbox",
"value": false
},
"imageHover": {
"name": "Image Hover",
"description": "Hovering over images with the mouse brings a full or window scaled version in view",
"type": "checkbox",
"value": true
},
"videoHover": {
"name": "Video Hover",
"description": "Hovering over videos with the mouse brings a full or window scaled version in view",
"type": "checkbox",
"value": true
},
"autoplayGifs": {
"name": "Autoplay embedded gifs",
"description": "Make embedded gifs play automatically",
"type": "checkbox",
"value": true
},
"autoplayVids": {
"name": "Autoplay embedded videos",
"description": "Make embedded videos play automatically (they start muted, expanding unmutes)",
"type": "checkbox",
"value": false
},
"customSize": {
"name": "Custom thumbnail size",
"description": "Specify the thumbnail dimensions",
"type": "checkbox",
"value": false,
"suboptions": {
"widthOP": {
"name": "OP image width",
"description": "The maximum width of OP images in pixels",
"type": "number",
"value": 250
},
"heightOP": {
"name": "OP image height",
"description": "The maximum height of OP images in pixels",
"type": "number",
"value": 250
},
"width": {
"name": "Post image width",
"description": "The maximum width of post images in pixels",
"type": "number",
"value": 125
},
"height": {
"name": "Post image height",
"description": "The maximum height of post images in pixels",
"type": "number",
"value": 125
}
}
},
"processSpoiler": {
"name": "Process spoilered images",
"description": "Make native spoilered images inline",
"type": "checkbox",
"value": true
}
}
},
"embedImages": {
"name": "Embed Media",
"description": "Embed image (and other media) links in thread",
"type": "checkbox",
"value": true,
"suboptions": {
"embedVideos": {
"name": "Embed Videos",
"description": "Embed video links in thread",
"type": "checkbox",
"value": true
},
"imgNumMaster": {
"name": "Embed Count",
"description": "The maximum number of images (or other media) to embed in each post",
"type": "number",
"value": 1
},
"titleYoutubeLinks": {
"name": "Title YouTube links",
"description": "Fetches the video name and alters the link text accordingly",
"type": "checkbox",
"value": true
}
}
},
"autoHost": {
"name": "Automatically Host Images",
"description": "When post is submitted image links will be automatically reuploaded to Imgur in an effort to avoid having dead 4chan image links",
"type": "select",
"value": {
"value": "Reupload 4chan links",
"options": ["Don't reupload links", "Reupload 4chan links", "Reupload all links"]
}
},
"embedGalleries": {
"name": "Embed Galleries",
"description": "Embed Imgur galleries into a single post for ease of image dumps",
"type": "checkbox",
"value": true,
"suboptions": {
"showDetails": {
"name": "Show Details",
"description": "Show the title, image description and view count for embedded Imgur albums",
"type": "checkbox",
"value": true
}
}
},
"gallery": {
"name": "Gallery",
"description": "Pressing G will bring up a view that displays all the images in a thread",
"type": "checkbox",
"value": true
},
"hidePosts": {
"name": "Hide Posts",
"description": "Allow user to hide posts manually",
"type": "checkbox",
"value": true,
"suboptions": {
"recursiveHiding": {
"name": "Recursive Hiding",
"description": "Hide replies to hidden posts",
"type": "checkbox",
"value": true,
"suboptions": {
"hideNewPosts": {
"name": "Hide New Posts",
"description": "Also hide replies to hidden posts that are fetched after page load",
"type": "checkbox",
"value": true
}
}
}
}
},
"newPosts": {
"name": "New Posts",
"description": "Reflect the number of new posts in the tab name",
"type": "checkbox",
"value": true
},
"favicon": {
"name": "Favicon",
"description": "Switch to a dynamic favicon that indicates unread posts and unread replies",
"type": "checkbox",
"value": true,
"suboptions": {
"customFavicons": {
"name": "Custom Favicons",
"description": "If disabled SpookyX will try its hand at automatically generating suitable favicons for the site. Enabling this allows you to manually specify which favicons it should use instead",
"type": "checkbox",
"value": false,
"suboptions": {
"unlit": {
"name": "Unlit",
"description": "Choose which favicon is used normally. Default is \""",
"type": "text",
"value": ""
},
"lit": {
"name": "Lit",
"description": "Choose which favicon is used to indicate there are unread posts. Preset numbers are 0-4, replace with link to custom image if you desire such as: \""",
"type": "text",
"value": "2"
},
"alert": {
"name": "Alert",
"description": "The favicon that indicates unread replies to your posts. Value is ignored if using a preset Lit favicon",
"type": "text",
"value": ""
},
"alertOverlay": {
"name": "Alert Overlay",
"description": "The favicon overlay that indicates unread replies. Default is \""",
"type": "text",
"value": ""
},
"notification": {
"name": "Notification image",
"description": "The image that is displayed in SpookyX generated notifications. 64px square is ideal. Default is \""",
"type": "text",
"value": ""
}
}
}
}
},
"labelYourPosts": {
"name": "Label Your Posts",
"description": "Add '(You)' to your posts and links that point to them",
"type": "checkbox",
"value": true
},
"inlineReplies": {
"name": "Inline Replies",
"description": "Click replies to expand them inline",
"type": "checkbox",
"value": true
},
"notifications": {
"name": "Enable notifications",
"description": "Browser notifications will be enabled, for example to alert you when your post has been replied to or if you encountered a posting error",
"type": "checkbox",
"value": true,
"suboptions": {
"spoiler": {
"name": "Hide spoilers",
"description": "When creating a notification to alert you of a reply the spoilered text will be replaced with black boxes since nofications cannot hide them like normal",
"type": "checkbox",
"value": true
},
"restrict": {
"name": "Restrict size",
"description": "Firefox option only. By default there is no size limit on Firefox notifications, use this option to keep notifications at a sensible size",
"type": "checkbox",
"value": false,
"suboptions": {
"lines": {
"name": "Line count",
"description": "Number of lines the notification is restricted to",
"type": "number",
"value": 5
},
"characters": {
"name": "Character count",
"description": "Number of characters per line the notification is restricted to",
"type": "number",
"value": 50
}
}
}
}
},
"relativeTimestamps": {
"name": "Relative Timestamps",
"description": "Timestamps will be replaced by elapsed time since post",
"type": "checkbox",
"value": true
},
"postQuote": {
"name": "Post Quote",
"description": "Clicking the post number will insert highlighted text into the reply box",
"type": "checkbox",
"value": true
},
"revealSpoilers": {
"name": "Reveal Spoilers",
"description": "Spoilered text will be displayed without needing to hover over it",
"type": "checkbox",
"value": false
},
"filter": {
"name": "Filter",
"description": "Hide undesirable posts from view",
"type": "checkbox",
"value": false,
"suboptions": {
"filterNotifications": {
"name": "Filter Notifications",
"description": "Enabling this will stop creating reply notifications if the reply is filtered with hide or remove mode. Purge mode filtered replies will never create notifications",
"type": "checkbox",
"value": true
},
"recursiveFiltering": {
"name": "Recursive Filtering",
"description": "Posts that reply to filtered posts will also be filtered",
"type": "checkbox",
"value": false
}
}
},
"adjustReplybox": {
"name": "Adjust Replybox",
"description": "Change the layout of the reply box",
"type": "checkbox",
"value": true,
"suboptions": {
"width": {
"name": "Width",
"description": "Specify the default width of the reply field in pixels",
"type": "number",
"value": 600
},
"hideQROptions": {
"name": "Hide QR Options",
"description": "Make the reply options hidden by default in the quick reply",
"type": "checkbox",
"value": true
},
"removeReset": {
"name": "Remove Reset",
"description": "Remove the reset button from the reply box to prevent unwanted usage",
"type": "checkbox",
"value": false
}
}
},
"postCounter": {
"name": "Post Counter",
"description": "Add a post counter to the reply box",
"type": "checkbox",
"value": true,
"suboptions": {
"location": {
"name": "Location",
"description": "Specify where the post counter is placed",
"type": "select",
"value": {"value": "Header bar", "options": ["Header bar", "Reply box"]}
},
"limits": {
"name": "Show count limits",
"description": "Adds count denominators, purely aesthetic",
"type": "checkbox",
"value": false,
"suboptions": {
"posts": {
"name": "Posts",
"description": "Specify the posts counter denominator",
"type": "number",
"value": 400
},
"images": {
"name": "Images",
"description": "Specify the images counter denominator",
"type": "number",
"value": 250
}
}
},
"countUnloaded": {
"name": "Count unloaded posts",
"description": "If only viewing the last x posts in a thread use this setting for the post counter to count the total number of posts rather than just the number of posts that have been loaded",
"type": "checkbox",
"value": true
},
"countHidden": {
"name": "Count hidden posts",
"description": "Adds a counter that displays how many posts of the total count are hidden",
"type": "checkbox",
"value": true,
"suboptions": {
"hideNullHiddenCounter": {
"name": "Auto-hide null hidden counter",
"description": "If there are no hidden posts the post counter will not display the hidden counter",
"type": "checkbox",
"value": true
}
}
}
}
},
"mascot": {
"name": "Mascot",
"description": "Place your favourite mascot on the background to keep you company!",
"type": "checkbox",
"value": false,
"suboptions": {
"mascotImage": {
"name": "Mascot image",
"description": "Specify a link to your custom mascot or leave blank for SpookyX defaults",
"type": "text",
"value": ""
},
"corner": {
"name": "Corner",
"description": "Specify which corner to align the mascot to",
"type": "select",
"value": {
"value": "Bottom Right",
"options": ["Top Right", "Bottom Right", "Bottom Left", "Top Left"]
}
},
"zindex": {
"name": "Z-index",
"description": "Determine what page elements the mascot is in front and behind of. Default value is -1",
"type": "number",
"value": -1
},
"opacity": {
"name": "Opacity",
"description": "Specify the opacity of the mascot, ranges from 0 to 1",
"type": "number",
"value": 1
},
"clickthrough": {
"name": "Click-through",
"description": "Allow you to click through the mascot if it is on top of buttons, etc",
"type": "checkbox",
"value": true
},
"width": {
"name": "Width",
"description": "Specify the width of the mascot in pixels. Use a negative number to leave it as the image's default width",
"type": "number",
"value": -1
},
"x": {
"name": "Horizontal Displacement",
"description": "Specify horizontal displacement of the mascot in pixels",
"type": "number",
"value": 0
},
"y": {
"name": "Vertical Displacement",
"description": "Specify vertical displacement of the mascot in pixels",
"type": "number",
"value": 0
},
"mute": {
"name": "Mute videos",
"description": "If using a video for a mascot the sound will be muted",
"type": "checkbox",
"value": true
}
}
},
"postFlow": {
"name": "Adjust post flow",
"description": "Change the way posts are laid out in the page",
"type": "checkbox",
"value": true,
"suboptions": {
"leftMargin": {
"name": "Left margin",
"description": "Specify the width in pixels of the gap between the start of the posts and the left side of the screen. Negative values set it to equal the mascot width",
"type": "number",
"value": 0
},
"rightMargin": {
"name": "Right margin",
"description": "Specify the width in pixels of the gap between the end of the posts and the right side of the screen. Negative values set it to equal the mascot width",
"type": "number",
"value": 0
},
"align": {
"name": "Align",
"description": "Specify how posts are aligned",
"type": "select",
"value": {"value": "Left", "options": ["Left", "Center", "Right"]}
},
"wordBreak": {
"name": "Word-break",
"description": "Firefox runs into difficulties with breaking really long words, test the options available until you find something that works. On auto this attempts to detect browser and select the most appropriate setting",
"type": "select",
"value": {"value": "Auto", "options": ["Auto", "Break-all", "Normal", "Overflow-Wrap"]}
}
}
},
"headerBar": {
"name": "Adjust Headerbar behaviour",
"description": "Determine whether the headerbar hides and how it does so",
"type": "checkbox",
"value": true,
"suboptions": {
"behaviour": {
"name": "Behaviour",
"description": "Firefox runs into difficulties with breaking really long words, test the options available until you find something that works. On auto this attempts to detect browser and select the most appropriate setting",
"type": "select",
"value": {
"value": "Collapse to button",
"options": ["Always show", "Full hide", "Collapse to button"]
},
"suboptions": {
"scroll": {
"name": "Hide on scroll",
"description": "Scrolling up will show the headerbar, scrolling down will hide it again",
"if": ["Full hide", "Collapse to button"],
"type": "checkbox",
"value": false
},
"defaultHidden": {
"name": "Default state hidden",
"description": "Check to make the headerbar hidden or collapsed by default on pageload",
"if": ["Full hide", "Collapse to button"],
"type": "checkbox",
"value": true
},
"contractedForm": {
"name": "Customise contracted form",
"description": "Specify what the contracted headerbar form contains",
"if": ["Collapse to button"],
"type": "checkbox",
"value": true,
"suboptions": {
"settings": {
"name": "Settings button",
"description": "Display the settings button in contracted headerbar",
"type": "checkbox",
"value": false
},
"postCounter": {
"name": "Post counter",
"description": "Display the post counter stats in contracted headerbar",
"type": "checkbox",
"value": true
}
}
}
}
},
"shortcut": {
"name": "Hide shortcut",
"description": "Pressing H will toggle the visiblity of the headerbar",
"type": "checkbox",
"value": true
}
}
},
"removeJfont": {
"name": "Remove Japanese Font",
"description": "Enabling this will make the addition of japanese characters to a post cease to change the post font and size. Presumably will cause issues for people whose default font doesn't support japanese characters",
"type": "checkbox",
"value": false
},
"labelDeletions": {
"name": "Label Deletions",
"description": "Enabling this will add 'Deleted' to all trashcan icons that designate deleted posts to allow for easier searching",
"type": "checkbox",
"value": false
}
},
"FilterSettings": {
"name": {
"name": "Name",
"value": [
{"comment": "#/久保島のミズゴロウ/;"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.post_author').html();
},
"responseObjFunction": function(response){
return response['name_processed'];
}
},
"tripcode": {
"name": "Tripcode",
"value": [
{"comment": "#/!!/90sanF9F3Z/;"},
{"comment": "#/!!T2TCnNZDvZu/;"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.post_tripcode').html();
},
"responseObjFunction": function(response){
return response['trip_processed'];
}
},
"uniqueID": {
"name": "Unique ID",
"value": [
{"comment": "# Remember to escape any special characters"},
{"comment": "# For example these are valid:"},
{"comment": "#/bUAl\\+t9X/;"},
{"comment": "#/ID:bUAl\\+t9X/;"},
{"comment": "# But this fails:"},
{"comment": "#/bUAl+t9X/; "},
{"comment": "# It's also worth noting that prefixing it with 'ID:' can cause the filter to fail to accurately detect when using recursive filtering. To assure it works fully stick to just using the hash like 'bUAl+t9X'"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.poster_hash').html();
},
"responseObjFunction": function(response){
return response['poster_hash_processed'];
}
},
"capcode": {
"name": "Capcode",
"value": [
{"comment": "# Set a custom class for mods:"},
{"comment": "#/Mod$/;highlight:mod;"},
{"comment": "# Set a custom class for moot:"},
{"comment": "#/Admin$/;highlight:moot;"},
{"comment": "# (highlighting isn't implemented yet)"},
{"comment": "# For recursive filter to always work you will need to add regex lines for M, A & D for Moderators, Admins and Developers respectively"},
{"comment": "# e.g. /A/; will filter Admins accurately always whilst /Admin/; won't always work for recursively filtered posts"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.post_level').html();
},
"responseObjFunction": function(response){
return response['capcode'];
}
},
"subject": {
"name": "Subject",
"value": [
{"comment": "#/(^|[^A-z])quest([^A-z]|$)/i;boards:tg;"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.post_title').html();
},
"responseObjFunction": function(response){
return response['title_processed'];
}
},
"comment": {
"name": "Comment",
"value": [
{"comment": "#/daki[\\\\S]*/i; boards:tg;"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.text').html();
},
"responseObjFunction": function(response){
return response['comment'];
}
},
"flag": {
"name": "Flag",
"value": [
{"comment": "#Remove kebob"},
{"comment": "#/turkey/i;mode:remove;"}
],
"threadPostFunction": function(currentPost){
return $(currentPost).find('.flag').attr('title');
},
"responseObjFunction": function(response){
return response['poster_country_name_processed'];
}
},
"filename": {
"name": "Filename",
"value": [],
"threadPostFunction": function(currentPost){
var combined = '';
if ($(currentPost).hasClass('thread')) {
combined = $(currentPost).find('.post_file_filename').html();
} else {
$.each($(currentPost).find('.post_file_filename'), function(){
combined += this.innerHTML;
});
}
return combined;
},
"responseObjFunction": function(response){
if (response['media'] === null || response['media'] === undefined) {
return '';
}
return response['media']['media_filename_processed'];
}
},
"fileurl": {
"name": "File URL",
"value": [
{"comment": "# Filter by site for example:"},
{"comment": "#/tumblr/;"}
],
"threadPostFunction": function(currentPost){
var combined = '';
var $currentPost = $(currentPost);
if ($currentPost.hasClass('thread')) {
var $currentPostFilename = $currentPost.find('.post_file_filename');
if ($currentPostFilename.length) {
combined = $currentPostFilename[0].href;
}
} else {
$.each($currentPost.find('.post_file_filename'), function(){
combined += this.href;
});
}
return combined;
},
"responseObjFunction": function(response){
if (response['media'] === null || response['media'] === undefined) {
return '';
}
return response['media']['remote_media_link'];
}
}
}
};
var defaultSettings = jQuery.extend(true, {}, settings);
var defaultMascots = [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
];
if (localStorage.SpookyXsettings !== undefined) {
$.extend(true, settings, JSON.parse(localStorage.SpookyXsettings));
}
var newPostCount = 0;
var notLoadedPostCount = 0;
var DocumentTitle = document.title;
var rulesBox = $(".rules_box").html();
var autoplayVid = '';
if (settings.UserSettings.inlineImages.suboptions.autoplayVids.value) {
autoplayVid = 'autoplay';
}
var filetypes = {
IMAGES: ['jpg', 'jpeg', 'png', 'gif'],
VIDEOS: ['webm', 'mp4', 'gifv']
};
var pattImageFiletypes = new RegExp('\\.(' + filetypes.IMAGES.join('|') + ')($|(\\?|:)[\\S]+$)', 'i');
var pattVideoFiletypes = new RegExp('\\.(' + filetypes.VIDEOS.join('|') + ')($|(\\?|:)[\\S]+$)', 'i');
var pattYoutubeLink = new RegExp('(youtube\\.com|youtu\\.be)', 'i');
var pattImgGal = new RegExp('http[s]?:);
var splitURL = (document.URL).toLowerCase().split("/");
var board = splitURL[3];
var threadID = splitURL[4];
if (threadID === "thread") {
threadID = splitURL[5];
} else if (threadID === "last") {
threadID = splitURL[6];
} else if (threadID !== "search" && threadID !== "reports") {
if (board === "_" || threadID === "page" || threadID === "ghost" || threadID === "" || threadID === undefined) {
if (board !== "" && board !== undefined && board !== "_") {
threadID = "board";
} else {
threadID = "other";
}
}
}
var boardPatt = new RegExp("(^|,)\\s*" + board + "\\s*(,|$)");
var Page = {
is: function(type){
if (Page.cache[type] !== undefined) {
return Page.cache[type];
} else {
if (Page.hasOwnProperty(type)) {
Page.cache[type] = Page[type]();
return Page.cache[type];
}
var typeArray = type.split(',');
for (var i = 0; i < typeArray.length; i++) {
if ((typeArray[i])) {
Page.cache[type] = true;
return true;
}
}
Page.cache[type] = false;
return false;
}
},
cache: {},
'thread': function(){
return /[0-9]+/.test(threadID);
},
'board': function(){
return /board/.test(threadID);
},
'gallery': function(){
return /gallery/.test(threadID);
},
'other': function(){
return /other/.test(threadID);
},
'quests': function(){
return /quests/.test(threadID);
},
'search': function(){
return /search/.test(threadID);
},
'statistics': function(){
return /statistics/.test(threadID);
}
};
;
+ board);
+ threadID);
// As taken from
function allowMockHover(){
// iterate over all styleSheets
for (var i = 0, l = document.styleSheets.length; i < l; i++) {
var s = document.styleSheets[i];
if (s.cssRules == null) continue;
// iterate over all rules in styleSheet
for (var x = 0, rl = s.cssRules.length; x < rl; x++) {
var r = s.cssRules[x];
if (r.selectorText && r.selectorText.indexOf(':hover') >= 0) {
fixRule(r);
}
}
}
}
function fixRule(rule){
// if the current rule has several selectors, treat them separately:
var parts = rule.selectorText.split(',');
for (var i = 0, l = parts.length; i < l; i++) {
if (parts[i].indexOf(':hover') >= 0) {
// update selector to be same + selector with class
parts[i] = [parts[i], parts[i].replace(/:hover/gi, '.mock-hover')].join(',');
}
}
// update rule
rule.selectorText = parts.join(',');
}
var imageWidthOP = 250;
var imageHeightOP = 250;
var imageWidth = 125;
var imageHeight = 125;
if (settings.UserSettings.inlineImages.suboptions.customSize.value) {
imageWidthOP = settings.UserSettings.inlineImages.suboptions.customSize.suboptions.widthOP.value;
imageHeightOP = settings.UserSettings.inlineImages.suboptions.customSize.suboptions.heightOP.value;
imageWidth = settings.UserSettings.inlineImages.suboptions.customSize.suboptions.width.value;
imageHeight = settings.UserSettings.inlineImages.suboptions.customSize.suboptions.height.value;
}
var yourPostsLookup = {};
if (('board,thread')) {
var crosslinkTracker = {};
if (localStorage.crosslinkTracker !== undefined) {
crosslinkTracker = JSON.parse(localStorage.crosslinkTracker);
}
if (crosslinkTracker[board] === undefined) {
crosslinkTracker[board] = {};
}
crosslinkTracker[board][threadID] = {};
localStorage.crosslinkTracker = JSON.stringify(crosslinkTracker);
}
var faviconUnlit;
var faviconLit;
var faviconAlert;
var faviconNotification;
var faviconState = "unlit";
function generateFavicons(){ // Generate dynamic favicons
if (settings.UserSettings.favicon.suboptions.customFavicons.value) {
switch (settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value) {
case "0":
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value = "";
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.alert.value = "";
break;
case "1":
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value = "";
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.alert.value = "";
break;
case "2":
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value = "";
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.alert.value = "";
break;
case "3":
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value = "";
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.alert.value = "";
break;
case "4":
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value = "";
settings.UserSettings.favicon.suboptions.customFavicons.suboptions.alert.value = "";
break;
default:
break;
}
faviconUnlit = settings.UserSettings.favicon.suboptions.customFavicons.suboptions.unlit.value; // Store unlit favicon
faviconLit = settings.UserSettings.favicon.suboptions.customFavicons.suboptions.lit.value; // Store lit favicon
faviconAlert = settings.UserSettings.favicon.suboptions.customFavicons.suboptions.alert.value; // Store alert favicon
faviconNotification = settings.UserSettings.favicon.suboptions.customFavicons.suboptions.notification.value; // Store notification favicon
setFavicon();
} else {
var faviconCanvas = document.createElement('canvas');
var nativeFavicon = $('');
var overlayFavicon = $('",
"/": "?",
"\\": "|"
};
Keys - and their codes
var special_keys = {
'esc': 27,
'escape': 27,
'tab': 9,
'space': 32,
'return': 13,
'enter': 13,
'backspace': 8,
'scrolllock': 145,
'scroll_lock': 145,
'scroll': 145,
'capslock': 20,
'caps_lock': 20,
'caps': 20,
'numlock': 144,
'num_lock': 144,
'num': 144,
'pause': 19,
'break': 19,
'insert': 45,
'home': 36,
'delete': 46,
'end': 35,
'pageup': 33,
'page_up': 33,
'pu': 33,
'pagedown': 34,
'page_down': 34,
'pd': 34,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123
};
var modifiers = {
shift: {wanted: false, pressed: false},
ctrl: {wanted: false, pressed: false},
alt: {wanted: false, pressed: false},
meta: {wanted: false, pressed: false} is Mac specific
};
if (e.ctrlKey) modifiers.ctrl.pressed = true;
if (e.shiftKey) modifiers.shift.pressed = true;
if (e.altKey) modifiers.alt.pressed = true;
if (e.metaKey) modifiers.meta.pressed = true;
for (var i = 0; k = keys[i], i < keys.length; i++) {
if (k == 'ctrl' || k == 'control') {
kp++;
modifiers.ctrl.wanted = true;
} else if (k == 'shift') {
kp++;
modifiers.shift.wanted = true;
} else if (k == 'alt') {
kp++;
modifiers.alt.wanted = true;
} else if (k == 'meta') {
kp++;
modifiers.meta.wanted = true;
} else if (k.length > 1) { it is a special key
if (special_keys[k] == code) kp++;
} else if (opt.keycode) {
if (opt.keycode == code) kp++;
} else { special keys did not match
if (character == k) kp++;
else {
if (shift_nums[character] && e.shiftKey) { Shift key bug created by using lowercase
character = shift_nums[character];
if (character == k) kp++;
}
}
}
}
if (kp == keys.length &&
modifiers.ctrl.pressed == modifiers.ctrl.wanted &&
modifiers.shift.pressed == modifiers.shift.wanted &&
modifiers.alt.pressed == modifiers.alt.wanted &&
modifiers.meta.pressed == modifiers.meta.wanted) {
callback(e);
if (!opt.propagate) { the event
is supported by IE - this will kill the bubbling process.
e.cancelBubble = true;
e.returnValue = false;
works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return false;
}
}
};
this.all_shortcuts[shortcut_combination] = {
'callback': func,
'target': ele,
'event': opt.type
};
the function with the event
if (ele.addEventListener) ele.addEventListener(opt.type, func, false);
else if (ele.attachEvent) ele.attachEvent('on' + opt.type, func);
else ele['on' + opt.type] = func;
},
the shortcut - just specify the shortcut and I will remove the binding
'remove': function(shortcut_combination){
shortcut_combination = shortcut_combination.toLowerCase();
var binding = this.all_shortcuts[shortcut_combination];
delete(this.all_shortcuts[shortcut_combination]);
if (!binding) return;
var type = binding.event;
var ele = binding.target;
var callback = binding.callback;
if (ele.detachEvent) ele.detachEvent('on' + type, callback);
else if (ele.removeEventListener) ele.removeEventListener(type, callback, false);
else ele['on' + type] = false;
}
};
function delayedLoad(posts){
posts.each(function(i, post){
($(post).hasClass('thread') ? $(post).children('.thread_image_box').find('img') : $(post).find('img')).each(function(i, image){
var $image = $(image);
$image.data('dontHover', true); // Stop imageHover displaying the thumbnails
$image.on('click', function(e){ // Stop the OP from returning all the sub images and thus duplicating them
if (!e.originalEvent.ctrlKey && e.which == 1) {
e.preventDefault();
var $target = $(e.target);
$target.removeData('dontHover');
$target.off('click'); // Remove event listener now that it's served its purpose
inlineImages($target.closest('article'));
if (!settings.UserSettings.inlineImages.suboptions.autoplayGifs.value) {
pauseGifs($target);
} // Stop gifs autoplaying
}
});
});
});
}
function inlineImages(posts){
posts.each(function(i, post){
var $post = $(post);
($post.hasClass('thread') ? $post.children('.thread_image_box') : $post.find('.thread_image_box')).each(function(i, currentImage){
var $currentImage = $(currentImage);
$currentImage.find('>a').each(function(j, imgLink){
var fullImage = imgLink.href;
if (settings.UserSettings.inlineImages.suboptions.processSpoiler.value && $currentImage.find('.spoiler_box').length) {
$(imgLink).html('
Spoiler
');
var $image = $currentImage.find('img');
$('load', function(e){
$currentImage.find(".spoilerText").css({"top": (e.target.height / 2) - 6.5}); // Center spoiler text
});
}
if (/\.webm$/i.test(fullImage)) { // Handle post webms
if (settings.UserSettings.inlineImages.suboptions.inlineVideos.value) {
$currentImage.prepend('');
$(imgLink).remove();
addHover($currentImage);
}
} else if (!/\.(pdf|swf)$/i.test(fullImage)) {
$currentImage.find('img').each(function(k, image){
var $image = $(image);
var thumbImage = $(image).attr('src');
$image.attr('src', fullImage);
$image.error(function(){ // Handle images that won't load
if (!$image.data('tried4pleb')) {
$image.data('tried4pleb', true);
var imgLink4pleb = fullImage.replace(', ');
$image.attr('src', imgLink4pleb);
$image.parent().attr('href', imgLink4pleb); // Change link
} else if (!$image.data('triedThumb')) {
$image.data('triedThumb', true);
if (fullImage !== thumbImage) { // If the image has a thumbnail aka was 4chan native then use that
$image.attr('src', thumbImage);
$image.parent().attr('href', fullImage); // Reset link if changed to 4pleb attempt
}
}
});
addHover($currentImage);
});
}
});
});
});
}
function getSelectionText(){
var text = "";
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
return text;
}
function togglePost(postID, mode){
var $postID = $('#' + postID);
if (mode == "hide") {
$postID.css({'display': 'none'});
$postID.prev().css({'display': 'block'});
} else if (mode == "show") {
$postID.css({'display': 'block'});
$postID.prev().css({'display': 'none'});
} else {
$postID.toggle();
$postID.prev().toggle();
}
postCounter(); // Update hidden post counter
}
function recursiveToggle(postID, mode){
var checkedPostCollection = {};
var postList = [postID];
for (var i = 0; i < postList.length; i++) {
checkedPostCollection[postList[i]] = true;
$('#p_b' + postList[i] + ' > a').each(function(i, backlink){
var backlinkID = ;
if (!checkedPostCollection[backlinkID]) {
postList.push(backlinkID);
}
});
}
for (var j = 0, len = postList.length; j < len; j++) {
togglePost(postList[j], mode);
}
}
function filter(posts){
posts.each(function(index, currentPost){
var $currentPost = $(currentPost);
if (!/!!UG0p3gRn3T1/.test($currentPost.find('.post_tripcode').html())) {
if (settings.UserSettings.filter.suboptions.recursiveFiltering.value && !$currentPost.hasClass('thread')) { // Recursive filter and not OP
var checkedBacklinks = {};
$currentPost.find('.text .backlink').each(function(i, backlink){
if (!checkedBacklinks[backlink.dataset.board + ]) { // Prevent reprocessing duplicate links
checkedBacklinks[backlink.dataset.board + ] = true;
var backlinkPost = $('#' + );
if (backlink.dataset.board === board && backlinkPost.length) { // If linked post is present in thread
if ((':visible')) { // If linked post is visible
if (backlinkPost.hasClass('shitpost')) { // If linked post is a shitpost
$currentPost.addClass('shitpost');
}
} else { // Linked post isn't visible
if (backlinkPost.prev().is(':visible')) { // If the hide post stub is visible (and thus the linked post is hidden)
togglePost(, "hide");
} else { // The linked post has been filtered with mode remove
$currentPost.hide();
}
}
} else { // Linked post isn't present in thread
$.ajax({
url: "/_/api/chan/post/",
data: {"board": backlink.dataset.board, "num": },
type: "GET"
}).done(function(response){
processPosts(checkFilter(response, false), $currentPost, currentPost);
$currentPost.find('.backlink_list .backlink').each(function(j, replyBacklink){ // Filter replies
filter($('#' + ));
});
});
}
}
});
}
if ($(currentPost).length) { // If after all that the post hasn't been purged
processPosts(checkFilter(currentPost, true), $currentPost, currentPost);
}
}
});
}
function processPosts(type, $currentPost, currentPost){
switch (type) {
case 1:
$currentPost.addClass('shitpost');
break;
case 2:
togglePost(, 'hide');
break;
case 3:
$currentPost.hide();
break;
case 4:
$currentPost.prev().remove();
$currentPost.remove();
}
}
function checkFilter(input, inThreadPost){
var output = 0;
for (var filterType in settings.FilterSettings) {
if (settings.FilterSettings.hasOwnProperty(filterType)) {
var testText = inThreadPost ? settings.FilterSettings[filterType].threadPostFunction(input) : settings.FilterSettings[filterType].responseObjFunction(input);
var shortcut = settings.FilterSettings[filterType].value;
for (var line in shortcut) {
if (shortcut.hasOwnProperty(line) && !shortcut[line].comment && shortcut[line].regex !== undefined) {
if (shortcut[line].boards === undefined || boardPatt.test(shortcut[line].boards)) {
var regex = new RegExp(shortcut[line].regex.pattern, shortcut[line].regex.flag);
if (regex.test(testText)) {
switch (shortcut[line].mode) {
case "fade":
if (output < 1) {
output = 1;
}
break;
case "hide":
if (output < 2) {
output = 2;
}
break;
case "remove":
output = 3;
break;
case "purge":
return 4;
default:
if (output < 1) {
output = 1;
}
}
}
}
}
}
}
}
return output;
}
var embedImages = function(posts){
posts.each(function(index, currentArticle){
var $currentArticle = $(currentArticle);
if (!$currentArticle.data('imgEmbed')) {
$currentArticle.data('imgEmbed', true);
var imgNum = settings.UserSettings.embedImages.suboptions.imgNumMaster.value - $currentArticle.find('.thread_image_box').length;
var isOP = $currentArticle.hasClass('thread');
(isOP ? $currentArticle.children('.text').find('a') : $currentArticle.find('.text a')).each(function(index, currentLink){
if (imgNum === 0) {
return false;
}
var mediaType = 'notMedia';
var mediaLink = currentLink.href;
if (pattImageFiletypes.test(mediaLink)) {
mediaType = 'image';
} else if (pattVideoFiletypes.test(mediaLink)) {
mediaType = 'video';
} else if (pattYoutubeLink.test(mediaLink)) {
mediaType = 'youtube';
}
if (mediaType == 'image' || mediaType == 'video') {
imgNum--;
var filename = '
');
}
removeLink(currentLink);
} else if (imgurLinkFragments[3] !== "gallery" && !isOP) { // I can't be bothered making this work properly for OPs, so let's just ignore it and pretend it doesn't exist, yes?
var link = pattImgGal.exec(currentLink.href);
var individualImages = link[0].match(/[A-z0-9]{7}/g);
var allDisplayed = true; // Track whether all of the linked images get inserted so that if not the link can be kept
$.each(individualImages, function(i, imgID){
if (imgNum) {
imgNum--;
var filename = '
';
$('#settingsContent').html(settingsHTML);
if (!settings.UserSettings.filter.value) {
$('#filterDisabledMessage').show(); // Show filter disabled message if the filter is disabled
}
$('#settingsMenu').show(); // Show the menu
$('#settingsContent .suboption-list > :visible:last').addClass('last');
$('#settingsContent > div').hide(); // Hide all tabs
$('#settingsContent #' + $('.sections-list .active').html()).show(); // Show active tab
$('#settingsContent select, #settingsContent input').each(function(i, el){
if (el.type !== "checkbox") { // Add the top margins for non-checkboxes to align description with name
$(el).parent().next().addClass('selectDescription');
}
if (el.nodeName === "SELECT") { // Hide the settings join line for select options that start with one or less visible suboptions
var visibleSubopsCount = 0;
$(el).closest('div:not(.settingFlexContainer)').children('.suboption-list').children().each(function(i, subop){
if ($(subop).css('display') !== "none") {
visibleSubopsCount++;
}
});
if (visibleSubopsCount <= 1) {
$(el).closest('.settingFlexContainer').children('.settingsJoinLine').hide();
}
}
});
updateExportLink(); // Create the export link
}
}
function generateFilterHTML(){
var settingsHTML = '';
var type;
settingsHTML += '
The Filter is currently disabled. Turn it on via the setting in the Main tab
Use regular expressions, one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string weeaboo, case-insensitive.
You can use these settings with each regular expression, separate them with semicolons:
Per boards, separate them with commas. It is global if not specified. For example: boards:a,tg;
Set the way the filter will handle the post with mode For example: mode:hide; Valid options are:
purge: Remove the post from the page entirely, the site will need to reload the post for hoverlinks and such to work.
remove: Remove the post from view but leave it in the page.
hide: Collapse the post, leave a button to restore it.
fade: Simply halve the opacity of the post. This is the default if the mode isn\'t specified.
';
for (type in settings.FilterSettings) {
if (settings.FilterSettings.hasOwnProperty(type)) {
settingsHTML += '';
}
}
return settingsHTML;
}
function generateSubOptionHTML(input, path){
var settingsHTML = '';
$.each(input, function(key, value){
if ( !== undefined) {
var checked = '';
var subOpsHidden = '';
if (value.value) {
checked = ' checked';
} else {
subOpsHidden = ' style="display: none;"';
}
if (value.if !== undefined) {
var parentPath = objpath(settings.UserSettings, path.substring(0, path.length - '.suboptions.'.length));
var pattTest = new RegExp(parentPath.value.value);
var ifMet = false;
$.each(value.if, function(i, v){
if (pattTest.test(v)) {
ifMet = true;
return false;
}
});
if (ifMet) {
settingsHTML += '