mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG revisions
This commit is contained in:
commit
b5b3712502
@ -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'
|
||||
|
@ -24,9 +24,10 @@ class Economist(BasicNewsRecipe):
|
||||
oldest_article = 7.0
|
||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||
dict(attrs={'class':['dblClkTrk']})]
|
||||
remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body')
|
||||
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
|
||||
keep_only_tags = [dict(id='ec-article-body')]
|
||||
needs_subscription = True
|
||||
no_stylesheets = True
|
||||
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
||||
lambda x:'</html>')]
|
||||
|
||||
@ -87,7 +88,7 @@ class Economist(BasicNewsRecipe):
|
||||
continue
|
||||
a = tag.find('a', href=True)
|
||||
if a is not None:
|
||||
url=a['href'].replace('displaystory', 'PrinterFriendly').strip()
|
||||
url=a['href'].split('?')[0]+'/print'
|
||||
if url.startswith('Printer'):
|
||||
url = '/'+url
|
||||
if url.startswith('/'):
|
||||
|
@ -17,8 +17,9 @@ class Economist(BasicNewsRecipe):
|
||||
oldest_article = 7.0
|
||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||
dict(attrs={'class':['dblClkTrk']})]
|
||||
remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body')
|
||||
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
|
||||
keep_only_tags = [dict(id='ec-article-body')]
|
||||
no_stylesheets = True
|
||||
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
||||
lambda x:'</html>')]
|
||||
|
||||
@ -88,19 +89,20 @@ class Economist(BasicNewsRecipe):
|
||||
br = browser()
|
||||
ret = br.open(url)
|
||||
raw = ret.read()
|
||||
url = br.geturl().replace('displaystory', 'PrinterFriendly').strip()
|
||||
url = br.geturl().split('?')[0]+'/print'
|
||||
root = html.fromstring(raw)
|
||||
matches = root.xpath('//*[@class = "article-section"]')
|
||||
matches = root.xpath('//*[@class = "ec-article-info"]')
|
||||
feedtitle = 'Miscellaneous'
|
||||
if matches:
|
||||
feedtitle = string.capwords(html.tostring(matches[0], method='text',
|
||||
encoding=unicode))
|
||||
feedtitle = string.capwords(html.tostring(matches[-1], method='text',
|
||||
encoding=unicode).split('|')[-1].strip())
|
||||
return (i, feedtitle, url, title, description, author, published)
|
||||
|
||||
def eco_article_found(self, req, result):
|
||||
from calibre.web.feeds import Article
|
||||
i, feedtitle, link, title, description, author, published = result
|
||||
self.log('Found print version for article:', title)
|
||||
self.log('Found print version for article:', title, 'in', feedtitle,
|
||||
'at', link)
|
||||
|
||||
a = Article(i, title, link, author, description, published, '')
|
||||
|
||||
|
@ -65,4 +65,4 @@ class Smh_au(BasicNewsRecipe):
|
||||
,'url' :url
|
||||
,'description':description
|
||||
})
|
||||
return [(soup.head.title.string, articles)]
|
||||
return [(self.tag_to_string(soup.find('title')), articles)]
|
||||
|
@ -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:
|
||||
|
@ -99,6 +99,8 @@ def _config():
|
||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||
help=_('tag browser categories not to display'))
|
||||
c.add_opt('gui_layout', choices=['wide', 'narrow'],
|
||||
help=_('The layout of the user interface'), default='narrow')
|
||||
return ConfigProxy(c)
|
||||
|
||||
config = _config()
|
||||
@ -125,6 +127,11 @@ def available_width():
|
||||
desktop = QCoreApplication.instance().desktop()
|
||||
return desktop.availableGeometry().width()
|
||||
|
||||
try:
|
||||
is_widescreen = float(available_width())/available_height() > 1.4
|
||||
except:
|
||||
is_widescreen = True
|
||||
|
||||
def extension(path):
|
||||
return os.path.splitext(path)[1][1:].lower()
|
||||
|
||||
|
@ -10,10 +10,11 @@ Module to implement the Cover Flow feature
|
||||
import sys, os, time
|
||||
|
||||
from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \
|
||||
QStackedLayout
|
||||
QStackedLayout, QLabel
|
||||
|
||||
from calibre import plugins
|
||||
from calibre.gui2 import config, available_height, available_width
|
||||
|
||||
pictureflow, pictureflowerror = plugins['pictureflow']
|
||||
|
||||
if pictureflow is not None:
|
||||
@ -79,12 +80,15 @@ if pictureflow is not None:
|
||||
def __init__(self, parent=None):
|
||||
pictureflow.PictureFlow.__init__(self, parent,
|
||||
config['cover_flow_queue_length']+1)
|
||||
self.setMinimumSize(QSize(10, 10))
|
||||
self.setMinimumSize(QSize(300, 150))
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding))
|
||||
self.setZoomFactor(150)
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
def wheelEvent(self, ev):
|
||||
ev.accept()
|
||||
if ev.delta() < 0:
|
||||
@ -108,56 +112,49 @@ class CoverFlowMixin(object):
|
||||
self.cover_flow_sync_timer.timeout.connect(self.cover_flow_do_sync)
|
||||
self.cover_flow_sync_flag = True
|
||||
self.cover_flow = CoverFlow(parent=self)
|
||||
self.cover_flow.setVisible(False)
|
||||
if not config['separate_cover_flow']:
|
||||
self.cb_layout.addWidget(self.cover_flow)
|
||||
self.cover_flow.currentChanged.connect(self.sync_listview_to_cf)
|
||||
self.library_view.selectionModel().currentRowChanged.connect(
|
||||
self.sync_cf_to_listview)
|
||||
self.db_images = DatabaseImages(self.library_view.model())
|
||||
self.cover_flow.setImages(self.db_images)
|
||||
ah, aw = available_height(), available_width()
|
||||
self._cb_layout_is_horizontal = float(aw)/ah >= 1.4
|
||||
self.cb_layout.setDirection(self.cb_layout.LeftToRight if
|
||||
self._cb_layout_is_horizontal else
|
||||
self.cb_layout.TopToBottom)
|
||||
|
||||
def toggle_cover_flow_visibility(self, show):
|
||||
else:
|
||||
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
|
||||
+'<br>'+pictureflowerror)
|
||||
self.cover_flow.setWordWrap(True)
|
||||
if config['separate_cover_flow']:
|
||||
if show:
|
||||
d = QDialog(self)
|
||||
ah, aw = available_height(), available_width()
|
||||
d.resize(int(aw/1.5), ah-60)
|
||||
d._layout = QStackedLayout()
|
||||
d.setLayout(d._layout)
|
||||
d.setWindowTitle(_('Browse by covers'))
|
||||
d.layout().addWidget(self.cover_flow)
|
||||
self.cover_flow.setVisible(True)
|
||||
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
||||
d.show()
|
||||
d.finished.connect(self.sidebar.external_cover_flow_finished)
|
||||
self.cf_dialog = d
|
||||
else:
|
||||
cfd = getattr(self, 'cf_dialog', None)
|
||||
if cfd is not None:
|
||||
self.cover_flow.setVisible(False)
|
||||
cfd.hide()
|
||||
self.cf_dialog = None
|
||||
self.cb_splitter.button.clicked.connect(self.toggle_cover_browser)
|
||||
if CoverFlow is not None:
|
||||
self.cover_flow.stop.connect(self.hide_cover_browser)
|
||||
else:
|
||||
if show:
|
||||
self.cover_flow.setVisible(True)
|
||||
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
||||
else:
|
||||
self.cover_flow.setVisible(False)
|
||||
self.cb_splitter.insertWidget(self.cb_splitter.side_index, self.cover_flow)
|
||||
if CoverFlow is not None:
|
||||
self.cover_flow.stop.connect(self.cb_splitter.hide_side_pane)
|
||||
self.cb_splitter.button.toggled.connect(self.cover_browser_toggled)
|
||||
|
||||
def toggle_cover_flow(self, show):
|
||||
if show:
|
||||
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
|
||||
self.library_view.setCurrentIndex(
|
||||
self.library_view.currentIndex())
|
||||
self.cover_flow_sync_timer.start(500)
|
||||
self.library_view.scroll_to_row(self.library_view.currentIndex().row())
|
||||
def toggle_cover_browser(self):
|
||||
cbd = getattr(self, 'cb_dialog', None)
|
||||
if cbd is not None:
|
||||
self.hide_cover_browser()
|
||||
else:
|
||||
self.show_cover_browser()
|
||||
|
||||
def cover_browser_toggled(self, *args):
|
||||
if self.cb_splitter.button.isChecked():
|
||||
self.cover_browser_shown()
|
||||
else:
|
||||
self.cover_browser_hidden()
|
||||
|
||||
def cover_browser_shown(self):
|
||||
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
||||
if CoverFlow is not None:
|
||||
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
|
||||
self.cover_flow_sync_timer.start(500)
|
||||
self.library_view.setCurrentIndex(
|
||||
self.library_view.currentIndex())
|
||||
self.library_view.scroll_to_row(self.library_view.currentIndex().row())
|
||||
|
||||
def cover_browser_hidden(self):
|
||||
if CoverFlow is not None:
|
||||
self.cover_flow_sync_timer.stop()
|
||||
idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0)
|
||||
if idx.isValid():
|
||||
@ -165,7 +162,28 @@ class CoverFlowMixin(object):
|
||||
sm.select(idx, sm.ClearAndSelect|sm.Rows)
|
||||
self.library_view.setCurrentIndex(idx)
|
||||
self.library_view.scroll_to_row(idx.row())
|
||||
self.toggle_cover_flow_visibility(show)
|
||||
|
||||
|
||||
def show_cover_browser(self):
|
||||
d = QDialog(self)
|
||||
ah, aw = available_height(), available_width()
|
||||
d.resize(int(aw/1.5), ah-60)
|
||||
d._layout = QStackedLayout()
|
||||
d.setLayout(d._layout)
|
||||
d.setWindowTitle(_('Browse by covers'))
|
||||
d.layout().addWidget(self.cover_flow)
|
||||
self.cover_flow.setVisible(True)
|
||||
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
||||
d.show()
|
||||
self.cb_splitter.button.set_state_to_hide()
|
||||
d.finished.connect(self.cb_splitter.button.set_state_to_show)
|
||||
self.cb_dialog = d
|
||||
|
||||
def hide_cover_browser(self):
|
||||
cbd = getattr(self, 'cb_dialog', None)
|
||||
if cbd is not None:
|
||||
cbd.accept()
|
||||
self.cb_dialog = None
|
||||
|
||||
def sync_cf_to_listview(self, current, previous):
|
||||
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
|
||||
|
@ -391,16 +391,14 @@ class DeviceMenu(QMenu): # {{{
|
||||
default_account = (dest, False, False, I('mail.svg'),
|
||||
_('Email to')+' '+account)
|
||||
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
||||
_('Email to')+' '+account, self)
|
||||
_('Email to')+' '+account)
|
||||
action2 = DeviceAction(dest, True, False, I('mail.svg'),
|
||||
_('Email to')+' '+account, self)
|
||||
_('Email to')+' '+account+ _(' and delete from library'))
|
||||
map(self.email_to_menu.addAction, (action1, action2))
|
||||
map(self._memory.append, (action1, action2))
|
||||
self.email_to_menu.addSeparator()
|
||||
self.connect(action1, SIGNAL('a_s(QAction)'),
|
||||
self.action_triggered)
|
||||
self.connect(action2, SIGNAL('a_s(QAction)'),
|
||||
self.action_triggered)
|
||||
action1.a_s.connect(self.action_triggered)
|
||||
action2.a_s.connect(self.action_triggered)
|
||||
|
||||
basic_actions = [
|
||||
('main:', False, False, I('reader.svg'),
|
||||
@ -1140,6 +1138,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 +1157,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 ''
|
||||
authors = re.sub('(?u)\W|[_]', '', authors)
|
||||
self.db_book_title_cache[title]['authors'][authors] = mi
|
||||
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 +1197,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 ''
|
||||
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
|
||||
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:
|
||||
|
@ -7,12 +7,16 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon
|
||||
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
|
||||
QWidget, QHBoxLayout, QToolBar, QSize, QSizePolicy
|
||||
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.constants import isosx
|
||||
from calibre.gui2 import config
|
||||
from calibre.constants import isosx, __appname__
|
||||
from calibre.gui2 import config, is_widescreen
|
||||
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
||||
from calibre.gui2.widgets import Splitter
|
||||
from calibre.gui2.tag_view import TagBrowserWidget
|
||||
|
||||
_keep_refs = []
|
||||
|
||||
@ -275,7 +279,123 @@ 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()
|
||||
|
||||
# }}}
|
||||
|
||||
class LibraryWidget(Splitter): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
orientation = Qt.Vertical if config['gui_layout'] == 'narrow' and \
|
||||
not is_widescreen else Qt.Horizontal
|
||||
#orientation = Qt.Vertical
|
||||
idx = 0 if orientation == Qt.Vertical else 1
|
||||
Splitter.__init__(self, 'cover_browser_splitter', _('Cover Browser'),
|
||||
I('cover_flow.svg'),
|
||||
orientation=orientation, parent=parent,
|
||||
connect_button=not config['separate_cover_flow'],
|
||||
side_index=idx, initial_side_size=400, initial_show=False)
|
||||
parent.library_view = BooksView(parent)
|
||||
parent.library_view.setObjectName('library_view')
|
||||
self.addWidget(parent.library_view)
|
||||
# }}}
|
||||
|
||||
class Stack(QStackedWidget): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
QStackedWidget.__init__(self, parent)
|
||||
|
||||
parent.cb_splitter = LibraryWidget(parent)
|
||||
self.tb_widget = TagBrowserWidget(parent)
|
||||
parent.tb_splitter = Splitter('tag_browser_splitter',
|
||||
_('Tag Browser'), I('tags.svg'),
|
||||
parent=parent, side_index=0, initial_side_size=200)
|
||||
parent.tb_splitter.addWidget(self.tb_widget)
|
||||
parent.tb_splitter.addWidget(parent.cb_splitter)
|
||||
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)
|
||||
|
||||
self.addWidget(parent.tb_splitter)
|
||||
for x in ('memory', 'card_a', 'card_b'):
|
||||
name = x+'_view'
|
||||
w = DeviceBooksView(parent)
|
||||
setattr(parent, name, w)
|
||||
self.addWidget(w)
|
||||
w.setObjectName(name)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class SideBar(QToolBar): # {{{
|
||||
|
||||
|
||||
def __init__(self, splitters, jobs_button, parent=None):
|
||||
QToolBar.__init__(self, _('Side bar'), parent)
|
||||
self.setOrientation(Qt.Vertical)
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.setIconSize(QSize(48, 48))
|
||||
self.spacer = QWidget(self)
|
||||
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
for s in splitters:
|
||||
self.addWidget(s.button)
|
||||
self.addWidget(self.spacer)
|
||||
self.addWidget(jobs_button)
|
||||
|
||||
for ch in self.children():
|
||||
if isinstance(ch, QToolButton):
|
||||
ch.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
# }}}
|
||||
|
||||
class LayoutMixin(object): # {{{
|
||||
|
||||
def __init__(self):
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(__appname__)
|
||||
|
||||
if config['gui_layout'] == 'narrow':
|
||||
from calibre.gui2.status import StatusBar
|
||||
self.status_bar = StatusBar(self)
|
||||
self.stack = Stack(self)
|
||||
self.bd_splitter = Splitter('book_details_splitter',
|
||||
_('Book Details'), I('book.svg'),
|
||||
orientation=Qt.Vertical, parent=self, side_index=1)
|
||||
self._layout_mem = [QWidget(self), QHBoxLayout()]
|
||||
self._layout_mem[0].setLayout(self._layout_mem[1])
|
||||
l = self._layout_mem[1]
|
||||
l.addWidget(self.stack)
|
||||
self.sidebar = SideBar([getattr(self, x+'_splitter')
|
||||
for x in ('bd', 'tb', 'cb')], self.jobs_button, parent=self)
|
||||
l.addWidget(self.sidebar)
|
||||
self.bd_splitter.addWidget(self._layout_mem[0])
|
||||
self.bd_splitter.addWidget(self.status_bar)
|
||||
self.bd_splitter.setCollapsible((self.bd_splitter.side_index+1)%2, False)
|
||||
self.centralwidget.layout().addWidget(self.bd_splitter)
|
||||
|
||||
def finalize_layout(self):
|
||||
m = self.library_view.model()
|
||||
if m.rowCount(None) > 0:
|
||||
self.library_view.set_current_row(0)
|
||||
m.current_changed(self.library_view.currentIndex(),
|
||||
self.library_view.currentIndex())
|
||||
|
||||
|
||||
def save_layout_state(self):
|
||||
for x in ('library', 'memory', 'card_a', 'card_b'):
|
||||
getattr(self, x+'_view').save_state()
|
||||
|
||||
for x in ('cb', 'tb', 'bd'):
|
||||
getattr(self, x+'_splitter').save_state()
|
||||
|
||||
def read_layout_settings(self):
|
||||
# View states are restored automatically when set_database is called
|
||||
|
||||
for x in ('cb', 'tb', 'bd'):
|
||||
getattr(self, x+'_splitter').restore_state()
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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
|
||||
|
@ -26,6 +26,15 @@ class BooksView(QTableView): # {{{
|
||||
|
||||
def __init__(self, parent, modelcls=BooksModel):
|
||||
QTableView.__init__(self, parent)
|
||||
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropOverwriteMode(False)
|
||||
self.setDragDropMode(self.DragDrop)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(self.SelectRows)
|
||||
self.setShowGrid(False)
|
||||
self.setWordWrap(False)
|
||||
|
||||
self.rating_delegate = RatingDelegate(self)
|
||||
self.timestamp_delegate = DateDelegate(self)
|
||||
self.pubdate_delegate = PubDateDelegate(self)
|
||||
@ -434,6 +443,18 @@ class BooksView(QTableView): # {{{
|
||||
self.scrollTo(self.model().index(row, i))
|
||||
break
|
||||
|
||||
def set_current_row(self, row, select=True):
|
||||
if row > -1:
|
||||
h = self.horizontalHeader()
|
||||
for i in range(h.count()):
|
||||
if not h.isSectionHidden(i):
|
||||
index = self.model().index(row, i)
|
||||
self.setCurrentIndex(index)
|
||||
if select:
|
||||
sm = self.selectionModel()
|
||||
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
||||
break
|
||||
|
||||
def close(self):
|
||||
self._model.close()
|
||||
|
||||
|
@ -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>
|
||||
@ -304,270 +310,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Splitter" name="vertical_splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="library">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="Splitter" name="horizontal_splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="TagsView" name="tags_view">
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="popularity">
|
||||
<property name="text">
|
||||
<string>Sort by &popularity</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="tag_match">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match any</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match all</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="edit_categories">
|
||||
<property name="toolTip">
|
||||
<string>Create, edit, and delete user categories</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Manage &user categories</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="">
|
||||
<layout class="QVBoxLayout" name="cb_layout">
|
||||
<item>
|
||||
<widget class="BooksView" name="library_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="main_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="memory_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="card_a_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="card_a_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="card_b_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="card_b_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SideBar" name="sidebar" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="StatusBar" name="status_bar" native="true"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="tool_bar">
|
||||
@ -804,26 +546,11 @@
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>BooksView</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>calibre/gui2/library/views.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>LocationView</class>
|
||||
<extends>QListView</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DeviceBooksView</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>calibre/gui2/library/views.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>TagsView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>calibre/gui2/tag_view.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SearchBox2</class>
|
||||
<extends>QComboBox</extends>
|
||||
@ -834,24 +561,6 @@
|
||||
<extends>QComboBox</extends>
|
||||
<header>calibre.gui2.search_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>StatusBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>calibre/gui2/status.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Splitter</class>
|
||||
<extends>QSplitter</extends>
|
||||
<header>calibre/gui2/widgets.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SideBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>calibre/gui2/sidebar.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../resources/images.qrc"/>
|
||||
|
@ -713,8 +713,9 @@ void PictureFlowPrivate::render()
|
||||
QPainter painter;
|
||||
painter.begin(&buffer);
|
||||
|
||||
QFont font("Arial", FONT_SIZE);
|
||||
QFont font = QFont();
|
||||
font.setBold(true);
|
||||
font.setPointSize(FONT_SIZE);
|
||||
painter.setFont(font);
|
||||
painter.setPen(Qt::white);
|
||||
//painter.setPen(QColor(255,255,255,127));
|
||||
@ -763,8 +764,9 @@ void PictureFlowPrivate::render()
|
||||
QPainter painter;
|
||||
painter.begin(&buffer);
|
||||
|
||||
QFont font("Arial", FONT_SIZE);
|
||||
QFont font = QFont();
|
||||
font.setBold(true);
|
||||
font.setPointSize(FONT_SIZE);
|
||||
painter.setFont(font);
|
||||
|
||||
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
||||
|
@ -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)
|
@ -1,147 +0,0 @@
|
||||
#!/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 functools import partial
|
||||
|
||||
from PyQt4.Qt import QToolBar, Qt, QIcon, QSizePolicy, QWidget, \
|
||||
QSize, QToolButton
|
||||
|
||||
from calibre.gui2 import dynamic
|
||||
|
||||
class SideBar(QToolBar):
|
||||
|
||||
toggle_texts = {
|
||||
'book_info' : (_('Show Book Details'), _('Hide Book Details')),
|
||||
'tag_browser' : (_('Show Tag Browser'), _('Hide Tag Browser')),
|
||||
'cover_browser': (_('Show Cover Browser'), _('Hide Cover Browser')),
|
||||
}
|
||||
toggle_icons = {
|
||||
'book_info' : 'book.svg',
|
||||
'tag_browser' : 'tags.svg',
|
||||
'cover_browser': 'cover_flow.svg',
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QToolBar.__init__(self, _('Side bar'), parent)
|
||||
self.setOrientation(Qt.Vertical)
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.setIconSize(QSize(48, 48))
|
||||
|
||||
for ac in ('book_info', 'tag_browser', 'cover_browser'):
|
||||
action = self.addAction(QIcon(I(self.toggle_icons[ac])),
|
||||
self.toggle_texts[ac][1], getattr(self, '_toggle_'+ac))
|
||||
setattr(self, 'action_toggle_'+ac, action)
|
||||
w = self.widgetForAction(action)
|
||||
w.setCheckable(True)
|
||||
setattr(self, 'show_'+ac, partial(getattr(self, '_toggle_'+ac),
|
||||
show=True))
|
||||
setattr(self, 'hide_'+ac, partial(getattr(self, '_toggle_'+ac),
|
||||
show=False))
|
||||
|
||||
|
||||
self.spacer = QWidget(self)
|
||||
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
self.addWidget(self.spacer)
|
||||
|
||||
self.show_cover_browser = partial(self._toggle_cover_browser, show=True)
|
||||
self.hide_cover_browser = partial(self._toggle_cover_browser,
|
||||
show=False)
|
||||
for ch in self.children():
|
||||
if isinstance(ch, QToolButton):
|
||||
ch.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def initialize(self, jobs_button, cover_browser, toggle_cover_browser,
|
||||
cover_browser_error, vertical_splitter, horizontal_splitter):
|
||||
self.cover_browser, self.do_toggle_cover_browser = cover_browser, \
|
||||
toggle_cover_browser
|
||||
if self.cover_browser is None:
|
||||
self.action_toggle_cover_browser.setEnabled(False)
|
||||
self.action_toggle_cover_browser.setText(
|
||||
_('Cover browser could not be loaded: ') + cover_browser_error)
|
||||
else:
|
||||
self.cover_browser.stop.connect(self.hide_cover_browser)
|
||||
self._toggle_cover_browser(dynamic.get('cover_flow_visible', False))
|
||||
|
||||
self.horizontal_splitter = horizontal_splitter
|
||||
self.vertical_splitter = vertical_splitter
|
||||
|
||||
tb_state = dynamic.get('tag_browser_state', None)
|
||||
if tb_state is not None:
|
||||
self.horizontal_splitter.restoreState(tb_state)
|
||||
tb_last_open_state = dynamic.get('tag_browser_last_open_state', None)
|
||||
if tb_last_open_state is not None and \
|
||||
not self.horizontal_splitter.is_side_index_hidden:
|
||||
self.horizontal_splitter.restoreState(tb_last_open_state)
|
||||
|
||||
bi_state = dynamic.get('book_info_state', None)
|
||||
if bi_state is not None:
|
||||
self.vertical_splitter.restoreState(bi_state)
|
||||
bi_last_open_state = dynamic.get('book_info_last_open_state', None)
|
||||
if bi_last_open_state is not None and \
|
||||
not self.vertical_splitter.is_side_index_hidden:
|
||||
self.vertical_splitter.restoreState(bi_last_open_state)
|
||||
|
||||
self.horizontal_splitter.initialize(name='tag_browser')
|
||||
self.vertical_splitter.initialize(name='book_info')
|
||||
self.view_status_changed('book_info', not
|
||||
self.vertical_splitter.is_side_index_hidden)
|
||||
self.view_status_changed('tag_browser', not
|
||||
self.horizontal_splitter.is_side_index_hidden)
|
||||
self.vertical_splitter.state_changed.connect(partial(self.view_status_changed,
|
||||
'book_info'), type=Qt.QueuedConnection)
|
||||
self.horizontal_splitter.state_changed.connect(partial(self.view_status_changed,
|
||||
'tag_browser'), type=Qt.QueuedConnection)
|
||||
self.addWidget(jobs_button)
|
||||
|
||||
|
||||
|
||||
def view_status_changed(self, name, visible):
|
||||
action = getattr(self, 'action_toggle_'+name)
|
||||
texts = self.toggle_texts[name]
|
||||
action.setText(texts[int(visible)])
|
||||
w = self.widgetForAction(action)
|
||||
w.setCheckable(True)
|
||||
w.setChecked(visible)
|
||||
|
||||
def location_changed(self, location):
|
||||
is_lib = location == 'library'
|
||||
for ac in ('cover_browser', 'tag_browser'):
|
||||
ac = getattr(self, 'action_toggle_'+ac)
|
||||
ac.setEnabled(is_lib)
|
||||
self.widgetForAction(ac).setVisible(is_lib)
|
||||
|
||||
def save_state(self):
|
||||
dynamic.set('cover_flow_visible', self.is_cover_browser_visible)
|
||||
dynamic.set('tag_browser_state',
|
||||
str(self.horizontal_splitter.saveState()))
|
||||
dynamic.set('book_info_state',
|
||||
str(self.vertical_splitter.saveState()))
|
||||
|
||||
|
||||
@property
|
||||
def is_cover_browser_visible(self):
|
||||
return self.cover_browser is not None and self.cover_browser.isVisible()
|
||||
|
||||
def _toggle_cover_browser(self, show=None):
|
||||
if show is None:
|
||||
show = not self.is_cover_browser_visible
|
||||
self.do_toggle_cover_browser(show)
|
||||
self.view_status_changed('cover_browser', show)
|
||||
|
||||
def external_cover_flow_finished(self, *args):
|
||||
self.view_status_changed('cover_browser', False)
|
||||
|
||||
def _toggle_tag_browser(self, show=None):
|
||||
self.horizontal_splitter.toggle_side_index()
|
||||
|
||||
def _toggle_book_info(self, show=None):
|
||||
self.vertical_splitter.toggle_side_index()
|
||||
|
||||
|
@ -10,9 +10,11 @@ Browsing book collection by tags.
|
||||
from itertools import izip
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
||||
QFont, QSize, QIcon, QPoint, \
|
||||
QAbstractItemModel, QVariant, QModelIndex, QMenu
|
||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
|
||||
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
|
||||
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
|
||||
QPushButton, QWidget
|
||||
|
||||
from calibre.gui2 import config, NONE
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.library.field_metadata import TagsIcons
|
||||
@ -597,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)
|
||||
@ -606,6 +609,8 @@ 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(lambda x:
|
||||
self.do_user_categories_edit())
|
||||
|
||||
def do_user_categories_edit(self, on_category=None):
|
||||
d = TagCategories(self, self.library_view.model().db, on_category)
|
||||
@ -633,3 +638,29 @@ class TagBrowserMixin(object): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class TagBrowserWidget(QWidget): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
QWidget.__init__(self, parent)
|
||||
self._layout = QVBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
|
||||
parent.tags_view = TagsView(parent)
|
||||
self._layout.addWidget(parent.tags_view)
|
||||
|
||||
parent.popularity = QCheckBox(parent)
|
||||
parent.popularity.setText(_('Sort by &popularity'))
|
||||
self._layout.addWidget(parent.popularity)
|
||||
|
||||
parent.tag_match = QComboBox(parent)
|
||||
for x in (_('Match any'), _('Match all')):
|
||||
parent.tag_match.addItem(x)
|
||||
parent.tag_match.setCurrentIndex(0)
|
||||
self._layout.addWidget(parent.tag_match)
|
||||
|
||||
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,\
|
||||
@ -37,7 +36,7 @@ from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
|
||||
Dispatcher, gprefs, \
|
||||
max_available_height, config, info_dialog, \
|
||||
GetMetadata
|
||||
from calibre.gui2.cover_flow import pictureflowerror, CoverFlowMixin
|
||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||
from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS
|
||||
from calibre.gui2.wizard import move_library
|
||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||
@ -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.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
|
||||
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin
|
||||
|
||||
class Listener(Thread): # {{{
|
||||
|
||||
@ -106,7 +105,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||
# }}}
|
||||
|
||||
class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin):
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin):
|
||||
'The main GUI'
|
||||
|
||||
def set_default_thumbnail(self, height):
|
||||
@ -143,35 +143,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self.check_messages_timer.start(1000)
|
||||
|
||||
Ui_MainWindow.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(__appname__)
|
||||
|
||||
# Jobs Button {{{
|
||||
self.job_manager = JobManager()
|
||||
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
||||
self.jobs_button = JobsButton()
|
||||
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
|
||||
# }}}
|
||||
|
||||
LayoutMixin.__init__(self)
|
||||
|
||||
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
|
||||
self.get_metadata = GetMetadata()
|
||||
self.read_settings()
|
||||
self.job_manager = JobManager()
|
||||
self.emailer = Emailer()
|
||||
self.emailer.start()
|
||||
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
||||
self.upload_memory = {}
|
||||
self.delete_memory = {}
|
||||
self.conversion_jobs = {}
|
||||
@ -225,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)
|
||||
|
||||
@ -265,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)
|
||||
@ -273,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)
|
||||
|
||||
@ -280,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'])
|
||||
@ -314,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 ################################
|
||||
|
||||
@ -324,18 +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)
|
||||
|
||||
# Jobs Button {{{
|
||||
self.jobs_button = JobsButton()
|
||||
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
|
||||
# }}}
|
||||
|
||||
####################### Side Bar ###############################
|
||||
|
||||
self.sidebar.initialize(self.jobs_button, self.cover_flow,
|
||||
self.toggle_cover_flow, pictureflowerror,
|
||||
self.vertical_splitter, self.horizontal_splitter)
|
||||
|
||||
|
||||
if config['autolaunch_server']:
|
||||
@ -362,13 +335,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
|
||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||
|
||||
|
||||
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()
|
||||
self.read_settings()
|
||||
self.finalize_layout()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
MainWindow.resizeEvent(self, ev)
|
||||
@ -451,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:
|
||||
@ -559,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):
|
||||
@ -1135,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():
|
||||
@ -1180,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()
|
||||
|
||||
############################################################################
|
||||
|
||||
@ -1850,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 ##################################
|
||||
@ -1934,7 +1832,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
|
||||
self.stack.setCurrentIndex(page)
|
||||
self.status_bar.reset_info()
|
||||
self.sidebar.location_changed(location)
|
||||
for x in ('tb', 'cb'):
|
||||
splitter = getattr(self, x+'_splitter')
|
||||
splitter.button.setEnabled(location == 'library')
|
||||
if location == 'library':
|
||||
self.action_edit.setEnabled(True)
|
||||
self.action_merge.setEnabled(True)
|
||||
@ -2037,14 +1937,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||
if geometry is not None:
|
||||
self.restoreGeometry(geometry)
|
||||
self.read_toolbar_settings()
|
||||
self.read_layout_settings()
|
||||
|
||||
def write_settings(self):
|
||||
config.set('main_window_geometry', self.saveGeometry())
|
||||
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||
self.sidebar.save_state()
|
||||
for view in ('library_view', 'memory_view', 'card_a_view',
|
||||
'card_b_view'):
|
||||
getattr(self, view).save_state()
|
||||
self.save_layout_state()
|
||||
|
||||
def restart(self):
|
||||
self.quit(restart=True)
|
||||
|
@ -4,16 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
Miscellaneous widgets used in the GUI
|
||||
'''
|
||||
import re, os, traceback
|
||||
|
||||
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
||||
QListWidgetItem, QTextCharFormat, QApplication, \
|
||||
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
||||
QPixmap, QPalette, QSplitterHandle, \
|
||||
QPixmap, QPalette, QSplitterHandle, QToolButton, \
|
||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
||||
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
|
||||
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
||||
QMenu, QStringListModel, QCompleter, QStringList
|
||||
QMenu, QStringListModel, QCompleter, QStringList, \
|
||||
QTimer
|
||||
|
||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, dynamic
|
||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
||||
|
||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||
from calibre import fit_image, human_readable
|
||||
@ -927,6 +929,7 @@ class SplitterHandle(QSplitterHandle):
|
||||
self.double_clicked.connect(splitter.double_clicked,
|
||||
type=Qt.QueuedConnection)
|
||||
self.highlight = False
|
||||
self.setToolTip(_('Drag to resize')+' '+splitter.label)
|
||||
|
||||
def splitter_moved(self, *args):
|
||||
oh = self.highlight
|
||||
@ -944,20 +947,62 @@ class SplitterHandle(QSplitterHandle):
|
||||
def mouseDoubleClickEvent(self, ev):
|
||||
self.double_clicked.emit(self)
|
||||
|
||||
class LayoutButton(QToolButton):
|
||||
|
||||
def __init__(self, icon, text, splitter, parent=None):
|
||||
QToolButton.__init__(self, parent)
|
||||
self.label = text
|
||||
self.setIcon(QIcon(icon))
|
||||
self.setCheckable(True)
|
||||
|
||||
self.splitter = splitter
|
||||
splitter.state_changed.connect(self.update_state)
|
||||
|
||||
def set_state_to_show(self, *args):
|
||||
self.setChecked(False)
|
||||
label =_('Show')
|
||||
self.setText(label + ' ' + self.label)
|
||||
|
||||
def set_state_to_hide(self, *args):
|
||||
self.setChecked(True)
|
||||
label = _('Hide')
|
||||
self.setText(label + ' ' + self.label)
|
||||
|
||||
def update_state(self, *args):
|
||||
if self.splitter.is_side_index_hidden:
|
||||
self.set_state_to_show()
|
||||
else:
|
||||
self.set_state_to_hide()
|
||||
|
||||
class Splitter(QSplitter):
|
||||
|
||||
state_changed = pyqtSignal(object)
|
||||
|
||||
def __init__(self, *args):
|
||||
QSplitter.__init__(self, *args)
|
||||
def __init__(self, name, label, icon, initial_show=True,
|
||||
initial_side_size=120, connect_button=True,
|
||||
orientation=Qt.Horizontal, side_index=0, parent=None):
|
||||
QSplitter.__init__(self, parent)
|
||||
self.resize_timer = QTimer(self)
|
||||
self.resize_timer.setSingleShot(True)
|
||||
self.desired_side_size = initial_side_size
|
||||
self.desired_show = initial_show
|
||||
self.resize_timer.setInterval(5)
|
||||
self.resize_timer.timeout.connect(self.do_resize)
|
||||
self.setOrientation(orientation)
|
||||
self.side_index = side_index
|
||||
self._name = name
|
||||
self.label = label
|
||||
self.initial_side_size = initial_side_size
|
||||
self.initial_show = initial_show
|
||||
self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection)
|
||||
self.button = LayoutButton(icon, label, self)
|
||||
if connect_button:
|
||||
self.button.clicked.connect(self.double_clicked)
|
||||
|
||||
def createHandle(self):
|
||||
return SplitterHandle(self.orientation(), self)
|
||||
|
||||
def initialize(self, name=None):
|
||||
if name is not None:
|
||||
self._name = name
|
||||
def initialize(self):
|
||||
for i in range(self.count()):
|
||||
h = self.handle(i)
|
||||
if h is not None:
|
||||
@ -965,40 +1010,115 @@ class Splitter(QSplitter):
|
||||
self.state_changed.emit(not self.is_side_index_hidden)
|
||||
|
||||
def splitter_moved(self, *args):
|
||||
self.desired_side_size = self.side_index_size
|
||||
self.state_changed.emit(not self.is_side_index_hidden)
|
||||
|
||||
@property
|
||||
def side_index(self):
|
||||
return 0 if self.orientation() == Qt.Horizontal else 1
|
||||
|
||||
@property
|
||||
def is_side_index_hidden(self):
|
||||
sizes = list(self.sizes())
|
||||
return sizes[self.side_index] == 0
|
||||
|
||||
def toggle_side_index(self):
|
||||
self.double_clicked(None)
|
||||
@property
|
||||
def save_name(self):
|
||||
ori = 'horizontal' if self.orientation() == Qt.Horizontal \
|
||||
else 'vertical'
|
||||
return self._name + '_' + ori
|
||||
|
||||
def double_clicked(self, handle):
|
||||
visible = not self.is_side_index_hidden
|
||||
sizes = list(self.sizes())
|
||||
if 0 in sizes:
|
||||
idx = sizes.index(0)
|
||||
sizes[idx] = 80
|
||||
else:
|
||||
sizes[self.side_index] = 0
|
||||
def print_sizes(self):
|
||||
if self.count() > 1:
|
||||
print self.save_name, 'side:', self.side_index_size, 'other:',
|
||||
print list(self.sizes())[self.other_index]
|
||||
|
||||
if visible:
|
||||
dynamic.set(self._name + '_last_open_state', str(self.saveState()))
|
||||
@dynamic_property
|
||||
def side_index_size(self):
|
||||
def fget(self):
|
||||
if self.count() < 2: return 0
|
||||
return self.sizes()[self.side_index]
|
||||
|
||||
def fset(self, val):
|
||||
if self.count() < 2: return
|
||||
if val == 0 and not self.is_side_index_hidden:
|
||||
self.save_state()
|
||||
sizes = list(self.sizes())
|
||||
for i in range(len(sizes)):
|
||||
sizes[i] = val if i == self.side_index else 10
|
||||
self.setSizes(sizes)
|
||||
total = sum(self.sizes())
|
||||
sizes = list(self.sizes())
|
||||
for i in range(len(sizes)):
|
||||
sizes[i] = val if i == self.side_index else total-val
|
||||
self.setSizes(sizes)
|
||||
self.initialize()
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def do_resize(self, *args):
|
||||
orig = self.desired_side_size
|
||||
QSplitter.resizeEvent(self, self._resize_ev)
|
||||
if orig > 20 and self.desired_show:
|
||||
c = 0
|
||||
while abs(self.side_index_size - orig) > 10 and c < 5:
|
||||
self.apply_state(self.get_state(), save_desired=False)
|
||||
c += 1
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
if self.resize_timer.isActive():
|
||||
self.resize_timer.stop()
|
||||
self._resize_ev = ev
|
||||
self.resize_timer.start()
|
||||
|
||||
def get_state(self):
|
||||
if self.count() < 2: return (False, 200)
|
||||
return (self.desired_show, self.desired_side_size)
|
||||
|
||||
def apply_state(self, state, save_desired=True):
|
||||
if state[0]:
|
||||
self.side_index_size = state[1]
|
||||
if save_desired:
|
||||
self.desired_side_size = self.side_index_size
|
||||
else:
|
||||
state = dynamic.get(self._name+ '_last_open_state', None)
|
||||
if state is not None:
|
||||
self.restoreState(state)
|
||||
else:
|
||||
self.setSizes(sizes)
|
||||
self.initialize()
|
||||
self.side_index_size = 0
|
||||
self.desired_show = state[0]
|
||||
|
||||
def default_state(self):
|
||||
return (self.initial_show, self.initial_side_size)
|
||||
|
||||
# Public API {{{
|
||||
|
||||
def save_state(self):
|
||||
if self.count() > 1:
|
||||
gprefs[self.save_name+'_state'] = self.get_state()
|
||||
|
||||
@property
|
||||
def other_index(self):
|
||||
return (self.side_index+1)%2
|
||||
|
||||
def restore_state(self):
|
||||
if self.count() > 1:
|
||||
state = gprefs.get(self.save_name+'_state',
|
||||
self.default_state())
|
||||
self.apply_state(state, save_desired=False)
|
||||
self.desired_side_size = state[1]
|
||||
|
||||
def toggle_side_pane(self, hide=None):
|
||||
if hide is None:
|
||||
action = 'show' if self.is_side_index_hidden else 'hide'
|
||||
else:
|
||||
action = 'hide' if hide else 'show'
|
||||
getattr(self, action+'_side_pane')()
|
||||
|
||||
def show_side_pane(self):
|
||||
if self.count() < 2 or not self.is_side_index_hidden:
|
||||
return
|
||||
self.apply_state((True, self.desired_side_size))
|
||||
|
||||
def hide_side_pane(self):
|
||||
if self.count() < 2 or self.is_side_index_hidden:
|
||||
return
|
||||
self.apply_state((False, self.desired_side_size))
|
||||
|
||||
def double_clicked(self, *args):
|
||||
self.toggle_side_pane()
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -96,14 +96,14 @@ class Kobo(Device):
|
||||
class Booq(Device):
|
||||
name = 'Booq Reader'
|
||||
manufacturer = 'Booq'
|
||||
output_profile = 'prs505'
|
||||
output_profile = 'sony'
|
||||
output_format = 'EPUB'
|
||||
id = 'booq'
|
||||
|
||||
class TheBook(Device):
|
||||
name = 'The Book'
|
||||
manufacturer = 'Augen'
|
||||
output_profile = 'prs505'
|
||||
output_profile = 'sony'
|
||||
output_format = 'EPUB'
|
||||
id = 'thebook'
|
||||
|
||||
|
@ -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)
|
||||
self.data.set(id, self.FIELD_MAP['sort'], title_sort(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:
|
||||
@ -1835,6 +1842,8 @@ books_series_link feeds
|
||||
os.remove(self.dbpath)
|
||||
shutil.copyfile(dest, self.dbpath)
|
||||
self.connect()
|
||||
self.field_metadata.remove_dynamic_categories()
|
||||
self.field_metadata.remove_custom_fields()
|
||||
self.initialize_dynamic()
|
||||
self.refresh()
|
||||
if os.path.exists(dest):
|
||||
|
@ -379,6 +379,17 @@ class FieldMetadata(dict):
|
||||
self._add_search_terms_to_map(key, [key])
|
||||
self.custom_label_to_key_map[label] = key
|
||||
|
||||
def remove_custom_fields(self):
|
||||
for key in self.get_custom_fields():
|
||||
del self._tb_cats[key]
|
||||
|
||||
def remove_dynamic_categories(self):
|
||||
for key in list(self._tb_cats.keys()):
|
||||
val = self._tb_cats[key]
|
||||
if val['is_category'] and val['kind'] in ('user', 'search'):
|
||||
del self._tb_cats[key]
|
||||
|
||||
|
||||
def add_user_category(self, label, name):
|
||||
if label in self._tb_cats:
|
||||
raise ValueError('Duplicate user field [%s]'%(label))
|
||||
|
@ -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)
|
||||
self.conn.create_function('title_sort', 1, title_sort)
|
||||
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)
|
||||
|
@ -137,7 +137,7 @@ class Feed(object):
|
||||
|
||||
def populate_from_preparsed_feed(self, title, articles, oldest_article=7,
|
||||
max_articles_per_feed=100):
|
||||
self.title = title if title else _('Unknown feed')
|
||||
self.title = unicode(title if title else _('Unknown feed'))
|
||||
self.description = ''
|
||||
self.image_url = None
|
||||
self.articles = []
|
||||
|
@ -65,6 +65,7 @@ class NavBarTemplate(Template):
|
||||
text = 'This article was downloaded by '
|
||||
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
||||
p[0].tail = ' from '
|
||||
navbar.append(p)
|
||||
navbar.append(BR())
|
||||
navbar.append(BR())
|
||||
else:
|
||||
@ -111,6 +112,7 @@ class TouchscreenNavBarTemplate(Template):
|
||||
text = 'This article was downloaded by '
|
||||
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
||||
p[0].tail = ' from '
|
||||
navbar.append(p)
|
||||
navbar.append(BR())
|
||||
navbar.append(BR())
|
||||
else:
|
||||
|
Loading…
x
Reference in New Issue
Block a user