mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add title and series sorting tweak. SONY Driver: Set timestamp correctly on windows. Fix #5744 (Tag browser re-opening closed tree after editing metadata). Always read metadata from files on devices, ignoring the preference to read metadata from filenames. Deleting books now refreshes ondevice and inlibrary columns
This commit is contained in:
commit
6475db508b
@ -61,3 +61,13 @@ sort_columns_at_startup = None
|
||||
# default if not set: MMM yyyy
|
||||
gui_pubdate_display_format = 'MMM yyyy'
|
||||
|
||||
# Control title and series sorting in the library view.
|
||||
# If set to 'library_order', Leading articles such as The and A will be ignored.
|
||||
# If set to 'strictly_alphabetic', the titles will be sorted without processing
|
||||
# For example, with library_order, The Client will sort under 'C'. With
|
||||
# strictly_alphabetic, the book will sort under 'T'.
|
||||
# This flag affects Calibre's library display. It has no effect on devices. In
|
||||
# addition, titles for books added before changing the flag will retain their
|
||||
# order until the title is edited. Double-clicking on a title and hitting return
|
||||
# without changing anything is sufficient to change the sort.
|
||||
title_series_sorting = 'library_order'
|
||||
|
@ -12,7 +12,7 @@ from uuid import uuid4
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre import prints, guess_type
|
||||
from calibre import prints, guess_type, iswindows
|
||||
from calibre.devices.errors import DeviceError
|
||||
from calibre.devices.usbms.driver import debug_print
|
||||
from calibre.constants import DEBUG
|
||||
@ -423,7 +423,10 @@ class XMLCache(object):
|
||||
return ans
|
||||
|
||||
def update_text_record(self, record, book, path, bl_index):
|
||||
timestamp = os.path.getctime(path)
|
||||
timestamp = os.path.getmtime(path)
|
||||
# Correct for MS DST time 'adjustment'
|
||||
if iswindows and time.daylight:
|
||||
timestamp -= time.altzone - time.timezone
|
||||
date = strftime(timestamp)
|
||||
if date != record.get('date', None):
|
||||
record.set('date', date)
|
||||
|
@ -294,6 +294,18 @@ class USBMS(CLI, Device):
|
||||
self.report_progress(1.0, _('Sending metadata to device...'))
|
||||
debug_print('USBMS: finished sync_booklists')
|
||||
|
||||
@classmethod
|
||||
def build_template_regexp(cls):
|
||||
def replfunc(match):
|
||||
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
|
||||
return '(?P<' + match.group(1) + '>.+?)'
|
||||
elif match.group(1) == 'authors':
|
||||
return '(?P<author>.+?)'
|
||||
else:
|
||||
return '(.+?)'
|
||||
template = cls.save_template().rpartition('/')[2]
|
||||
return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)')
|
||||
|
||||
@classmethod
|
||||
def path_to_unicode(cls, path):
|
||||
if isbytestring(path):
|
||||
@ -355,22 +367,22 @@ class USBMS(CLI, Device):
|
||||
from calibre.ebooks.metadata.meta import metadata_from_formats
|
||||
from calibre.customize.ui import quick_metadata
|
||||
with quick_metadata:
|
||||
return metadata_from_formats(fmts)
|
||||
return metadata_from_formats(fmts, force_read_metadata=True,
|
||||
pattern=cls.build_template_regexp())
|
||||
|
||||
@classmethod
|
||||
def book_from_path(cls, prefix, path):
|
||||
def book_from_path(cls, prefix, lpath):
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
|
||||
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
|
||||
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path)))
|
||||
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
|
||||
else:
|
||||
from calibre.ebooks.metadata.meta import metadata_from_filename
|
||||
mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)),
|
||||
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+'))
|
||||
|
||||
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
|
||||
cls.build_template_regexp())
|
||||
if mi is None:
|
||||
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0],
|
||||
mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
|
||||
[_('Unknown')])
|
||||
size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
|
||||
book = cls.book_class(prefix, path, other=mi, size=size)
|
||||
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
|
||||
book = cls.book_class(prefix, lpath, other=mi, size=size)
|
||||
return book
|
||||
|
@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES):
|
||||
def path_to_ext(path):
|
||||
return os.path.splitext(path)[1][1:].lower()
|
||||
|
||||
def metadata_from_formats(formats):
|
||||
def metadata_from_formats(formats, force_read_metadata=False, pattern=None):
|
||||
try:
|
||||
return _metadata_from_formats(formats)
|
||||
return _metadata_from_formats(formats, force_read_metadata, pattern)
|
||||
except:
|
||||
mi = metadata_from_filename(list(iter(formats))[0])
|
||||
mi = metadata_from_filename(list(iter(formats), pattern)[0])
|
||||
if not mi.authors:
|
||||
mi.authors = [_('Unknown')]
|
||||
return mi
|
||||
|
||||
def _metadata_from_formats(formats):
|
||||
def _metadata_from_formats(formats, force_read_metadata=False, pattern=None):
|
||||
mi = MetaInformation(None, None)
|
||||
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
|
||||
METADATA_PRIORITIES[path_to_ext(y)]))
|
||||
@ -51,7 +51,9 @@ def _metadata_from_formats(formats):
|
||||
with open(path, 'rb') as stream:
|
||||
try:
|
||||
newmi = get_metadata(stream, stream_type=ext,
|
||||
use_libprs_metadata=True)
|
||||
use_libprs_metadata=True,
|
||||
force_read_metadata=force_read_metadata,
|
||||
pattern=pattern)
|
||||
mi.smart_update(newmi)
|
||||
except:
|
||||
continue
|
||||
@ -69,18 +71,21 @@ def is_recipe(filename):
|
||||
return filename.startswith('calibre') and \
|
||||
filename.rpartition('.')[0].endswith('_recipe_out')
|
||||
|
||||
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
|
||||
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False,
|
||||
force_read_metadata=False, pattern=None):
|
||||
pos = 0
|
||||
if hasattr(stream, 'tell'):
|
||||
pos = stream.tell()
|
||||
try:
|
||||
return _get_metadata(stream, stream_type, use_libprs_metadata)
|
||||
return _get_metadata(stream, stream_type, use_libprs_metadata,
|
||||
force_read_metadata, pattern)
|
||||
finally:
|
||||
if hasattr(stream, 'seek'):
|
||||
stream.seek(pos)
|
||||
|
||||
|
||||
def _get_metadata(stream, stream_type, use_libprs_metadata):
|
||||
def _get_metadata(stream, stream_type, use_libprs_metadata,
|
||||
force_read_metadata=False, pattern=None):
|
||||
if stream_type: stream_type = stream_type.lower()
|
||||
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
|
||||
stream_type = 'html'
|
||||
@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata):
|
||||
|
||||
mi = MetaInformation(None, None)
|
||||
name = os.path.basename(getattr(stream, 'name', ''))
|
||||
base = metadata_from_filename(name)
|
||||
if is_recipe(name) or prefs['read_file_metadata']:
|
||||
base = metadata_from_filename(name, pat=pattern)
|
||||
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
|
||||
mi = get_file_type_metadata(stream, stream_type)
|
||||
if base.title == os.path.splitext(name)[0] and base.authors is None:
|
||||
# Assume that there was no metadata in the file and the user set pattern
|
||||
@ -139,7 +144,7 @@ def metadata_from_filename(name, pat=None):
|
||||
pat = re.compile(prefs.get('filename_pattern'))
|
||||
name = name.replace('_', ' ')
|
||||
match = pat.search(name)
|
||||
if match:
|
||||
if match is not None:
|
||||
try:
|
||||
mi.title = match.group('title')
|
||||
except IndexError:
|
||||
|
@ -1140,6 +1140,13 @@ class DeviceMixin(object):
|
||||
in cache['authors']:
|
||||
loc[i] = True
|
||||
continue
|
||||
# Also check author sort, because it can be used as author in
|
||||
# some formats
|
||||
if mi.author_sort and \
|
||||
re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \
|
||||
in cache['authors']:
|
||||
loc[i] = True
|
||||
continue
|
||||
return loc
|
||||
|
||||
def set_books_in_library(self, booklists, reset=False):
|
||||
@ -1152,10 +1159,16 @@ class DeviceMixin(object):
|
||||
mi = db.get_metadata(id, index_is_id=True)
|
||||
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
||||
if title not in self.db_book_title_cache:
|
||||
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}}
|
||||
authors = authors_to_string(mi.authors).lower() if mi.authors else ''
|
||||
self.db_book_title_cache[title] = \
|
||||
{'authors':{}, 'author_sort':{}, 'db_ids':{}}
|
||||
if mi.authors:
|
||||
authors = authors_to_string(mi.authors).lower()
|
||||
authors = re.sub('(?u)\W|[_]', '', authors)
|
||||
self.db_book_title_cache[title]['authors'][authors] = mi
|
||||
if mi.author_sort:
|
||||
aus = mi.author_sort.lower()
|
||||
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)
|
||||
|
||||
@ -1186,12 +1199,19 @@ class DeviceMixin(object):
|
||||
book.smart_update(d['db_ids'][book.db_id])
|
||||
resend_metadata = True
|
||||
continue
|
||||
book_authors = authors_to_string(book.authors).lower() if book.authors else ''
|
||||
if book.authors:
|
||||
# Compare against both author and author sort, because
|
||||
# either can appear as the author
|
||||
book_authors = authors_to_string(book.authors).lower()
|
||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
||||
if book_authors in d['authors']:
|
||||
book.in_library = True
|
||||
book.smart_update(d['authors'][book_authors])
|
||||
resend_metadata = True
|
||||
elif book_authors in d['author_sort']:
|
||||
book.in_library = True
|
||||
book.smart_update(d['author_sort'][book_authors])
|
||||
resend_metadata = True
|
||||
# Set author_sort if it isn't already
|
||||
asort = getattr(book, 'author_sort', None)
|
||||
if not asort and book.authors:
|
||||
|
@ -279,7 +279,10 @@ class LibraryViewMixin(object): # {{{
|
||||
if search:
|
||||
self.search.set_search_string(join.join(search))
|
||||
|
||||
|
||||
def search_done(self, view, ok):
|
||||
if view is self.current_view():
|
||||
self.search.search_done(ok)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -200,7 +200,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.count_changed()
|
||||
self.clear_caches()
|
||||
self.reset()
|
||||
|
||||
return ids
|
||||
|
||||
def delete_books_by_id(self, ids):
|
||||
for id in ids:
|
||||
@ -882,6 +882,15 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
ans.extend(v)
|
||||
return ans
|
||||
|
||||
def clear_ondevice(self, db_ids):
|
||||
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
|
||||
self.reset()
|
||||
|
||||
def flags(self, index):
|
||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
||||
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
||||
|
@ -169,6 +169,12 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="search_restriction">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Books display will be restricted to those matching the selected saved search</string>
|
||||
</property>
|
||||
|
@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
||||
pyqtSignal, SIGNAL
|
||||
pyqtSignal, SIGNAL, QObject, QDialog
|
||||
from PyQt4.QtGui import QCompleter
|
||||
|
||||
from calibre.gui2 import config
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||
from calibre.gui2.dialogs.search import SearchDialog
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
|
||||
class SearchLineEdit(QLineEdit):
|
||||
|
||||
@ -79,8 +83,7 @@ class SearchBox2(QComboBox):
|
||||
self.setMinimumContentsLength(25)
|
||||
self._in_a_search = False
|
||||
|
||||
def initialize(self, opt_name, colorize=False,
|
||||
help_text=_('Search')):
|
||||
def initialize(self, opt_name, colorize=False, help_text=_('Search')):
|
||||
self.as_you_type = config['search_as_you_type']
|
||||
self.opt_name = opt_name
|
||||
self.addItems(QStringList(list(set(config[opt_name]))))
|
||||
@ -239,9 +242,9 @@ class SavedSearchBox(QComboBox):
|
||||
self.setInsertPolicy(self.NoInsert)
|
||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.setMinimumContentsLength(10)
|
||||
self.tool_tip_text = self.toolTip()
|
||||
|
||||
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
|
||||
self.tool_tip_text = self.toolTip()
|
||||
self.saved_searches = _saved_searches
|
||||
self.search_box = _search_box
|
||||
self.help_text = help_text
|
||||
@ -331,3 +334,69 @@ class SavedSearchBox(QComboBox):
|
||||
if idx < 0:
|
||||
return
|
||||
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
|
||||
|
||||
class SearchBoxMixin(object):
|
||||
|
||||
def __init__(self):
|
||||
self.search.initialize('main_search_history', colorize=True,
|
||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
|
||||
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
|
||||
self.do_advanced_search)
|
||||
|
||||
self.search.clear()
|
||||
self.search.setFocus(Qt.OtherFocusReason)
|
||||
self.search.setMaximumWidth(self.width()-150)
|
||||
|
||||
def search_box_cleared(self):
|
||||
self.tags_view.clear()
|
||||
self.saved_search.clear_to_help()
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def do_advanced_search(self, *args):
|
||||
d = SearchDialog(self)
|
||||
if d.exec_() == QDialog.Accepted:
|
||||
self.search.set_search_string(d.search_string())
|
||||
|
||||
class SavedSearchBoxMixin(object):
|
||||
|
||||
def __init__(self):
|
||||
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
|
||||
self.saved_searches_changed()
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
||||
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
||||
help_text=_('Saved Searches'))
|
||||
self.connect(self.save_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.save_search_button_clicked)
|
||||
self.connect(self.delete_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.delete_search_button_clicked)
|
||||
self.connect(self.copy_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.copy_search_button_clicked)
|
||||
|
||||
|
||||
def saved_searches_changed(self):
|
||||
p = prefs['saved_searches'].keys()
|
||||
p.sort()
|
||||
t = unicode(self.search_restriction.currentText())
|
||||
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||
self.search_restriction.addItem('')
|
||||
self.tags_view.recount()
|
||||
for s in p:
|
||||
self.search_restriction.addItem(s)
|
||||
if t:
|
||||
if t in p: # redo the current restriction, if there was one
|
||||
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
|
||||
# self.tags_view.set_search_restriction(t)
|
||||
else:
|
||||
self.search_restriction.setCurrentIndex(0)
|
||||
self.apply_search_restriction('')
|
||||
|
||||
def do_saved_search_edit(self, search):
|
||||
d = SavedSearchEditor(self, search)
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
self.saved_searches_changed()
|
||||
self.saved_search.clear_to_help()
|
||||
|
||||
|
||||
|
57
src/calibre/gui2/search_restriction_mixin.py
Normal file
57
src/calibre/gui2/search_restriction_mixin.py
Normal file
@ -0,0 +1,57 @@
|
||||
'''
|
||||
Created on 10 Jun 2010
|
||||
|
||||
@author: charles
|
||||
'''
|
||||
|
||||
class SearchRestrictionMixin(object):
|
||||
|
||||
def __init__(self):
|
||||
self.search_restriction.activated[str].connect(self.apply_search_restriction)
|
||||
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
|
||||
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.search_restriction.setMinimumContentsLength(10)
|
||||
|
||||
'''
|
||||
Adding and deleting books while restricted creates a complexity. When added,
|
||||
they are displayed regardless of whether they match a search restriction.
|
||||
However, if they do not, they are removed at the next search. The counts
|
||||
must take this behavior into effect.
|
||||
'''
|
||||
|
||||
def restriction_count_changed(self, c):
|
||||
self.restriction_count_of_books_in_view += \
|
||||
c - self.restriction_count_of_books_in_library
|
||||
self.restriction_count_of_books_in_library = c
|
||||
if self.restriction_in_effect:
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def apply_search_restriction(self, r):
|
||||
r = unicode(r)
|
||||
if r is not None and r != '':
|
||||
self.restriction_in_effect = True
|
||||
restriction = 'search:"%s"'%(r)
|
||||
else:
|
||||
self.restriction_in_effect = False
|
||||
restriction = ''
|
||||
self.restriction_count_of_books_in_view = \
|
||||
self.library_view.model().set_search_restriction(restriction)
|
||||
self.search.clear_to_help()
|
||||
self.saved_search.clear_to_help()
|
||||
self.tags_view.set_search_restriction(restriction)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def set_number_of_books_shown(self):
|
||||
if self.current_view() == self.library_view and self.restriction_in_effect:
|
||||
t = _("({0} of {1})").format(self.current_view().row_count(),
|
||||
self.restriction_count_of_books_in_view)
|
||||
self.search_count.setStyleSheet \
|
||||
('QLabel { border-radius: 8px; background-color: yellow; }')
|
||||
else: # No restriction or not library view
|
||||
if not self.search.in_a_search():
|
||||
t = _("(all books)")
|
||||
else:
|
||||
t = _("({0} of all)").format(self.current_view().row_count())
|
||||
self.search_count.setStyleSheet(
|
||||
'QLabel { background-color: transparent; }')
|
||||
self.search_count.setText(t)
|
@ -599,6 +599,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
class TagBrowserMixin(object): # {{{
|
||||
|
||||
def __init__(self, db):
|
||||
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
||||
self.tags_view.set_database(self.library_view.model().db,
|
||||
self.tag_match, self.popularity)
|
||||
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
||||
@ -608,6 +609,7 @@ class TagBrowserMixin(object): # {{{
|
||||
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
||||
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
|
||||
self.edit_categories.clicked.connect(self.do_user_categories_edit)
|
||||
|
||||
def do_user_categories_edit(self, on_category=None):
|
||||
d = TagCategories(self, self.library_view.model().db, on_category)
|
||||
@ -658,5 +660,6 @@ class TagBrowserWidget(QWidget): # {{{
|
||||
parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
|
||||
self._layout.addWidget(parent.edit_categories)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -29,7 +29,6 @@ from calibre.utils.filenames import ascii_filename
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.config import prefs, dynamic
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.devices.errors import UserFeedback
|
||||
from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
|
||||
question_dialog,\
|
||||
@ -51,7 +50,6 @@ from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
||||
fetch_scheduled_recipe, generate_catalog
|
||||
from calibre.gui2.dialogs.config import ConfigDialog
|
||||
from calibre.gui2.dialogs.search import SearchDialog
|
||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||
from calibre.gui2.dialogs.book_info import BookInfo
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
@ -59,9 +57,10 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
from calibre.library.caches import CoverCache
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||
from calibre.gui2.tag_view import TagBrowserMixin
|
||||
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||
from calibre.gui2.tag_view import TagBrowserMixin
|
||||
|
||||
class Listener(Thread): # {{{
|
||||
|
||||
@ -106,7 +105,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||
# }}}
|
||||
|
||||
class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, LayoutMixin):
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin):
|
||||
'The main GUI'
|
||||
|
||||
def set_default_thumbnail(self, height):
|
||||
@ -156,20 +156,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self.restriction_count_of_books_in_view = 0
|
||||
self.restriction_count_of_books_in_library = 0
|
||||
self.restriction_in_effect = False
|
||||
self.search.initialize('main_search_history', colorize=True,
|
||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
||||
self.search.clear()
|
||||
|
||||
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
||||
help_text=_('Saved Searches'))
|
||||
self.connect(self.save_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.save_search_button_clicked)
|
||||
self.connect(self.delete_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.delete_search_button_clicked)
|
||||
self.connect(self.copy_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.copy_search_button_clicked)
|
||||
|
||||
self.progress_indicator = ProgressIndicator(self)
|
||||
self.verbose = opts.verbose
|
||||
@ -229,8 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self.connect(self.system_tray_icon,
|
||||
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
||||
self.system_tray_icon_activated)
|
||||
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
|
||||
self.do_advanced_search)
|
||||
|
||||
DeviceMixin.__init__(self)
|
||||
|
||||
@ -269,6 +253,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self.update_checker.update_found.connect(self.update_found,
|
||||
type=Qt.QueuedConnection)
|
||||
self.update_checker.start()
|
||||
|
||||
####################### Status Bar #####################
|
||||
self.status_bar.initialize(self.system_tray_icon)
|
||||
self.status_bar.show_book_info.connect(self.show_book_info)
|
||||
@ -277,6 +262,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
####################### Setup Toolbar #####################
|
||||
ToolbarMixin.__init__(self)
|
||||
|
||||
####################### Search boxes ########################
|
||||
SavedSearchBoxMixin.__init__(self)
|
||||
SearchBoxMixin.__init__(self)
|
||||
|
||||
####################### Library view ########################
|
||||
LibraryViewMixin.__init__(self, db)
|
||||
|
||||
@ -284,20 +273,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
|
||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||
self.hide_windows()
|
||||
self.stack.setCurrentIndex(0)
|
||||
self.search.setFocus(Qt.OtherFocusReason)
|
||||
self.cover_cache = CoverCache(self.library_path)
|
||||
self.cover_cache.start()
|
||||
self.library_view.model().cover_cache = self.cover_cache
|
||||
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
|
||||
self.search_restriction.activated[str].connect(self.apply_search_restriction)
|
||||
for x in (self.location_view.count_changed, self.tags_view.recount,
|
||||
self.restriction_count_changed):
|
||||
self.library_view.model().count_changed_signal.connect(x)
|
||||
|
||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
|
||||
self.saved_searches_changed()
|
||||
self.library_view.model().count_changed_signal.connect \
|
||||
(self.location_view.count_changed)
|
||||
if not gprefs.get('quick_start_guide_added', False):
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||
@ -318,8 +298,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
|
||||
########################### Tags Browser ##############################
|
||||
TagBrowserMixin.__init__(self, db)
|
||||
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.search_restriction.setMinimumContentsLength(10)
|
||||
|
||||
######################### Search Restriction ##########################
|
||||
SearchRestrictionMixin.__init__(self)
|
||||
|
||||
########################### Cover Flow ################################
|
||||
|
||||
@ -328,7 +309,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self._calculated_available_height = min(max_available_height()-15,
|
||||
self.height())
|
||||
self.resize(self.width(), self._calculated_available_height)
|
||||
self.search.setMaximumWidth(self.width()-150)
|
||||
|
||||
|
||||
if config['autolaunch_server']:
|
||||
@ -358,13 +338,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self.read_settings()
|
||||
self.finalize_layout()
|
||||
|
||||
def do_saved_search_edit(self, search):
|
||||
d = SavedSearchEditor(self, search)
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
self.saved_searches_changed()
|
||||
self.saved_search.clear_to_help()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
MainWindow.resizeEvent(self, ev)
|
||||
self.search.setMaximumWidth(self.width()-150)
|
||||
@ -446,76 +419,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
error_dialog(self, _('Failed to start content server'),
|
||||
unicode(self.content_server.exception)).exec_()
|
||||
|
||||
'''
|
||||
Restrictions.
|
||||
Adding and deleting books creates a complexity. When added, they are
|
||||
displayed regardless of whether they match a search restriction. However, if
|
||||
they do not, they are removed at the next search. The counts must take this
|
||||
behavior into effect.
|
||||
'''
|
||||
|
||||
def restriction_count_changed(self, c):
|
||||
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
|
||||
self.restriction_count_of_books_in_library = c
|
||||
if self.restriction_in_effect:
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def apply_search_restriction(self, r):
|
||||
r = unicode(r)
|
||||
if r is not None and r != '':
|
||||
self.restriction_in_effect = True
|
||||
restriction = 'search:"%s"'%(r)
|
||||
else:
|
||||
self.restriction_in_effect = False
|
||||
restriction = ''
|
||||
self.restriction_count_of_books_in_view = \
|
||||
self.library_view.model().set_search_restriction(restriction)
|
||||
self.search.clear_to_help()
|
||||
self.saved_search.clear_to_help()
|
||||
self.tags_view.set_search_restriction(restriction)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def set_number_of_books_shown(self):
|
||||
if self.current_view() == self.library_view and self.restriction_in_effect:
|
||||
t = _("({0} of {1})").format(self.current_view().row_count(),
|
||||
self.restriction_count_of_books_in_view)
|
||||
self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }')
|
||||
else: # No restriction or not library view
|
||||
if not self.search.in_a_search():
|
||||
t = _("(all books)")
|
||||
else:
|
||||
t = _("({0} of all)").format(self.current_view().row_count())
|
||||
self.search_count.setStyleSheet(
|
||||
'QLabel { background-color: transparent; }')
|
||||
self.search_count.setText(t)
|
||||
|
||||
def search_box_cleared(self):
|
||||
self.tags_view.clear()
|
||||
self.saved_search.clear_to_help()
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def search_done(self, view, ok):
|
||||
if view is self.current_view():
|
||||
self.search.search_done(ok)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def saved_searches_changed(self):
|
||||
p = prefs['saved_searches'].keys()
|
||||
p.sort()
|
||||
t = unicode(self.search_restriction.currentText())
|
||||
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||
self.search_restriction.addItem('')
|
||||
self.tags_view.recount()
|
||||
for s in p:
|
||||
self.search_restriction.addItem(s)
|
||||
if t:
|
||||
if t in p: # redo the current restriction, if there was one
|
||||
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
|
||||
# self.tags_view.set_search_restriction(t)
|
||||
else:
|
||||
self.search_restriction.setCurrentIndex(0)
|
||||
self.apply_search_restriction('')
|
||||
|
||||
|
||||
def another_instance_wants_to_talk(self):
|
||||
try:
|
||||
@ -554,8 +457,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
def booklists(self):
|
||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||
|
||||
|
||||
|
||||
########################## Connect to device ##############################
|
||||
|
||||
def save_device_view_settings(self):
|
||||
@ -1130,7 +1031,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
row = None
|
||||
if ci.isValid():
|
||||
row = ci.row()
|
||||
view.model().delete_books(rows)
|
||||
ids_deleted = view.model().delete_books(rows)
|
||||
for v in (self.memory_view, self.card_a_view, self.card_b_view):
|
||||
if v is None:
|
||||
continue
|
||||
v.model().clear_ondevice(ids_deleted)
|
||||
if row is not None:
|
||||
ci = view.model().index(row, 0)
|
||||
if ci.isValid():
|
||||
@ -1175,6 +1080,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self.booklists())
|
||||
model.paths_deleted(paths)
|
||||
self.upload_booklists()
|
||||
# Clear the ondevice info so it will be recomputed
|
||||
self.book_on_device(None, None, reset=True)
|
||||
# We want to reset all the ondevice flags in the library. Use a big
|
||||
# hammer, so we don't need to worry about whether some succeeded or not
|
||||
self.library_view.model().refresh()
|
||||
|
||||
############################################################################
|
||||
|
||||
@ -1845,13 +1755,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
|
||||
############################################################################
|
||||
|
||||
########################### Do advanced search #############################
|
||||
|
||||
def do_advanced_search(self, *args):
|
||||
d = SearchDialog(self)
|
||||
if d.exec_() == QDialog.Accepted:
|
||||
self.search.set_search_string(d.search_string())
|
||||
|
||||
############################################################################
|
||||
|
||||
############################### Do config ##################################
|
||||
|
@ -619,9 +619,12 @@ class ResultCache(SearchQueryParser):
|
||||
if self.first_sort:
|
||||
subsort = True
|
||||
self.first_sort = False
|
||||
fcmp = self.seriescmp if field == 'series' else \
|
||||
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
|
||||
asstr=as_string)
|
||||
fcmp = self.seriescmp \
|
||||
if field == 'series' and \
|
||||
tweaks['title_series_sorting'] == 'library_order' \
|
||||
else \
|
||||
functools.partial(self.cmp, self.FIELD_MAP[field],
|
||||
subsort=subsort, asstr=as_string)
|
||||
self._map.sort(cmp=fcmp, reverse=not ascending)
|
||||
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
||||
|
||||
|
@ -28,7 +28,7 @@ from calibre.customize.ui import run_plugins_on_import
|
||||
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||
from calibre.utils.magick_draw import save_cover_data_to
|
||||
@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
icon=icon, tooltip = tooltip)
|
||||
for r in data if item_not_zero_func(r)]
|
||||
if category == 'series' and not sort_on_count:
|
||||
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
|
||||
title_sort(y.name).lower()))
|
||||
if tweaks['title_series_sorting'] == 'library_order':
|
||||
ts = lambda x: title_sort(x)
|
||||
else:
|
||||
ts = lambda x:x
|
||||
categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
|
||||
ts(y.name).lower()))
|
||||
|
||||
# We delayed computing the standard formats category because it does not
|
||||
# use a view, but is computed dynamically
|
||||
@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
title = title.decode(preferred_encoding, 'replace')
|
||||
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
|
||||
self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True)
|
||||
if tweaks['title_series_sorting'] == 'library_order':
|
||||
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True)
|
||||
else:
|
||||
self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True)
|
||||
self.set_path(id, True)
|
||||
self.conn.commit()
|
||||
if notify:
|
||||
|
@ -15,6 +15,7 @@ from threading import RLock
|
||||
from datetime import datetime
|
||||
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.date import parse_date, isoformat
|
||||
|
||||
global_lock = RLock()
|
||||
@ -115,7 +116,10 @@ class DBThread(Thread):
|
||||
self.conn.create_aggregate('concat', 1, Concatenate)
|
||||
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
||||
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
|
||||
if tweaks['title_series_sorting'] == 'library_order':
|
||||
self.conn.create_function('title_sort', 1, title_sort)
|
||||
else:
|
||||
self.conn.create_function('title_sort', 1, lambda x:x)
|
||||
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
||||
# Dummy functions for dynamically created filters
|
||||
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user