fix #374 export to local folder filename customizable again
with a checkbox to export all remaining episodes to same folder. This restores the ability to choose the filename for each episode, while keeping export of a large number of episodes convenient (click export remaining...)
This commit is contained in:
parent
fe0b5c2052
commit
cdbeb04eb5
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.0"/>
|
||||
<object class="GtkFileChooserDialog" id="gPodderExportToLocalFolder">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Select destination</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="action">save</property>
|
||||
<property name="do_overwrite_confirmation">True</property>
|
||||
<property name="preview_widget_active">False</property>
|
||||
<property name="use_preview_label">False</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnOK">
|
||||
<property name="label">gtk-save</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="has_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnCancel">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="allsamefolder">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<!-- to be recognized by the embedded GtkFileChooser -->
|
||||
<action-widgets>
|
||||
<action-widget response="-3">btnOK</action-widget>
|
||||
<action-widget response="-6">btnCancel</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
</interface>
|
|
@ -125,6 +125,11 @@ defaults = {
|
|||
'height': 400,
|
||||
'x': -1, 'y': -1, 'maximized': False,
|
||||
},
|
||||
'export_to_local_folder': {
|
||||
'width': 500,
|
||||
'height': 400,
|
||||
'x': -1, 'y': -1, 'maximized': False,
|
||||
}
|
||||
},
|
||||
|
||||
'toolbar': False,
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2018 The gPodder Team
|
||||
#
|
||||
# gPodder is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gPodder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
|
||||
from gi.repository import Gtk, Pango
|
||||
|
||||
import gpodder
|
||||
from gpodder import util
|
||||
from gpodder.gtkui.interface.common import BuilderWidget
|
||||
|
||||
_ = gpodder.gettext
|
||||
N_ = gpodder.ngettext
|
||||
|
||||
|
||||
class gPodderExportToLocalFolder(BuilderWidget):
|
||||
""" Export to Local Folder UI: file dialog + checkbox to save all to same folder """
|
||||
def new(self):
|
||||
self._config.connect_gtk_window(self.gPodderExportToLocalFolder,
|
||||
'export_to_local_folder', True)
|
||||
self._ok = False
|
||||
|
||||
def on_btnOK_clicked(self, widget):
|
||||
self._ok = True
|
||||
self.gPodderExportToLocalFolder.hide()
|
||||
|
||||
def on_btnCancel_clicked(self, widget):
|
||||
self.gPodderExportToLocalFolder.hide()
|
||||
|
||||
def save_as(self, initial_directory, filename, remaining=0):
|
||||
"""
|
||||
blocking method: prompt for save to local folder
|
||||
:param str initial_directory: folder to show to user or None
|
||||
:param str filename: default export filename
|
||||
:param int remaining: remaining episodes (to show/hide and customize checkbox label)
|
||||
:return (bool, str, str, bool): notCancelled, selected folder, selected path,
|
||||
save all remaining episodes with default params
|
||||
"""
|
||||
if remaining:
|
||||
self.allsamefolder.set_label(
|
||||
N_('Export remaining %(count)d episode to this folder with its default name',
|
||||
'Export remaining %(count)d episodes to this folder with their default name',
|
||||
remaining) % {'count': remaining})
|
||||
else:
|
||||
self.allsamefolder.hide()
|
||||
if initial_directory is None:
|
||||
initial_directory = os.path.expanduser('~')
|
||||
self.gPodderExportToLocalFolder.set_current_folder(initial_directory)
|
||||
self.gPodderExportToLocalFolder.set_current_name(filename)
|
||||
self._ok = False
|
||||
self.gPodderExportToLocalFolder.run()
|
||||
notCancelled = self._ok
|
||||
allRemainingDefault = self.allsamefolder.get_active()
|
||||
if notCancelled:
|
||||
folder = self.gPodderExportToLocalFolder.get_current_folder()
|
||||
filename = self.gPodderExportToLocalFolder.get_filename()
|
||||
else:
|
||||
folder = None
|
||||
filename = None
|
||||
return (notCancelled, folder, filename, allRemainingDefault)
|
|
@ -48,6 +48,7 @@ from gpodder.gtkui import shownotes
|
|||
from gpodder.gtkui.config import UIConfig
|
||||
from gpodder.gtkui.desktop.channel import gPodderChannel
|
||||
from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
|
||||
from gpodder.gtkui.desktop.exportlocal import gPodderExportToLocalFolder
|
||||
from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
|
||||
from gpodder.gtkui.desktop.preferences import gPodderPreferences
|
||||
from gpodder.gtkui.desktop.sync import gPodderSyncUI
|
||||
|
@ -1787,30 +1788,56 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
return filename
|
||||
|
||||
def save_episodes_as_file(self, episodes):
|
||||
def do_save_episode(copy_from, copy_to):
|
||||
try:
|
||||
shutil.copyfile(copy_from, copy_to)
|
||||
except (OSError, IOError) as e:
|
||||
logger.warn('Error copying from %s to %s: %r', copy_from, copy_to, e, exc_info=True)
|
||||
folder, filename = os.path.split(copy_to)
|
||||
# Remove characters not supported by VFAT (#282)
|
||||
new_filename = re.sub(r"[\"*/:<>?\\|]", "_", filename)
|
||||
destination = os.path.join(folder, new_filename)
|
||||
if (copy_to != destination):
|
||||
shutil.copyfile(copy_from, destination)
|
||||
else:
|
||||
raise
|
||||
|
||||
PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
|
||||
folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
|
||||
(notCancelled, folder) = self.show_folder_select_dialog(initial_directory=folder)
|
||||
setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
|
||||
allRemainingDefault = False
|
||||
remaining = len(episodes)
|
||||
dialog = gPodderExportToLocalFolder(self.main_window,
|
||||
_config=self.config)
|
||||
for episode in episodes:
|
||||
remaining -= 1
|
||||
if episode.was_downloaded(and_exists=True):
|
||||
copy_from = episode.local_filename(create=False)
|
||||
assert copy_from is not None
|
||||
|
||||
if notCancelled:
|
||||
for episode in episodes:
|
||||
if episode.was_downloaded(and_exists=True):
|
||||
copy_from = episode.local_filename(create=False)
|
||||
assert copy_from is not None
|
||||
base, extension = os.path.splitext(copy_from)
|
||||
filename = self.build_filename(episode.sync_filename(), extension)
|
||||
|
||||
base, extension = os.path.splitext(copy_from)
|
||||
filename = self.build_filename(episode.sync_filename(), extension)
|
||||
copy_to = os.path.join(folder, filename)
|
||||
try:
|
||||
shutil.copyfile(copy_from, copy_to)
|
||||
except (OSError, IOError) as e:
|
||||
# Remove characters not supported by VFAT (#282)
|
||||
new_filename = re.sub(r"[\"*/:<>?\\|]", "_", filename)
|
||||
destination = os.path.join(folder, new_filename)
|
||||
if (copy_to != destination):
|
||||
shutil.copyfile(copy_from, destination)
|
||||
try:
|
||||
if allRemainingDefault:
|
||||
do_save_episode(copy_from, os.path.join(folder, filename))
|
||||
else:
|
||||
(notCancelled, folder, dest_path, allRemainingDefault) = dialog.save_as(folder, filename, remaining)
|
||||
if notCancelled:
|
||||
do_save_episode(copy_from, dest_path)
|
||||
else:
|
||||
raise
|
||||
break
|
||||
except (OSError, IOError) as e:
|
||||
if remaining:
|
||||
msg = _('Error saving to local folder: %(error)r.\n'
|
||||
'Would you like to continue?') % dict(error=e)
|
||||
if not self.show_confirmation(msg, _('Error saving to local folder')):
|
||||
logger.warn("Save to Local Folder cancelled following error")
|
||||
break
|
||||
else:
|
||||
self.notification(_('Error saving to local folder: %(error)r') % dict(error=e),
|
||||
_('Error saving to local folder'), important=True)
|
||||
|
||||
setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
|
||||
|
||||
def copy_episodes_bluetooth(self, episodes):
|
||||
episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
|
||||
|
|
Loading…
Reference in New Issue