From 5470d311a3adaeb84d8bf1681ee66dcbb6e703df Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 13 Apr 2023 13:31:38 +0530 Subject: [PATCH] Implement undo popup for book format deletion --- src/calibre/db/backend.py | 4 ++++ src/calibre/db/cache.py | 5 ++++- src/calibre/gui2/actions/delete.py | 36 ++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index 5514010db1..0db99b22af 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -1545,14 +1545,18 @@ class DB: def remove_formats(self, remove_map, metadata_map): self.ensure_trash_dir() + removed_map = {} for book_id, removals in iteritems(remove_map): paths = set() + removed_map[book_id] = set() for fmt, fname, path in removals: path = self.format_abspath(book_id, fmt, fname, path) if path: paths.add(path) + removed_map[book_id].add(fmt.upper()) if paths: self.move_book_files_to_trash(book_id, paths, metadata_map[book_id]) + return removed_map def cover_last_modified(self, path): path = os.path.abspath(os.path.join(self.library_path, path, COVER_FILE_NAME)) diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 9457cd2d0d..545241be72 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1848,9 +1848,11 @@ class Cache: :param formats_map: A mapping of book_id to a list of formats to be removed from the book. :param db_only: If True, only remove the record for the format from the db, do not delete the actual format file from the filesystem. + :return: A map of book id to set of formats actually deleted from the filesystem for that book ''' table = self.fields['formats'].table formats_map = {book_id:frozenset((f or '').upper() for f in fmts) for book_id, fmts in iteritems(formats_map)} + removed_map = {} for book_id, fmts in iteritems(formats_map): for fmt in fmts: @@ -1874,7 +1876,7 @@ class Cache: if removes[book_id]: metadata_map[book_id] = {'title': self._field_for('title', book_id), 'authors': self._field_for('authors', book_id)} if removes: - self.backend.remove_formats(removes, metadata_map) + removed_map = self.backend.remove_formats(removes, metadata_map) size_map = table.remove_formats(formats_map, self.backend) self.fields['size'].table.update_sizes(size_map) @@ -1884,6 +1886,7 @@ class Cache: self._update_last_modified(tuple(formats_map)) self.event_dispatcher(EventType.formats_removed, formats_map) + return removed_map @read_api def get_next_series_num_for(self, series, field='series', current_indices=False): diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 26b4f5856f..7989a75ad5 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -166,7 +166,7 @@ class DeleteAction(InterfaceAction): return set(map(self.gui.library_view.model().id, rows)) def _remove_formats_from_ids(self, fmts, ids): - self.gui.library_view.model().db.new_api.remove_formats({bid: fmts for bid in ids}) + self.show_undo_for_deleted_formats(self.gui.library_view.model().db.new_api.remove_formats({bid: fmts for bid in ids})) self.gui.library_view.model().refresh_ids(ids) self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) @@ -174,7 +174,7 @@ class DeleteAction(InterfaceAction): def remove_format_by_id(self, book_id, fmt): title = self.gui.current_db.title(book_id, index_is_id=True) if not confirm('

'+(_( - 'The %(fmt)s format will be permanently deleted from ' + 'The %(fmt)s format will be deleted from ' '%(title)s. Are you sure?')%dict(fmt=fmt, title=title)) + '

', 'library_delete_specific_format', self.gui): return @@ -193,8 +193,8 @@ class DeleteAction(InterfaceAction): return error_dialog(self.gui, _('Format not found'), _('The {} format is not present in the selected books.').format(fmt), show=True) if not confirm( '

'+ ngettext( - _('The {fmt} format will be permanently deleted from {title}.'), - _('The {fmt} format will be permanently deleted from all {num} selected books.'), + _('The {fmt} format will be deleted from {title}.'), + _('The {fmt} format will be deleted from all {num} selected books.'), len(ids)).format(fmt=fmt.upper(), num=len(ids), title=self.gui.current_db.title(next(iter(ids)), index_is_id=True) ) + ' ' + _('Are you sure?'), 'library_delete_specific_format_from_selected', self.gui ): @@ -217,7 +217,7 @@ class DeleteAction(InterfaceAction): if not fmts: return m = self.gui.library_view.model() - m.db.new_api.remove_formats({book_id:fmts for book_id in ids}) + self.show_undo_for_deleted_formats(m.db.new_api.remove_formats({book_id:fmts for book_id in ids})) m.refresh_ids(ids) m.current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) @@ -247,7 +247,7 @@ class DeleteAction(InterfaceAction): # formats removals[id] = rfmts if removals: - m.db.new_api.remove_formats(removals) + self.show_undo_for_deleted_formats(m.db.new_api.remove_formats(removals)) m.refresh_ids(ids) m.current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) @@ -270,7 +270,7 @@ class DeleteAction(InterfaceAction): if fmts: removals[id] = fmts.split(',') if removals: - db.new_api.remove_formats(removals) + self.show_undo_for_deleted_formats(db.new_api.remove_formats(removals)) self.gui.library_view.model().refresh_ids(ids) self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) @@ -367,9 +367,17 @@ class DeleteAction(InterfaceAction): if not hasattr(self, 'message_popup'): self.message_popup = MessagePopup(self.gui) self.message_popup.undo_requested.connect(self.undelete) - self.message_popup(ngettext('One book deleted.', '{} books deleted.', len(ids_deleted)).format(len(ids_deleted)), + self.message_popup(ngettext('One book deleted from library.', '{} books deleted from library.', len(ids_deleted)).format(len(ids_deleted)), show_undo=(self.gui.current_db.new_api.library_id, ids_deleted)) + def show_undo_for_deleted_formats(self, removed_map): + if not hasattr(self, 'message_popup'): + self.message_popup = MessagePopup(self.gui) + self.message_popup.undo_requested.connect(self.undelete) + num = sum(map(len, removed_map.values())) + self.message_popup(ngettext('One book format deleted.', '{} book formats deleted.', num).format(num), + show_undo=(self.gui.current_db.new_api.library_id, removed_map)) + def library_changed(self, db): if hasattr(self, 'message_popup'): self.message_popup.hide() @@ -377,7 +385,17 @@ class DeleteAction(InterfaceAction): def undelete(self, what): library_id, book_ids = what db = self.gui.current_db.new_api - if library_id == db.library_id: + if library_id != db.library_id: + return + current_idx = self.gui.library_view.currentIndex() + if isinstance(book_ids, dict): + with BusyCursor(): + for book_id, fmts in book_ids.items(): + for fmt in fmts: + db.move_format_from_trash(book_id, fmt) + if current_idx.isValid(): + self.gui.library_view.model().current_changed(current_idx, current_idx) + else: with BusyCursor(): for book_id in book_ids: db.move_book_from_trash(book_id)