0.6.1-7, fix for broken feeds
[feedingit] / src / FeedingIt.py
1 #!/usr/bin/env python2.5
2
3
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.
9 #
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.
14 #
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/>.
17 #
18
19 # ============================================================================
20 # Name        : FeedingIt.py
21 # Author      : Yves Marcoz
22 # Version     : 0.6.0
23 # Description : Simple RSS Reader
24 # ============================================================================
25
26 import gtk
27 from pango import FontDescription
28 import hildon
29 #import gtkhtml2
30 #try:
31 from webkit import WebView
32 #    has_webkit=True
33 #except:
34 #    import gtkhtml2
35 #    has_webkit=False
36 from os.path import isfile, isdir, exists
37 from os import mkdir, remove, stat
38 import gobject
39 from portrait import FremantleRotation
40 from threading import Thread, activeCount
41 from feedingitdbus import ServerObject
42 from updatedbus import UpdateServerObject, get_lock
43 from config import Config
44 from cgi import escape
45
46 from rss import Listing
47 from opml import GetOpmlData, ExportOpmlData
48
49 from urllib2 import install_opener, build_opener
50
51 from socket import setdefaulttimeout
52 timeout = 5
53 setdefaulttimeout(timeout)
54 del timeout
55
56 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
57 unread_color = color_style.lookup_color('ActiveTextColor')
58 read_color = color_style.lookup_color('DefaultTextColor')
59 del color_style
60
61 CONFIGDIR="/home/user/.feedingit/"
62 LOCK = CONFIGDIR + "update.lock"
63
64 from re import sub
65 from htmlentitydefs import name2codepoint
66
67 ##
68 # Removes HTML or XML character references and entities from a text string.
69 #
70 # @param text The HTML (or XML) source text.
71 # @return The plain text, as a Unicode string, if necessary.
72 # http://effbot.org/zone/re-sub.htm#unescape-html
73 def unescape(text):
74     def fixup(m):
75         text = m.group(0)
76         if text[:2] == "&#":
77             # character reference
78             try:
79                 if text[:3] == "&#x":
80                     return unichr(int(text[3:-1], 16))
81                 else:
82                     return unichr(int(text[2:-1]))
83             except ValueError:
84                 pass
85         else:
86             # named entity
87             try:
88                 text = unichr(name2codepoint[text[1:-1]])
89             except KeyError:
90                 pass
91         return text # leave as is
92     return sub("&#?\w+;", fixup, text)
93
94
95 class AddWidgetWizard(hildon.WizardDialog):
96     
97     def __init__(self, parent, urlIn, titleIn=None):
98         # Create a Notebook
99         self.notebook = gtk.Notebook()
100
101         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
102         self.nameEntry.set_placeholder("Enter Feed Name")
103         vbox = gtk.VBox(False,10)
104         label = gtk.Label("Enter Feed Name:")
105         vbox.pack_start(label)
106         vbox.pack_start(self.nameEntry)
107         if not titleIn == None:
108             self.nameEntry.set_text(titleIn)
109         self.notebook.append_page(vbox, None)
110         
111         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
112         self.urlEntry.set_placeholder("Enter a URL")
113         self.urlEntry.set_text(urlIn)
114         self.urlEntry.select_region(0,-1)
115         
116         vbox = gtk.VBox(False,10)
117         label = gtk.Label("Enter Feed URL:")
118         vbox.pack_start(label)
119         vbox.pack_start(self.urlEntry)
120         self.notebook.append_page(vbox, None)
121
122         labelEnd = gtk.Label("Success")
123         
124         self.notebook.append_page(labelEnd, None)      
125
126         hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
127    
128         # Set a handler for "switch-page" signal
129         #self.notebook.connect("switch_page", self.on_page_switch, self)
130    
131         # Set a function to decide if user can go to next page
132         self.set_forward_page_func(self.some_page_func)
133    
134         self.show_all()
135         
136     def getData(self):
137         return (self.nameEntry.get_text(), self.urlEntry.get_text())
138         
139     def on_page_switch(self, notebook, page, num, dialog):
140         return True
141    
142     def some_page_func(self, nb, current, userdata):
143         # Validate data for 1st page
144         if current == 0:
145             return len(self.nameEntry.get_text()) != 0
146         elif current == 1:
147             # Check the url is not null, and starts with http
148             return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
149         elif current != 2:
150             return False
151         else:
152             return True
153         
154 class Download(Thread):
155     def __init__(self, listing, key, config):
156         Thread.__init__(self)
157         self.listing = listing
158         self.key = key
159         self.config = config
160         
161     def run (self):
162         (use_proxy, proxy) = self.config.getProxy()
163         key_lock = get_lock(self.key)
164         if key_lock != None:
165             if use_proxy:
166                 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
167             else:
168                 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
169         del key_lock
170
171         
172 class DownloadBar(gtk.ProgressBar):
173     def __init__(self, parent, listing, listOfKeys, config, single=False):
174         
175         update_lock = get_lock("update_lock")
176         if update_lock != None:
177             gtk.ProgressBar.__init__(self)
178             self.listOfKeys = listOfKeys[:]
179             self.listing = listing
180             self.total = len(self.listOfKeys)
181             self.config = config
182             self.current = 0
183             self.single = single
184             (use_proxy, proxy) = self.config.getProxy()
185             if use_proxy:
186                 opener = build_opener(proxy)
187                 opener.addheaders = [('User-agent', 'Mozilla/5.0 (compatible; Maemo 5;) FeedingIt 0.6.1')]
188                 install_opener(opener)
189             else:
190                 opener = build_opener()
191                 opener.addheaders = [('User-agent', 'Mozilla/5.0 (compatible; Maemo 5;) FeedingIt 0.6.1')]
192                 install_opener(opener)
193
194             if self.total>0:
195                 self.set_text("Updating...")
196                 self.fraction = 0
197                 self.set_fraction(self.fraction)
198                 self.show_all()
199                 # Create a timeout
200                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
201
202     def update_progress_bar(self):
203         #self.progress_bar.pulse()
204         if activeCount() < 4:
205             x = activeCount() - 1
206             k = len(self.listOfKeys)
207             fin = self.total - k - x
208             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
209             #print x, k, fin, fraction
210             self.set_fraction(fraction)
211
212             if len(self.listOfKeys)>0:
213                 self.current = self.current+1
214                 key = self.listOfKeys.pop()
215                 #if self.single == True:
216                     # Check if the feed is being displayed
217                 download = Download(self.listing, key, self.config)
218                 download.start()
219                 return True
220             elif activeCount() > 1:
221                 return True
222             else:
223                 #self.waitingWindow.destroy()
224                 #self.destroy()
225                 try:
226                     del self.update_lock
227                 except:
228                     pass
229                 self.emit("download-done", "success")
230                 return False 
231         return True
232     
233     
234 class SortList(gtk.Dialog):
235     def __init__(self, parent, listing):
236         gtk.Dialog.__init__(self, "Organizer",  parent)
237         self.listing = listing
238         
239         self.vbox2 = gtk.VBox(False, 10)
240         
241         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
242         button.set_label("Move Up")
243         button.connect("clicked", self.buttonUp)
244         self.vbox2.pack_start(button, expand=False, fill=False)
245         
246         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
247         button.set_label("Move Down")
248         button.connect("clicked", self.buttonDown)
249         self.vbox2.pack_start(button, expand=False, fill=False)
250
251         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
252         button.set_label("Add Feed")
253         button.connect("clicked", self.buttonAdd)
254         self.vbox2.pack_start(button, expand=False, fill=False)
255
256         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
257         button.set_label("Edit Feed")
258         button.connect("clicked", self.buttonEdit)
259         self.vbox2.pack_start(button, expand=False, fill=False)
260         
261         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
262         button.set_label("Delete")
263         button.connect("clicked", self.buttonDelete)
264         self.vbox2.pack_start(button, expand=False, fill=False)
265         
266         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
267         #button.set_label("Done")
268         #button.connect("clicked", self.buttonDone)
269         #self.vbox.pack_start(button)
270         self.hbox2= gtk.HBox(False, 10)
271         self.pannableArea = hildon.PannableArea()
272         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
273         self.treeview = gtk.TreeView(self.treestore)
274         self.hbox2.pack_start(self.pannableArea, expand=True)
275         self.displayFeeds()
276         self.hbox2.pack_end(self.vbox2, expand=False)
277         self.set_default_size(-1, 600)
278         self.vbox.pack_start(self.hbox2)
279         
280         self.show_all()
281         #self.connect("destroy", self.buttonDone)
282         
283     def displayFeeds(self):
284         self.treeview.destroy()
285         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
286         self.treeview = gtk.TreeView()
287         
288         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
289         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
290         self.refreshList()
291         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
292
293         self.pannableArea.add(self.treeview)
294
295         #self.show_all()
296
297     def refreshList(self, selected=None, offset=0):
298         #rect = self.treeview.get_visible_rect()
299         #y = rect.y+rect.height
300         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
301         for key in self.listing.getListOfFeeds():
302             item = self.treestore.append([self.listing.getFeedTitle(key), key])
303             if key == selected:
304                 selectedItem = item
305         self.treeview.set_model(self.treestore)
306         if not selected == None:
307             self.treeview.get_selection().select_iter(selectedItem)
308             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
309         self.pannableArea.show_all()
310
311     def getSelectedItem(self):
312         (model, iter) = self.treeview.get_selection().get_selected()
313         if not iter:
314             return None
315         return model.get_value(iter, 1)
316
317     def findIndex(self, key):
318         after = None
319         before = None
320         found = False
321         for row in self.treestore:
322             if found:
323                 return (before, row.iter)
324             if key == list(row)[0]:
325                 found = True
326             else:
327                 before = row.iter
328         return (before, None)
329
330     def buttonUp(self, button):
331         key  = self.getSelectedItem()
332         if not key == None:
333             self.listing.moveUp(key)
334             self.refreshList(key, -10)
335
336     def buttonDown(self, button):
337         key = self.getSelectedItem()
338         if not key == None:
339             self.listing.moveDown(key)
340             self.refreshList(key, 10)
341
342     def buttonDelete(self, button):
343         key = self.getSelectedItem()
344         if not key == None:
345             self.listing.removeFeed(key)
346         self.refreshList()
347
348     def buttonEdit(self, button):
349         key = self.getSelectedItem()
350         if not key == None:
351             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
352             ret = wizard.run()
353             if ret == 2:
354                 (title, url) = wizard.getData()
355                 if (not title == '') and (not url == ''):
356                     self.listing.editFeed(key, title, url)
357             wizard.destroy()
358         self.refreshList()
359
360     def buttonDone(self, *args):
361         self.destroy()
362         
363     def buttonAdd(self, button, urlIn="http://"):
364         wizard = AddWidgetWizard(self, urlIn)
365         ret = wizard.run()
366         if ret == 2:
367             (title, url) = wizard.getData()
368             if (not title == '') and (not url == ''): 
369                self.listing.addFeed(title, url)
370         wizard.destroy()
371         self.refreshList()
372                
373
374 class DisplayArticle(hildon.StackableWindow):
375     def __init__(self, feed, id, key, config, listing):
376         hildon.StackableWindow.__init__(self)
377         #self.imageDownloader = ImageDownloader()
378         self.feed = feed
379         self.listing=listing
380         self.key = key
381         self.id = id
382         #self.set_title(feed.getTitle(id))
383         self.set_title(self.listing.getFeedTitle(key))
384         self.config = config
385         self.set_for_removal = False
386         
387         # Init the article display
388         #if self.config.getWebkitSupport():
389         self.view = WebView()
390             #self.view.set_editable(False)
391         #else:
392         #    import gtkhtml2
393         #    self.view = gtkhtml2.View()
394         #    self.document = gtkhtml2.Document()
395         #    self.view.set_document(self.document)
396         #    self.document.connect("link_clicked", self._signal_link_clicked)
397         self.pannable_article = hildon.PannableArea()
398         self.pannable_article.add(self.view)
399         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
400         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
401
402         #if self.config.getWebkitSupport():
403         contentLink = self.feed.getContentLink(self.id)
404         self.feed.setEntryRead(self.id)
405         #if key=="ArchivedArticles":
406         self.view.open("file://" + contentLink)
407         self.view.connect("motion-notify-event", lambda w,ev: True)
408         self.view.connect('load-started', self.load_started)
409         self.view.connect('load-finished', self.load_finished)
410
411         #else:
412         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
413         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
414         #else:
415         #    if not key == "ArchivedArticles":
416                 # Do not download images if the feed is "Archived Articles"
417         #        self.document.connect("request-url", self._signal_request_url)
418             
419         #    self.document.clear()
420         #    self.document.open_stream("text/html")
421         #    self.document.write_stream(self.text)
422         #    self.document.close_stream()
423         
424         menu = hildon.AppMenu()
425         # Create a button and add it to the menu
426         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
427         button.set_label("Allow Horizontal Scrolling")
428         button.connect("clicked", self.horiz_scrolling_button)
429         menu.append(button)
430         
431         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
432         button.set_label("Open in Browser")
433         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
434         menu.append(button)
435         
436         if key == "ArchivedArticles":
437             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
438             button.set_label("Remove from Archived Articles")
439             button.connect("clicked", self.remove_archive_button)
440         else:
441             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
442             button.set_label("Add to Archived Articles")
443             button.connect("clicked", self.archive_button)
444         menu.append(button)
445         
446         self.set_app_menu(menu)
447         menu.show_all()
448         
449         #self.event_box = gtk.EventBox()
450         #self.event_box.add(self.pannable_article)
451         self.add(self.pannable_article)
452         
453         
454         self.pannable_article.show_all()
455
456         self.destroyId = self.connect("destroy", self.destroyWindow)
457         
458         self.view.connect("button_press_event", self.button_pressed)
459         self.gestureId = self.view.connect("button_release_event", self.button_released)
460         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
461
462     def load_started(self, *widget):
463         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
464         
465     def load_finished(self, *widget):
466         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
467
468     def button_pressed(self, window, event):
469         #print event.x, event.y
470         self.coords = (event.x, event.y)
471         
472     def button_released(self, window, event):
473         x = self.coords[0] - event.x
474         y = self.coords[1] - event.y
475         
476         if (2*abs(y) < abs(x)):
477             if (x > 15):
478                 self.emit("article-previous", self.id)
479             elif (x<-15):
480                 self.emit("article-next", self.id)   
481         #print x, y
482         #print "Released"
483
484     #def gesture(self, widget, direction, startx, starty):
485     #    if (direction == 3):
486     #        self.emit("article-next", self.index)
487     #    if (direction == 2):
488     #        self.emit("article-previous", self.index)
489         #print startx, starty
490         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
491
492     def destroyWindow(self, *args):
493         self.disconnect(self.destroyId)
494         if self.set_for_removal:
495             self.emit("article-deleted", self.id)
496         else:
497             self.emit("article-closed", self.id)
498         #self.imageDownloader.stopAll()
499         self.destroy()
500         
501     def horiz_scrolling_button(self, *widget):
502         self.pannable_article.disconnect(self.gestureId)
503         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
504         
505     def archive_button(self, *widget):
506         # Call the listing.addArchivedArticle
507         self.listing.addArchivedArticle(self.key, self.id)
508         
509     def remove_archive_button(self, *widget):
510         self.set_for_removal = True
511         
512     #def reloadArticle(self, *widget):
513     #    if threading.activeCount() > 1:
514             # Image thread are still running, come back in a bit
515     #        return True
516     #    else:
517     #        for (stream, imageThread) in self.images:
518     #            imageThread.join()
519     #            stream.write(imageThread.data)
520     #            stream.close()
521     #        return False
522     #    self.show_all()
523
524     def _signal_link_clicked(self, object, link):
525         import dbus
526         bus = dbus.SessionBus()
527         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
528         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
529         iface.open_new_window(link)
530
531     #def _signal_request_url(self, object, url, stream):
532         #print url
533     #    self.imageDownloader.queueImage(url, stream)
534         #imageThread = GetImage(url)
535         #imageThread.start()
536         #self.images.append((stream, imageThread))
537
538
539 class DisplayFeed(hildon.StackableWindow):
540     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
541         hildon.StackableWindow.__init__(self)
542         self.listing = listing
543         self.feed = feed
544         self.feedTitle = title
545         self.set_title(title)
546         self.key=key
547         self.config = config
548         self.updateDbusHandler = updateDbusHandler
549         
550         self.downloadDialog = False
551         
552         #self.listing.setCurrentlyDisplayedFeed(self.key)
553         
554         self.disp = False
555         
556         menu = hildon.AppMenu()
557         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
558         button.set_label("Update Feed")
559         button.connect("clicked", self.button_update_clicked)
560         menu.append(button)
561         
562         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
563         button.set_label("Mark All As Read")
564         button.connect("clicked", self.buttonReadAllClicked)
565         menu.append(button)
566         
567         if key=="ArchivedArticles":
568             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
569             button.set_label("Purge Read Articles")
570             button.connect("clicked", self.buttonPurgeArticles)
571             menu.append(button)
572         
573         self.set_app_menu(menu)
574         menu.show_all()
575         
576         self.displayFeed()
577         
578         self.connect("destroy", self.destroyWindow)
579         
580     def destroyWindow(self, *args):
581         #self.feed.saveUnread(CONFIGDIR)
582         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
583         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
584         self.emit("feed-closed", self.key)
585         self.destroy()
586         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
587         #self.listing.closeCurrentlyDisplayedFeed()
588
589     def displayFeed(self):
590         self.vboxFeed = gtk.VBox(False, 10)
591         self.pannableFeed = hildon.PannableArea()
592         self.pannableFeed.add_with_viewport(self.vboxFeed)
593         self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
594         self.buttons = {}
595         for id in self.feed.getIds():
596             title = self.feed.getTitle(id)
597             
598             esc_title = unescape(title).replace("<em>","").replace("</em>","")
599             #title.replace("<em>","").replace("</em>","").replace("&amp;","&").replace("&mdash;", "-").replace("&#8217;", "'")
600             button = gtk.Button(esc_title)
601             button.set_alignment(0,0)
602             label = button.child
603
604             if self.feed.isEntryRead(id):
605                 #label.modify_font(FontDescription("sans 16"))
606                 label.modify_font(FontDescription(self.config.getReadFont()))
607                 label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
608             else:
609                 #print self.listing.getFont() + " bold"
610                 label.modify_font(FontDescription(self.config.getUnreadFont()))
611                 label.modify_fg(gtk.STATE_NORMAL, unread_color)
612             label.set_line_wrap(True)
613             
614             label.set_size_request(self.get_size()[0]-50, -1)
615             button.connect("clicked", self.button_clicked, id)
616             self.buttons[id] = button
617             
618             self.vboxFeed.pack_start(button, expand=False)
619
620         self.add(self.pannableFeed)
621         self.show_all()
622         
623     def clear(self):
624         self.pannableFeed.destroy()
625         #self.remove(self.pannableFeed)
626
627     def button_clicked(self, button, index, previous=False, next=False):
628         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
629         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
630         stack = hildon.WindowStack.get_default()
631         if previous:
632             tmp = stack.peek()
633             stack.pop_and_push(1, newDisp, tmp)
634             newDisp.show()
635             gobject.timeout_add(200, self.destroyArticle, tmp)
636             #print "previous"
637             self.disp = newDisp
638         elif next:
639             newDisp.show_all()
640             if type(self.disp).__name__ == "DisplayArticle":
641                 gobject.timeout_add(200, self.destroyArticle, self.disp)
642             self.disp = newDisp
643         else:
644             self.disp = newDisp
645             self.disp.show_all()
646         
647         self.ids = []
648         if self.key == "ArchivedArticles":
649             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
650         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
651         self.ids.append(self.disp.connect("article-next", self.nextArticle))
652         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
653
654     def buttonPurgeArticles(self, *widget):
655         self.clear()
656         self.feed.purgeReadArticles()
657         self.feed.saveUnread(CONFIGDIR)
658         self.feed.saveFeed(CONFIGDIR)
659         self.displayFeed()
660
661     def destroyArticle(self, handle):
662         handle.destroyWindow()
663
664     def nextArticle(self, object, index):
665         label = self.buttons[index].child
666         label.modify_font(FontDescription(self.config.getReadFont()))
667         label.modify_fg(gtk.STATE_NORMAL, read_color) #  gtk.gdk.color_parse("white"))
668         id = self.feed.getNextId(index)
669         self.button_clicked(object, id, next=True)
670
671     def previousArticle(self, object, index):
672         label = self.buttons[index].child
673         label.modify_font(FontDescription(self.config.getReadFont()))
674         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
675         id = self.feed.getPreviousId(index)
676         self.button_clicked(object, id, previous=True)
677
678     def onArticleClosed(self, object, index):
679         label = self.buttons[index].child
680         label.modify_font(FontDescription(self.config.getReadFont()))
681         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
682         self.buttons[index].show()
683         
684     def onArticleDeleted(self, object, index):
685         self.clear()
686         self.feed.removeArticle(index)
687         self.feed.saveUnread(CONFIGDIR)
688         self.feed.saveFeed(CONFIGDIR)
689         self.displayFeed()
690
691     def button_update_clicked(self, button):
692         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
693         if not type(self.downloadDialog).__name__=="DownloadBar":
694             self.pannableFeed.destroy()
695             self.vbox = gtk.VBox(False, 10)
696             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
697             self.downloadDialog.connect("download-done", self.onDownloadsDone)
698             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
699             self.add(self.vbox)
700             self.show_all()
701             
702     def onDownloadsDone(self, *widget):
703         self.vbox.destroy()
704         self.feed = self.listing.getFeed(self.key)
705         self.displayFeed()
706         self.updateDbusHandler.ArticleCountUpdated()
707         
708     def buttonReadAllClicked(self, button):
709         for index in self.feed.getIds():
710             self.feed.setEntryRead(index)
711             label = self.buttons[index].child
712             label.modify_font(FontDescription(self.config.getReadFont()))
713             label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
714             self.buttons[index].show()
715
716
717 class FeedingIt:
718     def __init__(self):
719         # Init the windows
720         self.window = hildon.StackableWindow()
721         self.window.set_title("FeedingIt")
722         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
723         self.mainVbox = gtk.VBox(False,10)
724         self.pannableListing = gtk.Label("Loading...")
725         self.mainVbox.pack_start(self.pannableListing)
726         self.window.add(self.mainVbox)
727         self.window.show_all()
728         self.config = Config(self.window, CONFIGDIR+"config.ini")
729         gobject.idle_add(self.createWindow)
730         
731     def createWindow(self):
732         self.app_lock = get_lock("app_lock")
733         if self.app_lock == None:
734             self.pannableListing.set_label("Update in progress, please wait.")
735             gobject.timeout_add_seconds(3, self.createWindow)
736             return False
737         self.listing = Listing(CONFIGDIR)
738         
739         self.downloadDialog = False
740         self.orientation = FremantleRotation("FeedingIt", main_window=self.window, app=self)
741         self.orientation.set_mode(self.config.getOrientation())
742         
743         menu = hildon.AppMenu()
744         # Create a button and add it to the menu
745         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
746         button.set_label("Update All Feeds")
747         button.connect("clicked", self.button_update_clicked, "All")
748         menu.append(button)
749         
750         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
751         button.set_label("Mark All As Read")
752         button.connect("clicked", self.button_markAll)
753         menu.append(button)
754         
755         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
756         button.set_label("Organize Feeds")
757         button.connect("clicked", self.button_organize_clicked)
758         menu.append(button)
759
760         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
761         button.set_label("Preferences")
762         button.connect("clicked", self.button_preferences_clicked)
763         menu.append(button)
764        
765         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
766         button.set_label("Import Feeds")
767         button.connect("clicked", self.button_import_clicked)
768         menu.append(button)
769         
770         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
771         button.set_label("Export Feeds")
772         button.connect("clicked", self.button_export_clicked)
773         menu.append(button)
774         
775         self.window.set_app_menu(menu)
776         menu.show_all()
777         
778         self.feedWindow = hildon.StackableWindow()
779         self.articleWindow = hildon.StackableWindow()
780
781         self.displayListing()
782         self.autoupdate = False
783         self.checkAutoUpdate()
784         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
785         gobject.idle_add(self.enableDbus)
786         
787     def enableDbus(self):
788         self.dbusHandler = ServerObject(self)
789         self.updateDbusHandler = UpdateServerObject(self)
790
791     def button_markAll(self, button):
792         for key in self.listing.getListOfFeeds():
793             feed = self.listing.getFeed(key)
794             for id in feed.getIds():
795                 feed.setEntryRead(id)
796             feed.saveUnread(CONFIGDIR)
797             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
798         self.refreshList()
799
800     def button_export_clicked(self, button):
801         opml = ExportOpmlData(self.window, self.listing)
802         
803     def button_import_clicked(self, button):
804         opml = GetOpmlData(self.window)
805         feeds = opml.getData()
806         for (title, url) in feeds:
807             self.listing.addFeed(title, url)
808         self.displayListing()
809
810     def addFeed(self, urlIn="http://"):
811         wizard = AddWidgetWizard(self.window, urlIn)
812         ret = wizard.run()
813         if ret == 2:
814             (title, url) = wizard.getData()
815             if (not title == '') and (not url == ''): 
816                self.listing.addFeed(title, url)
817         wizard.destroy()
818         self.displayListing()
819
820     def button_organize_clicked(self, button):
821         org = SortList(self.window, self.listing)
822         org.run()
823         org.destroy()
824         self.listing.saveConfig()
825         self.displayListing()
826         
827     def button_update_clicked(self, button, key):
828         if not type(self.downloadDialog).__name__=="DownloadBar":
829             self.updateDbusHandler.UpdateStarted()
830             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
831             self.downloadDialog.connect("download-done", self.onDownloadsDone)
832             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
833             self.mainVbox.show_all()
834         #self.displayListing()
835
836     def onDownloadsDone(self, *widget):
837         self.downloadDialog.destroy()
838         self.downloadDialog = False
839         #self.displayListing()
840         self.refreshList()
841         self.updateDbusHandler.UpdateFinished()
842         self.updateDbusHandler.ArticleCountUpdated()
843
844     def button_preferences_clicked(self, button):
845         dialog = self.config.createDialog()
846         dialog.connect("destroy", self.prefsClosed)
847
848     def show_confirmation_note(self, parent, title):
849         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
850
851         retcode = gtk.Dialog.run(note)
852         note.destroy()
853         
854         if retcode == gtk.RESPONSE_OK:
855             return True
856         else:
857             return False
858         
859     def displayListing(self):
860         try:
861             self.mainVbox.remove(self.pannableListing)
862         except:
863             pass
864         self.vboxListing = gtk.VBox(False,10)
865         self.pannableListing = hildon.PannableArea()
866         self.pannableListing.add_with_viewport(self.vboxListing)
867
868         self.buttons = {}
869         list = self.listing.getListOfFeeds()[:]
870         #list.reverse()
871         for key in list:
872             #button = gtk.Button(item)
873             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
874             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
875                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
876             button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
877                             + str(unreadItems) + " Unread Items")
878             button.set_alignment(0,0,1,1)
879             button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
880             self.vboxListing.pack_start(button, expand=False)
881             self.buttons[key] = button
882      
883         self.mainVbox.pack_start(self.pannableListing)
884         self.window.show_all()
885         gobject.idle_add(self.refreshList)
886
887     def refreshList(self):
888         for key in self.listing.getListOfFeeds():
889             if self.buttons.has_key(key):
890                 button = self.buttons[key]
891                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
892                 button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
893                             + str(unreadItems) + " Unread Items")
894                 label = button.child.child.get_children()[0].get_children()[1]
895                 if unreadItems == 0:
896                     label.modify_fg(gtk.STATE_NORMAL, read_color)
897                 else:
898                     label.modify_fg(gtk.STATE_NORMAL, unread_color)
899             else:
900                 self.displayListing()
901                 break
902
903     def buttonFeedClicked(widget, button, self, window, key):
904         try:
905             self.feed_lock
906         except:
907             # If feed_lock doesn't exist, we can open the feed, else we do nothing
908             self.feed_lock = get_lock(key)
909             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config, self.updateDbusHandler)
910             self.disp.connect("feed-closed", self.onFeedClosed)
911
912     def onFeedClosed(self, object, key):
913         #self.listing.saveConfig()
914         #del self.feed_lock
915         gobject.idle_add(self.onFeedClosedTimeout)
916         self.refreshList()
917         #self.updateDbusHandler.ArticleCountUpdated()
918         
919     def onFeedClosedTimeout(self):
920         self.listing.saveConfig()
921         del self.feed_lock
922         self.updateDbusHandler.ArticleCountUpdated()
923      
924     def run(self):
925         self.window.connect("destroy", gtk.main_quit)
926         gtk.main()
927         self.listing.saveConfig()
928         del self.app_lock
929
930     def prefsClosed(self, *widget):
931         self.orientation.set_mode(self.config.getOrientation())
932         self.checkAutoUpdate()
933
934     def checkAutoUpdate(self, *widget):
935         interval = int(self.config.getUpdateInterval()*3600000)
936         if self.config.isAutoUpdateEnabled():
937             if self.autoupdate == False:
938                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
939                 self.autoupdate = interval
940             elif not self.autoupdate == interval:
941                 # If auto-update is enabled, but not at the right frequency
942                 gobject.source_remove(self.autoupdateId)
943                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
944                 self.autoupdate = interval
945         else:
946             if not self.autoupdate == False:
947                 gobject.source_remove(self.autoupdateId)
948                 self.autoupdate = False
949
950     def automaticUpdate(self, *widget):
951         # Need to check for internet connection
952         # If no internet connection, try again in 10 minutes:
953         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
954         file = open("/home/user/.feedingit/feedingit_widget.log", "a")
955         from time import localtime, strftime
956         file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
957         file.close()
958         self.button_update_clicked(None, None)
959         return True
960     
961     def stopUpdate(self):
962         # Not implemented in the app (see update_feeds.py)
963         try:
964             self.downloadDialog.listOfKeys = []
965         except:
966             pass
967     
968     def getStatus(self):
969         status = ""
970         for key in self.listing.getListOfFeeds():
971             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
972                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
973         if status == "":
974             status = "No unread items"
975         return status
976
977 if __name__ == "__main__":
978     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
979     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
980     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
981     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
982     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
983     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
984     gobject.threads_init()
985     if not isdir(CONFIGDIR):
986         try:
987             mkdir(CONFIGDIR)
988         except:
989             print "Error: Can't create configuration directory"
990             from sys import exit
991             exit(1)
992     app = FeedingIt()
993     app.run()