install src/feedparser.py ${DESTDIR}/opt/FeedingIt
install src/portrait.py ${DESTDIR}/opt/FeedingIt
install src/rss.py ${DESTDIR}/opt/FeedingIt
+ install src/feedingitdbus.py ${DESTDIR}/opt/FeedingIt
install -d ${DESTDIR}/usr/share/applications/hildon
install src/FeedingIt.desktop ${DESTDIR}/usr/share/applications/hildon
install -d ${DESTDIR}/usr/share/icons/hicolor/40x40/apps/
install data/40x40/feedingit.png ${DESTDIR}/usr/share/icons/hicolor/40x40/apps/
install -d ${DESTDIR}/usr/share/icons/hicolor/26x26/apps/
- install data/26x26/feedingit.png ${DESTDIR}/usr/share/icons/hicolor/26x26/apps/
\ No newline at end of file
+ install data/26x26/feedingit.png ${DESTDIR}/usr/share/icons/hicolor/26x26/apps/
+ install -d ${DESTDIR}/usr/share/dbus-1/services/
+ install src/feedingit.service ${DESTDIR}/usr/share/dbus-1/services/
\ No newline at end of file
+feedingit (0.1.3-1) unstable; urgency=low
+
+ * Added dbus service to add new feed (#4992)
+ * Fixed #4979, delete feed issue
+ * Fixed "Add Feed" dialog
+ * Added progress bar and multi-threading for downloading feeds
+ * Automatic loading of images in HTML feeds
+ * Use dbus to open up external browser
+
+ -- Yves <yves@marcoz.org> Thu, 07 Jan 2010 23:09:19 -0800
+
feedingit (0.1.2-1) unstable; urgency=low
* Added read/unread support
Package: feedingit
Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, python-gtkhtml2, python, python-hildon
+Depends: ${shlibs:Depends}, ${misc:Depends}, python-gtkhtml2, python, python-hildon, libgtkhtml2-0, python-dbus
Description: Simple RSS Reader
- Simple RSS Reader, based on feedparser.py
+ Simple RSS Reader, with portrait mode support
XB-Maemo-Icon-26:
iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAAAXNSR0IArs4c
6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0
# ============================================================================
# Name : FeedingIt.py
# Author : Yves Marcoz
-# Version : 0.1.1
-# Description : PyGtk Example
+# Version : 0.1.3
+# Description : Simple RSS Reader
# ============================================================================
import gtk
import hildon
import gtkhtml2
import time
-import webbrowser
+import dbus
import pickle
from os.path import isfile, isdir
from os import mkdir
import urllib2
import gobject
from portrait import FremantleRotation
+import threading
+from feedingitdbus import ServerObject
from rss import *
class AddWidgetWizard(hildon.WizardDialog):
- def __init__(self, parent):
+ def __init__(self, parent, urlIn):
# Create a Notebook
self.notebook = gtk.Notebook()
self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
self.nameEntry.set_placeholder("Enter Feed Name")
+ vbox = gtk.VBox(False,10)
+ label = gtk.Label("Enter Feed Name:")
+ vbox.pack_start(label)
+ vbox.pack_start(self.nameEntry)
+ self.notebook.append_page(vbox, None)
self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
-
self.urlEntry.set_placeholder("Enter a URL")
-
+ self.urlEntry.set_text(urlIn)
+ vbox = gtk.VBox(False,10)
+ label = gtk.Label("Enter Feed Name:")
+ vbox.pack_start(label)
+ vbox.pack_start(self.urlEntry)
+ self.notebook.append_page(vbox, None)
+
labelEnd = gtk.Label("Success")
- self.notebook.append_page(self.nameEntry, None)
- self.notebook.append_page(self.urlEntry, None)
self.notebook.append_page(labelEnd, None)
hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
def some_page_func(self, nb, current, userdata):
# Validate data for 1st page
if current == 0:
- entry = nb.get_nth_page(current)
- # Check the name is not null
- return len(entry.get_text()) != 0
+ return len(self.nameEntry.get_text()) != 0
elif current == 1:
- entry = nb.get_nth_page(current)
# Check the url is not null, and starts with http
- return ( (len(entry.get_text()) != 0) and (entry.get_text().lower().startswith("http")) )
+ return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
elif current != 2:
return False
else:
return True
+
+
+class Download(threading.Thread):
+ def __init__(self, listing, key):
+ threading.Thread.__init__(self)
+ self.listing = listing
+ self.key = key
+
+ def run ( self ):
+ self.listing.updateFeed(self.key)
+
+
+class DownloadDialog():
+ def __init__(self, parent, listing, listOfKeys):
+ self.listOfKeys = listOfKeys
+ self.listing = listing
+ self.total = len(self.listOfKeys)
+ self.current = 0
+
+ if self.total>0:
+ self.progress = gtk.ProgressBar()
+ self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
+ progressbar=self.progress)
+ self.progress.set_text("Downloading")
+ self.fraction = 0
+ self.progress.set_fraction(self.fraction)
+ # Create a timeout
+ self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
+ self.waitingWindow.show_all()
+ response = self.waitingWindow.run()
+ self.listOfKeys = []
+ while threading.activeCount() > 1:
+ # Wait for current downloads to finish
+ time.sleep(0.5)
+ self.waitingWindow.destroy()
+
+ def update_progress_bar(self):
+ #self.progress_bar.pulse()
+ if threading.activeCount() < 4:
+ x = threading.activeCount() - 1
+ k = len(self.listOfKeys)
+ fin = self.total - k - x
+ fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
+ #print x, k, fin, fraction
+ self.progress.set_fraction(fraction)
+
+ if len(self.listOfKeys)>0:
+ self.current = self.current+1
+ key = self.listOfKeys.pop()
+ download = Download(self.listing, key)
+ download.start()
+ return True
+ elif threading.activeCount() > 1:
+ return True
+ else:
+ self.waitingWindow.destroy()
+ return False
+ return True
+
class DisplayArticle(hildon.StackableWindow):
def __init__(self, title, text, index):
self.document.connect("link_clicked", self._signal_link_clicked)
self.document.connect("request-url", self._signal_request_url)
self.connect("destroy", self.destroyWindow)
+ self.timeout_handler_id = gobject.timeout_add(200, self.reloadArticle)
def destroyWindow(self, *args):
self.emit("article-closed", self.index)
self.destroy()
- def reloadArticle(self, widget):
+ def reloadArticle(self, *widget):
self.document.open_stream("text/html")
self.document.write_stream(self.text)
self.document.close_stream()
def _signal_link_clicked(self, object, link):
- webbrowser.open(link)
+ bus = dbus.SystemBus()
+ proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
+ iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
+ #webbrowser.open(link)
+ iface.open_new_window(link)
def _signal_request_url(self, object, url, stream):
f = urllib2.urlopen(url)
class DisplayFeed(hildon.StackableWindow):
- def __init__(self, feed, title, key):
+ def __init__(self, listing, feed, title, key):
hildon.StackableWindow.__init__(self)
+ self.listing = listing
self.feed = feed
self.feedTitle = title
self.set_title(title)
self.buttons[index].show()
def button_update_clicked(self, button):
- self.listing.getFeed(key).updateFeed()
+ disp = DownloadDialog(self, self.listing, [self.key,] )
+ #self.feed.updateFeed()
self.clear()
- self.displayFeed(key)
+ self.displayFeed()
def buttonReadAllClicked(self, button):
for index in range(self.feed.getNumberOfEntries()):
self.displayListing()
- def button_add_clicked(self, button):
- wizard = AddWidgetWizard(self.window)
+ def button_add_clicked(self, button, urlIn="http://"):
+ wizard = AddWidgetWizard(self.window, urlIn)
ret = wizard.run()
if ret == 2:
(title, url) = wizard.getData()
self.displayListing()
def button_update_clicked(self, button, key):
- #hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
- if key == "All":
- self.listing.updateFeeds()
- else:
- self.listing.getFeed(key).updateFeed()
- self.displayFeed(key)
+ disp = DownloadDialog(self.window, self.listing, self.listing.getListOfFeeds() )
self.displayListing()
- #hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
def button_delete_clicked(self, button):
self.pickerDialog = hildon.PickerDialog(self.window)
self.pickerDialog.destroy()
if self.show_confirmation_note(self.window, current_selection):
self.listing.removeFeed(self.mapping[current_selection])
+
del self.mapping
self.displayListing()
button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
hildon.BUTTON_ARRANGEMENT_VERTICAL)
button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / "
- + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items / " + str(self.listing.getFeed(key).getNumberOfEntries()))
+ + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
button.set_alignment(0,0,1,1)
button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
self.vboxListing.pack_start(button, expand=False)
self.window.show_all()
def buttonFeedClicked(widget, button, self, window, key):
- disp = DisplayFeed(self.listing.getFeed(key), self.listing.getFeedTitle(key), key)
+ disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key)
disp.connect("feed-closed", self.onFeedClosed)
def onFeedClosed(self, object, key):
if __name__ == "__main__":
gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
-
+ gobject.threads_init()
if not isdir(CONFIGDIR):
try:
mkdir(CONFIGDIR)
print "Error: Can't create configuration directory"
sys.exit(1)
app = FeedingIt()
+ dbusHandler = ServerObject(app)
app.run()
--- /dev/null
+[D-BUS Service]
+Name=org.maemo.feedingit
+Exec=/usr/bin/FeedingIt
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env python2.5
+
+#
+# Copyright (c) 2007-2008 INdT.
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# ============================================================================
+# Name : FeedingIt.py
+# Author : Yves Marcoz
+# Version : 0.1.3
+# Description : Simple RSS Reader
+# ============================================================================
+
+import dbus
+import dbus.service
+
+class ServerObject(dbus.service.Object):
+ def __init__(self, app):
+ # Here the service name
+ bus_name = dbus.service.BusName('org.maemo.feedingit',bus=dbus.SessionBus())
+ # Here the object path
+ dbus.service.Object.__init__(self, bus_name, '/org/maemo/feedingit')
+ self.app = app
+
+ # Here the interface name, and the method is named same as on dbus.
+ @dbus.service.method('org.maemo.feedingit')
+ def AddFeed(self, url):
+ self.app.button_add_clicked(None, url)
+ return "Done"
--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# gPodder - A media aggregator and podcast client
+# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
+#
+# gPodder is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# gPodder is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import dbus
+import dbus.glib
+
+import hildon
+import osso
+
+# Replace this with your own gettext() functionality
+#import gpodder
+#_ = gpodder.gettext
+
+
+class FremantleRotation(object):
+ """thp's screen rotation for Maemo 5
+
+ Simply instantiate an object of this class and let it auto-rotate
+ your StackableWindows depending on the device orientation.
+
+ If you need to relayout a window, connect to its "configure-event"
+ signal and measure the ratio of width/height and relayout for that.
+
+ You can set the mode for rotation to AUTOMATIC (default), NEVER or
+ ALWAYS with the set_mode() method.
+ """
+ AUTOMATIC, NEVER, ALWAYS = range(3)
+
+ # Human-readable captions for the above constants
+ #MODE_CAPTIONS = (_('Automatic'), _('Landscape'), _('Portrait'))
+
+ # Privately-used constants
+ _PORTRAIT, _LANDSCAPE = ('portrait', 'landscape')
+ _ENABLE_ACCEL = 'req_accelerometer_enable'
+ _DISABLE_ACCEL = 'req_accelerometer_disable'
+
+ # Defined in mce/dbus-names.h
+ _MCE_SERVICE = 'com.nokia.mce'
+ _MCE_REQUEST_PATH = '/com/nokia/mce/request'
+ _MCE_REQUEST_IF = 'com.nokia.mce.request'
+
+ def __init__(self, app_name, main_window=None, version='1.0', mode=0):
+ """Create a new rotation manager
+
+ app_name ... The name of your application (for osso.Context)
+ main_window ... The root window (optional, hildon.StackableWindow)
+ version ... The version of your application (optional, string)
+ mode ... Initial mode for this manager (default: AUTOMATIC)
+ """
+ self._orientation = None
+ self._main_window = main_window
+ self._stack = hildon.WindowStack.get_default()
+ self._mode = -1
+ self._last_dbus_orientation = None
+ app_id = '-'.join((app_name, self.__class__.__name__))
+ self._osso_context = osso.Context(app_id, version, False)
+ program = hildon.Program.get_instance()
+ program.connect('notify::is-topmost', self._on_topmost_changed)
+ system_bus = dbus.Bus.get_system()
+ system_bus.add_signal_receiver(self._on_orientation_signal, \
+ signal_name='sig_device_orientation_ind', \
+ dbus_interface='com.nokia.mce.signal', \
+ path='/com/nokia/mce/signal')
+ self.set_mode(mode)
+
+ def get_mode(self):
+ """Get the currently-set rotation mode
+
+ This will return one of three values: AUTOMATIC, ALWAYS or NEVER.
+ """
+ return self._mode
+
+ def set_mode(self, new_mode):
+ """Set the rotation mode
+
+ You can set the rotation mode to AUTOMATIC (use hardware rotation
+ info), ALWAYS (force portrait) and NEVER (force landscape).
+ """
+ if new_mode not in (self.AUTOMATIC, self.ALWAYS, self.NEVER):
+ raise ValueError('Unknown rotation mode')
+
+ if self._mode != new_mode:
+ if self._mode == self.AUTOMATIC:
+ # Remember the current "automatic" orientation for later
+ self._last_dbus_orientation = self._orientation
+ # Tell MCE that we don't need the accelerometer anymore
+ self._send_mce_request(self._DISABLE_ACCEL)
+
+ if new_mode == self.NEVER:
+ self._orientation_changed(self._LANDSCAPE)
+ elif new_mode == self.ALWAYS:
+ self._orientation_changed(self._PORTRAIT)
+ elif new_mode == self.AUTOMATIC:
+ # Restore the last-known "automatic" orientation
+ self._orientation_changed(self._last_dbus_orientation)
+ # Tell MCE that we need the accelerometer again
+ self._send_mce_request(self._ENABLE_ACCEL)
+
+ self._mode = new_mode
+
+ def _send_mce_request(self, request):
+ rpc = osso.Rpc(self._osso_context)
+ rpc.rpc_run(self._MCE_SERVICE, \
+ self._MCE_REQUEST_PATH, \
+ self._MCE_REQUEST_IF, \
+ request, \
+ use_system_bus=True)
+
+ def _on_topmost_changed(self, program, property_spec):
+ # XXX: This seems to never get called on Fremantle(?)
+ if self._mode == self.AUTOMATIC:
+ if program.get_is_topmost():
+ self._send_mce_request(self._ENABLE_ACCEL)
+ else:
+ self._send_mce_request(self._DISABLE_ACCEL)
+
+ def _get_main_window(self):
+ if self._main_window:
+ # If we have gotten the main window as parameter, return it and
+ # don't try "harder" to find another window using the stack
+ return self._main_window
+ else:
+ # The main window is at the "bottom" of the window stack, and as
+ # the list we get with get_windows() is sorted "topmost first", we
+ # simply take the last item of the list to get our main window
+ windows = self._stack.get_windows()
+ if windows:
+ return windows[-1]
+ else:
+ return None
+
+ def _orientation_changed(self, orientation):
+ if self._orientation == orientation:
+ # Ignore repeated requests
+ return
+
+ flags = hildon.PORTRAIT_MODE_SUPPORT
+ if orientation == self._PORTRAIT:
+ flags |= hildon.PORTRAIT_MODE_REQUEST
+
+ window = self._get_main_window()
+ if window is not None:
+ hildon.hildon_gtk_window_set_portrait_flags(window, flags)
+
+ self._orientation = orientation
+
+ def _on_orientation_signal(self, orientation, stand, face, x, y, z):
+ if orientation in (self._PORTRAIT, self._LANDSCAPE):
+ if self._mode == self.AUTOMATIC:
+ # Automatically set the rotation based on hardware orientation
+ self._orientation_changed(orientation)
+ else:
+ # Ignore orientation changes for non-automatic modes, but save
+ # the current orientation for "automatic" mode later on
+ self._last_dbus_orientation = orientation
+
# ============================================================================
# Name : FeedingIt.py
# Author : Yves Marcoz
-# Version : 0.1
-# Description : PyGtk Example
+# Version : 0.1.3
+# Description : Simple RSS Reader
# ============================================================================
from os.path import isfile
self.countUnread = 0
# Initialize the new articles to unread
for index in range(self.getNumberOfEntries()):
- if not self.tmpReadItems.has_key(self.getTitle(index)):
- self.readItems[self.getTitle(index)] = False
+ if not self.tmpReadItems.has_key(self.getUniqueId(index)):
+ self.readItems[self.getUniqueId(index)] = False
else:
- self.readItems[self.getTitle(index)] = self.tmpReadItems[self.getTitle(index)]
- if self.readItems[self.getTitle(index)]==False:
+ self.readItems[self.getUniqueId(index)] = self.tmpReadItems[self.getUniqueId(index)]
+ if self.readItems[self.getUniqueId(index)]==False:
self.countUnread = self.countUnread + 1
del tmp
self.saveFeed()
def setEntryRead(self, index):
- if self.readItems[self.getTitle(index)]==False:
+ if self.readItems[self.getUniqueId(index)]==False:
self.countUnread = self.countUnread - 1
- self.readItems[self.getTitle(index)] = True
+ self.readItems[self.getUniqueId(index)] = True
def isEntryRead(self, index):
- return self.readItems[self.getTitle(index)]
+ return self.readItems[self.getUniqueId(index)]
def getTitle(self, index):
return self.entries[index]["title"]
+ def getUniqueId(self,index):
+ entry = self.entries[index]
+ return getId(time.strftime("%a, %d %b %Y %H:%M:%S",entry["updated_parsed"]) + entry["title"])
+
def getUpdateTime(self):
return self.updateTime
except:
return []
- def getArticle(self, index):
- self.setEntryRead(index)
+ def getContent(self, index):
entry = self.entries[index]
- title = entry.get('title', 'No title')
- #content = entry.get('content', entry.get('summary_detail', {}))
if entry.has_key('content'):
content = entry.content[0].value
else:
content = entry.get('summary', '')
- #print content.keys()
- #.get('value', "No Data")
+ return content
+
+ def getArticle(self, index):
+ self.setEntryRead(index)
+ entry = self.entries[index]
+ title = entry.get('title', 'No title')
+ #content = entry.get('content', entry.get('summary_detail', {}))
+ content = self.getContent(index)
+
link = entry.get('link', 'NoLink')
date = time.strftime("%a, %d %b %Y %H:%M:%S",entry["updated_parsed"])
#text = '''<div style="color: black; background-color: white;">'''
for key in self.listOfFeeds.keys():
self.feeds[key].updateFeed()
+ def updateFeed(self, key):
+ self.feeds[key].updateFeed()
+
def getFeed(self, key):
return self.feeds[key]
del self.feeds[key]
if isfile(CONFIGDIR+key):
remove(CONFIGDIR+key)
+ self.saveConfig()
def saveConfig(self):
file = open(CONFIGDIR+"feeds.pickle", "w")