1) send metadata to device while connected
2) ability to manually match books on device with books in library
This commit is contained in:
Charles Haley 2013-07-10 17:44:00 +02:00
parent 1ad3753e20
commit b6c0716cff
9 changed files with 398 additions and 4 deletions

View File

@ -886,6 +886,11 @@ class ActionEditCollections(InterfaceActionBase):
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
description = _('Edit the collections in which books are placed on your device')
class ActionMatchBooks(InterfaceActionBase):
name = 'Match Books'
actual_plugin = 'calibre.gui2.actions.match_books:MatchBookAction'
description = _('Match book on the devices to books in the library')
class ActionCopyToLibrary(InterfaceActionBase):
name = 'Copy To Library'
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
@ -936,7 +941,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionFetchNews, ActionSaveToDisk, ActionQuickview, ActionPolish,
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
ActionAddToLibrary, ActionEditCollections, ActionMatchBooks, ActionChooseLibrary,
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
ActionPluginUpdater, ActionPickRandom, ActionEditToC]

View File

@ -73,7 +73,7 @@ defs['action-layout-context-menu'] = (
defs['action-layout-context-menu-device'] = (
'View', 'Save To Disk', None, 'Remove Books', None,
'Add To Library', 'Edit Collections',
'Add To Library', 'Edit Collections', 'Match Books'
)
defs['action-layout-context-menu-cover-browser'] = (

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.gui2 import error_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.match_books import MatchBooks
class MatchBookAction(InterfaceAction):
name = 'Match Books'
action_spec = (_('Match book to library'), 'book.png',
_('Match this book to a book in the library'),
())
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.match_books_in_library)
def location_selected(self, loc):
enabled = loc != 'library'
self.qaction.setEnabled(enabled)
def match_books_in_library(self, *args):
view = self.gui.current_view()
rows = view.selectionModel().selectedRows()
if not rows or len(rows) != 1:
d = error_dialog(self.gui, _('Match books'), _('You must select one book'))
d.exec_()
return
id_ = view.model().indices(rows)[0]
MatchBooks(self.gui, view, id_).exec_()

View File

@ -1604,6 +1604,10 @@ class DeviceMixin(object): # {{{
except:
pass
def update_metadata_on_device(self):
self.set_books_in_library(self.booklists(), reset=True, force_send=True)
self.refresh_ondevice()
def book_on_device(self, id, reset=False):
'''
Return an indication of whether the given book represented by its db id
@ -1652,7 +1656,8 @@ class DeviceMixin(object): # {{{
loc[4] |= self.book_db_uuid_path_map[id]
return loc
def set_books_in_library(self, booklists, reset=False, add_as_step_to_job=None):
def set_books_in_library(self, booklists, reset=False, add_as_step_to_job=None,
force_send=False):
'''
Set the ondevice indications in the device database.
This method should be called before book_on_device is called, because
@ -1675,7 +1680,8 @@ class DeviceMixin(object): # {{{
x = x.lower() if x else ''
return string_pat.sub('', x)
update_metadata = device_prefs['manage_device_metadata'] == 'on_connect'
update_metadata = (
device_prefs['manage_device_metadata'] == 'on_connect' or force_send)
get_covers = False
desired_thumbnail_height = 0

View File

@ -0,0 +1,197 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import (Qt, QDialog, QAbstractItemView, QTableWidgetItem,
QByteArray)
from calibre.gui2 import gprefs, error_dialog
from calibre.gui2.dialogs.match_books_ui import Ui_MatchBooks
from calibre.utils.icu import sort_key
class TableItem(QTableWidgetItem):
'''
A QTableWidgetItem that sorts on a separate string and uses ICU rules
'''
def __init__(self, val, sort, idx=0):
self.sort = sort
self.sort_idx = idx
QTableWidgetItem.__init__(self, val)
self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
def __ge__(self, other):
l = sort_key(self.sort)
r = sort_key(other.sort)
if l > r:
return 1
if l == r:
return self.sort_idx >= other.sort_idx
return 0
def __lt__(self, other):
l = sort_key(self.sort)
r = sort_key(other.sort)
if l < r:
return 1
if l == r:
return self.sort_idx < other.sort_idx
return 0
class MatchBooks(QDialog, Ui_MatchBooks):
def __init__(self, gui, view, id_):
QDialog.__init__(self, gui, flags=Qt.Window)
Ui_MatchBooks.__init__(self)
self.setupUi(self)
self.isClosed = False
self.books_table_column_widths = None
try:
self.books_table_column_widths = \
gprefs.get('match_books_dialog_books_table_widths', None)
geom = gprefs.get('match_books_dialog_geometry', bytearray(''))
self.restoreGeometry(QByteArray(geom))
except:
pass
self.search_text.initialize('match_books_dialog')
# Remove the help button from the window title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.device_db = view.model().db
self.library_db = gui.library_view.model().db
self.view = view
self.gui = gui
self.current_device_book_id = id_
self.current_library_book_id = None
# Set up the books table columns
self.books_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.books_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.books_table.setColumnCount(3)
t = QTableWidgetItem(_('Title'))
self.books_table.setHorizontalHeaderItem(0, t)
t = QTableWidgetItem(_('Authors'))
self.books_table.setHorizontalHeaderItem(1, t)
t = QTableWidgetItem(_('Series'))
self.books_table.setHorizontalHeaderItem(2, t)
self.books_table_header_height = self.books_table.height()
self.books_table.cellDoubleClicked.connect(self.book_doubleclicked)
self.books_table.cellClicked.connect(self.book_clicked)
self.books_table.sortByColumn(0, Qt.AscendingOrder)
# get the standard table row height. Do this here because calling
# resizeRowsToContents can word wrap long cell contents, creating
# double-high rows
self.books_table.setRowCount(1)
self.books_table.setItem(0, 0, TableItem('A', ''))
self.books_table.resizeRowsToContents()
self.books_table_row_height = self.books_table.rowHeight(0)
self.books_table.setRowCount(0)
self.search_button.clicked.connect(self.do_search)
self.search_button.setDefault(False)
self.search_text.lineEdit().returnPressed.connect(self.return_pressed)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.ignore_next_key = False
def return_pressed(self):
self.ignore_next_key = True
self.do_search()
def keyPressEvent(self, e):
if self.ignore_next_key:
self.ignore_next_key = False
else:
QDialog.keyPressEvent(self, e)
def do_search(self):
query = unicode(self.search_text.text())
if not query:
d = error_dialog(self.gui, _('Match books'),
_('You must enter a search expression into the search box'))
d.exec_()
return
books = self.library_db.data.search(query, return_matches=True)
self.books_table.setRowCount(len(books))
self.books_table.setSortingEnabled(False)
for row, b in enumerate(books):
mi = self.library_db.get_metadata(b, index_is_id=True, get_user_categories=False)
a = TableItem(mi.title, mi.title_sort)
a.setData(Qt.UserRole, b)
self.books_table.setItem(row, 0, a)
a = TableItem(' & '.join(mi.authors), mi.author_sort)
self.books_table.setItem(row, 1, a)
series = mi.format_field('series')[1]
if series is None:
series = ''
a = TableItem(series, mi.series, mi.series_index)
self.books_table.setItem(row, 2, a)
self.books_table.setRowHeight(row, self.books_table_row_height)
self.books_table.setSortingEnabled(True)
# Deal with sizing the table columns. Done here because the numbers are not
# correct until the first paint.
def resizeEvent(self, *args):
QDialog.resizeEvent(self, *args)
if self.books_table_column_widths is not None:
for c,w in enumerate(self.books_table_column_widths):
self.books_table.setColumnWidth(c, w)
else:
# the vertical scroll bar might not be rendered, so might not yet
# have a width. Assume 25. Not a problem because user-changed column
# widths will be remembered
w = self.books_table.width() - 25 - self.books_table.verticalHeader().width()
w /= self.books_table.columnCount()
for c in range(0, self.books_table.columnCount()):
self.books_table.setColumnWidth(c, w)
self.save_state()
def book_clicked(self, row, column):
self.book_selected = True;
id_ = self.books_table.item(row, 0).data(Qt.UserRole).toInt()[0]
self.current_library_book_id = id_
def book_doubleclicked(self, row, column):
self.book_clicked(row, column)
self.accept()
def save_state(self):
self.books_table_column_widths = []
for c in range(0, self.books_table.columnCount()):
self.books_table_column_widths.append(self.books_table.columnWidth(c))
gprefs['match_books_dialog_books_table_widths'] = self.books_table_column_widths
gprefs['match_books_dialog_geometry'] = bytearray(self.saveGeometry())
self.search_text.save_history()
def close(self):
self.save_state()
# clean up to prevent memory leaks
self.device_db = self.view = self.gui = None
def accept(self):
if not self.current_library_book_id:
d = error_dialog(self.gui, _('Match books'),
_('You must select a matching book'))
d.exec_()
return
mi = self.library_db.get_metadata(self.current_library_book_id,
index_is_id=True, get_user_categories=False)
self.device_db[self.current_device_book_id].smart_update(mi, replace_metadata=True)
self.device_db[self.current_device_book_id].in_library_waiting = True
self.save_state()
QDialog.accept(self)
def reject(self):
self.close()
QDialog.reject(self)

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MatchBooks</class>
<widget class="QDialog" name="MatchBooks">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>768</width>
<height>342</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Match Books</string>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="HistoryLineEdit" name="search_text">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>0</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>30</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="search_button">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel">
<property name="text">
<string>Do a search to find the book you want to match</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QTableWidget" name="books_table">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding" >
<horstretch>4</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="columnCount">
<number>0</number>
</property>
<property name="rowCount">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel">
<property name="text">
<string>&lt;p&gt;Be sure to update metadata on the device when you are
finished matching books (Device -> Update Metadata)&lt;/p&gt;</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<customwidgets>
<customwidget>
<class>HistoryLineEdit</class>
<extends>QComboBox</extends>
<header>calibre/gui2/widgets.h</header>
</customwidget>
</customwidgets>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>MatchBooks</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>297</x>
<y>217</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>234</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -25,6 +25,7 @@ class LocationManager(QObject): # {{{
unmount_device = pyqtSignal()
location_selected = pyqtSignal(object)
configure_device = pyqtSignal()
update_device_metadata = pyqtSignal()
def __init__(self, parent=None):
QObject.__init__(self, parent)
@ -60,6 +61,9 @@ class LocationManager(QObject): # {{{
a = m.addAction(QIcon(I('config.png')), _('Configure this device'))
a.triggered.connect(self._configure_requested)
self._mem.append(a)
a = m.addAction(QIcon(I('sync.png')), _('Update metadata on device'))
a.triggered.connect(lambda x : self.update_device_metadata.emit())
self._mem.append(a)
else:
ac.setToolTip(tooltip)

View File

@ -1207,6 +1207,8 @@ class DeviceBooksModel(BooksModel): # {{{
self.search_engine = OnDeviceSearch(self)
self.editable = ['title', 'authors', 'collections']
self.book_in_library = None
self.sync_icon = QIcon(I('sync.png'))
def counts(self):
return Counts(len(self.db), len(self.db), len(self.map))
@ -1535,6 +1537,8 @@ class DeviceBooksModel(BooksModel): # {{{
elif DEBUG and cname == 'inlibrary':
return QVariant(self.db[self.map[row]].in_library)
elif role == Qt.ToolTipRole and index.isValid():
if col == 0 and hasattr(self.db[self.map[row]], 'in_library_waiting'):
return QVariant(_('Waiting for metadata to be updated'))
if self.is_row_marked_for_deletion(row):
return QVariant(_('Marked for deletion'))
if cname in ['title', 'authors'] or (cname == 'collections' and
@ -1543,6 +1547,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 hasattr(self.db[self.map[row]], 'in_library_waiting'):
return QVariant(self.sync_icon)
elif self.db[self.map[row]].in_library is not None:
return QVariant(self.bool_no_icon)
elif role == Qt.TextAlignmentRole:

View File

@ -293,6 +293,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.location_manager.location_selected.connect(self.location_selected)
self.location_manager.unmount_device.connect(self.device_manager.umount_device)
self.location_manager.configure_device.connect(self.configure_connected_device)
self.location_manager.update_device_metadata.connect(self.update_metadata_on_device)
self.eject_action.triggered.connect(self.device_manager.umount_device)
#################### Update notification ###################