Jon Aquino's Mental Garden

Engineering beautiful software jon aquino labs | personal blog

Saturday, March 26, 2005

A Better Bloglines Notifier

>> Download yabn-1.0-beta-1-windows.zip (1.5 MB) <<

Requirements: Java 1.4 or newer, Windows XP (Let me know if it works in other versions of Windows.) (Want to help me test the Mac/Unix/Linux versions? Send me an email!)

Installation: Unzip it to a folder on your computer. Edit yabn.ini to specify your email and password. Add a shortcut to yabn.bat to your Startup folder.

Caveat: As this is a Java application, don't be surprised if this puppy consumes 30 MB of memory. But it's worth it if you're a Bloglines junkie like me!


Below is the standard Bloglines Notifier. It's OK, but it doesn't provide very much information:



Basically when you have unread items, a red square appears in the upper-right corner.

Being a data-visualization junkie as well as a computer programmer, I decided to write my own alternative Bloglines notifier:



It improves on the original by displaying the number of unread items directly on the icon itself. I don't know of any other system-tray icon that displays numbers, although many have some sort of visual cue to indicate two or three different states. The 16x16 area of a system-tray icon presents interesting possibilities for a richer display of status information (cf. Edward Tufte's beautiful work on sparklines).

Another feature of this notifier is that when new posts arrive, it pops up a balloon containing an excerpt of each:



This is great if you are monitoring your conversations via PubSub or Technorati -- when someone talks about you or a topic you are interested in, the balloon will pop up showing their post. (Note that the balloon will not appear in real time per se, as Bloglines grabs new posts on an hourly basis). This feature was inspired by the GMail Notifier (screenshot), which also pops up an excerpt when new mail comes in. (The similarity points to the possible future convergence of RSS and email methinks).

The menu item provides a couple of additional features:



If the balloons went by too quickly and you want to see them again, click Show Unread Items Again. If nothing looks interesting, you can click Mark All Items As Read to save yourself a trip to Bloglines. This workflow presents the interesting possibility of never having to open the browser to check your feeds -- when new posts come in, they appear as balloons, and you can browse your feeds from the system tray. Of course, to complete the loop it would be nice to click on a balloon to open the link in your web browser, but I haven't implemented this yet.

If you want to enhance or customize the notifier, simply open yabn.groovy in your favourite text editor. It's written in a wonderfully expressive Java scripting language called Groovy, and as you can see below, it's not that complicated if you've done some programming before. The neato JDesktop Integration Components library (JDIC) was used to create the system-tray icon and to launch the browser.


# Thanks to the Groovy developers for creating the wonderfully
# expressive language in which this script is written [Jon Aquino 2005-03-25]

import java.awt.*
import java.awt.event.*
import java.awt.image.*
import java.io.*
import java.net.*
import javax.swing.*
import org.apache.commons.httpclient.*
import org.apache.commons.httpclient.methods.*
import groovy.swing.*
import org.jdesktop.jdic.desktop.*
import org.jdesktop.jdic.tray.*
import org.jdom.input.*

UIManager.setLookAndFeel(UIManager.systemLookAndFeelClassName)
properties = new Properties()
properties.load(new FileInputStream("yabn.ini"))

# Thanks to the Sun JDIC team for the ability to create a tray icon
# and launch a browser in 1 line. [Jon Aquino 2005-03-25]

image = Toolkit.defaultToolkit.getImage("Bloglines.png")
TrayIcon trayIcon = new TrayIcon(new ImageIcon(image), "Yet Another Bloglines Notifier", createMenu())
trayIcon.addActionListener(new ActionListenerClosure({
goToBloglines()
updateTrayIcon(trayIcon, image, 0)
}))
SystemTray.defaultSystemTray.addTrayIcon(trayIcon)

seenItems = new HashSet()
firstDisplay = true
putAt("showUnreadItemsAgainRequested", false)
while (true) {
items = items(false)
println("Found ${items.size()} unread items")
updateTrayIcon(trayIcon, image, items.size())
if (!getAt("showUnreadItemsAgainRequested")) {
items.removeAll(seenItems)
}
displayItems(trayIcon, firstDisplay && items.size() > 5 ? items[-5..-1] : items)
seenItems.addAll(items)
System.gc()
firstDisplay = false
println("Waiting for " + properties.getProperty("seconds-between-checking-for-new-items") + " seconds")
putAt("showUnreadItemsAgainRequested", false)
i = 0
while (!getAt("showUnreadItemsAgainRequested") && i < properties.getProperty("seconds-between-checking-for-new-items").toInteger()) {
Thread.sleep(1000)
i++
}
}

# Thanks to phk and blackdrag on the Groovy IRC channel for
# ActionListenerClosure [Jon Aquino 2005-03-25]

class ActionListenerClosure implements ActionListener {
closure
ActionListenerClosure(closure) { this.closure = closure }
void actionPerformed(ActionEvent e) { closure.doCall(e) }
}

def callBloglines(url) {
client = new HttpClient()
credentials = new UsernamePasswordCredentials(properties.getProperty("email"), properties.getProperty("password"))
client.state.setCredentials("Bloglines RPC", "rpc.bloglines.com", credentials)
get = new GetMethod(url)
get.doAuthentication = true
client.executeMethod(get)
get
}

def updateTrayIcon(trayIcon, image, numberOfUnreadItems) {
trayIcon.toolTip = "${numberOfUnreadItems} Unread Item${numberOfUnreadItems==1?'':'s'}"
bufferedImage = new BufferedImage(16, 16, 2)
graphics = bufferedImage.createGraphics()
graphics.drawImage(image, 0, 0, null)
if (numberOfUnreadItems > 0) {
graphics.color = Color.yellow
graphics.fillRect(0, 8, 16, 16)
graphics.color = Color.red
graphics.font = graphics.font.deriveFont(new Float(9))
graphics.drawString(numberOfUnreadItems.toString(), 0, 16)
}
trayIcon.setIcon(new ImageIcon(bufferedImage))
}

def displayItems(trayIcon, items) {
i = 0
items.each { item |
i += 1
trayIcon.displayMessage("${i}/${items.size()}: ${item.title}", "(${item.feed}) ${item.text}", TrayIcon.NONE_MESSAGE_TYPE)
Thread.sleep(properties.getProperty("seconds-between-displaying-each-item").toInteger() * 1000)
}
trayIcon.displayMessage("", "", TrayIcon.NONE_MESSAGE_TYPE)
}

def items(markAsRead) {
# Use responseBodyAsString rather than responseBodyAsStream, which
# seems susceptible to UTF-8 errors ("Invalid byte 2 of 3-byte UTF-8
# sequence.
") [Jon Aquino 2005-03-20]
response = callBloglines("http://rpc.bloglines.com/getitems?s=0&n=${markAsRead?1:0}").responseBodyAsString
if (response == null) { return [] }
items = []
new SAXBuilder().build(new StringReader(response)).rootElement.getChildren("channel").each { channelTag |
channelTag.getChildren("item").each { itemTag |
item = new Item(feed:channelTag.getChildTextTrim("title"), title:itemTag.getChildTextTrim("title"), text:itemTag.getChildTextTrim("description"))
items.add(item)
}
}
items
}

class Item {
feed
title
text
void setFeed(feed) { this.feed = clean(feed) }
void setTitle(title) { this.title = clean(title) }
void setText(text) { this.text = clean(text) }
boolean equals(Object other) { feed+text == other.feed+other.text }
int hashCode() { (feed+text).hashCode() }
clean(s) { s.replaceAll("<[^>]+>", "").replaceAll("&[^ ]+;", "#") }
}

def createMenu() {
x = this
aboutDialog = new SwingBuilder().optionPane(message:"Yet Another Bloglines Notifier 1.0 Beta 1\nby Jonathan Aquino").createDialog(null, "About YABN")
new SwingBuilder().popupMenu() {
menuItem() { action(name:"Show Unread Items Again", closure:{ x.putAt("showUnreadItemsAgainRequested", true) }) }
menuItem() { action(name:"Mark All Items As Read", closure:{ x.markAllItemsAsRead() }) }
menuItem() { action(name:"Go To Bloglines (click icon)", closure:{ x.goToBloglines() }) }
menuItem() { action(name:"About...", closure:{ aboutDialog.visible = true }) }
menuItem() { action(name:"Exit", closure:{ System.exit(0) }) }
}
}

def goToBloglines() { Desktop.browse(new URL("http://bloglines.com/myblogs")) }

def markAllItemsAsRead() {
items(true)
updateTrayIcon(trayIcon, image, 0)
}


>> Download yabn-1.0-beta-1-windows.zip (1.5 MB) <<

Requirements: Java 1.4 or newer, Windows XP (Let me know if it works in other versions of Windows.) (Want to help me test the Mac/Unix/Linux versions? Send me an email!)

Installation: Unzip it to a folder on your computer. Edit yabn.ini to specify your email and password. Add a shortcut to yabn.bat to your Startup folder.

Caveat: As this is a Java application, don't be surprised if this puppy consumes 30 MB of memory. But it's worth it if you're a Bloglines junkie like me!

2 Comments:

  • hi!..... can you please upload again your program???

    thanks

    By Anonymous Anonymous, at 10/26/2005 8:29 p.m.  

  • Hi anonymous - actually I wouldn't recommend it anymore because it's a bit slow and unreliable (written in Groovy, which is currently slow).

    By Blogger Jonathan, at 10/26/2005 10:16 p.m.  

Post a Comment

<< Home