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
51 logger = logging.getLogger(__name__)
53 from rss_sqlite import Listing
54 from opml import GetOpmlData, ExportOpmlData
57 from jobmanager import JobManager
59 from socket import setdefaulttimeout
61 setdefaulttimeout(timeout)
69 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
70 ABOUT_ICON = 'feedingit'
71 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
72 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
73 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
74 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
76 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
77 unread_color = color_style.lookup_color('ActiveTextColor')
78 read_color = color_style.lookup_color('DefaultTextColor')
81 CONFIGDIR="/home/user/.feedingit/"
82 LOCK = CONFIGDIR + "update.lock"
85 from htmlentitydefs import name2codepoint
87 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
89 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
93 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
94 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
95 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
97 # Build the markup template for the Maemo 5 text style
98 head_font = style.get_font_desc('SystemFont')
99 sub_font = style.get_font_desc('SmallSystemFont')
101 #head_color = style.get_color('ButtonTextColor')
102 head_color = style.get_color('DefaultTextColor')
103 sub_color = style.get_color('DefaultTextColor')
104 active_color = style.get_color('ActiveTextColor')
106 bg_color = style.get_color('DefaultBackgroundColor').to_string()
107 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
108 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
109 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
110 bg_color = "#" + c1 + c2 + c3
113 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
114 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
116 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
117 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
119 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
120 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
122 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
123 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
125 FEED_TEMPLATE = '\n'.join((head, normal_sub))
126 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
128 ENTRY_TEMPLATE = entry_head
129 ENTRY_TEMPLATE_UNREAD = entry_active_head
132 # Removes HTML or XML character references and entities from a text string.
134 # @param text The HTML (or XML) source text.
135 # @return The plain text, as a Unicode string, if necessary.
136 # http://effbot.org/zone/re-sub.htm#unescape-html
141 # character reference
143 if text[:3] == "&#x":
144 return unichr(int(text[3:-1], 16))
146 return unichr(int(text[2:-1]))
152 text = unichr(name2codepoint[text[1:-1]])
155 return text # leave as is
156 return sub("&#?\w+;", fixup, text)
159 class AddWidgetWizard(gtk.Dialog):
160 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
161 gtk.Dialog.__init__(self)
162 self.set_transient_for(parent)
164 #self.category = categories[0]
165 self.category = currentCat
168 self.set_title('Edit RSS feed')
170 self.set_title('Add new RSS feed')
173 self.btn_add = self.add_button('Save', 2)
175 self.btn_add = self.add_button('Add', 2)
177 self.set_default_response(2)
179 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
180 self.nameEntry.set_placeholder('Feed name')
181 # If titleIn matches urlIn, there is no title.
182 if not titleIn == None and titleIn != urlIn:
183 self.nameEntry.set_text(titleIn)
184 self.nameEntry.select_region(-1, -1)
186 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
187 self.urlEntry.set_placeholder('Feed URL')
188 self.urlEntry.set_text(urlIn)
189 self.urlEntry.select_region(-1, -1)
190 self.urlEntry.set_activates_default(True)
192 self.table = gtk.Table(3, 2, False)
193 self.table.set_col_spacings(5)
194 label = gtk.Label('Name:')
195 label.set_alignment(1., .5)
196 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
197 self.table.attach(self.nameEntry, 1, 2, 0, 1)
198 label = gtk.Label('URL:')
199 label.set_alignment(1., .5)
200 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
201 self.table.attach(self.urlEntry, 1, 2, 1, 2)
202 selector = self.create_selector(categories, listing)
203 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
204 picker.set_selector(selector)
205 picker.set_title("Select category")
206 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
207 picker.set_name('HildonButton-finger')
208 picker.set_alignment(0,0,1,1)
210 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
212 self.vbox.pack_start(self.table)
217 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
219 def create_selector(self, choices, listing):
220 #self.pickerDialog = hildon.PickerDialog(self.parent)
221 selector = hildon.TouchSelector(text=True)
225 title = listing.getCategoryTitle(item)
226 iter = selector.append_text(str(title))
227 if self.category == item:
228 selector.set_active(0, index)
229 self.map[title] = item
231 selector.connect("changed", self.selection_changed)
232 #self.pickerDialog.set_selector(selector)
235 def selection_changed(self, selector, button):
236 current_selection = selector.get_current_text()
237 if current_selection:
238 self.category = self.map[current_selection]
240 class AddCategoryWizard(gtk.Dialog):
241 def __init__(self, parent, titleIn=None, isEdit=False):
242 gtk.Dialog.__init__(self)
243 self.set_transient_for(parent)
246 self.set_title('Edit Category')
248 self.set_title('Add Category')
251 self.btn_add = self.add_button('Save', 2)
253 self.btn_add = self.add_button('Add', 2)
255 self.set_default_response(2)
257 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
258 self.nameEntry.set_placeholder('Category name')
259 if not titleIn == None:
260 self.nameEntry.set_text(titleIn)
261 self.nameEntry.select_region(-1, -1)
263 self.table = gtk.Table(1, 2, False)
264 self.table.set_col_spacings(5)
265 label = gtk.Label('Name:')
266 label.set_alignment(1., .5)
267 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
268 self.table.attach(self.nameEntry, 1, 2, 0, 1)
269 #label = gtk.Label('URL:')
270 #label.set_alignment(1., .5)
271 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
272 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
273 self.vbox.pack_start(self.table)
278 return self.nameEntry.get_text()
280 class DownloadBar(gtk.ProgressBar):
283 if hasattr (cls, 'class_init_done'):
287 jm.stats_hook_register (cls.update_progress,
288 run_in_main_thread=True)
290 cls.downloadbars = []
291 # Total number of jobs we are monitoring.
293 # Number of jobs complete (of those that we are monitoring).
298 cls.class_init_done = True
300 def __init__(self, parent):
303 gtk.ProgressBar.__init__(self)
305 self.downloadbars.append(weakref.ref (self))
307 self.__class__.update_bars()
311 def downloading(cls):
312 return hasattr (cls, 'jobs_at_start')
315 def update_progress(cls, jm, old_stats, new_stats, updated_feed):
316 if not cls.downloading():
317 cls.jobs_at_start = old_stats['jobs-completed']
319 if not cls.downloadbars:
322 if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
323 del cls.jobs_at_start
324 for ref in cls.downloadbars:
327 # The download bar disappeared.
328 cls.downloadbars.remove (ref)
330 bar.emit("download-done", None)
333 # This should never be called if new_stats['jobs'] is 0, but
335 cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
336 cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
337 cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
338 + new_stats['jobs-queued']) / cls.total
342 for ref in cls.downloadbars:
345 # The download bar disappeared.
346 cls.downloadbars.remove (ref)
348 bar.emit("download-done", updated_feed)
351 def update_bars(cls):
352 # In preparation for i18n/l10n
354 return (a if n == 1 else b)
356 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
358 % (cls.done, cls.total))
360 for ref in cls.downloadbars:
363 # The download bar disappeared.
364 cls.downloadbars.remove (ref)
367 bar.set_fraction(cls.progress)
369 class SortList(hildon.StackableWindow):
370 def __init__(self, parent, listing, feedingit, after_closing, category=None):
371 hildon.StackableWindow.__init__(self)
372 self.set_transient_for(parent)
374 self.isEditingCategories = False
375 self.category = category
376 self.set_title(listing.getCategoryTitle(category))
378 self.isEditingCategories = True
379 self.set_title('Categories')
380 self.listing = listing
381 self.feedingit = feedingit
382 self.after_closing = after_closing
384 self.connect('destroy', lambda w: self.after_closing())
385 self.vbox2 = gtk.VBox(False, 2)
387 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
388 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
389 button.connect("clicked", self.buttonUp)
390 self.vbox2.pack_start(button, expand=False, fill=False)
392 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
393 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
394 button.connect("clicked", self.buttonDown)
395 self.vbox2.pack_start(button, expand=False, fill=False)
397 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
399 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
400 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
401 button.connect("clicked", self.buttonAdd)
402 self.vbox2.pack_start(button, expand=False, fill=False)
404 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
405 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
406 button.connect("clicked", self.buttonEdit)
407 self.vbox2.pack_start(button, expand=False, fill=False)
409 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
410 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
411 button.connect("clicked", self.buttonDelete)
412 self.vbox2.pack_start(button, expand=False, fill=False)
414 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
415 #button.set_label("Done")
416 #button.connect("clicked", self.buttonDone)
417 #self.vbox.pack_start(button)
418 self.hbox2= gtk.HBox(False, 10)
419 self.pannableArea = hildon.PannableArea()
420 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
421 self.treeview = gtk.TreeView(self.treestore)
422 self.hbox2.pack_start(self.pannableArea, expand=True)
424 self.hbox2.pack_end(self.vbox2, expand=False)
425 self.set_default_size(-1, 600)
428 menu = hildon.AppMenu()
429 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
430 button.set_label("Import from OPML")
431 button.connect("clicked", self.feedingit.button_import_clicked)
434 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
435 button.set_label("Export to OPML")
436 button.connect("clicked", self.feedingit.button_export_clicked)
438 self.set_app_menu(menu)
442 #self.connect("destroy", self.buttonDone)
444 def displayFeeds(self):
445 self.treeview.destroy()
446 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
447 self.treeview = gtk.TreeView()
449 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
450 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
452 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
454 self.pannableArea.add(self.treeview)
458 def refreshList(self, selected=None, offset=0):
459 #rect = self.treeview.get_visible_rect()
460 #y = rect.y+rect.height
461 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
462 if self.isEditingCategories:
463 for key in self.listing.getListOfCategories():
464 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
468 for key in self.listing.getListOfFeeds(category=self.category):
469 item = self.treestore.append([self.listing.getFeedTitle(key), key])
472 self.treeview.set_model(self.treestore)
473 if not selected == None:
474 self.treeview.get_selection().select_iter(selectedItem)
475 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
476 self.pannableArea.show_all()
478 def getSelectedItem(self):
479 (model, iter) = self.treeview.get_selection().get_selected()
482 return model.get_value(iter, 1)
484 def findIndex(self, key):
488 for row in self.treestore:
490 return (before, row.iter)
491 if key == list(row)[0]:
495 return (before, None)
497 def buttonUp(self, button):
498 key = self.getSelectedItem()
500 if self.isEditingCategories:
501 self.listing.moveCategoryUp(key)
503 self.listing.moveUp(key)
504 self.refreshList(key, -10)
506 def buttonDown(self, button):
507 key = self.getSelectedItem()
509 if self.isEditingCategories:
510 self.listing.moveCategoryDown(key)
512 self.listing.moveDown(key)
513 self.refreshList(key, 10)
515 def buttonDelete(self, button):
516 key = self.getSelectedItem()
518 message = 'Really remove this feed and its entries?'
519 dlg = hildon.hildon_note_new_confirmation(self, message)
522 if response == gtk.RESPONSE_OK:
523 if self.isEditingCategories:
524 self.listing.removeCategory(key)
526 self.listing.removeFeed(key)
529 def buttonEdit(self, button):
530 key = self.getSelectedItem()
532 if key == 'ArchivedArticles':
533 message = 'Cannot edit the archived articles feed.'
534 hildon.hildon_banner_show_information(self, '', message)
536 if self.isEditingCategories:
538 SortList(self.parent, self.listing, self.feedingit, None, category=key)
541 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
544 (title, url, category) = wizard.getData()
546 self.listing.editFeed(key, title, url, category=category)
550 def buttonDone(self, *args):
553 def buttonAdd(self, button, urlIn="http://"):
554 if self.isEditingCategories:
555 wizard = AddCategoryWizard(self)
558 title = wizard.getData()
559 if (not title == ''):
560 self.listing.addCategory(title)
562 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
565 (title, url, category) = wizard.getData()
567 self.listing.addFeed(title, url, category=category)
572 class DisplayArticle(hildon.StackableWindow):
573 def __init__(self, feed, id, key, config, listing):
574 hildon.StackableWindow.__init__(self)
575 #self.imageDownloader = ImageDownloader()
580 #self.set_title(feed.getTitle(id))
581 self.set_title(self.listing.getFeedTitle(key))
583 self.set_for_removal = False
585 # Init the article display
586 #if self.config.getWebkitSupport():
587 self.view = WebView()
588 #self.view.set_editable(False)
591 # self.view = gtkhtml2.View()
592 # self.document = gtkhtml2.Document()
593 # self.view.set_document(self.document)
594 # self.document.connect("link_clicked", self._signal_link_clicked)
595 self.pannable_article = hildon.PannableArea()
596 self.pannable_article.add(self.view)
597 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
598 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
600 #if self.config.getWebkitSupport():
601 contentLink = self.feed.getContentLink(self.id)
602 self.feed.setEntryRead(self.id)
603 #if key=="ArchivedArticles":
604 self.loadedArticle = False
605 if contentLink.startswith("/home/user/"):
606 self.view.open("file://%s" % contentLink)
607 self.currentUrl = self.feed.getExternalLink(self.id)
609 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
610 self.currentUrl = "%s" % contentLink
611 self.view.connect("motion-notify-event", lambda w,ev: True)
612 self.view.connect('load-started', self.load_started)
613 self.view.connect('load-finished', self.load_finished)
615 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
617 menu = hildon.AppMenu()
618 # Create a button and add it to the menu
619 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
620 button.set_label("Allow horizontal scrolling")
621 button.connect("clicked", self.horiz_scrolling_button)
624 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
625 button.set_label("Open in browser")
626 button.connect("clicked", self.open_in_browser)
629 if key == "ArchivedArticles":
630 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631 button.set_label("Remove from archived articles")
632 button.connect("clicked", self.remove_archive_button)
634 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
635 button.set_label("Add to archived articles")
636 button.connect("clicked", self.archive_button)
639 self.set_app_menu(menu)
642 self.add(self.pannable_article)
644 self.pannable_article.show_all()
646 self.destroyId = self.connect("destroy", self.destroyWindow)
648 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
649 ## Still using an old version of WebKit, so using navigation-requested signal
650 self.view.connect('navigation-requested', self.navigation_requested)
652 self.view.connect("button_press_event", self.button_pressed)
653 self.gestureId = self.view.connect("button_release_event", self.button_released)
655 #def navigation_policy_decision(self, wv, fr, req, action, decision):
656 def navigation_requested(self, wv, fr, req):
657 if self.config.getOpenInExternalBrowser():
658 self.open_in_browser(None, req.get_uri())
663 def load_started(self, *widget):
664 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
666 def load_finished(self, *widget):
667 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
668 frame = self.view.get_main_frame()
669 if self.loadedArticle:
670 self.currentUrl = frame.get_uri()
672 self.loadedArticle = True
674 def button_pressed(self, window, event):
675 #print event.x, event.y
676 self.coords = (event.x, event.y)
678 def button_released(self, window, event):
679 x = self.coords[0] - event.x
680 y = self.coords[1] - event.y
682 if (2*abs(y) < abs(x)):
684 self.emit("article-previous", self.id)
686 self.emit("article-next", self.id)
688 def destroyWindow(self, *args):
689 self.disconnect(self.destroyId)
690 if self.set_for_removal:
691 self.emit("article-deleted", self.id)
693 self.emit("article-closed", self.id)
694 #self.imageDownloader.stopAll()
697 def horiz_scrolling_button(self, *widget):
698 self.pannable_article.disconnect(self.gestureId)
699 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
701 def archive_button(self, *widget):
702 # Call the listing.addArchivedArticle
703 self.listing.addArchivedArticle(self.key, self.id)
705 def remove_archive_button(self, *widget):
706 self.set_for_removal = True
708 def open_in_browser(self, object, link=None):
710 bus = dbus.SessionBus()
711 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
712 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
714 iface.open_new_window(self.currentUrl)
716 iface.open_new_window(link)
718 class DisplayFeed(hildon.StackableWindow):
719 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
720 hildon.StackableWindow.__init__(self)
721 self.listing = listing
723 self.feedTitle = title
724 self.set_title(title)
726 self.current = list()
728 self.updateDbusHandler = updateDbusHandler
730 self.downloadDialog = False
732 #self.listing.setCurrentlyDisplayedFeed(self.key)
736 menu = hildon.AppMenu()
737 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
738 button.set_label("Update feed")
739 button.connect("clicked", self.button_update_clicked)
742 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
743 button.set_label("Mark all as read")
744 button.connect("clicked", self.buttonReadAllClicked)
747 if key=="ArchivedArticles":
748 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
749 button.set_label("Delete read articles")
750 button.connect("clicked", self.buttonPurgeArticles)
753 self.set_app_menu(menu)
756 self.main_vbox = gtk.VBox(False, 0)
757 self.add(self.main_vbox)
759 self.pannableFeed = None
762 if DownloadBar.downloading ():
763 self.show_download_bar ()
765 self.connect('configure-event', self.on_configure_event)
766 self.connect("destroy", self.destroyWindow)
768 def on_configure_event(self, window, event):
769 if getattr(self, 'markup_renderer', None) is None:
772 # Fix up the column width for wrapping the text when the window is
773 # resized (i.e. orientation changed)
774 self.markup_renderer.set_property('wrap-width', event.width-20)
775 it = self.feedItems.get_iter_first()
776 while it is not None:
777 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
778 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
779 it = self.feedItems.iter_next(it)
781 def destroyWindow(self, *args):
782 #self.feed.saveUnread(CONFIGDIR)
783 self.listing.updateUnread(self.key)
784 self.emit("feed-closed", self.key)
786 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
787 #self.listing.closeCurrentlyDisplayedFeed()
789 def fix_title(self, title):
790 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
792 def displayFeed(self):
793 if self.pannableFeed:
794 self.pannableFeed.destroy()
796 self.pannableFeed = hildon.PannableArea()
798 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
800 self.feedItems = gtk.ListStore(str, str)
801 #self.feedList = gtk.TreeView(self.feedItems)
802 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
803 self.feedList.set_rules_hint(True)
805 selection = self.feedList.get_selection()
806 selection.set_mode(gtk.SELECTION_NONE)
807 #selection.connect("changed", lambda w: True)
809 self.feedList.set_model(self.feedItems)
810 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
813 self.feedList.set_hover_selection(False)
814 #self.feedList.set_property('enable-grid-lines', True)
815 #self.feedList.set_property('hildon-mode', 1)
816 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
818 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
820 vbox= gtk.VBox(False, 10)
821 vbox.pack_start(self.feedList)
823 self.pannableFeed.add_with_viewport(vbox)
825 self.markup_renderer = gtk.CellRendererText()
826 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
827 self.markup_renderer.set_property('background', bg_color) #"#333333")
828 (width, height) = self.get_size()
829 self.markup_renderer.set_property('wrap-width', width-20)
830 self.markup_renderer.set_property('ypad', 8)
831 self.markup_renderer.set_property('xpad', 5)
832 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
833 markup=FEED_COLUMN_MARKUP)
834 self.feedList.append_column(markup_column)
836 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
837 hideReadArticles = self.config.getHideReadArticles()
839 articles = self.feed.getIds(onlyUnread=True)
841 articles = self.feed.getIds()
844 self.current = list()
848 isRead = self.feed.isEntryRead(id)
851 if not ( isRead and hideReadArticles ):
852 title = self.fix_title(self.feed.getTitle(id))
853 self.current.append(id)
855 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
857 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
859 self.feedItems.append((markup, id))
862 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
864 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
865 self.feedItems.append((markup, ""))
867 self.main_vbox.pack_start(self.pannableFeed)
871 self.pannableFeed.destroy()
872 #self.remove(self.pannableFeed)
874 def on_feedList_row_activated(self, treeview, path): #, column):
875 selection = self.feedList.get_selection()
876 selection.set_mode(gtk.SELECTION_SINGLE)
877 self.feedList.get_selection().select_path(path)
878 model = treeview.get_model()
879 iter = model.get_iter(path)
880 key = model.get_value(iter, FEED_COLUMN_KEY)
881 # Emulate legacy "button_clicked" call via treeview
882 gobject.idle_add(self.button_clicked, treeview, key)
885 def button_clicked(self, button, index, previous=False, next=False):
886 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
887 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
888 stack = hildon.WindowStack.get_default()
891 stack.pop_and_push(1, newDisp, tmp)
893 gobject.timeout_add(200, self.destroyArticle, tmp)
898 if type(self.disp).__name__ == "DisplayArticle":
899 gobject.timeout_add(200, self.destroyArticle, self.disp)
906 if self.key == "ArchivedArticles":
907 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
908 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
909 self.ids.append(self.disp.connect("article-next", self.nextArticle))
910 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
912 def buttonPurgeArticles(self, *widget):
914 self.feed.purgeReadArticles()
915 #self.feed.saveFeed(CONFIGDIR)
918 def destroyArticle(self, handle):
919 handle.destroyWindow()
921 def mark_item_read(self, key):
922 it = self.feedItems.get_iter_first()
923 while it is not None:
924 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
926 title = self.fix_title(self.feed.getTitle(key))
927 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
928 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
930 it = self.feedItems.iter_next(it)
932 def nextArticle(self, object, index):
933 self.mark_item_read(index)
934 id = self.feed.getNextId(index)
935 while id not in self.current and id != index:
936 id = self.feed.getNextId(id)
938 self.button_clicked(object, id, next=True)
940 def previousArticle(self, object, index):
941 self.mark_item_read(index)
942 id = self.feed.getPreviousId(index)
943 while id not in self.current and id != index:
944 id = self.feed.getPreviousId(id)
946 self.button_clicked(object, id, previous=True)
948 def onArticleClosed(self, object, index):
949 selection = self.feedList.get_selection()
950 selection.set_mode(gtk.SELECTION_NONE)
951 self.mark_item_read(index)
953 def onArticleDeleted(self, object, index):
955 self.feed.removeArticle(index)
956 #self.feed.saveFeed(CONFIGDIR)
959 def button_update_clicked(self, button):
960 self.listing.updateFeed (self.key, priority=-1)
962 def show_download_bar(self):
963 if not type(self.downloadDialog).__name__=="DownloadBar":
964 self.downloadDialog = DownloadBar(self.window)
965 self.downloadDialog.connect("download-done", self.onDownloadDone)
966 self.main_vbox.pack_end(self.downloadDialog,
967 expand=False, fill=False)
970 def onDownloadDone(self, widget, feed):
971 if feed == self.feed or feed is None:
972 self.downloadDialog.destroy()
973 self.downloadDialog = False
974 self.feed = self.listing.getFeed(self.key)
976 self.updateDbusHandler.ArticleCountUpdated()
978 def buttonReadAllClicked(self, button):
980 self.feed.markAllAsRead()
981 it = self.feedItems.get_iter_first()
982 while it is not None:
983 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
984 title = self.fix_title(self.feed.getTitle(k))
985 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
986 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
987 it = self.feedItems.iter_next(it)
989 #for index in self.feed.getIds():
990 # self.feed.setEntryRead(index)
991 # self.mark_item_read(index)
997 self.window = hildon.StackableWindow()
998 self.window.set_title(__appname__)
999 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1000 self.mainVbox = gtk.VBox(False,10)
1002 if isfile(CONFIGDIR+"/feeds.db"):
1003 self.introLabel = gtk.Label("Loading...")
1005 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1007 self.mainVbox.pack_start(self.introLabel)
1009 self.window.add(self.mainVbox)
1010 self.window.show_all()
1011 self.config = Config(self.window, CONFIGDIR+"config.ini")
1012 gobject.idle_add(self.createWindow)
1014 # This is set to try when the user interacts with the program.
1015 # If, after an update is complete, we discover that the
1016 # environment variable DBUS_STARTED_ADDRESS is set and
1017 # self.had_interaction is False, we quit.
1018 self.had_interaction = False
1020 def createWindow(self):
1023 self.app_lock = get_lock("app_lock")
1024 if self.app_lock == None:
1026 self.stopButton.set_sensitive(True)
1028 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1029 self.stopButton.set_text("Stop update","")
1030 self.stopButton.connect("clicked", self.stop_running_update)
1031 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1032 self.window.show_all()
1033 self.introLabel.set_label("Update in progress, please wait.")
1034 gobject.timeout_add_seconds(3, self.createWindow)
1037 self.stopButton.destroy()
1040 self.listing = Listing(self.config, CONFIGDIR)
1042 self.downloadDialog = False
1044 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1045 self.orientation.set_mode(self.config.getOrientation())
1046 except Exception, e:
1047 logger.warn("Could not start rotation manager: %s" % str(e))
1049 menu = hildon.AppMenu()
1050 # Create a button and add it to the menu
1051 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1052 button.set_label("Update feeds")
1053 button.connect("clicked", self.button_update_clicked, "All")
1056 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1057 button.set_label("Mark all as read")
1058 button.connect("clicked", self.button_markAll)
1061 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1062 button.set_label("Add new feed")
1063 button.connect("clicked", lambda b: self.addFeed())
1066 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1067 button.set_label("Manage subscriptions")
1068 button.connect("clicked", self.button_organize_clicked)
1071 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1072 button.set_label("Settings")
1073 button.connect("clicked", self.button_preferences_clicked)
1076 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1077 button.set_label("About")
1078 button.connect("clicked", self.button_about_clicked)
1081 self.window.set_app_menu(menu)
1084 #self.feedWindow = hildon.StackableWindow()
1085 #self.articleWindow = hildon.StackableWindow()
1086 self.introLabel.destroy()
1087 self.pannableListing = hildon.PannableArea()
1088 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1089 self.feedList = gtk.TreeView(self.feedItems)
1090 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1091 #self.feedList.set_enable_tree_lines(True)
1092 #self.feedList.set_show_expanders(True)
1093 self.pannableListing.add(self.feedList)
1095 icon_renderer = gtk.CellRendererPixbuf()
1096 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1097 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1099 self.feedList.append_column(icon_column)
1101 markup_renderer = gtk.CellRendererText()
1102 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1103 markup=COLUMN_MARKUP)
1104 self.feedList.append_column(markup_column)
1105 self.mainVbox.pack_start(self.pannableListing)
1106 self.mainVbox.show_all()
1108 self.displayListing()
1109 self.autoupdate = False
1110 self.checkAutoUpdate()
1112 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1113 gobject.idle_add(self.late_init)
1115 def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1116 if (not self.downloadDialog
1117 and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1118 self.updateDbusHandler.UpdateStarted()
1120 self.downloadDialog = DownloadBar(self.window)
1121 self.downloadDialog.connect("download-done", self.onDownloadDone)
1122 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1123 self.mainVbox.show_all()
1125 if self.__dict__.get ('disp', None):
1126 self.disp.show_download_bar ()
1128 def onDownloadDone(self, widget, feed):
1130 self.downloadDialog.destroy()
1131 self.downloadDialog = False
1132 self.displayListing()
1133 self.updateDbusHandler.UpdateFinished()
1134 self.updateDbusHandler.ArticleCountUpdated()
1136 if not self.had_interaction and 'DBUS_STARTER_ADDRESS' in environ:
1138 "Update complete. No interaction, started by dbus: quitting.")
1140 def stop_running_update(self, button):
1141 self.stopButton.set_sensitive(False)
1143 bus=dbus.SessionBus()
1144 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1145 "/org/marcoz/feedingit/update" # Object's path
1147 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1150 def increase_download_parallelism(self):
1151 # The system has been idle for a while. Enable parallel
1153 JobManager().num_threads = 4
1154 gobject.source_remove (self.increase_download_parallelism_id)
1155 del self.increase_download_parallelism_id
1158 def system_inactivity_ind(self, idle):
1159 # The system's idle state changed.
1160 if (self.am_idle and idle) or (not self.am_idle and not idle):
1165 if hasattr (self, 'increase_download_parallelism_id'):
1166 gobject.source_remove (self.increase_download_parallelism_id)
1167 del self.increase_download_parallelism_id
1169 self.increase_download_parallelism_id = \
1170 gobject.timeout_add_seconds(
1171 60, self.increase_download_parallelism)
1174 JobManager().num_threads = 1
1178 def late_init(self):
1179 self.dbusHandler = ServerObject(self)
1180 self.updateDbusHandler = UpdateServerObject(self)
1183 jm.stats_hook_register (self.job_manager_update,
1184 run_in_main_thread=True)
1186 self.am_idle = False
1190 bus = dbus.SystemBus()
1191 proxy = bus.get_object('com.nokia.mce',
1192 '/com/nokia/mce/signal')
1193 iface = dbus.Interface(proxy, 'com.nokia.mce.signal')
1194 iface.connect_to_signal('system_inactivity_ind',
1195 self.system_inactivity_ind)
1197 def button_markAll(self, button):
1198 self.had_interaction = True
1199 for key in self.listing.getListOfFeeds():
1200 feed = self.listing.getFeed(key)
1201 feed.markAllAsRead()
1202 #for id in feed.getIds():
1203 # feed.setEntryRead(id)
1204 self.listing.updateUnread(key)
1205 self.displayListing()
1207 def button_about_clicked(self, button):
1208 self.had_interaction = True
1209 HeAboutDialog.present(self.window, \
1219 def button_export_clicked(self, button):
1220 self.had_interaction = True
1221 opml = ExportOpmlData(self.window, self.listing)
1223 def button_import_clicked(self, button):
1224 self.had_interaction = True
1225 opml = GetOpmlData(self.window)
1226 feeds = opml.getData()
1227 for (title, url) in feeds:
1228 self.listing.addFeed(title, url)
1229 self.displayListing()
1231 def addFeed(self, urlIn="http://"):
1232 self.had_interaction = True
1233 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1236 (title, url, category) = wizard.getData()
1238 self.listing.addFeed(title, url, category=category)
1240 self.displayListing()
1242 def button_organize_clicked(self, button):
1243 self.had_interaction = True
1244 def after_closing():
1245 self.displayListing()
1246 SortList(self.window, self.listing, self, after_closing)
1248 def button_update_clicked(self, button, key):
1249 self.had_interaction = True
1250 for k in self.listing.getListOfFeeds():
1251 self.listing.updateFeed (k)
1252 #self.displayListing()
1254 def onDownloadsDone(self, *widget):
1255 self.downloadDialog.destroy()
1256 self.downloadDialog = False
1257 self.displayListing()
1258 self.updateDbusHandler.UpdateFinished()
1259 self.updateDbusHandler.ArticleCountUpdated()
1261 def button_preferences_clicked(self, button):
1262 self.had_interaction = True
1263 dialog = self.config.createDialog()
1264 dialog.connect("destroy", self.prefsClosed)
1266 def show_confirmation_note(self, parent, title):
1267 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1269 retcode = gtk.Dialog.run(note)
1272 if retcode == gtk.RESPONSE_OK:
1277 def saveExpandedLines(self):
1278 self.expandedLines = []
1279 model = self.feedList.get_model()
1280 model.foreach(self.checkLine)
1282 def checkLine(self, model, path, iter, data = None):
1283 if self.feedList.row_expanded(path):
1284 self.expandedLines.append(path)
1286 def restoreExpandedLines(self):
1287 model = self.feedList.get_model()
1288 model.foreach(self.restoreLine)
1290 def restoreLine(self, model, path, iter, data = None):
1291 if path in self.expandedLines:
1292 self.feedList.expand_row(path, False)
1294 def displayListing(self):
1295 icon_theme = gtk.icon_theme_get_default()
1296 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1297 gtk.ICON_LOOKUP_USE_BUILTIN)
1299 self.saveExpandedLines()
1301 self.feedItems.clear()
1302 hideReadFeed = self.config.getHideReadFeeds()
1303 order = self.config.getFeedSortOrder()
1305 categories = self.listing.getListOfCategories()
1306 if len(categories) > 1:
1307 showCategories = True
1309 showCategories = False
1311 for categoryId in categories:
1313 title = self.listing.getCategoryTitle(categoryId)
1314 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1316 if showCategories and len(keys)>0:
1317 category = self.feedItems.append(None, (None, title, categoryId))
1318 #print "catID" + str(categoryId) + " " + str(self.category)
1319 if categoryId == self.category:
1321 expandedRow = category
1324 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1325 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1326 updateTime = self.listing.getFeedUpdateTime(key)
1328 updateTime = "Never"
1329 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1331 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1333 markup = FEED_TEMPLATE % (title, subtitle)
1336 icon_filename = self.listing.getFavicon(key)
1337 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1338 LIST_ICON_SIZE, LIST_ICON_SIZE)
1340 pixbuf = default_pixbuf
1343 self.feedItems.append(category, (pixbuf, markup, key))
1345 self.feedItems.append(None, (pixbuf, markup, key))
1348 self.restoreExpandedLines()
1351 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1355 def on_feedList_row_activated(self, treeview, path, column):
1356 self.had_interaction = True
1357 model = treeview.get_model()
1358 iter = model.get_iter(path)
1359 key = model.get_value(iter, COLUMN_KEY)
1362 #print "Key: " + str(key)
1364 self.category = catId
1365 if treeview.row_expanded(path):
1366 treeview.collapse_row(path)
1368 # treeview.expand_row(path, True)
1369 #treeview.collapse_all()
1370 #treeview.expand_row(path, False)
1371 #for i in range(len(path)):
1372 # self.feedList.expand_row(path[:i+1], False)
1373 #self.show_confirmation_note(self.window, "Working")
1379 def openFeed(self, key):
1383 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1385 self.feed_lock = get_lock(key)
1386 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1387 self.listing.getFeedTitle(key), key, \
1388 self.config, self.updateDbusHandler)
1389 self.disp.connect("feed-closed", self.onFeedClosed)
1391 def openArticle(self, key, id):
1395 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1397 self.feed_lock = get_lock(key)
1398 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1399 self.listing.getFeedTitle(key), key, \
1400 self.config, self.updateDbusHandler)
1401 self.disp.button_clicked(None, id)
1402 self.disp.connect("feed-closed", self.onFeedClosed)
1405 def onFeedClosed(self, object, key):
1406 #self.listing.saveConfig()
1408 gobject.idle_add(self.onFeedClosedTimeout)
1409 self.displayListing()
1410 #self.updateDbusHandler.ArticleCountUpdated()
1412 def onFeedClosedTimeout(self):
1414 self.updateDbusHandler.ArticleCountUpdated()
1416 def quit(self, *args):
1419 if hasattr (self, 'app_lock'):
1422 # Wait until all slave threads have properly exited before
1423 # terminating the mainloop.
1427 if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1430 gobject.timeout_add(500, self.quit)
1435 self.window.connect("destroy", self.quit)
1438 def prefsClosed(self, *widget):
1440 self.orientation.set_mode(self.config.getOrientation())
1443 self.displayListing()
1444 self.checkAutoUpdate()
1446 def checkAutoUpdate(self, *widget):
1447 interval = int(self.config.getUpdateInterval()*3600000)
1448 if self.config.isAutoUpdateEnabled():
1449 if self.autoupdate == False:
1450 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1451 self.autoupdate = interval
1452 elif not self.autoupdate == interval:
1453 # If auto-update is enabled, but not at the right frequency
1454 gobject.source_remove(self.autoupdateId)
1455 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1456 self.autoupdate = interval
1458 if not self.autoupdate == False:
1459 gobject.source_remove(self.autoupdateId)
1460 self.autoupdate = False
1462 def automaticUpdate(self, *widget):
1463 # Need to check for internet connection
1464 # If no internet connection, try again in 10 minutes:
1465 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1466 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1467 #from time import localtime, strftime
1468 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1470 self.button_update_clicked(None, None)
1473 def stopUpdate(self):
1474 # Not implemented in the app (see update_feeds.py)
1476 JobManager().cancel ()
1480 def getStatus(self):
1482 for key in self.listing.getListOfFeeds():
1483 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1484 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1486 status = "No unread items"
1489 if __name__ == "__main__":
1491 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1493 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1494 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1495 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1496 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1497 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1498 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1499 gobject.threads_init()
1500 if not isdir(CONFIGDIR):
1504 logger.error("Error: Can't create configuration directory")
1505 from sys import exit