1. Hi there, Guest

    Only registered users can really experience what DLP has to offer. Many forums are only accessible if you have an account. Why don't you register?
    Dismiss Notice

Yet Another GreaseMonkey Plugin

Discussion in 'General Discussion' started by Zyloch, Mar 25, 2011.

  1. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    Note: Hmm, just noticed that SerDel has already done something exactly like this, and probably better (he's got a database all set up too). Still was a good programming exercise; I'll leave this up if anyone wants to take a look.

    I've been feeling under the weather past few days with nothing to do and feeling inspired after Raven's autopager somehow made reading fanfiction so much easier and more enjoyable than before (what? but all it does is autoclick new chapter for me. *shrugs* who knows why). Thus, I spent a few hours learning how to make GreaseMonkey scripts and so present my own humble contribution, for better or worse.

    This is in the spirit of Jangel's plugin, though I've only tested mine on FF4 (and I'm fairly sure it won't work on Chrome). As before, the basic premise is that you or I can select stories to ignore. I've linked it to this lovely site so that, I imagine within reason, we can all ignore the same stories. It's very unsophisticated right now, so one person can go in and unignore everything to screw it up, but no one would do that, right? ;) :facepalm.

    Because I have no server of my own, I've put the code at http://notepad.cc/mowunu56. Use the password "dlp" without quotes. Just save it in a file, named storyhider.user.js and then navigate to it in Firefox with GreaseMonkey enabled.

    The way to use it is follows: navigate to a list of stories as usual. There should be by each story either a green or red square. You can click on it to toggle and also update the global list. Red stories are hidden -- if you so choose you can toggle viewing the summary by clicking on the row the story is on. That's it for now.

    Naturally, I would appreciate feedback, bug reports, and suggestions.

    ---------- Post automerged at 09:41 AM ---------- Previous post was at 09:04 AM ----------

    Updated. Fixed a few things, maybe regressed others. Time will tell.

    Update 2. Fixed a stupid regular expression mistake. Also, the Notepad.cc seems to cut out the   in the code. You can fix it by replacing " G " with " G " and " B " with " B " in the code.

    Screenshot:
    [​IMG]
     
    Last edited: Mar 25, 2011
  2. Riley

    Riley Alchemist DLP Supporter

    Joined:
    Nov 8, 2007
    Messages:
    2,345
    Location:
    On The Eastern Seaboard, USA
    Is there anyway to also enable Keyphrases ignoring. Such as, "HPDM" is on the ignore list along with "Slash" that way when you open to the FFN first page of whatever you reading, it basically does a search and eliminates any stories with those words in the viewable summary?

    If there's an easy and intuitive script out there for this please direct me to it because that seems a lot more feasible than looking at every story and having to click a button.
     
  3. Trig

    Trig Unspeakable

    Joined:
    Jan 27, 2010
    Messages:
    708
    Location:
    Germany
    FanFiction.Net - Extended Functionality has that option but it doesn't work for me. Maybe it's because I'm using Chrome.

    Or maybe it's because I'm using an outdated version. I'll try that and report back.

    Edit: Still doesn't work for me. It's a pity, I would love to be able to hide every single reading-the-books or highschool story (among others).
     
    Last edited: Mar 25, 2011
  4. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    That's an interesting idea. I'll see what I can do. In fact, I'm reworking the script right now so that it doesn't have the online functionality (that will go in again when I can write it in a good way), but hiding based on criteria is a good idea.
     
  5. Trig

    Trig Unspeakable

    Joined:
    Jan 27, 2010
    Messages:
    708
    Location:
    Germany
    Ah right, on topic:

    Using this script with Chrome doesn't work, like you said. I see the 'G' button next to each fic, but clicking it doesn't do anything. I don't know the difference between the way that FF and Chrome interpret their scripts, but using Tampermonkey on Chrome (Greasemonkey alternative) and checking each of it's script-fixes didn't change anything at all.

    Just thought that maybe you could use this sort of feedback.
     
  6. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    Thanks for the tips, Trig.

    The reason the original script didn't work before was because Chrome does not allow cross site scripting from GreaseMonkey scripts; it would have to be some sort of extension.

    Most of the script is rewritten now. It's still a bit rough, but I thought I'd put it up here for you guys to review. Here are the relevant points:

    • There is no online component anymore for now. :(

    • Script (seems to) work in FF4 and Chrome 11 Beta. Nothing earlier, I'm afraid, because I was learning how to use HTML 5's Indexed DB.

    • Four choices: Hide, Show, Recommended, or Neutral. Show is there so that in the future people can privately decide to keep a story on a list but allow others to decide on neutral stories.

    • In the control panel, there are three fields for automatically matching Hide, Show, and Recommended. Highest to lowest priority in matching is Recommended, then Show, then Hide. The idea is that when you browse to a new page these rules will automatically match and put those results in your local database. One rule on each line, it is a case insensitive regular expression. For those who don't know regular expressions, you can just put in your typical term, like "HP/DM" (without quotes). Would love more testing on this feature. We match against everything in the story listing, including title, author, summary, pairing, etc.

    • Known issue: does not work for favorites lists, because FFNet HTML code is so bad and nonuniform. It will take a little more work to adjust for that.

    EDIT: I forgot to mention, the control panel link is in the bottom right corner. My current testing list of rules to hide on are follows: Fem.Harry, Girl.Harry, slash, Draco.Harry

    The dot matches one character of anything (it's a regular expression character). Your mileage may vary.

    As always, I welcome comments, bug reports, suggestions.

    To install, save in same file name and open in FF4 or Chrome 11
    Code:
    // ==UserScript==
    // @name Story Hider
    // @namespace http://www.fanfiction.net/
    // @description Hides stories on FanFiction.Net
    // @version 1.1.2
    // @include http://*.fanfiction.net/*
    // @match http://*.fanfiction.net/*
    // ==/UserScript==
    
    (function() {
    
        // Preference button colors.
        const SHOW_COLOR = "green";
        const HIDE_COLOR = "#800000";
        const REC_COLOR = "#DAA520";
        const NEUT_COLOR = "#DDD";
    
        // Most configuration data is not big enough to require a database. For
        // those, we will use HTML5's local storage capabilities. These constants
        // control local storage key names.
    
        const LS_HIDE_RULES = "hideRules";      // Key for storing hide rules.
        const LS_SHOW_RULES = "showRules";      // Key for storing show rules.
        const LS_REC_RULES = "recRules";        // Key for storing rec rules.
        const LS_COMP_HIDE = "compHide";        // Key for complete hide option.
    
        // We anticipate storing a lot of story IDs. Therefore, we use HTML5's new
        // Indexed DB API to persistently store our data. The following constants
        // control some aspects of this approach.
    
        const DB_NAME = "StoryHider";       // Database name.
        const PRIVATE = "PrivatePref";      // Private stories preference store.
        const N_FLAG = 0;                   // In database: Neutral story.
        const S_FLAG = 1;                   // In database: Show story.
        const H_FLAG = 2;                   // In database: Hide story.
        const R_FLAG = 3;                   // In database: Recommended story.
    
        // Database error codes defined as constants.
        const DB_CONSTRAINT_ERR = 3;
        const DB_NON_TRANSIENT_ERR = 1;
        const DB_TRANSACTION_INACTIVE_ERR = 6;
        const DB_UNKNOWN_ERR = 0;
    
        // Database transaction types defined as constants. For now, it seems
        // that FF4 and Chrome 11 uses exactly opposite conventions.
        if (unsafeWindow.webkitIDBTransaction) {
            var DB_READ_ONLY = 1;
            var DB_READ_WRITE = 0;
        } else {
            var DB_READ_ONLY = 0;
            var DB_READ_WRITE = 1;
        }
    
        // Global variable used to hold story listing objects on current page.
        var StoryListObj = {};
    
        // Helper function that returns keys of an object as an array.
        var getKeys = function(obj) {
            var keys = [];
            for (var key in obj) {
                keys.push(key);
            }
            return keys;
        };
    
        // Helper function to trim whitespace off the ends of a string. 
        // By Adrian Flesler.
        var trim = function(str) {
            var start = -1,
            end = str.length;
            while( str.charCodeAt(--end) < 33 );
            while( str.charCodeAt(++start) < 33 );
            return str.slice( start, end + 1 );
        };
    
        // Create local database API for convenience.
        var Database = {};
        Database.db = null;
    
        // Until Indexed DB becomes standard, we will need some aliases.
        if (!unsafeWindow.indexedDB) {
            if (unsafeWindow.mozIndexedDB) {
                unsafeWindow.indexedDB = unsafeWindow.mozIndexedDB;
            } else if (unsafeWindow.webkitIndexedDB) {
                unsafeWindow.indexedDB = unsafeWindow.webkitIndexedDB;
            } else {
                alert("Your browser is not supported.");
            }
        }
    
        // Open the database and allow a callback function to be passed.
        Database.open = function(callback) {
            var request = unsafeWindow.indexedDB.open(DB_NAME);
    
            request.onsuccess = function(evt) {
                var version = "1.0";
                var db = Database.db = this.result;
                if (db.version !== version) {
                    var request = db.setVersion(version);
                    request.onsuccess = function(evt) {
                        // We will create all the object stores we need.
                        var privShowStore =
                            db.createObjectStore(PRIVATE, { keyPath : "sid" });
    
                        // We are done preparing the database. Move on.
                        if (callback) {
                            callback();
                        }
                    };
    
                    request.onerror = function(evt) {
                        if (request.errorCode === DB_CONSTRAINT_ERR) {
                            // Our object stores already exist. Forge onward.
                            if (callback) {
                                callback();
                            }
                        }
                    };
                }
                else
                {
                    // All we can do is hope our object stores were already made.
                    if (callback) {
                        callback();
                    }
                }
            };
    
            request.onerror = function(evt) {
                alert(request.errorCode);
                // TODO
            };
        };
    
        // Load preferences related to stories on the current page into the global
        // object designated to hold such data. Allow a callback to be passed.
        Database.loadCurrentPreferences = function(callback) {
            var currentStories = getKeys(StoryListObj);
            var transaction = Database.db.transaction([PRIVATE]);
            var store = transaction.objectStore(PRIVATE);
    
            // Cascade read all the entries we need to read.
            var loadNext = function() {
                if (currentStories.length > 0) {
                    var sid = currentStories.pop();
                    var req = store.get(sid);
                    req.onsuccess = function(evt) {
                        if (this.result) {
                            StoryListObj[sid].pref = 
                                parseInt(this.result.preference);
                        } else {
                            StoryListObj[sid].pref = N_FLAG;
                        }
                        loadNext();
                    };
                } else if (callback) {
                    callback();
                }
            };
            loadNext();
        };
    
        // Event handler we add to each story listing in order to toggle the
        // visibility of its summary.
        var toggleSummaryEH = function() {
            var sid = this.getAttribute("sid");
    
            // Do not toggle summary if story is not hidden.
            if (!StoryListObj[sid] || StoryListObj[sid].pref !== H_FLAG) {
                return;
            }
    
            var summary = this.getElementsByTagName("div")[0];
            if (summary.style.display === "none") {
                summary.style.display = "";
            } else {
                summary.style.display = "none";
            }
        };
    
        // Event handler we add to each private hide button.
        var toggleHideEH = function(evt) {
            var storyid = this.parentNode.getAttribute("sid");
            if (StoryListObj[storyid].pref !== H_FLAG) {
                var pref = H_FLAG;
            } else {
                var pref = N_FLAG;
            }
            updateStory(storyid, pref);
    
            evt.stopPropagation();
        };
    
        // Event handler we add to each private show button.
        var toggleShowEH = function() {
            var storyid = this.parentNode.getAttribute("sid");
            if (StoryListObj[storyid].pref !== S_FLAG 
                    && StoryListObj[storyid].pref !== R_FLAG) {
                var pref = S_FLAG;
            } else {
                var pref = N_FLAG;
            }
            updateStory(storyid, pref);
        };
    
        // Event handler we add to each private recommendation button.
        var toggleRecEH = function() {
            var storyid = this.parentNode.getAttribute("sid");
            if (StoryListObj[storyid].pref !== R_FLAG) {
                var pref = R_FLAG;
            } else {
                var pref = S_FLAG;
            }
            updateStory(storyid, pref);
        };
    
        // Update story status property.
        var updateStory = function(storyid, flag) {
            StoryListObj[storyid].pref = flag;
            updateStoryVisual(storyid);
    
            // Update in the local database.
            var transaction = Database.db.transaction([PRIVATE], DB_READ_WRITE);
            var request = transaction.objectStore(PRIVATE).put({
                sid: storyid,
                preference: flag 
            });
        };
    
        // Toggle visuals for a particular private hide button.
        var toggleHideVisual = function(btn, isOn) {
            btn.style.color = isOn ? HIDE_COLOR : NEUT_COLOR;
            btn.style.border = "1px solid " + ( isOn ? HIDE_COLOR : NEUT_COLOR );
        };
    
        // Toggle visuals for a particular private show button.
        var toggleShowVisual = function(btn, isOn) {
            btn.style.color = isOn ? SHOW_COLOR : NEUT_COLOR;
            btn.style.border = "1px solid " + ( isOn ? SHOW_COLOR : NEUT_COLOR );
        };
    
        // Toggle visuals for a particular private recommendation button.
        var toggleRecVisual = function(btn, isOn) {
            btn.style.color = isOn ? REC_COLOR : NEUT_COLOR;
            btn.style.border = "1px solid " + ( isOn ? REC_COLOR : NEUT_COLOR );
        };
    
        // Display each element on the page with our buttons, colors, visibility,
        // and event handlers. Also figure out which stories appear on this page.
        // The parentObj parameter is designed to help out when we want to view
        // author's favorites or sort author's lists. We need to reinitialize a
        // lot of elements in that case.
        var initializeList = function(parentObj) {
            if (!parentObj) {
                parentObj = document;
            }
            var stories = parentObj.getElementsByClassName("z-list");
            for (var i = 0; i < stories.length; i++) {
                // Get story id.
                var a = stories[i].getElementsByTagName("a")[0];
                var sid = a.href.match(/\/s\/(\d+)\//i)[1];
    
                // Add story object to current page list.
                stories[i].setAttribute("sid", sid);
                StoryListObj[sid] = { obj: stories[i] };
    
                // Add toggle event handler to each object.
                stories[i].addEventListener("click", toggleSummaryEH, false);
    
                // Add the appropriate preference buttons.
                var privRecBtn = document.createElement("span");
                privRecBtn.style.cursor = "pointer";
                privRecBtn.style.fontFamily = "monospace";
                privRecBtn.style.borderRadius = "3px";
                privRecBtn.style.color = NEUT_COLOR;
                privRecBtn.style.border = "1px solid " + NEUT_COLOR;
                a.parentNode.insertBefore(privRecBtn, a);
    
                // Clone other preference buttons/indicators.
                var privShowBtn = privRecBtn.cloneNode(false);
                var privHideBtn = privRecBtn.cloneNode(false);
                a.parentNode.insertBefore(privShowBtn, privRecBtn);
                a.parentNode.insertBefore(privHideBtn, privShowBtn);
    
                // Add individual preference button styles.
                privRecBtn.setAttribute("title", "Toggle Recommendation");
                privRecBtn.setAttribute("class", "recbtn");
                privRecBtn.style.marginRight = "5px";
                privRecBtn.innerHTML = "&nbsp;R&nbsp;";
                privRecBtn.addEventListener("click", toggleRecEH, false);
    
                privShowBtn.setAttribute("title", "Toggle Show");
                privShowBtn.setAttribute("class", "showbtn");
                privShowBtn.style.marginRight = "2px";
                privShowBtn.innerHTML = "&nbsp;S&nbsp;";
                privShowBtn.addEventListener("click", toggleShowEH, false);
    
                privHideBtn.setAttribute("title", "Toggle Hide");
                privHideBtn.setAttribute("class", "hidebtn");
                privHideBtn.style.marginRight = "2px";
                privHideBtn.innerHTML = "&nbsp;H&nbsp;";
                privHideBtn.addEventListener("click", toggleHideEH, false);
            }
        };
    
        // Fixes bad FFN lists caused by redrawing from JavaScript. The new HTML
        // is very unstructured, so we need to do a lot of workarounds. This may
        // need to be changed in the future if FFN changes their code.
        var fixBadList = function(parentObj) {
            var badContent = parentObj.innerHTML;
            var snippet1 = "<div class='z-list'>";
            var snippet2 = "<div class='z-indent z-padtop'>";
            var snippet3 = "</div></div>";
            badContent = badContent.replace(/<blockquote>/g, snippet2);
            var arr = badContent.split("</blockquote>");
            var last = arr.pop();
            var goodContent = arr.join(snippet3 + snippet1);
            goodContent = snippet1 + goodContent + snippet3 + last;
            parentObj.innerHTML = goodContent;
        };
    
        // Update the appearance of a story appropriately.
        var updateStoryVisual = function(sid) {
            var obj = StoryListObj[sid].obj;
            var pref = StoryListObj[sid].pref;
            var hideBtn = obj.getElementsByClassName("hidebtn")[0];
            var showBtn = obj.getElementsByClassName("showbtn")[0];
            var recBtn = obj.getElementsByClassName("recbtn")[0];
            var summary = obj.getElementsByTagName("div")[0];
            var lnks = obj.getElementsByTagName("a");
            if (pref === H_FLAG) {
                summary.style.display = "none";
                toggleHideVisual(hideBtn, true);
                toggleShowVisual(showBtn, false);
                toggleRecVisual(recBtn, false);
                obj.style.opacity = "0.8";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = HIDE_COLOR;
                    lnks[i].style.fontWeight = "";
                }
                if (unsafeWindow.localStorage[LS_COMP_HIDE]) {
                    obj.style.display = "none";
                } else {
                    obj.style.display = "";
                }
            } else if (pref === S_FLAG) {
                summary.style.display = "";
                toggleHideVisual(hideBtn, false);
                toggleShowVisual(showBtn, true);
                toggleRecVisual(recBtn, false);
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "";
                }
            } else if (pref === R_FLAG) {
                summary.style.display = "";
                toggleHideVisual(hideBtn, false);
                toggleShowVisual(showBtn, true);
                toggleRecVisual(recBtn, true);
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "bold";
                }
            } else {
                summary.style.display = "";
                toggleHideVisual(hideBtn, false);
                toggleShowVisual(showBtn, false);
                toggleRecVisual(recBtn, false);
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "";
                }
            }
        };
    
        // Update the appearance of all stories on the list.
        var updateAllStoriesVisual = function() {
            for (var sid in StoryListObj) {
                updateStoryVisual(sid);
            }
        };
    
        // Apply rules to our preferences.
        var applyRules = function() {
            var hideRules = [];
            var showRules = [];
            var recRules = [];
            var sr;
    
            // Get rules from storage. Firefox treats absence as NULL, Chrome
            // treats it as undefined, so we'll just take a shortcut here.
            if (unsafeWindow.localStorage[LS_HIDE_RULES]) {
                sr = unsafeWindow.localStorage[LS_HIDE_RULES].split("\n");
                for (var i = 0; i < sr.length; i++) {
                    sr[i] = trim(sr[i]);
                    if (sr[i].length <= 0) {
                        continue;
                    }
                    if (sr[i].substring(0,2) === "!#") {
                        hideRules.push(new RegExp(sr[i].substring(2)));
                    } else {
                        hideRules.push(new RegExp(sr[i], "i"));
                    }
                }
            }
            if (unsafeWindow.localStorage[LS_SHOW_RULES]) {
                sr = unsafeWindow.localStorage[LS_SHOW_RULES].split("\n");
                for (var i = 0; i < sr.length; i++) {
                    sr[i] = trim(sr[i]);
                    if (sr[i].length <= 0) {
                        continue;
                    }
                    if (sr[i].substring(0,2) === "!#") {
                        showRules.push(new RegExp(sr[i].substring(2)));
                    } else {
                        showRules.push(new RegExp(sr[i], "i"));
                    }
                }
            }
            if (unsafeWindow.localStorage[LS_REC_RULES]) {
                sr = unsafeWindow.localStorage[LS_REC_RULES].split("\n");
                for (var i = 0; i < sr.length; i++) {
                    sr[i] = trim(sr[i]);
                    if (sr[i].length <= 0) {
                        continue;
                    }
                    if (sr[i].substring(0,2) === "!#") {
                        recRules.push(new RegExp(sr[i].substring(2)));
                    } else {
                        recRules.push(new RegExp(sr[i], "i"));
                    }
                }
            }
            
            StoryLoop:
            for (var sid in StoryListObj) {
                // If story is not neutral, don't apply any rules.
                if (StoryListObj[sid].pref !== N_FLAG) {
                    continue;
                }
    
                var contents = StoryListObj[sid].obj.innerHTML;
    
                // First apply recommended, then show, then hide rules.
                for (var i = 0; i < recRules.length; i++) {
                    if (contents.match(recRules[i])) {
                        updateStory(sid, R_FLAG);
                        continue StoryLoop;
                    }
                }
                for (var i = 0; i < showRules.length; i++) {
                    if (contents.match(showRules[i])) {
                        updateStory(sid, S_FLAG);
                        continue StoryLoop;
                    }
                }
                for (var i = 0; i < hideRules.length; i++) {
                    if (contents.match(hideRules[i])) {
                        updateStory(sid, H_FLAG);
                        continue StoryLoop;
                    }
                }
            }
        };
    
        // Create the script's control panel.
        var createControlPanel = function() {
            // We want a nice looking CSS3 striped background, and to keep it as
            // cross browser compatible as possible, we'll add an actual style.
            // This style also happens to include some other stuff for the panel.
            var bgStyle = " \
    #StoryHiderCP { \
      background-color: #0ae; \
      background-image: -webkit-gradient(linear, 0 100%, 100% 0, \
        color-stop(.25, rgba(255, 255, 255, .2)), color-stop(.25, transparent), \
        color-stop(.5, transparent), color-stop(.5, rgba(255, 255, 255, .2)), \
        color-stop(.75, rgba(255, 255, 255, .2)), color-stop(.75, transparent), \
    	to(transparent)); \
      background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      background-image: linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      position: fixed; \
      right: 0px; \
      bottom: 0px; \
      border-top: \"2px solid #06C\"; \
      border-left: \"2px solid #06C\"; \
      padding: 10px; \
    } \
    #StoryHiderCP th { \
      text-align: left; \
      color: #191970; \
    } \
    #StoryHiderCP div.btn { \
      cursor: pointer; \
      padding: 5px 10px; \
      border: 1px solid #06C; \
      background: #0cf; \
      font-weight: bold; \
    } \
    #StoryHiderCP a { \
      color: blue; \
      text-decoration: none; \
      border-bottom: none; \
    }";
            GM_addStyle(bgStyle);
    
            var panel = document.createElement("div");
            panel.setAttribute("id", "StoryHiderCP");
    
            panel.innerHTML = " \
    <div id='StoryHiderCPMain' style='display: none;'> \
    <b>Control Panel</b> \
    <table style='width: 98%; margin: 0.6em auto;'> \
    <tr><th>Hide Rules</th><th>Show Rules</th><th>Recommend Rules</th></tr> \
    <tr> \
      <td><textarea id='StoryHiderT1' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
      <td><textarea id='StoryHiderT2' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
      <td><textarea id='StoryHiderT3' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
    </tr> \
    <tr><td colspan='3' style='font-size:0.9em;text-align:center;'> \
    Each line should be a <a href='http://www.regular-expressions.info/quickstart.html' target='_blank'> \
    regular expression</a> that matches against the story listing, which \
    includes the title, author, summary, and pairing. By priority, recommend \
    rules match first, then show rules, and lastly hide rules. For a case sensitive \
    match, put !# in front of your expression. \
    </td></tr> \
    </table> \
    <table style='margin: 0.6em auto;'> \
    <tr><th><label for='StoryHiderC1'>Completely Hide</label></th><th><input id='StoryHiderC1' type='checkbox'/></td></tr> \
    </table> \
    </div> \
    <div id='StoryHiderCPBtn' class='btn' style='float: right;'>Open</div> \
    ";
    
            document.getElementsByTagName("body")[0].appendChild(panel);
    
            // Load control panel with configurations.
            if (unsafeWindow.localStorage[LS_HIDE_RULES]) {
                document.getElementById("StoryHiderT1").innerHTML =
                    unsafeWindow.localStorage[LS_HIDE_RULES];
            }
            if (unsafeWindow.localStorage[LS_SHOW_RULES]) {
                document.getElementById("StoryHiderT2").innerHTML =
                    unsafeWindow.localStorage[LS_SHOW_RULES];
            }
            if (unsafeWindow.localStorage[LS_REC_RULES]) {
                document.getElementById("StoryHiderT3").innerHTML =
                    unsafeWindow.localStorage[LS_REC_RULES];
            }
            if (unsafeWindow.localStorage[LS_COMP_HIDE]) {
                document.getElementById("StoryHiderC1").checked = true;
            }
    
            // Add event handlers.
            document.getElementById("StoryHiderT1").addEventListener("change",
                function(evt) {
                    unsafeWindow.localStorage[LS_HIDE_RULES] = this.value;
                }, false);
            document.getElementById("StoryHiderT2").addEventListener("change",
                function(evt) {
                    unsafeWindow.localStorage[LS_SHOW_RULES] = this.value;
                }, false);
            document.getElementById("StoryHiderT3").addEventListener("change",
                function(evt) {
                    unsafeWindow.localStorage[LS_REC_RULES] = this.value;
                }, false);
            document.getElementById("StoryHiderCPBtn").addEventListener("click",
                function(evt) {
                    if (this.innerHTML === "Open") {
                        this.innerHTML = "Close";
                        this.parentNode.style.width = "50%";
                        document.getElementById("StoryHiderCPMain").style.display = "";
                    } else {
                        this.innerHTML = "Open";
                        this.parentNode.style.width = "";
                        document.getElementById("StoryHiderCPMain").style.display = "none";
                        applyRules();
                    }
                }, false);
            document.getElementById("StoryHiderC1").addEventListener("click",
                function(evt) {
                    if (this.checked) {
                        unsafeWindow.localStorage[LS_COMP_HIDE] = "1";
                    } else {
                        delete unsafeWindow.localStorage[LS_COMP_HIDE];
                    }
                    updateAllStoriesVisual();
                }, false);
    
            // Do not close the panel if a click happens on top of it.
            panel.addEventListener("click", function(evt) {
                evt.stopPropagation();
            }, false);
    
            // Close the panel if a click happens off of it.
            document.getElementsByTagName("body")[0].addEventListener("click",
                function(evt) {
                    var b = document.getElementById("StoryHiderCPBtn");
                    if (b.innerHTML === "Close") {
                        b.innerHTML = "Open";
                        b.parentNode.style.width = "";
                        document.getElementById("StoryHiderCPMain").style.display = "none";
                        applyRules();
                    }
            }, false);
    
        };
    
        // We need to create a watch for functions that force us to reinitialize
        // our lists (could possibly change depending on FFN).
        if (unsafeWindow.storylist_draw) {
            var original = unsafeWindow.storylist_draw;
            unsafeWindow.storylist_draw = function() {
                var parentObj = document.getElementById(arguments[0]);
                original.apply(null, arguments);
                fixBadList(parentObj);
                initializeList(parentObj);
                // We only do stuff if there are stories!
                if (getKeys(StoryListObj).length <= 0) {
                    return false;
                }
                Database.loadCurrentPreferences(function() {
                    applyRules();
                    updateAllStoriesVisual();
                });
            };
        }
    
        Database.open(function() {
            initializeList();
            // We only do stuff if there are stories!
            if (getKeys(StoryListObj).length <= 0) {
                return false;
            }
            Database.loadCurrentPreferences(function() {
                applyRules();
                updateAllStoriesVisual();
                createControlPanel();
            });
        });
    }());
    
    ---------- Post automerged at 09:53 PM ---------- Previous post was at 08:06 PM ----------

    Update: Fixed small bug with regular expressions where an empty line was taken too seriously. Added option in control panel to completely hide hidden stories so they are totally out of the way.

    ---------- Post automerged at 11:57 PM ---------- Previous post was at 09:53 PM ----------

    One last edit for tonight. Finally got those pesky Favorites and sorting in author's lists to work (at least I think). Only tested that on FF4 since I'm now damn tired. Also made it so that you can close the control panel by clicking anywhere else on the page.
     
    Last edited: Mar 26, 2011
  7. The Greek

    The Greek Second Year

    Joined:
    Jun 13, 2008
    Messages:
    64
    Gender:
    Male
    I'm not sure if I did something wrong or it's the script but there is a small window popping when I open up ff.net that says "your browser is not supported". The old script worked, the new version doesn't show the buttons at all. Anyone else has that problem or did I f*ck it up?
     
  8. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    The old script and the new one do almost completely different things. I hope maybe later to merge them into something useful. Are you using Firefox 4? The script uses something only available in FF4.

    EDIT: It also occurs to me that you should be using the latest release candidate. I know that they changed the internal variable name of the Indexed DB variable I used sometime in between the betas.

    ---------- Post automerged at 04:57 PM ---------- Previous post was at 12:30 AM ----------

    I have updated the script to version 1.1.3 (some small changes and a big bugfix for Chrome). I've included the source code at the bottom of this post. When I first wrote this script, I didn't know how effective or efficient it will be. I still need help from you all to determine that, but I have a better sense how to use it, so I will take some time and post a tutorial.

    Installation

    Save the code into a file called "storyhider.user.js". If you are running Firefox 4 (I recommend the latest release candidate) or the new Chrome 11 Beta, open the file in your browser, and you should be able to install.

    Usage

    Like other similar scripts, this one allows you to ignore certain stories. It works on story lists, C2 lists, and author profiles. Three buttons will appear to the left of the title of each story. They each toggle on and off. When all three buttons are gray, the story is neutral. When the first button is toggled on, the story is hidden. When the second button is toggled on, the story will always be shown. When the third button is toggled on, the story will be considered recommended. A recommended story is either one that you like, or one you may potentially like and will stand out in the story list.

    Control Panel

    The control panel link is in the bottom right of any page with stories. After the control panel opens, it can be closed by clicking on the close button or anywhere else on the page. The control panel will stay open between page loads if both pages contain stories.

    Hidden Stories

    Stories that are hidden can be hidden completely or not. This is controlled by the checkbox in the control panel. I find this feature very useful. Sometimes, I want to see which stories were hidden but other times I want to browser without the clutter. When a story is hidden but not completely, you can view the summary by clicking anywhere horizontal with the story listing to toggle.

    Automatic Matching

    This is the biggest feature. In the control panel, there are three fields for use in automatically categorizing stories. In each field, you will put one rule per line. Each rule is a regular expression, but you can use terms too (those are just simple regular expressions). By default, each regular expression is applied case insensitive, but you can make a rule case sensitive by putting !# in front of it. I haven't found huge use for this, but I imagine it may come in useful.

    The rules match against the story entry of each story. This includes the title, the author, the summary, and the pairing. Rules that are in the recommend field are matched first, followed by rules in the show field, followed by rules in the hide field. This means that if both a show rule and a hide rule match, the show rule takes precedence.

    Finally, rules only match stories that are neutral. If a story has already been matched or you chose a preference for it, it will not be matched again until it is neutral.

    A sample of starter rules could be:

    Hide Rules:

    slash|mpre?g
    DM.?HP|HP.?DM|LV.?HP|HP.?LV|SS.?HP|HP.?SS
    Fem.?Harry|Girl.?Harry
    Draco.?Harry|Harry.?Draco
    Snarry
    Spander
    soul.?bond
    reads? the .*books?
    marriage law
    - (?:Spanish|German|Swedish|French|Portuguese) -

    Show Rules:

    not? slash|non.?slash
    fem.?slash

    Recommend Rules:

    Fleur|Daphne
    blank101


    Several points to note. I use the | a lot in my regular expressions to group similar ones together. This may also be faster, since otherwise each line is its own regular expression, but it's not noticeable now.

    Also, observe that I try to catch slash in the first field, but I want to try to prevent catching not slash, no slash, non-slash, and similar monikers. Since show rules take precedence over hide rules, this method works.

    We all know that not all stories with Fleur or Daphne are good stories, but they have a higher probability, so I like them to stand out in my lists. You can also mark your favorite stories as recommended. Notice that you can stick an author name in there. Actually, the rules match on the underlying HTML, so you can match /s/storyid or /a/authorid to be precise.


    Changes

    Changes from the previous version. I may not remember all of them, but I reorganized the code, which probably fixed a few small bugs. The control panel now stays open in between page refreshes. There was I think a big bug in Chrome where the script failed on the favorites list of authors. This was actually very difficult to work around technically, but I think I have done so. People who try the script should let me know how it works on Chrome.

    Code:
    // ==UserScript==
    // @name Story Hider
    // @namespace http://www.fanfiction.net/
    // @description Hides stories on FanFiction.Net
    // @version 1.1.4
    // @include http://*.fanfiction.net/*
    // @match http://*.fanfiction.net/*
    // ==/UserScript==
    
    // @author Zyloch
    // Our code is tested on Firefox 4 RC2 and Google Chrome 11.
    (function StoryHider(unsafeWindow) {
    
        /** 
         * These constants determine the color of the preference buttons left of
         * each story title.
         */
        const SHOW_COLOR    = "green";
        const HIDE_COLOR    = "#800000";
        const REC_COLOR     = "#DAA520";
        const NEUT_COLOR    = "#DDDDDD";
    
        /**
         * These constants control the local storage key names we use. Most users
         * will not need to touch these. NOTE: Local storage is used by us to store 
         * configuration data.
         */
        const LS_HIDE_RULES = "hideRules";  // Key for storing hide rules.
        const LS_SHOW_RULES = "showRules";  // Key for storing show rules.
        const LS_REC_RULES  = "recRules";   // Key for storing recommend rules.
        const LS_COMP_HIDE  = "compHide";   // Key for complete hide option.
    
        /**
         * These constants control the session storage key names we use.
         */
        const SS_CP_OPEN = "StoryHideCPOpen";   // Key for storing CP open or not.
    
        /**
         * These constants control the names and values of database components in
         * the script. These should not need to be changed by the user.
         *
         * Note: We anticipate storing many story IDs. Therefore, we use HTML5's
         * new Indexed DB API to persistently store and retrieve our data. This
         * will hopefully increase performance in the long run.
         */
        const DB_NAME = "StoryHider";       // Database name.
        const STORE_PREF = "PrivatePref";   // Story preference store.
    
        const N_FLAG = 0;                   // In database: Neutral story.
        const S_FLAG = 1;                   // In database: Show story.
        const H_FLAG = 2;                   // In database: Hide story.
        const R_FLAG = 3;                   // In database: Recommended story.
    
        // Database error codes defined as constants.
        const DB_CONSTRAINT_ERR = 3;
        const DB_NON_TRANSIENT_ERR = 1;
        const DB_TRANSACTION_INACTIVE_ERR = 6;
        const DB_UNKNOWN_ERR = 0;
    
        // GreaseMonkey gives Firefox access to the actual not sandboxed copy of
        // the window object, called unsafeWindow. Chrome does not. Here, create
        // a local name alias to take care of either case.
        unsafeWindow = unsafeWindow || window;
    
        // This workaround is for Chrome, which does not give us access to the
        // variables and functions of the underlying page. We use a variant of
        // the Content Scope Runner technique to run the script twice, once in
        // GreaseMonkey context and once in page context.
        var pageScope = (typeof __STORY_HIDER_PAGE_SCOPE__ !== "undefined");
        var ua = navigator.userAgent.toLowerCase();
        var isChrome = ua.indexOf("chrome") !== -1;
    
        // Inject our page scope marker into the page.
        if (isChrome && !pageScope) {
            var src = "(" + StoryHider.caller.toString() + ")();";
            var marker = "var __STORY_HIDER_PAGE_SCOPE__ = true;";
            var script = document.createElement("script");
            script.setAttribute("type", "application/javascript");
            script.innerHTML = marker + src;
    
            // Insert the script node into the page, so it will run, and 
            // immediately remove it to clean up. Use setTimeout to force 
            // execution "outside" of the user script scope completely.
            setTimeout(function() {
                document.body.appendChild(script);
                document.body.removeChild(script);
            }, 0);
        }
    
        /** 
         * Until Indexed DB becomes standard, we need to take care of cross
         * browser differences by aliasing and defining constants. */
    
        // Give a uniform access to the main Indexed DB object.
        if (!("indexedDB" in unsafeWindow)) {
            if ("mozIndexedDB" in unsafeWindow) {
                unsafeWindow.indexedDB = unsafeWindow.mozIndexedDB;
            } else if ("webkitIndexedDB" in unsafeWindow) {
                unsafeWindow.indexedDB = unsafeWindow.webkitIndexedDB;
            } else if ("moz_indexedDB" in unsafeWindow) {
                unsafeWindow.indexedDB = unsafeWindow.moz_indexedDB;
            } else {
                alert("The script could not detect Indexed DB support.");
            }
        }
    
        // Give a uniform access to the main transaction object.
        if (!("IDBTransaction" in unsafeWindow)) {
            if ("webkitIDBTransaction" in unsafeWindow) {
                unsafeWindow.IDBTransaction = unsafeWindow.webkitIDBTransaction;
            }
        }
    
        // Give a uniform access to the database exception object.
        if (!("IDBDatabaseException" in unsafeWindow)) {
            if ("webkitIDBDatabaseException" in unsafeWindow) {
                unsafeWindow.IDBDatabaseException = unsafeWindow.webkitIDBDatabaseException;
            }
        }
    
        // FF4 RC2 exposes window.IDBTransaction but does not give it any of its
        // constant values for some reason. We need to define them here manually.
        if (!("READ_ONLY" in unsafeWindow.IDBTransaction)) {
            unsafeWindow.IDBTransaction.READ_ONLY = 0;
        }
        if (!("READ_WRITE" in unsafeWindow.IDBTransaction)) {
            unsafeWindow.IDBTransaction.READ_WRITE = 1;
        }
    
        // FF4 RC2 also exposes window.IDBDatabaseException but does not give it
        // any of its constant values. Define them manually. We would define
        // them manually, but the values listed on the documentation website do
        // not seem to be the actual values stored in memory. So, we hold off on
        // error checking for now.
        
        // Initialize variable that remembers whether control panel is open.
        if (!(SS_CP_OPEN in unsafeWindow.sessionStorage)) {
            unsafeWindow.sessionStorage[SS_CP_OPEN] = "0";
        }
    
        // Global variable used to hold story listing objects on current page.
        var StoryListObj = {};
    
        // Create several namespaces useful to compartmentalize our code.
        var Main = {};              // main methods
        var Database = {};          // database methods
        var Event = {};             // event handlers
        var Util = {};              // helper methods
        var Visual = {};            // page layour methods
    
        Database.db = null;
    
        // Merge the second object into the first one. The third parameter 
        // determines whether to overwrite existing properties or not.
        Util.extend = function(base, ext, ow) {
            for (var key in ext) {
                if (ow || !(key in base)) {
                    base[key] = ext[key];
                }
            }
        };
    
        // Returns object keys as an array.
        Util.getKeys = function(obj) {
            var keys = [];
            for (var key in obj) {
                keys.push(key);
            }
            return keys;
        };
    
        // Trim whitespace off the ends of a string. By Adrian Flesler.
        Util.trim = function(str) {
            var start = -1,
            end = str.length;
            while( str.charCodeAt(--end) < 33 );
            while( str.charCodeAt(++start) < 33 );
            return str.slice( start, end + 1 );
        };
    
        // Open the database. Allow a callback function to be passed that contains
        // everything that happens only after the database is opened.
        Database.open = function(callback) {
            var request = unsafeWindow.indexedDB.open(DB_NAME);
            request.onsuccess = function(evt) {
                var version = "1.0";
                var db = Database.db = this.result;
                // Object stores have not yet been created.
                if (db.version !== version) {
                    // Set the DB version so that next time we don't do this.
                    var request = db.setVersion(version);
                    request.onsuccess = function(evt) {
                        // Create the object stores that we need.
                        db.createObjectStore(STORE_PREF, { keyPath : "sid" });
    
                        // We are done preparing the database. Move on.
                        if (typeof callback === "function") {
                            callback();
                        }
                    };
    
                    request.onerror = function(evt) {
                        if (request.errorCode === DB_CONSTRAINT_ERR) {
                            // Our object stores already exist. Forge onward.
                            if (typeof callback === "function") {
                                callback();
                            }
                        }
                    };
                }
                else if (typeof callback === "function") {
                    callback();
                }
            };
    
            request.onerror = function(evt) {
                alert(request.errorCode);
            };
        };
    
        // Load preferences related to stories on the current page into the global
        // object designated to hold such data. Allow a callback to be passed.
        Database.loadCurrentPreferences = function(callback) {
            var currentStories = Util.getKeys(StoryListObj);
            var transaction = Database.db.transaction([STORE_PREF]);
            var store = transaction.objectStore(STORE_PREF);
    
            // Cascade read all the entries we need to read.
            var loadNext = function() {
                if (currentStories.length > 0) {
                    var sid = currentStories.pop();
                    var req = store.get(sid);
                    req.onsuccess = function(evt) {
                        if (this.result) {
                            StoryListObj[sid].pref = 
                                parseInt(this.result.preference);
                        } else {
                            StoryListObj[sid].pref = N_FLAG;
                        }
                        loadNext();
                    };
                } else if (typeof callback === "function") {
                    callback();
                }
            };
            loadNext();
        };
    
        // Update story preferences in the database.
        Database.updateStory = function(storyid, pref) {
            var transaction = Database.db.transaction([STORE_PREF], 
                unsafeWindow.IDBTransaction.READ_WRITE);
            var request = transaction.objectStore(STORE_PREF).put({
                sid: storyid,
                preference: pref
            });
        };
    
        // Add methods to set each preference button's visual look.
        Util.extend(Visual, (function() {
            var factory = function(oncolor, offcolor) {
                return function(btn, isOn) {
                    btn.style.color = isOn ? oncolor : offcolor;
                    btn.style.border = "1px solid " + ( isOn ? oncolor : offcolor );
                };
            };
            
            return {
                toggleHideVisual : factory(HIDE_COLOR, NEUT_COLOR),
                toggleRecVisual  : factory(REC_COLOR, NEUT_COLOR),
                toggleShowVisual : factory(SHOW_COLOR, NEUT_COLOR)
            };
        })());
    
        // Toggle the summary given a story listing object.
        Visual.toggleSummary = function(obj, isOn) {
            var summary = obj.getElementsByTagName("div")[0];
            if (typeof isOn === "undefined") {
                if (summary.style.display === "none") {
                    summary.style.display = "";
                } else {
                    summary.style.display = "none";
                }
            } else {
                summary.style.display = isOn ? "" : "none";
            }
        };
    
        // Give the story listing object our initial look and feel.
        Visual.initializeStory = function(obj) {
            var a = obj.getElementsByTagName("a")[0];
    
            // Do something when the story object is clicked.
            obj.addEventListener("click", Event.storyListingClicked, false);
    
            // Create preference buttons.
            var privRecBtn = document.createElement("span");
            privRecBtn.style.cursor = "pointer";
            privRecBtn.style.fontFamily = "monospace";
            privRecBtn.style.borderRadius = "3px";
            privRecBtn.style.color = NEUT_COLOR;
            privRecBtn.style.border = "1px solid " + NEUT_COLOR;
            a.parentNode.insertBefore(privRecBtn, a);
    
            var privShowBtn = privRecBtn.cloneNode(false);
            var privHideBtn = privRecBtn.cloneNode(false);
            a.parentNode.insertBefore(privShowBtn, privRecBtn);
            a.parentNode.insertBefore(privHideBtn, privShowBtn);
    
            // Add individual preference button properties.
            privRecBtn.setAttribute("title", "Toggle Recommendation");
            privRecBtn.setAttribute("class", "recbtn");
            privRecBtn.style.marginRight = "5px";
            privRecBtn.innerHTML = "&nbsp;R&nbsp;";
            privRecBtn.addEventListener("click", Event.recClicked, false);
    
            privShowBtn.setAttribute("title", "Toggle Show");
            privShowBtn.setAttribute("class", "showbtn");
            privShowBtn.style.marginRight = "2px";
            privShowBtn.innerHTML = "&nbsp;S&nbsp;";
            privShowBtn.addEventListener("click", Event.showClicked, false);
    
            privHideBtn.setAttribute("title", "Toggle Hide");
            privHideBtn.setAttribute("class", "hidebtn");
            privHideBtn.style.marginRight = "2px";
            privHideBtn.innerHTML = "&nbsp;H&nbsp;";
            privHideBtn.addEventListener("click", Event.hideClicked, false);
        };
    
        // Update the appearance of a story appropriately.
        Visual.updateStoryVisual = function(sid) {
            // Get references to all the objects we need.
            var obj  = StoryListObj[sid].obj;
            var pref = StoryListObj[sid].pref;
            var lnks = obj.getElementsByTagName("a");
            var hideBtn = obj.getElementsByClassName("hidebtn")[0];
            var showBtn = obj.getElementsByClassName("showbtn")[0];
            var recBtn  = obj.getElementsByClassName("recbtn")[0];
    
            if (pref === H_FLAG) {
                Visual.toggleSummary(obj, false);
                Visual.toggleHideVisual(hideBtn, true);
                Visual.toggleShowVisual(showBtn, false);
                Visual.toggleRecVisual(recBtn, false);
    
                obj.style.opacity = "0.8";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = HIDE_COLOR;
                    lnks[i].style.fontWeight = "";
                }
    
                obj.style.display = 
                    (unsafeWindow.localStorage[LS_COMP_HIDE]) ? "none" : "";
            } else if (pref === S_FLAG) {
                Visual.toggleSummary(obj, true);
                Visual.toggleHideVisual(hideBtn, false);
                Visual.toggleShowVisual(showBtn, true);
                Visual.toggleRecVisual(recBtn, false);
    
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "";
                }
            } else if (pref === R_FLAG) {
                Visual.toggleSummary(obj, true);
                Visual.toggleHideVisual(hideBtn, false);
                Visual.toggleShowVisual(showBtn, true);
                Visual.toggleRecVisual(recBtn, true);
    
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "bold";
                }
            } else {
                Visual.toggleSummary(obj, true);
                Visual.toggleHideVisual(hideBtn, false);
                Visual.toggleShowVisual(showBtn, false);
                Visual.toggleRecVisual(recBtn, false);
    
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "";
                }
            }
        };
    
        // Update the appearance of all stories on the list.
        Visual.updateAllStories = function() {
            for (var sid in StoryListObj) {
                Visual.updateStoryVisual(sid);
            }
        };
    
        // Event handler determines what happens when story listing is clicked.
        Event.storyListingClicked = function(evt) {
            var sid = this.getAttribute("sid");
    
            // Do not toggle summary if story is not hidden.
            if (!StoryListObj[sid] || StoryListObj[sid].pref !== H_FLAG) {
                return;
            }
    
            Visual.toggleSummary(this);
        };
    
        // Event handler for the hide button.
        Event.hideClicked = function(evt) {
            var sid = this.parentNode.getAttribute("sid");
            var pref = (StoryListObj[sid].pref !== H_FLAG) ? H_FLAG : N_FLAG;
            Main.updateStory(sid, pref);
            evt.stopPropagation();
        };
    
        // Event handler for the show button.
        Event.showClicked = function(evt) {
            var sid = this.parentNode.getAttribute("sid");
            var pref = (StoryListObj[sid].pref !== S_FLAG) ? S_FLAG : N_FLAG;
            Main.updateStory(sid, pref);
        };
    
        // Event handler for the recommend button.
        Event.recClicked = function(evt) {
            var sid = this.parentNode.getAttribute("sid");
            var pref = (StoryListObj[sid].pref !== R_FLAG) ? R_FLAG : N_FLAG;
            Main.updateStory(sid, pref);
        };
    
        // Update story status property. This automatically updates the global
        // object property, story visuals, and saves in database.
        Main.updateStory = function(sid, pref) {
            StoryListObj[sid].pref = pref;
            Visual.updateStoryVisual(sid);
            Database.updateStory(sid, pref);
        };
    
        // Give the story list elements our look and feel.
        Main.initializeStoryList = function(parentObj) {
            if (!parentObj) {
                parentObj = document;
            }
            var stories = parentObj.getElementsByClassName("z-list");
            for (var i = 0; i < stories.length; i++) {
                // Check if story has already been initialized once before. This
                // sometimes happens on repeated loading of a user's profile
                // account, so we make this extra check.
                if (stories[i].getAttribute("shr")) {
                    continue;
                }
    
                // Get story ID.
                var a = stories[i].getElementsByTagName("a")[0];
                var sid = a.href.match(/\/s\/(\d+)\//i)[1];
    
                // Add some DOM attributes to this object.
                stories[i].setAttribute("sid", sid);
                stories[i].setAttribute("shr", "1");
    
                // Add story object to current page list.
                StoryListObj[sid] = { obj: stories[i] };
    
                Visual.initializeStory(stories[i]);
            }
        };
    
        // Apply rules to our preferences.
        Main.applyRules = function() {
            var hideRules = [];
            var showRules = [];
            var recRules  = [];
    
            // Get rules from storage. Firefox treats absence as NULL, Chrome
            // treats it as undefined, so we'll just take a shortcut here, since
            // we don't do anything anyway if no rules are specified.
            var retrieveRules = function(key, arr) {
                if (!(key in unsafeWindow.localStorage)) {
                    return;
                }
                var ruleArray = unsafeWindow.localStorage[key].split("\n");
                for (var i = 0; i < ruleArray.length; i++) {
                    var rule = Util.trim(ruleArray[i]);
                    if (rule.length <= 0) {
                        continue;
                    }
                    if (rule.substring(0,2) === "!#") {
                        arr.push(new RegExp(rule.substring(2)));
                    } else {
                        arr.push(new RegExp(rule, "i"));
                    }
                }
            };
    
            retrieveRules(LS_HIDE_RULES, hideRules);
            retrieveRules(LS_SHOW_RULES, showRules);
            retrieveRules(LS_REC_RULES, recRules);
            
            var ruleOrder = [
                [ recRules,  R_FLAG ],
                [ showRules, S_FLAG ],
                [ hideRules, H_FLAG ]
            ];
    
            for (var sid in StoryListObj) {
                // If story is not neutral, don't apply any rules.
                if (StoryListObj[sid].pref !== N_FLAG) {
                    continue;
                }
    
                var contents = StoryListObj[sid].obj.innerHTML;
    
                ApplyLoop:
                for (var j = 0; j < ruleOrder.length; j++) {
                    var pref  = ruleOrder[j][1];
                    var rules = ruleOrder[j][0];
                    for (var i = 0; i < rules.length; i++) {
                        if (contents.match(rules[i])) {
                            Main.updateStory(sid, pref);
                            break ApplyLoop;
                        }
                    }
                }
            }
        };
    
        // Create the script's control panel.
        var createControlPanel = function() {
            // We want a nice looking CSS3 striped background, and to keep it as
            // cross browser compatible as possible, we'll add an actual style.
            // This style also happens to include some other stuff for the panel.
            var bgStyle = " \
    #StoryHiderCP { \
      background-color: #0ae; \
      background-image: -webkit-gradient(linear, 0 100%, 100% 0, \
        color-stop(.25, rgba(255, 255, 255, .2)), color-stop(.25, transparent), \
        color-stop(.5, transparent), color-stop(.5, rgba(255, 255, 255, .2)), \
        color-stop(.75, rgba(255, 255, 255, .2)), color-stop(.75, transparent), \
    	to(transparent)); \
      background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      background-image: linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      position: fixed; \
      right: 0px; \
      bottom: 0px; \
      border-top: \"2px solid #06C\"; \
      border-left: \"2px solid #06C\"; \
      padding: 10px; \
    } \
    #StoryHiderCP th { \
      text-align: left; \
      color: #191970; \
    } \
    #StoryHiderCP div.btn { \
      cursor: pointer; \
      padding: 5px 10px; \
      border: 1px solid #06C; \
      background: #0cf; \
      font-weight: bold; \
    } \
    #StoryHiderCP a { \
      color: blue; \
      text-decoration: none; \
      border-bottom: none; \
    }";
            GM_addStyle(bgStyle);
    
            var shadowStyle = "0px 0px 10px #08c";
    
            var panel = document.createElement("div");
            panel.setAttribute("id", "StoryHiderCP");
    
            panel.innerHTML = " \
    <div id='StoryHiderCPMain' style='display: none;'> \
    <b>Control Panel</b> \
    <table style='width: 98%; margin: 0.6em auto;'> \
    <tr><th>Hide Rules</th><th>Show Rules</th><th>Recommend Rules</th></tr> \
    <tr> \
      <td><textarea id='StoryHiderT1' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
      <td><textarea id='StoryHiderT2' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
      <td><textarea id='StoryHiderT3' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
    </tr> \
    <tr><td colspan='3' style='font-size:0.9em;text-align:center;'> \
    Each line should be a <a href='http://www.regular-expressions.info/quickstart.html' target='_blank'> \
    regular expression</a> that matches against the story listing, which \
    includes the title, author, summary, and pairing. By priority, recommend \
    rules match first, then show rules, and lastly hide rules. For a case sensitive \
    match, put !# in front of your expression. \
    </td></tr> \
    </table> \
    <table style='margin: 0.6em auto;'> \
    <tr><th><label for='StoryHiderC1'>Completely Hide</label></th><th><input id='StoryHiderC1' type='checkbox'/></td></tr> \
    </table> \
    </div> \
    <div id='StoryHiderCPBtn' class='btn' style='float: right;'>Open</div> \
    ";
    
            document.getElementsByTagName("body")[0].appendChild(panel);
    
            // Load control panel with configurations.
            var rules = {
                "StoryHiderT1": LS_HIDE_RULES,
                "StoryHiderT2": LS_SHOW_RULES,
                "StoryHiderT3": LS_REC_RULES };
            for (var ta in rules) {
                if (unsafeWindow.localStorage[rules[ta]]) {
                    document.getElementById(ta).innerHTML =
                        unsafeWindow.localStorage[rules[ta]];
                }
            }
            if (unsafeWindow.localStorage[LS_COMP_HIDE]) {
                document.getElementById("StoryHiderC1").checked = true;
            }
    
            // Add event handlers.
            for (var ta in rules) {
                (function(obj, key) {
                    obj.addEventListener("change", function() {
                        unsafeWindow.localStorage[key] = this.value;
                    }, false);
                })(document.getElementById(ta), rules[ta]);
            }
    
            var toggleBtnClicked = function(evt) {
                if (this.innerHTML === "Open") {
                    this.innerHTML = "Close";
                    this.parentNode.style.width = "50%";
                    document.getElementById("StoryHiderCPMain").style.display = "";
                    unsafeWindow.sessionStorage[SS_CP_OPEN] = "1";
                    var cp = document.getElementById("StoryHiderCP");
                    cp.style.boxShadow = shadowStyle;
                } else {
                    this.innerHTML = "Open";
                    this.parentNode.style.width = "";
                    document.getElementById("StoryHiderCPMain").style.display = "none";
                    unsafeWindow.sessionStorage[SS_CP_OPEN] = "0";
                    var cp = document.getElementById("StoryHiderCP");
                    cp.style.boxShadow = "";
                    Main.applyRules();
                }
            };
    
            var cpBtn = document.getElementById("StoryHiderCPBtn");
            cpBtn.addEventListener("click", toggleBtnClicked, false);
    
            document.getElementById("StoryHiderC1").addEventListener("click",
                function(evt) {
                    if (this.checked) {
                        unsafeWindow.localStorage[LS_COMP_HIDE] = "1";
                    } else {
                        delete unsafeWindow.localStorage[LS_COMP_HIDE];
                    }
                    Visual.updateAllStories();
                }, false);
    
            // Do not close the panel if a click happens on top of it.
            panel.addEventListener("click", function(evt) {
                evt.stopPropagation();
            }, false);
    
            // Close the panel if a click happens off of it.
            document.getElementsByTagName("body")[0].addEventListener("click",
                function(evt) {
                    // Keep panel open if we clicked a link, because we may
                    // be changing pages with the panel open.
                    var origElt = evt.srcElement || evt.originalTarget;
                    if (origElt.tagName.toLowerCase() === "a") {
                        return false;
                    }
                    var b = document.getElementById("StoryHiderCPBtn");
                    if (b.innerHTML === "Close") {
                        toggleBtnClicked.apply(b, null);
                    }
            }, false);
    
            if (unsafeWindow.sessionStorage[SS_CP_OPEN] === "1") {
                toggleBtnClicked.apply(cpBtn, null);
            }
        };
    
        // Create an extra special module which serves to make this script work
        // in the favorities and profiles of authors. It intercepts when FFN draws
        // those stories into the DOM and does its magic afterward.
        var ProfileWatch = (function() {
            // Fixes bad FFN lists caused by redrawing from JavaScript. We inject
            // the HTML that we need directly into the document.
            var fixBadList = function(parentObj) {
                var badContent = parentObj.innerHTML;
                var snippet1 = "<div class='z-list'>";
                var snippet2 = "<div class='z-indent z-padtop'>";
                var snippet3 = "</div></div>";
                badContent = badContent.replace(/<blockquote>/g, snippet2);
                var arr = badContent.split("</blockquote>");
                var last = arr.pop();
                var goodContent = arr.join(snippet3 + snippet1);
                goodContent = snippet1 + goodContent + snippet3 + last;
                parentObj.innerHTML = goodContent;
            };
    
            return {
                run: function() {
                    // Intercept this function call.
                    if (typeof unsafeWindow.storylist_draw === "function") {
                        var orig = unsafeWindow.storylist_draw;
                        unsafeWindow.storylist_draw = function() {
                            var parentObj = document.getElementById(arguments[0]);
                            orig.apply(null, arguments);
                            fixBadList(parentObj);
                            Main.initializeStoryList(parentObj);
                            // We only do stuff if there are stories!
                            if (Util.getKeys(StoryListObj).length <= 0) {
                                return;
                            }
                            Database.loadCurrentPreferences(function() {
                                Main.applyRules();
                                Visual.updateAllStories();
                            });
                        };
                    }
                }
            };
        })();
    
        if (!isChrome) {
            ProfileWatch.run();
        } else if (pageScope) {
            // Chrome will run this in the page scope. We need to redraw the
            // favorite list; there's no other easy way to get things in done
            // in the right order (it's complicated).
            Database.open(function() {
                ProfileWatch.run();
                unsafeWindow.onload();
            });
        }
    
        if (isChrome && pageScope) return;
    
        Database.open(function() {
            Main.initializeStoryList();
            // We only do stuff if there are stories!
            if (Util.getKeys(StoryListObj).length <= 0) {
                return false;
            }
            Database.loadCurrentPreferences(function() {
                Main.applyRules();
                Visual.updateAllStories();
                createControlPanel();
            });
        });
    
    }(unsafeWindow));
    
    Obligatory Screenshot

    [​IMG]

    Editted to add a drop shadow around the control panel when open. Kind of pointless really, but oh well.
     
    Last edited: Mar 27, 2011
  9. Hero of Stupidity

    Hero of Stupidity Villain of Sensibility ~ Prestige ~ DLP Supporter

    Joined:
    Oct 5, 2010
    Messages:
    342
    Gender:
    Male
    Location:
    Hungary
    High Score:
    3,172
    So it only works with Chrome 11 beta? Why not with the 10? Other than that awesome dude.
     
  10. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    Thanks. It works only with 11 because Chrome 10 doesn't include the local database the script relies on. It's just being included in the newest versions of Firefox and Chrome.
     
  11. Hero of Stupidity

    Hero of Stupidity Villain of Sensibility ~ Prestige ~ DLP Supporter

    Joined:
    Oct 5, 2010
    Messages:
    342
    Gender:
    Male
    Location:
    Hungary
    High Score:
    3,172
    That is just plain mean.
     
  12. Beonid

    Beonid Seventh Year DLP Supporter

    Joined:
    Jul 12, 2008
    Messages:
    201
    Location:
    Melbourne
    Great concept - unfortunately, it doesn't seem to work for me. I enter:

    into Hide Rules. I hit close, then refresh the page. As you can see in the images below, it does exactly the opposite to what it should.


    [​IMG]

    When I refresh a couple of times, I get this:

    [​IMG]

    I've disabled all other Greasemonkey scripts I had running to make sure it wasn't to do with how the other scripts work. I'm guessing it works for everyone else?

    I'm using the latest stable release of Firefox 4.



    [​IMG]
     
  13. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    No, you are correct. The problem occurred in the newest rewritten version and stems from me forgetting how closures work in JavaScript (Example 5 if you are interested).

    I have fixed the bug in the code above. Sorry for the confusion and thanks for the report.

    Another quick edit. The code above should be working, but I realized I left a few strings I didn't need in it as reference by accident. So I deleted those and reuploaded the script.
     
    Last edited: Mar 27, 2011
  14. Sacro

    Sacro Groundskeeper

    Joined:
    Dec 18, 2010
    Messages:
    303
    Location:
    Germany
    As far as I can see, you put them in the recommend rule box, at least according to your first screenshot.

    I will test this script later and then post feedback, hopefully it will get rid of all the slash, read the books etc.
     
  15. Kimili

    Kimili Muggle

    Joined:
    Feb 2, 2012
    Messages:
    4
    I had been using this script with no issue until I upgraded to Firefox 10 today. Now it no longer works. The only other script I use is Fanfiction tools.
     
  16. SmileOfTheKill

    SmileOfTheKill Magical Amber

    Joined:
    Mar 24, 2007
    Messages:
    1,219
    Location:
    Florida, Sigh...
    It... somewhat works.

    The favorite stories in any author page still has everything. The script is still working, but it is just not hooking properly. I wish I had the skills to update this.
     
  17. Kimili

    Kimili Muggle

    Joined:
    Feb 2, 2012
    Messages:
    4
    Me as well, since this script has been very handy.

    I checked and it works on the author's favorites.
     
  18. Zyloch

    Zyloch Fourth Year

    Joined:
    Jun 2, 2007
    Messages:
    116
    I haven't touched this script in awhile, but I'm glad people are finding a use for it. If I find more time perhaps I can expand this further somehow. However, I did find some time to at least investigate why it did not seem to be working in the newest FF versions.

    Here's an updated version for you guys to test. If it does not work, let me know:

    Code:
    // ==UserScript==
    // @name Story Hider
    // @namespace http://www.fanfiction.net/
    // @description Hides stories on FanFiction.Net
    // @version 1.1.4
    // @include http://*.fanfiction.net/*
    // @match http://*.fanfiction.net/*
    // ==/UserScript==
    
    // @author Zyloch
    // Our code is tested on Firefox 4 RC2 and Google Chrome 11.
    (function StoryHider(unsafeWindow) {
    
        /** 
         * These constants determine the color of the preference buttons left of
         * each story title.
         */
        const SHOW_COLOR    = "green";
        const HIDE_COLOR    = "#800000";
        const REC_COLOR     = "#DAA520";
        const NEUT_COLOR    = "#DDDDDD";
    
        /**
         * These constants control the local storage key names we use. Most users
         * will not need to touch these. NOTE: Local storage is used by us to store 
         * configuration data.
         */
        const LS_HIDE_RULES = "hideRules";  // Key for storing hide rules.
        const LS_SHOW_RULES = "showRules";  // Key for storing show rules.
        const LS_REC_RULES  = "recRules";   // Key for storing recommend rules.
        const LS_COMP_HIDE  = "compHide";   // Key for complete hide option.
    
        /**
         * These constants control the session storage key names we use.
         */
        const SS_CP_OPEN = "StoryHideCPOpen";   // Key for storing CP open or not.
    
        /**
         * These constants control the names and values of database components in
         * the script. These should not need to be changed by the user.
         *
         * Note: We anticipate storing many story IDs. Therefore, we use HTML5's
         * new Indexed DB API to persistently store and retrieve our data. This
         * will hopefully increase performance in the long run.
         */
        const DB_NAME = "StoryHider";       // Database name.
        const STORE_PREF = "PrivatePref";   // Story preference store.
    
        const N_FLAG = 0;                   // In database: Neutral story.
        const S_FLAG = 1;                   // In database: Show story.
        const H_FLAG = 2;                   // In database: Hide story.
        const R_FLAG = 3;                   // In database: Recommended story.
    
        // Database error codes defined as constants.
        const DB_CONSTRAINT_ERR = 3;
        const DB_NON_TRANSIENT_ERR = 1;
        const DB_TRANSACTION_INACTIVE_ERR = 6;
        const DB_UNKNOWN_ERR = 0;
    
        // GreaseMonkey gives Firefox access to the actual not sandboxed copy of
        // the window object, called unsafeWindow. Chrome does not. Here, create
        // a local name alias to take care of either case.
        unsafeWindow = unsafeWindow || window;
    
        // This workaround is for Chrome, which does not give us access to the
        // variables and functions of the underlying page. We use a variant of
        // the Content Scope Runner technique to run the script twice, once in
        // GreaseMonkey context and once in page context.
        var pageScope = (typeof __STORY_HIDER_PAGE_SCOPE__ !== "undefined");
        var ua = navigator.userAgent.toLowerCase();
        var isChrome = ua.indexOf("chrome") !== -1;
    
        // Inject our page scope marker into the page.
        if (isChrome && !pageScope) {
            var src = "(" + StoryHider.caller.toString() + ")();";
            var marker = "var __STORY_HIDER_PAGE_SCOPE__ = true;";
            var script = document.createElement("script");
            script.setAttribute("type", "application/javascript");
            script.innerHTML = marker + src;
    
            // Insert the script node into the page, so it will run, and 
            // immediately remove it to clean up. Use setTimeout to force 
            // execution "outside" of the user script scope completely.
            setTimeout(function() {
                document.body.appendChild(script);
                document.body.removeChild(script);
            }, 0);
        }
    
        /** 
         * Until Indexed DB becomes standard, we need to take care of cross
         * browser differences by aliasing and defining constants. */
    
        // Give a uniform access to the main Indexed DB object.
        if (!("indexedDB" in unsafeWindow)) {
            if ("mozIndexedDB" in unsafeWindow) {
                unsafeWindow.indexedDB = unsafeWindow.mozIndexedDB;
            } else if ("webkitIndexedDB" in unsafeWindow) {
                unsafeWindow.indexedDB = unsafeWindow.webkitIndexedDB;
            } else if ("moz_indexedDB" in unsafeWindow) {
                unsafeWindow.indexedDB = unsafeWindow.moz_indexedDB;
            } else {
                alert("The script could not detect Indexed DB support.");
            }
        }
    
        // Give a uniform access to the main transaction object.
        if (!("IDBTransaction" in unsafeWindow)) {
            if ("webkitIDBTransaction" in unsafeWindow) {
                unsafeWindow.IDBTransaction = unsafeWindow.webkitIDBTransaction;
            }
        }
    
        // Give a uniform access to the database exception object.
        if (!("IDBDatabaseException" in unsafeWindow)) {
            if ("webkitIDBDatabaseException" in unsafeWindow) {
                unsafeWindow.IDBDatabaseException = unsafeWindow.webkitIDBDatabaseException;
            }
        }
    
        // FF4 RC2 exposes window.IDBTransaction but does not give it any of its
        // constant values for some reason. We need to define them here manually.
        if (!("READ_ONLY" in unsafeWindow.IDBTransaction)) {
            unsafeWindow.IDBTransaction.READ_ONLY = 0;
        }
        if (!("READ_WRITE" in unsafeWindow.IDBTransaction)) {
            unsafeWindow.IDBTransaction.READ_WRITE = 1;
        }
    
        // FF4 RC2 also exposes window.IDBDatabaseException but does not give it
        // any of its constant values. Define them manually. We would define
        // them manually, but the values listed on the documentation website do
        // not seem to be the actual values stored in memory. So, we hold off on
        // error checking for now.
        
        // Initialize variable that remembers whether control panel is open.
        if (!(SS_CP_OPEN in unsafeWindow.sessionStorage)) {
            unsafeWindow.sessionStorage[SS_CP_OPEN] = "0";
        }
    
        // Global variable used to hold story listing objects on current page.
        var StoryListObj = {};
    
        // Create several namespaces useful to compartmentalize our code.
        var Main = {};              // main methods
        var Database = {};          // database methods
        var Event = {};             // event handlers
        var Util = {};              // helper methods
        var Visual = {};            // page layour methods
    
        Database.db = null;
    
        // Merge the second object into the first one. The third parameter 
        // determines whether to overwrite existing properties or not.
        Util.extend = function(base, ext, ow) {
            for (var key in ext) {
                if (ow || !(key in base)) {
                    base[key] = ext[key];
                }
            }
        };
    
        // Returns object keys as an array.
        Util.getKeys = function(obj) {
            var keys = [];
            for (var key in obj) {
                keys.push(key);
            }
            return keys;
        };
    
        // Trim whitespace off the ends of a string. By Adrian Flesler.
        Util.trim = function(str) {
            var start = -1,
            end = str.length;
            while( str.charCodeAt(--end) < 33 );
            while( str.charCodeAt(++start) < 33 );
            return str.slice( start, end + 1 );
        };
    
        // Open the database. Allow a callback function to be passed that contains
        // everything that happens only after the database is opened.
        Database.open = function(callback) {
            var request = unsafeWindow.indexedDB.open(DB_NAME);
            request.onsuccess = function(evt) {
                var version = "1.0";
                var db = Database.db = this.result;
    
                // FF10 and onwards have their separate method.
                if (!db.setVersion) {
                    if (parseInt(version) === db.version) {
                        // We are done preparing the database. Move on.
                        if (typeof callback === "function") {
                            callback();
                        }
                    }
                    return;
                }
    
                // Object stores have not yet been created.
                if (db.version !== version) {
                    // Set the DB version so that next time we don't do this.
                    var request = db.setVersion(version);
                    request.onsuccess = function(evt) {
                        // Create the object stores that we need.
                        db.createObjectStore(STORE_PREF, { keyPath : "sid" });
    
                        // We are done preparing the database. Move on.
                        if (typeof callback === "function") {
                            callback();
                        }
                    };
    
                    request.onerror = function(evt) {
                        if (request.errorCode === DB_CONSTRAINT_ERR) {
                            // Our object stores already exist. Forge onward.
                            if (typeof callback === "function") {
                                callback();
                            }
                        }
                    };
                }
                else if (typeof callback === "function") {
                    callback();
                }
            };
    
            request.onerror = function(evt) {
                alert(request.errorCode);
            };
    
            // For FF10 and onwards.
            req.onupgradeneeded = function(e) {
                // Create the object stores that we need.
                db.createObjectStore(STORE_PREF, { keyPath : "sid" });
    
                // We are done preparing the database. Move on.
                if (typeof callback === "function") {
                    callback();
                }
            };
        };
    
        // Load preferences related to stories on the current page into the global
        // object designated to hold such data. Allow a callback to be passed.
        Database.loadCurrentPreferences = function(callback) {
            var currentStories = Util.getKeys(StoryListObj);
            var transaction = Database.db.transaction([STORE_PREF]);
            var store = transaction.objectStore(STORE_PREF);
    
            // Cascade read all the entries we need to read.
            var loadNext = function() {
                if (currentStories.length > 0) {
                    var sid = currentStories.pop();
                    var req = store.get(sid);
                    req.onsuccess = function(evt) {
                        if (this.result) {
                            StoryListObj[sid].pref = 
                                parseInt(this.result.preference);
                        } else {
                            StoryListObj[sid].pref = N_FLAG;
                        }
                        loadNext();
                    };
                } else if (typeof callback === "function") {
                    callback();
                }
            };
            loadNext();
        };
    
        // Update story preferences in the database.
        Database.updateStory = function(storyid, pref) {
            var transaction = Database.db.transaction([STORE_PREF], 
                unsafeWindow.IDBTransaction.READ_WRITE);
            var request = transaction.objectStore(STORE_PREF).put({
                sid: storyid,
                preference: pref
            });
        };
    
        // Add methods to set each preference button's visual look.
        Util.extend(Visual, (function() {
            var factory = function(oncolor, offcolor) {
                return function(btn, isOn) {
                    btn.style.color = isOn ? oncolor : offcolor;
                    btn.style.border = "1px solid " + ( isOn ? oncolor : offcolor );
                };
            };
            
            return {
                toggleHideVisual : factory(HIDE_COLOR, NEUT_COLOR),
                toggleRecVisual  : factory(REC_COLOR, NEUT_COLOR),
                toggleShowVisual : factory(SHOW_COLOR, NEUT_COLOR)
            };
        })());
    
        // Toggle the summary given a story listing object.
        Visual.toggleSummary = function(obj, isOn) {
            var summary = obj.getElementsByTagName("div")[0];
            if (typeof isOn === "undefined") {
                if (summary.style.display === "none") {
                    summary.style.display = "";
                } else {
                    summary.style.display = "none";
                }
            } else {
                summary.style.display = isOn ? "" : "none";
            }
        };
    
        // Give the story listing object our initial look and feel.
        Visual.initializeStory = function(obj) {
            var a = obj.getElementsByTagName("a")[0];
    
            // Do something when the story object is clicked.
            obj.addEventListener("click", Event.storyListingClicked, false);
    
            // Create preference buttons.
            var privRecBtn = document.createElement("span");
            privRecBtn.style.cursor = "pointer";
            privRecBtn.style.fontFamily = "monospace";
            privRecBtn.style.borderRadius = "3px";
            privRecBtn.style.color = NEUT_COLOR;
            privRecBtn.style.border = "1px solid " + NEUT_COLOR;
            a.parentNode.insertBefore(privRecBtn, a);
    
            var privShowBtn = privRecBtn.cloneNode(false);
            var privHideBtn = privRecBtn.cloneNode(false);
            a.parentNode.insertBefore(privShowBtn, privRecBtn);
            a.parentNode.insertBefore(privHideBtn, privShowBtn);
    
            // Add individual preference button properties.
            privRecBtn.setAttribute("title", "Toggle Recommendation");
            privRecBtn.setAttribute("class", "recbtn");
            privRecBtn.style.marginRight = "5px";
            privRecBtn.innerHTML = "&nbsp;R&nbsp;";
            privRecBtn.addEventListener("click", Event.recClicked, false);
    
            privShowBtn.setAttribute("title", "Toggle Show");
            privShowBtn.setAttribute("class", "showbtn");
            privShowBtn.style.marginRight = "2px";
            privShowBtn.innerHTML = "&nbsp;S&nbsp;";
            privShowBtn.addEventListener("click", Event.showClicked, false);
    
            privHideBtn.setAttribute("title", "Toggle Hide");
            privHideBtn.setAttribute("class", "hidebtn");
            privHideBtn.style.marginRight = "2px";
            privHideBtn.innerHTML = "&nbsp;H&nbsp;";
            privHideBtn.addEventListener("click", Event.hideClicked, false);
        };
    
        // Update the appearance of a story appropriately.
        Visual.updateStoryVisual = function(sid) {
            // Get references to all the objects we need.
            var obj  = StoryListObj[sid].obj;
            var pref = StoryListObj[sid].pref;
            var lnks = obj.getElementsByTagName("a");
            var hideBtn = obj.getElementsByClassName("hidebtn")[0];
            var showBtn = obj.getElementsByClassName("showbtn")[0];
            var recBtn  = obj.getElementsByClassName("recbtn")[0];
    
            if (pref === H_FLAG) {
                Visual.toggleSummary(obj, false);
                Visual.toggleHideVisual(hideBtn, true);
                Visual.toggleShowVisual(showBtn, false);
                Visual.toggleRecVisual(recBtn, false);
    
                obj.style.opacity = "0.8";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = HIDE_COLOR;
                    lnks[i].style.fontWeight = "";
                }
    
                obj.style.display = 
                    (unsafeWindow.localStorage[LS_COMP_HIDE]) ? "none" : "";
            } else if (pref === S_FLAG) {
                Visual.toggleSummary(obj, true);
                Visual.toggleHideVisual(hideBtn, false);
                Visual.toggleShowVisual(showBtn, true);
                Visual.toggleRecVisual(recBtn, false);
    
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "";
                }
            } else if (pref === R_FLAG) {
                Visual.toggleSummary(obj, true);
                Visual.toggleHideVisual(hideBtn, false);
                Visual.toggleShowVisual(showBtn, true);
                Visual.toggleRecVisual(recBtn, true);
    
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "bold";
                }
            } else {
                Visual.toggleSummary(obj, true);
                Visual.toggleHideVisual(hideBtn, false);
                Visual.toggleShowVisual(showBtn, false);
                Visual.toggleRecVisual(recBtn, false);
    
                obj.style.opacity = "1.0";
                for (var i = 0; i < lnks.length; i++) {
                    lnks[i].style.color = "";
                    lnks[i].style.fontWeight = "";
                }
            }
        };
    
        // Update the appearance of all stories on the list.
        Visual.updateAllStories = function() {
            for (var sid in StoryListObj) {
                Visual.updateStoryVisual(sid);
            }
        };
    
        // Event handler determines what happens when story listing is clicked.
        Event.storyListingClicked = function(evt) {
            var sid = this.getAttribute("sid");
    
            // Do not toggle summary if story is not hidden.
            if (!StoryListObj[sid] || StoryListObj[sid].pref !== H_FLAG) {
                return;
            }
    
            Visual.toggleSummary(this);
        };
    
        // Event handler for the hide button.
        Event.hideClicked = function(evt) {
            var sid = this.parentNode.getAttribute("sid");
            var pref = (StoryListObj[sid].pref !== H_FLAG) ? H_FLAG : N_FLAG;
            Main.updateStory(sid, pref);
            evt.stopPropagation();
        };
    
        // Event handler for the show button.
        Event.showClicked = function(evt) {
            var sid = this.parentNode.getAttribute("sid");
            var pref = (StoryListObj[sid].pref !== S_FLAG) ? S_FLAG : N_FLAG;
            Main.updateStory(sid, pref);
        };
    
        // Event handler for the recommend button.
        Event.recClicked = function(evt) {
            var sid = this.parentNode.getAttribute("sid");
            var pref = (StoryListObj[sid].pref !== R_FLAG) ? R_FLAG : N_FLAG;
            Main.updateStory(sid, pref);
        };
    
        // Update story status property. This automatically updates the global
        // object property, story visuals, and saves in database.
        Main.updateStory = function(sid, pref) {
            StoryListObj[sid].pref = pref;
            Visual.updateStoryVisual(sid);
            Database.updateStory(sid, pref);
        };
    
        // Give the story list elements our look and feel.
        Main.initializeStoryList = function(parentObj) {
            if (!parentObj) {
                parentObj = document;
            }
            var stories = parentObj.getElementsByClassName("z-list");
            for (var i = 0; i < stories.length; i++) {
                // Check if story has already been initialized once before. This
                // sometimes happens on repeated loading of a user's profile
                // account, so we make this extra check.
                if (stories[i].getAttribute("shr")) {
                    continue;
                }
    
                // Get story ID.
                var a = stories[i].getElementsByTagName("a")[0];
                var sid = a.href.match(/\/s\/(\d+)\//i)[1];
    
                // Add some DOM attributes to this object.
                stories[i].setAttribute("sid", sid);
                stories[i].setAttribute("shr", "1");
    
                // Add story object to current page list.
                StoryListObj[sid] = { obj: stories[i] };
    
                Visual.initializeStory(stories[i]);
            }
        };
    
        // Apply rules to our preferences.
        Main.applyRules = function() {
            var hideRules = [];
            var showRules = [];
            var recRules  = [];
    
            // Get rules from storage. Firefox treats absence as NULL, Chrome
            // treats it as undefined, so we'll just take a shortcut here, since
            // we don't do anything anyway if no rules are specified.
            var retrieveRules = function(key, arr) {
                if (!(key in unsafeWindow.localStorage)) {
                    return;
                }
                var ruleArray = unsafeWindow.localStorage[key].split("\n");
                for (var i = 0; i < ruleArray.length; i++) {
                    var rule = Util.trim(ruleArray[i]);
                    if (rule.length <= 0) {
                        continue;
                    }
                    if (rule.substring(0,2) === "!#") {
                        arr.push(new RegExp(rule.substring(2)));
                    } else {
                        arr.push(new RegExp(rule, "i"));
                    }
                }
            };
    
            retrieveRules(LS_HIDE_RULES, hideRules);
            retrieveRules(LS_SHOW_RULES, showRules);
            retrieveRules(LS_REC_RULES, recRules);
            
            var ruleOrder = [
                [ recRules,  R_FLAG ],
                [ showRules, S_FLAG ],
                [ hideRules, H_FLAG ]
            ];
    
            for (var sid in StoryListObj) {
                // If story is not neutral, don't apply any rules.
                if (StoryListObj[sid].pref !== N_FLAG) {
                    continue;
                }
    
                var contents = StoryListObj[sid].obj.innerHTML;
    
                ApplyLoop:
                for (var j = 0; j < ruleOrder.length; j++) {
                    var pref  = ruleOrder[j][1];
                    var rules = ruleOrder[j][0];
                    for (var i = 0; i < rules.length; i++) {
                        if (contents.match(rules[i])) {
                            Main.updateStory(sid, pref);
                            break ApplyLoop;
                        }
                    }
                }
            }
        };
    
        // Create the script's control panel.
        var createControlPanel = function() {
            // We want a nice looking CSS3 striped background, and to keep it as
            // cross browser compatible as possible, we'll add an actual style.
            // This style also happens to include some other stuff for the panel.
            var bgStyle = " \
    #StoryHiderCP { \
      background-color: #0ae; \
      background-image: -webkit-gradient(linear, 0 100%, 100% 0, \
        color-stop(.25, rgba(255, 255, 255, .2)), color-stop(.25, transparent), \
        color-stop(.5, transparent), color-stop(.5, rgba(255, 255, 255, .2)), \
        color-stop(.75, rgba(255, 255, 255, .2)), color-stop(.75, transparent), \
    	to(transparent)); \
      background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      background-image: linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, \
    	transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, \
    	transparent 75%, transparent); \
      position: fixed; \
      right: 0px; \
      bottom: 0px; \
      border-top: \"2px solid #06C\"; \
      border-left: \"2px solid #06C\"; \
      padding: 10px; \
    } \
    #StoryHiderCP th { \
      text-align: left; \
      color: #191970; \
    } \
    #StoryHiderCP div.btn { \
      cursor: pointer; \
      padding: 5px 10px; \
      border: 1px solid #06C; \
      background: #0cf; \
      font-weight: bold; \
    } \
    #StoryHiderCP a { \
      color: blue; \
      text-decoration: none; \
      border-bottom: none; \
    }";
            GM_addStyle(bgStyle);
    
            var shadowStyle = "0px 0px 10px #08c";
    
            var panel = document.createElement("div");
            panel.setAttribute("id", "StoryHiderCP");
    
            panel.innerHTML = " \
    <div id='StoryHiderCPMain' style='display: none;'> \
    <b>Control Panel</b> \
    <table style='width: 98%; margin: 0.6em auto;'> \
    <tr><th>Hide Rules</th><th>Show Rules</th><th>Recommend Rules</th></tr> \
    <tr> \
      <td><textarea id='StoryHiderT1' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
      <td><textarea id='StoryHiderT2' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
      <td><textarea id='StoryHiderT3' style='width: 90%;' wrap='off' rows='5'></textarea></td> \
    </tr> \
    <tr><td colspan='3' style='font-size:0.9em;text-align:center;'> \
    Each line should be a <a href='http://www.regular-expressions.info/quickstart.html' target='_blank'> \
    regular expression</a> that matches against the story listing, which \
    includes the title, author, summary, and pairing. By priority, recommend \
    rules match first, then show rules, and lastly hide rules. For a case sensitive \
    match, put !# in front of your expression. \
    </td></tr> \
    </table> \
    <table style='margin: 0.6em auto;'> \
    <tr><th><label for='StoryHiderC1'>Completely Hide</label></th><th><input id='StoryHiderC1' type='checkbox'/></td></tr> \
    </table> \
    </div> \
    <div id='StoryHiderCPBtn' class='btn' style='float: right;'>Open</div> \
    ";
    
            document.getElementsByTagName("body")[0].appendChild(panel);
    
            // Load control panel with configurations.
            var rules = {
                "StoryHiderT1": LS_HIDE_RULES,
                "StoryHiderT2": LS_SHOW_RULES,
                "StoryHiderT3": LS_REC_RULES };
            for (var ta in rules) {
                if (unsafeWindow.localStorage[rules[ta]]) {
                    document.getElementById(ta).innerHTML =
                        unsafeWindow.localStorage[rules[ta]];
                }
            }
            if (unsafeWindow.localStorage[LS_COMP_HIDE]) {
                document.getElementById("StoryHiderC1").checked = true;
            }
    
            // Add event handlers.
            for (var ta in rules) {
                (function(obj, key) {
                    obj.addEventListener("change", function() {
                        unsafeWindow.localStorage[key] = this.value;
                    }, false);
                })(document.getElementById(ta), rules[ta]);
            }
    
            var toggleBtnClicked = function(evt) {
                if (this.innerHTML === "Open") {
                    this.innerHTML = "Close";
                    this.parentNode.style.width = "50%";
                    document.getElementById("StoryHiderCPMain").style.display = "";
                    unsafeWindow.sessionStorage[SS_CP_OPEN] = "1";
                    var cp = document.getElementById("StoryHiderCP");
                    cp.style.boxShadow = shadowStyle;
                } else {
                    this.innerHTML = "Open";
                    this.parentNode.style.width = "";
                    document.getElementById("StoryHiderCPMain").style.display = "none";
                    unsafeWindow.sessionStorage[SS_CP_OPEN] = "0";
                    var cp = document.getElementById("StoryHiderCP");
                    cp.style.boxShadow = "";
                    Main.applyRules();
                }
            };
    
            var cpBtn = document.getElementById("StoryHiderCPBtn");
            cpBtn.addEventListener("click", toggleBtnClicked, false);
    
            document.getElementById("StoryHiderC1").addEventListener("click",
                function(evt) {
                    if (this.checked) {
                        unsafeWindow.localStorage[LS_COMP_HIDE] = "1";
                    } else {
                        delete unsafeWindow.localStorage[LS_COMP_HIDE];
                    }
                    Visual.updateAllStories();
                }, false);
    
            // Do not close the panel if a click happens on top of it.
            panel.addEventListener("click", function(evt) {
                evt.stopPropagation();
            }, false);
    
            // Close the panel if a click happens off of it.
            document.getElementsByTagName("body")[0].addEventListener("click",
                function(evt) {
                    // Keep panel open if we clicked a link, because we may
                    // be changing pages with the panel open.
                    var origElt = evt.srcElement || evt.originalTarget;
                    if (origElt.tagName.toLowerCase() === "a") {
                        return false;
                    }
                    var b = document.getElementById("StoryHiderCPBtn");
                    if (b.innerHTML === "Close") {
                        toggleBtnClicked.apply(b, null);
                    }
            }, false);
    
            if (unsafeWindow.sessionStorage[SS_CP_OPEN] === "1") {
                toggleBtnClicked.apply(cpBtn, null);
            }
        };
    
        // Create an extra special module which serves to make this script work
        // in the favorities and profiles of authors. It intercepts when FFN draws
        // those stories into the DOM and does its magic afterward.
        var ProfileWatch = (function() {
            // Fixes bad FFN lists caused by redrawing from JavaScript. We inject
            // the HTML that we need directly into the document.
            var fixBadList = function(parentObj) {
                var badContent = parentObj.innerHTML;
                var snippet1 = "<div class='z-list'>";
                var snippet2 = "<div class='z-indent z-padtop'>";
                var snippet3 = "</div></div>";
                badContent = badContent.replace(/<blockquote>/g, snippet2);
                var arr = badContent.split("</blockquote>");
                var last = arr.pop();
                var goodContent = arr.join(snippet3 + snippet1);
                goodContent = snippet1 + goodContent + snippet3 + last;
                parentObj.innerHTML = goodContent;
            };
    
            return {
                run: function() {
                    // Intercept this function call.
                    if (typeof unsafeWindow.storylist_draw === "function") {
                        var orig = unsafeWindow.storylist_draw;
                        unsafeWindow.storylist_draw = function() {
                            var parentObj = document.getElementById(arguments[0]);
                            orig.apply(null, arguments);
                            fixBadList(parentObj);
                            Main.initializeStoryList(parentObj);
                            // We only do stuff if there are stories!
                            if (Util.getKeys(StoryListObj).length <= 0) {
                                return;
                            }
                            Database.loadCurrentPreferences(function() {
                                Main.applyRules();
                                Visual.updateAllStories();
                            });
                        };
                    }
                }
            };
        })();
    
        if (!isChrome) {
            ProfileWatch.run();
        } else if (pageScope) {
            // Chrome will run this in the page scope. We need to redraw the
            // favorite list; there's no other easy way to get things in done
            // in the right order (it's complicated).
            Database.open(function() {
                ProfileWatch.run();
                unsafeWindow.onload();
            });
        }
    
        if (isChrome && pageScope) return;
    
        Database.open(function() {
            Main.initializeStoryList();
            // We only do stuff if there are stories!
            if (Util.getKeys(StoryListObj).length <= 0) {
                return false;
            }
            Database.loadCurrentPreferences(function() {
                Main.applyRules();
                Visual.updateAllStories();
                createControlPanel();
            });
        });
    
    }(unsafeWindow));
    Thanks for the support.
     
  19. Kimili

    Kimili Muggle

    Joined:
    Feb 2, 2012
    Messages:
    4
    It works now. Thank you muchly.
     
  20. SmileOfTheKill

    SmileOfTheKill Magical Amber

    Joined:
    Mar 24, 2007
    Messages:
    1,219
    Location:
    Florida, Sigh...
    I love you so god damn much right now.

    Endless praise from me.