Trash dialog: Allow right clicking on an entry to save it to disk. Fixes #2030342 [Trash Bin: Option to copy file out](https://bugs.launchpad.net/calibre/+bug/2030342)

This commit is contained in:
Kovid Goyal 2023-08-16 18:18:45 +05:30
parent c68ba38533
commit 05464cc4f9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 54 additions and 6 deletions

View File

@ -2098,6 +2098,12 @@ class DB:
dest = os.path.abspath(os.path.join(self.library_path, path)) dest = os.path.abspath(os.path.join(self.library_path, path))
copy_tree(bdir, dest, delete_source=True) copy_tree(bdir, dest, delete_source=True)
def copy_book_from_trash(self, book_id, dest):
bdir = os.path.join(self.trash_dir, 'b', str(book_id))
if not os.path.isdir(bdir):
raise ValueError(f'The book {book_id} not present in the trash folder')
copy_tree(bdir, dest, delete_source=False)
def path_for_trash_format(self, book_id, fmt): def path_for_trash_format(self, book_id, fmt):
bdir = os.path.join(self.trash_dir, 'f', str(book_id)) bdir = os.path.join(self.trash_dir, 'f', str(book_id))
if not os.path.isdir(bdir): if not os.path.isdir(bdir):

View File

@ -2702,6 +2702,14 @@ class Cache:
e.cover_path = self.format_abspath(e.book_id, '__COVER_INTERNAL__') e.cover_path = self.format_abspath(e.book_id, '__COVER_INTERNAL__')
return books, formats return books, formats
@read_api
def copy_format_from_trash(self, book_id, fmt, dest):
fmt = fmt.upper()
fpath = self.backend.path_for_trash_format(book_id, fmt)
if not fpath:
raise ValueError(f'No format {fmt} found in book {book_id}')
shutil.copyfile(fpath, dest)
@write_api @write_api
def move_format_from_trash(self, book_id, fmt): def move_format_from_trash(self, book_id, fmt):
''' Undelete a format from the trash directory ''' ''' Undelete a format from the trash directory '''
@ -2722,6 +2730,10 @@ class Cache:
self.event_dispatcher(EventType.format_added, book_id, fmt) self.event_dispatcher(EventType.format_added, book_id, fmt)
self.backend.remove_trash_formats_dir_if_empty(book_id) self.backend.remove_trash_formats_dir_if_empty(book_id)
@read_api
def copy_book_from_trash(self, book_id, dest: str):
self.backend.copy_book_from_trash(book_id, dest)
@write_api @write_api
def move_book_from_trash(self, book_id): def move_book_from_trash(self, book_id):
''' Undelete a book from the trash directory ''' ''' Undelete a book from the trash directory '''

View File

@ -6,14 +6,14 @@ import traceback
from operator import attrgetter from operator import attrgetter
from qt.core import ( from qt.core import (
QAbstractItemView, QDialogButtonBox, QHBoxLayout, QIcon, QLabel, QListWidget, QAbstractItemView, QDialogButtonBox, QHBoxLayout, QIcon, QLabel, QListWidget,
QListWidgetItem, QPainter, QPalette, QPixmap, QRectF, QSize, QSpinBox, QStyle, QListWidgetItem, QMenu, QPainter, QPalette, QPixmap, QRectF, QSize, QSpinBox,
QStyledItemDelegate, Qt, QTabWidget, QVBoxLayout, pyqtSignal, QStyle, QStyledItemDelegate, Qt, QTabWidget, QVBoxLayout, pyqtSignal,
) )
from typing import Iterator, List from typing import Iterator, List
from calibre import fit_image from calibre import fit_image
from calibre.db.constants import DEFAULT_TRASH_EXPIRY_TIME_SECONDS, TrashEntry from calibre.db.constants import DEFAULT_TRASH_EXPIRY_TIME_SECONDS, TrashEntry
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog, choose_dir, choose_save_file
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.widgets import BusyCursor from calibre.gui2.widgets import BusyCursor
from calibre.gui2.widgets2 import Dialog from calibre.gui2.widgets2 import Dialog
@ -84,8 +84,9 @@ class TrashList(QListWidget):
restore_item = pyqtSignal(object, object) restore_item = pyqtSignal(object, object)
def __init__(self, entries: List[TrashEntry], parent: 'TrashView'): def __init__(self, entries: List[TrashEntry], parent: 'TrashView', is_books: bool):
super().__init__(parent) super().__init__(parent)
self.is_books = is_books
self.db = parent.db self.db = parent.db
self.delegate = TrashItemDelegate(self) self.delegate = TrashItemDelegate(self)
self.setItemDelegate(self.delegate) self.setItemDelegate(self.delegate)
@ -95,6 +96,8 @@ class TrashList(QListWidget):
i.setData(Qt.ItemDataRole.UserRole, entry) i.setData(Qt.ItemDataRole.UserRole, entry)
self.addItem(i) self.addItem(i)
self.itemDoubleClicked.connect(self.double_clicked) self.itemDoubleClicked.connect(self.double_clicked)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
@property @property
def selected_entries(self) -> Iterator[TrashEntry]: def selected_entries(self) -> Iterator[TrashEntry]:
@ -104,6 +107,33 @@ class TrashList(QListWidget):
def double_clicked(self, item): def double_clicked(self, item):
self.restore_item.emit(self, item) self.restore_item.emit(self, item)
def show_context_menu(self, pos):
item = self.itemAt(pos)
if item is None:
return
m = QMenu(self)
entry = item.data(Qt.ItemDataRole.UserRole)
m.addAction(QIcon.ic('save.png'), _('Save "{}" to disk').format(entry.title)).triggered.connect(self.save_current_item)
m.exec(self.mapToGlobal(pos))
def save_current_item(self):
item = self.currentItem()
if item is not None:
self.save_entry(item.data(Qt.ItemDataRole.UserRole))
def save_entry(self, entry: TrashEntry):
if self.is_books:
dest = choose_dir(self, 'save-trash-book', _('Choose a location to save: {}').format(entry.title))
if not dest:
return
self.db.copy_book_from_trash(entry.book_id, dest)
else:
for fmt in entry.formats:
dest = choose_save_file(self, 'save-trash-format', _('Choose a location to save: {}').format(
entry.title +'.' + fmt.lower()), initial_filename=entry.title + '.' + fmt.lower())
if dest:
self.db.copy_format_from_trash(entry.book_id, fmt, dest)
class TrashView(Dialog): class TrashView(Dialog):
@ -122,9 +152,9 @@ class TrashView(Dialog):
with BusyCursor(): with BusyCursor():
books, formats = self.db.list_trash_entries() books, formats = self.db.list_trash_entries()
self.books = TrashList(books, self) self.books = TrashList(books, self, True)
self.books.restore_item.connect(self.restore_item) self.books.restore_item.connect(self.restore_item)
self.formats = TrashList(formats, self) self.formats = TrashList(formats, self, False)
self.formats.restore_item.connect(self.restore_item) self.formats.restore_item.connect(self.restore_item)
self.tabs = t = QTabWidget(self) self.tabs = t = QTabWidget(self)