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 # ============================================================================
21 # Author : Yves Marcoz
23 # Description : Simple RSS Reader
24 # ============================================================================
34 from os.path import isfile, isdir
39 from portrait import FremantleRotation
42 from feedingitdbus import ServerObject
43 from config import Config
46 from opml import GetOpmlData, ExportOpmlData
50 socket.setdefaulttimeout(timeout)
52 CONFIGDIR="/home/user/.feedingit/"
54 class AddWidgetWizard(hildon.WizardDialog):
56 def __init__(self, parent, urlIn):
58 self.notebook = gtk.Notebook()
60 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
61 self.nameEntry.set_placeholder("Enter Feed Name")
62 vbox = gtk.VBox(False,10)
63 label = gtk.Label("Enter Feed Name:")
64 vbox.pack_start(label)
65 vbox.pack_start(self.nameEntry)
66 self.notebook.append_page(vbox, None)
68 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
69 self.urlEntry.set_placeholder("Enter a URL")
70 self.urlEntry.set_text(urlIn)
71 self.urlEntry.select_region(0,-1)
73 vbox = gtk.VBox(False,10)
74 label = gtk.Label("Enter Feed URL:")
75 vbox.pack_start(label)
76 vbox.pack_start(self.urlEntry)
77 self.notebook.append_page(vbox, None)
79 labelEnd = gtk.Label("Success")
81 self.notebook.append_page(labelEnd, None)
83 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
85 # Set a handler for "switch-page" signal
86 #self.notebook.connect("switch_page", self.on_page_switch, self)
88 # Set a function to decide if user can go to next page
89 self.set_forward_page_func(self.some_page_func)
94 return (self.nameEntry.get_text(), self.urlEntry.get_text())
96 def on_page_switch(self, notebook, page, num, dialog):
99 def some_page_func(self, nb, current, userdata):
100 # Validate data for 1st page
102 return len(self.nameEntry.get_text()) != 0
104 # Check the url is not null, and starts with http
105 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
111 class GetImage(threading.Thread):
112 def __init__(self, url, stream):
113 threading.Thread.__init__(self)
118 f = urllib2.urlopen(self.url)
121 self.stream.write(data)
124 class ImageDownloader():
127 self.downloading = False
129 def queueImage(self, url, stream):
130 self.images.append((url, stream))
131 if not self.downloading:
132 self.downloading = True
133 gobject.timeout_add(50, self.checkQueue)
135 def checkQueue(self):
136 for i in range(4-threading.activeCount()):
137 if len(self.images) > 0:
138 (url, stream) = self.images.pop()
139 GetImage(url, stream).start()
140 if len(self.images)>0:
141 gobject.timeout_add(200, self.checkQueue)
143 self.downloading=False
149 class Download(threading.Thread):
150 def __init__(self, listing, key, config):
151 threading.Thread.__init__(self)
152 self.listing = listing
157 self.listing.updateFeed(self.key, self.config.getExpiry())
160 class DownloadDialog():
161 def __init__(self, parent, listing, listOfKeys, config):
162 self.listOfKeys = listOfKeys[:]
163 self.listing = listing
164 self.total = len(self.listOfKeys)
169 self.progress = gtk.ProgressBar()
170 self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
171 progressbar=self.progress)
172 self.progress.set_text("Downloading")
174 self.progress.set_fraction(self.fraction)
176 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
177 self.waitingWindow.show_all()
178 response = self.waitingWindow.run()
180 while threading.activeCount() > 1:
181 # Wait for current downloads to finish
183 self.waitingWindow.destroy()
185 def update_progress_bar(self):
186 #self.progress_bar.pulse()
187 if threading.activeCount() < 4:
188 x = threading.activeCount() - 1
189 k = len(self.listOfKeys)
190 fin = self.total - k - x
191 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
192 #print x, k, fin, fraction
193 self.progress.set_fraction(fraction)
195 if len(self.listOfKeys)>0:
196 self.current = self.current+1
197 key = self.listOfKeys.pop()
198 download = Download(self.listing, key, self.config)
201 elif threading.activeCount() > 1:
204 self.waitingWindow.destroy()
209 class SortList(gtk.Dialog):
210 def __init__(self, parent, listing):
211 gtk.Dialog.__init__(self, "Organizer", parent)
212 self.listing = listing
214 self.vbox2 = gtk.VBox(False, 10)
216 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
217 button.set_label("Move Up")
218 button.connect("clicked", self.buttonUp)
219 self.vbox2.pack_start(button, expand=False, fill=False)
221 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
222 button.set_label("Move Down")
223 button.connect("clicked", self.buttonDown)
224 self.vbox2.pack_start(button, expand=False, fill=False)
226 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
227 button.set_label("Delete")
228 button.connect("clicked", self.buttonDelete)
229 self.vbox2.pack_start(button, expand=False, fill=False)
231 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
232 #button.set_label("Done")
233 #button.connect("clicked", self.buttonDone)
234 #self.vbox.pack_start(button)
235 self.hbox2= gtk.HBox(False, 10)
236 self.pannableArea = hildon.PannableArea()
237 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
238 self.treeview = gtk.TreeView(self.treestore)
239 self.hbox2.pack_start(self.pannableArea, expand=True)
241 self.hbox2.pack_end(self.vbox2, expand=False)
242 self.set_default_size(-1, 600)
243 self.vbox.pack_start(self.hbox2)
246 #self.connect("destroy", self.buttonDone)
248 def displayFeeds(self):
249 self.treeview.destroy()
250 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
251 self.treeview = gtk.TreeView()
253 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
254 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
256 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
258 self.pannableArea.add(self.treeview)
262 def refreshList(self, selected=None, offset=0):
263 rect = self.treeview.get_visible_rect()
264 y = rect.y+rect.height
265 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
266 for key in self.listing.getListOfFeeds():
267 item = self.treestore.append([self.listing.getFeedTitle(key), key])
270 self.treeview.set_model(self.treestore)
271 if not selected == None:
272 self.treeview.get_selection().select_iter(selectedItem)
273 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
274 self.pannableArea.show_all()
276 def getSelectedItem(self):
277 (model, iter) = self.treeview.get_selection().get_selected()
280 return model.get_value(iter, 1)
282 def findIndex(self, key):
286 for row in self.treestore:
288 return (before, row.iter)
289 if key == list(row)[0]:
293 return (before, None)
295 def buttonUp(self, button):
296 key = self.getSelectedItem()
298 self.listing.moveUp(key)
299 self.refreshList(key, -10)
301 def buttonDown(self, button):
302 key = self.getSelectedItem()
304 self.listing.moveDown(key)
305 self.refreshList(key, 10)
307 def buttonDelete(self, button):
308 key = self.getSelectedItem()
310 self.listing.removeFeed(key)
313 def buttonDone(self, *args):
317 class DisplayArticle(hildon.StackableWindow):
318 def __init__(self, title, text, link, index, key, listing):
319 hildon.StackableWindow.__init__(self)
320 self.imageDownloader = ImageDownloader()
326 self.set_title(title)
329 # Init the article display
330 self.view = gtkhtml2.View()
331 self.pannable_article = hildon.PannableArea()
332 self.pannable_article.add(self.view)
333 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
334 self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
335 self.document = gtkhtml2.Document()
336 self.view.set_document(self.document)
337 self.document.connect("link_clicked", self._signal_link_clicked)
338 self.document.connect("request-url", self._signal_request_url)
339 self.document.clear()
340 self.document.open_stream("text/html")
341 self.document.write_stream(self.text)
342 self.document.close_stream()
344 menu = hildon.AppMenu()
345 # Create a button and add it to the menu
346 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
347 button.set_label("Allow Horizontal Scrolling")
348 button.connect("clicked", self.horiz_scrolling_button)
351 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
352 button.set_label("Open in Browser")
353 button.connect("clicked", self._signal_link_clicked, self.link)
356 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
357 button.set_label("Add to Archived Articles")
358 button.connect("clicked", self.archive_button)
361 self.set_app_menu(menu)
364 self.add(self.pannable_article)
366 self.pannable_article.show_all()
368 self.destroyId = self.connect("destroy", self.destroyWindow)
369 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
371 def gesture(self, widget, direction, startx, starty):
373 self.emit("article-next", self.index)
375 self.emit("article-previous", self.index)
376 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
378 def destroyWindow(self, *args):
379 self.disconnect(self.destroyId)
380 self.emit("article-closed", self.index)
381 self.imageDownloader.stopAll()
384 def horiz_scrolling_button(self, *widget):
385 self.pannable_article.disconnect(self.gestureId)
386 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
388 def archive_button(self, *widget):
389 # Call the listing.addArchivedArticle
390 self.listing.addArchivedArticle(self.key, self.index)
392 #def reloadArticle(self, *widget):
393 # if threading.activeCount() > 1:
394 # Image thread are still running, come back in a bit
397 # for (stream, imageThread) in self.images:
399 # stream.write(imageThread.data)
404 def _signal_link_clicked(self, object, link):
405 bus = dbus.SystemBus()
406 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
407 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
408 iface.open_new_window(link)
410 def _signal_request_url(self, object, url, stream):
412 self.imageDownloader.queueImage(url, stream)
413 #imageThread = GetImage(url)
415 #self.images.append((stream, imageThread))
418 class DisplayFeed(hildon.StackableWindow):
419 def __init__(self, listing, feed, title, key, config):
420 hildon.StackableWindow.__init__(self)
421 self.listing = listing
423 self.feedTitle = title
424 self.set_title(title)
430 menu = hildon.AppMenu()
431 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
432 button.set_label("Update Feed")
433 button.connect("clicked", self.button_update_clicked)
436 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
437 button.set_label("Mark All As Read")
438 button.connect("clicked", self.buttonReadAllClicked)
440 self.set_app_menu(menu)
445 self.connect("destroy", self.destroyWindow)
447 def destroyWindow(self, *args):
448 self.emit("feed-closed", self.key)
450 self.feed.saveFeed(CONFIGDIR)
452 def displayFeed(self):
453 self.vboxFeed = gtk.VBox(False, 10)
454 self.pannableFeed = hildon.PannableArea()
455 self.pannableFeed.add_with_viewport(self.vboxFeed)
456 self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
458 for index in range(self.feed.getNumberOfEntries()):
459 button = gtk.Button(self.feed.getTitle(index))
460 button.set_alignment(0,0)
462 if self.feed.isEntryRead(index):
463 #label.modify_font(pango.FontDescription("sans 16"))
464 label.modify_font(pango.FontDescription(self.config.getReadFont()))
466 #print self.listing.getFont() + " bold"
467 label.modify_font(pango.FontDescription(self.config.getUnreadFont()))
468 #label.modify_font(pango.FontDescription("sans bold 23"))
470 label.set_line_wrap(True)
472 label.set_size_request(self.get_size()[0]-50, -1)
473 button.connect("clicked", self.button_clicked, index)
474 self.buttons.append(button)
476 self.vboxFeed.pack_start(button, expand=False)
479 self.add(self.pannableFeed)
483 self.remove(self.pannableFeed)
485 def button_clicked(self, button, index, previous=False):
486 newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing)
488 self.ids.append(newDisp.connect("article-closed", self.onArticleClosed))
489 self.ids.append(newDisp.connect("article-next", self.nextArticle))
490 self.ids.append(newDisp.connect("article-previous", self.previousArticle))
491 stack = hildon.WindowStack.get_default()
498 #if not self.disp == False:
499 # self.disp.destroyWindow()
501 if not self.disp == False:
503 stack.pop_and_push(1,newDisp)
507 #if not self.disp == False:
508 #self.disp.destroyWindow()
511 def nextArticle(self, object, index):
512 label = self.buttons[index].child
513 label.modify_font(pango.FontDescription(self.config.getReadFont()))
514 index = (index+1) % self.feed.getNumberOfEntries()
515 self.button_clicked(object, index)
517 def previousArticle(self, object, index):
518 label = self.buttons[index].child
519 label.modify_font(pango.FontDescription(self.config.getReadFont()))
520 index = (index-1) % self.feed.getNumberOfEntries()
521 self.button_clicked(object, index, True)
523 def onArticleClosed(self, object, index):
524 label = self.buttons[index].child
525 label.modify_font(pango.FontDescription(self.config.getReadFont()))
526 self.buttons[index].show()
528 def button_update_clicked(self, button):
529 disp = DownloadDialog(self, self.listing, [self.key,], self.config )
530 #self.feed.updateFeed()
534 def buttonReadAllClicked(self, button):
535 for index in range(self.feed.getNumberOfEntries()):
536 self.feed.setEntryRead(index)
537 label = self.buttons[index].child
538 label.modify_font(pango.FontDescription(self.config.getReadFont()))
539 self.buttons[index].show()
544 self.listing = Listing(CONFIGDIR)
547 self.window = hildon.StackableWindow()
548 self.config = Config(self.window, CONFIGDIR+"config.ini")
549 self.window.set_title("FeedingIt")
550 FremantleRotation("FeedingIt", main_window=self.window)
551 menu = hildon.AppMenu()
552 # Create a button and add it to the menu
553 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
554 button.set_label("Update All Feeds")
555 button.connect("clicked", self.button_update_clicked, "All")
558 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
559 button.set_label("Add Feed")
560 button.connect("clicked", self.button_add_clicked)
563 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
564 button.set_label("Organize Feeds")
565 button.connect("clicked", self.button_organize_clicked)
568 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
569 button.set_label("Preferences")
570 button.connect("clicked", self.button_preferences_clicked)
573 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
574 button.set_label("Import Feeds")
575 button.connect("clicked", self.button_import_clicked)
578 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
579 button.set_label("Export Feeds")
580 button.connect("clicked", self.button_export_clicked)
583 self.window.set_app_menu(menu)
586 self.feedWindow = hildon.StackableWindow()
587 self.articleWindow = hildon.StackableWindow()
589 self.displayListing()
590 self.autoupdate = False
591 self.checkAutoUpdate()
594 def button_export_clicked(self, button):
595 opml = ExportOpmlData(self.window, self.listing)
597 def button_import_clicked(self, button):
598 opml = GetOpmlData(self.window)
599 feeds = opml.getData()
600 for (title, url) in feeds:
601 self.listing.addFeed(title, url)
602 self.displayListing()
604 def button_organize_clicked(self, button):
605 org = SortList(self.window, self.listing)
608 self.listing.saveConfig()
609 self.displayListing()
611 def button_add_clicked(self, button, urlIn="http://"):
612 wizard = AddWidgetWizard(self.window, urlIn)
615 (title, url) = wizard.getData()
616 if (not title == '') and (not url == ''):
617 self.listing.addFeed(title, url)
619 self.displayListing()
621 def button_update_clicked(self, button, key):
622 disp = DownloadDialog(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
623 self.displayListing()
625 def button_preferences_clicked(self, button):
626 dialog = self.config.createDialog()
627 dialog.connect("destroy", self.checkAutoUpdate)
629 def show_confirmation_note(self, parent, title):
630 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
632 retcode = gtk.Dialog.run(note)
635 if retcode == gtk.RESPONSE_OK:
640 def displayListing(self):
642 self.window.remove(self.pannableListing)
645 self.vboxListing = gtk.VBox(False,10)
646 self.pannableListing = hildon.PannableArea()
647 self.pannableListing.add_with_viewport(self.vboxListing)
650 for key in self.listing.getListOfFeeds():
651 #button = gtk.Button(item)
652 button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
653 hildon.BUTTON_ARRANGEMENT_VERTICAL)
654 button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / "
655 + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
656 button.set_alignment(0,0,1,1)
657 button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
658 self.vboxListing.pack_start(button, expand=False)
659 self.buttons[key] = button
660 self.window.add(self.pannableListing)
661 self.window.show_all()
663 def buttonFeedClicked(widget, button, self, window, key):
664 disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config)
665 disp.connect("feed-closed", self.onFeedClosed)
667 def onFeedClosed(self, object, key):
668 self.displayListing()
669 #self.buttons[key].set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / "
670 # + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
671 #self.buttons[key].show()
674 self.window.connect("destroy", gtk.main_quit)
676 self.listing.saveConfig()
678 def checkAutoUpdate(self, *widget):
679 if self.config.isAutoUpdateEnabled():
680 if not self.autoupdate:
681 self.autoupdateId = gobject.timeout_add(int(self.config.getUpdateInterval()*3600000), self.automaticUpdate)
682 self.autoupdate = True
685 gobject.source_remove(self.autoupdateId)
686 self.autoupdate = False
688 def automaticUpdate(self, *widget):
689 # Need to check for internet connection
690 # If no internet connection, try again in 10 minutes:
691 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
692 self.button_update_clicked(None, None)
695 if __name__ == "__main__":
696 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
697 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
698 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
699 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
700 gobject.threads_init()
701 if not isdir(CONFIGDIR):
705 print "Error: Can't create configuration directory"
708 dbusHandler = ServerObject(app)