Nachdem ich für mein neues Macbook Pro auf der Suche nach einem schönen Desktop Client für Google Reader war – mir aber Adobe AIR Applikationen nicht so gut gefallen – war ich zunächst sehr erfreut darüber, dass NewsGator in der neuen NetNewsWire Version einen Google Reader Connector eingebaut hat. Was mich aber am neuen NetNewsWire gestört hat, war die blinkende Werbung in der linken Fensterecke. Darum habe ich mich daran gemacht, mir mit Fluid, HelvetiReader und ein paar Userscripts einen schicken Desktop Client zu bauen. Fluid ist eine Software mit der sich Anwendungen aus Webseiten erstellen lassen, die man mit Scripten erweitern kann.
Dieser von mir gebaute Desktop Client unterstützt Growl Notifications das Dock Icon hat ein Badge, dass die ungelesenen Reader-Items zeigt, Favicons der Feeds werden angezeigt und er sieht so aus:
Nur wie habe ich die Applikation gebaut? Als Erstes habe ich Fluid herunter geladen und installiert. Hinterher habe ich damit ein neues Programm erstellt:
Das Dock Icon für die Anwendung gibt es von Joshua Brewer bei Flickr.
Als nächstes kann das soeben erstellte Programm gestartet werden und es geht ans erstellen der Userscripts. Dazu muss auf die schwarze Papierrolle in der Menuleiste geklickt werden und ein neues Userscript erstellt werden.
Das erste Stück Script ist für das Aussehen unseres Desktop Clients nötig. Da mir der HelvetiReader gefällt habe ich auch dessen Skript eingefügt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // ==UserScript== // @name Helvetireader // @description Helvetireader style for Google Reader // @include https://*.google.com/reader/view/* // @include http://*.google.com/reader/view/* // @include htt*://*.google.*/reader/view* // @author Helvetireader by Jon Hicks (http://www.hicksdesign.co.uk) with favicon override by MkFly // ==/UserScript== var favvy = document.createElement('link'); favvy.setAttribute('type', 'image/x-icon'); favvy.setAttribute('rel', 'shortcut icon'); favvy.setAttribute('href', 'http://www.helvetireader.com/favicon.png'); var head = document.getElementsByTagName('head')[0]; head.appendChild(favvy); var cssNode = document.createElement('link'); cssNode.type = 'text/css'; cssNode.rel = 'stylesheet'; cssNode.href = 'http://www.helvetireader.com/css/helvetireader.css'; cssNode.media = 'screen'; cssNode.title = 'dynamicLoadedSheet'; document.getElementsByTagName("head")[0].appendChild(cssNode); |
Als nächstes braucht ihr den Code für das Laden der Favicons den ich beim Userscripts User sethaurus gefunden habe:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | function fetch(url, callback) { var xhr = new XMLHttpRequest; xhr.open('get', url); xhr.onload = function () { callback(xhr.responseText); }; xhr.send(null); }; function each(list, callback) { Array.prototype.forEach.call(list, callback); }; function filter(list, callback) { return Array.prototype.filter.call(list, callback); }; // -- var EXPORT_URL = '/reader/subscriptions/export', ICON_CLASS = 'sub-icon', UNFIXED_ICONS = '.' + ICON_CLASS + ':not([iconbase])', ICON_CLASS = new RegExp('\b' + ICON_CLASS + '\b'), POLL_INTERVAL = 1000, FAVICON_TEMPLATE = ['background-position:0px; background-image:url(/s2/favicons?domain=', ')'], SOURCE_URL_PREFIX = ['xmlUrl="', '" htmlUrl="']; function drawFavicon(node) { node.style.cssText = FAVICON_TEMPLATE.join(node.getAttribute('iconbase').split('/')[2]); }; function getSourceUrlFromOpml(feedUrl, opml) { return (opml.split(SOURCE_URL_PREFIX.join(feedUrl))[1] || '').split('"')[0]; }; function getIconNodes() { if (document.querySelectorAll) return document.querySelectorAll(UNFIXED_ICONS); return filter(document.getElementsByTagName('span'), function (span) { return ICON_CLASS.test(span.className) && ! span.hasAttribute('iconbase'); }); }; (function () { setTimeout(arguments.callee, POLL_INTERVAL); var iconNodes = document.querySelectorAll(UNFIXED_ICONS); if (! iconNodes.length) return; each(iconNodes, function(icon){ icon.setAttribute('iconbase', unescape(icon.parentNode.href.split('/')[6])); drawFavicon(icon); }); fetch(EXPORT_URL, function(opml) { each(iconNodes, function(icon){ var iconbase = icon.getAttribute('iconbase'); icon.setAttribute('iconbase', getSourceUrlFromOpml(iconbase, opml)); drawFavicon(icon); }); }); })(); |
Für die Growl Notifications ist der Code von tanguy zuständig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | (function() { if (!window.fluid) { return; } var focused = true; window.onfocus = function(){focused=true;}; window.onblur = function(){focused=false;}; var fluid_unread = 0; var old_fluid_unread = 0 window.fluid.addDockMenuItem("Refresh", function() { refresh(); }); function updateDockBadge() { var title = document.title; old_fluid_unread = fluid_unread || 0; if (title && title.length) { var start = title.indexOf("("); var end = title.indexOf(")"); if (start > -1 && end > -1) { start++; fluid_unread = title.substring(start, end); } else { fluid_unread = 0; } } //set the dock badge /* if ((fluid_unread || 0) > 0) { window.fluid.setDockBadge(fluid_unread); } else { window.fluid.setDockBadge(""); } */ //growl if there are more unread items than last time if ((fluid_unread || 0) > old_fluid_unread) { window.fluid.setDockBadge(fluid_unread); if(!focused) { if(refresh()) setTimeout(growleachnewnews, 5000); } fluid.showGrowlNotification({ title: "Google Reader", description: (fluid_unread || "") + " unread item(s)", priority: 3, onclick: activate_window, sticky: false }); } else if(fluid_unread == 0) { window.fluid.setDockBadge(""); } } setInterval(function(){updateDockBadge();}, 6000); function refresh() { //alert(window.fluid.dockBadge); var refreshelm = document.getElementById('viewer-refresh'); if(refreshelm) { var e = document.createEvent('MouseEvents'); //e.initEvent('click', true, false); e.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); //document.getElementById('viewer-refresh').dispatchEvent(e); refreshelm.dispatchEvent(e); return true; } else return false; } function growleachnewnews() { //return; var news = getElementsByClassName('entry read'); for(i=0; i < ((fluid_unread/1)-(old_fluid_unread/1)); i++) { var feed = news[i].getElementsByClassName('entry-source-title link')[0].innerText; var preview = news[i].getElementsByClassName('entry-title')[0].innerHTML; //alert(i); fluid.showGrowlNotification({ title: feed, description: preview, priority: 1, onclick: activate_window, identifier: "greader_" + i, sticky: false }); } } function activate_window() { //alert('hi'); window.fluid.activate(); window.fluid.unhide() this.window.focus(); window.focus(); } function setfocused() { focused = true; } function notfocused() { focused = false; } })(); /* === GETELEMENTSBYCLASSNAME === Developed by Robert Nyman, http://www.robertnyman.com Code/licensing: http://code.google.com/p/getelementsbyclassname/ ============================== */ var getElementsByClassName = function(className, tag, elm) { if (document.getElementsByClassName) { getElementsByClassName = function(className, tag, elm) { elm = elm || document; var elements = elm.getElementsByClassName(className), nodeName = (tag) ? new RegExp("\\b" + tag + "\\b", "i") : null, returnElements = [], current; for (var i = 0, il = elements.length; i < il; i += 1) { current = elements[i]; if (!nodeName || nodeName.test(current.nodeName)) { returnElements.push(current); } } return returnElements; }; } else if (document.evaluate) { getElementsByClassName = function(className, tag, elm) { tag = tag || "*"; elm = elm || document; var classes = className.split(" "), classesToCheck = "", xhtmlNamespace = "http://www.w3.org/1999/xhtml", namespaceResolver = (document.documentElement.namespaceURI === xhtmlNamespace) ? xhtmlNamespace: null, returnElements = [], elements, node; for (var j = 0, jl = classes.length; j < jl; j += 1) { classesToCheck += "[contains(concat(' ', @class, ' '), ' " + classes[j] + " ')]"; } try { elements = document.evaluate(".//" + tag + classesToCheck, elm, namespaceResolver, 0, null); } catch(e) { elements = document.evaluate(".//" + tag + classesToCheck, elm, null, 0, null); } while ((node = elements.iterateNext())) { returnElements.push(node); } return returnElements; }; } else { getElementsByClassName = function(className, tag, elm) { tag = tag || "*"; elm = elm || document; var classes = className.split(" "), classesToCheck = [], elements = (tag === "*" && elm.all) ? elm.all: elm.getElementsByTagName(tag), current, returnElements = [], match; for (var k = 0, kl = classes.length; k < kl; k += 1) { classesToCheck.push(new RegExp("(^|\\s)" + classes[k] + "(\\s|$)")); } for (var l = 0, ll = elements.length; l < ll; l += 1) { current = elements[l]; match = false; for (var m = 0, ml = classesToCheck.length; m < ml; m += 1) { match = classesToCheck[m].test(current.className); if (!match) { break; } } if (match) { returnElements.push(current); } } return returnElements; }; } return getElementsByClassName(className, tag, elm); }; /* === //GETELEMENTSBYCLASSNAME === */ |
Wenn man diese Codestücke ins Userscript eingefügt hat, kann man es speichern. Jetzt muss es nur noch aktiviert werden, dies geht indem man nochmals auf die schwarze Papier Rolle im Menü klickt und ein Häkchen vor dem Script setzt. Ein Neustart genügt und man ist fertig.
Für Kommentare und Anregungen wäre ich dankbar. 🙂