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
37 from dbus
.mainloop
.glib
import DBusGMainLoop
45 from gpodder
import core
46 from gpodder
import util
47 from gpodder
import my
48 from gpodder
import query
50 from gpodder
.model
import Model
52 from gpodder
.qmlui
import model
53 from gpodder
.qmlui
import helper
54 from gpodder
.qmlui
import images
57 logger
= logging
.getLogger("qmlui")
60 EPISODE_LIST_FILTERS
= [
61 # (UI label, EQL expression)
63 (_('Hide deleted'), 'not deleted'),
64 (_('New'), 'new or downloading'),
65 (_('Downloaded'), 'downloaded or downloading'),
66 (_('Deleted'), 'deleted'),
67 (_('Finished'), 'finished'),
68 (_('Archived'), 'downloaded and archive'),
69 (_('Videos'), 'video'),
73 class ConfigProxy(QObject
):
74 def __init__(self
, config
):
75 QObject
.__init
__(self
)
78 config
.add_observer(self
._on
_config
_changed
)
80 def _on_config_changed(self
, name
, old_value
, new_value
):
81 if name
== 'ui.qml.autorotate':
82 self
.autorotateChanged
.emit()
83 elif name
== 'flattr.token':
84 self
.flattrTokenChanged
.emit()
85 elif name
== 'flattr.flattr_on_play':
86 self
.flattrOnPlayChanged
.emit()
88 def get_autorotate(self
):
89 return self
._config
.ui
.qml
.autorotate
91 def set_autorotate(self
, autorotate
):
92 self
._config
.ui
.qml
.autorotate
= autorotate
94 autorotateChanged
= Signal()
96 autorotate
= Property(bool, get_autorotate
, set_autorotate
,
97 notify
=autorotateChanged
)
99 def get_flattr_token(self
):
100 return self
._config
.flattr
.token
102 def set_flattr_token(self
, flattr_token
):
103 self
._config
.flattr
.token
= flattr_token
105 flattrTokenChanged
= Signal()
107 flattrToken
= Property(unicode, get_flattr_token
, set_flattr_token
,
108 notify
=flattrTokenChanged
)
110 def get_flattr_on_play(self
):
111 return self
._config
.flattr
.flattr_on_play
113 def set_flattr_on_play(self
, flattr_on_play
):
114 self
._config
.flattr
.flattr_on_play
= flattr_on_play
116 flattrOnPlayChanged
= Signal()
118 flattrOnPlay
= Property(bool, get_flattr_on_play
, set_flattr_on_play
,
119 notify
=flattrOnPlayChanged
)
122 class Controller(QObject
):
123 def __init__(self
, root
):
124 QObject
.__init
__(self
)
126 self
.context_menu_actions
= []
127 self
.episode_list_title
= u
''
128 self
.current_input_dialog
= None
129 self
.root
.config
.add_observer(self
.on_config_changed
)
130 self
._flattr
= self
.root
.core
.flattr
131 self
.flattr_button_text
= u
''
133 def on_config_changed(self
, name
, old_value
, new_value
):
134 logger
.info('Config changed: %s (%s -> %s)', name
,
135 old_value
, new_value
)
136 if name
== 'mygpo.enabled':
137 self
.myGpoEnabledChanged
.emit()
138 elif name
== 'mygpo.username':
139 self
.myGpoUsernameChanged
.emit()
140 elif name
== 'mygpo.password':
141 self
.myGpoPasswordChanged
.emit()
142 elif name
== 'mygpo.device.caption':
143 self
.myGpoDeviceCaptionChanged
.emit()
145 episodeListTitleChanged
= Signal()
147 def setEpisodeListTitle(self
, title
):
148 if self
.episode_list_title
!= title
:
149 self
.episode_list_title
= title
150 self
.episodeListTitleChanged
.emit()
152 def getEpisodeListTitle(self
):
153 return self
.episode_list_title
155 episodeListTitle
= Property(unicode, getEpisodeListTitle
, \
156 setEpisodeListTitle
, notify
=episodeListTitleChanged
)
158 flattrButtonTextChanged
= Signal()
160 def setFlattrButtonText(self
, flattr_button_text
):
161 if self
.flattr_button_text
!= flattr_button_text
:
162 self
.flattr_button_text
= flattr_button_text
163 self
.flattrButtonTextChanged
.emit()
165 def getFlattrButtonText(self
):
166 return self
.flattr_button_text
168 flattrButtonText
= Property(unicode, getFlattrButtonText
,
169 setFlattrButtonText
, notify
=flattrButtonTextChanged
)
172 def onPlayback(self
, qepisode
):
173 if (qepisode
.payment_url
and self
.root
.config
.flattr
.token
and
174 self
.root
.config
.flattr
.flattr_on_play
):
175 success
, message
= self
._flattr
.flattr_url(qepisode
.payment_url
)
177 logger
.warn('Flattr message on playback action: %s', message
)
180 def updateFlattrButtonText(self
, qepisode
):
181 self
.setFlattrButtonText('')
186 episode
= qepisode
._episode
188 if not episode
.payment_url
:
190 if not self
._flattr
.has_token():
191 self
.setFlattrButtonText(_('Sign in'))
194 @util.run_in_background
195 def get_flattr_info():
196 flattrs
, flattred
= self
._flattr
.get_thing_info(episode
.payment_url
)
199 self
.setFlattrButtonText(_('Flattred (%(count)d)') % {
203 self
.setFlattrButtonText(_('Flattr this (%(count)d)') % {
208 def flattrEpisode(self
, qepisode
):
212 episode
= qepisode
._episode
214 if not episode
.payment_url
:
216 if not self
._flattr
.has_token():
217 self
.root
.show_message(_('Sign in to Flattr in the settings.'))
220 self
.root
.start_progress(_('Flattring episode...'))
222 @util.run_in_background
223 def flattr_episode():
225 success
, message
= self
._flattr
.flattr_url(episode
.payment_url
)
227 self
.updateFlattrButtonText(qepisode
)
229 self
.root
.show_message(message
)
231 self
.root
.end_progress()
234 def getFlattrLoginURL(self
):
235 return self
._flattr
.get_auth_url()
238 def getFlattrCallbackURL(self
):
239 return self
._flattr
.CALLBACK
242 def processFlattrCode(self
, url
):
243 if not self
._flattr
.process_retrieved_code(url
):
244 self
.root
.show_message(_('Could not log in to Flattr.'))
246 @Slot(result
='QStringList')
247 def getEpisodeListFilterNames(self
):
248 return [caption
for caption
, eql
in EPISODE_LIST_FILTERS
]
250 @Slot('QVariant', str)
251 def multiEpisodeAction(self
, selected
, action
):
255 count
= len(selected
)
256 episodes
= map(self
.root
.episode_model
.get_object_by_index
, selected
)
259 for episode
in episodes
:
260 if not episode
.qarchive
:
261 episode
.delete_episode()
262 self
.update_subset_stats()
263 self
.root
.mygpo_client
.on_delete(episodes
)
264 self
.root
.mygpo_client
.flush()
265 for episode
in episodes
:
266 self
.root
.on_episode_deleted(episode
)
267 self
.root
.episode_model
.sort()
269 if action
== 'delete':
270 msg
= N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count
) % {'count':count
}
271 self
.confirm_action(msg
, _('Delete'), delete
)
272 elif action
== 'download':
273 for episode
in episodes
:
274 if episode
.qdownloaded
:
275 print ' XXX already downloaded'
277 episode
.qdownload(self
.root
.config
, self
.update_subset_stats
)
278 self
.root
.mygpo_client
.on_download(episodes
)
279 self
.root
.mygpo_client
.flush()
280 elif action
== 'play':
281 for episode
in episodes
:
282 self
.root
.enqueue_episode(episode
)
284 @Slot(str, result
=str)
285 def translate(self
, x
):
288 @Slot(str, str, int, result
=str)
289 def ntranslate(self
, singular
, plural
, count
):
290 return N_(singular
, plural
, count
)
292 @Slot(str, int, result
=str)
293 def formatCount(self
, template
, count
):
294 return template
% {'count': count
}
297 def getVersion(self
):
298 return gpodder
.__version
__
301 def getReleased(self
):
302 return gpodder
.__date
__
304 @Slot(result
=unicode)
305 def getCredits(self
):
306 credits_file
= os
.path
.join(gpodder
.prefix
, 'share', 'gpodder', 'credits.txt')
307 return util
.convert_bytes(open(credits_file
).read())
309 @Slot(result
=unicode)
310 def getCopyright(self
):
311 return util
.convert_bytes(gpodder
.__copyright
__)
314 def getLicense(self
):
315 return gpodder
.__license
__
319 return gpodder
.__url
__
322 def loadLastEpisode(self
):
323 self
.root
.load_last_episode()
325 @Slot(QObject
, int, int)
326 def storePlaybackAction(self
, episode
, start
, end
):
328 logger
.info('Ignoring too short playback action.')
330 total
= episode
.qduration
331 self
.root
.mygpo_client
.on_playback_full(episode
, start
, end
, total
)
332 self
.root
.mygpo_client
.flush()
335 def playVideo(self
, episode
):
336 """Video Playback on MeeGo 1.2 Harmattan"""
339 self
.update_subset_stats()
341 url
= episode
.get_playback_url()
342 if gpodder
.ui
.harmattan
:
343 subprocess
.Popen(['video-suite', url
])
347 self
.root
.mygpo_client
.on_playback([episode
])
348 self
.root
.mygpo_client
.flush()
351 def podcastSelected(self
, podcast
):
352 self
.setEpisodeListTitle(podcast
.qtitle
)
353 self
.root
.select_podcast(podcast
)
355 windowTitleChanged
= Signal()
357 def getWindowTitle(self
):
358 return self
.root
.view
.windowTitle()
360 def setWindowTitle(self
, windowTitle
):
361 if gpodder
.ui
.fremantle
:
362 self
.root
.view
.setWindowTitle(windowTitle
)
364 windowTitle
= Property(unicode, getWindowTitle
,
365 setWindowTitle
, notify
=windowTitleChanged
)
368 def myGpoUploadList(self
):
369 def upload_proc(self
):
370 self
.root
.start_progress(_('Uploading subscriptions...'))
374 self
.root
.mygpo_client
.set_subscriptions([podcast
.url
375 for podcast
in self
.root
.podcast_model
.get_podcasts()])
377 self
.root
.show_message('\n'.join((_('Error on upload:'), unicode(e
))))
379 self
.root
.end_progress()
381 util
.run_in_background(lambda: upload_proc(self
))
384 def saveMyGpoSettings(self
):
385 # Update the device settings and upload changes
386 self
.root
.mygpo_client
.create_device()
387 self
.root
.mygpo_client
.flush(now
=True)
389 myGpoEnabledChanged
= Signal()
391 def getMyGpoEnabled(self
):
392 return self
.root
.config
.mygpo
.enabled
394 def setMyGpoEnabled(self
, enabled
):
395 self
.root
.config
.mygpo
.enabled
= enabled
397 myGpoEnabled
= Property(bool, getMyGpoEnabled
,
398 setMyGpoEnabled
, notify
=myGpoEnabledChanged
)
400 myGpoUsernameChanged
= Signal()
402 def getMyGpoUsername(self
):
403 return model
.convert(self
.root
.config
.mygpo
.username
)
405 def setMyGpoUsername(self
, username
):
406 self
.root
.config
.mygpo
.username
= username
408 myGpoUsername
= Property(unicode, getMyGpoUsername
,
409 setMyGpoUsername
, notify
=myGpoUsernameChanged
)
411 myGpoPasswordChanged
= Signal()
413 def getMyGpoPassword(self
):
414 return model
.convert(self
.root
.config
.mygpo
.password
)
416 def setMyGpoPassword(self
, password
):
417 self
.root
.config
.mygpo
.password
= password
419 myGpoPassword
= Property(unicode, getMyGpoPassword
,
420 setMyGpoPassword
, notify
=myGpoPasswordChanged
)
422 myGpoDeviceCaptionChanged
= Signal()
424 def getMyGpoDeviceCaption(self
):
425 return model
.convert(self
.root
.config
.mygpo
.device
.caption
)
427 def setMyGpoDeviceCaption(self
, caption
):
428 self
.root
.config
.mygpo
.device
.caption
= caption
430 myGpoDeviceCaption
= Property(unicode, getMyGpoDeviceCaption
,
431 setMyGpoDeviceCaption
, notify
=myGpoDeviceCaptionChanged
)
434 def podcastContextMenu(self
, podcast
):
437 if isinstance(podcast
, model
.EpisodeSubsetView
):
438 menu
.append(helper
.Action(_('Update all'), 'update-all', podcast
))
440 menu
.append(helper
.Action(_('Update'), 'update', podcast
))
441 menu
.append(helper
.Action(_('Mark episodes as old'), 'mark-as-read', podcast
))
442 menu
.append(helper
.Action(_('Rename'), 'rename-podcast', podcast
))
443 menu
.append(helper
.Action(_('Change section'), 'change-section', podcast
))
444 menu
.append(helper
.Action(_('Unsubscribe'), 'unsubscribe', podcast
))
446 #menu.append(helper.Action('Force update all', 'force-update-all', podcast))
447 #menu.append(helper.Action('Force update', 'force-update', podcast))
449 self
.show_context_menu(menu
)
451 def show_context_menu(self
, actions
):
452 if gpodder
.ui
.harmattan
:
453 actions
= filter(lambda a
: a
.caption
!= '', actions
)
454 self
.context_menu_actions
= actions
455 self
.root
.open_context_menu(self
.context_menu_actions
)
457 def update_subset_stats(self
):
458 # This should be called when an episode changes state,
459 # so that all subset views (e.g. "All episodes") can
460 # update its status (i.e. download/new counts, etc..)
461 for podcast
in self
.root
.podcast_model
.get_objects():
462 if isinstance(podcast
, model
.EpisodeSubsetView
):
465 def find_episode(self
, podcast_url
, episode_url
):
466 for podcast
in self
.root
.podcast_model
.get_podcasts():
467 if podcast
.url
== podcast_url
:
468 for episode
in podcast
.get_all_episodes():
469 if episode
.url
== episode_url
:
474 def updateAllPodcasts(self
):
475 # Process episode actions received from gpodder.net
476 def merge_proc(self
):
477 self
.root
.start_progress(_('Merging episode actions...'))
479 def find_episode(podcast_url
, episode_url
, counter
):
481 self
.root
.start_progress(_('Merging episode actions (%d)')
483 return self
.find_episode(podcast_url
, episode_url
)
486 d
= {'x': 0} # Used to "remember" the counter inside find_episode
487 self
.root
.mygpo_client
.process_episode_actions(lambda x
, y
:
488 find_episode(x
, y
, d
))
490 self
.root
.end_progress()
492 util
.run_in_background(lambda: merge_proc(self
))
494 for podcast
in self
.root
.podcast_model
.get_objects():
495 podcast
.qupdate(finished_callback
=self
.update_subset_stats
)
498 def contextMenuResponse(self
, index
):
499 assert index
< len(self
.context_menu_actions
)
500 action
= self
.context_menu_actions
[index
]
501 if action
.action
== 'update':
502 action
.target
.qupdate(finished_callback
=self
.update_subset_stats
)
503 elif action
.action
== 'force-update':
504 action
.target
.qupdate(force
=True, \
505 finished_callback
=self
.update_subset_stats
)
506 elif action
.action
== 'update-all':
507 self
.updateAllPodcasts()
508 elif action
.action
== 'force-update-all':
509 for podcast
in self
.root
.podcast_model
.get_objects():
510 podcast
.qupdate(force
=True, finished_callback
=self
.update_subset_stats
)
511 if action
.action
== 'unsubscribe':
513 action
.target
.remove_downloaded()
514 action
.target
.delete()
515 self
.root
.remove_podcast(action
.target
)
517 self
.confirm_action(_('Remove this podcast and episodes?'),
518 _('Unsubscribe'), unsubscribe
)
519 elif action
.action
== 'episode-toggle-new':
520 action
.target
.toggle_new()
521 self
.update_subset_stats()
522 elif action
.action
== 'episode-toggle-archive':
523 action
.target
.toggle_archive()
524 self
.update_subset_stats()
525 elif action
.action
== 'episode-delete':
526 self
.deleteEpisode(action
.target
)
527 elif action
.action
== 'episode-enqueue':
528 self
.root
.enqueue_episode(action
.target
)
529 elif action
.action
== 'mark-as-read':
530 for episode
in action
.target
.get_all_episodes():
531 if not episode
.was_downloaded(and_exists
=True):
532 episode
.mark(is_played
=True)
533 action
.target
.changed
.emit()
534 self
.update_subset_stats()
535 elif action
.action
== 'change-section':
536 def section_changer(podcast
):
537 section
= yield (_('New section name:'), podcast
.section
,
539 if section
and section
!= podcast
.section
:
540 podcast
.set_section(section
)
541 self
.root
.resort_podcast_list()
543 self
.start_input_dialog(section_changer(action
.target
))
544 elif action
.action
== 'rename-podcast':
545 def title_changer(podcast
):
546 title
= yield (_('New name:'), podcast
.title
,
548 if title
and title
!= podcast
.title
:
549 podcast
.rename(title
)
550 self
.root
.resort_podcast_list()
552 self
.start_input_dialog(title_changer(action
.target
))
554 def confirm_action(self
, message
, affirmative
, callback
):
555 def confirm(message
, affirmative
, callback
):
556 args
= (message
, '', affirmative
, _('Cancel'), False)
560 self
.start_input_dialog(confirm(message
, affirmative
, callback
))
562 def start_input_dialog(self
, generator
):
563 """Carry out an input dialog with the UI
565 This function takes a generator function as argument
566 which should yield a tuple of arguments for the
567 "show_input_dialog" function (i.e. message, default
568 value, accept and reject message - only the message
569 is mandatory, the other arguments have default values).
571 The generator will receive the user's response as a
572 result of the yield expression. If the user accepted
573 the dialog, a string is returned (the value that has
574 been input), otherwise None is returned.
579 result = yield ('A simple message', 'default value')
581 # user has rejected the dialog
583 # user has accepted, new value in "result"
585 start_input_dialog(some_function())
587 assert self
.current_input_dialog
is None
588 self
.current_input_dialog
= generator
589 args
= generator
.next()
590 self
.root
.show_input_dialog(*args
)
592 @Slot(bool, str, bool)
593 def inputDialogResponse(self
, accepted
, value
, is_text
):
600 self
.current_input_dialog
.send(value
)
601 except StopIteration:
602 # This is expected, as the generator
603 # should only have one yield statement
606 self
.current_input_dialog
= None
609 def downloadEpisode(self
, episode
):
610 episode
.qdownload(self
.root
.config
, self
.update_subset_stats
)
611 self
.root
.mygpo_client
.on_download([episode
])
612 self
.root
.mygpo_client
.flush()
615 def cancelDownload(self
, episode
):
616 episode
.download_task
.cancel()
617 episode
.download_task
.removed_from_list()
620 def deleteEpisode(self
, episode
):
622 episode
.delete_episode()
623 self
.update_subset_stats()
624 self
.root
.mygpo_client
.on_delete([episode
])
625 self
.root
.mygpo_client
.flush()
626 self
.root
.on_episode_deleted(episode
)
627 self
.root
.episode_model
.sort()
629 self
.confirm_action(_('Delete this episode?'), _('Delete'), delete
)
632 def acquireEpisode(self
, episode
):
633 self
.root
.add_active_episode(episode
)
636 def releaseEpisode(self
, episode
):
637 self
.root
.remove_active_episode(episode
)
640 def contextMenuClosed(self
):
641 self
.context_menu_actions
= []
644 def episodeContextMenu(self
, episode
):
647 toggle_new
= _('Mark as old') if episode
.is_new
else _('Mark as new')
648 menu
.append(helper
.Action(toggle_new
, 'episode-toggle-new', episode
))
650 toggle_archive
= _('Allow deletion') if episode
.archive
else _('Archive')
651 menu
.append(helper
.Action(toggle_archive
, 'episode-toggle-archive', episode
))
653 if episode
.state
!= gpodder
.STATE_DELETED
:
654 menu
.append(helper
.Action(_('Delete'), 'episode-delete', episode
))
656 menu
.append(helper
.Action(_('Add to play queue'), 'episode-enqueue', episode
))
658 self
.show_context_menu(menu
)
661 def addSubscriptions(self
, urls
):
662 def not_yet_subscribed(url
):
663 for podcast
in self
.root
.podcast_model
.get_objects():
664 if isinstance(podcast
, model
.EpisodeSubsetView
):
667 if podcast
.url
== url
:
668 logger
.info('Already subscribed: %s', url
)
673 urls
= map(util
.normalize_feed_url
, urls
)
674 urls
= filter(not_yet_subscribed
, urls
)
676 def subscribe_proc(self
, urls
):
677 self
.root
.start_progress(_('Adding podcasts...'))
679 for idx
, url
in enumerate(urls
):
681 self
.root
.start_progress(_('Adding podcasts...') + ' (%d/%d)' % (idx
, len(urls
)))
683 podcast
= self
.root
.model
.load_podcast(url
=url
, create
=True,
684 max_episodes
=self
.root
.config
.max_episodes_per_feed
)
686 self
.root
.insert_podcast(model
.QPodcast(podcast
))
688 logger
.warn('Cannot add pocast: %s', e
)
689 # XXX: Visual feedback in the QML UI
691 self
.root
.end_progress()
693 util
.run_in_background(lambda: subscribe_proc(self
, urls
))
696 def currentEpisodeChanging(self
):
697 self
.root
.save_pending_data()
701 self
.root
.quit
.emit()
705 if gpodder
.ui
.harmattan
:
706 self
.root
.view
.showMinimized()
707 elif gpodder
.ui
.fremantle
:
708 os
.system('dbus-send /com/nokia/hildon_desktop '+
709 'com.nokia.hildon_desktop.exit_app_view')
711 self
.root
.view
.showMinimized()
714 class gPodderListModel(QAbstractListModel
):
715 def __init__(self
, objects
=None):
716 QAbstractListModel
.__init
__(self
)
719 self
._objects
= objects
720 self
.setRoleNames({0: 'modelData', 1: 'section'})
723 # Unimplemented for the generic list model
726 def insert_object(self
, o
):
727 self
._objects
.append(o
)
730 def remove_object(self
, o
):
731 self
._objects
.remove(o
)
734 def set_objects(self
, objects
):
735 self
._objects
= objects
738 def get_objects(self
):
741 def get_object(self
, index
):
742 return self
._objects
[index
.row()]
744 def rowCount(self
, parent
=QModelIndex()):
745 return len(self
.get_objects())
747 def data(self
, index
, role
):
750 return self
.get_object(index
)
752 return self
.get_object(index
).qsection
755 class gPodderPodcastListModel(gPodderListModel
):
756 def set_podcasts(self
, db
, podcasts
):
758 model
.EpisodeSubsetView(db
, self
, _('All episodes'), ''),
760 self
.set_objects(views
+ podcasts
)
762 def get_podcasts(self
):
763 return filter(lambda podcast
: isinstance(podcast
, model
.QPodcast
),
767 self
._objects
= sorted(self
._objects
, key
=model
.QPodcast
.sort_key
)
770 class gPodderEpisodeListModel(gPodderListModel
):
771 def __init__(self
, config
):
772 gPodderListModel
.__init
__(self
)
773 self
._filter
= config
.ui
.qml
.state
.episode_list_filter
775 self
._is
_subset
_view
= False
777 self
._config
= config
778 config
.add_observer(self
._on
_config
_changed
)
780 is_subset_view_changed
= Signal()
782 def get_is_subset_view(self
):
783 return self
._is
_subset
_view
785 def set_is_subset_view(self
, is_subset_view
):
786 if is_subset_view
!= self
.is_subset_view
:
787 self
._is
_subset
_view
= is_subset_view
788 self
.is_subset_view_changed
.emit()
790 is_subset_view
= Property(bool, get_is_subset_view
,
791 set_is_subset_view
, notify
=is_subset_view_changed
)
793 def _on_config_changed(self
, name
, old_value
, new_value
):
794 if name
== 'ui.qml.state.episode_list_filter':
795 self
._filter
= new_value
799 caption
, eql
= EPISODE_LIST_FILTERS
[self
._filter
]
802 self
._filtered
= self
._objects
805 match
= lambda episode
: eql
.match(episode
._episode
)
806 self
._filtered
= filter(match
, self
._objects
)
810 def get_objects(self
):
811 return self
._filtered
813 def get_object(self
, index
):
814 return self
._filtered
[index
.row()]
816 @Slot(int, result
=QObject
)
817 def get_object_by_index(self
, index
):
818 return self
._filtered
[int(index
)]
825 def setFilter(self
, filter_index
):
826 self
._config
.ui
.qml
.state
.episode_list_filter
= filter_index
830 for folder
in gpodder
.ui_folders
:
831 filename
= os
.path
.join(folder
, filename
)
832 if os
.path
.exists(filename
):
835 class DeclarativeView(QDeclarativeView
):
837 QDeclarativeView
.__init
__(self
)
838 self
.setAttribute(Qt
.WA_OpaquePaintEvent
)
839 self
.setAttribute(Qt
.WA_NoSystemBackground
)
840 self
.viewport().setAttribute(Qt
.WA_OpaquePaintEvent
)
841 self
.viewport().setAttribute(Qt
.WA_NoSystemBackground
)
845 def closeEvent(self
, event
):
849 class qtPodder(QObject
):
850 def __init__(self
, args
, gpodder_core
, dbus_bus_name
):
851 QObject
.__init
__(self
)
853 self
.dbus_bus_name
= dbus_bus_name
854 # TODO: Expose the same D-Bus API as the Gtk UI D-Bus object (/gui)
855 # TODO: Create a gpodder.dbusproxy.DBusPodcastsProxy object (/podcasts)
857 # Enable OpenGL rendering without requiring QtOpenGL
858 # On Harmattan we let the system choose the best graphicssystem
859 if '-graphicssystem' not in args
and not gpodder
.ui
.harmattan
:
860 if gpodder
.ui
.fremantle
:
861 args
+= ['-graphicssystem', 'opengl']
862 elif not gpodder
.win32
:
863 args
+= ['-graphicssystem', 'raster']
865 self
.app
= QApplication(args
)
866 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
867 self
.quit
.connect(self
.on_quit
)
869 self
.core
= gpodder_core
870 self
.config
= self
.core
.config
871 self
.db
= self
.core
.db
872 self
.model
= self
.core
.model
874 self
.config_proxy
= ConfigProxy(self
.config
)
876 # Initialize the gpodder.net client
877 self
.mygpo_client
= my
.MygPoClient(self
.config
)
879 gpodder
.user_extensions
.on_ui_initialized(self
.model
,
880 self
.extensions_podcast_update_cb
,
881 self
.extensions_episode_download_cb
)
883 self
.view
= DeclarativeView()
884 self
.view
.closing
.connect(self
.on_quit
)
885 self
.view
.setResizeMode(QDeclarativeView
.SizeRootObjectToView
)
887 self
.controller
= Controller(self
)
888 self
.media_buttons_handler
= helper
.MediaButtonsHandler()
889 self
.tracker_miner_config
= helper
.TrackerMinerConfig()
890 self
.podcast_model
= gPodderPodcastListModel()
891 self
.episode_model
= gPodderEpisodeListModel(self
.config
)
892 self
.last_episode
= None
894 # A dictionary of episodes that are currently active
895 # in some way (i.e. playing back or downloading)
896 self
.active_episode_wrappers
= {}
898 engine
= self
.view
.engine()
900 # Maemo 5: Experimental Qt Mobility packages are installed in /opt
901 if gpodder
.ui
.fremantle
:
902 for path
in ('/opt/qtm11/imports', '/opt/qtm12/imports'):
903 engine
.addImportPath(path
)
905 for path
in (r
'C:\QtSDK\Desktop\Qt\4.7.4\msvc2008\imports',):
906 engine
.addImportPath(path
)
908 # Add the cover art image provider
909 self
.cover_provider
= images
.LocalCachedImageProvider()
910 engine
.addImageProvider('cover', self
.cover_provider
)
912 root_context
= self
.view
.rootContext()
913 root_context
.setContextProperty('controller', self
.controller
)
914 root_context
.setContextProperty('configProxy', self
.config_proxy
)
915 root_context
.setContextProperty('mediaButtonsHandler',
916 self
.media_buttons_handler
)
917 root_context
.setContextProperty('trackerMinerConfig',
918 self
.tracker_miner_config
)
920 # Load the QML UI (this could take a while...)
921 self
.view
.setSource(QUrl
.fromLocalFile(QML('main_default.qml')))
923 # Proxy to the "main" QML object for direct access to Qt Properties
924 self
.main
= helper
.QObjectProxy(self
.view
.rootObject().property('main'))
926 self
.main
.podcastModel
= self
.podcast_model
927 self
.main
.episodeModel
= self
.episode_model
929 self
.view
.setWindowTitle('gPodder')
931 if gpodder
.ui
.harmattan
:
932 self
.view
.showFullScreen()
933 elif gpodder
.ui
.fremantle
:
934 self
.view
.setAttribute(Qt
.WA_Maemo5AutoOrientation
, True)
935 self
.view
.showFullScreen()
937 # On the Desktop, scale to fit my small laptop screen..
938 desktop
= self
.app
.desktop()
939 if desktop
.height() < 1000:
941 self
.view
.scale(FACTOR
, FACTOR
)
942 size
= self
.view
.size()
944 self
.view
.resize(size
)
947 self
.do_start_progress
.connect(self
.on_start_progress
)
948 self
.do_end_progress
.connect(self
.on_end_progress
)
949 self
.do_show_message
.connect(self
.on_show_message
)
953 def add_active_episode(self
, episode
):
954 self
.active_episode_wrappers
[episode
.id] = episode
955 episode
.episode_wrapper_refcount
+= 1
957 def remove_active_episode(self
, episode
):
958 episode
.episode_wrapper_refcount
-= 1
959 if episode
.episode_wrapper_refcount
== 0:
960 del self
.active_episode_wrappers
[episode
.id]
962 def load_last_episode(self
):
965 for podcast
in self
.podcast_model
.get_podcasts():
966 for episode
in podcast
.get_all_episodes():
967 if not episode
.last_playback
:
969 if last_episode
is None or \
970 episode
.last_playback
> last_episode
.last_playback
:
971 last_episode
= episode
972 last_podcast
= podcast
974 if last_episode
is not None:
975 self
.last_episode
= self
.wrap_episode(last_podcast
, last_episode
)
976 # FIXME: Send last episode to player
977 #self.select_episode(self.last_episode)
979 def on_episode_deleted(self
, episode
):
980 # Remove episode from play queue (if it's in there)
981 self
.main
.removeQueuedEpisode(episode
)
983 # If the episode that has been deleted is currently
984 # being played back (or paused), stop playback now.
985 if self
.main
.currentEpisode
== episode
:
986 self
.main
.togglePlayback(None)
988 def enqueue_episode(self
, episode
):
989 self
.main
.enqueueEpisode(episode
)
992 return self
.app
.exec_()
997 # Make sure the audio playback is stopped immediately
998 self
.main
.togglePlayback(None)
999 self
.save_pending_data()
1001 self
.core
.shutdown()
1004 do_show_message
= Signal(unicode)
1007 def on_show_message(self
, message
):
1008 self
.main
.showMessage(message
)
1010 def show_message(self
, message
):
1011 self
.do_show_message
.emit(message
)
1013 def show_input_dialog(self
, message
, value
='', accept
=_('OK'),
1014 reject
=_('Cancel'), is_text
=True):
1015 self
.main
.showInputDialog(message
, value
, accept
, reject
, is_text
)
1017 def open_context_menu(self
, items
):
1018 self
.main
.openContextMenu(items
)
1020 do_start_progress
= Signal(str)
1023 def on_start_progress(self
, text
):
1024 self
.main
.startProgress(text
)
1026 def start_progress(self
, text
=_('Please wait...')):
1027 self
.do_start_progress
.emit(text
)
1029 do_end_progress
= Signal()
1032 def on_end_progress(self
):
1033 self
.main
.endProgress()
1035 def end_progress(self
):
1036 self
.do_end_progress
.emit()
1038 def resort_podcast_list(self
):
1039 self
.podcast_model
.sort()
1041 def insert_podcast(self
, podcast
):
1042 self
.podcast_model
.insert_object(podcast
)
1043 self
.mygpo_client
.on_subscribe([podcast
.url
])
1044 self
.mygpo_client
.flush()
1046 def remove_podcast(self
, podcast
):
1047 # Remove queued episodes for this specific podcast
1048 self
.main
.removeQueuedEpisodesForPodcast(podcast
)
1050 if self
.main
.currentEpisode
is not None:
1051 # If the currently-playing episode is in the podcast
1052 # that is to be deleted, stop playback immediately.
1053 if self
.main
.currentEpisode
.qpodcast
== podcast
:
1054 self
.main
.togglePlayback(None)
1055 self
.podcast_model
.remove_object(podcast
)
1056 self
.mygpo_client
.on_unsubscribe([podcast
.url
])
1057 self
.mygpo_client
.flush()
1059 def load_podcasts(self
):
1060 podcasts
= map(model
.QPodcast
, self
.model
.get_podcasts())
1061 self
.podcast_model
.set_podcasts(self
.db
, podcasts
)
1063 def wrap_episode(self
, podcast
, episode
):
1065 return self
.active_episode_wrappers
[episode
.id]
1067 return model
.QEpisode(self
, podcast
, episode
)
1069 def select_podcast(self
, podcast
):
1070 if isinstance(podcast
, model
.QPodcast
):
1071 # Normal QPodcast instance
1072 wrap
= functools
.partial(self
.wrap_episode
, podcast
)
1073 objects
= podcast
.get_all_episodes()
1074 self
.episode_model
.set_is_subset_view(False)
1077 wrap
= lambda args
: self
.wrap_episode(*args
)
1078 objects
= podcast
.get_all_episodes_with_podcast()
1079 self
.episode_model
.set_is_subset_view(True)
1081 self
.episode_model
.set_objects(map(wrap
, objects
))
1082 self
.main
.state
= 'episodes'
1084 def save_pending_data(self
):
1085 current_ep
= self
.main
.currentEpisode
1086 if isinstance(current_ep
, model
.QEpisode
):
1089 def podcast_to_qpodcast(self
, podcast
):
1090 podcasts
= filter(lambda p
: p
._podcast
== podcast
,
1091 self
.podcast_model
.get_podcasts())
1092 assert len(podcasts
) <= 1
1097 def extensions_podcast_update_cb(self
, podcast
):
1098 logger
.debug('extensions_podcast_update_cb(%s)', podcast
)
1100 qpodcast
= self
.podcast_to_qpodcast(podcast
)
1101 if qpodcast
is not None:
1103 finished_callback
=self
.controller
.update_subset_stats
)
1104 except Exception, e
:
1105 logger
.exception('extensions_podcast_update_cb(%s): %s', podcast
, e
)
1107 def extensions_episode_download_cb(self
, episode
):
1108 logger
.debug('extensions_episode_download_cb(%s)', episode
)
1110 qpodcast
= self
.podcast_to_qpodcast(episode
.channel
)
1111 qepisode
= self
.wrap_episode(qpodcast
, episode
)
1112 self
.controller
.downloadEpisode(qepisode
)
1113 except Exception, e
:
1114 logger
.exception('extensions_episode_download_cb(%s): %s', episode
, e
)
1118 dbus_main_loop
= DBusGMainLoop(set_as_default
=True)
1119 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
1121 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
,
1122 bus
=gpodder
.dbus_session_bus
)
1123 except dbus
.exceptions
.DBusException
, dbe
:
1124 logger
.warn('Cannot get "on the bus".', exc_info
=True)
1127 gui
= qtPodder(args
, core
.Core(), bus_name
)