您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Make up for how limited Fanfiction.net's result filtering is
当前为
// ==UserScript== // @name Fanfiction.net Unwanted Result Filter // @namespace http://www.ficfan.org/ // @description Make up for how limited Fanfiction.net's result filtering is // compared to sites like Twisting the Hellmouth. // @copyright 2014-2015, Stephan Sokolow (http://www.ssokolow.com/) // @license MIT; http://www.opensource.org/licenses/mit-license.php // @version 0.1.9 // // @require http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.2.min.js // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_log // @noframes // // @compatible firefox Tested regularly under Greasemonkey. // @compatible chrome Tested occasionally under Tampermonkey. // // @match *://www.fanfiction.net/* // ==/UserScript== (function($) { /* NOTE TO USERS OF VERSION 0.0.1: * Sorry for erasing any modifications you made to the filter settings. * * I didn't put much thought into version 0.0.1 and, as a result, it was * impossible to release an update without erasing modifications. * * I'm now storing settings via GM_setValue, so it should never happen again * and there's a proper configuration GUI available at * Greasemonkey > User Script Commands... > Configure Result Filter... */ // ----==== Configuration ====---- var IGNORED_OPACITY = 0.3; var fieldDefs = { 'filter_slash': { 'section': ['Slash Filter', 'Hide stories with slash based on warnings in story descriptions'], 'label': 'Enabled', 'labelPos': 'right', 'type': 'checkbox', 'default': true, }, 'hide_slash': { 'label': 'No Placeholder', 'labelPos': 'right', 'type': 'checkbox', 'default': false, }, 'slash_pat': { 'label': 'Slash is...', 'title': 'A regular expression which matches slash in descriptions', 'type': 'text', 'size': 255, 'default': ".*(slash|yaoi)([.,!:) -]|$)" }, 'not_slash_pat': { 'label': '...but not...', 'title': 'A regular expression which matches "not slash" and so on', 'type': 'text', 'size': 255, 'default': ".*(fem|not?[ ]+)(slash|yaoi)([.,!: ]|$)" }, 'filter_cats': { 'section': ['Unwanted Category Filter', 'Hide unwanted fandoms in author pages and "All Crossovers" searches'], 'label': 'Enabled', 'labelPos': 'right', 'type': 'checkbox', 'default': true, }, 'hide_cats': { 'label': 'No Placeholder', 'labelPos': 'right', 'type': 'checkbox', 'default': false, }, 'unwanted_cats': { 'label': 'Unwanted categories (One per line, blank lines and lines ' + 'beginning with # will be ignored):', 'type': 'textarea', 'size': 100, 'default': [ "# --== Never wanted ==-- ", "Invader Zim", "Supernatural", "Twilight", "", "# --== Not right now ==-- ", "Harry Potter & Avengers", "Naruto", ].join("\n"), }, 'unwanted_cats_escape': { 'label': 'Lines are literal strings (uncheck for regular expressions)', 'labelPos': 'right', 'title': 'NOTE: Leading/trailing whitespace is always ignored and ' + 'newlines always have OR behaviour.', 'type': 'checkbox', 'default': true, }, 'unwanted_cats_commute': { 'label': 'Automatically generate "B & A" lines from "A & B" lines', 'labelPos': 'right', 'title': "WARNING: This will break regexes with & inside () or []", 'type': 'checkbox', 'default': true, }, 'filter_manual': { 'section': ['Manual Filter', 'Hide an arbitrary list of story IDs'], 'label': 'Enabled', 'labelPos': 'right', 'type': 'checkbox', 'default': true, }, 'hide_manual': { 'label': 'No Placeholder', 'labelPos': 'right', 'type': 'checkbox', 'default': false, }, 'manual_reason': { 'label': 'Reason to display in placeholders:', 'type': 'text', 'size': 255, 'default': "Already Read" }, 'unwanted_manual': { 'label': 'Unwanted story IDs (One per line, blank lines and lines ' + 'beginning with # will be ignored):', 'type': 'textarea', 'size': 100, 'default': "", }, // TODO: Ideas for future filters: // - Genre (allowing more than one whitelist/blacklist entry) // TODO: Ideas for filters requiring an in-page UI: // - sort orders not already offered (eg. faves/follows on authors faves) // - min/max words as a freeform range entry // - filter sets which can be toggled }; var frame = $('<div>').appendTo('body')[0]; var config_params = { 'id': 'ffnet_result_filter', 'title': 'Fanfiction.net Unwanted Result Filter', 'fields': fieldDefs, 'css': ('#ffnet_result_filter ' + [ // Match Fanfiction.net styling more closely ".section_header { background-color: #339 !important; }", ".section_desc { background-color: #f6f7ee !important; border: none; " + " padding: 1px; }", ".config_header { font-size: 16pt; }", ".field_label { font-size: 13px; font-weight: normal; }", // Layout adjustments for using a non-iframe container "label { display: inline; }", ".section_header_holder { padding: 15px; margin-bottom: 2em; }", ".modal-footer { margin-top: -2em; }", ".saveclose_buttons.btn { margin: 0 0 0 5px !important; }", ".reset_holder { padding-right: 12px; }", "input[type=checkbox] { margin: 2px 4px 2px; }", // Make the panel sanely scrollable "#ffnet_result_filter_wrapper { " + " display: flex; flex-direction: column; height: 100%; }\n" + "#ffnrfilter_contentbox { " + " flex: 1 1 auto; min-height: 0px; overflow-y: scroll; }\n" + // Form layout fixes "input[type=text], textarea " + " { width: calc(100% - 1.1em); resize: vertical; }\n" + "#ffnet_result_filter_filter_manual_var, " + "#ffnet_result_filter_filter_slash_var, " + "#ffnet_result_filter_filter_cats_var, " + "#ffnet_result_filter_hide_manual_var, " + "#ffnet_result_filter_hide_slash_var, " + "#ffnet_result_filter_hide_cats_var, " + "#ffnet_result_filter_slash_pat_var, " + "#ffnet_result_filter_not_slash_pat_var " + " { display: inline-block; margin-right: 1em !important; } " + "#ffnet_result_filter_field_slash_pat, " + "#ffnet_result_filter_field_not_slash_pat " + " { max-width: 20em; } " + "#ffnet_result_filter_field_unwanted_cats { min-height: 10em; }" ].join('\n#ffnet_result_filter ')), 'events': { 'open': function(doc) { // Reconcile GM_config and Bootstrap CSS $(this.frame).css({ 'z-index': 1050, 'top': '50px', 'height': 'calc(99% - 100px)', 'border-color': '#d4d4d4', }).addClass('modal fade in'); $('button', this.frame).addClass('btn'); $('.reset_holder').addClass('btn pull-left'); $('#ffnet_result_filter_buttons_holder').addClass('modal-footer'); $('#ffnet_result_filter_slash_pat_var').before("<br>"); var header = $('.config_header').addClass('modal-header'); // Move the content into a wrapper DIV for styling $('<div>', {id: 'ffnrfilter_contentbox'}) .insertAfter(header) .append($('.section_header_holder').detach()); // Add a clickable backdrop for the panel $("<div>", {'id': 'gm_modal_back'}).click(function() { GM_config.close(); }).addClass('modal-backdrop fade in').appendTo('body'); }, 'close': function() { // Plumb the added backdrop into the close handler $('#gm_modal_back').remove(); } }, 'frame': frame, }; // ----==== Functions ====---- /// Escape string for literal meaning inside a regular expression /// Source: http://stackoverflow.com/a/3561711/435253 var re_escape = function(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); }; /// Use with Array.filter() to remove comments and blank lines var rows_filter = function(elem, idx, arr) { elem = elem.trim() if (elem.charAt(0) == '#' || !elem) { return false; } return true; }; /// Parse an array from a newline-delimited string containing # comments var parse_lines = function(s) { return s.split("\n").map(function(e, i, a) { return e.trim(); }).filter(rows_filter); }; /// Parse a usable list of category patterns from a raw string var parse_cats_list = function(s, escape, commute) { // Parse the config var cats_raw = parse_lines(s); if (escape) { cats_raw = cats_raw.map(re_escape) } if (commute) { var cats_out = []; for (var i = 0, len = cats_raw.length; i < len; i++) { var line = cats_raw[i]; cats_out.push(line); var parts = line.split(' & '); if (parts.length > 1) { cats_out.push(parts[1] + ' & ' + parts[0]); } } } return cats_out; }; // Parse a usable list of story IDs from a raw string var parse_id_list = function(s) { return parse_lines(s).filter(rows_filter); }; /// Hide a story entry in a way it can be recovered var hide_entry = function(node) { $(node).addClass('filtered').hide(); }; /// Hide a story entry and add a clickable placeholder var add_placeholder = function(node, reason) { var $story = $(node); var $placeholder = $story.clone(); $placeholder.html("Click to Show (" + reason + ")").css({ minHeight: 0, maxHeight: '1em', color: 'lightgray', textAlign: 'center', cursor: 'pointer', }).click($story, function(e) { $(this).slideUp(); e.data.css('min-height', 0).slideDown(); }).addClass('filter_placeholder').insertBefore($story); hide_entry($story); }; /// Code which must be re-run to reapply filters after changing the config var initialize_filters_and_apply = function() { // Parse the config var bad_cats = parse_cats_list( GM_config.get('unwanted_cats'), GM_config.get('unwanted_cats_escape'), GM_config.get('unwanted_cats_commute') ); if (GM_config.get('filter_manual')) { var manual_story_ids = parse_id_list(GM_config.get('unwanted_manual')); } else { var manual_story_ids = []; } var story_link_re = new RegExp("\/s\/(\\d+)\/"); // Generate RegExp objects from the parsed config var slash_re = new RegExp(GM_config.get('slash_pat'), 'i'); var not_slash_re = new RegExp(GM_config.get('not_slash_pat'), 'i'); var cats_re = new RegExp("(?:^|- )(.*(" + bad_cats.join('|') + ').*) - Rated:.*'); var cat_link_re = new RegExp(bad_cats.join('|')); // Clean up after any previous run $(".filter_placeholder").remove(); $(".filtered").show(); $(".ignored").css('opacity', 1).show().removeClass('ignored'); var results = $(".z-list"); for (var i = 0, reslen = results.length; i < reslen; i++) { var story = results[i]; var meta_row = $('.xgray', story).text(); var description = $('.z-padtop', story).contents()[0].data; // TODO: Redesign to collapse runs of hidden entries var reason = null; if (GM_config.get('filter_slash') && slash_re.test(description) && !not_slash_re.test(description)) { if (GM_config.get('hide_slash')) { hide_entry(story); } else { add_placeholder(story, "Slash"); } continue; } if (GM_config.get('filter_cats')) { var matches = meta_row.match(cats_re); if (matches && matches.length > 0) { if (GM_config.get('hide_cats')) { hide_entry(story); } else { add_placeholder(story, matches[1]); } continue; } } if (manual_story_ids.length > 0) { var story_url = $('a.stitle', story).attr('href'); var story_id = story_link_re.exec(story_url)[1]; if (story_id && manual_story_ids.indexOf(story_id) != -1) { if (GM_config.get('hide_manual')) { hide_entry(story); } else { add_placeholder(story, GM_config.get('manual_reason')); } } } } if (GM_config.get('filter_cats')) { var hide_cats = GM_config.get('hide_cats'); $('#list_output a').each(function() { $this = $(this); // "Browse" wraps the title and entry count in a <div> that lets us // easily fade both while "Community" puts them directly in the <td> // which defines the column. var $parent = $this.parent('div'); if ($parent.length > 0) { $this = $parent; } if (cat_link_re.test($this.text())) { $this.addClass('ignored'); if (hide_cats) { $this.hide(); } else { $this.css('opacity', IGNORED_OPACITY); } } }); } }; // ----==== Begin Main Program ====---- // Stuff which either must or need only be called once GM_config.init(config_params); GM_config.onSave = initialize_filters_and_apply; GM_registerMenuCommand("Configure Result Filter...", function() { GM_config.open(); }, 'C'); // Clear out ad box which misaligns "Hidden" message if it's first result $($('#content_wrapper_inner ins.adsbygoogle').parent()[0]).remove(); initialize_filters_and_apply(); }).call(this, jQuery);