您需要先安装一个扩展,例如 篡改猴、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, 2018, Stephan Sokolow (http://www.ssokolow.com/)
- // @license MIT; http://www.opensource.org/licenses/mit-license.php
- // @version 0.1.11
- //
- // @match *://www.fanfiction.net/*
- // @noframes
- //
- // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
- // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
- //
- // @grant GM_registerMenuCommand
- // @grant GM.registerMenuCommand
- //
- // @grant GM_getValue
- // @grant GM_setValue
- // NOTE: GM_config doesn't currently support GM4 APIs, so allowing the GM4
- // versions of these isn't helpful and could result in users losing
- // access to their settings if GM_config suddenly starts supporting
- // them without transparently migrating data from its localStorage
- // fallback.
- // ==/UserScript==
- // ----==== 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
- };
- let frame = document.createElement('div')
- document.body.append(frame);
- 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.style.zIndex = 1050;
- this.frame.style.top = "50px";
- this.frame.style.height = "calc(99% - 100px)";
- this.frame.style.borderColor = "#d4d4d4";
- this.frame.classList.add('modal', 'fade', 'in');
- let header = document.querySelector('.config_header');
- header.classList.add('modal-header');
- this.frame.querySelectorAll('button').forEach(
- function(node) { node.classList.add('btn'); });
- this.frame.querySelectorAll('.reset_holder').forEach(
- function(node) { node.classList.add('btn', 'pull-left'); });
- console.log(document.getElementById('ffnet_result_filter_buttons_holder'));
- document.getElementById('ffnet_result_filter_buttons_holder')
- .classList.add('modal-footer');
- document.getElementById('ffnet_result_filter_slash_pat_var').before(
- document.createElement("br"));
- // Move the content into a wrapper DIV for styling
- let contentbox = document.createElement('div');
- contentbox.id = "ffnrfilter_contentbox";
- header.after(contentbox);
- this.frame.querySelectorAll('.section_header_holder').forEach(function(node) {
- node.remove();
- contentbox.append(node);
- });
- // Add a clickable backdrop for the panel
- let modal_back = document.createElement('div');
- modal_back.id = "gm_modal_back";
- modal_back.addEventListener("click", function() { GM_config.close(); });
- modal_back.classList.add('modal-backdrop', 'fade', 'in')
- document.body.append(modal_back);
- },
- 'close': function() {
- // Plumb the added backdrop into the close handler
- document.getElementById("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.classList.add('filtered')
- node.style.display = "none";
- };
- /// Hide a story entry and add a clickable placeholder
- var add_placeholder = function(node, reason) {
- let placeholder = node.cloneNode();
- placeholder.textContent = "Click to Show (" + reason + ")";
- placeholder.style.minHeight = 0;
- placeholder.style.maxHeight = "1em";
- placeholder.style.color = "lightgray";
- placeholder.style.textAlign = "center";
- placeholder.style.cursor = "pointer";
- placeholder.classList.add("filter_placeholder");
- placeholder.addEventListener("click", (e) => {
- e.stopPropagation();
- e.target.remove();
- node.style.display = "block";
- });
- node.before(placeholder);
- hide_entry(node);
- };
- /// 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
- document.querySelectorAll(".filter_placeholder").forEach(function(node) { node.remove() });
- document.querySelectorAll(".filtered").forEach(function(node) { node.style.display = "block"; });
- document.querySelectorAll(".ignored").forEach(function(node) {
- node.style.opacity = 1;
- node.style.display = "block";
- node.classList.remove('ignored');
- });
- document.querySelectorAll(".z-list").forEach(function(story) {
- let meta_row = story.querySelector('.xgray').textContent;
- let description = story.querySelector('.z-padtop').childNodes[0].data;
- // TODO: Redesign to collapse runs of hidden entries
- // TODO: Show a comma-separated list of reasons something was hidden
- var reason = null;
- if (manual_story_ids.length > 0) {
- var story_url = story.querySelector('a.stitle').getAttribute('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'));
- }
- return;
- }
- }
- 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");
- }
- return;
- }
- 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]);
- }
- return;
- }
- }
- });
- if (GM_config.get('filter_cats')) {
- let hide_cats = GM_config.get('hide_cats');
- document.querySelectorAll('#list_output a').forEach(function(node) {
- // "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.
- let parent = node.closest('div');
- if (parent) { node = parent; }
- if (cat_link_re.test(node.textContent)) {
- node.classList.add('ignored');
- if (hide_cats) {
- node.style.display = "none";
- } else {
- node.style.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;
- // Only clutter up the context menu (in GM4) on relevant pages
- if (document.querySelector('#list_output a, .z-list')) {
- GM.registerMenuCommand("Configure Result Filter...",
- function() { GM_config.open(); }, 'C');
- }
- // Clear out ad box which misaligns "Hidden" message if it's first result
- try {
- document.querySelector('#content_wrapper_inner ins.adsbygoogle')
- .parentElement.remove();
- } catch(_) {}
- initialize_filters_and_apply();