QML: Settings, about box, graphics update (bug 1554)
[gpodder.git] / src / gpodder / qmlui / __init__.py
blobb3884bc4fb430830e0643e45fb96c89d9db4f373
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2012 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # gpodder.qmlui - gPodder's QML interface
21 # Thomas Perl <thp@gpodder.org>; 2011-02-06
24 from PySide.QtGui import QApplication
25 from PySide.QtCore import Qt, QObject, Signal, Slot, Property, QUrl
26 from PySide.QtCore import QAbstractListModel, QModelIndex
27 from PySide.QtDeclarative import QDeclarativeView
29 import os
30 import threading
31 import signal
32 import functools
33 import gpodder
34 import subprocess
36 _ = gpodder.gettext
37 N_ = gpodder.ngettext
39 from gpodder import core
40 from gpodder import util
41 from gpodder import my
42 from gpodder import query
44 from gpodder.model import Model
46 from gpodder.qmlui import model
47 from gpodder.qmlui import helper
48 from gpodder.qmlui import images
50 import logging
51 logger = logging.getLogger("qmlui")
54 EPISODE_LIST_FILTERS = [
55 # (UI label, EQL expression)
56 (_('All'), None),
57 (_('Hide deleted'), 'not deleted'),
58 (_('New'), 'new or downloading'),
59 (_('Downloaded'), 'downloaded or downloading'),
60 (_('Deleted'), 'deleted'),
61 (_('Finished'), 'finished'),
62 (_('Archived'), 'downloaded and archive'),
63 (_('Videos'), 'video'),
67 class ConfigProxy(QObject):
68 def __init__(self, config):
69 QObject.__init__(self)
70 self._config = config
72 config.add_observer(self._on_config_changed)
74 def _on_config_changed(self, name, old_value, new_value):
75 if name == 'ui.qml.autorotate':
76 self.autorotateChanged.emit()
78 def get_autorotate(self):
79 return self._config.ui.qml.autorotate
81 def set_autorotate(self, autorotate):
82 self._config.ui.qml.autorotate = autorotate
84 autorotateChanged = Signal()
86 autorotate = Property(bool, get_autorotate, set_autorotate,
87 notify=autorotateChanged)
89 class Controller(QObject):
90 def __init__(self, root):
91 QObject.__init__(self)
92 self.root = root
93 self.context_menu_actions = []
94 self.episode_list_title = u''
95 self.current_input_dialog = None
96 self.root.config.add_observer(self.on_config_changed)
98 def on_config_changed(self, name, old_value, new_value):
99 logger.info('Config changed: %s (%s -> %s)', name,
100 old_value, new_value)
101 if name == 'mygpo.enabled':
102 self.myGpoEnabledChanged.emit()
103 elif name == 'mygpo.username':
104 self.myGpoUsernameChanged.emit()
105 elif name == 'mygpo.password':
106 self.myGpoPasswordChanged.emit()
107 elif name == 'mygpo.device.caption':
108 self.myGpoDeviceCaptionChanged.emit()
110 episodeListTitleChanged = Signal()
112 def setEpisodeListTitle(self, title):
113 if self.episode_list_title != title:
114 self.episode_list_title = title
115 self.episodeListTitleChanged.emit()
117 def getEpisodeListTitle(self):
118 return self.episode_list_title
120 episodeListTitle = Property(unicode, getEpisodeListTitle, \
121 setEpisodeListTitle, notify=episodeListTitleChanged)
123 @Slot(result='QStringList')
124 def getEpisodeListFilterNames(self):
125 return [caption for caption, eql in EPISODE_LIST_FILTERS]
127 @Slot(str, result=str)
128 def translate(self, x):
129 return _(x)
131 @Slot(str, str, int, result=str)
132 def ntranslate(self, singular, plural, count):
133 return N_(singular, plural, count)
135 @Slot(str, int, result=str)
136 def formatCount(self, template, count):
137 return template % {'count': count}
139 @Slot(result=str)
140 def getVersion(self):
141 return gpodder.__version__
143 @Slot(result=str)
144 def getReleased(self):
145 return gpodder.__date__
147 @Slot(result=unicode)
148 def getCredits(self):
149 credits_file = os.path.join(gpodder.prefix, 'share', 'gpodder', 'credits.txt')
150 return util.convert_bytes(open(credits_file).read())
152 @Slot(result=unicode)
153 def getCopyright(self):
154 return util.convert_bytes(gpodder.__copyright__)
156 @Slot(result=str)
157 def getLicense(self):
158 return gpodder.__license__
160 @Slot(result=str)
161 def getURL(self):
162 return gpodder.__url__
164 @Slot()
165 def loadLastEpisode(self):
166 self.root.load_last_episode()
168 @Slot(QObject, int, int)
169 def storePlaybackAction(self, episode, start, end):
170 if end - 5 < start:
171 logger.info('Ignoring too short playback action.')
172 return
173 total = episode.qduration
174 self.root.mygpo_client.on_playback_full(episode, start, end, total)
175 self.root.mygpo_client.flush()
177 @Slot(QObject)
178 def playVideo(self, episode):
179 """Video Playback on MeeGo 1.2 Harmattan"""
180 if episode.qnew:
181 episode.toggle_new()
182 self.update_subset_stats()
184 url = episode.get_playback_url()
185 if gpodder.ui.harmattan:
186 subprocess.Popen(['video-suite', url])
187 else:
188 util.gui_open(url)
190 self.root.mygpo_client.on_playback([episode])
191 self.root.mygpo_client.flush()
193 @Slot(QObject)
194 def podcastSelected(self, podcast):
195 self.setEpisodeListTitle(podcast.qtitle)
196 self.root.select_podcast(podcast)
198 windowTitleChanged = Signal()
200 def getWindowTitle(self):
201 return self.root.view.windowTitle()
203 def setWindowTitle(self, windowTitle):
204 if gpodder.ui.fremantle:
205 self.root.view.setWindowTitle(windowTitle)
207 windowTitle = Property(unicode, getWindowTitle,
208 setWindowTitle, notify=windowTitleChanged)
210 @Slot()
211 def myGpoUploadList(self):
212 def upload_proc(self):
213 self.root.start_progress(_('Uploading subscriptions...'))
215 try:
216 try:
217 self.root.mygpo_client.set_subscriptions([podcast.url
218 for podcast in self.root.podcast_model.get_podcasts()])
219 except Exception, e:
220 self.root.show_message('\n'.join((_('Error on upload:'), unicode(e))))
221 finally:
222 self.root.end_progress()
224 t = threading.Thread(target=upload_proc, args=[self])
225 t.start()
227 @Slot()
228 def saveMyGpoSettings(self):
229 # Update the device settings and upload changes
230 self.root.mygpo_client.create_device()
231 self.root.mygpo_client.flush(now=True)
233 myGpoEnabledChanged = Signal()
235 def getMyGpoEnabled(self):
236 return self.root.config.mygpo.enabled
238 def setMyGpoEnabled(self, enabled):
239 self.root.config.mygpo.enabled = enabled
241 myGpoEnabled = Property(bool, getMyGpoEnabled,
242 setMyGpoEnabled, notify=myGpoEnabledChanged)
244 myGpoUsernameChanged = Signal()
246 def getMyGpoUsername(self):
247 return model.convert(self.root.config.mygpo.username)
249 def setMyGpoUsername(self, username):
250 self.root.config.mygpo.username = username
252 myGpoUsername = Property(unicode, getMyGpoUsername,
253 setMyGpoUsername, notify=myGpoUsernameChanged)
255 myGpoPasswordChanged = Signal()
257 def getMyGpoPassword(self):
258 return model.convert(self.root.config.mygpo.password)
260 def setMyGpoPassword(self, password):
261 self.root.config.mygpo.password = password
263 myGpoPassword = Property(unicode, getMyGpoPassword,
264 setMyGpoPassword, notify=myGpoPasswordChanged)
266 myGpoDeviceCaptionChanged = Signal()
268 def getMyGpoDeviceCaption(self):
269 return model.convert(self.root.config.mygpo.device.caption)
271 def setMyGpoDeviceCaption(self, caption):
272 self.root.config.mygpo.device.caption = caption
274 myGpoDeviceCaption = Property(unicode, getMyGpoDeviceCaption,
275 setMyGpoDeviceCaption, notify=myGpoDeviceCaptionChanged)
277 @Slot(QObject)
278 def podcastContextMenu(self, podcast):
279 menu = []
281 if isinstance(podcast, model.EpisodeSubsetView):
282 menu.append(helper.Action(_('Update all'), 'update-all', podcast))
283 else:
284 menu.append(helper.Action(_('Update'), 'update', podcast))
285 menu.append(helper.Action(_('Mark episodes as old'), 'mark-as-read', podcast))
286 menu.append(helper.Action(_('Rename'), 'rename-podcast', podcast))
287 menu.append(helper.Action(_('Change section'), 'change-section', podcast))
288 menu.append(helper.Action(_('Unsubscribe'), 'unsubscribe', podcast))
290 #menu.append(helper.Action('Force update all', 'force-update-all', podcast))
291 #menu.append(helper.Action('Force update', 'force-update', podcast))
293 self.show_context_menu(menu)
295 def show_context_menu(self, actions):
296 if gpodder.ui.harmattan:
297 actions = filter(lambda a: a.caption != '', actions)
298 self.context_menu_actions = actions
299 self.root.open_context_menu(self.context_menu_actions)
301 def update_subset_stats(self):
302 # This should be called when an episode changes state,
303 # so that all subset views (e.g. "All episodes") can
304 # update its status (i.e. download/new counts, etc..)
305 for podcast in self.root.podcast_model.get_objects():
306 if isinstance(podcast, model.EpisodeSubsetView):
307 podcast.qupdate()
309 def find_episode(self, podcast_url, episode_url):
310 for podcast in self.root.podcast_model.get_podcasts():
311 if podcast.url == podcast_url:
312 for episode in podcast.get_all_episodes():
313 if episode.url == episode_url:
314 return episode
315 return None
317 @Slot()
318 def updateAllPodcasts(self):
319 # Process episode actions received from gpodder.net
320 def merge_proc(self):
321 self.root.start_progress(_('Merging episode actions...'))
323 def find_episode(podcast_url, episode_url, counter):
324 counter['x'] += 1
325 self.root.start_progress(_('Merging episode actions (%d)')
326 % counter['x'])
327 return self.find_episode(podcast_url, episode_url)
329 try:
330 d = {'x': 0} # Used to "remember" the counter inside find_episode
331 self.root.mygpo_client.process_episode_actions(lambda x, y:
332 find_episode(x, y, d))
333 finally:
334 self.root.end_progress()
336 t = threading.Thread(target=merge_proc, args=[self])
337 t.start()
339 for podcast in self.root.podcast_model.get_objects():
340 podcast.qupdate(finished_callback=self.update_subset_stats)
342 @Slot(int)
343 def contextMenuResponse(self, index):
344 assert index < len(self.context_menu_actions)
345 action = self.context_menu_actions[index]
346 if action.action == 'update':
347 action.target.qupdate(finished_callback=self.update_subset_stats)
348 elif action.action == 'force-update':
349 action.target.qupdate(force=True, \
350 finished_callback=self.update_subset_stats)
351 elif action.action == 'update-all':
352 self.updateAllPodcasts()
353 elif action.action == 'force-update-all':
354 for podcast in self.root.podcast_model.get_objects():
355 podcast.qupdate(force=True, finished_callback=self.update_subset_stats)
356 if action.action == 'unsubscribe':
357 def unsubscribe():
358 action.target.remove_downloaded()
359 action.target.delete()
360 self.root.remove_podcast(action.target)
362 self.confirm_action(_('Remove this podcast and episodes?'),
363 _('Unsubscribe'), unsubscribe)
364 elif action.action == 'episode-toggle-new':
365 action.target.toggle_new()
366 self.update_subset_stats()
367 elif action.action == 'episode-toggle-archive':
368 action.target.toggle_archive()
369 self.update_subset_stats()
370 elif action.action == 'episode-delete':
371 self.deleteEpisode(action.target)
372 elif action.action == 'episode-enqueue':
373 self.root.enqueue_episode(action.target)
374 elif action.action == 'mark-as-read':
375 for episode in action.target.get_all_episodes():
376 if not episode.was_downloaded(and_exists=True):
377 episode.mark(is_played=True)
378 action.target.changed.emit()
379 self.update_subset_stats()
380 elif action.action == 'change-section':
381 def section_changer(podcast):
382 section = yield (_('New section name:'), podcast.section,
383 _('Rename'))
384 if section and section != podcast.section:
385 podcast.set_section(section)
386 self.root.resort_podcast_list()
388 self.start_input_dialog(section_changer(action.target))
389 elif action.action == 'rename-podcast':
390 def title_changer(podcast):
391 title = yield (_('New name:'), podcast.title,
392 _('Rename'))
393 if title and title != podcast.title:
394 podcast.rename(title)
395 self.root.resort_podcast_list()
397 self.start_input_dialog(title_changer(action.target))
399 def confirm_action(self, message, affirmative, callback):
400 def confirm(message, affirmative, callback):
401 args = (message, '', affirmative, _('Cancel'), False)
402 if (yield args):
403 callback()
405 self.start_input_dialog(confirm(message, affirmative, callback))
407 def start_input_dialog(self, generator):
408 """Carry out an input dialog with the UI
410 This function takes a generator function as argument
411 which should yield a tuple of arguments for the
412 "show_input_dialog" function (i.e. message, default
413 value, accept and reject message - only the message
414 is mandatory, the other arguments have default values).
416 The generator will receive the user's response as a
417 result of the yield expression. If the user accepted
418 the dialog, a string is returned (the value that has
419 been input), otherwise None is returned.
421 Example usage:
423 def some_function():
424 result = yield ('A simple message', 'default value')
425 if result is None:
426 # user has rejected the dialog
427 else:
428 # user has accepted, new value in "result"
430 start_input_dialog(some_function())
432 assert self.current_input_dialog is None
433 self.current_input_dialog = generator
434 args = generator.next()
435 self.root.show_input_dialog(*args)
437 @Slot(bool, str, bool)
438 def inputDialogResponse(self, accepted, value, is_text):
439 if not is_text:
440 value = accepted
441 elif not accepted:
442 value = None
444 try:
445 self.current_input_dialog.send(value)
446 except StopIteration:
447 # This is expected, as the generator
448 # should only have one yield statement
449 pass
451 self.current_input_dialog = None
453 @Slot(QObject)
454 def downloadEpisode(self, episode):
455 episode.qdownload(self.root.config, self.update_subset_stats)
456 self.root.mygpo_client.on_download([episode])
457 self.root.mygpo_client.flush()
459 @Slot(QObject)
460 def cancelDownload(self, episode):
461 episode.download_task.cancel()
462 episode.download_task.removed_from_list()
464 @Slot(QObject)
465 def deleteEpisode(self, episode):
466 def delete():
467 episode.delete_episode()
468 self.update_subset_stats()
469 self.root.mygpo_client.on_delete([episode])
470 self.root.mygpo_client.flush()
471 self.root.on_episode_deleted(episode)
472 self.root.episode_model.sort()
474 self.confirm_action(_('Delete this episode?'), _('Delete'), delete)
476 @Slot(QObject)
477 def acquireEpisode(self, episode):
478 self.root.add_active_episode(episode)
480 @Slot(QObject)
481 def releaseEpisode(self, episode):
482 self.root.remove_active_episode(episode)
484 @Slot()
485 def contextMenuClosed(self):
486 self.context_menu_actions = []
488 @Slot(QObject)
489 def episodeContextMenu(self, episode):
490 menu = []
492 toggle_new = _('Mark as old') if episode.is_new else _('Mark as new')
493 menu.append(helper.Action(toggle_new, 'episode-toggle-new', episode))
495 toggle_archive = _('Allow deletion') if episode.archive else _('Archive')
496 menu.append(helper.Action(toggle_archive, 'episode-toggle-archive', episode))
498 if episode.state != gpodder.STATE_DELETED:
499 menu.append(helper.Action(_('Delete'), 'episode-delete', episode))
501 menu.append(helper.Action(_('Add to play queue'), 'episode-enqueue', episode))
503 self.show_context_menu(menu)
505 @Slot('QVariant')
506 def addSubscriptions(self, urls):
507 def not_yet_subscribed(url):
508 for podcast in self.root.podcast_model.get_objects():
509 if isinstance(podcast, model.EpisodeSubsetView):
510 continue
512 if podcast.url == url:
513 logger.info('Already subscribed: %s', url)
514 return False
516 return True
518 urls = map(util.normalize_feed_url, urls)
519 urls = filter(not_yet_subscribed, urls)
521 def subscribe_proc(self, urls):
522 self.root.start_progress(_('Adding podcasts...'))
523 try:
524 for idx, url in enumerate(urls):
525 print idx, url
526 self.root.start_progress(_('Adding podcasts...') + ' (%d/%d)' % (idx, len(urls)))
527 try:
528 podcast = self.root.model.load_podcast(url=url, create=True,
529 max_episodes=self.root.config.max_episodes_per_feed)
530 podcast.save()
531 self.root.insert_podcast(model.QPodcast(podcast))
532 except Exception, e:
533 logger.warn('Cannot add pocast: %s', e)
534 # XXX: Visual feedback in the QML UI
535 finally:
536 self.root.end_progress()
538 t = threading.Thread(target=subscribe_proc, args=[self, urls])
539 t.start()
541 @Slot()
542 def currentEpisodeChanging(self):
543 self.root.save_pending_data()
545 @Slot()
546 def quit(self):
547 self.root.quit.emit()
549 @Slot()
550 def switcher(self):
551 if gpodder.ui.harmattan:
552 self.root.view.showMinimized()
553 elif gpodder.ui.fremantle:
554 os.system('dbus-send /com/nokia/hildon_desktop '+
555 'com.nokia.hildon_desktop.exit_app_view')
556 else:
557 self.root.view.showMinimized()
560 class gPodderListModel(QAbstractListModel):
561 def __init__(self, objects=None):
562 QAbstractListModel.__init__(self)
563 if objects is None:
564 objects = []
565 self._objects = objects
566 self.setRoleNames({0: 'modelData', 1: 'section'})
568 def sort(self):
569 # Unimplemented for the generic list model
570 self.reset()
572 def insert_object(self, o):
573 self._objects.append(o)
574 self.sort()
576 def remove_object(self, o):
577 self._objects.remove(o)
578 self.reset()
580 def set_objects(self, objects):
581 self._objects = objects
582 self.sort()
584 def get_objects(self):
585 return self._objects
587 def get_object(self, index):
588 return self._objects[index.row()]
590 def rowCount(self, parent=QModelIndex()):
591 return len(self.get_objects())
593 def data(self, index, role):
594 if index.isValid():
595 if role == 0:
596 return self.get_object(index)
597 elif role == 1:
598 return self.get_object(index).qsection
599 return None
601 class gPodderPodcastListModel(gPodderListModel):
602 def set_podcasts(self, db, podcasts):
603 views = [
604 model.EpisodeSubsetView(db, self, _('All episodes'), ''),
606 self.set_objects(views + podcasts)
608 def get_podcasts(self):
609 return filter(lambda podcast: isinstance(podcast, model.QPodcast),
610 self.get_objects())
612 def sort(self):
613 self._objects = sorted(self._objects, key=model.QPodcast.sort_key)
614 self.reset()
616 class gPodderEpisodeListModel(gPodderListModel):
617 def __init__(self, config):
618 gPodderListModel.__init__(self)
619 self._filter = config.ui.qml.state.episode_list_filter
620 self._filtered = []
622 self._config = config
623 config.add_observer(self._on_config_changed)
625 def _on_config_changed(self, name, old_value, new_value):
626 if name == 'ui.qml.state.episode_list_filter':
627 self._filter = new_value
628 self.sort()
630 def sort(self):
631 caption, eql = EPISODE_LIST_FILTERS[self._filter]
633 if eql is None:
634 self._filtered = self._objects
635 else:
636 eql = query.EQL(eql)
637 match = lambda episode: eql.match(episode._episode)
638 self._filtered = filter(match, self._objects)
640 self.reset()
642 def get_objects(self):
643 return self._filtered
645 def get_object(self, index):
646 return self._filtered[index.row()]
648 @Slot(result=int)
649 def getFilter(self):
650 return self._filter
652 @Slot(int)
653 def setFilter(self, filter_index):
654 self._config.ui.qml.state.episode_list_filter = filter_index
657 def QML(filename):
658 for folder in gpodder.ui_folders:
659 filename = os.path.join(folder, filename)
660 if os.path.exists(filename):
661 return filename
663 class DeclarativeView(QDeclarativeView):
664 def __init__(self):
665 QDeclarativeView.__init__(self)
666 self.setAttribute(Qt.WA_OpaquePaintEvent)
667 self.setAttribute(Qt.WA_NoSystemBackground)
668 self.viewport().setAttribute(Qt.WA_OpaquePaintEvent)
669 self.viewport().setAttribute(Qt.WA_NoSystemBackground)
671 closing = Signal()
673 def closeEvent(self, event):
674 self.closing.emit()
675 event.ignore()
677 class qtPodder(QObject):
678 def __init__(self, args, gpodder_core):
679 QObject.__init__(self)
681 # Enable OpenGL rendering without requiring QtOpenGL
682 # On Harmattan we let the system choose the best graphicssystem
683 if '-graphicssystem' not in args and not gpodder.ui.harmattan:
684 if gpodder.ui.fremantle:
685 args += ['-graphicssystem', 'opengl']
686 elif not gpodder.win32:
687 args += ['-graphicssystem', 'raster']
689 self.app = QApplication(args)
690 signal.signal(signal.SIGINT, signal.SIG_DFL)
691 self.quit.connect(self.on_quit)
693 self.core = gpodder_core
694 self.config = self.core.config
695 self.db = self.core.db
696 self.model = self.core.model
698 self.config_proxy = ConfigProxy(self.config)
700 # Initialize the gpodder.net client
701 self.mygpo_client = my.MygPoClient(self.config)
703 gpodder.user_extensions.on_ui_initialized(self.model,
704 self.extensions_podcast_update_cb,
705 self.extensions_episode_download_cb)
707 self.view = DeclarativeView()
708 self.view.closing.connect(self.on_quit)
709 self.view.setResizeMode(QDeclarativeView.SizeRootObjectToView)
711 self.controller = Controller(self)
712 self.media_buttons_handler = helper.MediaButtonsHandler()
713 self.podcast_model = gPodderPodcastListModel()
714 self.episode_model = gPodderEpisodeListModel(self.config)
715 self.last_episode = None
717 # A dictionary of episodes that are currently active
718 # in some way (i.e. playing back or downloading)
719 self.active_episode_wrappers = {}
721 engine = self.view.engine()
723 # Maemo 5: Experimental Qt Mobility packages are installed in /opt
724 if gpodder.ui.fremantle:
725 for path in ('/opt/qtm11/imports', '/opt/qtm12/imports'):
726 engine.addImportPath(path)
727 elif gpodder.win32:
728 for path in (r'C:\QtSDK\Desktop\Qt\4.7.4\msvc2008\imports',):
729 engine.addImportPath(path)
731 # Add the cover art image provider
732 self.cover_provider = images.LocalCachedImageProvider()
733 engine.addImageProvider('cover', self.cover_provider)
735 root_context = self.view.rootContext()
736 root_context.setContextProperty('controller', self.controller)
737 root_context.setContextProperty('configProxy', self.config_proxy)
738 root_context.setContextProperty('mediaButtonsHandler',
739 self.media_buttons_handler)
741 # Load the QML UI (this could take a while...)
742 self.view.setSource(QUrl.fromLocalFile(QML('main_default.qml')))
744 # Proxy to the "main" QML object for direct access to Qt Properties
745 self.main = helper.QObjectProxy(self.view.rootObject().property('main'))
747 self.main.podcastModel = self.podcast_model
748 self.main.episodeModel = self.episode_model
750 self.view.setWindowTitle('gPodder')
752 if gpodder.ui.harmattan:
753 self.view.showFullScreen()
754 elif gpodder.ui.fremantle:
755 self.view.setAttribute(Qt.WA_Maemo5AutoOrientation, True)
756 self.view.showFullScreen()
757 else:
758 # On the Desktop, scale to fit my small laptop screen..
759 desktop = self.app.desktop()
760 if desktop.height() < 1000:
761 FACTOR = .8
762 self.view.scale(FACTOR, FACTOR)
763 size = self.view.size()
764 size *= FACTOR
765 self.view.resize(size)
766 self.view.show()
768 self.do_start_progress.connect(self.on_start_progress)
769 self.do_end_progress.connect(self.on_end_progress)
770 self.do_show_message.connect(self.on_show_message)
772 self.load_podcasts()
774 def add_active_episode(self, episode):
775 self.active_episode_wrappers[episode.id] = episode
776 episode.episode_wrapper_refcount += 1
778 def remove_active_episode(self, episode):
779 episode.episode_wrapper_refcount -= 1
780 if episode.episode_wrapper_refcount == 0:
781 del self.active_episode_wrappers[episode.id]
783 def load_last_episode(self):
784 last_episode = None
785 last_podcast = None
786 for podcast in self.podcast_model.get_podcasts():
787 for episode in podcast.get_all_episodes():
788 if not episode.last_playback:
789 continue
790 if last_episode is None or \
791 episode.last_playback > last_episode.last_playback:
792 last_episode = episode
793 last_podcast = podcast
795 if last_episode is not None:
796 self.last_episode = self.wrap_episode(last_podcast, last_episode)
797 # FIXME: Send last episode to player
798 #self.select_episode(self.last_episode)
800 def on_episode_deleted(self, episode):
801 # Remove episode from play queue (if it's in there)
802 self.main.removeQueuedEpisode(episode)
804 # If the episode that has been deleted is currently
805 # being played back (or paused), stop playback now.
806 if self.main.currentEpisode == episode:
807 self.main.togglePlayback(None)
809 def enqueue_episode(self, episode):
810 self.main.enqueueEpisode(episode)
812 def run(self):
813 return self.app.exec_()
815 quit = Signal()
817 def on_quit(self):
818 # Make sure the audio playback is stopped immediately
819 self.main.togglePlayback(None)
820 self.save_pending_data()
821 self.view.hide()
822 self.core.shutdown()
823 self.app.quit()
825 do_show_message = Signal(unicode)
827 @Slot(unicode)
828 def on_show_message(self, message):
829 self.main.showMessage(message)
831 def show_message(self, message):
832 self.do_show_message.emit(message)
834 def show_input_dialog(self, message, value='', accept=_('OK'),
835 reject=_('Cancel'), is_text=True):
836 self.main.showInputDialog(message, value, accept, reject, is_text)
838 def open_context_menu(self, items):
839 self.main.openContextMenu(items)
841 do_start_progress = Signal(str)
843 @Slot(str)
844 def on_start_progress(self, text):
845 self.main.startProgress(text)
847 def start_progress(self, text=_('Please wait...')):
848 self.do_start_progress.emit(text)
850 do_end_progress = Signal()
852 @Slot()
853 def on_end_progress(self):
854 self.main.endProgress()
856 def end_progress(self):
857 self.do_end_progress.emit()
859 def resort_podcast_list(self):
860 self.podcast_model.sort()
862 def insert_podcast(self, podcast):
863 self.podcast_model.insert_object(podcast)
864 self.mygpo_client.on_subscribe([podcast.url])
865 self.mygpo_client.flush()
867 def remove_podcast(self, podcast):
868 # Remove queued episodes for this specific podcast
869 self.main.removeQueuedEpisodesForPodcast(podcast)
871 if self.main.currentEpisode is not None:
872 # If the currently-playing episode is in the podcast
873 # that is to be deleted, stop playback immediately.
874 if self.main.currentEpisode.qpodcast == podcast:
875 self.main.togglePlayback(None)
876 self.podcast_model.remove_object(podcast)
877 self.mygpo_client.on_unsubscribe([podcast.url])
878 self.mygpo_client.flush()
880 def load_podcasts(self):
881 podcasts = map(model.QPodcast, self.model.get_podcasts())
882 self.podcast_model.set_podcasts(self.db, podcasts)
884 def wrap_episode(self, podcast, episode):
885 try:
886 return self.active_episode_wrappers[episode.id]
887 except KeyError:
888 return model.QEpisode(self, podcast, episode)
890 def select_podcast(self, podcast):
891 if isinstance(podcast, model.QPodcast):
892 # Normal QPodcast instance
893 wrap = functools.partial(self.wrap_episode, podcast)
894 objects = podcast.get_all_episodes()
895 else:
896 # EpisodeSubsetView
897 wrap = lambda args: self.wrap_episode(*args)
898 objects = podcast.get_all_episodes_with_podcast()
900 self.episode_model.set_objects(map(wrap, objects))
901 self.main.state = 'episodes'
903 def save_pending_data(self):
904 current_ep = self.main.currentEpisode
905 if isinstance(current_ep, model.QEpisode):
906 current_ep.save()
908 def podcast_to_qpodcast(self, podcast):
909 podcasts = filter(lambda p: p._podcast == podcast,
910 self.podcast_model.get_podcasts())
911 assert len(podcasts) <= 1
912 if podcasts:
913 return podcasts[0]
914 return None
916 def extensions_podcast_update_cb(self, podcast):
917 logger.debug('extensions_podcast_update_cb(%s)', podcast)
918 try:
919 qpodcast = self.podcast_to_qpodcast(podcast)
920 if qpodcast is not None:
921 qpodcast.qupdate(
922 finished_callback=self.controller.update_subset_stats)
923 except Exception, e:
924 logger.exception('extensions_podcast_update_cb(%s): %s', podcast, e)
926 def extensions_episode_download_cb(self, episode):
927 logger.debug('extensions_episode_download_cb(%s)', episode)
928 try:
929 qpodcast = self.podcast_to_qpodcast(episode.channel)
930 qepisode = self.wrap_episode(qpodcast, episode)
931 self.controller.downloadEpisode(qepisode)
932 except Exception, e:
933 logger.exception('extensions_episode_download_cb(%s): %s', episode, e)
935 def main(args):
936 gui = qtPodder(args, core.Core())
937 return gui.run()