Google Reader - Colorful List View

By kepp Last update Aug 22, 2009 — Installed 23,117 times. Daily Installs: 31, 27, 29, 32, 33, 25, 25, 47, 21, 32, 30, 26, 28, 31, 41, 25, 29, 40, 19, 38, 35, 40, 33, 37, 24, 35, 34, 54, 28, 40, 45, 24

There are 7 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           Google Reader - Colorful List View
// @namespace      http://google.reader.colorful.list.view/kepp
// @include        http://www.google.com/reader/*
// @include        https://www.google.com/reader/*
// @include        http://userscripts.org/scripts/source/8782.meta.js
// @description    Colorizes the item headers in Google Reader list view and
// @description    the entries in expanded view.
// @version        20090822
// ==/UserScript==

/**
 * 20090822
 * Fix to ensure that all items get colored.
 * Fixes for Google Reader update
 * Added script update notification to the settings page
 * Added Opera compatibility
 * - Remove use of "for each"
 * - Add alternatives to GM_ functions (GM_addStyle, GM_setValue, GM_getValue)
 * - Modify coloring CSS
 * Cleaned up some code
 * Also added DOM Storage fallback option
 * 
 *
 * 20081214
 * Prefs split out into independent items and updated to apply instantly.
 *  Pref notification messages are also fixed to work properly.
 * Fix for script not working if Google Gears was installed (my bad design).
 * Works on expanded view too now. Possible/easier with Google Reader now using
 *  CSS for rounded borders.
 **
 * 20081104 
 * Added settings for coloring read/unread items
 * Adjusted things in the settings
 **
 * 20080730
 * Fixed css mistake of read items not being colored.
 * Added https:// url to the include list
 * Added coloring option on settings page, added settings page to the exclude
 * list
 **/

// var script = document.createElement("script");
// script.innerHTML = "(" + 
(function()
{

  // info used to check for script updates
  const SCRIPT_INFO =
  {
    version:    "20090822",
    date:       "Mon, 22 Aug 2009 00:00:00 GMT",
    updateUrl:  "http://userscripts.org/scripts/source/8782.meta.js",
    installUrl: "http://userscripts.org/scripts/source/8782.user.js"
  };

  // CSS to allow items to be colored
  const BASE_CSS = "\
    .entry-likers /* like count */\
    {\
      background-color: transparent !important;\
    }\
    .gm-color-lv .collapsed /* list view headers */\
    {\
      border-color: transparent !important;\
    }\
    #entries.list.gm-color-lv #current-entry .collapsed\
    {\
      border: 2px solid #8181DC !important;\
    }\
    #entries.list.gm-color-ui #current-entry.expanded .collapsed\
    {\
      border-bottom-color: transparent !important;\
      border-width: 2px 0 !important;\
    }\
    #entries .entry\
    {\
      padding: 5px 0;\
    }";

  const STRINGS =
  {
    // pref labels
    list:       "When in list view.",
    expanded:   "When in expanded view.",
    read:       "That have been marked as read.",
    unread:     "That are unread.",

    scheme:     "Color Scheme: ",
    def:        "Default",
    custom:     "Custom"
  };


//=============================================================================


  function $id( id )
  {
    return document.getElementById( id );
  }

  function $x( query, context )
  {
    var doc = ( context ) ? context.ownerDocument : document
    return doc.evaluate( query, ( context || doc ), null, 
           XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue;
  }

  function $xa( query )
  {
    var res = document.evaluate( query, document, null,
              XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
    var element, array = [];
    while ( element = res.iterateNext() )
      array.push( element );

    return array;
  }

  function addStyle( css )
  {
    var style = document.createElement( "style" );
    style.type = "text/css";
    style.innerHTML = css;
    document.getElementsByTagName( "head" )[0].appendChild( style );
    return style;
  }


//=============================================================================


  // script updater
  var updater =
  {
    loader: null,
    version: 0,
    homeUrl: "",
    updateUrl: "",
    installUrl: "",

    init: function()
    {
      for ( var prop in SCRIPT_INFO )
        this[ prop ] = SCRIPT_INFO[ prop ];

      // test if this is the script meta info page that loaded
      if ( location.href == SCRIPT_INFO.updateUrl )
      {
        document.body.setAttribute( "style", "visibility: hidden;\
                                              overflow: hidden;" );

        // running on userscripts.org domain  
        // update link not inserted yet document.write and there's an update
        if ( !document.getElementsByTagName( "a" ).length )
        {
          if ( this.parseMetaInfo() )
            this.insertUpdateLink(); // this will "reload" the page
        }
        else
        {
          document.body.setAttribute( "style", "visibility: visible;\
            overflow: hidden;font-family: Arial, sans-serif;color: #2244BB;" );
        }
        return true; // notify that this was the script meta page
      }

      var loader = document.createElement( "iframe" );
      loader.setAttribute( "style", "position: absolute;\
                                     height: 0; width: 0;" );
      this.loader = document.body.appendChild( loader );
    },

    parseMetaInfo: function()
    {
      var scriptInfo = document.body.innerHTML;
      var updateAvailable;

      // compare script versions
      if ( /@version\s*([\S]+)/.test( scriptInfo ) )
        updateAvailable = this.version < RegExp.$1;

      // compare script dates
      else if ( /@uso:timestamp\s*(\S.+)/.test( scriptInfo ) )
        updateAvailable = new Date( this.date ) < new Date( RegExp.$1 );

      return updateAvailable;
    },

    insertUpdateLink: function()
    {
      var me = this;
      setTimeout( function() {

        // insert update url. hackish =\
        document.write( "<html><body>\<a href=\"" + me.installUrl + 
          "\" target=\"_blank\">Userscript Update Available</a>\
          </body></html>" );

        document.close();
      }, 0 )
    },


    check: function() // runs on google.com domain
    {
      var lastCheck = storage.getItem( "last-check", 0 );
      if ( new Date().getTime() - lastCheck < 3*24*60*60*1000 ) // 3 days
        return false;

      this.loader.setAttribute( "style", "visibility: visible;\
                                          overflow: hidden;\
                                          position: absolute; right: 2em;\
                                          height: 2em; width: 20em;" );
      this.loader.src = this.updateUrl;

      storage.setItem( "last-check", new Date().getTime() + "" );
      return this.loader;
    }
  };

  // user interface for script settings added on settings page
  var settings = 
  {
    timeoutID: 0,
    entries: null,

    init: function() // insert page color options into the settings page
    {
      // ascend out of iframe. hopefully this works
      this.entries = frameElement.ownerDocument.getElementById( "entries" );

      var sect = this.addPrefs();

      var check = updater.check();
      if ( check )
        sect.insertBefore( check, sect.firstChild );
    },

    addPrefs: function()
    {
      var sect = document.createElement( "div" );
      sect.className = "extra";

      sect.innerHTML = "<div class=\"extra-header\">Colors</div>\
                       Color items:\
                       <ul style=\"list-style-type: none; padding-left: 0;\
                                   margin-bottom: 1em;\">\
                       </ul>";// + STRINGS.scheme;

      $id( "setting-extras-body" ).appendChild( sect );
      var list = sect.lastChild; //.previousSibling;

      var me = this;
      function tc( event )
      {
        me.toggleColors( event.target.id );
      }
      this.addColorPref( list, "gm-color-lv", STRINGS.list, tc );
      this.addColorPref( list, "gm-color-ev", STRINGS.expanded, tc );
      this.addColorPref( list, "gm-color-ri", STRINGS.read, tc );
      this.addColorPref( list, "gm-color-ui", STRINGS.unread, tc );

      // this.addSchemePref( sect );
      return sect;
    },

    addColorPref: function ( list, id, text, handler )
    {
      var pref = document.createElement( "li" );
      var selected = storage.getItem( id, id + " " );
      pref.innerHTML = "<label><input id=\"" + id + "\" type=\"checkbox\" " +
                       ( ( selected ) ? "checked=\"on\"" : "" ) +
                       "\"/>" + text + "</label>";
      list.appendChild( pref );

      var label = pref.firstChild.firstChild;
      label.addEventListener( "change", handler, false );
    },

    addSchemePref: function( sect )
    {
      var sel = document.createElement("select");
      var addButton = document.createElement( "input" );

      sel.addEventListener( "change", this.togglePref, false )
      sel.innerHTML = "<option style=\"font-style: normal;\">" +
                         STRINGS.def + "</option>\
                       <option style=\"font-style: italic;\">" +
                         STRINGS.custom + "</option>";

      addButton.type = "button";
      addButton.disabled = "true";
      addButton.value = "Save";
      addButton.style.marginLeft = "1em";

      sect.appendChild( sel );
      sect.appendChild( addButton );
      this.addColorPickers( sect );
    },

    addColorPickers: function( section )
    {
      var bgRange = this.makeColorRange();
      var fontRange = this.makeColorRange();
      

      section.appendChild( bgRange );
     // section.appendChild( fontRange );
    },

    makeColorRange: function()
    {
      var range = document.createElement( "div" );
      range.setAttribute( "style", "width: 360px; height: 10px; margin-top: 1em; border: 1px solid #FFCC66;" );
      for ( var i = 360; i; i-- )
      {
        var color = document.createElement( "div" );
        color.setAttribute( "style", "float: left; width: 1px; height: 100%; background-color: hsl(" + i + ",100%,50%" );
        range.appendChild( color );
      }
      var rangeContainer = document.createElement( "div" );
      rangeContainer.setAttribute( "style", "height: 19px; width: 362px;" );
      
      var rangeBegin = document.createElement( "div" );
      rangeBegin.setAttribute( "style", "position: absolute; width: 1px; height: 13px; border: 1px solid black; background: transparent;" );
      
      var rangeEnd = rangeBegin.cloneNode( true );
      rangeBegin.style.marginLeft = "-1px";
      rangeEnd.style.marginLeft = "359px";
      rangeBegin.style.borderLeftWidth = "2px";
      rangeEnd.style.borderRightWidth = "2px";

      rangeContainer.appendChild( rangeBegin );
      rangeContainer.appendChild( rangeEnd );
      rangeContainer.appendChild( range );

      return rangeContainer;
    },

    toggleColors: function( id )
    {
      var pref = storage.getItem( id, id );

      var msg, newPref = "", cName = "";
      if ( !pref )
      {
        newPref = id;
        cName = id + " ";
        msg = "<em>will</em>";
      }
      else
      {
        msg = "<em>will not</em>";
      }

      var re = new RegExp( id + " |^", "g" );
      this.entries.className = this.entries.className.replace( re, cName );
      storage.setItem( id, newPref );
      this.setMessage( id, msg );
    },

    togglePref: function( event )
    {
      if ( this.value == "Custom" )
      {
        this.style.fontStyle =  "italic";
        addButton.removeAttribute( "disabled" );
      }
      else
      {
        this.style.fontStyle = "";
        addButton.setAttribute( "disabled", "true" );
      }
    },

    setMessage: function( id, msg )
    {
      clearTimeout( this.timeoutID );
      var inner = $x( "id( 'message-area-inner' )" );
      var outer = $x( "id( 'message-area-outer' )" );

      // get the message string to insert into the page
      var type = ( id == "gm-color-lv" ) ? "List view items " :
                 ( id == "gm-color-ev" ) ? "Expanded view items " :
                 ( id == "gm-color-ui" ) ? "Unread items " :
                 ( id == "gm-color-ri" ) ? "Read items " : "unknown";

      var newMsg = type + msg + " be colored."; 
      inner.innerHTML = newMsg; // set the message
      
      // force display and set position and width
      outer.setAttribute( "style", "display: block !important;" +
                                   "margin-left:" +
                                   Math.round( inner.offsetWidth/-2 ) + "px;" +
                                   "width:" + (inner.offsetWidth + 10) + "px;" );
      outer.className = "message-area info-message";

      this.timeoutID = setTimeout( function()
      {
        outer.style.display = "";

        // test if the same message is still showing.
        // force lowercase to handle any (tag name) capitalization change
        if ( inner.innerHTML.toLowerCase() == newMsg.toLowerCase() )
        {
          outer.className = outer.className.replace( / hidden|$/, " hidden" );
        }
      }, 7*1000 );
    },

    getColorPrefs: function()
    {
      var prefs = "";

      prefs += storage.getItem( "gm-color-lv", "gm-color-lv" ) + " ";
      prefs += storage.getItem( "gm-color-ev", "gm-color-ev" ) + " ";
      prefs += storage.getItem( "gm-color-ui", "gm-color-ui" ) + " ";
      prefs += storage.getItem( "gm-color-ri", "gm-color-ri" ) + " ";

      return prefs;
    }
  };

  // provide local data storage
  var storage =
  {
    cookie: {},

    init: function() // initialize methods for data storage access
    {
      if ( typeof GM_getValue != "undefined" )
      {
        this.getItem = GM_getValue;
        this.setItem = GM_setValue;
        return;
      }

      if ( typeof localStorage != "undefined" )
      {
        this.getItem = function( key, def )
                       {
                         var value = localStorage.getItem( key );
                         return ( typeof value == "undefined" ) ? def : value;
                       };
        this.setItem = function( key, value )
                       {
                         localStorage.setItem( key, value );
                       };
        return;
      }

      var pairs = {};
      if ( /gm-color=([^;]*)/.test( unescape( document.cookie ) ) )
      {
        var cookie = RegExp.$1;

        cookie.split( "/" ).forEach( function( pair )
        {
          var set = pair.split( ":" );
          pairs[ set[ 0 ] ] = set[ 1 ];
        } );
      }

      this.cookie = pairs;
    },

    getItem: function( name, def )
    {
      var cookieVal = this.cookie[ name ];
      return ( typeof cookieVal == "undefined" ) ? def : cookieVal;
    },

    setItem: function( name, value )
    {
      this.cookie[ name ] = value;
      var strCookie = "gm-color=";

      for ( var prop in this.cookie )
        strCookie += prop + ":" + this.cookie[ prop ] + "/";

      var future = new Date( ( new Date().getTime() + 10*365*24*60*60*1000 ) );
      strCookie += ";path=/reader;expires=" + future.toGMTString();

      document.cookie = strCookie;
    },
  };

  // used to keep track of all the calculated colors
  var colors = {};


//=============================================================================


  // calculate item hue
  function getHue( title )
  {
    var hue = 0;

    for ( var i = 0, ch; ch = title[ i ]; i++ )
      hue += ch.charCodeAt( 0 );
    hue %= 360;

    colors[ title ] = hue;
    return hue;
  }

  function getColorCss( title )
  {
    var hue = getHue( title );

    return "\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ] .card,\
      /* .ccard, .t2, .t3 in Opera expanded view */\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ] .ccard,\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ] .t2,\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ] .t3,\
      .gm-color-lv.gm-color-ui div[ colored='" + title + "' ] .collapsed\
      {\
        background: hsl(" + hue + ", 70%, 80% ) !important;\
      }\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ]:hover .card,\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ]:hover .ccard,\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ]:hover .t2,\
      .gm-color-ev.gm-color-ui div[ colored='" + title + "' ]:hover .t3,\
      .gm-color-lv.gm-color-ui div[ colored='" + title + "' ]:hover .collapsed\
      {\
        background: hsl(" + hue + ", 90%, 85% ) !important;\
      }\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ] .card,\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ] .ccard,\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ] .t2,\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ] .t3,\
      .gm-color-lv.gm-color-ui .read[ colored='" + title + "' ] .collapsed\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ]:hover .card,\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ]:hover .ccard,\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ]:hover .t2,\
      .gm-color-ev.gm-color-ui .read[ colored='" + title + "' ]:hover .t3,\
      .gm-color-lv.gm-color-ui .read[ colored='" + title + "' ]:hover .collapsed\
      {\
        background: #F3F5FC !important; /* override to force no color */\
      }\
      .gm-color-ev.gm-color-ri div.read[ colored='" + title + "' ] .card,\
      .gm-color-ev.gm-color-ri div.read[ colored='" + title + "' ] .ccard,\
      .gm-color-ev.gm-color-ri div.read[ colored='" + title + "' ] .t2,\
      .gm-color-ev.gm-color-ri div.read[ colored='" + title + "' ] .t3,\
      .gm-color-lv.gm-color-ri div.read[ colored='" + title + "' ] .collapsed\
      {\
        /* color read items. overrides the unread item setting. */\
        background: hsl(" + hue + ", 50%, 90% ) !important;\
      }\
      .gm-color-ev.gm-color-ri div[ colored='" + title + "' ].read:hover .card,\
      .gm-color-ev.gm-color-ri div[ colored='" + title + "' ].read:hover .ccard,\
      .gm-color-ev.gm-color-ri div[ colored='" + title + "' ].read:hover .t2,\
      .gm-color-ev.gm-color-ri div[ colored='" + title + "' ].read:hover .t3,\
      .gm-color-lv.gm-color-ri div.read[ colored='" + title + "' ]:hover\
        .collapsed\
      {\
        /* color read items. overrides the unread item setting. */\
        background: hsl(" + hue + ", 70%, 95% ) !important;\
      }";
  }

  // inject color css into the page
  function setColor()
  {
    // pick up all uncolored entries, including ones missed previously
    var nocolor = $xa( "id( 'entries' )/div[ contains( @class, 'entry' ) ]" +
                       "[ not( @colored ) ]" );

    if ( !nocolor.length )
      return;

    nocolor.forEach( function( nc )
    {
      // title is an "<a>" for expanded view, "<span>" for list view
      var title = $x( ".//*[ contains( @class,'entry-source-title' ) ]" +
                      "[ not( * ) ]", nc ).innerHTML.replace( /\W/g, "-" );
      nc.setAttribute( "colored", title );
      if ( colors[ title ] == undefined )
        addStyle( getColorCss( title ) );
    } );
  }

  function watchLoading( chrome )
  {
    // pull this out here out of unsafeWindow context
    var prefs = settings.getColorPrefs();

    function setup( event )
    {
      var entries = $id( "entries" );
      if ( entries )
      {
        chrome.removeEventListener( "DOMNodeInserted", setup, false );

        // initial setup and toggling of settings
        entries.className = prefs + entries.className; 
        entries.addEventListener( "DOMNodeInserted", setColor, false );
      }
    }

    chrome.addEventListener( "DOMNodeInserted", setup, false );
  }

  (function()
  {
    var chrome = $id( "chrome" );
    storage.init();

    if ( chrome )
      watchLoading( chrome ); // watch for the loading of rss entries

    else // setting and script meta info page have no "chrome" element
    {
      if ( updater.init() ) // script meta info page
        return;

      settings.init();
    }

    addStyle( BASE_CSS );

  } )();

})();

// .toString() + ")();";

// document.body.appendChild(script);