1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # ============================================================================
21 __appname__ = 'FeedingIt'
22 __author__ = 'Yves Marcoz'
23 __version__ = '0.9.1~woodchuck'
24 __description__ = 'A simple RSS Reader for Maemo 5'
25 # ============================================================================
28 from pango import FontDescription
33 from webkit import WebView
38 from os.path import isfile, isdir, exists
39 from os import mkdir, remove, stat, environ
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from updatedbus import UpdateServerObject, get_lock
46 from config import Config
47 from cgi import escape
50 from rss_sqlite import Listing
51 from opml import GetOpmlData, ExportOpmlData
54 from jobmanager import JobManager
56 from socket import setdefaulttimeout
58 setdefaulttimeout(timeout)
66 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
67 ABOUT_ICON = 'feedingit'
68 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
69 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
70 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
71 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
73 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
74 unread_color = color_style.lookup_color('ActiveTextColor')
75 read_color = color_style.lookup_color('DefaultTextColor')
78 CONFIGDIR="/home/user/.feedingit/"
79 LOCK = CONFIGDIR + "update.lock"
82 from htmlentitydefs import name2codepoint
84 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
86 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
90 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
91 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
92 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
94 # Build the markup template for the Maemo 5 text style
95 head_font = style.get_font_desc('SystemFont')
96 sub_font = style.get_font_desc('SmallSystemFont')
98 #head_color = style.get_color('ButtonTextColor')
99 head_color = style.get_color('DefaultTextColor')
100 sub_color = style.get_color('DefaultTextColor')
101 active_color = style.get_color('ActiveTextColor')
103 bg_color = style.get_color('DefaultBackgroundColor').to_string()
104 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
105 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
106 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
107 bg_color = "#" + c1 + c2 + c3
110 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
111 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
113 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
114 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
116 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
117 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
119 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
120 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
122 FEED_TEMPLATE = '\n'.join((head, normal_sub))
123 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
125 ENTRY_TEMPLATE = entry_head
126 ENTRY_TEMPLATE_UNREAD = entry_active_head
129 # Removes HTML or XML character references and entities from a text string.
131 # @param text The HTML (or XML) source text.
132 # @return The plain text, as a Unicode string, if necessary.
133 # http://effbot.org/zone/re-sub.htm#unescape-html
138 # character reference
140 if text[:3] == "&#x":
141 return unichr(int(text[3:-1], 16))
143 return unichr(int(text[2:-1]))
149 text = unichr(name2codepoint[text[1:-1]])
152 return text # leave as is
153 return sub("&#?\w+;", fixup, text)
156 class AddWidgetWizard(gtk.Dialog):
157 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
158 gtk.Dialog.__init__(self)
159 self.set_transient_for(parent)
161 #self.category = categories[0]
162 self.category = currentCat
165 self.set_title('Edit RSS feed')
167 self.set_title('Add new RSS feed')
170 self.btn_add = self.add_button('Save', 2)
172 self.btn_add = self.add_button('Add', 2)
174 self.set_default_response(2)
176 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
177 self.nameEntry.set_placeholder('Feed name')
178 # If titleIn matches urlIn, there is no title.
179 if not titleIn == None and titleIn != urlIn:
180 self.nameEntry.set_text(titleIn)
181 self.nameEntry.select_region(-1, -1)
183 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
184 self.urlEntry.set_placeholder('Feed URL')
185 self.urlEntry.set_text(urlIn)
186 self.urlEntry.select_region(-1, -1)
187 self.urlEntry.set_activates_default(True)
189 self.table = gtk.Table(3, 2, False)
190 self.table.set_col_spacings(5)
191 label = gtk.Label('Name:')
192 label.set_alignment(1., .5)
193 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
194 self.table.attach(self.nameEntry, 1, 2, 0, 1)
195 label = gtk.Label('URL:')
196 label.set_alignment(1., .5)
197 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
198 self.table.attach(self.urlEntry, 1, 2, 1, 2)
199 selector = self.create_selector(categories, listing)
200 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
201 picker.set_selector(selector)
202 picker.set_title("Select category")
203 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
204 picker.set_name('HildonButton-finger')
205 picker.set_alignment(0,0,1,1)
207 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
209 self.vbox.pack_start(self.table)
214 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
216 def create_selector(self, choices, listing):
217 #self.pickerDialog = hildon.PickerDialog(self.parent)
218 selector = hildon.TouchSelector(text=True)
222 title = listing.getCategoryTitle(item)
223 iter = selector.append_text(str(title))
224 if self.category == item:
225 selector.set_active(0, index)
226 self.map[title] = item
228 selector.connect("changed", self.selection_changed)
229 #self.pickerDialog.set_selector(selector)
232 def selection_changed(self, selector, button):
233 current_selection = selector.get_current_text()
234 if current_selection:
235 self.category = self.map[current_selection]
237 class AddCategoryWizard(gtk.Dialog):
238 def __init__(self, parent, titleIn=None, isEdit=False):
239 gtk.Dialog.__init__(self)
240 self.set_transient_for(parent)
243 self.set_title('Edit Category')
245 self.set_title('Add Category')
248 self.btn_add = self.add_button('Save', 2)
250 self.btn_add = self.add_button('Add', 2)
252 self.set_default_response(2)
254 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
255 self.nameEntry.set_placeholder('Category name')
256 if not titleIn == None:
257 self.nameEntry.set_text(titleIn)
258 self.nameEntry.select_region(-1, -1)
260 self.table = gtk.Table(1, 2, False)
261 self.table.set_col_spacings(5)
262 label = gtk.Label('Name:')
263 label.set_alignment(1., .5)
264 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
265 self.table.attach(self.nameEntry, 1, 2, 0, 1)
266 #label = gtk.Label('URL:')
267 #label.set_alignment(1., .5)
268 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
269 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
270 self.vbox.pack_start(self.table)
275 return self.nameEntry.get_text()
277 class DownloadBar(gtk.ProgressBar):
280 if hasattr (cls, 'class_init_done'):
284 jm.stats_hook_register (cls.update_progress,
285 run_in_main_thread=True)
287 cls.downloadbars = []
288 # Total number of jobs we are monitoring.
290 # Number of jobs complete (of those that we are monitoring).
295 cls.class_init_done = True
297 def __init__(self, parent):
300 gtk.ProgressBar.__init__(self)
302 self.downloadbars.append(weakref.ref (self))
304 self.__class__.update_bars()
308 def downloading(cls):
309 return hasattr (cls, 'jobs_at_start')
312 def update_progress(cls, jm, old_stats, new_stats, updated_feed):
313 if not cls.downloading():
314 cls.jobs_at_start = old_stats['jobs-completed']
316 if not cls.downloadbars:
319 if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
320 del cls.jobs_at_start
321 for ref in cls.downloadbars:
324 # The download bar disappeared.
325 cls.downloadbars.remove (ref)
327 bar.emit("download-done", None)
330 # This should never be called if new_stats['jobs'] is 0, but
332 cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
333 cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
334 cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
335 + new_stats['jobs-queued']) / cls.total
339 for ref in cls.downloadbars:
342 # The download bar disappeared.
343 cls.downloadbars.remove (ref)
345 bar.emit("download-done", updated_feed)
348 def update_bars(cls):
349 # In preparation for i18n/l10n
351 return (a if n == 1 else b)
353 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
355 % (cls.done, cls.total))
357 for ref in cls.downloadbars:
360 # The download bar disappeared.
361 cls.downloadbars.remove (ref)
364 bar.set_fraction(cls.progress)
366 class SortList(hildon.StackableWindow):
367 def __init__(self, parent, listing, feedingit, after_closing, category=None):
368 hildon.StackableWindow.__init__(self)
369 self.set_transient_for(parent)
371 self.isEditingCategories = False
372 self.category = category
373 self.set_title(listing.getCategoryTitle(category))
375 self.isEditingCategories = True
376 self.set_title('Categories')
377 self.listing = listing
378 self.feedingit = feedingit
379 self.after_closing = after_closing
381 self.connect('destroy', lambda w: self.after_closing())
382 self.vbox2 = gtk.VBox(False, 2)
384 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
385 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
386 button.connect("clicked", self.buttonUp)
387 self.vbox2.pack_start(button, expand=False, fill=False)
389 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
390 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
391 button.connect("clicked", self.buttonDown)
392 self.vbox2.pack_start(button, expand=False, fill=False)
394 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
396 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
397 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
398 button.connect("clicked", self.buttonAdd)
399 self.vbox2.pack_start(button, expand=False, fill=False)
401 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
402 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
403 button.connect("clicked", self.buttonEdit)
404 self.vbox2.pack_start(button, expand=False, fill=False)
406 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
407 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
408 button.connect("clicked", self.buttonDelete)
409 self.vbox2.pack_start(button, expand=False, fill=False)
411 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
412 #button.set_label("Done")
413 #button.connect("clicked", self.buttonDone)
414 #self.vbox.pack_start(button)
415 self.hbox2= gtk.HBox(False, 10)
416 self.pannableArea = hildon.PannableArea()
417 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
418 self.treeview = gtk.TreeView(self.treestore)
419 self.hbox2.pack_start(self.pannableArea, expand=True)
421 self.hbox2.pack_end(self.vbox2, expand=False)
422 self.set_default_size(-1, 600)
425 menu = hildon.AppMenu()
426 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
427 button.set_label("Import from OPML")
428 button.connect("clicked", self.feedingit.button_import_clicked)
431 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
432 button.set_label("Export to OPML")
433 button.connect("clicked", self.feedingit.button_export_clicked)
435 self.set_app_menu(menu)
439 #self.connect("destroy", self.buttonDone)
441 def displayFeeds(self):
442 self.treeview.destroy()
443 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
444 self.treeview = gtk.TreeView()
446 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
447 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
449 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
451 self.pannableArea.add(self.treeview)
455 def refreshList(self, selected=None, offset=0):
456 #rect = self.treeview.get_visible_rect()
457 #y = rect.y+rect.height
458 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
459 if self.isEditingCategories:
460 for key in self.listing.getListOfCategories():
461 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
465 for key in self.listing.getListOfFeeds(category=self.category):
466 item = self.treestore.append([self.listing.getFeedTitle(key), key])
469 self.treeview.set_model(self.treestore)
470 if not selected == None:
471 self.treeview.get_selection().select_iter(selectedItem)
472 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
473 self.pannableArea.show_all()
475 def getSelectedItem(self):
476 (model, iter) = self.treeview.get_selection().get_selected()
479 return model.get_value(iter, 1)
481 def findIndex(self, key):
485 for row in self.treestore:
487 return (before, row.iter)
488 if key == list(row)[0]:
492 return (before, None)
494 def buttonUp(self, button):
495 key = self.getSelectedItem()
497 if self.isEditingCategories:
498 self.listing.moveCategoryUp(key)
500 self.listing.moveUp(key)
501 self.refreshList(key, -10)
503 def buttonDown(self, button):
504 key = self.getSelectedItem()
506 if self.isEditingCategories:
507 self.listing.moveCategoryDown(key)
509 self.listing.moveDown(key)
510 self.refreshList(key, 10)
512 def buttonDelete(self, button):
513 key = self.getSelectedItem()
515 message = 'Really remove this feed and its entries?'
516 dlg = hildon.hildon_note_new_confirmation(self, message)
519 if response == gtk.RESPONSE_OK:
520 if self.isEditingCategories:
521 self.listing.removeCategory(key)
523 self.listing.removeFeed(key)
526 def buttonEdit(self, button):
527 key = self.getSelectedItem()
529 if key == 'ArchivedArticles':
530 message = 'Cannot edit the archived articles feed.'
531 hildon.hildon_banner_show_information(self, '', message)
533 if self.isEditingCategories:
535 SortList(self.parent, self.listing, self.feedingit, None, category=key)
538 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
541 (title, url, category) = wizard.getData()
543 self.listing.editFeed(key, title, url, category=category)
547 def buttonDone(self, *args):
550 def buttonAdd(self, button, urlIn="http://"):
551 if self.isEditingCategories:
552 wizard = AddCategoryWizard(self)
555 title = wizard.getData()
556 if (not title == ''):
557 self.listing.addCategory(title)
559 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
562 (title, url, category) = wizard.getData()
564 self.listing.addFeed(title, url, category=category)
569 class DisplayArticle(hildon.StackableWindow):
570 def __init__(self, feed, id, key, config, listing):
571 hildon.StackableWindow.__init__(self)
572 #self.imageDownloader = ImageDownloader()
577 #self.set_title(feed.getTitle(id))
578 self.set_title(self.listing.getFeedTitle(key))
580 self.set_for_removal = False
582 # Init the article display
583 #if self.config.getWebkitSupport():
584 self.view = WebView()
585 #self.view.set_editable(False)
588 # self.view = gtkhtml2.View()
589 # self.document = gtkhtml2.Document()
590 # self.view.set_document(self.document)
591 # self.document.connect("link_clicked", self._signal_link_clicked)
592 self.pannable_article = hildon.PannableArea()
593 self.pannable_article.add(self.view)
594 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
595 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
597 #if self.config.getWebkitSupport():
598 contentLink = self.feed.getContentLink(self.id)
599 self.feed.setEntryRead(self.id)
600 #if key=="ArchivedArticles":
601 self.loadedArticle = False
602 if contentLink.startswith("/home/user/"):
603 self.view.open("file://%s" % contentLink)
604 self.currentUrl = self.feed.getExternalLink(self.id)
606 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
607 self.currentUrl = "%s" % contentLink
608 self.view.connect("motion-notify-event", lambda w,ev: True)
609 self.view.connect('load-started', self.load_started)
610 self.view.connect('load-finished', self.load_finished)
612 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
614 menu = hildon.AppMenu()
615 # Create a button and add it to the menu
616 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
617 button.set_label("Allow horizontal scrolling")
618 button.connect("clicked", self.horiz_scrolling_button)
621 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
622 button.set_label("Open in browser")
623 button.connect("clicked", self.open_in_browser)
626 if key == "ArchivedArticles":
627 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
628 button.set_label("Remove from archived articles")
629 button.connect("clicked", self.remove_archive_button)
631 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
632 button.set_label("Add to archived articles")
633 button.connect("clicked", self.archive_button)
636 self.set_app_menu(menu)
639 self.add(self.pannable_article)
641 self.pannable_article.show_all()
643 self.destroyId = self.connect("destroy", self.destroyWindow)
645 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
646 ## Still using an old version of WebKit, so using navigation-requested signal
647 self.view.connect('navigation-requested', self.navigation_requested)
649 self.view.connect("button_press_event", self.button_pressed)
650 self.gestureId = self.view.connect("button_release_event", self.button_released)
652 #def navigation_policy_decision(self, wv, fr, req, action, decision):
653 def navigation_requested(self, wv, fr, req):
654 if self.config.getOpenInExternalBrowser():
655 self.open_in_browser(None, req.get_uri())
660 def load_started(self, *widget):
661 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
663 def load_finished(self, *widget):
664 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
665 frame = self.view.get_main_frame()
666 if self.loadedArticle:
667 self.currentUrl = frame.get_uri()
669 self.loadedArticle = True
671 def button_pressed(self, window, event):
672 #print event.x, event.y
673 self.coords = (event.x, event.y)
675 def button_released(self, window, event):
676 x = self.coords[0] - event.x
677 y = self.coords[1] - event.y
679 if (2*abs(y) < abs(x)):
681 self.emit("article-previous", self.id)
683 self.emit("article-next", self.id)
685 def destroyWindow(self, *args):
686 self.disconnect(self.destroyId)
687 if self.set_for_removal:
688 self.emit("article-deleted", self.id)
690 self.emit("article-closed", self.id)
691 #self.imageDownloader.stopAll()
694 def horiz_scrolling_button(self, *widget):
695 self.pannable_article.disconnect(self.gestureId)
696 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
698 def archive_button(self, *widget):
699 # Call the listing.addArchivedArticle
700 self.listing.addArchivedArticle(self.key, self.id)
702 def remove_archive_button(self, *widget):
703 self.set_for_removal = True
705 def open_in_browser(self, object, link=None):
707 bus = dbus.SessionBus()
708 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
709 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
711 iface.open_new_window(self.currentUrl)
713 iface.open_new_window(link)
715 class DisplayFeed(hildon.StackableWindow):
716 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
717 hildon.StackableWindow.__init__(self)
718 self.listing = listing
720 self.feedTitle = title
721 self.set_title(title)
723 self.current = list()
725 self.updateDbusHandler = updateDbusHandler
727 self.downloadDialog = False
729 #self.listing.setCurrentlyDisplayedFeed(self.key)
733 menu = hildon.AppMenu()
734 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
735 button.set_label("Update feed")
736 button.connect("clicked", self.button_update_clicked)
739 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
740 button.set_label("Mark all as read")
741 button.connect("clicked", self.buttonReadAllClicked)
744 if key=="ArchivedArticles":
745 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
746 button.set_label("Delete read articles")
747 button.connect("clicked", self.buttonPurgeArticles)
750 self.set_app_menu(menu)
753 self.main_vbox = gtk.VBox(False, 0)
754 self.add(self.main_vbox)
756 self.pannableFeed = None
759 if DownloadBar.downloading ():
760 self.show_download_bar ()
762 self.connect('configure-event', self.on_configure_event)
763 self.connect("destroy", self.destroyWindow)
765 def on_configure_event(self, window, event):
766 if getattr(self, 'markup_renderer', None) is None:
769 # Fix up the column width for wrapping the text when the window is
770 # resized (i.e. orientation changed)
771 self.markup_renderer.set_property('wrap-width', event.width-20)
772 it = self.feedItems.get_iter_first()
773 while it is not None:
774 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
775 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
776 it = self.feedItems.iter_next(it)
778 def destroyWindow(self, *args):
779 #self.feed.saveUnread(CONFIGDIR)
780 self.listing.updateUnread(self.key)
781 self.emit("feed-closed", self.key)
783 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
784 #self.listing.closeCurrentlyDisplayedFeed()
786 def fix_title(self, title):
787 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
789 def displayFeed(self):
790 if self.pannableFeed:
791 self.pannableFeed.destroy()
793 self.pannableFeed = hildon.PannableArea()
795 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
797 self.feedItems = gtk.ListStore(str, str)
798 #self.feedList = gtk.TreeView(self.feedItems)
799 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
800 self.feedList.set_rules_hint(True)
802 selection = self.feedList.get_selection()
803 selection.set_mode(gtk.SELECTION_NONE)
804 #selection.connect("changed", lambda w: True)
806 self.feedList.set_model(self.feedItems)
807 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
810 self.feedList.set_hover_selection(False)
811 #self.feedList.set_property('enable-grid-lines', True)
812 #self.feedList.set_property('hildon-mode', 1)
813 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
815 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
817 vbox= gtk.VBox(False, 10)
818 vbox.pack_start(self.feedList)
820 self.pannableFeed.add_with_viewport(vbox)
822 self.markup_renderer = gtk.CellRendererText()
823 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
824 self.markup_renderer.set_property('background', bg_color) #"#333333")
825 (width, height) = self.get_size()
826 self.markup_renderer.set_property('wrap-width', width-20)
827 self.markup_renderer.set_property('ypad', 8)
828 self.markup_renderer.set_property('xpad', 5)
829 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
830 markup=FEED_COLUMN_MARKUP)
831 self.feedList.append_column(markup_column)
833 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
834 hideReadArticles = self.config.getHideReadArticles()
836 articles = self.feed.getIds(onlyUnread=True)
838 articles = self.feed.getIds()
841 self.current = list()
845 isRead = self.feed.isEntryRead(id)
848 if not ( isRead and hideReadArticles ):
849 title = self.fix_title(self.feed.getTitle(id))
850 self.current.append(id)
852 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
854 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
856 self.feedItems.append((markup, id))
859 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
861 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
862 self.feedItems.append((markup, ""))
864 self.main_vbox.pack_start(self.pannableFeed)
868 self.pannableFeed.destroy()
869 #self.remove(self.pannableFeed)
871 def on_feedList_row_activated(self, treeview, path): #, column):
872 selection = self.feedList.get_selection()
873 selection.set_mode(gtk.SELECTION_SINGLE)
874 self.feedList.get_selection().select_path(path)
875 model = treeview.get_model()
876 iter = model.get_iter(path)
877 key = model.get_value(iter, FEED_COLUMN_KEY)
878 # Emulate legacy "button_clicked" call via treeview
879 gobject.idle_add(self.button_clicked, treeview, key)
882 def button_clicked(self, button, index, previous=False, next=False):
883 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
884 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
885 stack = hildon.WindowStack.get_default()
888 stack.pop_and_push(1, newDisp, tmp)
890 gobject.timeout_add(200, self.destroyArticle, tmp)
895 if type(self.disp).__name__ == "DisplayArticle":
896 gobject.timeout_add(200, self.destroyArticle, self.disp)
903 if self.key == "ArchivedArticles":
904 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
905 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
906 self.ids.append(self.disp.connect("article-next", self.nextArticle))
907 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
909 def buttonPurgeArticles(self, *widget):
911 self.feed.purgeReadArticles()
912 #self.feed.saveFeed(CONFIGDIR)
915 def destroyArticle(self, handle):
916 handle.destroyWindow()
918 def mark_item_read(self, key):
919 it = self.feedItems.get_iter_first()
920 while it is not None:
921 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
923 title = self.fix_title(self.feed.getTitle(key))
924 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
925 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
927 it = self.feedItems.iter_next(it)
929 def nextArticle(self, object, index):
930 self.mark_item_read(index)
931 id = self.feed.getNextId(index)
932 while id not in self.current and id != index:
933 id = self.feed.getNextId(id)
935 self.button_clicked(object, id, next=True)
937 def previousArticle(self, object, index):
938 self.mark_item_read(index)
939 id = self.feed.getPreviousId(index)
940 while id not in self.current and id != index:
941 id = self.feed.getPreviousId(id)
943 self.button_clicked(object, id, previous=True)
945 def onArticleClosed(self, object, index):
946 selection = self.feedList.get_selection()
947 selection.set_mode(gtk.SELECTION_NONE)
948 self.mark_item_read(index)
950 def onArticleDeleted(self, object, index):
952 self.feed.removeArticle(index)
953 #self.feed.saveFeed(CONFIGDIR)
956 def button_update_clicked(self, button):
957 self.listing.updateFeed (self.key, priority=-1)
959 def show_download_bar(self):
960 if not type(self.downloadDialog).__name__=="DownloadBar":
961 self.downloadDialog = DownloadBar(self.window)
962 self.downloadDialog.connect("download-done", self.onDownloadDone)
963 self.main_vbox.pack_end(self.downloadDialog,
964 expand=False, fill=False)
967 def onDownloadDone(self, widget, feed):
968 if feed == self.feed or feed is None:
969 self.downloadDialog.destroy()
970 self.downloadDialog = False
971 self.feed = self.listing.getFeed(self.key)
973 self.updateDbusHandler.ArticleCountUpdated()
975 def buttonReadAllClicked(self, button):
977 self.feed.markAllAsRead()
978 it = self.feedItems.get_iter_first()
979 while it is not None:
980 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
981 title = self.fix_title(self.feed.getTitle(k))
982 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
983 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
984 it = self.feedItems.iter_next(it)
986 #for index in self.feed.getIds():
987 # self.feed.setEntryRead(index)
988 # self.mark_item_read(index)
994 self.window = hildon.StackableWindow()
995 self.window.set_title(__appname__)
996 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
997 self.mainVbox = gtk.VBox(False,10)
999 if isfile(CONFIGDIR+"/feeds.db"):
1000 self.introLabel = gtk.Label("Loading...")
1002 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1004 self.mainVbox.pack_start(self.introLabel)
1006 self.window.add(self.mainVbox)
1007 self.window.show_all()
1008 self.config = Config(self.window, CONFIGDIR+"config.ini")
1009 gobject.idle_add(self.createWindow)
1011 # This is set to try when the user interacts with the program.
1012 # If, after an update is complete, we discover that the
1013 # environment variable DBUS_STARTED_ADDRESS is set and
1014 # self.had_interaction is False, we quit.
1015 self.had_interaction = False
1017 def createWindow(self):
1020 self.app_lock = get_lock("app_lock")
1021 if self.app_lock == None:
1023 self.stopButton.set_sensitive(True)
1025 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1026 self.stopButton.set_text("Stop update","")
1027 self.stopButton.connect("clicked", self.stop_running_update)
1028 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1029 self.window.show_all()
1030 self.introLabel.set_label("Update in progress, please wait.")
1031 gobject.timeout_add_seconds(3, self.createWindow)
1034 self.stopButton.destroy()
1037 self.listing = Listing(self.config, CONFIGDIR)
1039 self.downloadDialog = False
1041 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1042 self.orientation.set_mode(self.config.getOrientation())
1044 print "Could not start rotation manager"
1046 menu = hildon.AppMenu()
1047 # Create a button and add it to the menu
1048 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1049 button.set_label("Update feeds")
1050 button.connect("clicked", self.button_update_clicked, "All")
1053 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1054 button.set_label("Mark all as read")
1055 button.connect("clicked", self.button_markAll)
1058 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1059 button.set_label("Add new feed")
1060 button.connect("clicked", lambda b: self.addFeed())
1063 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1064 button.set_label("Manage subscriptions")
1065 button.connect("clicked", self.button_organize_clicked)
1068 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1069 button.set_label("Settings")
1070 button.connect("clicked", self.button_preferences_clicked)
1073 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1074 button.set_label("About")
1075 button.connect("clicked", self.button_about_clicked)
1078 self.window.set_app_menu(menu)
1081 #self.feedWindow = hildon.StackableWindow()
1082 #self.articleWindow = hildon.StackableWindow()
1083 self.introLabel.destroy()
1084 self.pannableListing = hildon.PannableArea()
1085 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1086 self.feedList = gtk.TreeView(self.feedItems)
1087 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1088 #self.feedList.set_enable_tree_lines(True)
1089 #self.feedList.set_show_expanders(True)
1090 self.pannableListing.add(self.feedList)
1092 icon_renderer = gtk.CellRendererPixbuf()
1093 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1094 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1096 self.feedList.append_column(icon_column)
1098 markup_renderer = gtk.CellRendererText()
1099 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1100 markup=COLUMN_MARKUP)
1101 self.feedList.append_column(markup_column)
1102 self.mainVbox.pack_start(self.pannableListing)
1103 self.mainVbox.show_all()
1105 self.displayListing()
1106 self.autoupdate = False
1107 self.checkAutoUpdate()
1109 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1110 gobject.idle_add(self.late_init)
1112 def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1113 if (not self.downloadDialog
1114 and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1115 self.updateDbusHandler.UpdateStarted()
1117 self.downloadDialog = DownloadBar(self.window)
1118 self.downloadDialog.connect("download-done", self.onDownloadDone)
1119 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1120 self.mainVbox.show_all()
1122 if self.__dict__.get ('disp', None):
1123 self.disp.show_download_bar ()
1125 def onDownloadDone(self, widget, feed):
1127 self.downloadDialog.destroy()
1128 self.downloadDialog = False
1129 self.displayListing()
1130 self.updateDbusHandler.UpdateFinished()
1131 self.updateDbusHandler.ArticleCountUpdated()
1133 if not self.had_interaction and 'DBUS_STARTER_ADDRESS' in environ:
1134 print "Update complete. No interaction, started by dbus: quitting."
1136 def stop_running_update(self, button):
1137 self.stopButton.set_sensitive(False)
1139 bus=dbus.SessionBus()
1140 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1141 "/org/marcoz/feedingit/update" # Object's path
1143 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1146 def increase_download_parallelism(self):
1147 # The system has been idle for a while. Enable parallel
1149 JobManager().num_threads = 4
1150 gobject.source_remove (self.increase_download_parallelism_id)
1151 del self.increase_download_parallelism_id
1154 def system_inactivity_ind(self, idle):
1155 # The system's idle state changed.
1156 if (self.am_idle and idle) or (not self.am_idle and not idle):
1161 if hasattr (self, 'increase_download_parallelism_id'):
1162 gobject.source_remove (self.increase_download_parallelism_id)
1163 del self.increase_download_parallelism_id
1165 self.increase_download_parallelism_id = \
1166 gobject.timeout_add_seconds(
1167 60, self.increase_download_parallelism)
1170 JobManager().num_threads = 1
1174 def late_init(self):
1175 self.dbusHandler = ServerObject(self)
1176 self.updateDbusHandler = UpdateServerObject(self)
1179 jm.stats_hook_register (self.job_manager_update,
1180 run_in_main_thread=True)
1182 self.am_idle = False
1186 bus = dbus.SystemBus()
1187 proxy = bus.get_object('com.nokia.mce',
1188 '/com/nokia/mce/signal')
1189 iface = dbus.Interface(proxy, 'com.nokia.mce.signal')
1190 iface.connect_to_signal('system_inactivity_ind',
1191 self.system_inactivity_ind)
1193 def button_markAll(self, button):
1194 self.had_interaction = True
1195 for key in self.listing.getListOfFeeds():
1196 feed = self.listing.getFeed(key)
1197 feed.markAllAsRead()
1198 #for id in feed.getIds():
1199 # feed.setEntryRead(id)
1200 self.listing.updateUnread(key)
1201 self.displayListing()
1203 def button_about_clicked(self, button):
1204 self.had_interaction = True
1205 HeAboutDialog.present(self.window, \
1215 def button_export_clicked(self, button):
1216 self.had_interaction = True
1217 opml = ExportOpmlData(self.window, self.listing)
1219 def button_import_clicked(self, button):
1220 self.had_interaction = True
1221 opml = GetOpmlData(self.window)
1222 feeds = opml.getData()
1223 for (title, url) in feeds:
1224 self.listing.addFeed(title, url)
1225 self.displayListing()
1227 def addFeed(self, urlIn="http://"):
1228 self.had_interaction = True
1229 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1232 (title, url, category) = wizard.getData()
1234 self.listing.addFeed(title, url, category=category)
1236 self.displayListing()
1238 def button_organize_clicked(self, button):
1239 self.had_interaction = True
1240 def after_closing():
1241 self.displayListing()
1242 SortList(self.window, self.listing, self, after_closing)
1244 def button_update_clicked(self, button, key):
1245 self.had_interaction = True
1246 for k in self.listing.getListOfFeeds():
1247 self.listing.updateFeed (k)
1248 #self.displayListing()
1250 def onDownloadsDone(self, *widget):
1251 self.downloadDialog.destroy()
1252 self.downloadDialog = False
1253 self.displayListing()
1254 self.updateDbusHandler.UpdateFinished()
1255 self.updateDbusHandler.ArticleCountUpdated()
1257 def button_preferences_clicked(self, button):
1258 self.had_interaction = True
1259 dialog = self.config.createDialog()
1260 dialog.connect("destroy", self.prefsClosed)
1262 def show_confirmation_note(self, parent, title):
1263 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1265 retcode = gtk.Dialog.run(note)
1268 if retcode == gtk.RESPONSE_OK:
1273 def saveExpandedLines(self):
1274 self.expandedLines = []
1275 model = self.feedList.get_model()
1276 model.foreach(self.checkLine)
1278 def checkLine(self, model, path, iter, data = None):
1279 if self.feedList.row_expanded(path):
1280 self.expandedLines.append(path)
1282 def restoreExpandedLines(self):
1283 model = self.feedList.get_model()
1284 model.foreach(self.restoreLine)
1286 def restoreLine(self, model, path, iter, data = None):
1287 if path in self.expandedLines:
1288 self.feedList.expand_row(path, False)
1290 def displayListing(self):
1291 icon_theme = gtk.icon_theme_get_default()
1292 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1293 gtk.ICON_LOOKUP_USE_BUILTIN)
1295 self.saveExpandedLines()
1297 self.feedItems.clear()
1298 hideReadFeed = self.config.getHideReadFeeds()
1299 order = self.config.getFeedSortOrder()
1301 categories = self.listing.getListOfCategories()
1302 if len(categories) > 1:
1303 showCategories = True
1305 showCategories = False
1307 for categoryId in categories:
1309 title = self.listing.getCategoryTitle(categoryId)
1310 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1312 if showCategories and len(keys)>0:
1313 category = self.feedItems.append(None, (None, title, categoryId))
1314 #print "catID" + str(categoryId) + " " + str(self.category)
1315 if categoryId == self.category:
1317 expandedRow = category
1320 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1321 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1322 updateTime = self.listing.getFeedUpdateTime(key)
1324 updateTime = "Never"
1325 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1327 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1329 markup = FEED_TEMPLATE % (title, subtitle)
1332 icon_filename = self.listing.getFavicon(key)
1333 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1334 LIST_ICON_SIZE, LIST_ICON_SIZE)
1336 pixbuf = default_pixbuf
1339 self.feedItems.append(category, (pixbuf, markup, key))
1341 self.feedItems.append(None, (pixbuf, markup, key))
1344 self.restoreExpandedLines()
1347 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1351 def on_feedList_row_activated(self, treeview, path, column):
1352 self.had_interaction = True
1353 model = treeview.get_model()
1354 iter = model.get_iter(path)
1355 key = model.get_value(iter, COLUMN_KEY)
1358 #print "Key: " + str(key)
1360 self.category = catId
1361 if treeview.row_expanded(path):
1362 treeview.collapse_row(path)
1364 # treeview.expand_row(path, True)
1365 #treeview.collapse_all()
1366 #treeview.expand_row(path, False)
1367 #for i in range(len(path)):
1368 # self.feedList.expand_row(path[:i+1], False)
1369 #self.show_confirmation_note(self.window, "Working")
1375 def openFeed(self, key):
1379 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1381 self.feed_lock = get_lock(key)
1382 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1383 self.listing.getFeedTitle(key), key, \
1384 self.config, self.updateDbusHandler)
1385 self.disp.connect("feed-closed", self.onFeedClosed)
1387 def openArticle(self, key, id):
1391 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1393 self.feed_lock = get_lock(key)
1394 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1395 self.listing.getFeedTitle(key), key, \
1396 self.config, self.updateDbusHandler)
1397 self.disp.button_clicked(None, id)
1398 self.disp.connect("feed-closed", self.onFeedClosed)
1401 def onFeedClosed(self, object, key):
1402 #self.listing.saveConfig()
1404 gobject.idle_add(self.onFeedClosedTimeout)
1405 self.displayListing()
1406 #self.updateDbusHandler.ArticleCountUpdated()
1408 def onFeedClosedTimeout(self):
1410 self.updateDbusHandler.ArticleCountUpdated()
1412 def quit(self, *args):
1415 if hasattr (self, 'app_lock'):
1418 # Wait until all slave threads have properly exited before
1419 # terminating the mainloop.
1423 if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1426 gobject.timeout_add(500, self.quit)
1431 self.window.connect("destroy", self.quit)
1434 def prefsClosed(self, *widget):
1436 self.orientation.set_mode(self.config.getOrientation())
1439 self.displayListing()
1440 self.checkAutoUpdate()
1442 def checkAutoUpdate(self, *widget):
1443 interval = int(self.config.getUpdateInterval()*3600000)
1444 if self.config.isAutoUpdateEnabled():
1445 if self.autoupdate == False:
1446 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1447 self.autoupdate = interval
1448 elif not self.autoupdate == interval:
1449 # If auto-update is enabled, but not at the right frequency
1450 gobject.source_remove(self.autoupdateId)
1451 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1452 self.autoupdate = interval
1454 if not self.autoupdate == False:
1455 gobject.source_remove(self.autoupdateId)
1456 self.autoupdate = False
1458 def automaticUpdate(self, *widget):
1459 # Need to check for internet connection
1460 # If no internet connection, try again in 10 minutes:
1461 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1462 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1463 #from time import localtime, strftime
1464 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1466 self.button_update_clicked(None, None)
1469 def stopUpdate(self):
1470 # Not implemented in the app (see update_feeds.py)
1472 JobManager().cancel ()
1476 def getStatus(self):
1478 for key in self.listing.getListOfFeeds():
1479 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1480 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1482 status = "No unread items"
1485 if __name__ == "__main__":
1488 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1489 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1490 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1491 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1492 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1493 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1494 gobject.threads_init()
1495 if not isdir(CONFIGDIR):
1499 print "Error: Can't create configuration directory"
1500 from sys import exit