diff --git a/src/calibre/devices/hanlin/driver.py b/src/calibre/devices/hanlin/driver.py index adb4b353f3..0d972afc76 100644 --- a/src/calibre/devices/hanlin/driver.py +++ b/src/calibre/devices/hanlin/driver.py @@ -81,9 +81,6 @@ class HANLINV3(USBMS): return drives - - - class HANLINV5(HANLINV3): name = 'Hanlin V5 driver' gui_name = 'Hanlin V5' @@ -120,8 +117,22 @@ class BOOX(HANLINV3): MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory' STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card' - EBOOK_DIR_MAIN = 'MyBooks' - EBOOK_DIR_CARD_A = 'MyBooks' + EBOOK_DIR_MAIN = ['MyBooks'] + EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' + 'send e-books to on the device. The first one that exists will ' + 'be used.') + EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) + + # EBOOK_DIR_CARD_A = 'MyBooks' ## Am quite sure we need this. + + def post_open_callback(self): + opts = self.settings() + dirs = opts.extra_customization + if not dirs: + dirs = self.EBOOK_DIR_MAIN + else: + dirs = [x.strip() for x in dirs.split(',')] + self.EBOOK_DIR_MAIN = dirs def windows_sort_drives(self, drives): return drives diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py index 6da04a41be..38ba922274 100644 --- a/src/calibre/gui2/actions.py +++ b/src/calibre/gui2/actions.py @@ -28,6 +28,7 @@ from calibre.constants import preferred_encoding, filesystem_encoding, \ from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog class AnnotationsAction(object): # {{{ @@ -471,6 +472,45 @@ class DeleteAction(object): # {{{ if ids: self.tags_view.recount() + def remove_matching_books_from_device(self, *args): + if not self.device_manager.is_device_connected: + d = error_dialog(self, _('Cannot delete books'), + _('No device is connected')) + d.exec_() + return + ids = self._get_selected_ids() + if not ids: + #For some reason the delete dialog reports no selection, so + #we need to do it here + return + to_delete = {} + some_to_delete = False + for model,name in ((self.memory_view.model(), _('Main memory')), + (self.card_a_view.model(), _('Storage Card A')), + (self.card_b_view.model(), _('Storage Card B'))): + to_delete[name] = (model, model.paths_for_db_ids(ids)) + if len(to_delete[name][1]) > 0: + some_to_delete = True + if not some_to_delete: + d = error_dialog(self, _('No books to delete'), + _('None of the selected books are on the device')) + d.exec_() + return + d = DeleteMatchingFromDeviceDialog(self, to_delete) + if d.exec_(): + paths = {} + ids = {} + for (model, id, path) in d.result: + if model not in paths: + paths[model] = [] + ids[model] = [] + paths[model].append(path) + ids[model].append(id) + for model in paths: + job = self.remove_paths(paths[model]) + self.delete_memory[job] = (paths[model], model) + model.mark_for_deletion(job, ids[model], rows_are_ids=True) + self.status_bar.show_message(_('Deleting books from device.'), 1000) def delete_covers(self, *args): ids = self._get_selected_ids() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index b3a7196b20..d00dd2782c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1347,7 +1347,7 @@ class DeviceMixin(object): # {{{ if reset: # First build a cache of the library, so the search isn't On**2 self.db_book_title_cache = {} - self.db_book_uuid_cache = set() + self.db_book_uuid_cache = {} db = self.library_view.model().db for id in db.data.iterallids(): mi = db.get_metadata(id, index_is_id=True) @@ -1364,7 +1364,7 @@ class DeviceMixin(object): # {{{ aus = re.sub('(?u)\W|[_]', '', aus) self.db_book_title_cache[title]['author_sort'][aus] = mi self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi - self.db_book_uuid_cache.add(mi.uuid) + self.db_book_uuid_cache[mi.uuid] = mi.application_id # Now iterate through all the books on the device, setting the # in_library field Fastest and most accurate key is the uuid. Second is @@ -1376,11 +1376,13 @@ class DeviceMixin(object): # {{{ for book in booklist: if getattr(book, 'uuid', None) in self.db_book_uuid_cache: book.in_library = True + # ensure that the correct application_id is set + book.application_id = self.db_book_uuid_cache[book.uuid] continue book_title = book.title.lower() if book.title else '' book_title = re.sub('(?u)\W|[_]', '', book_title) - book.in_library = False + book.in_library = None d = self.db_book_title_cache.get(book_title, None) if d is not None: if getattr(book, 'application_id', None) in d['db_ids']: diff --git a/src/calibre/gui2/dialogs/delete_matching_from_device.py b/src/calibre/gui2/dialogs/delete_matching_from_device.py new file mode 100644 index 0000000000..63ee9c4012 --- /dev/null +++ b/src/calibre/gui2/dialogs/delete_matching_from_device.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + +from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView, QIcon + +from calibre.ebooks.metadata import authors_to_string +from calibre.gui2.dialogs.delete_matching_from_device_ui import Ui_DeleteMatchingFromDeviceDialog + +class tableItem(QTableWidgetItem): + + def __init__(self, text): + QTableWidgetItem.__init__(self, text) + self.setFlags(Qt.ItemIsEnabled) + + def __ge__(self, other): + return unicode(self.text()).lower() >= unicode(other.text()).lower() + + def __lt__(self, other): + return unicode(self.text()).lower() < unicode(other.text()).lower() + +class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog): + + def __init__(self, parent, items): + QDialog.__init__(self, parent) + Ui_DeleteMatchingFromDeviceDialog.__init__(self) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.accepted) + self.table.cellClicked.connect(self.cell_clicked) + self.table.setSelectionMode(QAbstractItemView.NoSelection) + self.table.setColumnCount(5) + self.table.setHorizontalHeaderLabels(['', _('Location'), _('Title'), + _('Author'), _('Format')]) + del_icon = QIcon(I('list_remove.svg')) + rows = 0 + for card in items: + rows += len(items[card][1]) + self.table.setRowCount(rows) + row = 0 + for card in items: + (model,books) = items[card] + for (id,book) in books: + item = QTableWidgetItem(del_icon, '') + item.setData(Qt.UserRole, (model, id, book.path)) + self.table.setItem(row, 0, item) + self.table.setItem(row, 1, tableItem(card)) + self.table.setItem(row, 2, tableItem(book.title)) + self.table.setItem(row, 3, tableItem(authors_to_string(book.authors))) + self.table.setItem(row, 4, tableItem(book.path.rpartition('.')[2])) + row += 1 + self.table.resizeColumnsToContents() + self.table.setSortingEnabled(True) + self.table.sortByColumn(2, Qt.AscendingOrder) + + def accepted(self): + self.result = [] + for row in range(self.table.rowCount()): + (model, id, path) = self.table.item(row, 0).data(Qt.UserRole).toPyObject() + path = unicode(path) + self.result.append((model, id, path)) + return + + def cell_clicked(self, row, col): + if col == 0: + self.table.removeRow(row) \ No newline at end of file diff --git a/src/calibre/gui2/dialogs/delete_matching_from_device.ui b/src/calibre/gui2/dialogs/delete_matching_from_device.ui new file mode 100644 index 0000000000..eabd4d6346 --- /dev/null +++ b/src/calibre/gui2/dialogs/delete_matching_from_device.ui @@ -0,0 +1,86 @@ + + + DeleteMatchingFromDeviceDialog + + + + 0 + 0 + 730 + 342 + + + + + 0 + 0 + + + + Delete from device + + + + + + + 0 + 0 + + + + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + DeleteMatchingFromDeviceDialog + accept() + + + 229 + 211 + + + 157 + 234 + + + + + buttonBox + rejected() + DeleteMatchingFromDeviceDialog + reject() + + + 297 + 217 + + + 286 + 234 + + + + + diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 0a82d3b75b..b40253fad2 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -131,6 +131,9 @@ class ToolbarMixin(object): # {{{ self.delete_all_but_selected_formats) self.delete_menu.addAction( _('Remove covers from selected books'), self.delete_covers) + self.delete_menu.addAction( + _('Remove matching books from device'), + self.remove_matching_books_from_device) self.action_del.setMenu(self.delete_menu) self.action_open_containing_folder.setShortcut(Qt.Key_O) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 8080769377..787b77251f 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -769,6 +769,7 @@ class OnDeviceSearch(SearchQueryParser): # {{{ 'format', 'formats', 'title', + 'inlibrary' ] @@ -807,12 +808,21 @@ class OnDeviceSearch(SearchQueryParser): # {{{ 'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(), 'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(), 'format':lambda x: os.path.splitext(x.path)[1].lower(), + 'inlibrary':lambda x : getattr(x, 'in_library') } for x in ('author', 'format'): q[x+'s'] = q[x] for index, row in enumerate(self.model.db): for locvalue in locations: accessor = q[locvalue] + if query == 'true': + if accessor(row) is not None: + matches.add(index) + continue + if query == 'false': + if accessor(row) is None: + matches.add(index) + continue try: ### Can't separate authors because comma is used for name sep and author sep ### Exact match might not get what you want. For that reason, turn author @@ -862,11 +872,15 @@ class DeviceBooksModel(BooksModel): # {{{ self.editable = True self.book_in_library = None - def mark_for_deletion(self, job, rows): - self.marked_for_deletion[job] = self.indices(rows) - for row in rows: - indices = self.row_indices(row) - self.dataChanged.emit(indices[0], indices[-1]) + def mark_for_deletion(self, job, rows, rows_are_ids=False): + if rows_are_ids: + self.marked_for_deletion[job] = rows + self.reset() + else: + self.marked_for_deletion[job] = self.indices(rows) + for row in rows: + indices = self.row_indices(row) + self.dataChanged.emit(indices[0], indices[-1]) def deletion_done(self, job, succeeded=True): if not self.marked_for_deletion.has_key(job): @@ -888,13 +902,13 @@ class DeviceBooksModel(BooksModel): # {{{ ans.extend(v) return ans - def clear_ondevice(self, db_ids): + def clear_ondevice(self, db_ids, to_what=None): for data in self.db: if data is None: continue app_id = getattr(data, 'application_id', None) if app_id is not None and app_id in db_ids: - data.in_library = False + data.in_library = to_what self.reset() def flags(self, index): @@ -1049,6 +1063,13 @@ class DeviceBooksModel(BooksModel): # {{{ def paths(self, rows): return [self.db[self.map[r.row()]].path for r in rows ] + def paths_for_db_ids(self, db_ids): + res = [] + for r,b in enumerate(self.db): + if b.application_id in db_ids: + res.append((r,b)) + return res + def indices(self, rows): ''' Return indices into underlying database from rows @@ -1089,6 +1110,8 @@ class DeviceBooksModel(BooksModel): # {{{ elif role == Qt.DecorationRole and cname == 'inlibrary': if self.db[self.map[row]].in_library: return QVariant(self.bool_yes_icon) + elif self.db[self.map[row]].in_library is not None: + return QVariant(self.bool_no_icon) elif role == Qt.TextAlignmentRole: cname = self.column_map[index.column()] ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,