mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
c68ba38533
commit
05464cc4f9
@ -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):
|
||||||
|
@ -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 '''
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user