1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__ = 'Yves Marcoz'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
27 from pango import FontDescription
32 from webkit import WebView
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
51 from urllib2 import install_opener, build_opener
53 from socket import setdefaulttimeout
55 setdefaulttimeout(timeout)
61 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
62 ABOUT_ICON = 'feedingit'
63 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
64 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
65 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
66 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
68 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
69 unread_color = color_style.lookup_color('ActiveTextColor')
70 read_color = color_style.lookup_color('DefaultTextColor')
73 CONFIGDIR="/home/user/.feedingit/"
74 LOCK = CONFIGDIR + "update.lock"
77 from htmlentitydefs import name2codepoint
79 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
81 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
85 MARKUP_TEMPLATE = '<span font_desc="%s" foreground="%s">%%s</span>'
87 # Build the markup template for the Maemo 5 text style
88 head_font = style.get_font_desc('SystemFont')
89 sub_font = style.get_font_desc('SmallSystemFont')
91 head_color = style.get_color('ButtonTextColor')
92 sub_color = style.get_color('DefaultTextColor')
93 active_color = style.get_color('ActiveTextColor')
95 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
96 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
98 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
99 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
101 FEED_TEMPLATE = '\n'.join((head, normal_sub))
102 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
104 ENTRY_TEMPLATE = head
105 ENTRY_TEMPLATE_UNREAD = active_head
108 # Removes HTML or XML character references and entities from a text string.
110 # @param text The HTML (or XML) source text.
111 # @return The plain text, as a Unicode string, if necessary.
112 # http://effbot.org/zone/re-sub.htm#unescape-html
117 # character reference
119 if text[:3] == "&#x":
120 return unichr(int(text[3:-1], 16))
122 return unichr(int(text[2:-1]))
128 text = unichr(name2codepoint[text[1:-1]])
131 return text # leave as is
132 return sub("&#?\w+;", fixup, text)
135 class AddWidgetWizard(hildon.WizardDialog):
137 def __init__(self, parent, urlIn, titleIn=None):
139 self.notebook = gtk.Notebook()
141 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
142 self.nameEntry.set_placeholder("Enter Feed Name")
143 vbox = gtk.VBox(False,10)
144 label = gtk.Label("Enter Feed Name:")
145 vbox.pack_start(label)
146 vbox.pack_start(self.nameEntry)
147 if not titleIn == None:
148 self.nameEntry.set_text(titleIn)
149 self.notebook.append_page(vbox, None)
151 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
152 self.urlEntry.set_placeholder("Enter a URL")
153 self.urlEntry.set_text(urlIn)
154 self.urlEntry.select_region(0,-1)
156 vbox = gtk.VBox(False,10)
157 label = gtk.Label("Enter Feed URL:")
158 vbox.pack_start(label)
159 vbox.pack_start(self.urlEntry)
160 self.notebook.append_page(vbox, None)
162 labelEnd = gtk.Label("Success")
164 self.notebook.append_page(labelEnd, None)
166 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
168 # Set a handler for "switch-page" signal
169 #self.notebook.connect("switch_page", self.on_page_switch, self)
171 # Set a function to decide if user can go to next page
172 self.set_forward_page_func(self.some_page_func)
177 return (self.nameEntry.get_text(), self.urlEntry.get_text())
179 def on_page_switch(self, notebook, page, num, dialog):
182 def some_page_func(self, nb, current, userdata):
183 # Validate data for 1st page
185 return len(self.nameEntry.get_text()) != 0
187 # Check the url is not null, and starts with http
188 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
194 class Download(Thread):
195 def __init__(self, listing, key, config):
196 Thread.__init__(self)
197 self.listing = listing
202 (use_proxy, proxy) = self.config.getProxy()
203 key_lock = get_lock(self.key)
206 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
208 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
212 class DownloadBar(gtk.ProgressBar):
213 def __init__(self, parent, listing, listOfKeys, config, single=False):
215 update_lock = get_lock("update_lock")
216 if update_lock != None:
217 gtk.ProgressBar.__init__(self)
218 self.listOfKeys = listOfKeys[:]
219 self.listing = listing
220 self.total = len(self.listOfKeys)
224 (use_proxy, proxy) = self.config.getProxy()
226 opener = build_opener(proxy)
227 opener.addheaders = [('User-agent', USER_AGENT)]
228 install_opener(opener)
230 opener = build_opener()
231 opener.addheaders = [('User-agent', USER_AGENT)]
232 install_opener(opener)
235 self.set_text("Updating...")
237 self.set_fraction(self.fraction)
240 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
242 def update_progress_bar(self):
243 #self.progress_bar.pulse()
244 if activeCount() < 4:
245 x = activeCount() - 1
246 k = len(self.listOfKeys)
247 fin = self.total - k - x
248 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
249 #print x, k, fin, fraction
250 self.set_fraction(fraction)
252 if len(self.listOfKeys)>0:
253 self.current = self.current+1
254 key = self.listOfKeys.pop()
255 #if self.single == True:
256 # Check if the feed is being displayed
257 download = Download(self.listing, key, self.config)
260 elif activeCount() > 1:
263 #self.waitingWindow.destroy()
269 self.emit("download-done", "success")
274 class SortList(gtk.Dialog):
275 def __init__(self, parent, listing):
276 gtk.Dialog.__init__(self, "Organizer", parent)
277 self.listing = listing
279 self.vbox2 = gtk.VBox(False, 10)
281 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
282 button.set_label("Move Up")
283 button.connect("clicked", self.buttonUp)
284 self.vbox2.pack_start(button, expand=False, fill=False)
286 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
287 button.set_label("Move Down")
288 button.connect("clicked", self.buttonDown)
289 self.vbox2.pack_start(button, expand=False, fill=False)
291 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
292 button.set_label("Add Feed")
293 button.connect("clicked", self.buttonAdd)
294 self.vbox2.pack_start(button, expand=False, fill=False)
296 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
297 button.set_label("Edit Feed")
298 button.connect("clicked", self.buttonEdit)
299 self.vbox2.pack_start(button, expand=False, fill=False)
301 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
302 button.set_label("Delete")
303 button.connect("clicked", self.buttonDelete)
304 self.vbox2.pack_start(button, expand=False, fill=False)
306 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
307 #button.set_label("Done")
308 #button.connect("clicked", self.buttonDone)
309 #self.vbox.pack_start(button)
310 self.hbox2= gtk.HBox(False, 10)
311 self.pannableArea = hildon.PannableArea()
312 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
313 self.treeview = gtk.TreeView(self.treestore)
314 self.hbox2.pack_start(self.pannableArea, expand=True)
316 self.hbox2.pack_end(self.vbox2, expand=False)
317 self.set_default_size(-1, 600)
318 self.vbox.pack_start(self.hbox2)
321 #self.connect("destroy", self.buttonDone)
323 def displayFeeds(self):
324 self.treeview.destroy()
325 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
326 self.treeview = gtk.TreeView()
328 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
329 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
331 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
333 self.pannableArea.add(self.treeview)
337 def refreshList(self, selected=None, offset=0):
338 #rect = self.treeview.get_visible_rect()
339 #y = rect.y+rect.height
340 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
341 for key in self.listing.getListOfFeeds():
342 item = self.treestore.append([self.listing.getFeedTitle(key), key])
345 self.treeview.set_model(self.treestore)
346 if not selected == None:
347 self.treeview.get_selection().select_iter(selectedItem)
348 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
349 self.pannableArea.show_all()
351 def getSelectedItem(self):
352 (model, iter) = self.treeview.get_selection().get_selected()
355 return model.get_value(iter, 1)
357 def findIndex(self, key):
361 for row in self.treestore:
363 return (before, row.iter)
364 if key == list(row)[0]:
368 return (before, None)
370 def buttonUp(self, button):
371 key = self.getSelectedItem()
373 self.listing.moveUp(key)
374 self.refreshList(key, -10)
376 def buttonDown(self, button):
377 key = self.getSelectedItem()
379 self.listing.moveDown(key)
380 self.refreshList(key, 10)
382 def buttonDelete(self, button):
383 key = self.getSelectedItem()
385 self.listing.removeFeed(key)
388 def buttonEdit(self, button):
389 key = self.getSelectedItem()
391 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
394 (title, url) = wizard.getData()
395 if (not title == '') and (not url == ''):
396 self.listing.editFeed(key, title, url)
400 def buttonDone(self, *args):
403 def buttonAdd(self, button, urlIn="http://"):
404 wizard = AddWidgetWizard(self, urlIn)
407 (title, url) = wizard.getData()
408 if (not title == '') and (not url == ''):
409 self.listing.addFeed(title, url)
414 class DisplayArticle(hildon.StackableWindow):
415 def __init__(self, feed, id, key, config, listing):
416 hildon.StackableWindow.__init__(self)
417 #self.imageDownloader = ImageDownloader()
422 #self.set_title(feed.getTitle(id))
423 self.set_title(self.listing.getFeedTitle(key))
425 self.set_for_removal = False
427 # Init the article display
428 #if self.config.getWebkitSupport():
429 self.view = WebView()
430 #self.view.set_editable(False)
433 # self.view = gtkhtml2.View()
434 # self.document = gtkhtml2.Document()
435 # self.view.set_document(self.document)
436 # self.document.connect("link_clicked", self._signal_link_clicked)
437 self.pannable_article = hildon.PannableArea()
438 self.pannable_article.add(self.view)
439 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
440 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
442 #if self.config.getWebkitSupport():
443 contentLink = self.feed.getContentLink(self.id)
444 self.feed.setEntryRead(self.id)
445 #if key=="ArchivedArticles":
446 if contentLink.startswith("/home/user/"):
447 self.view.open("file://" + contentLink)
449 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, "text/html", "utf-8", self.contentLink)
450 self.view.connect("motion-notify-event", lambda w,ev: True)
451 self.view.connect('load-started', self.load_started)
452 self.view.connect('load-finished', self.load_finished)
455 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
456 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
458 # if not key == "ArchivedArticles":
459 # Do not download images if the feed is "Archived Articles"
460 # self.document.connect("request-url", self._signal_request_url)
462 # self.document.clear()
463 # self.document.open_stream("text/html")
464 # self.document.write_stream(self.text)
465 # self.document.close_stream()
467 menu = hildon.AppMenu()
468 # Create a button and add it to the menu
469 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
470 button.set_label("Allow Horizontal Scrolling")
471 button.connect("clicked", self.horiz_scrolling_button)
474 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
475 button.set_label("Open in Browser")
476 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
479 if key == "ArchivedArticles":
480 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
481 button.set_label("Remove from Archived Articles")
482 button.connect("clicked", self.remove_archive_button)
484 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
485 button.set_label("Add to Archived Articles")
486 button.connect("clicked", self.archive_button)
489 self.set_app_menu(menu)
492 #self.event_box = gtk.EventBox()
493 #self.event_box.add(self.pannable_article)
494 self.add(self.pannable_article)
497 self.pannable_article.show_all()
499 self.destroyId = self.connect("destroy", self.destroyWindow)
501 self.view.connect("button_press_event", self.button_pressed)
502 self.gestureId = self.view.connect("button_release_event", self.button_released)
503 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
505 def load_started(self, *widget):
506 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
508 def load_finished(self, *widget):
509 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
511 def button_pressed(self, window, event):
512 #print event.x, event.y
513 self.coords = (event.x, event.y)
515 def button_released(self, window, event):
516 x = self.coords[0] - event.x
517 y = self.coords[1] - event.y
519 if (2*abs(y) < abs(x)):
521 self.emit("article-previous", self.id)
523 self.emit("article-next", self.id)
527 #def gesture(self, widget, direction, startx, starty):
528 # if (direction == 3):
529 # self.emit("article-next", self.index)
530 # if (direction == 2):
531 # self.emit("article-previous", self.index)
532 #print startx, starty
533 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
535 def destroyWindow(self, *args):
536 self.disconnect(self.destroyId)
537 if self.set_for_removal:
538 self.emit("article-deleted", self.id)
540 self.emit("article-closed", self.id)
541 #self.imageDownloader.stopAll()
544 def horiz_scrolling_button(self, *widget):
545 self.pannable_article.disconnect(self.gestureId)
546 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
548 def archive_button(self, *widget):
549 # Call the listing.addArchivedArticle
550 self.listing.addArchivedArticle(self.key, self.id)
552 def remove_archive_button(self, *widget):
553 self.set_for_removal = True
555 #def reloadArticle(self, *widget):
556 # if threading.activeCount() > 1:
557 # Image thread are still running, come back in a bit
560 # for (stream, imageThread) in self.images:
562 # stream.write(imageThread.data)
567 def _signal_link_clicked(self, object, link):
569 bus = dbus.SessionBus()
570 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
571 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
572 iface.open_new_window(link)
574 #def _signal_request_url(self, object, url, stream):
576 # self.imageDownloader.queueImage(url, stream)
577 #imageThread = GetImage(url)
579 #self.images.append((stream, imageThread))
582 class DisplayFeed(hildon.StackableWindow):
583 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
584 hildon.StackableWindow.__init__(self)
585 self.listing = listing
587 self.feedTitle = title
588 self.set_title(title)
591 self.updateDbusHandler = updateDbusHandler
593 self.downloadDialog = False
595 #self.listing.setCurrentlyDisplayedFeed(self.key)
599 menu = hildon.AppMenu()
600 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
601 button.set_label("Update Feed")
602 button.connect("clicked", self.button_update_clicked)
605 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
606 button.set_label("Mark All As Read")
607 button.connect("clicked", self.buttonReadAllClicked)
610 if key=="ArchivedArticles":
611 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
612 button.set_label("Purge Read Articles")
613 button.connect("clicked", self.buttonPurgeArticles)
616 self.set_app_menu(menu)
621 self.connect('configure-event', self.on_configure_event)
622 self.connect("destroy", self.destroyWindow)
624 def on_configure_event(self, window, event):
625 if getattr(self, 'markup_renderer', None) is None:
628 # Fix up the column width for wrapping the text when the window is
629 # resized (i.e. orientation changed)
630 self.markup_renderer.set_property('wrap-width', event.width-10)
632 def destroyWindow(self, *args):
633 #self.feed.saveUnread(CONFIGDIR)
634 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
635 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
636 self.emit("feed-closed", self.key)
638 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
639 #self.listing.closeCurrentlyDisplayedFeed()
641 def fix_title(self, title):
642 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
644 def displayFeed(self):
645 self.pannableFeed = hildon.PannableArea()
647 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
649 self.feedItems = gtk.ListStore(str, str)
650 self.feedList = gtk.TreeView(self.feedItems)
651 self.feedList.connect('row-activated', self.on_feedList_row_activated)
652 self.pannableFeed.add(self.feedList)
654 self.markup_renderer = gtk.CellRendererText()
655 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
656 self.markup_renderer.set_property('wrap-width', 780)
657 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
658 markup=FEED_COLUMN_MARKUP)
659 self.feedList.append_column(markup_column)
661 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
663 for id in self.feed.getIds():
664 if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
665 #title = self.feed.getTitle(id)
666 title = self.fix_title(self.feed.getTitle(id))
668 if self.feed.isEntryRead(id):
669 markup = ENTRY_TEMPLATE % title
671 markup = ENTRY_TEMPLATE_UNREAD % title
673 self.feedItems.append((markup, id))
675 self.add(self.pannableFeed)
679 self.pannableFeed.destroy()
680 #self.remove(self.pannableFeed)
682 def on_feedList_row_activated(self, treeview, path, column):
683 model = treeview.get_model()
684 iter = model.get_iter(path)
685 key = model.get_value(iter, FEED_COLUMN_KEY)
686 # Emulate legacy "button_clicked" call via treeview
687 self.button_clicked(treeview, key)
689 def button_clicked(self, button, index, previous=False, next=False):
690 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
691 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
692 stack = hildon.WindowStack.get_default()
695 stack.pop_and_push(1, newDisp, tmp)
697 gobject.timeout_add(200, self.destroyArticle, tmp)
702 if type(self.disp).__name__ == "DisplayArticle":
703 gobject.timeout_add(200, self.destroyArticle, self.disp)
710 if self.key == "ArchivedArticles":
711 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
712 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
713 self.ids.append(self.disp.connect("article-next", self.nextArticle))
714 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
716 def buttonPurgeArticles(self, *widget):
718 self.feed.purgeReadArticles()
719 self.feed.saveUnread(CONFIGDIR)
720 self.feed.saveFeed(CONFIGDIR)
723 def destroyArticle(self, handle):
724 handle.destroyWindow()
726 def mark_item_read(self, key):
727 it = self.feedItems.get_iter_first()
728 while it is not None:
729 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
731 title = self.fix_title(self.feed.getTitle(key))
732 markup = ENTRY_TEMPLATE % title
733 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
735 it = self.feedItems.iter_next(it)
737 def nextArticle(self, object, index):
738 self.mark_item_read(index)
739 id = self.feed.getNextId(index)
740 self.button_clicked(object, id, next=True)
742 def previousArticle(self, object, index):
743 self.mark_item_read(index)
744 id = self.feed.getPreviousId(index)
745 self.button_clicked(object, id, previous=True)
747 def onArticleClosed(self, object, index):
748 self.mark_item_read(index)
750 def onArticleDeleted(self, object, index):
752 self.feed.removeArticle(index)
753 self.feed.saveUnread(CONFIGDIR)
754 self.feed.saveFeed(CONFIGDIR)
757 def button_update_clicked(self, button):
758 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
759 if not type(self.downloadDialog).__name__=="DownloadBar":
760 self.pannableFeed.destroy()
761 self.vbox = gtk.VBox(False, 10)
762 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
763 self.downloadDialog.connect("download-done", self.onDownloadsDone)
764 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
768 def onDownloadsDone(self, *widget):
770 self.feed = self.listing.getFeed(self.key)
772 self.updateDbusHandler.ArticleCountUpdated()
774 def buttonReadAllClicked(self, button):
775 for index in self.feed.getIds():
776 self.feed.setEntryRead(index)
777 self.mark_item_read(index)
783 self.window = hildon.StackableWindow()
784 self.window.set_title(__appname__)
785 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
786 self.mainVbox = gtk.VBox(False,10)
788 self.pannableListing = hildon.PannableArea()
789 self.mainVbox.pack_start(self.pannableListing)
791 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
792 self.feedList = gtk.TreeView(self.feedItems)
793 self.feedList.connect('row-activated', self.on_feedList_row_activated)
794 self.pannableListing.add(self.feedList)
796 icon_renderer = gtk.CellRendererPixbuf()
797 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
798 icon_column = gtk.TreeViewColumn('', icon_renderer, \
800 self.feedList.append_column(icon_column)
802 markup_renderer = gtk.CellRendererText()
803 markup_column = gtk.TreeViewColumn('', markup_renderer, \
804 markup=COLUMN_MARKUP)
805 self.feedList.append_column(markup_column)
807 self.window.add(self.mainVbox)
808 self.window.show_all()
809 self.config = Config(self.window, CONFIGDIR+"config.ini")
810 gobject.idle_add(self.createWindow)
812 def createWindow(self):
813 self.app_lock = get_lock("app_lock")
814 if self.app_lock == None:
815 self.pannableListing.set_label("Update in progress, please wait.")
816 gobject.timeout_add_seconds(3, self.createWindow)
818 self.listing = Listing(CONFIGDIR)
820 self.downloadDialog = False
821 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
822 self.orientation.set_mode(self.config.getOrientation())
824 menu = hildon.AppMenu()
825 # Create a button and add it to the menu
826 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
827 button.set_label("Update All Feeds")
828 button.connect("clicked", self.button_update_clicked, "All")
831 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
832 button.set_label("Mark All As Read")
833 button.connect("clicked", self.button_markAll)
836 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
837 button.set_label("Organize Feeds")
838 button.connect("clicked", self.button_organize_clicked)
841 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
842 button.set_label("Preferences")
843 button.connect("clicked", self.button_preferences_clicked)
846 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
847 button.set_label("Import Feeds")
848 button.connect("clicked", self.button_import_clicked)
851 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
852 button.set_label("Export Feeds")
853 button.connect("clicked", self.button_export_clicked)
856 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
857 button.set_label("About")
858 button.connect("clicked", self.button_about_clicked)
861 self.window.set_app_menu(menu)
864 self.feedWindow = hildon.StackableWindow()
865 self.articleWindow = hildon.StackableWindow()
867 self.displayListing()
868 self.autoupdate = False
869 self.checkAutoUpdate()
870 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
871 gobject.idle_add(self.enableDbus)
873 def enableDbus(self):
874 self.dbusHandler = ServerObject(self)
875 self.updateDbusHandler = UpdateServerObject(self)
877 def button_markAll(self, button):
878 for key in self.listing.getListOfFeeds():
879 feed = self.listing.getFeed(key)
880 for id in feed.getIds():
881 feed.setEntryRead(id)
882 feed.saveUnread(CONFIGDIR)
883 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
884 self.displayListing()
886 def button_about_clicked(self, button):
887 HeAboutDialog.present(self.window, \
897 def button_export_clicked(self, button):
898 opml = ExportOpmlData(self.window, self.listing)
900 def button_import_clicked(self, button):
901 opml = GetOpmlData(self.window)
902 feeds = opml.getData()
903 for (title, url) in feeds:
904 self.listing.addFeed(title, url)
905 self.displayListing()
907 def addFeed(self, urlIn="http://"):
908 wizard = AddWidgetWizard(self.window, urlIn)
911 (title, url) = wizard.getData()
912 if (not title == '') and (not url == ''):
913 self.listing.addFeed(title, url)
915 self.displayListing()
917 def button_organize_clicked(self, button):
918 org = SortList(self.window, self.listing)
921 self.listing.saveConfig()
922 self.displayListing()
924 def button_update_clicked(self, button, key):
925 if not type(self.downloadDialog).__name__=="DownloadBar":
926 self.updateDbusHandler.UpdateStarted()
927 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
928 self.downloadDialog.connect("download-done", self.onDownloadsDone)
929 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
930 self.mainVbox.show_all()
931 #self.displayListing()
933 def onDownloadsDone(self, *widget):
934 self.downloadDialog.destroy()
935 self.downloadDialog = False
936 self.displayListing()
937 self.updateDbusHandler.UpdateFinished()
938 self.updateDbusHandler.ArticleCountUpdated()
940 def button_preferences_clicked(self, button):
941 dialog = self.config.createDialog()
942 dialog.connect("destroy", self.prefsClosed)
944 def show_confirmation_note(self, parent, title):
945 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
947 retcode = gtk.Dialog.run(note)
950 if retcode == gtk.RESPONSE_OK:
955 def displayListing(self):
956 icon_theme = gtk.icon_theme_get_default()
957 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
958 gtk.ICON_LOOKUP_USE_BUILTIN)
960 self.feedItems.clear()
961 for key in self.listing.getListOfFeeds():
962 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
963 if unreadItems > 0 or not self.config.getHideReadFeeds():
964 title = self.listing.getFeedTitle(key)
965 updateTime = self.listing.getFeedUpdateTime(key)
967 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
970 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
972 markup = FEED_TEMPLATE % (title, subtitle)
975 icon_filename = self.listing.getFavicon(key)
976 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
977 LIST_ICON_SIZE, LIST_ICON_SIZE)
979 pixbuf = default_pixbuf
981 self.feedItems.append((pixbuf, markup, key))
983 def on_feedList_row_activated(self, treeview, path, column):
984 model = treeview.get_model()
985 iter = model.get_iter(path)
986 key = model.get_value(iter, COLUMN_KEY)
991 # If feed_lock doesn't exist, we can open the feed, else we do nothing
992 self.feed_lock = get_lock(key)
993 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
994 self.listing.getFeedTitle(key), key, \
995 self.config, self.updateDbusHandler)
996 self.disp.connect("feed-closed", self.onFeedClosed)
998 def onFeedClosed(self, object, key):
999 #self.listing.saveConfig()
1001 gobject.idle_add(self.onFeedClosedTimeout)
1002 self.displayListing()
1003 #self.updateDbusHandler.ArticleCountUpdated()
1005 def onFeedClosedTimeout(self):
1006 self.listing.saveConfig()
1008 self.updateDbusHandler.ArticleCountUpdated()
1011 self.window.connect("destroy", gtk.main_quit)
1013 self.listing.saveConfig()
1016 def prefsClosed(self, *widget):
1017 self.orientation.set_mode(self.config.getOrientation())
1018 self.displayListing()
1019 self.checkAutoUpdate()
1021 def checkAutoUpdate(self, *widget):
1022 interval = int(self.config.getUpdateInterval()*3600000)
1023 if self.config.isAutoUpdateEnabled():
1024 if self.autoupdate == False:
1025 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1026 self.autoupdate = interval
1027 elif not self.autoupdate == interval:
1028 # If auto-update is enabled, but not at the right frequency
1029 gobject.source_remove(self.autoupdateId)
1030 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1031 self.autoupdate = interval
1033 if not self.autoupdate == False:
1034 gobject.source_remove(self.autoupdateId)
1035 self.autoupdate = False
1037 def automaticUpdate(self, *widget):
1038 # Need to check for internet connection
1039 # If no internet connection, try again in 10 minutes:
1040 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1041 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1042 #from time import localtime, strftime
1043 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1045 self.button_update_clicked(None, None)
1048 def stopUpdate(self):
1049 # Not implemented in the app (see update_feeds.py)
1051 self.downloadDialog.listOfKeys = []
1055 def getStatus(self):
1057 for key in self.listing.getListOfFeeds():
1058 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1059 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1061 status = "No unread items"
1064 if __name__ == "__main__":
1065 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1066 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1067 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1068 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1069 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1070 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1071 gobject.threads_init()
1072 if not isdir(CONFIGDIR):
1076 print "Error: Can't create configuration directory"
1077 from sys import exit