From f3924cb75e5fbb24f55b503e8bf3fd6fd26ab1ed Mon Sep 17 00:00:00 2001 From: epage Date: Fri, 7 May 2010 03:07:01 +0000 Subject: [PATCH] Setting up a trunk for branching git-svn-id: file:///svnroot/nqaap/trunk@9 00ff6f12-f5ab-46b1-af0e-967c86d3154f --- build.sh | 7 + build_nqaap.py | 82 +++ build_nqaap_maemo5.py | 82 +++ .../Nqa-Audiobook-player/.ropeproject/config.py | 85 +++ src/opt/Nqa-Audiobook-player/Audiobook.py | 176 +++++ src/opt/Nqa-Audiobook-player/Browser.py | 5 + src/opt/Nqa-Audiobook-player/CallMonitor.py | 145 ++++ src/opt/Nqa-Audiobook-player/FileStorage.py | 68 ++ src/opt/Nqa-Audiobook-player/Gui.py | 594 ++++++++++++++++ src/opt/Nqa-Audiobook-player/Help/nqaap.html | 111 +++ src/opt/Nqa-Audiobook-player/NoCover.png | Bin 0 -> 18504 bytes src/opt/Nqa-Audiobook-player/Player.py | 185 +++++ src/opt/Nqa-Audiobook-player/SimpleGStreamer.py | 93 +++ src/opt/Nqa-Audiobook-player/SimpleOSSOPlayer.py | 115 +++ src/opt/Nqa-Audiobook-player/__init__.py | 1 + src/opt/Nqa-Audiobook-player/constants.py | 11 + src/opt/Nqa-Audiobook-player/gtk_toolbox.py | 577 +++++++++++++++ src/opt/Nqa-Audiobook-player/hildonize.py | 743 ++++++++++++++++++++ src/opt/Nqa-Audiobook-player/nqaap.py | 28 + src/opt/Nqa-Audiobook-player/nqaap_gtk.py | 32 + src/opt/Nqa-Audiobook-player/settings.py | 104 +++ src/usr/share/applications/hildon/nqaap.desktop | 10 + src/usr/share/icons/hicolor/48x48/hildon/nqaap.png | Bin 0 -> 1218 bytes .../share/icons/hicolor/scalable/hildon/nqaap.png | Bin 0 -> 1666 bytes 24 files changed, 3254 insertions(+) create mode 100644 build.sh create mode 100644 build_nqaap.py create mode 100644 build_nqaap_maemo5.py create mode 100644 src/opt/Nqa-Audiobook-player/.ropeproject/config.py create mode 100644 src/opt/Nqa-Audiobook-player/Audiobook.py create mode 100644 src/opt/Nqa-Audiobook-player/Browser.py create mode 100644 src/opt/Nqa-Audiobook-player/CallMonitor.py create mode 100644 src/opt/Nqa-Audiobook-player/FileStorage.py create mode 100644 src/opt/Nqa-Audiobook-player/Gui.py create mode 100644 src/opt/Nqa-Audiobook-player/Help/nqaap.html create mode 100644 src/opt/Nqa-Audiobook-player/NoCover.png create mode 100644 src/opt/Nqa-Audiobook-player/Player.py create mode 100644 src/opt/Nqa-Audiobook-player/SimpleGStreamer.py create mode 100644 src/opt/Nqa-Audiobook-player/SimpleOSSOPlayer.py create mode 100644 src/opt/Nqa-Audiobook-player/__init__.py create mode 100644 src/opt/Nqa-Audiobook-player/constants.py create mode 100644 src/opt/Nqa-Audiobook-player/gtk_toolbox.py create mode 100644 src/opt/Nqa-Audiobook-player/hildonize.py create mode 100644 src/opt/Nqa-Audiobook-player/nqaap.py create mode 100644 src/opt/Nqa-Audiobook-player/nqaap_gtk.py create mode 100644 src/opt/Nqa-Audiobook-player/settings.py create mode 100644 src/usr/share/applications/hildon/nqaap.desktop create mode 100644 src/usr/share/icons/hicolor/48x48/hildon/nqaap.png create mode 100644 src/usr/share/icons/hicolor/scalable/hildon/nqaap.png diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..0a7610c --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +rm -r ~/nqaap; +svn export http://www.pengworld.com/python/n900/Audiobook/nqaap/ ~/nqaap +chmod +x ~/nqaap/src/opt/Nqa-Audiobook-player/nqaap.py +python ~/nqaap/build_nqaap.py + diff --git a/build_nqaap.py b/build_nqaap.py new file mode 100644 index 0000000..09e1de5 --- /dev/null +++ b/build_nqaap.py @@ -0,0 +1,82 @@ +#!/usr/bin/python2.5 + # -*- coding: utf-8 -*- + ## This program 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; version 2 only. + ## + ## 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 General Public License for more details. + ## +import py2deb +import os +if __name__ == "__main__": + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + print + p=py2deb.Py2deb("nqaap") #This is the package name and MUST be in + #lowercase! (using e.g. "mClock" fails + #miserably...) + p.description="Very simple Audiobook player. \nSupports playing, pausing, seeking (sort of) and saving state when changing book/closing.\nPlays books arranged as dirs under myDocs/Audiobooks" + p.author="Soeren 'Pengman' Pedersen" + p.mail="pengmeister@gmail.com" + p.depends = "python2.5, python2.5-gtk2, python-gst0.10" + p.section="user/multimedia" + p.icon = "/usr/share/icons/hicolor/48x48/hildon/nqaap.png" + p.arch="all" #should be all for python, any for all arch + p.urgency="low" #not used in maemo onl for deb os + p.distribution="fremantle" + p.repository="extras-devel" + p.xsbc_bugtracker="http://talk.maemo.org/showthread.php?p=619738" + # p.postinstall="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your post install script + # p.postremove="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your post remove script + # p.preinstall="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your pre install script + # p.preremove="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your pre remove script + version = "0.8.0" #Version of your software, e.g. "1.2.0" or "0.8.2" + build = "2" #Build number, e.g. "1" for the first build of this + #version of your software. Increment + #for later re-builds of the same + #version of your software. Text with + #changelog information to be displayed + #in the package "Details" tab of the + #Maemo Application Manager + changeloginformation = "Merged changes from EPage (proper changelog later)\nNew Icon by Strutten." + # 0.7.2 : Seek bar now responds to clicks (rather than drags)\nFixed bug with wrong text showing on button after changed chapter. + # 0.7.1 : Fixed crash when current points to non existing book + # 0.7.0 : Now ignores pressed outside the chapter selection menu\nAdded help + # 0.6.1 : Fixed bug that prevented running on devices without Audiobook folder. + # Added tip on where to place audiobooks. + # 0.6.0 : Now also plays .mp3 files + # 0.5.0 : Second release. Now shows which chapter is playing, and scrolls to it when changing. + # 0.4.9 : First release. Now it should work + # + dir_name = "src" #Name of the subfolder containing your package + #source files + #(e.g. usr\share\icons\hicolor\scalable\myappicon.svg, + #usr\lib\myapp\somelib.py). We suggest + #to leave it named src in all projects + #and will refer to that in the wiki + #article on maemo.org + + #Thanks to DareTheHair from talk.maemo.org for this snippet that + #recursively builds the file list + for root, dirs, files in os.walk(dir_name): + real_dir = root[len(dir_name):] + if '.' in real_dir: + continue # if some part of the dirname contains '.' we + # ignore all files (avoid .svn + # and others) + fake_file = [] + for f in files: + fake_file.append(root + os.sep + f + "|" + f) + if len(fake_file) > 0: + p[real_dir] = fake_file + print p + r = p.generate(version,build,changelog=changeloginformation,tar=True,dsc=True,changes=True,build=False,src=True) diff --git a/build_nqaap_maemo5.py b/build_nqaap_maemo5.py new file mode 100644 index 0000000..5854d83 --- /dev/null +++ b/build_nqaap_maemo5.py @@ -0,0 +1,82 @@ +#!/usr/bin/python2.5 + # -*- coding: utf-8 -*- + ## This program 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; version 2 only. + ## + ## 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 General Public License for more details. + ## +import py2deb +import os +if __name__ == "__main__": + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + print + p=py2deb.Py2deb("nqaap") #This is the package name and MUST be in + #lowercase! (using e.g. "mClock" fails + #miserably...) + p.description="Very simple Audiobook player. \nSupports playing, pausing, seeking (sort of) and saving state when changing book/closing.\nPlays books arranged as dirs under myDocs/Audiobooks" + p.author="Soeren 'Pengman' Pedersen" + p.mail="pengmeister@gmail.com" + p.depends = "python2.5, python2.5-gtk2, python2.5-dbus, python2.5-telepathy, python2.5-gobject, python-gst0.10" + p.section="user/multimedia" + p.icon = "/usr/share/icons/hicolor/48x48/hildon/nqaap.png" + p.arch="all" #should be all for python, any for all arch + p.urgency="low" #not used in maemo onl for deb os + p.distribution="fremantle" + p.repository="extras-devel" + p.xsbc_bugtracker="http://talk.maemo.org/showthread.php?p=619738" + # p.postinstall="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your post install script + # p.postremove="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your post remove script + # p.preinstall="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your pre install script + # p.preremove="""#!/bin/sh + # chmod +x /usr/bin/mclock.py""" #Set here your pre remove script + version = "0.8.0" #Version of your software, e.g. "1.2.0" or "0.8.2" + build = "2" #Build number, e.g. "1" for the first build of this + #version of your software. Increment + #for later re-builds of the same + #version of your software. Text with + #changelog information to be displayed + #in the package "Details" tab of the + #Maemo Application Manager + changeloginformation = "Merged changes from EPage (proper changelog later)\nNew Icon by Strutten." + # 0.7.2 : Seek bar now responds to clicks (rather than drags)\nFixed bug with wrong text showing on button after changed chapter. + # 0.7.1 : Fixed crash when current points to non existing book + # 0.7.0 : Now ignores pressed outside the chapter selection menu\nAdded help + # 0.6.1 : Fixed bug that prevented running on devices without Audiobook folder. + # Added tip on where to place audiobooks. + # 0.6.0 : Now also plays .mp3 files + # 0.5.0 : Second release. Now shows which chapter is playing, and scrolls to it when changing. + # 0.4.9 : First release. Now it should work + # + dir_name = "src" #Name of the subfolder containing your package + #source files + #(e.g. usr\share\icons\hicolor\scalable\myappicon.svg, + #usr\lib\myapp\somelib.py). We suggest + #to leave it named src in all projects + #and will refer to that in the wiki + #article on maemo.org + + #Thanks to DareTheHair from talk.maemo.org for this snippet that + #recursively builds the file list + for root, dirs, files in os.walk(dir_name): + real_dir = root[len(dir_name):] + if '.' in real_dir: + continue # if some part of the dirname contains '.' we + # ignore all files (avoid .svn + # and others) + fake_file = [] + for f in files: + fake_file.append(root + os.sep + f + "|" + f) + if len(fake_file) > 0: + p[real_dir] = fake_file + print p + r = p.generate(version,build,changelog=changeloginformation,tar=True,dsc=True,changes=True,build=False,src=True) diff --git a/src/opt/Nqa-Audiobook-player/.ropeproject/config.py b/src/opt/Nqa-Audiobook-player/.ropeproject/config.py new file mode 100644 index 0000000..ffebcd4 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/.ropeproject/config.py @@ -0,0 +1,85 @@ +# The default ``config.py`` + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + #prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + #prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + #prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/src/opt/Nqa-Audiobook-player/Audiobook.py b/src/opt/Nqa-Audiobook-player/Audiobook.py new file mode 100644 index 0000000..00fac87 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/Audiobook.py @@ -0,0 +1,176 @@ +from __future__ import with_statement + +import os + +import logging + + +_moduleLogger = logging.getLogger(__name__) + + +class Audiobook(object): + + def __init__(self, path, current_chapter = 0): + self.title = "" + self._coverPath = "" + self._chapterPaths = [] + self.current_chapter = current_chapter + + if is_playlist_book(path): + self._scan_index(path) + elif is_dir_book(path): + self._scan_dir(path) + elif is_single_chapter(path): + self._scan_chapter(path) + else: + _moduleLogger.info("Audiobook not found in path: " + path) + raise IOError("Audiobook directory not found") + + if len(self._chapterPaths) <= self.current_chapter: + _moduleLogger.warning( + "Audiobook chapter out of range (%s/%s)" % ( + self.current_chapter, len(self._chapterPaths) + ) + ) + self.current_chapter = 0 + + @property + def chapters(self): + return self._chapterPaths + + def get_current_chapter(self): + return self._chapterPaths[self.current_chapter] + + def set_chapter(self, chapter): + if chapter in self.chapters: + self.current_chapter = self.chapters.index(chapter) + else: + raise Exception("Unknown chapter set") + + def get_previous_chapter(self): + """ + @returns the file name for the next chapter, without path + """ + if 0 == self.current_chapter: + return False + else: + self.current_chapter -= 1 + return self._chapterPaths[self.current_chapter] + + def get_next_chapter(self): + """ + @returns the file name for the next chapter, without path + """ + if len(self._chapterPaths) == self.current_chapter: + return False + else: + self.current_chapter += 1 + return self._chapterPaths[self.current_chapter] + + def get_cover_img(self): + if self._coverPath: + return self._coverPath + else: + return "%s/NoCover.png" % os.path.dirname(__file__) + + def _scan_dir(self, root): + self.title = os.path.split(root)[-1] + dirContent = ( + os.path.join(root, f) + for f in os.listdir(root) + if not f.startswith(".") + ) + + files = [ + path + for path in dirContent + if os.path.isfile(os.path.join(root, path)) + ] + + images = [ + path + for path in files + if path.rsplit(".", 1)[-1] in ["png", "gif", "jpg", "jpeg"] + ] + if 0 < len(images): + self.cover = images[0] + + self._chapterPaths = [ + path + for path in files + if is_single_chapter(path) + ] + self._chapterPaths.sort() + + def _scan_chapter(self, file): + self._chapterPaths = [file] + + def _scan_playlist(self, file): + root = os.path.dirname(file) + self.title = os.path.basename(file).rsplit(".")[0] + + with open(file, 'r') as f: + for line in f: + if line.startswith("#"): + continue + path = line + if not os.path.isabs(path): + path = os.path.normpath(os.path.join(root, path)) + self._chapterPaths.append(path) + # Not sorting, assuming the file is in the desired order + + def _scan_index(self, file): + import unicodedata + + # Reading file + looking_for_title = False + looking_for_cover = False + looking_for_chapters = False + + with open(file, 'r') as f: + for line in f: + # title + ascii = unicodedata.normalize('NFKD', unicode(line, "latin-1")).encode('ascii', 'ignore') + print line[:-1], "PIC\n" in line, line in "#PIC" + if "#BOOK" in line: + looking_for_title = True + continue + if looking_for_title: + self.title = line[:-1] + looking_for_title = False + if "#PIC" in line: + looking_for_cover = True + continue + if looking_for_cover: + self.cover = line[:-1] + looking_for_cover = False + if "#TRACKS" in line: + looking_for_chapters = True + continue + if looking_for_chapters: + if "#CHAPTERS" in line: + break # no further information needed + self.chapters.append(line.split(':')[0]) + + +def is_dir_book(path): + return os.path.isdir(path) + + +def is_playlist_book(path): + return path.rsplit(".", 1)[-1] in ["m3u"] + + +def is_single_chapter(path): + return path.rsplit(".", 1)[-1] in ["awb", "mp3", "spx", "ogg", "ac3", "wav"] + + +def is_book(path): + if is_dir_book(path): + return True + elif is_playlist_book(path): + return True + elif is_single_chapter(path): + return True + else: + return False diff --git a/src/opt/Nqa-Audiobook-player/Browser.py b/src/opt/Nqa-Audiobook-player/Browser.py new file mode 100644 index 0000000..36d4e34 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/Browser.py @@ -0,0 +1,5 @@ +import os + + +def open(url): + os.system('dbus-send --system --type=method_call --dest="com.nokia.osso_browser" /com/nokia/osso_browser/request com.nokia.osso_browser.load_url string:"%s"' % url) diff --git a/src/opt/Nqa-Audiobook-player/CallMonitor.py b/src/opt/Nqa-Audiobook-player/CallMonitor.py new file mode 100644 index 0000000..97be5a7 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/CallMonitor.py @@ -0,0 +1,145 @@ +import logging + +import gobject +import dbus +import telepathy + +import gtk_toolbox + + +_moduleLogger = logging.getLogger(__name__) +DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' + + +class NewChannelSignaller(object): + + def __init__(self, on_new_channel): + self._sessionBus = dbus.SessionBus() + self._on_user_new_channel = on_new_channel + + def start(self): + self._sessionBus.add_signal_receiver( + self._on_new_channel, + "NewChannel", + "org.freedesktop.Telepathy.Connection", + None, + None + ) + + def stop(self): + self._sessionBus.remove_signal_receiver( + self._on_new_channel, + "NewChannel", + "org.freedesktop.Telepathy.Connection", + None, + None + ) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_new_channel( + self, channelObjectPath, channelType, handleType, handle, supressHandler + ): + connObjectPath = channel_path_to_conn_path(channelObjectPath) + serviceName = path_to_service_name(channelObjectPath) + try: + self._on_user_new_channel( + self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType + ) + except Exception: + _moduleLogger.exception("Blocking exception from being passed up") + + +class ChannelClosed(object): + + def __init__(self, bus, conn, chan, on_closed): + self.__on_closed = on_closed + + chan[telepathy.interfaces.CHANNEL].connect_to_signal( + "Closed", + self._on_closed, + ) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_closed(self): + self.__on_closed(self) + + +class CallMonitor(gobject.GObject): + + __gsignals__ = { + 'call_start' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (), + ), + 'call_end' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (), + ), + } + + def __init__(self): + gobject.GObject.__init__(self) + self._isActive = False + self._newChannelMonitor = NewChannelSignaller(self._on_new_channel) + self._channelClosedMonitors = [] + + def start(self): + self._isActive = True + self._newChannelMonitor.start() + + def stop(self): + self._isActive = False + self._newChannelMonitor.stop() + + def _on_new_channel(self, sessionBus, serviceName, connObjectPath, channelObjectPath, channelType): + if not self._isActive: + return + + if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA: + return + + cmName = cm_from_path(connObjectPath) + conn = telepathy.client.Connection(serviceName, connObjectPath) + try: + chan = telepathy.client.Channel(serviceName, channelObjectPath) + except dbus.exceptions.UnknownMethodException: + _moduleLogger.exception("Client might not have implemented a deprecated method") + return + + missDetection = ChannelClosed( + sessionBus, conn, chan, self._on_close + ) + self._outstandingRequests.append(missDetection) + if len(self._outstandingRequests) == 1: + self.emit("call_start") + + def _on_close(self, channelCloseMonitor): + self._outstandingRequests.remove(channelCloseMonitor) + if not self._outstandingRequests: + self.emit("call_stop") + + +def channel_path_to_conn_path(channelObjectPath): + """ + >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") + '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME' + """ + return channelObjectPath.rsplit("/", 1)[0] + + +def path_to_service_name(path): + """ + >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") + 'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME' + """ + return ".".join(path[1:].split("/")[0:7]) + + +def cm_from_path(path): + """ + >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") + 'theonering' + """ + return path[1:].split("/")[4] diff --git a/src/opt/Nqa-Audiobook-player/FileStorage.py b/src/opt/Nqa-Audiobook-player/FileStorage.py new file mode 100644 index 0000000..44cfc36 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/FileStorage.py @@ -0,0 +1,68 @@ +from __future__ import with_statement # enable with + +import os + +import logging + + +log = logging.getLogger(__name__) + + +class FileStorage(object): + + def __init__(self, path="~/.SornPlayer/"): + # Setup dir + log.info("init filestorage") + self.path = path + self.books_path = os.path.join(self.path, "books/") + if not os.path.isdir(self.books_path): + os.makedirs(self.books_path) + + # Read config file + self.conf = os.path.join(self.path, "current") + self.selected = None + + if os.path.isfile(self.conf): + with open(self.conf) as f: + self.selected = f.readline() + + # Read current book file + + def get_selected(self): + """returns the currently selected book""" + return self.selected + + def select_book(self, bookName): + """ Sets the book as the currently playing, and adds it to the + database if it is not already there""" + book_file = os.path.join(self.books_path, bookName) + if not os.path.isfile(book_file): + with open(book_file, 'w') as f: + f.write("0\n") #Current chapter + f.write("0\n") #Current position + + self.selected = bookName + with open(self.conf, 'w') as f: + f.write(self.selected) # + + def set_time(self, chapter, position): + """ Sets the current time for the book that is currently selected""" + try: + book_file = os.path.join(self.books_path, self.selected) + log.debug("writing time (%s, %s) to: %s"%( chapter, position, book_file )) + with open(book_file, 'w') as f: + f.write(str(int(chapter)) + "\n") #Current chapter + f.write(str(int(position)) + "\n") #Current position + except: + log.error("Unable to save to file: %s" % book_file) + + def get_time(self): + """Returns the current saved time for the current selected book""" + chapter, position = 0 , 0 + book_file = os.path.join(self.books_path, self.selected) + log.debug("getting time from: " + book_file) + with open(book_file, 'r') as f: + chapter = int(f.readline()) + position = int(f.readline()) + + return chapter, position diff --git a/src/opt/Nqa-Audiobook-player/Gui.py b/src/opt/Nqa-Audiobook-player/Gui.py new file mode 100644 index 0000000..0f51f36 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/Gui.py @@ -0,0 +1,594 @@ +from __future__ import with_statement + +import os +import ConfigParser +import logging + +import gobject +import gtk + +import constants +import hildonize +import gtk_toolbox +import Browser +import CallMonitor +import settings + +if hildonize.IS_FREMANTLE_SUPPORTED: + # I don't normally do this but I want to error as loudly as possibly when an issue arises + import hildon + + +_moduleLogger = logging.getLogger(__name__) + + +class Gui(object): + + def __init__(self): + _moduleLogger.info("Starting GUI") + self._callMonitor = CallMonitor.CallMonitor() + self.__settingsWindow = None + self.__settingsManager = None + self._bookSelection = [] + self._bookSelectionIndex = -1 + self._chapterSelection = [] + self._chapterSelectionIndex = -1 + self._sleepSelection = ["0", "1", "10", "20", "30", "60"] + self._sleepSelectionIndex = 0 + + self.__window_in_fullscreen = False #The window isn't in full screen mode initially. + self.__isPortrait = False + + self.controller = None + self.sleep_timer = None + self.auto_chapter_selected = False # true if we are in the + # midle of an automatic + # chapter change + + self.ignore_next_chapter_change = False + # set up gui + self.setup() + self._callMonitor.connect("call_start", self.__on_call_started) + self._callMonitor.start() + + def setup(self): + if hildonize.IS_FREMANTLE_SUPPORTED: + gtk.set_application_name(constants.__pretty_app_name__) # window title + self._app = hildonize.get_app_class()() + self.win = gtk.Window() + self.win = hildonize.hildonize_window(self._app, self.win) + + # Cover image + self.cover = gtk.Image() + + # Controls: + + # Label that hold the title of the book,and maybe the chapter + self.title = gtk.Label("Select a book to start listening") + self.title.set_justify(gtk.JUSTIFY_CENTER) + + # Seekbar + if hildonize.IS_FREMANTLE_SUPPORTED: + self.seek = hildon.Seekbar() + self.seek.set_range(0.0, 100) + self.seek.set_draw_value(False) + self.seek.set_update_policy(gtk.UPDATE_DISCONTINUOUS) + self.seek.connect('change-value', self.seek_changed) # event + # self.seek.connect('value-changed',self.seek_changed) # event + else: + adjustment = gtk.Adjustment(0, 0, 101, 1, 5, 1) + self.seek = gtk.HScale(adjustment) + self.seek.connect('change-value', self.seek_changed) # event + + # Pause button + if hildonize.IS_FREMANTLE_SUPPORTED: + self.backButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL) + image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.HILDON_SIZE_FINGER_HEIGHT) + self.backButton.set_image(image) + + self.button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL) + + self.forwardButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL) + image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.HILDON_SIZE_FINGER_HEIGHT) + self.forwardButton.set_image(image) + else: + self.backButton = gtk.Button(stock=gtk.STOCK_GO_BACK) + self.button = gtk.Button() + self.forwardButton = gtk.Button(stock=gtk.STOCK_GO_FORWARD) + self.set_button_text("Play", "Start playing the audiobook") + self.backButton.connect('clicked', self._on_previous_chapter) + self.button.connect('clicked', self.play_pressed) # event + self.forwardButton.connect('clicked', self._on_next_chapter) + + self._toolbar = gtk.HBox() + self._toolbar.pack_start(self.backButton, False, False, 0) + self._toolbar.pack_start(self.button, True, True, 0) + self._toolbar.pack_start(self.forwardButton, False, False, 0) + + # Box to hold the controls: + self._controlLayout = gtk.VBox() + self._controlLayout.pack_start(self.title, True, True, 0) + self._controlLayout.pack_start(self.seek, True, True, 0) + self._controlLayout.pack_start(self._toolbar, False, True, 0) + + #Box that divides the layout in two: cover on the lefta + #and controls on the right + self._viewLayout = gtk.HBox() + self._viewLayout.pack_start(self.cover, True, True, 0) + self._viewLayout.add(self._controlLayout) + + self._menuBar = gtk.MenuBar() + self._menuBar.show() + + self._mainLayout = gtk.VBox() + self._mainLayout.pack_start(self._menuBar, False, False, 0) + self._mainLayout.pack_start(self._viewLayout) + + # Add hbox to the window + self.win.add(self._mainLayout) + + #Menu: + # Create menu + self._populate_menu() + + self.win.connect("delete_event", self.quit) # Add shutdown event + self.win.connect("key-press-event", self.on_key_press) + self.win.connect("window-state-event", self._on_window_state_change) + + self.win.show_all() + + # Run update timer + self.setup_timers() + + def _populate_menu(self): + self._menuBar = hildonize.hildonize_menu( + self.win, + self._menuBar, + ) + if hildonize.IS_FREMANTLE_SUPPORTED: + # Create a picker button + self.book_button = hildon.Button(gtk.HILDON_SIZE_AUTO, + hildon.BUTTON_ARRANGEMENT_VERTICAL) + self.book_button.set_title("Audiobook") # Set a title to the button + self.book_button.connect("clicked", self._on_select_audiobook) + + # Create a picker button + self.chapter_button = hildon.Button(gtk.HILDON_SIZE_AUTO, + hildon.BUTTON_ARRANGEMENT_VERTICAL) + self.chapter_button.set_title("Chapter") # Set a title to the button + self.chapter_button.connect("clicked", self._on_select_chapter) + + # Create a picker button + self.sleeptime_button = hildon.Button(gtk.HILDON_SIZE_AUTO, + hildon.BUTTON_ARRANGEMENT_VERTICAL) + self.sleeptime_button.set_title("Sleeptimer") # Set a title to the button + self.sleeptime_button.connect("clicked", self._on_select_sleep) + + settings_button = hildon.Button(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL) + settings_button.set_label("Settings") + settings_button.connect("clicked", self._on_settings) + + help_button = hildon.Button(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL) + help_button.set_label("Help") + help_button.connect("clicked", self.get_help) + + self._menuBar.append(self.book_button) # Add the button to menu + self._menuBar.append(self.chapter_button) # Add the button to menu + self._menuBar.append(self.sleeptime_button) # Add the button to menu + self._menuBar.append(settings_button) + self._menuBar.append(help_button) + self._menuBar.show_all() + else: + self._audiobookMenuItem = gtk.MenuItem("Audiobook: ") + self._audiobookMenuItem.connect("activate", self._on_select_audiobook) + + self._chapterMenuItem = gtk.MenuItem("Chapter: ") + self._chapterMenuItem.connect("activate", self._on_select_chapter) + + self._sleepMenuItem = gtk.MenuItem("Sleeptimer: 0") + self._sleepMenuItem.connect("activate", self._on_select_sleep) + + settingsMenuItem = gtk.MenuItem("Settings") + settingsMenuItem.connect("activate", self._on_settings) + + helpMenuItem = gtk.MenuItem("Help") + helpMenuItem.connect("activate", self.get_help) + + booksMenu = gtk.Menu() + booksMenu.append(self._audiobookMenuItem) + booksMenu.append(self._chapterMenuItem) + booksMenu.append(self._sleepMenuItem) + booksMenu.append(settingsMenuItem) + booksMenu.append(helpMenuItem) + + booksMenuItem = gtk.MenuItem("Books") + booksMenuItem.show() + booksMenuItem.set_submenu(booksMenu) + self._menuBar.append(booksMenuItem) + self._menuBar.show_all() + + def setup_timers(self): + self.seek_timer = timeout_add_seconds(3, self.update_seek) + + def save_settings(self): + config = ConfigParser.SafeConfigParser() + self._save_settings(config) + with open(constants._user_settings_, "wb") as configFile: + config.write(configFile) + + def _save_settings(self, config): + config.add_section(constants.__pretty_app_name__) + config.set(constants.__pretty_app_name__, "portrait", str(self.__isPortrait)) + config.set(constants.__pretty_app_name__, "fullscreen", str(self.__window_in_fullscreen)) + config.set(constants.__pretty_app_name__, "audiopath", self.controller.get_books_path()) + + def load_settings(self): + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + self._load_settings(config) + + def _load_settings(self, config): + isPortrait = False + window_in_fullscreen = False + booksPath = constants._default_book_path_ + try: + isPortrait = config.getboolean(constants.__pretty_app_name__, "portrait") + window_in_fullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen") + booksPath = config.get(constants.__pretty_app_name__, "audiopath") + except ConfigParser.NoSectionError, e: + _moduleLogger.info( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ) + ) + + if isPortrait ^ self.__isPortrait: + if isPortrait: + orientation = gtk.ORIENTATION_VERTICAL + else: + orientation = gtk.ORIENTATION_HORIZONTAL + self.set_orientation(orientation) + + self.__window_in_fullscreen = window_in_fullscreen + if self.__window_in_fullscreen: + self.win.fullscreen() + else: + self.win.unfullscreen() + + self.controller.load_books_path(booksPath) + + @staticmethod + def __format_name(path): + if os.path.isfile(path): + return os.path.basename(path).rsplit(".", 1)[0] + else: + return os.path.basename(path) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_select_audiobook(self, *args): + if not self._bookSelection: + return + index = hildonize.touch_selector( + self.win, + "Audiobook", + (self.__format_name(bookPath) for bookPath in self._bookSelection), + self._bookSelectionIndex if 0 <= self._bookSelectionIndex else 0, + ) + self._bookSelectionIndex = index + bookName = self._bookSelection[index] + self.controller.set_book(bookName) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_select_chapter(self, *args): + if not self._chapterSelection: + return + index = hildonize.touch_selector( + self.win, + "Chapter", + (self.__format_name(chapterPath) for chapterPath in self._chapterSelection), + self._chapterSelectionIndex if 0 <= self._chapterSelectionIndex else 0, + ) + self._chapterSelectionIndex = index + chapterName = self._chapterSelection[index] + self.controller.set_chapter(chapterName) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_select_sleep(self, *args): + if self.sleep_timer is not None: + gobject.source_remove(self.sleep_timer) + + try: + index = hildonize.touch_selector( + self.win, + "Sleeptimer", + self._sleepSelection, + self._sleepSelectionIndex if 0 <= self._sleepSelectionIndex else 0, + ) + except RuntimeError: + _moduleLogger.exception("Handling as if user cancelled") + hildonize.show_information_banner(self.win, "Sleep timer canceled") + index = 0 + + self._sleepSelectionIndex = index + sleepName = self._sleepSelection[index] + + time_out = int(sleepName) + if 0 < time_out: + timeout_add_seconds(time_out * 60, self.sleep) + + if hildonize.IS_FREMANTLE_SUPPORTED: + self.sleeptime_button.set_text("Sleeptimer", sleepName) + else: + self._sleepMenuItem.get_child().set_text("Sleeptimer: %s" % (sleepName, )) + + @gtk_toolbox.log_exception(_moduleLogger) + def __on_call_started(self, callMonitor): + self.pause() + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_settings(self, *args): + if self.__settingsWindow is None: + vbox = gtk.VBox() + self.__settingsManager = settings.SettingsDialog(vbox) + + self.__settingsWindow = gtk.Window() + self.__settingsWindow.add(vbox) + self.__settingsWindow = hildonize.hildonize_window(self._app, self.__settingsWindow) + self.__settingsManager.window = self.__settingsWindow + + self.__settingsWindow.set_title("Settings") + self.__settingsWindow.set_transient_for(self.win) + self.__settingsWindow.set_default_size(*self.win.get_size()) + self.__settingsWindow.connect("delete-event", self._on_settings_delete) + self.__settingsManager.set_portrait_state(self.__isPortrait) + self.__settingsManager.set_audiobook_path(self.controller.get_books_path()) + self.__settingsWindow.set_modal(True) + self.__settingsWindow.show_all() + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_settings_delete(self, *args): + self.__settingsWindow.emit_stop_by_name("delete-event") + self.__settingsWindow.hide() + self.__settingsWindow.set_modal(False) + + isPortrait = self.__settingsManager.is_portrait() + if isPortrait ^ self.__isPortrait: + if isPortrait: + orientation = gtk.ORIENTATION_VERTICAL + else: + orientation = gtk.ORIENTATION_HORIZONTAL + self.set_orientation(orientation) + if self.__settingsManager.get_audiobook_path() != self.controller.get_books_path(): + self.controller.load_books_path(self.__settingsManager.get_audiobook_path()) + + return True + + @gtk_toolbox.log_exception(_moduleLogger) + def update_seek(self): + #print self.controller.get_percentage() + if self.controller.is_playing(): + gtk.gdk.threads_enter() + self.seek.set_value(self.controller.get_percentage() * 100) + gtk.gdk.threads_leave() + #self.controller.get_percentage() + return True # run again + + @gtk_toolbox.log_exception(_moduleLogger) + def sleep(self): + _moduleLogger.info("sleep time timeout") + hildonize.show_information_banner(self.win, "Sleep timer") + self.controller.stop() + self.set_button_text("Resume", "Resume playing the audiobook") + return False # do not repeat + + @gtk_toolbox.log_exception(_moduleLogger) + def get_help(self, button): + Browser.open("file:///opt/Nqa-Audiobook-player/Help/nqaap.html") + + @gtk_toolbox.log_exception(_moduleLogger) + def seek_changed(self, seek, scroll , value): + # print "sok", scroll + self.controller.seek_percent(seek.get_value() / 100.0) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_next_chapter(self, *args): + self.controller.next_chapter() + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_previous_chapter(self, *args): + self.controller.previous_chapter() + + @gtk_toolbox.log_exception(_moduleLogger) + def play_pressed(self, button): + if self.controller.is_playing(): + self.pause() + else: + self.play() + + @gtk_toolbox.log_exception(_moduleLogger) + def on_key_press(self, widget, event, *args): + RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter) + isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK) + if ( + event.keyval == gtk.keysyms.F6 or + event.keyval in RETURN_TYPES and isCtrl + ): + # The "Full screen" hardware key has been pressed + if self.__window_in_fullscreen: + self.win.unfullscreen () + else: + self.win.fullscreen () + return True + elif event.keyval == gtk.keysyms.o and isCtrl: + self._toggle_rotate() + return True + elif ( + event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and + event.get_state() & gtk.gdk.CONTROL_MASK + ): + self.quit() + elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK: + with open(constants._user_logpath_, "r") as f: + logLines = f.xreadlines() + log = "".join(logLines) + self._clipboard.set_text(str(log)) + return True + elif event.keyval in RETURN_TYPES: + if self.controller.is_playing(): + self.pause() + else: + self.play() + return True + elif event.keyval == gtk.keysyms.Left: + self.controller.previous_chapter() + return True + elif event.keyval == gtk.keysyms.Right: + self.controller.next_chapter() + return True + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_window_state_change(self, widget, event, *args): + if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: + self.__window_in_fullscreen = True + else: + self.__window_in_fullscreen = False + + @gtk_toolbox.log_exception(_moduleLogger) + def quit(self, *args): # what are the arguments? + _moduleLogger.info("Shutting down") + try: + self.save_settings() + self.controller.stop() # to save the state + finally: + gtk.main_quit() + + # Actions: + + def play(self): + self.set_button_text("Stop", "Stop playing the audiobook") + self.controller.play() + + def pause(self): + self.set_button_text("Resume", "Resume playing the audiobook") + self.controller.stop() + + def set_orientation(self, orientation): + if orientation == gtk.ORIENTATION_VERTICAL: + if self.__isPortrait: + return + hildonize.window_to_portrait(self.win) + self.__isPortrait = True + + self._viewLayout.remove(self._controlLayout) + self._mainLayout.add(self._controlLayout) + elif orientation == gtk.ORIENTATION_HORIZONTAL: + if not self.__isPortrait: + return + hildonize.window_to_landscape(self.win) + self.__isPortrait = False + + self._mainLayout.remove(self._controlLayout) + self._viewLayout.add(self._controlLayout) + else: + raise NotImplementedError(orientation) + + def get_orientation(self): + return gtk.ORIENTATION_VERTICAL if self.__isPortrait else gtk.ORIENTATION_HORIZONTAL + + def _toggle_rotate(self): + if self.__isPortrait: + self.set_orientation(gtk.ORIENTATION_HORIZONTAL) + else: + self.set_orientation(gtk.ORIENTATION_VERTICAL) + + def change_chapter(self, chapterName): + if chapterName is None: + _moduleLogger.debug("chapter selection canceled.") + #import pdb; pdb.set_trace() # start debugger + self.ignore_next_chapter_change = True + return True # this should end the function and indicate it has been handled + + if self.ignore_next_chapter_change: + self.ignore_next_chapter_change = False + _moduleLogger.debug("followup chapter selection canceled.") + #import pdb; pdb.set_trace() # start debugger + return True # this should end the function and indicate it has been handled + + if self.auto_chapter_selected: + _moduleLogger.debug("chapter changed (by controller) to: %s" % chapterName) + self.auto_chapter_selected = False + # do nothing + else: + _moduleLogger.debug("chapter selection sendt to controller: %s" % chapterName) + self.controller.set_chapter(chapterName) # signal controller + self.set_button_text("Play", "Start playing the audiobook") # reset button + + def set_button_text(self, title, text): + if hildonize.IS_FREMANTLE_SUPPORTED: + self.button.set_text("Resume", "Resume playing the audiobook") + else: + self.button.set_label("%s - %s" % (title, text)) + + def set_books(self, books): + _moduleLogger.debug("new books") + del self._bookSelection[:] + self._bookSelection.extend(books) + if len(books) == 0 and self.controller is not None: + hildonize.show_information_banner(self.win, "No audiobooks found. \nPlease place your audiobooks in the directory %s" % self.controller.get_books_path()) + + def set_book(self, bookPath, cover): + bookName = self.__format_name(bookPath) + + self.set_button_text("Play", "Start playing the audiobook") # reset button + self.title.set_text(bookName) + if hildonize.IS_FREMANTLE_SUPPORTED: + self.book_button.set_text("Audiobook", bookName) + else: + self._audiobookMenuItem.get_child().set_text("Audiobook: %s" % (bookName, )) + if cover != "": + self.cover.set_from_file(cover) + + def set_chapter(self, chapterIndex): + ''' + Called from controller whenever a new chapter is started + + chapter parameter is supposed to be the index for the chapter, not the name + ''' + self.auto_chapter_selected = True + if hildonize.IS_FREMANTLE_SUPPORTED: + self.chapter_button.set_text("Chapter", str(chapterIndex)) + else: + self._chapterMenuItem.get_child().set_text("Chapter: %s" % (chapterIndex, )) + + def set_chapters(self, chapters): + _moduleLogger.debug("setting chapters" ) + del self._chapterSelection[:] + self._chapterSelection.extend(chapters) + + def set_sleep_timer(self, mins): + pass + + # Utils + def set_selected_value(self, button, value): + i = button.get_selector().get_model(0).index[value] # get index of value from list + button.set_active(i) # set active index to that index + + +def _old_timeout_add_seconds(timeout, callback): + return gobject.timeout_add(timeout * 1000, callback) + + +def _timeout_add_seconds(timeout, callback): + return gobject.timeout_add_seconds(timeout, callback) + + +try: + gobject.timeout_add_seconds + timeout_add_seconds = _timeout_add_seconds +except AttributeError: + timeout_add_seconds = _old_timeout_add_seconds + + +if __name__ == "__main__": + g = Gui(None) diff --git a/src/opt/Nqa-Audiobook-player/Help/nqaap.html b/src/opt/Nqa-Audiobook-player/Help/nqaap.html new file mode 100644 index 0000000..4bd4598 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/Help/nqaap.html @@ -0,0 +1,111 @@ + + + + nQa Audiobook Player + + + + + + +

nQa Audiobook Player

+ + +
+

Table of Contents

+
+ +
+
+ +
+

1 Create book

+
+ +
    +
  • + Find audiofiles for the book in either mp3 or awb format (encoding + instructions later) +
  • +
  • + Ensure that files are named in such a way that a lexical sort places + them in playing order. +
  • +
  • + Create a directory in ~/MyDocs/Audiobooks (create this directory too if it + does not exist) with the name of the book. +
  • +
  • + Place audiofiles in this dir, all files must be in this + directory. It cant find files in further subdirectories. +
  • +
  • + (Optional) Place an imagefile in the directory. If prsent this file + will be used as the cover of the book. +
  • +
+
+ +
+ +
+

2 Play

+
+ +
    +
  • + Open nqaap +
  • +
  • + Open menu +
  • +
  • + Select book +
  • +
  • + Press play +
  • +
+
+
+

Author: Søren 'Pengman' Pedersen + <pengmeister@hotmail.com> +

+

Date: 2010-04-17 21:23:57

+

HTML generated by org-mode 6.09a in emacs 23

+
+ diff --git a/src/opt/Nqa-Audiobook-player/NoCover.png b/src/opt/Nqa-Audiobook-player/NoCover.png new file mode 100644 index 0000000000000000000000000000000000000000..80e142baf6f4576a89f36c87c7c7b05eab2cd40f GIT binary patch literal 18504 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 literal 0 HcmV?d00001 diff --git a/src/opt/Nqa-Audiobook-player/Player.py b/src/opt/Nqa-Audiobook-player/Player.py new file mode 100644 index 0000000..522b2fa --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/Player.py @@ -0,0 +1,185 @@ +import os +import threading +import time +import logging + +import constants +import hildonize +import Audiobook +import FileStorage + + +_moduleLogger = logging.getLogger(__name__) + + +class Player(object): + + def __init__(self, ui): + self.storage = FileStorage.FileStorage(path = constants._data_path_) + if hildonize.IS_HILDON_SUPPORTED and not hildonize.IS_FREMANTLE_SUPPORTED: + import SimpleOSSOPlayer as _SimplePlayer + SimpleGStreamer = _SimplePlayer # silence PyFlakes + else: + import SimpleGStreamer as SimplePlayer + self.player = SimplePlayer.SimplePlayer(self.next_chapter) + self.ui = ui + self.audiobook = None + self._bookDir = None + self._bookPaths = {} + self.load_books_path(constants._default_book_path_) + + def get_books_path(self): + return self._bookDir + + def load_books_path(self, booksPath): + if self.audiobook is not None: + position = self.player.elapsed() + self.storage.set_time(self.audiobook.current_chapter, position) + + self._bookDir = booksPath + + self._bookPaths = dict( + (self.__format_name(bookPath), bookPath) + for bookPath in self._find_books() + ) + if self.ui is not None: + bookPaths = self._bookPaths.values() + bookPaths.sort() + self.ui.set_books(bookPaths) + + lastBookName = self.storage.get_selected() + if lastBookName is not None: + _moduleLogger.info("continuing book: %s" % lastBookName) + try: + bookPath = self._bookPaths[lastBookName] + self.set_book(bookPath) + except KeyError: + _moduleLogger.info("Audiobook was not found") + except IOError: + _moduleLogger.info("Audiobook could not be loaded") + + @staticmethod + def __format_name(path): + if os.path.isfile(path): + return os.path.basename(path).rsplit(".", 1)[0] + else: + return os.path.basename(path) + + def set_book(self, bookPath): + oldBookName = self.storage.get_selected() + try: + bookName = self.__format_name(bookPath) + self.storage.select_book(bookName) + chapter_num, _ = self.storage.get_time() + self.audiobook = Audiobook.Audiobook( + bookPath, + chapter_num + ) + except: + self.storage.select_book(oldBookName) + raise + + # self.player.set_file(self.audiobook.get_current_chapter()) + # self.player.seek_time(time) + + if self.ui is not None: + self.ui.set_book(bookPath, self.audiobook.get_cover_img()) + self.ui.set_chapters(self.audiobook.chapters) + + chapter_title = self.audiobook.chapters[chapter_num] + self.set_chapter(chapter_title, True) + + def set_chapter(self, chapter, continuing = False): + _moduleLogger.info("set chapter:" + chapter + " : Continuing: " + str(continuing)) + self.audiobook.set_chapter(chapter) + self.player.set_file(self.audiobook.get_current_chapter()) + if not continuing: + self.storage.set_time(self.audiobook.current_chapter, 0) + + if self.ui is not None: + self.ui.set_chapter(self.audiobook.current_chapter) + + def previous_chapter(self, *args): + _moduleLogger.info("Going back a chapter") + self.player.stop() + next_file = self.audiobook.get_previous_chapter() + if next_file is not False: + self.set_chapter(next_file) + self.player.play() + else: # the book is over + self.storage.set_time(0, 0) + + def next_chapter(self, *args): + _moduleLogger.info("Advancing a chapter") + self.player.stop() + next_file = self.audiobook.get_next_chapter() + if next_file is not False: + self.set_chapter(next_file) + self.player.play() + else: # the book is over + self.storage.set_time(0, 0) + + def play(self): + if self.audiobook is not None: + self.player.play() + _, target_time = self.storage.get_time() + if 0 < target_time: + time.sleep(1) + self.player.seek_time(target_time) + #print self.player.elapsed() + else: + print "No book selected, find one in ", self._bookDir + + def stop(self): + position = self.player.elapsed() + self.player.stop() + + if self.audiobook is not None: + self.storage.set_time(self.audiobook.current_chapter, position) + + def is_playing(self): + return self.player.playing + + def sleeptimer(self, secs): + #print "sleeper", secs + time.sleep(secs) + #print "now its time to sleep" + self.stop() + + def start_sleeptimer(self, secs): + #print "startin sleep" + sleep_thread = threading.Thread(target=self.sleeptimer, args=(secs, )) + sleep_thread.start() + #print "started sleep" + + def get_percentage(self): + try: + return float(self.player.elapsed()) / float(self.player.duration()) + except ZeroDivisionError: + return 0.0 + + def seek_percent(self, ratio): + try: + target = self.player.duration() * ratio # Calculate where to seek + self.player.seek_time(int(target)) # seek + + position = self.player.elapsed() + self.storage.set_time(self.audiobook.current_chapter, target) # save position + return True + except: + _moduleLogger.excetion("Seek failed") + return False + + def _find_books(self): + try: + paths = ( + os.path.join(self._bookDir, f) + for f in os.listdir(self._bookDir) + ) + return ( + path + for path in paths + if Audiobook.is_book(path) + ) + except OSError: + return () diff --git a/src/opt/Nqa-Audiobook-player/SimpleGStreamer.py b/src/opt/Nqa-Audiobook-player/SimpleGStreamer.py new file mode 100644 index 0000000..75fce6b --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/SimpleGStreamer.py @@ -0,0 +1,93 @@ +import os +import logging + +import gst + +import gtk_toolbox + + +_moduleLogger = logging.getLogger(__name__) + + +class SimplePlayer(object): + + def __init__(self, on_playing_done = None): + #Fields + self.has_file = False + self.playing = False + self.__elapsed = 0 + + #Event callbacks + self.on_playing_done = on_playing_done + + #Set up GStreamer + self.player = gst.element_factory_make("playbin2", "player") + fakesink = gst.element_factory_make("fakesink", "fakesink") + self.player.set_property("video-sink", fakesink) + bus = self.player.get_bus() + bus.add_signal_watch() + bus.connect("message", self.on_message) + + #Constants + self.time_format = gst.Format(gst.FORMAT_TIME) + self.seek_flag = gst.SEEK_FLAG_FLUSH + + @gtk_toolbox.log_exception(_moduleLogger) + def on_message(self, bus, message): + t = message.type + if t == gst.MESSAGE_EOS: # End-Of-Stream + self.player.set_state(gst.STATE_NULL) + self.playing = False + if self.on_playing_done is not None: # event callback + self.on_playing_done(self) + elif t == gst.MESSAGE_ERROR: + self.player.set_state(gst.STATE_NULL) + err, debug = message.parse_error() + #print "Error: %s" % err, debug + _moduleLogger.error("Error: %s, (%s)" % (err, debug)) + self.playing = False + + def set_file(self, file): + _moduleLogger.info("set file: %s", file) + if os.path.isfile(file): + if self.playing: + self.stop() + + file = os.path.abspath(file) # ensure absolute path + _moduleLogger.debug("set file (absolute path): %s "%file) + self.player.set_property("uri", "file://" + file) + self.has_file = True + else: + _moduleLogger.error("File: %s not found" % file) + + def play(self): + _moduleLogger.info("Started playing") + self.player.set_state(gst.STATE_PLAYING) + self.playing = True + + def stop(self): + self.player.set_state(gst.STATE_NULL) + self.playing = False + _moduleLogger.info("Stopped playing") + + def elapsed(self): + try: + self.__elapsed = self.player.query_position(self.time_format, None)[0] + except: + pass + return self.__elapsed + + def duration(self): + try: + return self.player.query_duration(self.time_format, None)[0] + except: + _moduleLogger.exception("Query failed") + return 0 + + def seek_time(self, ns): + _moduleLogger.debug("Seeking to: %s", ns) + self.player.seek_simple(self.time_format, self.seek_flag, ns) + + def __seek_percent(self, percent): + format = gst.Format(gst.FORMAT_PERCENT) + self.player.seek_simple(format, self.seek_flag, percent) diff --git a/src/opt/Nqa-Audiobook-player/SimpleOSSOPlayer.py b/src/opt/Nqa-Audiobook-player/SimpleOSSOPlayer.py new file mode 100644 index 0000000..9129f1a --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/SimpleOSSOPlayer.py @@ -0,0 +1,115 @@ +import os +import logging + +import dbus + +import gtk_toolbox + + +_moduleLogger = logging.getLogger(__name__) + + +class SimplePlayer(object): + + SERVICE_NAME = "com.nokia.osso_media_server" + OBJECT_PATH = "/com/nokia/osso_media_server" + AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music" + + def __init__(self, on_playing_done = None): + #Fields + self.has_file = False + self.playing = False + self.__elapsed = 0 + + #Event callbacks + self.on_playing_done = on_playing_done + + session_bus = dbus.SessionBus() + + # Get the osso-media-player proxy object + oms_object = session_bus.get_object( + self.SERVICE_NAME, + self.OBJECT_PATH, + introspect=False, + follow_name_owner_changes=True, + ) + # Use the audio interface + oms_audio_interface = dbus.Interface( + oms_object, + self.AUDIO_INTERFACE_NAME, + ) + self._audioProxy = oms_audio_interface + + self._audioProxy.connect_to_signal("state_changed", self._on_state_changed) + self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream) + + error_signals = [ + "no_media_selected", + "file_not_found", + "type_not_found", + "unsupported_type", + "gstreamer", + "dsp", + "device_unavailable", + "corrupted_file", + "out_of_memory", + "audio_codec_not_supported", + ] + for error in error_signals: + self._audioProxy.connect_to_signal(error, self._on_error) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_error(self, *args): + self.playing = False + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_end_of_stream(self, *args): + self.playing = False + if self.on_playing_done is not None: # event callback + self.on_playing_done(self) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_state_changed(self, state): + _moduleLogger.info("State: %s", state) + + def set_file(self, file): + _moduleLogger.info("set file: %s", file) + if os.path.isfile(file): + if self.playing: + self.stop() + + uri = "file://" + file + self._audioProxy.set_media_location(uri) + self.has_file = True + else: + _moduleLogger.error("File: %s not found" % file) + + def play(self): + _moduleLogger.info("Started playing") + self._audioProxy.play() + self.playing = True + + def stop(self): + self._audioProxy.stop() + self.playing = False + _moduleLogger.info("Stopped playing") + + def elapsed(self): + pos_info = self._audioProxy.get_position() + if isinstance(pos_info, tuple): + pos, _ = pos_info + return pos + else: + return 0 + + def duration(self): + pos_info = self._audioProxy.get_position() + if isinstance(pos_info, tuple): + _, dur = pos_info + return dur + else: + return 0 + + def seek_time(self, ns): + _moduleLogger.debug("Seeking to: %s", ns) + self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) ) diff --git a/src/opt/Nqa-Audiobook-player/__init__.py b/src/opt/Nqa-Audiobook-player/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/src/opt/Nqa-Audiobook-player/constants.py b/src/opt/Nqa-Audiobook-player/constants.py new file mode 100644 index 0000000..9f7bea9 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/constants.py @@ -0,0 +1,11 @@ +import os + +__pretty_app_name__ = "nQa Audiobook Player" +__app_name__ = "nqaap" +__version__ = "0.7.2-1" +__build__ = 0 +__app_magic__ = 0xdeadbeef +_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__) +_user_settings_ = "%s/settings.ini" % _data_path_ +_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__) +_default_book_path_ = os.path.join(os.path.expanduser("~"), "MyDocs/Audiobooks/") diff --git a/src/opt/Nqa-Audiobook-player/gtk_toolbox.py b/src/opt/Nqa-Audiobook-player/gtk_toolbox.py new file mode 100644 index 0000000..784c871 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/gtk_toolbox.py @@ -0,0 +1,577 @@ +#!/usr/bin/python + +from __future__ import with_statement + +import os +import errno +import sys +import time +import itertools +import functools +import contextlib +import logging +import threading +import Queue + +import gobject +import gtk + + +_moduleLogger = logging.getLogger(__name__) + + +def get_screen_orientation(): + width, height = gtk.gdk.get_default_root_window().get_size() + if width < height: + return gtk.ORIENTATION_VERTICAL + else: + return gtk.ORIENTATION_HORIZONTAL + + +def orientation_change_connect(handler, *args): + """ + @param handler(orientation, *args) -> None(?) + """ + initialScreenOrientation = get_screen_orientation() + orientationAndArgs = list(itertools.chain((initialScreenOrientation, ), args)) + + def _on_screen_size_changed(screen): + newScreenOrientation = get_screen_orientation() + if newScreenOrientation != orientationAndArgs[0]: + orientationAndArgs[0] = newScreenOrientation + handler(*orientationAndArgs) + + rootScreen = gtk.gdk.get_default_root_window() + return gtk.connect(rootScreen, "size-changed", _on_screen_size_changed) + + +@contextlib.contextmanager +def flock(path, timeout=-1): + WAIT_FOREVER = -1 + DELAY = 0.1 + timeSpent = 0 + + acquired = False + + while timeSpent <= timeout or timeout == WAIT_FOREVER: + try: + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR) + acquired = True + break + except OSError, e: + if e.errno != errno.EEXIST: + raise + time.sleep(DELAY) + timeSpent += DELAY + + assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout) + + try: + yield fd + finally: + os.unlink(path) + + +@contextlib.contextmanager +def gtk_lock(): + gtk.gdk.threads_enter() + try: + yield + finally: + gtk.gdk.threads_leave() + + +def find_parent_window(widget): + while True: + parent = widget.get_parent() + if isinstance(parent, gtk.Window): + return parent + widget = parent + + +def make_idler(func): + """ + Decorator that makes a generator-function into a function that will continue execution on next call + """ + a = [] + + @functools.wraps(func) + def decorated_func(*args, **kwds): + if not a: + a.append(func(*args, **kwds)) + try: + a[0].next() + return True + except StopIteration: + del a[:] + return False + + return decorated_func + + +def asynchronous_gtk_message(original_func): + """ + @note Idea came from http://www.aclevername.com/articles/python-webgui/ + """ + + def execute(allArgs): + args, kwargs = allArgs + with gtk_lock(): + original_func(*args, **kwargs) + return False + + @functools.wraps(original_func) + def delayed_func(*args, **kwargs): + gobject.idle_add(execute, (args, kwargs)) + + return delayed_func + + +def synchronous_gtk_message(original_func): + """ + @note Idea came from http://www.aclevername.com/articles/python-webgui/ + """ + + @functools.wraps(original_func) + def immediate_func(*args, **kwargs): + with gtk_lock(): + return original_func(*args, **kwargs) + + return immediate_func + + +def autostart(func): + """ + >>> @autostart + ... def grep_sink(pattern): + ... print "Looking for %s" % pattern + ... while True: + ... line = yield + ... if pattern in line: + ... print line, + >>> g = grep_sink("python") + Looking for python + >>> g.send("Yeah but no but yeah but no") + >>> g.send("A series of tubes") + >>> g.send("python generators rock!") + python generators rock! + >>> g.close() + """ + + @functools.wraps(func) + def start(*args, **kwargs): + cr = func(*args, **kwargs) + cr.next() + return cr + + return start + + +@autostart +def printer_sink(format = "%s"): + """ + >>> pr = printer_sink("%r") + >>> pr.send("Hello") + 'Hello' + >>> pr.send("5") + '5' + >>> pr.send(5) + 5 + >>> p = printer_sink() + >>> p.send("Hello") + Hello + >>> p.send("World") + World + >>> # p.throw(RuntimeError, "Goodbye") + >>> # p.send("Meh") + >>> # p.close() + """ + while True: + item = yield + print format % (item, ) + + +@autostart +def null_sink(): + """ + Good for uses like with cochain to pick up any slack + """ + while True: + item = yield + + +@autostart +def comap(function, target): + """ + >>> p = printer_sink() + >>> cm = comap(lambda x: x+1, p) + >>> cm.send((0, )) + 1 + >>> cm.send((1.0, )) + 2.0 + >>> cm.send((-2, )) + -1 + """ + while True: + try: + item = yield + mappedItem = function(*item) + target.send(mappedItem) + except Exception, e: + _moduleLogger.exception("Forwarding exception!") + target.throw(e.__class__, str(e)) + + +def _flush_queue(queue): + while not queue.empty(): + yield queue.get() + + +@autostart +def queue_sink(queue): + """ + >>> q = Queue.Queue() + >>> qs = queue_sink(q) + >>> qs.send("Hello") + >>> qs.send("World") + >>> qs.throw(RuntimeError, "Goodbye") + >>> qs.send("Meh") + >>> qs.close() + >>> print [i for i in _flush_queue(q)] + [(None, 'Hello'), (None, 'World'), (, 'Goodbye'), (None, 'Meh'), (, None)] + """ + while True: + try: + item = yield + queue.put((None, item)) + except Exception, e: + queue.put((e.__class__, str(e))) + except GeneratorExit: + queue.put((GeneratorExit, None)) + raise + + +def decode_item(item, target): + if item[0] is None: + target.send(item[1]) + return False + elif item[0] is GeneratorExit: + target.close() + return True + else: + target.throw(item[0], item[1]) + return False + + +def nonqueue_source(queue, target): + isDone = False + while not isDone: + item = queue.get() + isDone = decode_item(item, target) + while not queue.empty(): + queue.get_nowait() + + +def threaded_stage(target, thread_factory = threading.Thread): + messages = Queue.Queue() + + run_source = functools.partial(nonqueue_source, messages, target) + thread = thread_factory(target=run_source) + thread.setDaemon(True) + thread.start() + + # Sink running in current thread + return queue_sink(messages) + + +def log_exception(logger): + + def log_exception_decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwds): + try: + return func(*args, **kwds) + except Exception: + logger.exception(func.__name__) + + return wrapper + + return log_exception_decorator + + +class LoginWindow(object): + + def __init__(self, widgetTree): + """ + @note Thread agnostic + """ + self._dialog = widgetTree.get_widget("loginDialog") + self._parentWindow = widgetTree.get_widget("mainWindow") + self._serviceCombo = widgetTree.get_widget("serviceCombo") + self._usernameEntry = widgetTree.get_widget("usernameentry") + self._passwordEntry = widgetTree.get_widget("passwordentry") + + self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING) + self._serviceCombo.set_model(self._serviceList) + cell = gtk.CellRendererText() + self._serviceCombo.pack_start(cell, True) + self._serviceCombo.add_attribute(cell, 'text', 1) + self._serviceCombo.set_active(0) + + widgetTree.get_widget("loginbutton").connect("clicked", self._on_loginbutton_clicked) + widgetTree.get_widget("logins_close_button").connect("clicked", self._on_loginclose_clicked) + + def request_credentials(self, + parentWindow = None, + defaultCredentials = ("", "") + ): + """ + @note UI Thread + """ + if parentWindow is None: + parentWindow = self._parentWindow + + self._serviceCombo.hide() + self._serviceList.clear() + + self._usernameEntry.set_text(defaultCredentials[0]) + self._passwordEntry.set_text(defaultCredentials[1]) + + try: + self._dialog.set_transient_for(parentWindow) + self._dialog.set_default_response(gtk.RESPONSE_OK) + response = self._dialog.run() + if response != gtk.RESPONSE_OK: + raise RuntimeError("Login Cancelled") + + username = self._usernameEntry.get_text() + password = self._passwordEntry.get_text() + self._passwordEntry.set_text("") + finally: + self._dialog.hide() + + return username, password + + def request_credentials_from(self, + services, + parentWindow = None, + defaultCredentials = ("", "") + ): + """ + @note UI Thread + """ + if parentWindow is None: + parentWindow = self._parentWindow + + self._serviceList.clear() + for serviceIdserviceName in services: + self._serviceList.append(serviceIdserviceName) + self._serviceCombo.set_active(0) + self._serviceCombo.show() + + self._usernameEntry.set_text(defaultCredentials[0]) + self._passwordEntry.set_text(defaultCredentials[1]) + + try: + self._dialog.set_transient_for(parentWindow) + self._dialog.set_default_response(gtk.RESPONSE_OK) + response = self._dialog.run() + if response != gtk.RESPONSE_OK: + raise RuntimeError("Login Cancelled") + + username = self._usernameEntry.get_text() + password = self._passwordEntry.get_text() + finally: + self._dialog.hide() + + itr = self._serviceCombo.get_active_iter() + serviceId = int(self._serviceList.get_value(itr, 0)) + self._serviceList.clear() + return serviceId, username, password + + def _on_loginbutton_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + + def _on_loginclose_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + + +def safecall(f, errorDisplay=None, default=None, exception=Exception): + ''' + Returns modified f. When the modified f is called and throws an + exception, the default value is returned + ''' + def _safecall(*args, **argv): + try: + return f(*args,**argv) + except exception, e: + if errorDisplay is not None: + errorDisplay.push_exception(e) + return default + return _safecall + + +class ErrorDisplay(object): + + def __init__(self, widgetTree): + super(ErrorDisplay, self).__init__() + self.__errorBox = widgetTree.get_widget("errorEventBox") + self.__errorDescription = widgetTree.get_widget("errorDescription") + self.__errorClose = widgetTree.get_widget("errorClose") + self.__parentBox = self.__errorBox.get_parent() + + self.__errorBox.connect("button_release_event", self._on_close) + + self.__messages = [] + self.__parentBox.remove(self.__errorBox) + + def push_message_with_lock(self, message): + with gtk_lock(): + self.push_message(message) + + def push_message(self, message): + self.__messages.append(message) + if 1 == len(self.__messages): + self.__show_message(message) + + def push_exception_with_lock(self): + with gtk_lock(): + self.push_exception() + + def push_exception(self): + userMessage = str(sys.exc_info()[1]) + self.push_message(userMessage) + _moduleLogger.exception(userMessage) + + def pop_message(self): + del self.__messages[0] + if 0 == len(self.__messages): + self.__hide_message() + else: + self.__errorDescription.set_text(self.__messages[0]) + + def _on_close(self, *args): + self.pop_message() + + def __show_message(self, message): + self.__errorDescription.set_text(message) + self.__parentBox.pack_start(self.__errorBox, False, False) + self.__parentBox.reorder_child(self.__errorBox, 1) + + def __hide_message(self): + self.__errorDescription.set_text("") + self.__parentBox.remove(self.__errorBox) + + +class DummyErrorDisplay(object): + + def __init__(self): + super(DummyErrorDisplay, self).__init__() + + self.__messages = [] + + def push_message_with_lock(self, message): + self.push_message(message) + + def push_message(self, message): + if 0 < len(self.__messages): + self.__messages.append(message) + else: + self.__show_message(message) + + def push_exception(self, exception = None): + userMessage = str(sys.exc_value) + _moduleLogger.exception(userMessage) + + def pop_message(self): + if 0 < len(self.__messages): + self.__show_message(self.__messages[0]) + del self.__messages[0] + + def __show_message(self, message): + _moduleLogger.debug(message) + + +class MessageBox(gtk.MessageDialog): + + def __init__(self, message): + parent = None + gtk.MessageDialog.__init__( + self, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK, + message, + ) + self.set_default_response(gtk.RESPONSE_OK) + self.connect('response', self._handle_clicked) + + def _handle_clicked(self, *args): + self.destroy() + + +class MessageBox2(gtk.MessageDialog): + + def __init__(self, message): + parent = None + gtk.MessageDialog.__init__( + self, + parent, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK, + message, + ) + self.set_default_response(gtk.RESPONSE_OK) + self.connect('response', self._handle_clicked) + + def _handle_clicked(self, *args): + self.destroy() + + +class PopupCalendar(object): + + def __init__(self, parent, displayDate, title = ""): + self._displayDate = displayDate + + self._calendar = gtk.Calendar() + self._calendar.select_month(self._displayDate.month, self._displayDate.year) + self._calendar.select_day(self._displayDate.day) + self._calendar.set_display_options( + gtk.CALENDAR_SHOW_HEADING | + gtk.CALENDAR_SHOW_DAY_NAMES | + gtk.CALENDAR_NO_MONTH_CHANGE | + 0 + ) + self._calendar.connect("day-selected", self._on_day_selected) + + self._popupWindow = gtk.Window() + self._popupWindow.set_title(title) + self._popupWindow.add(self._calendar) + self._popupWindow.set_transient_for(parent) + self._popupWindow.set_modal(True) + self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self._popupWindow.set_skip_pager_hint(True) + self._popupWindow.set_skip_taskbar_hint(True) + + def run(self): + self._popupWindow.show_all() + + def _on_day_selected(self, *args): + try: + self._calendar.select_month(self._displayDate.month, self._displayDate.year) + self._calendar.select_day(self._displayDate.day) + except Exception, e: + _moduleLogger.exception(e) + + +if __name__ == "__main__": + if False: + import datetime + cal = PopupCalendar(None, datetime.datetime.now()) + cal._popupWindow.connect("destroy", lambda w: gtk.main_quit()) + cal.run() + + gtk.main() diff --git a/src/opt/Nqa-Audiobook-player/hildonize.py b/src/opt/Nqa-Audiobook-player/hildonize.py new file mode 100644 index 0000000..08049ba --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/hildonize.py @@ -0,0 +1,743 @@ +#!/usr/bin/env python + +""" +Open Issues + @bug not all of a message is shown + @bug Buttons are too small +""" + + +import gobject +import gtk +import dbus + + +class _NullHildonModule(object): + pass + + +try: + import hildon as _hildon + hildon = _hildon # Dumb but gets around pyflakiness +except (ImportError, OSError): + hildon = _NullHildonModule + + +IS_HILDON_SUPPORTED = hildon is not _NullHildonModule + + +class _NullHildonProgram(object): + + def add_window(self, window): + pass + + +def _hildon_get_app_class(): + return hildon.Program + + +def _null_get_app_class(): + return _NullHildonProgram + + +try: + hildon.Program + get_app_class = _hildon_get_app_class +except AttributeError: + get_app_class = _null_get_app_class + + +def _hildon_set_application_title(window, title): + pass + + +def _null_set_application_title(window, title): + window.set_title(title) + + +if IS_HILDON_SUPPORTED: + set_application_title = _hildon_set_application_title +else: + set_application_title = _null_set_application_title + + +def _fremantle_hildonize_window(app, window): + oldWindow = window + newWindow = hildon.StackableWindow() + if oldWindow.get_child() is not None: + oldWindow.get_child().reparent(newWindow) + app.add_window(newWindow) + return newWindow + + +def _hildon_hildonize_window(app, window): + oldWindow = window + newWindow = hildon.Window() + if oldWindow.get_child() is not None: + oldWindow.get_child().reparent(newWindow) + app.add_window(newWindow) + return newWindow + + +def _null_hildonize_window(app, window): + return window + + +try: + hildon.StackableWindow + hildonize_window = _fremantle_hildonize_window +except AttributeError: + try: + hildon.Window + hildonize_window = _hildon_hildonize_window + except AttributeError: + hildonize_window = _null_hildonize_window + + +def _fremantle_hildonize_menu(window, gtkMenu): + appMenu = hildon.AppMenu() + window.set_app_menu(appMenu) + gtkMenu.get_parent().remove(gtkMenu) + return appMenu + + +def _hildon_hildonize_menu(window, gtkMenu): + hildonMenu = gtk.Menu() + for child in gtkMenu.get_children(): + child.reparent(hildonMenu) + window.set_menu(hildonMenu) + gtkMenu.destroy() + return hildonMenu + + +def _null_hildonize_menu(window, gtkMenu): + return gtkMenu + + +try: + hildon.AppMenu + GTK_MENU_USED = False + IS_FREMANTLE_SUPPORTED = True + hildonize_menu = _fremantle_hildonize_menu +except AttributeError: + GTK_MENU_USED = True + IS_FREMANTLE_SUPPORTED = False + if IS_HILDON_SUPPORTED: + hildonize_menu = _hildon_hildonize_menu + else: + hildonize_menu = _null_hildonize_menu + + +def _hildon_set_button_auto_selectable(button): + button.set_theme_size(hildon.HILDON_SIZE_AUTO_HEIGHT) + + +def _null_set_button_auto_selectable(button): + pass + + +try: + hildon.HILDON_SIZE_AUTO_HEIGHT + gtk.Button.set_theme_size + set_button_auto_selectable = _hildon_set_button_auto_selectable +except AttributeError: + set_button_auto_selectable = _null_set_button_auto_selectable + + +def _hildon_set_button_finger_selectable(button): + button.set_theme_size(hildon.HILDON_SIZE_FINGER_HEIGHT) + + +def _null_set_button_finger_selectable(button): + pass + + +try: + hildon.HILDON_SIZE_FINGER_HEIGHT + gtk.Button.set_theme_size + set_button_finger_selectable = _hildon_set_button_finger_selectable +except AttributeError: + set_button_finger_selectable = _null_set_button_finger_selectable + + +def _hildon_set_button_thumb_selectable(button): + button.set_theme_size(hildon.HILDON_SIZE_THUMB_HEIGHT) + + +def _null_set_button_thumb_selectable(button): + pass + + +try: + hildon.HILDON_SIZE_THUMB_HEIGHT + gtk.Button.set_theme_size + set_button_thumb_selectable = _hildon_set_button_thumb_selectable +except AttributeError: + set_button_thumb_selectable = _null_set_button_thumb_selectable + + +def _hildon_set_cell_thumb_selectable(renderer): + renderer.set_property("scale", 1.5) + + +def _null_set_cell_thumb_selectable(renderer): + pass + + +if IS_HILDON_SUPPORTED: + set_cell_thumb_selectable = _hildon_set_cell_thumb_selectable +else: + set_cell_thumb_selectable = _null_set_cell_thumb_selectable + + +def _hildon_set_pix_cell_thumb_selectable(renderer): + renderer.set_property("stock-size", 48) + + +def _null_set_pix_cell_thumb_selectable(renderer): + pass + + +if IS_HILDON_SUPPORTED: + set_pix_cell_thumb_selectable = _hildon_set_pix_cell_thumb_selectable +else: + set_pix_cell_thumb_selectable = _null_set_pix_cell_thumb_selectable + + +def _fremantle_show_information_banner(parent, message): + hildon.hildon_banner_show_information(parent, "", message) + + +def _hildon_show_information_banner(parent, message): + hildon.hildon_banner_show_information(parent, None, message) + + +def _null_show_information_banner(parent, message): + pass + + +if IS_FREMANTLE_SUPPORTED: + show_information_banner = _fremantle_show_information_banner +else: + try: + hildon.hildon_banner_show_information + show_information_banner = _hildon_show_information_banner + except AttributeError: + show_information_banner = _null_show_information_banner + + +def _fremantle_show_busy_banner_start(parent, message): + hildon.hildon_gtk_window_set_progress_indicator(parent, True) + return parent + + +def _fremantle_show_busy_banner_end(banner): + hildon.hildon_gtk_window_set_progress_indicator(banner, False) + + +def _hildon_show_busy_banner_start(parent, message): + return hildon.hildon_banner_show_animation(parent, None, message) + + +def _hildon_show_busy_banner_end(banner): + banner.destroy() + + +def _null_show_busy_banner_start(parent, message): + return None + + +def _null_show_busy_banner_end(banner): + assert banner is None + + +try: + hildon.hildon_gtk_window_set_progress_indicator + show_busy_banner_start = _fremantle_show_busy_banner_start + show_busy_banner_end = _fremantle_show_busy_banner_end +except AttributeError: + try: + hildon.hildon_banner_show_animation + show_busy_banner_start = _hildon_show_busy_banner_start + show_busy_banner_end = _hildon_show_busy_banner_end + except AttributeError: + show_busy_banner_start = _null_show_busy_banner_start + show_busy_banner_end = _null_show_busy_banner_end + + +def _hildon_hildonize_text_entry(textEntry): + textEntry.set_property('hildon-input-mode', 7) + + +def _null_hildonize_text_entry(textEntry): + pass + + +if IS_HILDON_SUPPORTED: + hildonize_text_entry = _hildon_hildonize_text_entry +else: + hildonize_text_entry = _null_hildonize_text_entry + + +def _hildon_window_to_portrait(window): + # gtk documentation is unclear whether this does a "=" or a "|=" + flags = hildon.PORTRAIT_MODE_SUPPORT | hildon.PORTRAIT_MODE_REQUEST + hildon.hildon_gtk_window_set_portrait_flags(window, flags) + + +def _hildon_window_to_landscape(window): + # gtk documentation is unclear whether this does a "=" or a "&= ~" + flags = hildon.PORTRAIT_MODE_SUPPORT + hildon.hildon_gtk_window_set_portrait_flags(window, flags) + + +def _null_window_to_portrait(window): + pass + + +def _null_window_to_landscape(window): + pass + + +try: + hildon.PORTRAIT_MODE_SUPPORT + hildon.PORTRAIT_MODE_REQUEST + hildon.hildon_gtk_window_set_portrait_flags + + window_to_portrait = _hildon_window_to_portrait + window_to_landscape = _hildon_window_to_landscape +except AttributeError: + window_to_portrait = _null_window_to_portrait + window_to_landscape = _null_window_to_landscape + + +def get_device_orientation(): + bus = dbus.SystemBus() + try: + rawMceRequest = bus.get_object("com.nokia.mce", "/com/nokia/mce/request") + mceRequest = dbus.Interface(rawMceRequest, dbus_interface="com.nokia.mce.request") + orientation, standState, faceState, xAxis, yAxis, zAxis = mceRequest.get_device_orientation() + except dbus.exception.DBusException: + # catching for documentation purposes that when a system doesn't + # support this, this is what to expect + raise + + if orientation == "": + return gtk.ORIENTATION_HORIZONTAL + elif orientation == "": + return gtk.ORIENTATION_VERTICAL + else: + raise RuntimeError("Unknown orientation: %s" % orientation) + + +def _hildon_hildonize_password_entry(textEntry): + textEntry.set_property('hildon-input-mode', 7 | (1 << 29)) + + +def _null_hildonize_password_entry(textEntry): + pass + + +if IS_HILDON_SUPPORTED: + hildonize_password_entry = _hildon_hildonize_password_entry +else: + hildonize_password_entry = _null_hildonize_password_entry + + +def _hildon_hildonize_combo_entry(comboEntry): + comboEntry.set_property('hildon-input-mode', 1 << 4) + + +def _null_hildonize_combo_entry(textEntry): + pass + + +if IS_HILDON_SUPPORTED: + hildonize_combo_entry = _hildon_hildonize_combo_entry +else: + hildonize_combo_entry = _null_hildonize_combo_entry + + +def _fremantle_hildonize_scrollwindow(scrolledWindow): + pannableWindow = hildon.PannableArea() + + child = scrolledWindow.get_child() + scrolledWindow.remove(child) + pannableWindow.add(child) + + parent = scrolledWindow.get_parent() + if parent is not None: + parent.remove(scrolledWindow) + parent.add(pannableWindow) + + return pannableWindow + + +def _hildon_hildonize_scrollwindow(scrolledWindow): + hildon.hildon_helper_set_thumb_scrollbar(scrolledWindow, True) + return scrolledWindow + + +def _null_hildonize_scrollwindow(scrolledWindow): + return scrolledWindow + + +try: + hildon.PannableArea + hildonize_scrollwindow = _fremantle_hildonize_scrollwindow + hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow +except AttributeError: + try: + hildon.hildon_helper_set_thumb_scrollbar + hildonize_scrollwindow = _hildon_hildonize_scrollwindow + hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow + except AttributeError: + hildonize_scrollwindow = _null_hildonize_scrollwindow + hildonize_scrollwindow_with_viewport = _null_hildonize_scrollwindow + + +def _hildon_request_number(parent, title, range, default): + spinner = hildon.NumberEditor(*range) + spinner.set_value(default) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(spinner) + + try: + dialog.show_all() + response = dialog.run() + + if response == gtk.RESPONSE_OK: + return spinner.get_value() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + finally: + dialog.hide() + dialog.destroy() + + +def _null_request_number(parent, title, range, default): + adjustment = gtk.Adjustment(default, range[0], range[1], 1, 5, 0) + spinner = gtk.SpinButton(adjustment, 0, 0) + spinner.set_wrap(False) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(spinner) + + try: + dialog.show_all() + response = dialog.run() + + if response == gtk.RESPONSE_OK: + return spinner.get_value_as_int() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + finally: + dialog.hide() + dialog.destroy() + + +try: + hildon.NumberEditor # TODO deprecated in fremantle + request_number = _hildon_request_number +except AttributeError: + request_number = _null_request_number + + +def _hildon_touch_selector(parent, title, items, defaultIndex): + model = gtk.ListStore(gobject.TYPE_STRING) + for item in items: + model.append((item, )) + + selector = hildon.TouchSelector() + selector.append_text_column(model, True) + selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE) + selector.set_active(0, defaultIndex) + + dialog = hildon.PickerDialog(parent) + dialog.set_selector(selector) + + try: + dialog.show_all() + response = dialog.run() + + if response == gtk.RESPONSE_OK: + return selector.get_active(0) + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + finally: + dialog.hide() + dialog.destroy() + + +def _on_null_touch_selector_activated(treeView, path, column, dialog, pathData): + dialog.response(gtk.RESPONSE_OK) + pathData[0] = path + + +def _null_touch_selector(parent, title, items, defaultIndex = -1): + parentSize = parent.get_size() + + model = gtk.ListStore(gobject.TYPE_STRING) + for item in items: + model.append((item, )) + + cell = gtk.CellRendererText() + set_cell_thumb_selectable(cell) + column = gtk.TreeViewColumn(title) + column.pack_start(cell, expand=True) + column.add_attribute(cell, "text", 0) + + treeView = gtk.TreeView() + treeView.set_model(model) + treeView.append_column(column) + selection = treeView.get_selection() + selection.set_mode(gtk.SELECTION_SINGLE) + if 0 < defaultIndex: + selection.select_path((defaultIndex, )) + + scrolledWin = gtk.ScrolledWindow() + scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolledWin.add(treeView) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(scrolledWin) + dialog.resize(parentSize[0], max(parentSize[1]-100, 100)) + + scrolledWin = hildonize_scrollwindow(scrolledWin) + pathData = [None] + treeView.connect("row-activated", _on_null_touch_selector_activated, dialog, pathData) + + try: + dialog.show_all() + response = dialog.run() + + if response == gtk.RESPONSE_OK: + if pathData[0] is None: + raise RuntimeError("No selection made") + return pathData[0][0] + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + finally: + dialog.hide() + dialog.destroy() + + +try: + hildon.PickerDialog + hildon.TouchSelector + touch_selector = _hildon_touch_selector +except AttributeError: + touch_selector = _null_touch_selector + + +def _hildon_touch_selector_entry(parent, title, items, defaultItem): + # Got a segfault when using append_text_column with TouchSelectorEntry, so using this way + try: + selector = hildon.TouchSelectorEntry(text=True) + except TypeError: + selector = hildon.hildon_touch_selector_entry_new_text() + defaultIndex = -1 + for i, item in enumerate(items): + selector.append_text(item) + if item == defaultItem: + defaultIndex = i + + dialog = hildon.PickerDialog(parent) + dialog.set_selector(selector) + + if 0 < defaultIndex: + selector.set_active(0, defaultIndex) + else: + selector.get_entry().set_text(defaultItem) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + return selector.get_entry().get_text() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +def _on_null_touch_selector_entry_entry_changed(entry, result, selection, defaultIndex): + custom = entry.get_text().strip() + if custom: + result[0] = custom + selection.unselect_all() + else: + result[0] = None + selection.select_path((defaultIndex, )) + + +def _on_null_touch_selector_entry_entry_activated(customEntry, dialog, result): + dialog.response(gtk.RESPONSE_OK) + result[0] = customEntry.get_text() + + +def _on_null_touch_selector_entry_tree_activated(treeView, path, column, dialog, result): + dialog.response(gtk.RESPONSE_OK) + model = treeView.get_model() + itr = model.get_iter(path) + if itr is not None: + result[0] = model.get_value(itr, 0) + + +def _null_touch_selector_entry(parent, title, items, defaultItem): + parentSize = parent.get_size() + + model = gtk.ListStore(gobject.TYPE_STRING) + defaultIndex = -1 + for i, item in enumerate(items): + model.append((item, )) + if item == defaultItem: + defaultIndex = i + + cell = gtk.CellRendererText() + set_cell_thumb_selectable(cell) + column = gtk.TreeViewColumn(title) + column.pack_start(cell, expand=True) + column.add_attribute(cell, "text", 0) + + treeView = gtk.TreeView() + treeView.set_model(model) + treeView.append_column(column) + selection = treeView.get_selection() + selection.set_mode(gtk.SELECTION_SINGLE) + + scrolledWin = gtk.ScrolledWindow() + scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolledWin.add(treeView) + + customEntry = gtk.Entry() + + layout = gtk.VBox() + layout.pack_start(customEntry, expand=False) + layout.pack_start(scrolledWin) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(layout) + dialog.resize(parentSize[0], max(parentSize[1]-100, 100)) + + scrolledWin = hildonize_scrollwindow(scrolledWin) + + result = [None] + if 0 < defaultIndex: + selection.select_path((defaultIndex, )) + result[0] = defaultItem + else: + customEntry.set_text(defaultItem) + + customEntry.connect("activate", _on_null_touch_selector_entry_entry_activated, dialog, result) + customEntry.connect("changed", _on_null_touch_selector_entry_entry_changed, result, selection, defaultIndex) + treeView.connect("row-activated", _on_null_touch_selector_entry_tree_activated, dialog, result) + + try: + dialog.show_all() + response = dialog.run() + + if response == gtk.RESPONSE_OK: + _, itr = selection.get_selected() + if itr is not None: + return model.get_value(itr, 0) + else: + enteredText = customEntry.get_text().strip() + if enteredText: + return enteredText + elif result[0] is not None: + return result[0] + else: + raise RuntimeError("No selection made") + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + finally: + dialog.hide() + dialog.destroy() + + +try: + hildon.PickerDialog + hildon.TouchSelectorEntry + touch_selector_entry = _hildon_touch_selector_entry +except AttributeError: + touch_selector_entry = _null_touch_selector_entry + + +if __name__ == "__main__": + app = get_app_class()() + + label = gtk.Label("Hello World from a Label!") + + win = gtk.Window() + win.add(label) + win = hildonize_window(app, win) + if False and IS_FREMANTLE_SUPPORTED: + appMenu = hildon.AppMenu() + for i in xrange(5): + b = gtk.Button(str(i)) + appMenu.append(b) + win.set_app_menu(appMenu) + win.show_all() + appMenu.show_all() + gtk.main() + elif False: + print touch_selector(win, "Test", ["A", "B", "C", "D"], 2) + elif False: + print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "C") + print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "Blah") + elif False: + import pprint + name, value = "", "" + goodLocals = [ + (name, value) for (name, value) in locals().iteritems() + if not name.startswith("_") + ] + pprint.pprint(goodLocals) + elif False: + import time + show_information_banner(win, "Hello World") + time.sleep(5) + elif False: + import time + banner = show_busy_banner_start(win, "Hello World") + time.sleep(5) + show_busy_banner_end(banner) diff --git a/src/opt/Nqa-Audiobook-player/nqaap.py b/src/opt/Nqa-Audiobook-player/nqaap.py new file mode 100644 index 0000000..ab32ba2 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/nqaap.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import os +import logging + +import constants +import nqaap_gtk + + +_moduleLogger = logging.getLogger(__name__) + + +try: + os.makedirs(constants._data_path_) +except OSError, e: + if e.errno != 17: + raise + +logging.basicConfig(level=logging.DEBUG, filename=constants._user_logpath_) +_moduleLogger.info("%s %s-%s" % (constants.__pretty_app_name__, constants.__version__, constants.__build__)) +_moduleLogger.info("OS: %s" % (os.uname()[0], )) +_moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:]) +_moduleLogger.info("Hostname: %s" % os.uname()[1]) + +try: + nqaap_gtk.run() +finally: + logging.shutdown() diff --git a/src/opt/Nqa-Audiobook-player/nqaap_gtk.py b/src/opt/Nqa-Audiobook-player/nqaap_gtk.py new file mode 100644 index 0000000..eb2ed8f --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/nqaap_gtk.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python + +import logging + +import dbus +import dbus.mainloop.glib +import gobject +import gtk + +from Player import Player +from Gui import Gui + + +_moduleLogger = logging.getLogger(__name__) + + +def run(): + l = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + gobject.threads_init() + gtk.gdk.threads_init() + + gui = Gui() + controller = Player(ui = gui) + gui.controller = controller + gui.load_settings() + + gtk.main() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + run() diff --git a/src/opt/Nqa-Audiobook-player/settings.py b/src/opt/Nqa-Audiobook-player/settings.py new file mode 100644 index 0000000..afb50a9 --- /dev/null +++ b/src/opt/Nqa-Audiobook-player/settings.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +This file is part of Multilist. + +Multilist 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. + +Multilist 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 Multilist. If not, see . + +Copyright (C) 2008 Christoph Würstle +""" + +import logging + +import gtk + +import hildonize +import gtk_toolbox + +try: + _ +except NameError: + _ = lambda x: x + + +_moduleLogger = logging.getLogger(__name__) + + +class SettingsDialog(object): + + def __init__(self, parent): + self.window = None + + self.__isPortraitCheckbutton = gtk.CheckButton("Portrait Mode") + + self.__rotationSection = gtk.VBox() + self.__rotationSection.pack_start(self.__isPortraitCheckbutton, False, True) + + rotationFrame = gtk.Frame("Rotation") + rotationFrame.add(self.__rotationSection) + + self.__audioBooksPath = gtk.Entry() + self.__audioBooksPathButton = gtk.Button("Choose") + self.__audioBooksPathButton.connect("clicked", self._on_path_choose) + + self.__audiobookPathSection = gtk.HBox() + self.__audiobookPathSection.pack_start(self.__audioBooksPath, True, True) + self.__audiobookPathSection.pack_start(self.__audioBooksPathButton, False, True) + + self.__audiobookSection = gtk.VBox() + self.__audiobookSection.pack_start(self.__audiobookPathSection) + + audiobookFrame = gtk.Frame("Audiobooks") + audiobookFrame.add(self.__audiobookSection) + + settingsBox = gtk.VBox() + settingsBox.pack_start(rotationFrame, False, True) + settingsBox.pack_start(audiobookFrame, False, True) + settingsView = gtk.Viewport() + settingsView.add(settingsBox) + settingsScrollView = gtk.ScrolledWindow() + settingsScrollView.add(settingsView) + settingsScrollView.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + parent.pack_start(settingsScrollView, True, True) + + settingsScrollView = hildonize.hildonize_scrollwindow(settingsScrollView) + + def set_portrait_state(self, isPortrait): + self.__isPortraitCheckbutton.set_active(isPortrait) + + def is_portrait(self): + return self.__isPortraitCheckbutton.get_active() + + def set_audiobook_path(self, path): + self.__audioBooksPath.set_text(path) + + def get_audiobook_path(self): + return self.__audioBooksPath.get_text() + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_path_choose(self, *args): + fileChooser = gtk.FileChooserDialog( + title="Audiobooks", + action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + parent=self.window, + ) + fileChooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + fileChooser.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) + fileChooser.set_filename(self.__audioBooksPath.get_text()) + userResponse = fileChooser.run() + fileChooser.hide() + if userResponse == gtk.RESPONSE_OK: + filename = fileChooser.get_filename() + self.__audioBooksPath.set_text(filename) diff --git a/src/usr/share/applications/hildon/nqaap.desktop b/src/usr/share/applications/hildon/nqaap.desktop new file mode 100644 index 0000000..4054f62 --- /dev/null +++ b/src/usr/share/applications/hildon/nqaap.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0.0 +Encoding=UTF-8 +Name=nQaap +Exec=python /opt/Nqa-Audiobook-player/nqaap.py +Icon=nqaap +X-Icon-path=/usr/share/icons +X-Window-Icon=nqaap +Type=Application +X-Osso-Type=application/x-executable \ No newline at end of file diff --git a/src/usr/share/icons/hicolor/48x48/hildon/nqaap.png b/src/usr/share/icons/hicolor/48x48/hildon/nqaap.png new file mode 100644 index 0000000000000000000000000000000000000000..9e38229b1e9aa7b792458bf488e742bb67c77d85 GIT binary patch literal 1218 zcmV;z1U>tSP)c|jl@JC_5v3)k;KH$a4=f+0MUbq5icI_10!(oU=*VsghVfB42dEpmb3;5 zQ1Bzzg3{2fP=(g5u!wKErK!O#yW45w3)xP}%=Gu(|IM43O`D&eC#0H>p`HFdALE5* z^h=$}peGF*S8Um!-7I0wGzmaI*6~?_4#>xx@#Xn@w z-~VX%zH<@@dTjmi^3|2tfPkaUaSj4w*6*}#s$CK#)*Q60dEgq11qH2}YA^sn@}9Mw z6gVTlKM+@tkEf?hh*I^vmlk4EBW*oG0@W4OgVyaMPat3k0YS9EKOJcRrHBL!n5xzs zy;IPnHBy@Gmix*E6GMZC38fG5Le$?U~!O zXq2^QsydIX&Lz2j^$v`jI^WWC`AAQ{n|auI;P2VGWJ4cE+^AURlnzdfMz?{*2^u#- zG_#OH{+4)ssAv72wq9riKKUkWEr`o=G~5d^w>#A{a<}h*0lN zE|B5EV!e2eu*NpzM3Pbm6j*s$jMVE`&Yr@2lTKxT9p2>cmvTUh4b|PUHu${lVun65QtyWrW&RFm+HKvmI zZmfe7%NO#tgiAkWpaP8u1)H7MzbnyR=JfaJNCVFcUFkrY7zL@ddj8I(*_NzzWkEIo g;o*OU`bU5P0MRMm5($ap>;M1&07*qoM6N<$f-%xLHvj+t literal 0 HcmV?d00001 diff --git a/src/usr/share/icons/hicolor/scalable/hildon/nqaap.png b/src/usr/share/icons/hicolor/scalable/hildon/nqaap.png new file mode 100644 index 0000000000000000000000000000000000000000..8538783dc241218681aabf0fa3e70913a6bd59c9 GIT binary patch literal 1666 zcmV-|27UR7P)0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU&BuPX;RCwCdnqN#?RT#ie|3NReFp9RF zWsHBa;n;99s}4*iOT?Hp(`hna)CFFg$ zj~(6q5nSZ}0KHp=PeNe8*j{`P>?b&s0Rw#4Inj;{(QIqs#UKO#v~PAaVY7kF>Z~t{ zuK^Bf;LMFvFehII-T?91pRl#04tHTQ^I}l*bvL#Xw!MVfvpE0*I{Flj|KbM#^Wv<9 z+9yfmB#6yi0~v+A0rgE{fId$TUZkL|H&z}&nGvW5WV9YYW+$u|q5vRxpx)teJ z&WWq1;HFp5x>hdDI=~j=Ql5RHNHG%@X#m(L-dBIQYX0BW5)kdT4N zM4LW@7G|VXi23p~T(b|ouHIA#gV6QC;K1)NhC2z(3zyln+&#b+H82U(xG5%kaG2>|aM`)=2c zlGC3Z?z%ku?GINv)_Tfp7K0xY377>Z(pR&FOC3ULjv0(6qWzueoqBn6?z%e`Er0t1 zx4gWAZ0>yY(XK~z;O&9rLMlbHSbx7<+ym4vbN}0elgFzA1OS z8p(E;X*^I8`YlHljLq^-FIx7?QFp@r5zqPfW3m+cvvC5Xpc5+gJRwB}eszXgiXEcfP<=7At(@-s{ zvV2upeh~n~&h_e$&6(d>dsIuRj22pFAPc?TM|e>u-L`ry@4>c?VLsPYx;XNnH;)oXlBI zA>wI%+x9y?k!n+6J^k2+5$%&yPd)%(fU+K57-1$TN_}$gQSb9t!h5)sa&GSjfZ_4? zQH4WQbe6rhjB+H56!z5BW~zx8p7SvjzxH^W+57Pcv$c`SNL8_noS1fA5HoFxFXQ=RWPPB7>k)=iolaV>6 zN~PldB0_NhgVaEkv6}nu6$ii`$SDFqL+JSarK+Gh^=YoG07Mb*b0ml#j$hp=G=#td zCcQcB3;N6g7`BLdnF%$tbK;kb15_9^nRCMUeHX_!G{ON4I#mWwRtoDmW`g2!fK6ps z(Js0!cp2*d2LP%=H__+mhNdX(lB`vZ-gGbkVbe9)4s9GH0Cb~Y)SQvN8%>?oLq369 znO;x#kJqk(-VIydp+huyV0!_lhC(<%u2z*ct%*(f96M?=VQJp|v3Fr6`h9hz{@Bd5 znN8vfLwfi%TZw>1r^?Gq+ypZ-qdq68HIQ1;LG?#JZ0Gb<z`bq5uE@ literal 0 HcmV?d00001 -- 1.7.9.5