Make welcome screen more beautiful, fix mygpo bug
[gpodder.git] / src / gpodder / gui.py
blobf2fc8ab4bb59ad0804f513a38a437827ad181ce7
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 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 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import subprocess
28 import glob
29 import time
30 import urllib
31 import urllib2
32 import datetime
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
42 from gpodder import util
43 from gpodder import opml
44 from gpodder import services
45 from gpodder import sync
46 from gpodder import download
47 from gpodder import SimpleGladeApp
48 from gpodder import my
49 from gpodder.liblogger import log
50 from gpodder.dbsqlite import db
51 from gpodder import resolver
53 try:
54 from gpodder import trayicon
55 have_trayicon = True
56 except Exception, exc:
57 log('Warning: Could not import gpodder.trayicon.', traceback=True)
58 log('Warning: This probably means your PyGTK installation is too old!')
59 have_trayicon = False
61 from libpodcasts import podcastChannel
62 from libpodcasts import LocalDBReader
63 from libpodcasts import podcastItem
64 from libpodcasts import channels_to_model
65 from libpodcasts import update_channel_model_by_iter
66 from libpodcasts import load_channels
67 from libpodcasts import update_channels
68 from libpodcasts import save_channels
69 from libpodcasts import can_restore_from_opml
70 from libpodcasts import HTTPAuthError
72 from gpodder.libgpodder import gl
74 from libplayers import UserAppsReader
76 from libtagupdate import tagging_supported
78 if gpodder.interface == gpodder.GUI:
79 WEB_BROWSER_ICON = 'web-browser'
80 elif gpodder.interface == gpodder.MAEMO:
81 import hildon
82 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
84 app_name = "gpodder"
85 app_version = "unknown" # will be set in main() call
86 app_authors = [
87 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
88 '',
89 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
90 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
91 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
92 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
93 'Carlos Moffat', 'Chris Arnold', 'Clark Burbidge', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
94 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'FFranci72', 'Florian Richter', 'Frank Harper',
95 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
96 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
97 'Jérôme Chabod', 'Jerry Moss',
98 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
99 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
100 'Jürgen Schinker', 'Justin Forest',
101 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
102 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Mykola Nikishov', 'narf',
103 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
104 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
105 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
106 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
107 'Robert Young', 'Roel Groeneveld',
108 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
109 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
110 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
111 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
112 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
114 'List may be incomplete - please contact me.'
116 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
117 app_website = 'http://www.gpodder.org/'
119 # these will be filled with pathnames in bin/gpodder
120 glade_dir = [ 'share', 'gpodder' ]
121 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
122 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
125 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
126 gpodder_main_window = None
127 finger_friendly_widgets = []
129 def __init__( self, **kwargs):
130 path = os.path.join( glade_dir, '%s.glade' % app_name)
131 root = self.__class__.__name__
132 domain = app_name
134 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
136 # Set widgets to finger-friendly mode if on Maemo
137 for widget_name in self.finger_friendly_widgets:
138 self.set_finger_friendly(getattr(self, widget_name))
140 if root == 'gPodder':
141 GladeWidget.gpodder_main_window = self.gPodder
142 else:
143 # If we have a child window, set it transient for our main window
144 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
146 if gpodder.interface == gpodder.GUI:
147 if hasattr( self, 'center_on_widget'):
148 ( x, y ) = self.gpodder_main_window.get_position()
149 a = self.center_on_widget.allocation
150 ( x, y ) = ( x + a.x, y + a.y )
151 ( w, h ) = ( a.width, a.height )
152 ( pw, ph ) = getattr( self, root).get_size()
153 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
154 else:
155 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
157 def notification(self, message, title=None):
158 util.idle_add(self.show_message, message, title)
160 def show_message( self, message, title = None):
161 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
162 if title is None:
163 title = 'gPodder'
164 self.tray_icon.send_notification(message, title)
165 return
167 if gpodder.interface == gpodder.GUI:
168 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
169 if title:
170 dlg.set_title(str(title))
171 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
172 else:
173 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
174 elif gpodder.interface == gpodder.MAEMO:
175 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
177 dlg.run()
178 dlg.destroy()
180 def set_finger_friendly(self, widget):
182 If we are on Maemo, we carry out the necessary
183 operations to turn a widget into a finger-friendly
184 one, depending on which type of widget it is (i.e.
185 buttons will have more padding, TreeViews a thick
186 scrollbar, etc..)
188 if gpodder.interface == gpodder.MAEMO:
189 if isinstance(widget, gtk.Misc):
190 widget.set_padding(0, 5)
191 elif isinstance(widget, gtk.Button):
192 for child in widget.get_children():
193 if isinstance(child, gtk.Alignment):
194 child.set_padding(10, 10, 5, 5)
195 else:
196 child.set_padding(10, 10)
197 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
198 parent = widget.get_parent()
199 if isinstance(parent, gtk.ScrolledWindow):
200 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
201 elif isinstance(widget, gtk.MenuItem):
202 for child in widget.get_children():
203 self.set_finger_friendly(child)
204 else:
205 log('Cannot set widget finger-friendly: %s', widget, sender=self)
207 return widget
209 def show_confirmation( self, message, title = None):
210 if gpodder.interface == gpodder.GUI:
211 affirmative = gtk.RESPONSE_YES
212 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
213 if title:
214 dlg.set_title(str(title))
215 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
216 else:
217 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
218 elif gpodder.interface == gpodder.MAEMO:
219 affirmative = gtk.RESPONSE_OK
220 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
222 response = dlg.run()
223 dlg.destroy()
225 return response == affirmative
227 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
228 """ An authentication dialog based on
229 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
231 dialog = gtk.MessageDialog(
232 GladeWidget.gpodder_main_window,
233 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
234 gtk.MESSAGE_QUESTION,
235 gtk.BUTTONS_OK_CANCEL )
237 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
239 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
240 dialog.set_title(_('Authentication required'))
241 dialog.format_secondary_markup(message)
242 dialog.set_default_response(gtk.RESPONSE_OK)
244 if register_callback is not None:
245 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
247 username_entry = gtk.Entry()
248 password_entry = gtk.Entry()
250 username_entry.connect('activate', lambda w: password_entry.grab_focus())
251 password_entry.set_visibility(False)
252 password_entry.set_activates_default(True)
254 if username is not None:
255 username_entry.set_text(username)
256 if password is not None:
257 password_entry.set_text(password)
259 table = gtk.Table(2, 2)
260 table.set_row_spacings(6)
261 table.set_col_spacings(6)
263 username_label = gtk.Label()
264 username_label.set_markup('<b>' + username_prompt + ':</b>')
265 username_label.set_alignment(0.0, 0.5)
266 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
267 table.attach(username_entry, 1, 2, 0, 1)
269 password_label = gtk.Label()
270 password_label.set_markup('<b>' + _('Password') + ':</b>')
271 password_label.set_alignment(0.0, 0.5)
272 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
273 table.attach(password_entry, 1, 2, 1, 2)
275 dialog.vbox.pack_end(table, True, True, 0)
276 dialog.show_all()
277 response = dialog.run()
279 while response == gtk.RESPONSE_HELP:
280 register_callback()
281 response = dialog.run()
283 password_entry.set_visibility(True)
284 dialog.destroy()
286 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
288 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
289 if dst_filename is None:
290 dst_filename = src_filename
292 if dst_directory is None:
293 dst_directory = os.path.expanduser( '~')
295 ( base, extension ) = os.path.splitext( src_filename)
297 if not dst_filename.endswith( extension):
298 dst_filename += extension
300 if gpodder.interface == gpodder.GUI:
301 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
302 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
303 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
304 elif gpodder.interface == gpodder.MAEMO:
305 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
307 dlg.set_do_overwrite_confirmation( True)
308 dlg.set_current_name( os.path.basename( dst_filename))
309 dlg.set_current_folder( dst_directory)
311 result = False
312 folder = dst_directory
313 if dlg.run() == gtk.RESPONSE_OK:
314 result = True
315 dst_filename = dlg.get_filename()
316 folder = dlg.get_current_folder()
317 if not dst_filename.endswith( extension):
318 dst_filename += extension
320 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
322 try:
323 shutil.copyfile( src_filename, dst_filename)
324 except:
325 log( 'Error copying file.', sender = self, traceback = True)
327 dlg.destroy()
328 return (result, folder)
331 class gPodder(GladeWidget):
332 finger_friendly_widgets = ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
333 ENTER_URL_TEXT = _('Enter podcast URL...')
335 def new(self):
336 if gpodder.interface == gpodder.MAEMO:
337 # Maemo-specific changes to the UI
338 global scalable_dir
339 scalable_dir = scalable_dir.replace('.svg', '.png')
341 self.app = hildon.Program()
342 gtk.set_application_name('gPodder')
343 self.window = hildon.Window()
344 self.window.connect('delete-event', self.on_gPodder_delete_event)
345 self.window.connect('window-state-event', self.window_state_event)
347 self.itemUpdateChannel.show()
348 self.UpdateChannelSeparator.show()
350 # Give toolbar to the hildon window
351 self.toolbar.parent.remove(self.toolbar)
352 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
353 self.window.add_toolbar(self.toolbar)
355 self.app.add_window(self.window)
356 self.vMain.reparent(self.window)
357 self.gPodder = self.window
359 # Reparent the main menu
360 menu = gtk.Menu()
361 for child in self.mainMenu.get_children():
362 child.reparent(menu)
363 self.itemQuit.reparent(menu)
364 self.window.set_menu(menu)
366 self.mainMenu.destroy()
367 self.window.show()
369 # do some widget hiding
370 self.toolbar.remove(self.toolTransfer)
371 self.itemTransferSelected.hide_all()
372 self.item_email_subscriptions.hide_all()
374 # Feed cache update button
375 self.label120.set_text(_('Update'))
377 # get screen real estate
378 self.hboxContainer.set_border_width(0)
380 self.gPodder.connect('key-press-event', self.on_key_press)
381 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
383 if gl.config.show_url_entry_in_podcast_list:
384 self.hboxAddChannel.show()
386 if not gl.config.show_toolbar:
387 self.toolbar.hide_all()
389 gl.config.add_observer(self.on_config_changed)
390 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
391 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
392 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
393 self.entry_add_channel_unfocus(self.entryAddChannel, None)
395 self.uar = None
396 self.tray_icon = None
398 self.fullscreen = False
399 self.minimized = False
400 self.gPodder.connect('window-state-event', self.window_state_event)
402 self.already_notified_new_episodes = []
403 self.show_hide_tray_icon()
404 self.no_episode_selected.set_sensitive(False)
406 self.itemShowToolbar.set_active(gl.config.show_toolbar)
407 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
409 gl.config.connect_gtk_window(self.gPodder, 'main_window')
410 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
412 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
413 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
414 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
415 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
417 # Make sure we free/close the download queue when we
418 # update the "max downloads" spin button
419 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
420 self.spinMaxDownloads.connect('value-changed', changed_cb)
422 self.default_title = None
423 if app_version.rfind('git') != -1:
424 self.set_title('gPodder %s' % app_version)
425 else:
426 title = self.gPodder.get_title()
427 if title is not None:
428 self.set_title(title)
429 else:
430 self.set_title(_('gPodder'))
432 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
434 # cell renderers for channel tree
435 iconcolumn = gtk.TreeViewColumn('')
437 iconcell = gtk.CellRendererPixbuf()
438 iconcolumn.pack_start( iconcell, False)
439 iconcolumn.add_attribute( iconcell, 'pixbuf', 5)
440 self.cell_channel_icon = iconcell
442 namecolumn = gtk.TreeViewColumn('')
443 namecell = gtk.CellRendererText()
444 namecell.set_property('foreground-set', True)
445 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
446 namecolumn.pack_start( namecell, True)
447 namecolumn.add_attribute( namecell, 'markup', 2)
448 namecolumn.add_attribute( namecell, 'foreground', 8)
450 iconcell = gtk.CellRendererPixbuf()
451 iconcell.set_property('xalign', 1.0)
452 namecolumn.pack_start( iconcell, False)
453 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
454 namecolumn.add_attribute(iconcell, 'visible', 7)
455 self.cell_channel_pill = iconcell
457 self.treeChannels.append_column(iconcolumn)
458 self.treeChannels.append_column(namecolumn)
459 self.treeChannels.set_headers_visible(False)
461 # enable alternating colors hint
462 self.treeAvailable.set_rules_hint( True)
463 self.treeChannels.set_rules_hint( True)
465 # connect to tooltip signals
466 try:
467 self.treeChannels.set_property('has-tooltip', True)
468 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
469 self.treeAvailable.set_property('has-tooltip', True)
470 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
471 except:
472 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
473 self.last_tooltip_channel = None
474 self.last_tooltip_episode = None
475 self.podcast_list_can_tooltip = True
476 self.episode_list_can_tooltip = True
478 # Add our context menu to treeAvailable
479 if gpodder.interface == gpodder.MAEMO:
480 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
481 else:
482 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
483 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
485 iconcell = gtk.CellRendererPixbuf()
486 if gpodder.interface == gpodder.MAEMO:
487 status_column_label = ''
488 else:
489 status_column_label = _('Status')
490 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
492 namecell = gtk.CellRendererText()
493 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
494 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
495 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
496 namecolumn.set_expand(True)
498 sizecell = gtk.CellRendererText()
499 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
501 releasecell = gtk.CellRendererText()
502 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
504 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
505 itemcolumn.set_reorderable(True)
506 self.treeAvailable.append_column(itemcolumn)
508 if gpodder.interface == gpodder.MAEMO:
509 # Due to screen space contraints, we
510 # hide these columns here by default
511 self.column_size = sizecolumn
512 self.column_released = releasecolumn
513 self.column_released.set_visible(False)
514 self.column_size.set_visible(False)
516 # enable search in treeavailable
517 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
519 # enable multiple selection support
520 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
521 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
523 # columns and renderers for "download progress" tab
524 episodecell = gtk.CellRendererText()
525 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
526 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
527 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
528 episodecolumn.set_expand(True)
530 speedcell = gtk.CellRendererText()
531 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
533 progresscell = gtk.CellRendererProgress()
534 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
535 progresscolumn.set_expand(True)
537 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
538 self.treeDownloads.append_column( itemcolumn)
540 # After we've set up most of the window, show it :)
541 if not gpodder.interface == gpodder.MAEMO:
542 self.gPodder.show()
544 if self.tray_icon:
545 if gl.config.start_iconified:
546 self.iconify_main_window()
547 elif gl.config.minimize_to_tray:
548 self.tray_icon.set_visible(False)
550 services.download_status_manager.register( 'list-changed', self.download_status_updated)
551 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
552 services.cover_downloader.register('cover-available', self.cover_download_finished)
553 services.cover_downloader.register('cover-removed', self.cover_file_removed)
554 self.cover_cache = {}
556 self.treeDownloads.set_model( services.download_status_manager.tree_model)
558 #Add Drag and Drop Support
559 flags = gtk.DEST_DEFAULT_ALL
560 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
561 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
562 self.treeChannels.drag_dest_set( flags, targets, actions)
563 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
565 # Subscribed channels
566 self.active_channel = None
567 self.channels = load_channels()
568 self.update_podcasts_tab()
570 # load list of user applications for audio playback
571 self.user_apps_reader = UserAppsReader(['audio', 'video'])
572 Thread(target=self.read_apps).start()
574 # Clean up old, orphaned download files
575 gl.clean_up_downloads( delete_partial = True)
577 # Set the "Device" menu item for the first time
578 self.update_item_device()
580 # Last folder used for saving episodes
581 self.folder_for_saving_episodes = None
583 # Set up default channel colors
584 self.channel_colors = {
585 'default': None,
586 'updating': gl.config.color_updating_feeds,
587 'parse_error': '#ff0000',
590 # Now, update the feed cache, when everything's in place
591 self.btnUpdateFeeds.show_all()
592 self.updated_feeds = 0
593 self.updating_feed_cache = False
594 self.feed_cache_update_cancelled = False
595 self.update_feed_cache(force_update=gl.config.update_on_startup)
597 # Start the auto-update procedure
598 self.auto_update_procedure(first_run=True)
600 # Delete old episodes if the user wishes to
601 if gl.config.auto_remove_old_episodes:
602 old_episodes = self.get_old_episodes()
603 if len(old_episodes) > 0:
604 self.delete_episode_list(old_episodes, confirm=False)
605 self.updateComboBox()
607 # First-time users should be asked if they want to see the OPML
608 if len(self.channels) == 0:
609 util.idle_add(self.on_itemUpdate_activate, None)
611 def on_tree_channels_resize(self, widget, allocation):
612 if not gl.config.podcast_sidebar_save_space:
613 return
615 window_allocation = self.gPodder.get_allocation()
616 percentage = 100. * float(allocation.width) / float(window_allocation.width)
617 if hasattr(self, 'cell_channel_icon'):
618 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
619 if hasattr(self, 'cell_channel_pill'):
620 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
622 def entry_add_channel_focus(self, widget, event):
623 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
624 if widget.get_text() == self.ENTER_URL_TEXT:
625 widget.set_text('')
627 def entry_add_channel_unfocus(self, widget, event):
628 if widget.get_text() == '':
629 widget.set_text(self.ENTER_URL_TEXT)
630 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
632 def on_config_changed(self, name, old_value, new_value):
633 if name == 'show_toolbar':
634 if new_value:
635 self.toolbar.show_all()
636 else:
637 self.toolbar.hide_all()
638 elif name == 'episode_list_descriptions':
639 self.updateTreeView()
640 elif name == 'show_url_entry_in_podcast_list':
641 if new_value:
642 self.hboxAddChannel.show()
643 else:
644 self.hboxAddChannel.hide()
646 def read_apps(self):
647 time.sleep(3) # give other parts of gpodder a chance to start up
648 self.user_apps_reader.read()
649 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
650 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
652 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
653 # With get_bin_window, we get the window that contains the rows without
654 # the header. The Y coordinate of this window will be the height of the
655 # treeview header. This is the amount we have to subtract from the
656 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
657 (x_bin, y_bin) = treeview.get_bin_window().get_position()
658 y -= x_bin
659 y -= y_bin
660 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
662 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
663 self.last_tooltip_episode = None
664 return False
666 if path is not None:
667 model = treeview.get_model()
668 iter = model.get_iter(path)
669 url = model.get_value(iter, 0)
670 description = model.get_value(iter, 7)
671 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
672 self.last_tooltip_episode = None
673 return False
674 self.last_tooltip_episode = url
676 if len(description) > 400:
677 description = description[:398]+'[...]'
679 tooltip.set_text(description)
680 return True
682 self.last_tooltip_episode = None
683 return False
685 def podcast_list_allow_tooltips(self):
686 self.podcast_list_can_tooltip = True
688 def episode_list_allow_tooltips(self):
689 self.episode_list_can_tooltip = True
691 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
692 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
694 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
695 self.last_tooltip_channel = None
696 return False
698 if path is not None:
699 model = treeview.get_model()
700 iter = model.get_iter(path)
701 url = model.get_value(iter, 0)
702 for channel in self.channels:
703 if channel.url == url:
704 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
705 self.last_tooltip_channel = None
706 return False
707 self.last_tooltip_channel = channel
708 channel.request_save_dir_size()
709 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
710 error_str = model.get_value(iter, 6)
711 if error_str:
712 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
713 error_str = '<span foreground="#ff0000">%s</span>' % error_str
714 table = gtk.Table(rows=3, columns=3)
715 table.set_row_spacings(5)
716 table.set_col_spacings(5)
717 table.set_border_width(5)
719 heading = gtk.Label()
720 heading.set_alignment(0, 1)
721 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
722 table.attach(heading, 0, 1, 0, 1)
723 size_info = gtk.Label()
724 size_info.set_alignment(1, 1)
725 size_info.set_justify(gtk.JUSTIFY_RIGHT)
726 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
727 table.attach(size_info, 2, 3, 0, 1)
729 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
731 if len(channel.description) < 500:
732 description = channel.description
733 else:
734 pos = channel.description.find('\n\n')
735 if pos == -1 or pos > 500:
736 description = channel.description[:498]+'[...]'
737 else:
738 description = channel.description[:pos]
740 description = gtk.Label(description)
741 if error_str:
742 description.set_markup(error_str)
743 description.set_alignment(0, 0)
744 description.set_line_wrap(True)
745 table.attach(description, 0, 3, 2, 3)
747 table.show_all()
748 tooltip.set_custom(table)
750 return True
752 self.last_tooltip_channel = None
753 return False
755 def update_m3u_playlist_clicked(self, widget):
756 self.active_channel.update_m3u_playlist()
757 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
759 def treeview_channels_button_pressed( self, treeview, event):
760 global WEB_BROWSER_ICON
762 if event.button == 3:
763 ( x, y ) = ( int(event.x), int(event.y) )
764 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
766 paths = []
768 # Did the user right-click into a selection?
769 selection = treeview.get_selection()
770 if selection.count_selected_rows() and path:
771 ( model, paths ) = selection.get_selected_rows()
772 if path not in paths:
773 # We have right-clicked, but not into the
774 # selection, assume we don't want to operate
775 # on the selection
776 paths = []
778 # No selection or right click not in selection:
779 # Select the single item where we clicked
780 if not len( paths) and path:
781 treeview.grab_focus()
782 treeview.set_cursor( path, column, 0)
784 ( model, paths ) = ( treeview.get_model(), [ path ] )
786 # We did not find a selection, and the user didn't
787 # click on an item to select -- don't show the menu
788 if not len( paths):
789 return True
791 menu = gtk.Menu()
793 item = gtk.ImageMenuItem( _('Open download folder'))
794 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
795 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
796 menu.append( item)
798 item = gtk.ImageMenuItem( _('Update Feed'))
799 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
800 item.connect('activate', self.on_itemUpdateChannel_activate )
801 item.set_sensitive( not self.updating_feed_cache )
802 menu.append( item)
804 if gl.config.create_m3u_playlists:
805 item = gtk.ImageMenuItem(_('Update M3U playlist'))
806 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
807 item.connect('activate', self.update_m3u_playlist_clicked)
808 menu.append(item)
810 if self.active_channel.link:
811 item = gtk.ImageMenuItem(_('Visit website'))
812 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
813 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
814 menu.append(item)
816 if self.active_channel.channel_is_locked:
817 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
818 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
819 item.connect('activate', self.on_channel_toggle_lock_activate)
820 menu.append(self.set_finger_friendly(item))
821 else:
822 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
823 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
824 item.connect('activate', self.on_channel_toggle_lock_activate)
825 menu.append(self.set_finger_friendly(item))
828 menu.append( gtk.SeparatorMenuItem())
830 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
831 item.connect( 'activate', self.on_itemEditChannel_activate)
832 menu.append( item)
834 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
835 item.connect( 'activate', self.on_itemRemoveChannel_activate)
836 menu.append( item)
838 menu.show_all()
839 # Disable tooltips while we are showing the menu, so
840 # the tooltip will not appear over the menu
841 self.podcast_list_can_tooltip = False
842 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
843 menu.popup( None, None, None, event.button, event.time)
845 return True
847 def on_itemClose_activate(self, widget):
848 if self.tray_icon is not None:
849 if gpodder.interface == gpodder.MAEMO:
850 self.gPodder.set_property('visible', False)
851 else:
852 self.iconify_main_window()
853 else:
854 self.on_gPodder_delete_event(widget)
856 def cover_file_removed(self, channel_url):
858 The Cover Downloader calls this when a previously-
859 available cover has been removed from the disk. We
860 have to update our cache to reflect this change.
862 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
863 for row in self.treeChannels.get_model():
864 if row[COLUMN_URL] == channel_url:
865 row[COLUMN_PIXBUF] = None
866 key = (channel_url, gl.config.podcast_list_icon_size, \
867 gl.config.podcast_list_icon_size)
868 if key in self.cover_cache:
869 del self.cover_cache[key]
872 def cover_download_finished(self, channel_url, pixbuf):
874 The Cover Downloader calls this when it has finished
875 downloading (or registering, if already downloaded)
876 a new channel cover, which is ready for displaying.
878 if pixbuf is not None:
879 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
880 for row in self.treeChannels.get_model():
881 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
882 new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size, channel_url, self.cover_cache)
883 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
885 def save_episode_as_file( self, url, *args):
886 episode = self.active_channel.find_episode(url)
888 folder = self.folder_for_saving_episodes
889 (result, folder) = self.show_copy_dialog(src_filename=episode.local_filename(), dst_filename=episode.sync_filename(), dst_directory=folder)
890 self.folder_for_saving_episodes = folder
892 def copy_episode_bluetooth(self, url, *args):
893 episode = self.active_channel.find_episode(url)
894 filename = episode.local_filename()
896 if gl.config.bluetooth_use_device_address:
897 device = gl.config.bluetooth_device_address
898 else:
899 device = None
901 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
902 (base, ext) = os.path.splitext(filename)
903 if not destfile.endswith(ext):
904 destfile += ext
906 if gl.config.bluetooth_use_converter:
907 title = _('Converting file')
908 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
909 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
910 dlg.set_title(title)
911 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
912 dlg.show_all()
913 else:
914 dlg = None
916 def convert_and_send_thread(filename, destfile, device, dialog, notify):
917 if gl.config.bluetooth_use_converter:
918 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
919 result = p.wait()
920 if dialog is not None:
921 dialog.destroy()
922 else:
923 try:
924 shutil.copyfile(filename, destfile)
925 result = 0
926 except:
927 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
928 result = 1
930 if result == 0 or not os.path.exists(destfile):
931 util.bluetooth_send_file(destfile, device)
932 else:
933 notify(_('Error converting file.'), _('Bluetooth file transfer'))
934 util.delete_file(destfile)
936 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
938 def treeview_button_pressed( self, treeview, event):
939 global WEB_BROWSER_ICON
941 # Use right-click for the Desktop version and left-click for Maemo
942 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
943 (event.button == 3 and gpodder.interface == gpodder.GUI):
944 ( x, y ) = ( int(event.x), int(event.y) )
945 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
947 paths = []
949 # Did the user right-click into a selection?
950 selection = self.treeAvailable.get_selection()
951 if selection.count_selected_rows() and path:
952 ( model, paths ) = selection.get_selected_rows()
953 if path not in paths:
954 # We have right-clicked, but not into the
955 # selection, assume we don't want to operate
956 # on the selection
957 paths = []
959 # No selection or right click not in selection:
960 # Select the single item where we clicked
961 if not len( paths) and path:
962 treeview.grab_focus()
963 treeview.set_cursor( path, column, 0)
965 ( model, paths ) = ( treeview.get_model(), [ path ] )
967 # We did not find a selection, and the user didn't
968 # click on an item to select -- don't show the menu
969 if not len( paths):
970 return True
972 first_url = model.get_value( model.get_iter( paths[0]), 0)
973 episode = db.load_episode(first_url)
975 menu = gtk.Menu()
977 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
979 if can_play:
980 if open_instead_of_play:
981 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
982 else:
983 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
984 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
985 menu.append(self.set_finger_friendly(item))
987 if not episode['is_locked'] and can_delete:
988 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
989 item.connect('activate', self.on_btnDownloadedDelete_clicked)
990 menu.append(self.set_finger_friendly(item))
992 if can_cancel:
993 item = gtk.ImageMenuItem( _('Cancel download'))
994 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
995 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
996 menu.append(self.set_finger_friendly(item))
998 if can_download:
999 item = gtk.ImageMenuItem(_('Download'))
1000 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1001 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
1002 menu.append(self.set_finger_friendly(item))
1004 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
1005 item = gtk.ImageMenuItem(_('Do not download'))
1006 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1007 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1008 menu.append(self.set_finger_friendly(item))
1009 elif episode['state'] == db.STATE_NORMAL and can_download:
1010 item = gtk.ImageMenuItem(_('Mark as new'))
1011 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1012 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1013 menu.append(self.set_finger_friendly(item))
1015 if can_play and not can_download:
1016 menu.append( gtk.SeparatorMenuItem())
1017 item = gtk.ImageMenuItem(_('Save to disk'))
1018 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1019 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
1020 menu.append(self.set_finger_friendly(item))
1021 if gl.bluetooth_available:
1022 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1023 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1024 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
1025 menu.append(self.set_finger_friendly(item))
1026 if can_transfer:
1027 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
1028 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1029 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
1030 menu.append(self.set_finger_friendly(item))
1032 if can_play:
1033 menu.append( gtk.SeparatorMenuItem())
1034 is_played = episode['is_played']
1035 if is_played:
1036 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1037 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1038 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1039 menu.append(self.set_finger_friendly(item))
1040 else:
1041 item = gtk.ImageMenuItem(_('Mark as played'))
1042 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1043 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1044 menu.append(self.set_finger_friendly(item))
1046 is_locked = episode['is_locked']
1047 if is_locked:
1048 item = gtk.ImageMenuItem(_('Allow deletion'))
1049 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1050 item.connect('activate', self.on_item_toggle_lock_activate)
1051 menu.append(self.set_finger_friendly(item))
1052 else:
1053 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1054 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1055 item.connect('activate', self.on_item_toggle_lock_activate)
1056 menu.append(self.set_finger_friendly(item))
1058 if len(paths) == 1:
1059 menu.append(gtk.SeparatorMenuItem())
1060 # Single item, add episode information menu item
1061 episode_url = model.get_value( model.get_iter( paths[0]), 0)
1062 item = gtk.ImageMenuItem(_('Episode details'))
1063 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1064 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1065 menu.append(self.set_finger_friendly(item))
1066 episode = self.active_channel.find_episode(episode_url)
1067 # If we have it, also add episode website link
1068 if episode and episode.link and episode.link != episode.url:
1069 item = gtk.ImageMenuItem(_('Visit website'))
1070 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1071 item.connect('activate', lambda w: util.open_website(episode.link))
1072 menu.append(self.set_finger_friendly(item))
1074 if gpodder.interface == gpodder.MAEMO:
1075 # Because we open the popup on left-click for Maemo,
1076 # we also include a non-action to close the menu
1077 menu.append(gtk.SeparatorMenuItem())
1078 item = gtk.ImageMenuItem(_('Close this menu'))
1079 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1080 menu.append(self.set_finger_friendly(item))
1082 menu.show_all()
1083 # Disable tooltips while we are showing the menu, so
1084 # the tooltip will not appear over the menu
1085 self.episode_list_can_tooltip = False
1086 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1087 menu.popup( None, None, None, event.button, event.time)
1089 return True
1091 def set_title(self, new_title):
1092 self.default_title = new_title
1093 self.gPodder.set_title(new_title)
1095 def download_progress_updated( self, count, percentage):
1096 title = [ self.default_title ]
1098 total_speed = gl.format_filesize(services.download_status_manager.total_speed())
1100 if count == 1:
1101 title.append( _('downloading one file'))
1102 elif count > 1:
1103 title.append( _('downloading %d files') % count)
1105 if len(title) == 2:
1106 title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
1108 self.gPodder.set_title( ' - '.join( title))
1110 # Have all the downloads completed?
1111 # If so execute user command if defined, else do nothing
1112 if count == 0:
1113 if len(gl.config.cmd_all_downloads_complete) > 0:
1114 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1116 def playback_episode(self, episode, stream=False):
1117 (success, application) = gl.playback_episode(episode, stream)
1118 if not success:
1119 self.show_message( _('The selected player application cannot be found. Please check your media player settings in the preferences dialog.'), _('Error opening player: %s') % ( saxutils.escape( application), ))
1120 self.updateComboBox(only_selected_channel=True)
1122 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1123 if model is None:
1124 return True
1126 key = key.lower()
1128 # columns, as defined in libpodcasts' get model method
1129 # 1 = episode title, 7 = description
1130 columns = (1, 7)
1132 for column in columns:
1133 value = model.get_value( iter, column).lower()
1134 if value.find( key) != -1:
1135 return False
1137 return True
1139 def change_menu_item(self, menuitem, icon=None, label=None):
1140 if icon is not None:
1141 menuitem.get_image().set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1142 if label is not None:
1143 label_widget = menuitem.get_child()
1144 label_widget.set_text(label)
1146 def play_or_download(self):
1147 if self.wNotebook.get_current_page() > 0:
1148 return
1150 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1151 ( is_played, is_locked ) = (False,)*2
1153 open_instead_of_play = False
1155 selection = self.treeAvailable.get_selection()
1156 if selection.count_selected_rows() > 0:
1157 (model, paths) = selection.get_selected_rows()
1159 for path in paths:
1160 url = model.get_value( model.get_iter( path), 0)
1161 local_filename = model.get_value( model.get_iter( path), 8)
1163 episode = podcastItem.load(url, self.active_channel)
1165 if episode.file_type() not in ('audio', 'video'):
1166 open_instead_of_play = True
1168 if episode.was_downloaded():
1169 can_play = episode.was_downloaded(and_exists=True)
1170 can_delete = True
1171 is_played = episode.is_played
1172 is_locked = episode.is_locked
1173 if not can_play:
1174 can_download = True
1175 else:
1176 if services.download_status_manager.is_download_in_progress(url):
1177 can_cancel = True
1178 else:
1179 can_download = True
1181 can_download = can_download and not can_cancel
1182 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1183 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1185 if open_instead_of_play:
1186 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1187 can_transfer = False
1188 else:
1189 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1191 self.toolPlay.set_sensitive( can_play)
1192 self.toolDownload.set_sensitive( can_download)
1193 self.toolTransfer.set_sensitive( can_transfer)
1194 self.toolCancel.set_sensitive( can_cancel)
1196 if can_cancel:
1197 self.item_cancel_download.show_all()
1198 else:
1199 self.item_cancel_download.hide_all()
1200 if can_download:
1201 self.itemDownloadSelected.show_all()
1202 else:
1203 self.itemDownloadSelected.hide_all()
1204 if can_play:
1205 if open_instead_of_play:
1206 self.itemOpenSelected.show_all()
1207 self.itemPlaySelected.hide_all()
1208 else:
1209 self.itemPlaySelected.show_all()
1210 self.itemOpenSelected.hide_all()
1211 if not can_download:
1212 self.itemDeleteSelected.show_all()
1213 else:
1214 self.itemDeleteSelected.hide_all()
1215 self.item_toggle_played.show_all()
1216 self.item_toggle_lock.show_all()
1217 self.separator9.show_all()
1218 if is_played:
1219 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1220 else:
1221 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1222 if is_locked:
1223 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1224 else:
1225 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1226 else:
1227 self.itemPlaySelected.hide_all()
1228 self.itemOpenSelected.hide_all()
1229 self.itemDeleteSelected.hide_all()
1230 self.item_toggle_played.hide_all()
1231 self.item_toggle_lock.hide_all()
1232 self.separator9.hide_all()
1233 if can_play or can_download or can_cancel:
1234 self.item_episode_details.show_all()
1235 self.separator16.show_all()
1236 self.no_episode_selected.hide_all()
1237 else:
1238 self.item_episode_details.hide_all()
1239 self.separator16.hide_all()
1240 self.no_episode_selected.show_all()
1242 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1244 def download_status_updated( self):
1245 count = services.download_status_manager.count()
1246 if count:
1247 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1248 else:
1249 self.labelDownloads.set_text( _('Downloads'))
1251 self.updateComboBox()
1253 def on_cbMaxDownloads_toggled(self, widget, *args):
1254 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1256 def on_cbLimitDownloads_toggled(self, widget, *args):
1257 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1259 def episode_new_status_changed(self):
1260 self.updateComboBox()
1261 self.updateTreeView()
1263 def updateComboBox(self, selected_url=None, only_selected_channel=False):
1264 (model, iter) = self.treeChannels.get_selection().get_selected()
1266 if only_selected_channel:
1267 if iter and self.active_channel is not None:
1268 update_channel_model_by_iter( self.treeChannels.get_model(),
1269 iter, self.active_channel, self.channel_colors,
1270 self.cover_cache, *(gl.config.podcast_list_icon_size,)*2 )
1271 else:
1272 if model and iter and selected_url is None:
1273 # Get the URL of the currently-selected podcast
1274 selected_url = model.get_value(iter, 0)
1276 rect = self.treeChannels.get_visible_rect()
1277 self.treeChannels.set_model( channels_to_model( self.channels,
1278 self.channel_colors, self.cover_cache,
1279 *(gl.config.podcast_list_icon_size,)*2 ))
1280 util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
1282 try:
1283 selected_path = (0,)
1284 # Find the previously-selected URL in the new
1285 # model if we have an URL (else select first)
1286 if selected_url is not None:
1287 model = self.treeChannels.get_model()
1288 pos = model.get_iter_first()
1289 while pos is not None:
1290 url = model.get_value(pos, 0)
1291 if url == selected_url:
1292 selected_path = model.get_path(pos)
1293 break
1294 pos = model.iter_next(pos)
1296 self.treeChannels.get_selection().select_path(selected_path)
1297 except:
1298 log( 'Cannot set selection on treeChannels', sender = self)
1299 self.on_treeChannels_cursor_changed( self.treeChannels)
1301 def updateTreeView(self, retain_position=True):
1302 if self.channels and self.active_channel is not None:
1303 rect = self.treeAvailable.get_visible_rect()
1304 self.treeAvailable.set_model(self.active_channel.tree_model)
1305 if retain_position:
1306 util.idle_add(self.treeAvailable.scroll_to_point, rect.x, rect.y)
1307 self.treeAvailable.columns_autosize()
1308 self.play_or_download()
1309 else:
1310 if self.treeAvailable.get_model():
1311 self.treeAvailable.get_model().clear()
1313 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1314 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1316 dnd_channel = None
1317 if path is not None:
1318 model = self.treeChannels.get_model()
1319 iter = model.get_iter(path)
1320 url = model.get_value(iter, 0)
1321 for channel in self.channels:
1322 if channel.url == url:
1323 dnd_channel = channel
1324 break
1326 result = sel.data
1327 rl = result.strip().lower()
1328 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1329 services.cover_downloader.replace_cover(dnd_channel, result)
1330 else:
1331 self.add_new_channel(result)
1333 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1334 result = util.normalize_feed_url( result)
1336 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1337 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1338 waitdlg.set_title(_('Downloading episode list'))
1339 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1340 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1341 waitpb = gtk.ProgressBar()
1342 if block:
1343 waitdlg.vbox.add(waitpb)
1344 waitdlg.show_all()
1345 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1347 if not result:
1348 title = _('URL scheme not supported')
1349 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
1350 self.show_message( message, title)
1351 return
1353 for old_channel in self.channels:
1354 if old_channel.url == result:
1355 log( 'Channel already exists: %s', result)
1356 # Select the existing channel in combo box
1357 for i in range( len( self.channels)):
1358 if self.channels[i] == old_channel:
1359 self.treeChannels.get_selection().select_path( (i,))
1360 self.on_treeChannels_cursor_changed(self.treeChannels)
1361 break
1362 self.show_message( _('You have already subscribed to this podcast: %s') % (
1363 saxutils.escape( old_channel.title), ), _('Already added'))
1364 waitdlg.destroy()
1365 return
1367 self.entryAddChannel.set_text(_('Downloading feed...'))
1368 self.entryAddChannel.set_sensitive(False)
1369 self.btnAddChannel.set_sensitive(False)
1370 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1371 thread = Thread( target=self.add_new_channel_proc, args=args )
1372 thread.start()
1374 while block and thread.isAlive():
1375 while gtk.events_pending():
1376 gtk.main_iteration( False)
1377 waitpb.pulse()
1378 time.sleep(0.05)
1381 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1382 log( 'Adding new channel: %s', url)
1383 channel = error = None
1384 try:
1385 channel = podcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1386 except HTTPAuthError, e:
1387 error = e
1388 except Exception, e:
1389 log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1391 util.idle_add( callback, channel, url, error, *callback_args )
1393 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1394 if channel is not None:
1395 self.channels.append( channel)
1396 save_channels( self.channels)
1397 if not quiet:
1398 # download changed channels and select the new episode in the UI afterwards
1399 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1401 (username, password) = util.username_password_from_url( url)
1402 if username and self.show_confirmation( _('You have supplied <b>%s</b> as username and a password for this feed. Would you like to use the same authentication data for downloading episodes?') % ( saxutils.escape( username), ), _('Password authentication')):
1403 channel.username = username
1404 channel.password = password
1405 log('Saving authentication data for episode downloads..', sender = self)
1406 channel.save()
1407 # We need to update the channel list otherwise the authentication
1408 # data won't show up in the channel editor.
1409 # TODO: Only updated the newly added feed to save some cpu cycles
1410 self.channels = load_channels()
1412 if ask_download_new:
1413 new_episodes = channel.get_new_episodes()
1414 if len(new_episodes):
1415 self.new_episodes_show(new_episodes)
1417 elif isinstance( error, HTTPAuthError ):
1418 response, auth_tokens = self.UsernamePasswordDialog(
1419 _('Feed requires authentication'), _('Please enter your username and password.'))
1421 if response:
1422 self.add_new_channel( url, authentication_tokens=auth_tokens )
1424 else:
1425 # Ok, the URL is not a channel, or there is some other
1426 # error - let's see if it's a web page or OPML file...
1427 try:
1428 data = urllib2.urlopen(url).read().lower()
1429 if '</opml>' in data:
1430 # This looks like an OPML feed
1431 self.on_item_import_from_file_activate(None, url)
1433 elif '</html>' in data:
1434 # This looks like a web page
1435 title = _('The URL is a website')
1436 message = _('The URL you specified points to a web page. You need to find the "feed" URL of the podcast to add to gPodder. Do you want to visit this website now and look for the podcast feed URL?\n\n(Hint: Look for "XML feed", "RSS feed" or "Podcast feed" if you are unsure for what to look. If there is only an iTunes URL, try adding this one.)')
1437 if self.show_confirmation(message, title):
1438 util.open_website(url)
1440 except Exception, e:
1441 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1443 title = _('Error adding podcast')
1444 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1445 self.show_message( message, title)
1447 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
1448 self.entryAddChannel.set_sensitive(True)
1449 self.btnAddChannel.set_sensitive(True)
1450 self.update_podcasts_tab()
1451 waitdlg.destroy()
1454 def update_feed_cache_finish_callback(self, channels=None,
1455 notify_no_new_episodes=False, select_url_afterwards=None):
1457 db.commit()
1459 self.updating_feed_cache = False
1460 self.hboxUpdateFeeds.hide_all()
1461 self.btnUpdateFeeds.show_all()
1462 self.itemUpdate.set_sensitive(True)
1463 self.itemUpdateChannel.set_sensitive(True)
1465 # If we want to select a specific podcast (via its URL)
1466 # after the update, we give it to updateComboBox here to
1467 # select exactly this podcast after updating the view
1468 self.updateComboBox(selected_url=select_url_afterwards)
1470 if self.tray_icon:
1471 self.tray_icon.set_status(None)
1472 if self.minimized:
1473 new_episodes = []
1474 # look for new episodes to notify
1475 for channel in self.channels:
1476 for episode in channel.get_new_episodes():
1477 if not episode in self.already_notified_new_episodes:
1478 new_episodes.append(episode)
1479 self.already_notified_new_episodes.append(episode)
1480 # notify new episodes
1482 if len(new_episodes) == 0:
1483 if notify_no_new_episodes and self.tray_icon is not None:
1484 msg = _('No new episodes available for download')
1485 self.tray_icon.send_notification(msg)
1486 return
1487 elif len(new_episodes) == 1:
1488 title = _('gPodder has found %s') % (_('one new episode:'),)
1489 else:
1490 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1491 message = self.tray_icon.format_episode_list(new_episodes)
1493 #auto download new episodes
1494 if gl.config.auto_download_when_minimized:
1495 message += '\n<i>(%s...)</i>' % _('downloading')
1496 self.download_episode_list(new_episodes)
1497 self.tray_icon.send_notification(message, title)
1498 return
1500 # open the episodes selection dialog
1501 self.channels = load_channels()
1502 self.updateComboBox()
1503 if not self.feed_cache_update_cancelled:
1504 self.download_all_new(channels=channels)
1506 def update_feed_cache_callback(self, progressbar, title, position, count):
1507 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
1508 progressbar.set_text(progression)
1509 if self.tray_icon:
1510 self.tray_icon.set_status(
1511 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
1512 if count > 0:
1513 progressbar.set_fraction(float(position)/float(count))
1515 def update_feed_cache_proc( self, channel, total_channels, semaphore,
1516 callback_proc, finish_proc):
1518 semaphore.acquire()
1519 if not self.feed_cache_update_cancelled:
1520 try:
1521 channel.update()
1522 except:
1523 log('Darn SQLite LOCK!', sender=self, traceback=True)
1525 # By the time we get here the update may have already been cancelled
1526 if not self.feed_cache_update_cancelled:
1527 callback_proc(channel.title, self.updated_feeds, total_channels)
1529 self.updated_feeds += 1
1530 self.treeview_channel_set_color( channel, 'default' )
1531 channel.update_flag = False
1533 semaphore.release()
1534 if self.updated_feeds == total_channels:
1535 finish_proc()
1537 def on_btnCancelFeedUpdate_clicked(self, widget):
1538 self.pbFeedUpdate.set_text(_('Cancelling...'))
1539 self.feed_cache_update_cancelled = True
1541 def update_feed_cache(self, channels=None, force_update=True,
1542 notify_no_new_episodes=False, select_url_afterwards=None):
1544 if self.updating_feed_cache:
1545 return
1547 if not force_update:
1548 self.channels = load_channels()
1549 self.updateComboBox()
1550 return
1552 self.updating_feed_cache = True
1553 self.itemUpdate.set_sensitive(False)
1554 self.itemUpdateChannel.set_sensitive(False)
1556 if self.tray_icon:
1557 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1559 if channels is None:
1560 channels = self.channels
1562 if len(channels) == 1:
1563 text = _('Updating %d feed.')
1564 else:
1565 text = _('Updating %d feeds.')
1566 self.pbFeedUpdate.set_text( text % len(channels))
1567 self.pbFeedUpdate.set_fraction(0)
1569 # let's get down to business..
1570 callback_proc = lambda title, pos, count: util.idle_add(
1571 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
1572 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
1573 channels, notify_no_new_episodes, select_url_afterwards )
1575 self.updated_feeds = 0
1576 self.feed_cache_update_cancelled = False
1577 self.btnUpdateFeeds.hide_all()
1578 self.hboxUpdateFeeds.show_all()
1579 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
1581 for channel in channels:
1582 self.treeview_channel_set_color( channel, 'updating' )
1583 channel.update_flag = True
1584 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
1585 thread = Thread( target = self.update_feed_cache_proc, args = args)
1586 thread.start()
1588 def treeview_channel_set_color( self, channel, color ):
1589 if self.treeChannels.get_model():
1590 if color in self.channel_colors:
1591 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
1592 else:
1593 self.treeChannels.get_model().set(channel.iter, 8, color)
1595 def on_gPodder_delete_event(self, widget, *args):
1596 """Called when the GUI wants to close the window
1597 Displays a confirmation dialog (and closes/hides gPodder)
1600 downloading = services.download_status_manager.has_items()
1602 # Only iconify if we are using the window's "X" button,
1603 # but not when we are using "Quit" in the menu or toolbar
1604 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1605 self.iconify_main_window()
1606 elif gl.config.on_quit_ask or downloading:
1607 if gpodder.interface == gpodder.MAEMO:
1608 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1609 if result:
1610 self.close_gpodder()
1611 else:
1612 return True
1613 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1614 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1615 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1617 title = _('Quit gPodder')
1618 if downloading:
1619 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1620 else:
1621 message = _('Do you really want to quit gPodder now?')
1623 dialog.set_title(title)
1624 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1625 if not downloading:
1626 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1627 dialog.vbox.pack_start(cb_ask)
1628 cb_ask.show_all()
1630 result = dialog.run()
1631 dialog.destroy()
1633 if result == gtk.RESPONSE_CLOSE:
1634 if not downloading and cb_ask.get_active() == True:
1635 gl.config.on_quit_ask = False
1636 self.close_gpodder()
1637 else:
1638 self.close_gpodder()
1640 return True
1642 def close_gpodder(self):
1643 """ clean everything and exit properly
1645 if self.channels:
1646 if save_channels(self.channels):
1647 if gl.config.my_gpodder_autoupload:
1648 log('Uploading to my.gpodder.org on close', sender=self)
1649 util.idle_add(self.on_upload_to_mygpo, None)
1650 else:
1651 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1653 services.download_status_manager.cancel_all()
1654 self.gPodder.hide()
1655 while gtk.events_pending():
1656 gtk.main_iteration(False)
1658 db.close()
1660 self.gtk_main_quit()
1661 sys.exit( 0)
1663 def get_old_episodes(self):
1664 episodes = []
1665 for channel in self.channels:
1666 for episode in channel.get_downloaded_episodes():
1667 if episode.is_old() and not episode.is_locked and episode.is_played:
1668 episodes.append(episode)
1669 return episodes
1671 def for_each_selected_episode_url( self, callback):
1672 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1673 for path in paths:
1674 url = model.get_value( model.get_iter( path), 0)
1675 try:
1676 callback( url)
1677 except Exception, e:
1678 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1680 self.updateComboBox(only_selected_channel=True)
1682 def delete_episode_list( self, episodes, confirm = True):
1683 if len(episodes) == 0:
1684 return
1686 if len(episodes) == 1:
1687 message = _('Do you really want to delete this episode?')
1688 else:
1689 message = _('Do you really want to delete %d episodes?') % len(episodes)
1691 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1692 return
1694 for episode in episodes:
1695 log('Deleting episode: %s', episode.title, sender = self)
1696 episode.delete_from_disk()
1698 self.download_status_updated()
1700 def on_itemRemoveOldEpisodes_activate( self, widget):
1701 columns = (
1702 ('title_and_description', None, None, _('Episode')),
1703 ('channel_prop', None, None, _('Podcast')),
1704 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1705 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1706 ('played_prop', None, None, _('Status')),
1707 ('age_prop', None, None, _('Downloaded')),
1710 selection_buttons = {
1711 _('Select played'): lambda episode: episode.is_played,
1712 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1715 instructions = _('Select the episodes you want to delete from your hard disk.')
1717 episodes = []
1718 selected = []
1719 for channel in self.channels:
1720 for episode in channel.get_downloaded_episodes():
1721 if not episode.is_locked:
1722 episodes.append(episode)
1723 selected.append(episode.is_played)
1725 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1726 episodes = episodes, selected = selected, columns = columns, \
1727 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1728 selection_buttons = selection_buttons)
1730 def mark_selected_episodes_new(self):
1731 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1732 self.for_each_selected_episode_url(callback)
1734 def mark_selected_episodes_old(self):
1735 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1736 self.for_each_selected_episode_url(callback)
1738 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1739 if toggle:
1740 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1741 else:
1742 callback = lambda url: db.mark_episode(url, is_played=new_value)
1744 self.for_each_selected_episode_url(callback)
1746 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1747 if toggle:
1748 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1749 else:
1750 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1752 self.for_each_selected_episode_url(callback)
1754 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1756 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
1757 db.update_channel_lock(self.active_channel)
1759 if self.active_channel.channel_is_locked:
1760 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
1761 else:
1762 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
1764 for episode in self.active_channel.get_all_episodes():
1765 db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked)
1766 self.updateComboBox()
1769 def on_item_email_subscriptions_activate(self, widget):
1770 if not self.channels:
1771 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1772 elif not gl.send_subscriptions():
1773 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1775 def on_itemUpdateChannel_activate(self, widget=None):
1776 self.update_feed_cache(channels=[self.active_channel,])
1778 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1779 restore_from = can_restore_from_opml()
1781 if self.channels:
1782 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1783 elif restore_from is not None:
1784 title = _('Database upgrade required')
1785 message = _('gPodder is now using a new (much faster) database backend and needs to convert your current data. This can take some time. Start the conversion now?')
1786 if self.show_confirmation(message, title):
1787 add_callback = lambda url: self.add_new_channel(url, False, True)
1788 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1789 w.set_has_separator(False)
1790 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1791 w.set_default_size(500, -1)
1792 pb = gtk.ProgressBar()
1793 l = gtk.Label()
1794 l.set_padding(6, 3)
1795 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1796 l.set_alignment(0.0, 0.5)
1797 w.vbox.pack_start(l)
1798 l = gtk.Label()
1799 l.set_padding(6, 3)
1800 l.set_alignment(0.0, 0.5)
1801 l.set_text(_('Please wait while your settings are converted.'))
1802 w.vbox.pack_start(l)
1803 w.vbox.pack_start(pb)
1804 lb = gtk.Label()
1805 lb.set_ellipsize(pango.ELLIPSIZE_END)
1806 lb.set_alignment(0.0, 0.5)
1807 lb.set_padding(6, 6)
1808 w.vbox.pack_start(lb)
1810 def set_pb_status(pb, lb, fraction, text):
1811 pb.set_fraction(float(fraction)/100.0)
1812 pb.set_text('%.0f %%' % fraction)
1813 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1814 while gtk.events_pending():
1815 gtk.main_iteration(False)
1816 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1817 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1818 w.show_all()
1819 start = datetime.datetime.now()
1820 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1821 # Refresh the view with the updated episodes
1822 self.updateComboBox()
1823 time_taken = str(datetime.datetime.now()-start)
1824 status_callback(100.0, _('Migration finished in %s') % time_taken)
1825 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1826 w.run()
1827 w.destroy()
1828 else:
1829 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
1831 def download_episode_list( self, episodes):
1832 services.download_status_manager.start_batch_mode()
1833 for episode in episodes:
1834 log('Downloading episode: %s', episode.title, sender = self)
1835 filename = episode.local_filename()
1836 if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url):
1837 download.DownloadThread( episode.channel, episode, self.notification).start()
1838 services.download_status_manager.end_batch_mode()
1840 def new_episodes_show(self, episodes):
1841 columns = (
1842 ('title_and_description', None, None, _('Episode')),
1843 ('channel_prop', None, None, _('Podcast')),
1844 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1845 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1848 if len(episodes) > 0:
1849 instructions = _('Select the episodes you want to download now.')
1851 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1852 episodes=episodes, columns=columns, selected_default=True, \
1853 stock_ok_button = 'gpodder-download', \
1854 callback=self.download_episode_list, \
1855 remove_callback=lambda e: e.mark_old(), \
1856 remove_action=_('Never download'), \
1857 remove_finished=self.episode_new_status_changed)
1858 else:
1859 title = _('No new episodes')
1860 message = _('No new episodes to download.\nPlease check for new episodes later.')
1861 self.show_message(message, title)
1863 def on_itemDownloadAllNew_activate(self, widget, *args):
1864 self.download_all_new()
1866 def download_all_new(self, channels=None):
1867 if channels is None:
1868 channels = self.channels
1869 episodes = []
1870 for channel in channels:
1871 for episode in channel.get_new_episodes():
1872 episodes.append(episode)
1873 self.new_episodes_show(episodes)
1875 def get_all_episodes(self, exclude_nonsignificant=True ):
1876 """'exclude_nonsignificant' will exclude non-downloaded episodes
1877 and all episodes from channels that are set to skip when syncing"""
1878 episode_list = []
1879 for channel in self.channels:
1880 if not channel.sync_to_devices and exclude_nonsignificant:
1881 log('Skipping channel: %s', channel.title, sender=self)
1882 continue
1883 for episode in channel.get_all_episodes():
1884 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1885 episode_list.append(episode)
1886 return episode_list
1888 def ipod_delete_played(self, device):
1889 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1890 episodes_on_device = device.get_all_tracks()
1891 for local_episode in all_episodes:
1892 device_episode = device.episode_on_device(local_episode)
1893 if device_episode and ( local_episode.is_played and not local_episode.is_locked
1894 or local_episode.state == db.STATE_DELETED ):
1895 log("mp3_player_delete_played: removing %s" % device_episode.title)
1896 device.remove_track(device_episode)
1898 def on_sync_to_ipod_activate(self, widget, episodes=None):
1899 # make sure gpod is available before even trying to sync
1900 if gl.config.device_type == 'ipod' and not sync.gpod_available:
1901 title = _('Cannot Sync To iPod')
1902 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
1903 self.notification( message, title )
1904 return
1905 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
1906 title = _('Cannot sync to MTP device')
1907 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
1908 self.notification( message, title )
1909 return
1911 device = sync.open_device()
1912 device.register( 'post-done', self.sync_to_ipod_completed )
1914 if device is None:
1915 title = _('No device configured')
1916 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1917 self.notification(message, title)
1918 return
1920 if not device.open():
1921 title = _('Cannot open device')
1922 message = _('There has been an error opening your device.')
1923 self.notification(message, title)
1924 return
1926 if gl.config.ipod_purge_old_episodes:
1927 device.purge()
1929 sync_all_episodes = not bool(episodes)
1931 if episodes is None:
1932 episodes = self.get_all_episodes()
1934 # make sure we have enough space on the device
1935 total_size = 0
1936 free_space = device.get_free_space()
1937 for episode in episodes:
1938 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
1939 total_size += util.calculate_size(str(episode.local_filename()))
1941 if total_size > free_space:
1942 # can be negative because of the 10 MiB for reserved for the iTunesDB
1943 free_space = max( free_space, 0 )
1944 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
1945 title = _('Not enough space left on device.')
1946 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
1947 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
1948 self.notification(message, title)
1949 else:
1950 # start syncing!
1951 gPodderSync(device=device, gPodder=self)
1952 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
1953 if self.tray_icon:
1954 self.tray_icon.set_synchronisation_device(device)
1956 def sync_to_ipod_completed(self, device, successful_sync):
1957 device.unregister( 'post-done', self.sync_to_ipod_completed )
1959 if self.tray_icon:
1960 self.tray_icon.release_synchronisation_device()
1962 if not successful_sync:
1963 title = _('Error closing device')
1964 message = _('There has been an error closing your device.')
1965 self.notification(message, title)
1967 # update model for played state updates after sync
1968 util.idle_add(self.updateComboBox)
1970 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
1971 if sync_all_episodes:
1972 device.add_tracks(episodes)
1973 # 'only_sync_not_played' must be used or else all the played
1974 # tracks will be copied then immediately deleted
1975 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
1976 self.ipod_delete_played(device)
1977 else:
1978 device.add_tracks(episodes, force_played=True)
1979 device.close()
1981 def ipod_cleanup_callback(self, device, tracks):
1982 title = _('Delete podcasts from device?')
1983 message = _('The selected episodes will be removed from your device. This cannot be undone. Files in your gPodder library will be unaffected. Do you really want to delete these episodes from your device?')
1984 if len(tracks) > 0 and self.show_confirmation(message, title):
1985 device.remove_tracks(tracks)
1987 if not device.close():
1988 title = _('Error closing device')
1989 message = _('There has been an error closing your device.')
1990 self.show_message(message, title)
1991 return
1993 def on_cleanup_ipod_activate(self, widget, *args):
1994 columns = (
1995 ('title', None, None, _('Episode')),
1996 ('podcast', None, None, _('Podcast')),
1997 ('filesize', None, None, _('Size')),
1998 ('modified', None, None, _('Copied')),
1999 ('playcount', None, None, _('Play count')),
2000 ('released', None, None, _('Released')),
2003 device = sync.open_device()
2005 if device is None:
2006 title = _('No device configured')
2007 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2008 self.show_message(message, title)
2009 return
2011 if not device.open():
2012 title = _('Cannot open device')
2013 message = _('There has been an error opening your device.')
2014 self.show_message(message, title)
2015 return
2017 gPodderSync(device=device, gPodder=self)
2019 tracks = device.get_all_tracks()
2020 if len(tracks) > 0:
2021 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2022 wanted_columns = []
2023 for key, sort_name, sort_type, caption in columns:
2024 want_this_column = False
2025 for track in tracks:
2026 if getattr(track, key) is not None:
2027 want_this_column = True
2028 break
2030 if want_this_column:
2031 wanted_columns.append((key, sort_name, sort_type, caption))
2032 title = _('Remove podcasts from device')
2033 instructions = _('Select the podcast episodes you want to remove from your device.')
2034 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2035 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2036 else:
2037 title = _('No files on device')
2038 message = _('The devices contains no files to be removed.')
2039 self.show_message(message, title)
2040 device.close()
2042 def on_manage_device_playlist(self, widget):
2043 # make sure gpod is available before even trying to sync
2044 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2045 title = _('Cannot manage iPod playlist')
2046 message = _('This feature is not available for iPods.')
2047 self.notification( message, title )
2048 return
2049 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2050 title = _('Cannot manage MTP device playlist')
2051 message = _('This feature is not available for MTP devices.')
2052 self.notification( message, title )
2053 return
2055 device = sync.open_device()
2057 if device is None:
2058 title = _('No device configured')
2059 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2060 self.notification(message, title)
2061 return
2063 if not device.open():
2064 title = _('Cannot open device')
2065 message = _('There has been an error opening your device.')
2066 self.notification(message, title)
2067 return
2069 gPodderPlaylist(device=device, gPodder=self)
2070 device.close()
2072 def show_hide_tray_icon(self):
2073 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2074 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
2075 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2076 self.tray_icon.set_visible(False)
2077 del self.tray_icon
2078 self.tray_icon = None
2080 if gl.config.minimize_to_tray and self.tray_icon:
2081 self.tray_icon.set_visible(self.minimized)
2082 elif self.tray_icon:
2083 self.tray_icon.set_visible(True)
2085 def on_itemShowToolbar_activate(self, widget):
2086 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2088 def on_itemShowDescription_activate(self, widget):
2089 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2091 def update_item_device( self):
2092 if gl.config.device_type != 'none':
2093 self.itemDevice.show_all()
2094 (label,) = self.itemDevice.get_children()
2095 label.set_text(gl.get_device_name())
2096 else:
2097 self.itemDevice.hide_all()
2099 def properties_closed( self):
2100 self.show_hide_tray_icon()
2101 self.update_item_device()
2102 self.updateComboBox()
2104 def on_itemPreferences_activate(self, widget, *args):
2105 if gpodder.interface == gpodder.GUI:
2106 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2107 else:
2108 gPodderMaemoPreferences()
2110 def on_itemDependencies_activate(self, widget):
2111 gPodderDependencyManager()
2113 def on_add_new_google_search(self, widget, *args):
2114 def add_google_video_search(query):
2115 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2117 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2119 def require_my_gpodder_authentication(self):
2120 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2121 success, authentication = self.UsernamePasswordDialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username=gl.config.my_gpodder_username, password=gl.config.my_gpodder_password, username_prompt=_('E-Mail Address'), register_callback=lambda: util.open_website('http://my.gpodder.org/register'))
2122 if success and authentication[0] and authentication[1]:
2123 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2124 return True
2125 else:
2126 return False
2128 return True
2130 def my_gpodder_offer_autoupload(self):
2131 if not gl.config.my_gpodder_autoupload:
2132 if self.show_confirmation(_('gPodder can automatically upload your subscription list to my.gpodder.org when you close it. Do you want to enable this feature?'), _('Upload subscriptions on quit')):
2133 gl.config.my_gpodder_autoupload = True
2135 def on_download_from_mygpo(self, widget):
2136 if self.require_my_gpodder_authentication():
2137 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2138 opml_data = client.download_subscriptions()
2139 if len(opml_data) > 0:
2140 fp = open(gl.channel_opml_file, 'w')
2141 fp.write(opml_data)
2142 fp.close()
2143 (added, skipped) = (0, 0)
2144 i = opml.Importer(gl.channel_opml_file)
2145 for item in i.items:
2146 url = item['url']
2147 if url not in (c.url for c in self.channels):
2148 self.add_new_channel(url, ask_download_new=False, block=True)
2149 added += 1
2150 else:
2151 log('Already added: %s', url, sender=self)
2152 skipped += 1
2153 self.updateComboBox()
2154 if added > 0:
2155 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2156 elif widget is not None:
2157 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2158 self.my_gpodder_offer_autoupload()
2159 else:
2160 gl.config.my_gpodder_password = ''
2161 self.on_download_from_mygpo(widget)
2162 else:
2163 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2165 def on_upload_to_mygpo(self, widget):
2166 if self.require_my_gpodder_authentication():
2167 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2168 save_channels(self.channels)
2169 success, messages = client.upload_subscriptions(gl.channel_opml_file)
2170 if widget is not None:
2171 self.show_message('\n'.join(messages), _('Results of upload'))
2172 if not success:
2173 gl.config.my_gpodder_password = ''
2174 self.on_upload_to_mygpo(widget)
2175 else:
2176 self.my_gpodder_offer_autoupload()
2177 elif not success:
2178 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2179 elif widget is not None:
2180 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2182 def on_itemAddChannel_activate(self, widget, *args):
2183 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2185 def on_itemEditChannel_activate(self, widget, *args):
2186 if self.active_channel is None:
2187 title = _('No podcast selected')
2188 message = _('Please select a podcast in the podcasts list to edit.')
2189 self.show_message( message, title)
2190 return
2192 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2194 def change_channel_url(self, old_url, new_url):
2195 channel = None
2196 try:
2197 channel = podcastChannel.load(url=new_url, create=True)
2198 except:
2199 channel = None
2201 if channel is None:
2202 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2203 return
2205 for channel in self.channels:
2206 if channel.url == old_url:
2207 log('=> change channel url from %s to %s', old_url, new_url)
2208 old_save_dir = channel.save_dir
2209 channel.url = new_url
2210 new_save_dir = channel.save_dir
2211 log('old save dir=%s', old_save_dir, sender=self)
2212 log('new save dir=%s', new_save_dir, sender=self)
2213 files = glob.glob(os.path.join(old_save_dir, '*'))
2214 log('moving %d files to %s', len(files), new_save_dir, sender=self)
2215 for file in files:
2216 log('moving %s', file, sender=self)
2217 shutil.move(file, new_save_dir)
2218 try:
2219 os.rmdir(old_save_dir)
2220 except:
2221 log('Warning: cannot delete %s', old_save_dir, sender=self)
2223 save_channels(self.channels)
2224 # update feed cache and select the podcast with the new URL afterwards
2225 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2227 def on_itemRemoveChannel_activate(self, widget, *args):
2228 try:
2229 if gpodder.interface == gpodder.GUI:
2230 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2231 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2232 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2234 title = _('Remove podcast and episodes?')
2235 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2237 dialog.set_title(title)
2238 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2240 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2241 dialog.vbox.pack_start(cb_ask)
2242 cb_ask.show_all()
2243 affirmative = gtk.RESPONSE_YES
2244 elif gpodder.interface == gpodder.MAEMO:
2245 cb_ask = gtk.CheckButton('') # dummy check button
2246 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2247 affirmative = gtk.RESPONSE_OK
2249 result = dialog.run()
2250 dialog.destroy()
2252 if result == affirmative:
2253 # delete downloaded episodes only if checkbox is unchecked
2254 if cb_ask.get_active() == False:
2255 self.active_channel.remove_downloaded()
2256 else:
2257 log('Not removing downloaded episodes', sender=self)
2259 # only delete partial files if we do not have any downloads in progress
2260 delete_partial = not services.download_status_manager.has_items()
2261 gl.clean_up_downloads(delete_partial)
2263 # cancel any active downloads from this channel
2264 if not delete_partial:
2265 for episode in self.active_channel.get_all_episodes():
2266 services.download_status_manager.cancel_by_url(episode.url)
2268 # get the URL of the podcast we want to select next
2269 position = self.channels.index(self.active_channel)
2270 if position == len(self.channels)-1:
2271 # this is the last podcast, so select the URL
2272 # of the item before this one (i.e. the "new last")
2273 select_url = self.channels[position-1].url
2274 else:
2275 # there is a podcast after the deleted one, so
2276 # we simply select the one that comes after it
2277 select_url = self.channels[position+1].url
2279 # Remove the channel
2280 self.active_channel.delete()
2281 self.channels.remove(self.active_channel)
2282 save_channels(self.channels)
2284 # Re-load the channels and select the desired new channel
2285 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2286 except:
2287 log('There has been an error removing the channel.', traceback=True, sender=self)
2288 self.update_podcasts_tab()
2290 def get_opml_filter(self):
2291 filter = gtk.FileFilter()
2292 filter.add_pattern('*.opml')
2293 filter.add_pattern('*.xml')
2294 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2295 return filter
2297 def on_item_import_from_file_activate(self, widget, filename=None):
2298 if filename is None:
2299 if gpodder.interface == gpodder.GUI:
2300 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2301 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2302 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2303 elif gpodder.interface == gpodder.MAEMO:
2304 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2305 dlg.set_filter(self.get_opml_filter())
2306 response = dlg.run()
2307 filename = None
2308 if response == gtk.RESPONSE_OK:
2309 filename = dlg.get_filename()
2310 dlg.destroy()
2312 if filename is not None:
2313 gPodderOpmlLister(custom_title=_('Import podcasts from OPML file'), hide_url_entry=True).get_channels_from_url(filename, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2315 def on_itemExportChannels_activate(self, widget, *args):
2316 if not self.channels:
2317 title = _('Nothing to export')
2318 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2319 self.show_message( message, title)
2320 return
2322 if gpodder.interface == gpodder.GUI:
2323 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2324 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2325 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2326 elif gpodder.interface == gpodder.MAEMO:
2327 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2328 dlg.set_filter(self.get_opml_filter())
2329 response = dlg.run()
2330 if response == gtk.RESPONSE_OK:
2331 filename = dlg.get_filename()
2332 dlg.destroy()
2333 exporter = opml.Exporter( filename)
2334 if exporter.write(self.channels):
2335 if len(self.channels) == 1:
2336 title = _('One subscription exported')
2337 else:
2338 title = _('%d subscriptions exported') % len(self.channels)
2339 self.show_message(_('Your podcast list has been successfully exported.'), title)
2340 else:
2341 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2342 else:
2343 dlg.destroy()
2345 def on_itemImportChannels_activate(self, widget, *args):
2346 gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2348 def on_homepage_activate(self, widget, *args):
2349 util.open_website(app_website)
2351 def on_wiki_activate(self, widget, *args):
2352 util.open_website('http://wiki.gpodder.org/')
2354 def on_bug_tracker_activate(self, widget, *args):
2355 util.open_website('http://bugs.gpodder.org/')
2357 def on_itemAbout_activate(self, widget, *args):
2358 dlg = gtk.AboutDialog()
2359 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
2360 dlg.set_version( app_version)
2361 dlg.set_copyright( app_copyright)
2362 dlg.set_website( app_website)
2363 dlg.set_translator_credits( _('translator-credits'))
2364 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2366 if gpodder.interface == gpodder.GUI:
2367 # For the "GUI" version, we add some more
2368 # items to the about dialog (credits and logo)
2369 dlg.set_authors(app_authors)
2370 try:
2371 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(scalable_dir))
2372 except:
2373 pass
2375 dlg.run()
2377 def on_wNotebook_switch_page(self, widget, *args):
2378 page_num = args[1]
2379 if gpodder.interface == gpodder.MAEMO:
2380 page = self.wNotebook.get_nth_page(page_num)
2381 tab_label = self.wNotebook.get_tab_label(page).get_text()
2382 if page_num == 0 and self.active_channel is not None:
2383 self.set_title(self.active_channel.title)
2384 else:
2385 self.set_title(tab_label)
2386 if page_num == 0:
2387 self.play_or_download()
2388 else:
2389 self.toolDownload.set_sensitive( False)
2390 self.toolPlay.set_sensitive( False)
2391 self.toolTransfer.set_sensitive( False)
2392 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2394 def on_treeChannels_row_activated(self, widget, *args):
2395 self.on_itemEditChannel_activate( self.treeChannels)
2397 def on_treeChannels_cursor_changed(self, widget, *args):
2398 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2400 if model is not None and iter != None:
2401 id = model.get_path( iter)[0]
2402 self.active_channel = self.channels[id]
2404 if gpodder.interface == gpodder.MAEMO:
2405 self.set_title(self.active_channel.title)
2406 self.itemEditChannel.show_all()
2407 self.itemRemoveChannel.show_all()
2408 self.channel_toggle_lock.show_all()
2409 if self.active_channel.channel_is_locked:
2410 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2411 else:
2412 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2414 else:
2415 self.active_channel = None
2416 self.itemEditChannel.hide_all()
2417 self.itemRemoveChannel.hide_all()
2418 self.channel_toggle_lock.hide_all()
2420 self.updateTreeView(False)
2422 def on_entryAddChannel_changed(self, widget, *args):
2423 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2424 self.btnAddChannel.set_sensitive( active)
2426 def on_btnAddChannel_clicked(self, widget, *args):
2427 url = self.entryAddChannel.get_text()
2428 self.entryAddChannel.set_text('')
2429 self.add_new_channel( url)
2431 def on_btnEditChannel_clicked(self, widget, *args):
2432 self.on_itemEditChannel_activate( widget, args)
2434 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
2436 What this function does depends on from which widget it is called.
2437 It gets the selected episodes of the current podcast and runs one
2438 of the following actions on them:
2440 * Transfer (to MP3 player, iPod, etc..)
2441 * Playback/open files
2442 * Show the episode info dialog
2443 * Download episodes
2445 try:
2446 selection = self.treeAvailable.get_selection()
2447 (model, paths) = selection.get_selected_rows()
2449 wname = widget.get_name()
2450 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
2451 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2452 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
2454 episodes = []
2455 for path in paths:
2456 it = model.get_iter(path)
2457 url = model.get_value(it, 0)
2458 episode = self.active_channel.find_episode(url)
2459 episodes.append(episode)
2461 if len(episodes) == 0:
2462 log('No episodes selected', sender=self)
2464 if do_transfer:
2465 self.on_sync_to_ipod_activate(widget, episodes)
2466 elif do_playback:
2467 for episode in episodes:
2468 # Make sure to mark the episode as downloaded
2469 if os.path.exists(episode.local_filename()):
2470 episode.channel.addDownloadedItem(episode)
2471 self.playback_episode(episode)
2472 elif gl.config.enable_streaming:
2473 self.playback_episode(episode, stream=True)
2474 elif do_epdialog:
2475 play_callback = lambda: self.playback_episode(episode)
2476 download_callback = lambda: self.download_episode_list([episode])
2477 gPodderEpisode(episode=episode, download_callback=download_callback, play_callback=play_callback)
2478 else:
2479 self.download_episode_list(episodes)
2480 except:
2481 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
2483 def on_treeAvailable_button_release_event(self, widget, *args):
2484 self.play_or_download()
2486 def auto_update_procedure(self, first_run=False):
2487 log('auto_update_procedure() got called', sender=self)
2488 if not first_run and gl.config.auto_update_feeds and self.minimized:
2489 self.update_feed_cache(force_update=True)
2491 next_update = 60*1000*gl.config.auto_update_frequency
2492 gobject.timeout_add(next_update, self.auto_update_procedure)
2494 def on_treeDownloads_row_activated(self, widget, *args):
2495 cancel_urls = []
2497 if self.wNotebook.get_current_page() > 0:
2498 # Use the download list treeview + model
2499 ( tree, column ) = ( self.treeDownloads, 3 )
2500 else:
2501 # Use the available podcasts treeview + model
2502 ( tree, column ) = ( self.treeAvailable, 0 )
2504 selection = tree.get_selection()
2505 (model, paths) = selection.get_selected_rows()
2506 for path in paths:
2507 url = model.get_value( model.get_iter( path), column)
2508 cancel_urls.append( url)
2510 if len( cancel_urls) == 0:
2511 log('Nothing selected.', sender = self)
2512 return
2514 if len( cancel_urls) == 1:
2515 title = _('Cancel download?')
2516 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2517 else:
2518 title = _('Cancel downloads?')
2519 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2521 if self.show_confirmation( message, title):
2522 services.download_status_manager.start_batch_mode()
2523 for url in cancel_urls:
2524 services.download_status_manager.cancel_by_url( url)
2525 services.download_status_manager.end_batch_mode()
2527 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2528 self.on_treeDownloads_row_activated( widget, None)
2530 def on_btnCancelAll_clicked(self, widget, *args):
2531 self.treeDownloads.get_selection().select_all()
2532 self.on_treeDownloads_row_activated( self.toolCancel, None)
2533 self.treeDownloads.get_selection().unselect_all()
2535 def on_btnDownloadedDelete_clicked(self, widget, *args):
2536 if self.active_channel is None:
2537 return
2539 channel_url = self.active_channel.url
2540 selection = self.treeAvailable.get_selection()
2541 ( model, paths ) = selection.get_selected_rows()
2543 if selection.count_selected_rows() == 0:
2544 log( 'Nothing selected - will not remove any downloaded episode.')
2545 return
2547 if selection.count_selected_rows() == 1:
2548 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2550 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2551 if episode['is_locked']:
2552 title = _('%s is locked') % episode_title
2553 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2554 self.notification(message, title)
2555 return
2557 title = _('Remove %s?') % episode_title
2558 message = _("If you remove this episode, it will be deleted from your computer. If you want to listen to this episode again, you will have to re-download it.")
2559 else:
2560 title = _('Remove %d episodes?') % selection.count_selected_rows()
2561 message = _('If you remove these episodes, they will be deleted from your computer. If you want to listen to any of these episodes again, you will have to re-download the episodes in question.')
2563 locked_count = 0
2564 for path in paths:
2565 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2566 if episode['is_locked']:
2567 locked_count += 1
2569 if selection.count_selected_rows() == locked_count:
2570 title = _('Episodes are locked')
2571 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2572 self.notification(message, title)
2573 return
2574 elif locked_count > 0:
2575 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2576 message = _('The selection contains locked episodes. These will not be deleted. If you want to listen to any of these episodes again, then you will have to re-download them.')
2578 # if user confirms deletion, let's remove some stuff ;)
2579 if self.show_confirmation( message, title):
2580 try:
2581 # iterate over the selection, see also on_treeDownloads_row_activated
2582 for path in paths:
2583 url = model.get_value( model.get_iter( path), 0)
2584 self.active_channel.delete_episode_by_url( url)
2586 # now, clear local db cache so we can re-read it
2587 self.updateComboBox()
2588 except:
2589 log( 'Error while deleting (some) downloads.')
2591 # only delete partial files if we do not have any downloads in progress
2592 delete_partial = not services.download_status_manager.has_items()
2593 gl.clean_up_downloads(delete_partial)
2594 self.updateTreeView()
2596 def on_key_press(self, widget, event):
2597 # Allow tab switching with Ctrl + PgUp/PgDown
2598 if event.state & gtk.gdk.CONTROL_MASK:
2599 if event.keyval == gtk.keysyms.Page_Up:
2600 self.wNotebook.prev_page()
2601 return True
2602 elif event.keyval == gtk.keysyms.Page_Down:
2603 self.wNotebook.next_page()
2604 return True
2606 # After this code we only handle Maemo hardware keys,
2607 # so if we are not a Maemo app, we don't do anything
2608 if gpodder.interface != gpodder.MAEMO:
2609 return False
2611 if event.keyval == gtk.keysyms.F6:
2612 if self.fullscreen:
2613 self.window.unfullscreen()
2614 else:
2615 self.window.fullscreen()
2616 if event.keyval == gtk.keysyms.Escape:
2617 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2618 self.vboxChannelNavigator.set_property('visible', new_visibility)
2619 self.column_size.set_visible(not new_visibility)
2620 self.column_released.set_visible(not new_visibility)
2622 diff = 0
2623 if event.keyval == gtk.keysyms.F7: #plus
2624 diff = 1
2625 elif event.keyval == gtk.keysyms.F8: #minus
2626 diff = -1
2628 if diff != 0:
2629 selection = self.treeChannels.get_selection()
2630 (model, iter) = selection.get_selected()
2631 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2632 self.on_treeChannels_cursor_changed(self.treeChannels)
2634 def window_state_event(self, widget, event):
2635 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2636 self.fullscreen = True
2637 else:
2638 self.fullscreen = False
2640 old_minimized = self.minimized
2642 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
2643 if gpodder.interface == gpodder.MAEMO:
2644 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
2646 if old_minimized != self.minimized and self.tray_icon:
2647 self.gPodder.set_skip_taskbar_hint(self.minimized)
2648 elif not self.tray_icon:
2649 self.gPodder.set_skip_taskbar_hint(False)
2651 if gl.config.minimize_to_tray and self.tray_icon:
2652 self.tray_icon.set_visible(self.minimized)
2654 def uniconify_main_window(self):
2655 if self.minimized:
2656 self.gPodder.present()
2658 def iconify_main_window(self):
2659 if not self.minimized:
2660 self.gPodder.iconify()
2662 def update_podcasts_tab(self):
2663 if len(self.channels):
2664 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
2665 else:
2666 self.label2.set_text(_('Podcasts'))
2668 class gPodderChannel(GladeWidget):
2669 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description']
2671 def new(self):
2672 global WEB_BROWSER_ICON
2673 self.changed = False
2674 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2675 self.gPodderChannel.set_title( self.channel.title)
2676 self.entryTitle.set_text( self.channel.title)
2677 self.entryURL.set_text( self.channel.url)
2679 self.LabelDownloadTo.set_text( self.channel.save_dir)
2680 self.LabelWebsite.set_text( self.channel.link)
2682 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2683 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2684 if self.channel.username:
2685 self.FeedUsername.set_text( self.channel.username)
2686 if self.channel.password:
2687 self.FeedPassword.set_text( self.channel.password)
2689 services.cover_downloader.register('cover-available', self.cover_download_finished)
2690 services.cover_downloader.request_cover(self.channel)
2692 # Hide the website button if we don't have a valid URL
2693 if not self.channel.link:
2694 self.btn_website.hide_all()
2696 b = gtk.TextBuffer()
2697 b.set_text( self.channel.description)
2698 self.channel_description.set_buffer( b)
2700 #Add Drag and Drop Support
2701 flags = gtk.DEST_DEFAULT_ALL
2702 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2703 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2704 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2705 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2707 def on_btn_website_clicked(self, widget):
2708 util.open_website(self.channel.link)
2710 def on_btnDownloadCover_clicked(self, widget):
2711 if gpodder.interface == gpodder.GUI:
2712 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2713 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2714 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2715 elif gpodder.interface == gpodder.MAEMO:
2716 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2718 if dlg.run() == gtk.RESPONSE_OK:
2719 url = dlg.get_uri()
2720 services.cover_downloader.replace_cover(self.channel, url)
2722 dlg.destroy()
2724 def on_btnClearCover_clicked(self, widget):
2725 services.cover_downloader.replace_cover(self.channel)
2727 def cover_download_finished(self, channel_url, pixbuf):
2728 if pixbuf is not None:
2729 self.imgCover.set_from_pixbuf(pixbuf)
2730 self.gPodderChannel.show()
2732 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2733 files = sel.data.strip().split('\n')
2734 if len(files) != 1:
2735 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2736 return
2738 file = files[0]
2740 if file.startswith('file://') or file.startswith('http://'):
2741 services.cover_downloader.replace_cover(self.channel, file)
2742 return
2744 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2746 def on_gPodderChannel_destroy(self, widget, *args):
2747 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2749 def on_btnOK_clicked(self, widget, *args):
2750 entered_url = self.entryURL.get_text()
2751 channel_url = self.channel.url
2753 if entered_url != channel_url:
2754 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2755 if hasattr(self, 'callback_change_url'):
2756 self.gPodderChannel.hide_all()
2757 self.callback_change_url(channel_url, entered_url)
2759 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2760 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2761 self.channel.set_custom_title( self.entryTitle.get_text())
2762 self.channel.username = self.FeedUsername.get_text().strip()
2763 self.channel.password = self.FeedPassword.get_text()
2764 self.channel.save()
2766 self.gPodderChannel.destroy()
2767 self.callback_closed()
2769 class gPodderAddPodcastDialog(GladeWidget):
2770 finger_friendly_widgets = ['btn_close', 'btn_add']
2772 def new(self):
2773 if not hasattr(self, 'url_callback'):
2774 log('No url callback set', sender=self)
2775 self.url_callback = None
2776 if hasattr(self, 'custom_label'):
2777 self.label_add.set_text(self.custom_label)
2778 if hasattr(self, 'custom_title'):
2779 self.gPodderAddPodcastDialog.set_title(self.custom_title)
2781 def on_btn_close_clicked(self, widget):
2782 self.gPodderAddPodcastDialog.destroy()
2784 def on_entry_url_changed(self, widget):
2785 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2787 def on_btn_add_clicked(self, widget):
2788 url = self.entry_url.get_text()
2789 self.on_btn_close_clicked(widget)
2790 if self.url_callback is not None:
2791 self.url_callback(url)
2794 class gPodderMaemoPreferences(GladeWidget):
2795 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2797 def new(self):
2798 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2799 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2800 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2801 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2803 self.restart_required = False
2804 self.show_tray_icon.connect('clicked', self.on_restart_required)
2805 self.show_notifications.connect('clicked', self.on_restart_required)
2807 def on_restart_required(self, widget):
2808 self.restart_required = True
2810 def on_btn_advanced_clicked(self, widget):
2811 self.gPodderMaemoPreferences.destroy()
2812 gPodderConfigEditor()
2814 def on_btn_close_clicked(self, widget):
2815 self.gPodderMaemoPreferences.destroy()
2816 if self.restart_required:
2817 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2820 class gPodderProperties(GladeWidget):
2821 def new(self):
2822 if not hasattr( self, 'callback_finished'):
2823 self.callback_finished = None
2825 if gpodder.interface == gpodder.MAEMO:
2826 self.table5.hide_all() # player
2827 self.gPodderProperties.fullscreen()
2829 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2830 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2831 gl.config.connect_gtk_editable( 'player', self.openApp)
2832 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2833 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2834 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2835 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2836 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2837 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2838 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2839 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2840 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2841 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2842 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2843 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2844 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2845 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2846 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2847 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2848 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2849 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2850 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2851 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2853 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2854 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2856 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2858 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2859 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2860 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2862 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2863 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
2865 if tagging_supported():
2866 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
2867 else:
2868 self.updatetags.set_sensitive( False)
2869 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
2870 self.updatetags.set_label( new_label)
2872 # device type
2873 self.comboboxDeviceType.set_active( 0)
2874 if gl.config.device_type == 'ipod':
2875 self.comboboxDeviceType.set_active( 1)
2876 elif gl.config.device_type == 'filesystem':
2877 self.comboboxDeviceType.set_active( 2)
2878 elif gl.config.device_type == 'mtp':
2879 self.comboboxDeviceType.set_active( 3)
2881 # setup cell renderers
2882 cellrenderer = gtk.CellRendererPixbuf()
2883 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
2884 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2885 cellrenderer = gtk.CellRendererText()
2886 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
2887 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2889 cellrenderer = gtk.CellRendererPixbuf()
2890 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
2891 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2892 cellrenderer = gtk.CellRendererText()
2893 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
2894 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2896 if not hasattr(self, 'user_apps_reader'):
2897 self.user_apps_reader = UserAppsReader(['audio', 'video'])
2899 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
2900 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
2902 if gpodder.interface == gpodder.GUI:
2903 self.user_apps_reader.read()
2905 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
2906 index = self.find_active_audio_app()
2907 self.comboAudioPlayerApp.set_active(index)
2908 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
2909 index = self.find_active_video_app()
2910 self.comboVideoPlayerApp.set_active(index)
2912 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
2914 def is_row_separator(self, model, iter):
2915 return model.get_value(iter, 0) == ''
2917 def update_mountpoint( self, ipod):
2918 if ipod is None or ipod.mount_point is None:
2919 self.iPodMountpoint.set_label( '')
2920 else:
2921 self.iPodMountpoint.set_label( ipod.mount_point)
2923 def find_active_audio_app(self):
2924 model = self.comboAudioPlayerApp.get_model()
2925 iter = model.get_iter_first()
2926 index = 0
2927 while iter is not None:
2928 command = model.get_value(iter, 1)
2929 if command == self.openApp.get_text():
2930 return index
2931 iter = model.iter_next(iter)
2932 index += 1
2933 # return last item = custom command
2934 return index-1
2936 def find_active_video_app( self):
2937 model = self.comboVideoPlayerApp.get_model()
2938 iter = model.get_iter_first()
2939 index = 0
2940 while iter is not None:
2941 command = model.get_value(iter, 1)
2942 if command == self.openVideoApp.get_text():
2943 return index
2944 iter = model.iter_next(iter)
2945 index += 1
2946 # return last item = custom command
2947 return index-1
2949 def set_download_dir( self, new_download_dir, event = None):
2950 gl.downloaddir = self.chooserDownloadTo.get_filename()
2951 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2952 self.notification(_('There has been an error moving your downloads to the specified location. The old download directory will be used instead.'), _('Error moving downloads'))
2954 if event:
2955 event.set()
2957 def on_auto_update_feeds_toggled( self, widget, *args):
2958 self.auto_update_frequency.set_sensitive(widget.get_active())
2960 def on_display_tray_icon_toggled( self, widget, *args):
2961 self.enable_notifications.set_sensitive(widget.get_active())
2962 self.minimize_to_tray.set_sensitive(widget.get_active())
2964 def on_cbCustomSyncName_toggled( self, widget, *args):
2965 self.entryCustomSyncName.set_sensitive( widget.get_active())
2967 def on_only_sync_not_played_toggled( self, widget, *args):
2968 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
2969 if not widget.get_active():
2970 self.delete_episodes_marked_played.set_active(False)
2972 def on_delete_episodes_marked_played_toggled( self, widget, *args):
2973 if widget.get_active() and self.only_sync_not_played.get_active():
2974 self.on_sync_leave.set_active(True)
2975 self.on_sync_delete.set_sensitive(not widget.get_active())
2976 self.on_sync_mark_played.set_sensitive(not widget.get_active())
2978 def on_btnCustomSyncNameHelp_clicked( self, widget):
2979 examples = [
2980 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
2981 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
2982 '<i>{episode.published}</i> -&gt; <b>20070908</b>',
2983 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
2986 info = [
2987 _('You can specify a custom format string for the file names on your MP3 player here.'),
2988 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2989 '\n'.join( [ ' %s' % s for s in examples ])
2992 self.show_message( '\n\n'.join( info), _('Custom format strings'))
2994 def on_gPodderProperties_destroy(self, widget, *args):
2995 self.on_btnOK_clicked( widget, *args)
2997 def on_btnConfigEditor_clicked(self, widget, *args):
2998 self.on_btnOK_clicked(widget, *args)
2999 gPodderConfigEditor()
3001 def on_comboAudioPlayerApp_changed(self, widget, *args):
3002 # find out which one
3003 iter = self.comboAudioPlayerApp.get_active_iter()
3004 model = self.comboAudioPlayerApp.get_model()
3005 command = model.get_value( iter, 1)
3006 if command == '':
3007 if self.openApp.get_text() == 'default':
3008 self.openApp.set_text('')
3009 self.openApp.set_sensitive( True)
3010 self.openApp.show()
3011 self.labelCustomCommand.show()
3012 else:
3013 self.openApp.set_text( command)
3014 self.openApp.set_sensitive( False)
3015 self.openApp.hide()
3016 self.labelCustomCommand.hide()
3018 def on_comboVideoPlayerApp_changed(self, widget, *args):
3019 # find out which one
3020 iter = self.comboVideoPlayerApp.get_active_iter()
3021 model = self.comboVideoPlayerApp.get_model()
3022 command = model.get_value(iter, 1)
3023 if command == '':
3024 if self.openVideoApp.get_text() == 'default':
3025 self.openVideoApp.set_text('')
3026 self.openVideoApp.set_sensitive(True)
3027 self.openVideoApp.show()
3028 self.labelCustomVideoCommand.show()
3029 else:
3030 self.openVideoApp.set_text(command)
3031 self.openVideoApp.set_sensitive(False)
3032 self.openVideoApp.hide()
3033 self.labelCustomVideoCommand.hide()
3035 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3036 sens = not self.cbEnvironmentVariables.get_active()
3037 self.httpProxy.set_sensitive( sens)
3038 self.ftpProxy.set_sensitive( sens)
3040 def on_comboboxDeviceType_changed(self, widget, *args):
3041 active_item = self.comboboxDeviceType.get_active()
3043 # None
3044 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3045 self.imageSyncOptions, self. separatorSyncOptions,
3046 self.on_sync_mark_played, self.on_sync_delete,
3047 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
3048 for widget in sync_widgets:
3049 if active_item == 0:
3050 widget.hide_all()
3051 else:
3052 widget.show_all()
3054 # iPod
3055 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3056 self.ipod_write_gtkpod_extended)
3057 for widget in ipod_widgets:
3058 if active_item == 1:
3059 widget.show_all()
3060 else:
3061 widget.hide_all()
3063 # filesystem-based MP3 player
3064 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3065 self.cbChannelSubfolder, self.cbCustomSyncName,
3066 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
3067 for widget in fs_widgets:
3068 if active_item == 2:
3069 widget.show_all()
3070 else:
3071 widget.hide_all()
3073 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3074 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3075 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3076 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3077 fs.set_current_folder(self.iPodMountpoint.get_label())
3078 if fs.run() == gtk.RESPONSE_OK:
3079 self.iPodMountpoint.set_label( fs.get_filename())
3080 fs.destroy()
3082 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3083 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3084 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3085 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3086 fs.set_current_folder(self.filesystemMountpoint.get_label())
3087 if fs.run() == gtk.RESPONSE_OK:
3088 self.filesystemMountpoint.set_label( fs.get_filename())
3089 fs.destroy()
3091 def on_btnOK_clicked(self, widget, *args):
3092 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3093 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3095 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3096 new_download_dir = self.chooserDownloadTo.get_filename()
3097 download_dir_size = util.calculate_size( gl.downloaddir)
3098 download_dir_size_string = gl.format_filesize( download_dir_size)
3099 event = Event()
3101 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3102 dlg.vbox.set_spacing( 5)
3103 dlg.set_border_width( 5)
3105 label = gtk.Label()
3106 label.set_line_wrap( True)
3107 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3108 myprogressbar = gtk.ProgressBar()
3110 # put it all together
3111 dlg.vbox.pack_start( label)
3112 dlg.vbox.pack_end( myprogressbar)
3114 # switch windows
3115 dlg.show_all()
3116 self.gPodderProperties.hide_all()
3118 # hide action area and separator line
3119 dlg.action_area.hide()
3120 dlg.set_has_separator( False)
3122 args = ( new_download_dir, event, )
3124 thread = Thread( target = self.set_download_dir, args = args)
3125 thread.start()
3127 while not event.isSet():
3128 try:
3129 new_download_dir_size = util.calculate_size( new_download_dir)
3130 except:
3131 new_download_dir_size = 0
3132 if download_dir_size > 0:
3133 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3134 else:
3135 fract = 0.0
3136 if fract < 0.99:
3137 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3138 else:
3139 myprogressbar.set_text( _('Finishing... please wait.'))
3140 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3141 event.wait( 0.1)
3142 while gtk.events_pending():
3143 gtk.main_iteration( False)
3145 dlg.destroy()
3147 device_type = self.comboboxDeviceType.get_active()
3148 if device_type == 0:
3149 gl.config.device_type = 'none'
3150 elif device_type == 1:
3151 gl.config.device_type = 'ipod'
3152 elif device_type == 2:
3153 gl.config.device_type = 'filesystem'
3154 elif device_type == 3:
3155 gl.config.device_type = 'mtp'
3156 self.gPodderProperties.destroy()
3157 if self.callback_finished:
3158 self.callback_finished()
3161 class gPodderEpisode(GladeWidget):
3162 finger_friendly_widgets = ['episode_description', 'btnCloseWindow', 'btnDownload',
3163 'btnCancel', 'btnPlay', 'btn_website']
3165 def new(self):
3166 global WEB_BROWSER_ICON
3167 self.image3166.set_property('icon-name', WEB_BROWSER_ICON)
3168 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
3169 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
3171 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
3173 if gpodder.interface == gpodder.MAEMO:
3174 # Hide the advanced prefs expander
3175 self.expander1.hide_all()
3177 try:
3178 import gtkhtml2
3179 document = gtkhtml2.Document()
3180 document.connect('link-clicked', lambda d, url: util.open_website(url))
3181 def request_url(document, url, stream):
3182 stream.write(urllib2.urlopen(url).read())
3183 stream.close()
3184 document.connect('request-url', request_url)
3185 document.clear()
3186 document.open_stream('text/html')
3187 document.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3188 document.write_stream('<h2>%s</h2><small><em>from <a href="%s">%s</a>, released %s</em></small><br><hr style="border: 1px #eeeeee solid;">' % (saxutils.escape(self.episode.title), self.episode.link, saxutils.escape(self.episode.channel.title), self.episode.cute_pubdate()))
3189 document.write_stream(self.episode.description)
3190 document.write_stream('<br><hr style="border: 1px #eeeeee solid;"><p style="font-size: 8px;">%s</p>' % self.episode.link)
3191 document.write_stream('</body></html>')
3192 document.close_stream()
3194 self.episode_title.hide_all()
3195 self.channel_title.hide_all()
3196 self.btn_website.hide_all()
3197 self.expander1.hide_all()
3199 view = gtkhtml2.View()
3200 view.set_document(document)
3201 self.scrolledwindow4.remove(self.scrolledwindow4.get_child())
3202 self.scrolledwindow4.add(view)
3203 view.show()
3204 except ImportError, ie:
3205 b = gtk.TextBuffer()
3206 b.set_text(strip(util.remove_html_tags(self.episode.description)))
3207 self.episode_description.set_buffer( b)
3209 self.gPodderEpisode.set_title( self.episode.title)
3210 self.LabelDownloadLink.set_text( self.episode.url)
3211 self.LabelWebsiteLink.set_text( self.episode.link)
3212 self.labelPubDate.set_text(self.episode.cute_pubdate())
3214 # Hide the "Go to website" button if we don't have a valid URL
3215 if self.episode.link == self.episode.url or not self.episode.link:
3216 self.btn_website.hide_all()
3218 self.channel_title.set_markup(_('<i>from %s</i>') % saxutils.escape(self.episode.channel.title))
3220 self.hide_show_widgets()
3221 services.download_status_manager.request_progress_detail( self.episode.url)
3222 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3224 def on_btnCancel_clicked( self, widget):
3225 services.download_status_manager.cancel_by_url( self.episode.url)
3227 def on_gPodderEpisode_destroy( self, widget):
3228 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
3229 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
3231 def on_download_status_changed( self):
3232 self.hide_show_widgets()
3234 def on_btn_website_clicked(self, widget):
3235 util.open_website(self.episode.link)
3237 def on_download_status_progress( self, url, progress, speed):
3238 if url == self.episode.url:
3239 progress = float(min(100.0,max(0.0,progress)))
3240 self.progress_bar.set_fraction(progress/100.0)
3241 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
3243 def hide_show_widgets( self):
3244 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
3245 if is_downloading:
3246 self.progress_bar.show_all()
3247 self.btnCancel.show_all()
3248 self.btnPlay.hide_all()
3249 self.btnDownload.hide_all()
3250 else:
3251 self.progress_bar.hide_all()
3252 self.btnCancel.hide_all()
3253 if os.path.exists( self.episode.local_filename()):
3254 if self.episode.file_type() in ('audio', 'video'):
3255 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3256 else:
3257 self.btnPlay.set_label(gtk.STOCK_OPEN)
3258 self.btnPlay.set_use_stock(True)
3259 self.btnPlay.show_all()
3260 self.btnDownload.hide_all()
3261 else:
3262 self.btnPlay.hide_all()
3263 self.btnDownload.show_all()
3265 def on_btnCloseWindow_clicked(self, widget, *args):
3266 self.gPodderEpisode.destroy()
3268 def on_btnDownload_clicked(self, widget, *args):
3269 if self.download_callback:
3270 self.download_callback()
3272 def on_btnPlay_clicked(self, widget, *args):
3273 if self.play_callback:
3274 self.play_callback()
3276 self.gPodderEpisode.destroy()
3279 class gPodderSync(GladeWidget):
3280 def new(self):
3281 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3283 self.device.register('progress', self.on_progress)
3284 self.device.register('sub-progress', self.on_sub_progress)
3285 self.device.register('status', self.on_status)
3286 self.device.register('done', self.on_done)
3288 def on_progress(self, pos, max, text=None):
3289 if text is None:
3290 text = _('%d of %d done') % (pos, max)
3291 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3292 util.idle_add(self.progressbar.set_text, text)
3294 def on_sub_progress(self, percentage):
3295 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3297 def on_status(self, status):
3298 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3300 def on_done(self):
3301 util.idle_add(self.gPodderSync.destroy)
3302 if not self.gPodder.minimized:
3303 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
3305 def on_gPodderSync_destroy(self, widget, *args):
3306 self.device.unregister('progress', self.on_progress)
3307 self.device.unregister('sub-progress', self.on_sub_progress)
3308 self.device.unregister('status', self.on_status)
3309 self.device.unregister('done', self.on_done)
3310 self.device.cancel()
3312 def on_cancel_button_clicked(self, widget, *args):
3313 self.device.cancel()
3316 class gPodderOpmlLister(GladeWidget):
3317 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3319 def new(self):
3320 # initiate channels list
3321 self.channels = []
3322 self.callback_for_channel = None
3323 self.callback_finished = None
3325 if hasattr(self, 'custom_title'):
3326 self.gPodderOpmlLister.set_title(self.custom_title)
3327 if hasattr(self, 'hide_url_entry'):
3328 self.hbox25.hide_all()
3330 self.setup_treeview(self.treeviewChannelChooser)
3331 self.setup_treeview(self.treeviewTopPodcastsChooser)
3332 self.setup_treeview(self.treeviewYouTubeChooser)
3334 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
3336 def setup_treeview(self, tv):
3337 togglecell = gtk.CellRendererToggle()
3338 togglecell.set_property( 'activatable', True)
3339 togglecell.connect( 'toggled', self.callback_edited)
3340 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
3342 titlecell = gtk.CellRendererText()
3343 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
3344 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
3346 for itemcolumn in ( togglecolumn, titlecolumn ):
3347 tv.append_column(itemcolumn)
3349 def callback_edited( self, cell, path):
3350 model = self.get_treeview().get_model()
3352 url = model[path][2]
3354 model[path][0] = not model[path][0]
3355 if model[path][0]:
3356 self.channels.append( url)
3357 else:
3358 self.channels.remove( url)
3360 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
3362 def get_selected_channels(self, tab=None):
3363 channels = []
3365 model = self.get_treeview(tab).get_model()
3366 if model is not None:
3367 for row in model:
3368 if row[0]:
3369 channels.append(row[2])
3371 return channels
3373 def on_change_tab(self, tab):
3374 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
3376 def thread_finished(self, model, tab=0):
3377 if tab == 1:
3378 tv = self.treeviewTopPodcastsChooser
3379 elif tab == 2:
3380 tv = self.treeviewYouTubeChooser
3381 self.entryYoutubeSearch.set_sensitive(True)
3382 self.btnSearchYouTube.set_sensitive(True)
3383 self.btnOK.set_sensitive(False)
3384 else:
3385 tv = self.treeviewChannelChooser
3386 self.btnDownloadOpml.set_sensitive(True)
3387 self.entryURL.set_sensitive(True)
3388 self.channels = []
3390 tv.set_model(model)
3391 tv.set_sensitive(True)
3393 def thread_func(self, tab=0):
3394 if tab == 1:
3395 model = opml.Importer(gl.config.toplist_url).get_model()
3396 if len(model) == 0:
3397 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3398 elif tab == 2:
3399 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
3400 if len(model) == 0:
3401 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
3402 else:
3403 model = opml.Importer(self.entryURL.get_text()).get_model()
3404 if len(model) == 0:
3405 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3407 util.idle_add(self.thread_finished, model, tab)
3409 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
3410 if callback_for_channel:
3411 self.callback_for_channel = callback_for_channel
3412 if callback_finished:
3413 self.callback_finished = callback_finished
3414 self.entryURL.set_text( url)
3415 self.btnDownloadOpml.set_sensitive( False)
3416 self.entryURL.set_sensitive( False)
3417 self.btnOK.set_sensitive( False)
3418 self.treeviewChannelChooser.set_sensitive( False)
3419 Thread( target = self.thread_func).start()
3420 Thread( target = lambda: self.thread_func(1)).start()
3422 def select_all( self, value ):
3423 enabled = False
3424 model = self.get_treeview().get_model()
3425 if model is not None:
3426 for row in model:
3427 row[0] = value
3428 if value:
3429 enabled = True
3430 self.btnOK.set_sensitive(enabled)
3432 def on_gPodderOpmlLister_destroy(self, widget, *args):
3433 pass
3435 def on_btnDownloadOpml_clicked(self, widget, *args):
3436 self.get_channels_from_url( self.entryURL.get_text())
3438 def on_btnSearchYouTube_clicked(self, widget, *args):
3439 self.entryYoutubeSearch.set_sensitive(False)
3440 self.treeviewYouTubeChooser.set_sensitive(False)
3441 self.btnSearchYouTube.set_sensitive(False)
3442 Thread(target = lambda: self.thread_func(2)).start()
3444 def on_btnSelectAll_clicked(self, widget, *args):
3445 self.select_all(True)
3447 def on_btnSelectNone_clicked(self, widget, *args):
3448 self.select_all(False)
3450 def on_btnOK_clicked(self, widget, *args):
3451 self.channels = self.get_selected_channels()
3452 self.gPodderOpmlLister.destroy()
3454 # add channels that have been selected
3455 for url in self.channels:
3456 if self.callback_for_channel:
3457 self.callback_for_channel( url)
3459 if self.callback_finished:
3460 util.idle_add(self.callback_finished)
3462 def on_btnCancel_clicked(self, widget, *args):
3463 self.gPodderOpmlLister.destroy()
3465 def on_entryYoutubeSearch_key_press_event(self, widget, event):
3466 if event.keyval == gtk.keysyms.Return:
3467 self.on_btnSearchYouTube_clicked(widget)
3469 def get_treeview(self, tab=None):
3470 if tab is None:
3471 tab = self.notebookChannelAdder.get_current_page()
3473 if tab == 0:
3474 return self.treeviewChannelChooser
3475 elif tab == 1:
3476 return self.treeviewTopPodcastsChooser
3477 else:
3478 return self.treeviewYouTubeChooser
3480 class gPodderEpisodeSelector( GladeWidget):
3481 """Episode selection dialog
3483 Optional keyword arguments that modify the behaviour of this dialog:
3485 - callback: Function that takes 1 parameter which is a list of
3486 the selected episodes (or empty list when none selected)
3487 - remove_callback: Function that takes 1 parameter which is a list
3488 of episodes that should be "removed" (see below)
3489 (default is None, which means remove not possible)
3490 - remove_action: Label for the "remove" action (default is "Remove")
3491 - remove_finished: Callback after all remove callbacks have finished
3492 (default is None, also depends on remove_callback)
3493 - episodes: List of episodes that are presented for selection
3494 - selected: (optional) List of boolean variables that define the
3495 default checked state for the given episodes
3496 - selected_default: (optional) The default boolean value for the
3497 checked state if no other value is set
3498 (default is False)
3499 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3500 columns, the name is the attribute name of the episode to be
3501 read from each episode object. The sort name is the
3502 attribute name of the episode to be used to sort this column.
3503 If the sort_name is None it will use the attribute name for
3504 sorting. The sort type is the type of the sort column.
3505 The caption attribute is the text that appear as column caption
3506 (default is [('title_and_description', None, None, 'Episode'),])
3507 - title: (optional) The title of the window + heading
3508 - instructions: (optional) A one-line text describing what the
3509 user should select / what the selection is for
3510 - stock_ok_button: (optional) Will replace the "OK" button with
3511 another GTK+ stock item to be used for the
3512 affirmative button of the dialog (e.g. can
3513 be gtk.STOCK_DELETE when the episodes to be
3514 selected will be deleted after closing the
3515 dialog)
3516 - selection_buttons: (optional) A dictionary with labels as
3517 keys and callbacks as values; for each
3518 key a button will be generated, and when
3519 the button is clicked, the callback will
3520 be called for each episode and the return
3521 value of the callback (True or False) will
3522 be the new selected state of the episode
3523 - size_attribute: (optional) The name of an attribute of the
3524 supplied episode objects that can be used to
3525 calculate the size of an episode; set this to
3526 None if no total size calculation should be
3527 done (in cases where total size is useless)
3528 (default is 'length')
3529 - tooltip_attribute: (optional) The name of an attribute of
3530 the supplied episode objects that holds
3531 the text for the tooltips when hovering
3532 over an episode (default is 'description')
3535 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3537 COLUMN_INDEX = 0
3538 COLUMN_TOOLTIP = 1
3539 COLUMN_TOGGLE = 2
3540 COLUMN_ADDITIONAL = 3
3542 def new( self):
3543 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
3544 if not hasattr( self, 'callback'):
3545 self.callback = None
3547 if not hasattr(self, 'remove_callback'):
3548 self.remove_callback = None
3550 if not hasattr(self, 'remove_action'):
3551 self.remove_action = _('Remove')
3553 if not hasattr(self, 'remove_finished'):
3554 self.remove_finished = None
3556 if not hasattr( self, 'episodes'):
3557 self.episodes = []
3559 if not hasattr( self, 'size_attribute'):
3560 self.size_attribute = 'length'
3562 if not hasattr(self, 'tooltip_attribute'):
3563 self.tooltip_attribute = 'description'
3565 if not hasattr( self, 'selection_buttons'):
3566 self.selection_buttons = {}
3568 if not hasattr( self, 'selected_default'):
3569 self.selected_default = False
3571 if not hasattr( self, 'selected'):
3572 self.selected = [self.selected_default]*len(self.episodes)
3574 if len(self.selected) < len(self.episodes):
3575 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3577 if not hasattr( self, 'columns'):
3578 self.columns = (('title_and_description', None, None, _('Episode')),)
3580 if hasattr( self, 'title'):
3581 self.gPodderEpisodeSelector.set_title( self.title)
3582 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3584 if gpodder.interface == gpodder.MAEMO:
3585 self.labelHeading.hide()
3587 if hasattr( self, 'instructions'):
3588 self.labelInstructions.set_text( self.instructions)
3589 self.labelInstructions.show_all()
3591 if hasattr(self, 'stock_ok_button'):
3592 if self.stock_ok_button == 'gpodder-download':
3593 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3594 self.btnOK.set_label(_('Download'))
3595 else:
3596 self.btnOK.set_label(self.stock_ok_button)
3597 self.btnOK.set_use_stock(True)
3599 # check/uncheck column
3600 toggle_cell = gtk.CellRendererToggle()
3601 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3602 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3604 next_column = self.COLUMN_ADDITIONAL
3605 for name, sort_name, sort_type, caption in self.columns:
3606 renderer = gtk.CellRendererText()
3607 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3608 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
3609 column.set_resizable( True)
3610 # Only set "expand" on the first column (so more text is displayed there)
3611 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3612 if sort_name is not None:
3613 column.set_sort_column_id(next_column+1)
3614 else:
3615 column.set_sort_column_id(next_column)
3616 self.treeviewEpisodes.append_column( column)
3617 next_column += 1
3619 if sort_name is not None:
3620 # add the sort column
3621 column = gtk.TreeViewColumn()
3622 column.set_visible(False)
3623 self.treeviewEpisodes.append_column( column)
3624 next_column += 1
3626 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
3627 # add string column type plus sort column type if it exists
3628 for name, sort_name, sort_type, caption in self.columns:
3629 column_types.append(gobject.TYPE_STRING)
3630 if sort_name is not None:
3631 column_types.append(sort_type)
3632 self.model = gtk.ListStore( *column_types)
3634 tooltip = None
3635 for index, episode in enumerate( self.episodes):
3636 if self.tooltip_attribute is not None:
3637 try:
3638 tooltip = getattr(episode, self.tooltip_attribute)
3639 except:
3640 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
3641 tooltip = None
3642 row = [ index, tooltip, self.selected[index] ]
3643 for name, sort_name, sort_type, caption in self.columns:
3644 if not hasattr(episode, name):
3645 log('Warning: Missing attribute "%s"', name, sender=self)
3646 row.append(None)
3647 else:
3648 row.append(getattr( episode, name))
3650 if sort_name is not None:
3651 if not hasattr(episode, sort_name):
3652 log('Warning: Missing attribute "%s"', sort_name, sender=self)
3653 row.append(None)
3654 else:
3655 row.append(getattr( episode, sort_name))
3656 self.model.append( row)
3658 if self.remove_callback is not None:
3659 self.btnRemoveAction.show()
3660 self.btnRemoveAction.set_label(self.remove_action)
3662 # connect to tooltip signals
3663 if self.tooltip_attribute is not None:
3664 try:
3665 self.treeviewEpisodes.set_property('has-tooltip', True)
3666 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
3667 except:
3668 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
3669 self.last_tooltip_episode = None
3670 self.episode_list_can_tooltip = True
3672 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
3673 self.treeviewEpisodes.set_rules_hint( True)
3674 self.treeviewEpisodes.set_model( self.model)
3675 self.treeviewEpisodes.columns_autosize()
3676 self.calculate_total_size()
3678 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
3679 # With get_bin_window, we get the window that contains the rows without
3680 # the header. The Y coordinate of this window will be the height of the
3681 # treeview header. This is the amount we have to subtract from the
3682 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3683 (x_bin, y_bin) = treeview.get_bin_window().get_position()
3684 y -= x_bin
3685 y -= y_bin
3686 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
3688 if not self.episode_list_can_tooltip:
3689 self.last_tooltip_episode = None
3690 return False
3692 if path is not None:
3693 model = treeview.get_model()
3694 iter = model.get_iter(path)
3695 index = model.get_value(iter, self.COLUMN_INDEX)
3696 description = model.get_value(iter, self.COLUMN_TOOLTIP)
3697 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
3698 self.last_tooltip_episode = None
3699 return False
3700 self.last_tooltip_episode = index
3702 if description is not None:
3703 tooltip.set_text(description)
3704 return True
3705 else:
3706 return False
3708 self.last_tooltip_episode = None
3709 return False
3711 def treeview_episodes_button_pressed(self, treeview, event):
3712 if event.button == 3:
3713 menu = gtk.Menu()
3715 if len(self.selection_buttons):
3716 for label in self.selection_buttons:
3717 item = gtk.MenuItem(label)
3718 item.connect('activate', self.custom_selection_button_clicked, label)
3719 menu.append(item)
3720 menu.append(gtk.SeparatorMenuItem())
3722 item = gtk.MenuItem(_('Select all'))
3723 item.connect('activate', self.on_btnCheckAll_clicked)
3724 menu.append(item)
3726 item = gtk.MenuItem(_('Select none'))
3727 item.connect('activate', self.on_btnCheckNone_clicked)
3728 menu.append(item)
3730 menu.show_all()
3731 # Disable tooltips while we are showing the menu, so
3732 # the tooltip will not appear over the menu
3733 self.episode_list_can_tooltip = False
3734 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
3735 menu.popup(None, None, None, event.button, event.time)
3737 return True
3739 def episode_list_allow_tooltips(self):
3740 self.episode_list_can_tooltip = True
3742 def calculate_total_size( self):
3743 if self.size_attribute is not None:
3744 (total_size, count) = (0, 0)
3745 for episode in self.get_selected_episodes():
3746 try:
3747 total_size += int(getattr( episode, self.size_attribute))
3748 count += 1
3749 except:
3750 log( 'Cannot get size for %s', episode.title, sender = self)
3752 text = []
3753 if count == 0:
3754 text.append(_('Nothing selected'))
3755 elif count == 1:
3756 text.append(_('One episode selected'))
3757 else:
3758 text.append(_('%d episodes selected') % count)
3759 if total_size > 0:
3760 text.append(_('total size: %s') % gl.format_filesize(total_size))
3761 self.labelTotalSize.set_text(', '.join(text))
3762 self.btnOK.set_sensitive(count>0)
3763 self.btnRemoveAction.set_sensitive(count>0)
3764 if count > 0:
3765 self.btnCancel.set_label(gtk.STOCK_CANCEL)
3766 else:
3767 self.btnCancel.set_label(gtk.STOCK_CLOSE)
3768 else:
3769 self.btnOK.set_sensitive(False)
3770 self.btnRemoveAction.set_sensitive(False)
3771 for index, row in enumerate(self.model):
3772 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3773 self.btnOK.set_sensitive(True)
3774 self.btnRemoveAction.set_sensitive(True)
3775 break
3776 self.labelTotalSize.set_text('')
3778 def toggle_cell_handler( self, cell, path):
3779 model = self.treeviewEpisodes.get_model()
3780 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3782 self.calculate_total_size()
3784 def custom_selection_button_clicked(self, button, label):
3785 callback = self.selection_buttons[label]
3787 for index, row in enumerate( self.model):
3788 new_value = callback( self.episodes[index])
3789 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3791 self.calculate_total_size()
3793 def on_btnCheckAll_clicked( self, widget):
3794 for row in self.model:
3795 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3797 self.calculate_total_size()
3799 def on_btnCheckNone_clicked( self, widget):
3800 for row in self.model:
3801 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3803 self.calculate_total_size()
3805 def on_remove_action_activate(self, widget):
3806 episodes = self.get_selected_episodes(remove_episodes=True)
3808 for episode in episodes:
3809 self.remove_callback(episode)
3811 if self.remove_finished is not None:
3812 self.remove_finished()
3813 self.calculate_total_size()
3815 def get_selected_episodes( self, remove_episodes=False):
3816 selected_episodes = []
3818 for index, row in enumerate( self.model):
3819 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
3820 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
3822 if remove_episodes:
3823 for episode in selected_episodes:
3824 index = self.episodes.index(episode)
3825 iter = self.model.get_iter_first()
3826 while iter is not None:
3827 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
3828 self.model.remove(iter)
3829 break
3830 iter = self.model.iter_next(iter)
3832 return selected_episodes
3834 def on_btnOK_clicked( self, widget):
3835 self.gPodderEpisodeSelector.destroy()
3836 if self.callback is not None:
3837 self.callback( self.get_selected_episodes())
3839 def on_btnCancel_clicked( self, widget):
3840 self.gPodderEpisodeSelector.destroy()
3841 if self.callback is not None:
3842 self.callback([])
3844 class gPodderConfigEditor(GladeWidget):
3845 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
3847 def new(self):
3848 name_column = gtk.TreeViewColumn(_('Setting'))
3849 name_renderer = gtk.CellRendererText()
3850 name_column.pack_start(name_renderer)
3851 name_column.add_attribute(name_renderer, 'text', 0)
3852 name_column.add_attribute(name_renderer, 'style', 5)
3853 self.configeditor.append_column(name_column)
3855 value_column = gtk.TreeViewColumn(_('Set to'))
3856 value_check_renderer = gtk.CellRendererToggle()
3857 value_column.pack_start(value_check_renderer, expand=False)
3858 value_column.add_attribute(value_check_renderer, 'active', 7)
3859 value_column.add_attribute(value_check_renderer, 'visible', 6)
3860 value_column.add_attribute(value_check_renderer, 'activatable', 6)
3861 value_check_renderer.connect('toggled', self.value_toggled)
3863 value_renderer = gtk.CellRendererText()
3864 value_column.pack_start(value_renderer)
3865 value_column.add_attribute(value_renderer, 'text', 2)
3866 value_column.add_attribute(value_renderer, 'visible', 4)
3867 value_column.add_attribute(value_renderer, 'editable', 4)
3868 value_column.add_attribute(value_renderer, 'style', 5)
3869 value_renderer.connect('edited', self.value_edited)
3870 self.configeditor.append_column(value_column)
3872 self.model = gl.config.model()
3873 self.filter = self.model.filter_new()
3874 self.filter.set_visible_func(self.visible_func)
3876 self.configeditor.set_model(self.filter)
3877 self.configeditor.set_rules_hint(True)
3879 def visible_func(self, model, iter, user_data=None):
3880 text = self.entryFilter.get_text().lower()
3881 if text == '':
3882 return True
3883 else:
3884 # either the variable name or its value
3885 return (text in model.get_value(iter, 0).lower() or
3886 text in model.get_value(iter, 2).lower())
3888 def value_edited(self, renderer, path, new_text):
3889 model = self.configeditor.get_model()
3890 iter = model.get_iter(path)
3891 name = model.get_value(iter, 0)
3892 type_cute = model.get_value(iter, 1)
3894 if not gl.config.update_field(name, new_text):
3895 self.notification(_('Cannot set value of <b>%s</b> to <i>%s</i>.\n\nNeeded data type: %s') % (saxutils.escape(name), saxutils.escape(new_text), saxutils.escape(type_cute)), _('Error updating %s') % saxutils.escape(name))
3897 def value_toggled(self, renderer, path):
3898 model = self.configeditor.get_model()
3899 iter = model.get_iter(path)
3900 field_name = model.get_value(iter, 0)
3901 field_type = model.get_value(iter, 3)
3903 # Flip the boolean config flag
3904 if field_type == bool:
3905 gl.config.toggle_flag(field_name)
3907 def on_entryFilter_changed(self, widget):
3908 self.filter.refilter()
3910 def on_btnShowAll_clicked(self, widget):
3911 self.entryFilter.set_text('')
3912 self.entryFilter.grab_focus()
3914 def on_btnClose_clicked(self, widget):
3915 self.gPodderConfigEditor.destroy()
3917 class gPodderPlaylist(GladeWidget):
3918 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
3920 def new(self):
3921 self.m3u_header = '#EXTM3U\n'
3922 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
3923 if self.mountpoint == '/':
3924 self.mountpoint = gl.config.mp3_player_folder
3925 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
3926 self.playlist_file = os.path.join(self.mountpoint,
3927 gl.config.mp3_player_playlist_file)
3928 icon_theme = gtk.icon_theme_get_default()
3929 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
3931 # add column two
3932 check_cell = gtk.CellRendererToggle()
3933 check_cell.set_property('activatable', True)
3934 check_cell.connect('toggled', self.cell_toggled)
3935 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
3936 self.treeviewPlaylist.append_column(check_column)
3938 # add column three
3939 column = gtk.TreeViewColumn(_('Filename'))
3940 icon_cell = gtk.CellRendererPixbuf()
3941 column.pack_start(icon_cell, False)
3942 column.add_attribute(icon_cell, 'pixbuf', 0)
3943 filename_cell = gtk.CellRendererText()
3944 column.pack_start(filename_cell, True)
3945 column.add_attribute(filename_cell, 'text', 2)
3947 column.set_resizable(True)
3948 self.treeviewPlaylist.append_column(column)
3950 # Make treeview reorderable
3951 self.treeviewPlaylist.set_reorderable(True)
3953 # init liststore
3954 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
3955 self.treeviewPlaylist.set_model(self.playlist)
3957 # read device and playlist and fill the TreeView
3958 self.m3u = self.read_m3u()
3959 self.device = self.read_device()
3960 self.write2gui()
3962 def cell_toggled(self, cellrenderertoggle, path):
3963 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
3964 it = liststore.get_iter(path)
3965 liststore.set_value(it, 1, not liststore.get_value(it, 1))
3967 def on_btnCancelPlaylist_clicked(self, widget):
3968 self.gPodderPlaylist.destroy()
3970 def on_btnSavePlaylist_clicked(self, widget):
3971 self.write_m3u()
3972 self.gPodderPlaylist.destroy()
3974 def read_m3u(self):
3976 read all files from the existing playlist
3978 tracks = []
3979 if os.path.exists(self.playlist_file):
3980 for line in open(self.playlist_file, 'r'):
3981 if line != self.m3u_header:
3982 if line.startswith('#'):
3983 tracks.append([False, line[1:].strip()])
3984 else:
3985 tracks.append([True, line.strip()])
3986 return tracks
3988 def write_m3u(self):
3990 write the list into the playlist on the device
3992 playlist_folder = os.path.split(self.playlist_file)[0]
3993 if not util.make_directory(playlist_folder):
3994 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
3995 else:
3996 try:
3997 fp = open(self.playlist_file, 'w')
3998 fp.write(self.m3u_header)
3999 for icon, checked, filename in self.playlist:
4000 if not checked:
4001 fp.write('#')
4002 fp.write(filename)
4003 fp.write('\n')
4004 fp.close()
4005 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4006 except IOError, ioe:
4007 self.show_message(str(ioe), _('Error writing playlist file'))
4009 def read_device(self):
4011 read all files from the device
4013 tracks = []
4014 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4015 for file in files:
4016 filename = os.path.join(root, file)
4018 if filename == self.playlist_file:
4019 # We don't want to have our playlist file as
4020 # an entry in our file list, so skip it!
4021 break
4023 if not gl.config.mp3_player_playlist_absolute_path:
4024 filename = filename[len(self.mountpoint):]
4026 if gl.config.mp3_player_playlist_win_path:
4027 filename = filename.replace( '/', '\\')
4029 tracks.append(filename)
4030 return tracks
4032 def write2gui(self):
4033 # add the files from the device to the list only when
4034 # they are not yet in the playlist
4035 # mark this files as NEW
4036 for filename in self.device[:]:
4037 m3ulist = [file[1] for file in self.m3u]
4038 if filename not in m3ulist:
4039 self.playlist.append([self.icon_new, False, filename])
4041 # add the files from the playlist to the list only when
4042 # they are on the device
4043 for checked, filename in self.m3u[:]:
4044 if filename in self.device:
4045 self.playlist.append([None, checked, filename])
4047 class gPodderDependencyManager(GladeWidget):
4048 def new(self):
4049 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4050 self.treeview_components.append_column(col_name)
4051 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4052 self.treeview_components.append_column(col_installed)
4053 self.treeview_components.set_model(services.dependency_manager.get_model())
4054 self.btn_about.set_sensitive(False)
4056 def on_btn_about_clicked(self, widget):
4057 selection = self.treeview_components.get_selection()
4058 model, iter = selection.get_selected()
4059 if iter is not None:
4060 title = model.get_value(iter, 0)
4061 description = model.get_value(iter, 1)
4062 available = model.get_value(iter, 3)
4063 missing = model.get_value(iter, 4)
4065 if not available:
4066 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4068 self.show_message(description, title)
4070 def on_btn_install_clicked(self, widget):
4071 # TODO: Implement package manager integration
4072 pass
4074 def on_treeview_components_cursor_changed(self, treeview):
4075 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4076 # TODO: If installing is possible, enable btn_install
4078 def on_gPodderDependencyManager_response(self, dialog, response_id):
4079 self.gPodderDependencyManager.destroy()
4081 class gPodderWelcome(GladeWidget):
4082 def new(self):
4083 pass
4085 def on_show_example_podcasts(self, button):
4086 self.gPodderWelcome.destroy()
4087 self.show_example_podcasts_callback(None)
4089 def on_setup_my_gpodder(self, gpodder):
4090 self.gPodderWelcome.destroy()
4091 self.setup_my_gpodder_callback(None)
4093 def on_btnCancel_clicked(self, button):
4094 self.gPodderWelcome.destroy()
4096 def main():
4097 gobject.threads_init()
4098 gtk.window_set_default_icon_name( 'gpodder')
4100 gPodder().run()