Google Reader - Colorful List View

By kepp Last update Dec 22, 2011 — Installed 79,276 times.

There are 16 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://www.google.tld/reader/*
// @include        https://www.google.tld/reader/*
// @include        http://userscripts.org/scripts/source/8782.meta.js
// @description    Colors items in Google Reader.
// @author         kepp
// @updateURL      http://userscripts.org/scripts/source/8782.meta.js
// @version        20111209
// ==/UserScript==


/**
 * 2011-12-22
 * Quick fix to for Google+ share window been hidden. Thanks norti.
 **
 * 2011-12-09
 * Correct dates in change log.
 * Removed jsversion metadata as not useful/inaccurate.
 * Limit saturation and lightness ranges to better match previous color scheme.
 *  Quick fix. Future release will make the colors user adjustable.
 * Additional fixes for Google Reader style update.
 *  Seems additional changes were rolled out after my last update.
 * - Removed option to color entry frames. Needs rework with new styling.
 * - Use box-shadow to emphasize currently selected entry. Want to change
 *   background color to do this, but not feasible at this point.
 **
 * 2011-11-05
 * Remove JSLint config sections and other testing leftovers.
 * Remove $id shortcut function. Inline function.
 * Remove $x shortcut function. Replace with getElementById/ClassName.
 * Remove $xa shortcut function. Replace with getElementsByClassName NodeList.
 * Change test for undefined to use "void 0".
 * Change base css from global variable to theme controller object property.
 * Reorgaize storage code and change tests for GM and DOM Storage.
 * Update string property names to be more consistent, maybe.
 * Use strict equality operators for everything.
 * Use stricter version parsing regular expression in updater.
 * Don't run in sharing bookmarklet popup iframe.
 * Simplify hue calculation process.
 * Clean up rgb color parsing.
 * Simplify hsl color calculation.
 * Move script info from global variable to property of updater.
 * Use decode/encodeURIComponent instead of un/escape for cookies.
 * Declare variables at top of scope and use single var statement.
 * Better date formatting in change log.
 * Disabled updater code until better solution is found.
 * Remove Comment View/Google Buzz support.
 * Fix for Google Reader style update.
 * - New id for element we're sampling to try and match page theme.
 * - Adjusted script for greater entry heights.
 * - Removed image replacement for link to external article.
 * - Added semi-transparent card actions background.
 * - Removed unused CSS.
 **
 * 2011-02-16
 * Update for Greasemonkey 9.0 compatibility
 **
 * 2010-02-26
 * Fix for updates not showing.
 * Substituted custom image for those used to link to the original article.
 * Consolidated where styles are inserted.
 * Added some theme support (attempts to match header colors).
 * Misc changes to code to validate on JSLint.
 * Removed jsversion metadata from header as not useful.
 * Added option to color "Comment view".
 * Fix for incorrect pref display in Google Chrome.
 **
 * 2009-11-22
 * Fix for breakage on expanded view.
 * CSS modified to increase selector priority to ensure colors get applied.
 **
 * 2009-11-20
 * Fix pref settings when DOM Storage is used.
 * Fix for GM_getValue detection on Google Chrome dev channel.
 * Fix for not working after encountering a shared item. Script also works on
 *  the "Your stuff", Shared Items and Notes pages with this.
 * Switch code bracing style.
 **
 * 2009-11-17
 * Added prefs to select what is colored in expanded view, entry body or
 *  outline.
 * Fix for update version check being in the wrong direction.
 * Fix for coloring unread items only in list view.
 * Added Google Chrome support.
 **
 * 2009-08-21
 * 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.
 * *
 * 2008-12-14
 * 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.
 **
 * 2008-12-04
 * Added settings for coloring read/unread items.
 * Adjusted things in the settings.
 **
 * 2008-07-30
 * 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.
 **/

( function() {

  var STRINGS = {
    // pref labels
    colorLbl:   "Color these items:",
    lvLbl:      "List view headers.",
    evLbl:      "Expanded view entry bodies.",
    // efLbl:      "Expanded view entry frames.",
    // cvLbl:      "Comment view entries.",
    riLbl:      "Read items.",
    uiLbl:      "Unread items.",
    updateLbl:  "Userscript Update Available",
    installLbl: "Install",

    // pref messages
    setMsg:     "will",
    unsetMsg:   "will not",
    colorMsg:   " be colored.",
    lvMsg:      "List view items ",
    evMsg:      "Expanded view entry bodies ",
    // efMsg:      "Expanded view entry frames ",
    // cvMsg:      "Comment view items ",
    uiMsg:      "Unread items ",
    riMsg:      "Read items ",
    udMsg:      "Undefined",
  },

  storage = null,
  updater = null,
  settings = null,
  theme = null;


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


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

  function bind( func, thisArg ) {
    var args = Array.prototype.slice.call( arguments, 2 );

    return function() {
      var bargs = args.concat( Array.prototype.slice.call( arguments ) );
      func.apply( thisArg, bargs );
    };
  }


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


  // provide local data storage
  // use cookies by default and override with GM_* or localStorage if available
  storage = {
    cookie: {},

    init: function() { // initialize methods for data storage access
      var pairs, cookie;

      if ( this.testGMStorage() || this.testDOMStorage() ) {
        return;
      }

      pairs = {};
      cookie = decodeURIComponent( document.cookie );
      if ( /gm-color=([\w-:]+);/.test( cookie ) ) {
        cookie = RegExp.$1;

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

      this.cookie = pairs;
    },

    testGMStorage: function() {

      // test for existance of GM_setValue and GM_getValue
      // Google Chrome stubs these functions with 'not supported' messages
      if ( typeof GM_getValue === "function" &&
           typeof GM_setValue === "function" ) {
        GM_setValue( "test", "test value" );

        if ( GM_getValue( "test" ) === "test value" ) {
          if ( typeof GM_deleteValue === "function" ) {
            GM_deleteValue( "test" );
          }
          this.getItem = GM_getValue;
          this.setItem = GM_setValue;
          return true;
        }
      }

    },

    testDOMStorage: function() {

      // Google Chrome gives null for localStorage if not enabled,
      // Opera gives undefined
      // http://www.w3.org/TR/webstorage/#the-storage-interface
      if ( localStorage !== void 0 && localStorage !== null ) {

        this.getItem = function( key, def ) {
          var value = localStorage.getItem( key );
          return ( value === null ) ? def : value;
        };

        this.setItem = function( key, value ) {
          localStorage.setItem( key, value );
        };
        return;
      }
    },

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

    setItem: function( name, value ) {
      var strCookie = "gm-color=",
          future = new Date( ( new Date().getTime() + 10*365*24*60*60*1000 ) ),
          prop = "";

      this.cookie[ name ] = value;

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

      strCookie += ";path=/reader;expires=" + future.toGMTString();

      document.cookie = encodeURIComponent( strCookie );
    }
  };

  // script updater
  updater = {
    loader: null,

    // info used to check for script updates
    scriptInfo: {
      version:    "20110216",
      date:       "Wed, 16 Feb 2011 19:07:18 GMT",
      updateUrl:  "http://userscripts.org/scripts/source/8782.meta.js",
      installUrl: "http://userscripts.org/scripts/source/8782.user.js"
    },

    init: function() {
      var loader;

      // test if this is the script meta info page that loaded
      if ( location.href === this.scriptInfo.updateUrl ) {
        // running on userscripts.org domain

        document.body.setAttribute( "style",
                                    "visibility: hidden; overflow: hidden;" );

        // check if there is an update
        this.parseMetaInfo( this.scriptInfo.installUrl );
        return true; // notify that this was the script meta page
      }

      loader = document.createElement( "iframe" );
      loader.setAttribute( "style", "position: absolute;" +
                                    "height: 0; width: 0;" );
      loader.addEventListener( "load", this.showUpdate, false );

      this.loader = document.body.appendChild( loader );
      return false;
    },

    parseMetaInfo: function( url ) {
      var metaInfo = document.body.innerHTML,
          hasUpdate = false;

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

      // compare script dates
      } else if ( /@uso:timestamp\s*(\S[\da-zA-Z,:;+ ]+)/.test( metaInfo ) ) {
        hasUpdate = new Date( this.scriptInfo.date ) < new Date( RegExp.$1 );
      }

      if ( hasUpdate ) {
        location.href = "data:text/html," +
          "<style>body{ visibility: visible; overflow: hidden;" +
          "font-family: Arial, sans-serif; color: #2244BB; }</style>" +
          STRINGS.updateLbl + ": <a href=\"" + url + "\" target=\"_blank\">" +
          STRINGS.installLbl + "</a>";
      }
    },

    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.src = this.scriptInfo.updateUrl;

      // just check it every time the settings page is opened
      // storage.setItem( "last-check", new Date().getTime() + "" );
      return this.loader;
    },

    showUpdate: function( event ) {
      event.target.setAttribute( "style", "visibility: visible;" +
                                          "overflow: hidden;" +
                                          "position: absolute; right: 2em;" +
                                          "height: 2em; width: 20em;" );
    }
  };

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

    init: function() { // insert page color options into the settings page
      var section = this.addPrefs(),
          check = false;

      // ascend out of iframe
      this.entries = frameElement.ownerDocument.getElementById( "entries" );

      // check for userscript updates
      // comment this section out if you want to disable update checks
      // check = updater.check();
      // if ( check ) {
      //  section.insertBefore( check, section.firstChild );
      // }
    },

    addPrefs: function() {
      var sect = document.createElement( "div" ),
          lists, list1, list2,
          tc = bind( this.toggleColors, this );

      sect.className = "extra";

      // two column list
      sect.innerHTML = "<div class=\"extra-header\">Colors</div>" +
                       STRINGS.colorLbl + "<div style=\"width: 30em;" +
                                             "margin: 0pt 0pt 1em 1em;\">" +
                       "<ul style=\"list-style-type: none; padding-left: 0;" +
                            "float: right;\">" +
                       "</ul>" +
                       "<ul style=\"list-style-type: none;" +
                            "padding-left: 0;\">" +
                       "</ul></div>";
      lists = sect.getElementsByTagName( "ul" );
      list1 = lists[ 1 ];
      list2 = lists[ 0 ];

      document.getElementById( "setting-extras-body" ).appendChild( sect );

      this.addColorPref( list2, "gm-color-ri", STRINGS.riLbl, tc );
      this.addColorPref( list2, "gm-color-ui", STRINGS.uiLbl, tc );
      this.addColorPref( list1, "gm-color-lv", STRINGS.lvLbl, tc );
      this.addColorPref( list1, "gm-color-ev", STRINGS.evLbl, tc );
      // this.addColorPref( list1, "gm-color-ef", STRINGS.efLbl, tc, 0 );
      // this.addColorPref( list1, "gm-color-cv", STRINGS.cvLbl, tc, 0 );

      return sect;
    },

    addColorPref: function ( list, id, text, handler, def ) {
      var pref = document.createElement( "li" ),
          chk,
          selected = storage.getItem( id, def === void 0 ? 1 : def );

      pref.innerHTML = "<label><input id=\"" + id + "\" type=\"checkbox\"/>" +
                       text + "</label>";
      chk = pref.firstChild.firstChild;

      // just setting the "checked" attribute doesn't seem to work in Chrome
      // I should figure out why

      chk.checked = ( selected ) ? true : false;
      list.appendChild( pref );
      chk.addEventListener( "change", handler, false );
    },

    toggleColors: function( event ) {
      var id = event.target.id, curr = event.target.checked,
          msg = "", newPref = "", cName = "",
          re = new RegExp( id + " |^", "g" );

      if ( curr ) {
        newPref = id;
        cName = id + " ";
        msg = "<em>" + STRINGS.setMsg + "</em>";
      }
      else {
        msg = "<em>" + STRINGS.unsetMsg + "</em>";
      }

      this.entries.className = this.entries.className.replace( re, cName );
      storage.setItem( id, newPref );
      this.setMessage( id, msg );
    },

    setMessage: function( id, msg ) {
      clearTimeout( this.timeoutID );
      var inner = document.getElementById( "message-area-inner" ),
          outer = document.getElementById( "message-area-outer" ),
          type = "", newMsg = "";

      // get the message string to insert into the page
      type = ( id === "gm-color-lv" ) ? STRINGS.lvMsg :
             ( id === "gm-color-ev" ) ? STRINGS.evMsg :
             // ( id === "gm-color-ef" ) ? STRINGS.efMsg :
             ( id === "gm-color-ui" ) ? STRINGS.uiMsg :
             ( id === "gm-color-ri" ) ? STRINGS.riMsg :
             // ( id === "gm-color-cv" ) ? STRINGS.cvMsg :
             STRINGS.udMsg;

      newMsg = type + msg + STRINGS.colorMsg;
      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-ef", "" ) + " ";
      // prefs += storage.getItem( "gm-color-cv", "" ) + " ";

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

      return prefs;
    }
  };

  // controls and applies colors
  theme = {
    colors: {}, // used to store the calculated colors
    prefs: "",  // pref settings
    bgColor: null, // theme settings
    textColor: null,
    styles: null, // dom node colorizing css will be injected into
    entries: null, // NodeList of entries in reading list

    // CSS to allow items to be colored
    baseCss: "/* css to allow colors to apply */" +
      "#entries .entry-likers, /* let colors show through */" +
      "#entries.list .collapsed .entry-source-title," +
      "#entries.list .collapsed .entry-secondary," +
      "#entries.list .collapsed .entry-title {" +
      // "#entries.comment-cards .entry-comments {" +
      "  background-color: transparent !important;" +
      "}" +
      // ".gm-color-lv .collapsed /* list view headers */ {" +
      // "#entries.gm-color-ev.gm-color-ef .card {" +
      // "#entries.comment-cards .entry .comment-entry /* comment view */ {" +
      // "  border-color: transparent !important;" +
      // "}" +
      "#entries.cards.gm-color-ev .card-actions {" +
      "  background-color: rgba( 0, 0, 0, 0.05 ) !important;" +
      "}" +
      "#entries.cards.gm-color-ev .card-bottom {" +
      "  border-color: rgba( 0, 0, 0, 0.1 ) !important;" +
      "}" +
      // "#entries.list.gm-color-lv #current-entry .collapsed {" +
      // "  border-width: 2px 0 !important;" +
      // "  border-color: #777777 !important;" + // this needs more contrast
      // "}" +
      // "#entries.list.gm-color-lv #current-entry.expanded .collapsed {" +
      // "  border-bottom-color: transparent !important;" +
      // "  border-width: 2px 0 !important;" +
      // "}" +
      "#entries.cards.gm-color-ev .card {" +
      // "#entries.cards.gm-color-ef .card {" +
      "  padding-right: 1em;" +
      "}" +
      "#entries.cards.gm-color-ev .card-bottom {" +
      "  margin-bottom: 1ex !important;" +
      "}" +
      "#current-entry {" +
      "  box-shadow: 0 0 1px 2px rgb(22, 88, 120);" +
      "  z-index: 10;" +
      "}",
      // "#entries .entry {" +
      // "  padding: 5px 0;" +
      // "}",
      // "#entries.list .collapsed {" +
      // "  line-height: 2.4ex !important; /* hide entry snippet 2nd line */" +
      // "}" +
      // "#entries .collapsed .entry-original," +
      // ".entry .entry-title .entry-title-go-to { /* article link image */" +
      // "  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4A" +
      // "AAAOBAMAAADtZjDiAAAALVBMVEX////R3fPm6/nJ1PPV3vXc5fbCz/He5fjCz/H////U3" +
      // "/Tj6fjr7/r3+f3O2fTPDCQ+AAAAB3RSTlMA71DmxK0A9H5uGAAAAFtJREFUeF41jbEJgE" +
      // "AUQx+CG1hYCm4gOICdpeACh5sIwk+jAxw3yI3iOY0I/jSBkLxANanpgFXSCLU+LfSWLWt" +
      // "gtmRJB+1VnnLvSGc8o9w9957vnPNzA2zfT3gBaL8sJRF+PgoAAAAASUVORK5CYII=')" +
      // "  no-repeat !important;" +
      // "}" +
      // ".entry .entry-title .entry-title-go-to {" +
      // "  background-position: left 3px !important;" + */
      // "}",

    init: function( chrome ) {
      var setup = this.setup, thm = this,
          set = function() {
            setup.call( thm );
            chrome.removeEventListener( "DOMNodeInserted", set, false );
          };

      addStyle( this.baseCss );
      this.styles = addStyle("/* css to color entries */");
      this.prefs = settings.getColorPrefs();

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

    setup: function() { // initial setup and toggling of settings
      var prefs = this.prefs,
          entries = document.getElementById( "entries" );
      this.initConfig(); // put this in here so theme scripts run first

      if ( entries ) {
        entries.className = prefs + entries.className;
        entries.addEventListener( "DOMNodeInserted",
                                  bind( this.process, this ), false );
        this.entries = entries.getElementsByClassName( "entry" );
      }
    },

    // determine what color theme to use by looking at the header colors
    initConfig: function() {
      var style = getComputedStyle( 
                  document.getElementById( "viewer-container" ), null ),
          bg = this.rgbToHsl( style.getPropertyValue( "background-color" ) ),
          color = this.rgbToHsl( style.getPropertyValue( "color" ) );

      // a min saturation & lightness is needed to distinguish colors.
      // for read items, a value is further subtracted from these
      this.bgColor = { hue: bg[ 0 ],
                       sat: Math.max( Math.min( bg[ 1 ], 73 ), 50 ),
                       lt: Math.max( Math.min( bg[ 2 ], 85 ), 10 ) };

      this.textColor = { hue: color[ 0 ], sat: color[ 1 ], lt: color[ 2 ] };
      this.setTextColor();
    },

    rgbToHsl: function( color ) { // calculate hsl from rgb css color string
      var hue = 0, sat = 0, lt,

          rgb = this.getRgb( color ),
          max = Math.max.apply( Math, rgb ),
          min = Math.min.apply( Math, rgb ),
          chroma = max - min,

          index = rgb.indexOf( max );

      rgb.splice( index, 1 );

      lt = ( max + min )/2;
      if ( chroma ) { // max not equal to min
        sat = ( lt > 0.5 ) ? ( chroma )/( 2 - ( max + min ) ) :
                             ( chroma )/( max + min );
        hue = 60*( ( rgb[ 0 ] - rgb[ 1 ] )/( chroma ) + index*2 );
      }

      return [ hue, sat*100, lt*100 ];
    },

    getRgb: function( color ) {
      var red, green, blue,
          hexRegExp;

      if ( /(\d+), (\d+), (\d+)/.test( color ) ) {
        return [ RegExp.$1/255, RegExp.$2/255, RegExp.$3/255 ];
      }

      // Opera returns a hex value, so convert hex to decimal
      hexRegExp = /#([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})/; // <-------------------------------------------------------------------- CHECKME

      if ( hexRegExp.test( color ) ) {
        red = parseInt( RegExp.$1, 16 );
        green = parseInt( RegExp.$2, 16 );
        blue = parseInt( RegExp.$3, 16 );

        return [ red/255, green/255, blue/255 ];
      }

      return [ 1, 0, 0 ];
    },

    setTextColor: function() {
      var hue = this.textColor.hue,
          sat = this.textColor.sat,
          lt = this.textColor.lt,

      // default color lightnesses:
      // bg lt: 85.3
      // color lt: 0

          // text lightnesses are set to values in the range between title text
          // and background color lightnesses
          range = this.bgColor.lt - lt,

          css = "  color: hsl(" + hue + "," + sat + "%, ";

      this.styles.textContent += ( "" +
        "#entries .collapsed .entry-title {" +
        css + lt + "% ) !important;" + // 000000 <- default color
        "}" +
        "#entries.list .collapsed .entry-main .entry-source-title {" +
        css + ( lt + range*0.42 ) + "% ) !important;" + // 555555
        "}" +
        ".entry .entry-author, .entry .entry-date {" +
        // ".entry-comments .comment-time {" +
        css + ( lt + range*0.50 ) + "% ) !important;" + // 666666
        "}" +
        "#entries.list .collapsed .entry-secondary {" +
        css + ( lt + range*0.59 ) + "% ) !important;" + // 777777
        "}" +
        // "a, a:visited, .link {" + // shouldn't need to mess with link color
        // css + lt + "% ) !important;" + // 2244BB
        // "}" +
        "#entries .item-body {" +
        css + lt + "% ) !important;" + // 000000
        "}" );
    },

    // inject color css into the page
    process: function() {
      var bgColor = this.bgColor,
          styles = this.styles,
          thm = this,

         // pick up all uncolored entries, including ones missed previously
         noColor = Array.prototype.slice.apply( this.entries );
         noColor = noColor.filter(function( entry ) {
                     return !entry.hasAttribute( "colored" );
                   } );

      if ( !noColor.length ) {
        return;
      }

      noColor.forEach( function( nc ) {
        thm.setColors( styles, bgColor, nc );
      } );
    },

    setColors: function( styles, bgColor, nc ) {

      // search for a node that has 'entry-source-title' class name
      var title = nc.getElementsByClassName( "entry-source-title" )[ 0 ].
                  textContent.replace( /\W/g, "-" );

      nc.setAttribute( "colored", title );
      if ( this.colors[ title ] === void 0 ) {
        styles.textContent += this.getColorCss( title, bgColor );
      }
    },

    getColorCss: function( title, bgColor ) { // generate css to color items
      var hue = "background-color: hsl(" + this.getHue( title ),
          imp = "% ) !important;",
          sat = bgColor.sat,
          lt = bgColor.lt,

          // set direction entry lightness is modified on read/hover
          dir = ( lt > 50 ) ? 1 : -1,

          // need to clean this up
          hsl = {
            norm: hue + "," + ( sat + 7 ) + "%," + ( lt - dir*5 )+ imp,
            hover: hue + "," + ( sat + 27 ) + "%, " + lt + imp,
            read: hue + "," + ( sat - 13 ) + "%," + ( lt + dir*5 ) + imp,
            readhvr: hue + "," + ( sat + 7 ) + "%," + ( lt + dir*10 ) + imp
          };

      return this.getLvCss( title, hsl ) + this.getEvCss( title, hsl ); // +
             // this.getEfCss( title, hsl ) + this.getCvCss( title, hsl );
    },

    getLvCss: function( ttl, hsl ) { // css for coloring items in list view
      // this selector should be take priority over any other selector
      var us = "#entries.gm-color-lv.gm-color-ui div[ colored='" + ttl + "' ]",
          rs = "#entries.gm-color-lv.gm-color-ri div[ colored='" + ttl + "' ]";

      return "" +
        us + ":not( .read ) .collapsed {" + hsl.norm + "}" +
        us + ":not( .read ):hover .collapsed {" + hsl.hover + "}" +

        rs + ".read .collapsed { /* override unread item colors */" +
             hsl.read + "}" +
        rs + ".read:hover .collapsed { /* override unread item colors */ " +
             hsl.readhvr + "}" +

        "#entries.gm-color-lv.gm-color-ui:not( .gm-color-ri ) .read .collapsed," +
        "#entries.gm-color-lv.gm-color-ri:not( .gm-color-ui ) :not( .read ) .collapsed {" +
        "  /* override current entry colors */" +
        "  background-color: white !important;" +
        "}";
    },

    // css for coloring expanded view item bodies
    getEvCss: function( ttl, hsl ) {
      var us = "#entries.gm-color-ev.gm-color-ui div[ colored='" + ttl + "' ]",
          rs = "#entries.gm-color-ev.gm-color-ri div[ colored='" + ttl + "' ]";

      return "" +
        us + " .card," +
        /* .ccard, .t2, .t3 used for Opera expanded view */
        us + " .ccard," +
        us + " .t2," +
        us + " .t3 {" + hsl.norm + "}" +

        us + ":hover .card," +
        us + ":hover .ccard," +
        us + ":hover .t2," +
        us + ":hover .t3 {" + hsl.hover + "}" +

        us + ".read .card," +
        us + ".read .ccard," +
        us + ".read .t2," +
        us + ".read .t3," +
        us + ".read:hover .card," +
        us + ".read:hover .ccard," +
        us + ".read:hover .t2," +
        us + ".read:hover .t3 { /* force no color for read items */ " +
             "background-color: white !important; }" +

        rs + ".read .card," +
        rs + ".read .ccard," +
        rs + ".read .t2," +
        rs + ".read .t3 { /* override unread item colors */ " +
             hsl.read + "}" +

        rs + ".read:hover .card," +
        rs + ".read:hover .ccard," +
        rs + ".read:hover .t2," +
        rs + ".read:hover .t3 { /* override unread item colors */ " +
             hsl.readhvr + "}";
    },

    // css for coloring expanded view item frames
    // getEfCss: function( ttl, hsl ) {
      // var us = "#entries.gm-color-ef.gm-color-ui div[ colored='" + ttl + "' ]",
          // rs = "#entries.gm-color-ef.gm-color-ri div[ colored='" + ttl + "' ]";

      // return "" +
        // us + " {" + hsl.norm + "}" +
        // us + ":hover {" + hsl.hover + "}" +
        // us + ".read," +
        // us + ".read:hover { /* force no color for read items */ " +
             // "background-color: #F3F5FC !important; }" +
        // rs + ".read { /* override unread item colors */ " +
             // hsl.read + "}" +
        // rs + ".read:hover { /* override unread item colors */ " +
             // hsl.readhvr + "}";
    // },

    // getCvCss: function( ttl, hsl ) {
      // var us = "#entries.gm-color-cv.gm-color-ui div[ colored='" + ttl + "' ]",
          // rs = "#entries.gm-color-cv.gm-color-ri div[ colored='" + ttl + "' ]";

      // comment view doesn't have read/unread
      // return  "" +
        // us + " .comment-entry {" + hsl.norm + "}" +
        // us + ":hover .comment-entry {" + hsl.hover + "}" +
        // us + ".read .comment-entry," +
        // us + ".read:hover .comment-entry { /* force no color for read items */ " +
             // "background-color: white !important; }" +
        // rs + ".read .comment-entry { /* override unread item colors */ " +
             // hsl.read + "}" +
        // rs + ".read:hover .comment-entry { /* override unread item colors */ " +
             // hsl.readhvr + "}";
    // },

    getHue: function( title ) { // calculate item hue
      var hue = 0,
          i = 0, cc = 0;

      for ( i = 0; cc = title.charCodeAt( i ); i++ ) {
        hue += cc;
      }
      hue %= 360;

      this.colors[ title ] = hue;
      return hue;
    }
  };


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


  ( function() {
    var chrome = document.getElementById( "chrome" );

    // test if this is a google reader sharing bookmarklet popup
    if ( location.href.search( "link-frame" ) >= 0 ) {
      return;
    }

    storage.init();

    if ( chrome ) {
      theme.init( chrome ); // watch for the loading of feed entries
    }

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

      settings.init();
    }

  }() );

}() );