mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
f747483f3d
@ -12,6 +12,7 @@ from Queue import Empty
|
|||||||
|
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
||||||
from calibre import extract, CurrentDir, prints
|
from calibre import extract, CurrentDir, prints
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.utils.ipc.job import ParallelJob
|
from calibre.utils.ipc.job import ParallelJob
|
||||||
@ -21,6 +22,10 @@ def extract_comic(path_to_comic_file):
|
|||||||
Un-archive the comic file.
|
Un-archive the comic file.
|
||||||
'''
|
'''
|
||||||
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
|
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
|
||||||
|
if not isinstance(tdir, unicode):
|
||||||
|
# Needed in case the zip file has wrongly encoded unicode file/dir
|
||||||
|
# names
|
||||||
|
tdir = tdir.decode(filesystem_encoding)
|
||||||
extract(path_to_comic_file, tdir)
|
extract(path_to_comic_file, tdir)
|
||||||
return tdir
|
return tdir
|
||||||
|
|
||||||
|
@ -716,6 +716,7 @@ class MobiReader(object):
|
|||||||
ent_pat = re.compile(r'&(\S+?);')
|
ent_pat = re.compile(r'&(\S+?);')
|
||||||
if elems:
|
if elems:
|
||||||
tocobj = TOC()
|
tocobj = TOC()
|
||||||
|
found = False
|
||||||
reached = False
|
reached = False
|
||||||
for x in root.iter():
|
for x in root.iter():
|
||||||
if x == elems[-1]:
|
if x == elems[-1]:
|
||||||
@ -732,7 +733,8 @@ class MobiReader(object):
|
|||||||
text = ent_pat.sub(entity_to_unicode, text)
|
text = ent_pat.sub(entity_to_unicode, text)
|
||||||
tocobj.add_item(toc.partition('#')[0], href[1:],
|
tocobj.add_item(toc.partition('#')[0], href[1:],
|
||||||
text)
|
text)
|
||||||
if reached and x.get('class', None) == 'mbp_pagebreak':
|
found = True
|
||||||
|
if reached and found and x.get('class', None) == 'mbp_pagebreak':
|
||||||
break
|
break
|
||||||
if tocobj is not None:
|
if tocobj is not None:
|
||||||
opf.set_toc(tocobj)
|
opf.set_toc(tocobj)
|
||||||
|
@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QMenu, QToolButton, QDialog, QVBoxLayout
|
from PyQt4.Qt import QMenu
|
||||||
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
class StoreAction(InterfaceAction):
|
class StoreAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Store'
|
name = 'Store'
|
||||||
action_spec = (_('Store'), 'store.png', None, None)
|
action_spec = (_('Get books'), 'store.png', None, None)
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.search)
|
self.qaction.triggered.connect(self.search)
|
||||||
self.store_menu = QMenu()
|
self.store_menu = QMenu()
|
||||||
self.load_menu()
|
self.load_menu()
|
||||||
|
|
||||||
def load_menu(self):
|
def load_menu(self):
|
||||||
self.store_menu.clear()
|
self.store_menu.clear()
|
||||||
self.store_menu.addAction(_('Search'), self.search)
|
self.store_menu.addAction(_('Search'), self.search)
|
||||||
@ -29,11 +29,11 @@ class StoreAction(InterfaceAction):
|
|||||||
for n, p in self.gui.istores.items():
|
for n, p in self.gui.istores.items():
|
||||||
self.store_menu.addAction(n, partial(self.open_store, p))
|
self.store_menu.addAction(n, partial(self.open_store, p))
|
||||||
self.qaction.setMenu(self.store_menu)
|
self.qaction.setMenu(self.store_menu)
|
||||||
|
|
||||||
def search(self):
|
def search(self):
|
||||||
from calibre.gui2.store.search import SearchDialog
|
from calibre.gui2.store.search import SearchDialog
|
||||||
sd = SearchDialog(self.gui.istores, self.gui)
|
sd = SearchDialog(self.gui.istores, self.gui)
|
||||||
sd.exec_()
|
sd.exec_()
|
||||||
|
|
||||||
def open_store(self, store_plugin):
|
def open_store(self, store_plugin):
|
||||||
store_plugin.open(self.gui)
|
store_plugin.open(self.gui)
|
||||||
|
@ -68,7 +68,7 @@ class DaysOfWeek(Base):
|
|||||||
def initialize(self, typ=None, val=None):
|
def initialize(self, typ=None, val=None):
|
||||||
if typ is None:
|
if typ is None:
|
||||||
typ = 'day/time'
|
typ = 'day/time'
|
||||||
val = (-1, 9, 0)
|
val = (-1, 6, 0)
|
||||||
if typ == 'day/time':
|
if typ == 'day/time':
|
||||||
val = convert_day_time_schedule(val)
|
val = convert_day_time_schedule(val)
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ class DaysOfMonth(Base):
|
|||||||
|
|
||||||
def initialize(self, typ=None, val=None):
|
def initialize(self, typ=None, val=None):
|
||||||
if val is None:
|
if val is None:
|
||||||
val = ((1,), 9, 0)
|
val = ((1,), 6, 0)
|
||||||
days_of_month, hour, minute = val
|
days_of_month, hour, minute = val
|
||||||
self.days.setText(', '.join(map(str, map(int, days_of_month))))
|
self.days.setText(', '.join(map(str, map(int, days_of_month))))
|
||||||
self.time.setTime(QTime(hour, minute))
|
self.time.setTime(QTime(hour, minute))
|
||||||
@ -380,7 +380,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
if d < timedelta(days=366):
|
if d < timedelta(days=366):
|
||||||
ld_text = tm
|
ld_text = tm
|
||||||
else:
|
else:
|
||||||
typ, sch = 'day/time', (-1, 9, 0)
|
typ, sch = 'day/time', (-1, 6, 0)
|
||||||
sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
|
sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
|
||||||
'interval':2}[typ]
|
'interval':2}[typ]
|
||||||
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
|
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
|
||||||
|
@ -200,13 +200,6 @@ class SearchBar(QWidget): # {{{
|
|||||||
x.setIcon(QIcon(I('arrow-down.png')))
|
x.setIcon(QIcon(I('arrow-down.png')))
|
||||||
l.addWidget(x)
|
l.addWidget(x)
|
||||||
|
|
||||||
x = parent.search_options_button = QToolButton(self)
|
|
||||||
x.setIcon(QIcon(I('config.png')))
|
|
||||||
x.setObjectName("search_option_button")
|
|
||||||
l.addWidget(x)
|
|
||||||
x.setToolTip(_("Change the way searching for books works"))
|
|
||||||
x.setVisible(False)
|
|
||||||
|
|
||||||
x = parent.saved_search = SavedSearchBox(self)
|
x = parent.saved_search = SavedSearchBox(self)
|
||||||
x.setMaximumSize(QSize(150, 16777215))
|
x.setMaximumSize(QSize(150, 16777215))
|
||||||
x.setMinimumContentsLength(15)
|
x.setMinimumContentsLength(15)
|
||||||
@ -324,6 +317,8 @@ class BaseToolBar(QToolBar): # {{{
|
|||||||
QToolBar.resizeEvent(self, ev)
|
QToolBar.resizeEvent(self, ev)
|
||||||
style = self.get_text_style()
|
style = self.get_text_style()
|
||||||
self.setToolButtonStyle(style)
|
self.setToolButtonStyle(style)
|
||||||
|
if hasattr(self, 'd_widget'):
|
||||||
|
self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly)
|
||||||
|
|
||||||
def get_text_style(self):
|
def get_text_style(self):
|
||||||
style = Qt.ToolButtonTextUnderIcon
|
style = Qt.ToolButtonTextUnderIcon
|
||||||
@ -406,7 +401,8 @@ class ToolBar(BaseToolBar): # {{{
|
|||||||
self.d_widget.layout().addWidget(self.donate_button)
|
self.d_widget.layout().addWidget(self.donate_button)
|
||||||
if isosx:
|
if isosx:
|
||||||
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
||||||
self.d_widget.layout().addWidget(QLabel(u'\u00a0'))
|
self.d_widget.filler = QLabel(u'\u00a0')
|
||||||
|
self.d_widget.layout().addWidget(self.d_widget.filler)
|
||||||
bar.addWidget(self.d_widget)
|
bar.addWidget(self.d_widget)
|
||||||
self.showing_donate = True
|
self.showing_donate = True
|
||||||
elif what in self.gui.iactions:
|
elif what in self.gui.iactions:
|
||||||
|
@ -743,6 +743,8 @@ class BooksView(QTableView): # {{{
|
|||||||
id_to_select = self._model.get_current_highlighted_id()
|
id_to_select = self._model.get_current_highlighted_id()
|
||||||
if id_to_select is not None:
|
if id_to_select is not None:
|
||||||
self.select_rows([id_to_select], using_ids=True)
|
self.select_rows([id_to_select], using_ids=True)
|
||||||
|
elif self._model.highlight_only:
|
||||||
|
self.clearSelection()
|
||||||
self.setFocus(Qt.OtherFocusReason)
|
self.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
def connect_to_search_box(self, sb, search_done):
|
def connect_to_search_box(self, sb, search_done):
|
||||||
|
@ -222,7 +222,8 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
'red, then the authors and this text do not match.')
|
'red, then the authors and this text do not match.')
|
||||||
LABEL = _('Author s&ort:')
|
LABEL = _('Author s&ort:')
|
||||||
|
|
||||||
def __init__(self, parent, authors_edit, autogen_button, db):
|
def __init__(self, parent, authors_edit, autogen_button, db,
|
||||||
|
copy_a_to_as_action, copy_as_to_a_action):
|
||||||
EnLineEdit.__init__(self, parent)
|
EnLineEdit.__init__(self, parent)
|
||||||
self.authors_edit = authors_edit
|
self.authors_edit = authors_edit
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -241,6 +242,8 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
self.textChanged.connect(self.update_state)
|
self.textChanged.connect(self.update_state)
|
||||||
|
|
||||||
autogen_button.clicked.connect(self.auto_generate)
|
autogen_button.clicked.connect(self.auto_generate)
|
||||||
|
copy_a_to_as_action.triggered.connect(self.auto_generate)
|
||||||
|
copy_as_to_a_action.triggered.connect(self.copy_to_authors)
|
||||||
self.update_state()
|
self.update_state()
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
@ -273,6 +276,14 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
self.setToolTip(tt)
|
self.setToolTip(tt)
|
||||||
self.setWhatsThis(tt)
|
self.setWhatsThis(tt)
|
||||||
|
|
||||||
|
def copy_to_authors(self):
|
||||||
|
aus = self.current_val
|
||||||
|
if aus:
|
||||||
|
ln, _, rest = aus.partition(',')
|
||||||
|
if rest:
|
||||||
|
au = rest.strip() + ' ' + ln.strip()
|
||||||
|
self.authors_edit.current_val = [au]
|
||||||
|
|
||||||
def auto_generate(self, *args):
|
def auto_generate(self, *args):
|
||||||
au = unicode(self.authors_edit.text())
|
au = unicode(self.authors_edit.text())
|
||||||
au = re.sub(r'\s+et al\.$', '', au)
|
au = re.sub(r'\s+et al\.$', '', au)
|
||||||
|
@ -13,7 +13,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
|
from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
|
||||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont,
|
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont,
|
||||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem,
|
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem,
|
||||||
QSizePolicy, QPalette, QFrame, QSize, QKeySequence)
|
QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu)
|
||||||
|
|
||||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||||
from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data
|
from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data
|
||||||
@ -102,15 +102,19 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
self.deduce_title_sort_button)
|
self.deduce_title_sort_button)
|
||||||
self.basic_metadata_widgets.extend([self.title, self.title_sort])
|
self.basic_metadata_widgets.extend([self.title, self.title_sort])
|
||||||
|
|
||||||
self.authors = AuthorsEdit(self)
|
self.deduce_author_sort_button = b = QToolButton(self)
|
||||||
self.deduce_author_sort_button = QToolButton(self)
|
b.setToolTip(_(
|
||||||
self.deduce_author_sort_button.setToolTip(_(
|
|
||||||
'Automatically create the author sort entry based on the current'
|
'Automatically create the author sort entry based on the current'
|
||||||
' author entry.\n'
|
' author entry.\n'
|
||||||
'Using this button to create author sort will change author sort from'
|
'Using this button to create author sort will change author sort from'
|
||||||
' red to green.'))
|
' red to green.'))
|
||||||
self.author_sort = AuthorSortEdit(self, self.authors,
|
b.m = m = QMenu()
|
||||||
self.deduce_author_sort_button, self.db)
|
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
|
||||||
|
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
|
||||||
|
b.setMenu(m)
|
||||||
|
self.authors = AuthorsEdit(self)
|
||||||
|
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
||||||
|
ac2)
|
||||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||||
|
|
||||||
self.swap_title_author_button = QToolButton(self)
|
self.swap_title_author_button = QToolButton(self)
|
||||||
|
@ -319,9 +319,12 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
|
|||||||
:return: True iff a restart is required for the changes made by the user to
|
:return: True iff a restart is required for the changes made by the user to
|
||||||
take effect
|
take effect
|
||||||
'''
|
'''
|
||||||
|
from calibre.gui2 import gprefs
|
||||||
pl = get_plugin(category, name)
|
pl = get_plugin(category, name)
|
||||||
d = ConfigDialog(parent)
|
d = ConfigDialog(parent)
|
||||||
d.resize(750, 550)
|
d.resize(750, 550)
|
||||||
|
conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name)
|
||||||
|
geom = gprefs.get(conf_name, None)
|
||||||
d.setWindowTitle(_('Configure ') + name)
|
d.setWindowTitle(_('Configure ') + name)
|
||||||
d.setWindowIcon(QIcon(I('config.png')))
|
d.setWindowIcon(QIcon(I('config.png')))
|
||||||
bb = QDialogButtonBox(d)
|
bb = QDialogButtonBox(d)
|
||||||
@ -345,7 +348,11 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
|
|||||||
mygui = True
|
mygui = True
|
||||||
w.genesis(gui)
|
w.genesis(gui)
|
||||||
w.initialize()
|
w.initialize()
|
||||||
|
if geom is not None:
|
||||||
|
d.restoreGeometry(geom)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
geom = bytearray(d.saveGeometry())
|
||||||
|
gprefs[conf_name] = geom
|
||||||
rr = getattr(d, 'restart_required', False)
|
rr = getattr(d, 'restart_required', False)
|
||||||
if show_restart_msg and rr:
|
if show_restart_msg and rr:
|
||||||
from calibre.gui2 import warning_dialog
|
from calibre.gui2 import warning_dialog
|
||||||
|
@ -364,7 +364,6 @@ class SearchBoxMixin(object): # {{{
|
|||||||
unicode(self.search.toolTip())))
|
unicode(self.search.toolTip())))
|
||||||
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
|
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
|
||||||
self.clear_button.setStatusTip(self.clear_button.toolTip())
|
self.clear_button.setStatusTip(self.clear_button.toolTip())
|
||||||
self.search_options_button.clicked.connect(self.search_options_button_clicked)
|
|
||||||
self.set_highlight_only_button_icon()
|
self.set_highlight_only_button_icon()
|
||||||
self.highlight_only_button.clicked.connect(self.highlight_only_clicked)
|
self.highlight_only_button.clicked.connect(self.highlight_only_clicked)
|
||||||
tt = _('Enable or disable search highlighting.') + '<br><br>'
|
tt = _('Enable or disable search highlighting.') + '<br><br>'
|
||||||
@ -374,6 +373,8 @@ class SearchBoxMixin(object): # {{{
|
|||||||
def highlight_only_clicked(self, state):
|
def highlight_only_clicked(self, state):
|
||||||
config['highlight_search_matches'] = not config['highlight_search_matches']
|
config['highlight_search_matches'] = not config['highlight_search_matches']
|
||||||
self.set_highlight_only_button_icon()
|
self.set_highlight_only_button_icon()
|
||||||
|
self.search.do_search()
|
||||||
|
self.focus_to_library()
|
||||||
|
|
||||||
def set_highlight_only_button_icon(self):
|
def set_highlight_only_button_icon(self):
|
||||||
if config['highlight_search_matches']:
|
if config['highlight_search_matches']:
|
||||||
@ -404,10 +405,6 @@ class SearchBoxMixin(object): # {{{
|
|||||||
self.search.do_search()
|
self.search.do_search()
|
||||||
self.focus_to_library()
|
self.focus_to_library()
|
||||||
|
|
||||||
def search_options_button_clicked(self):
|
|
||||||
self.iactions['Preferences'].do_config(initial_plugin=('Interface',
|
|
||||||
'Search'), close_after_initial=True)
|
|
||||||
|
|
||||||
def focus_to_library(self):
|
def focus_to_library(self):
|
||||||
self.current_view().setFocus(Qt.OtherFocusReason)
|
self.current_view().setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
|
@ -19,27 +19,27 @@ class StorePlugin(object): # {{{
|
|||||||
|
|
||||||
If two :class:`StorePlugin` objects have the same name, the one with higher
|
If two :class:`StorePlugin` objects have the same name, the one with higher
|
||||||
priority takes precedence.
|
priority takes precedence.
|
||||||
|
|
||||||
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
||||||
|
|
||||||
Regarding :meth:`open`. Most stores only make themselves available
|
Regarding :meth:`open`. Most stores only make themselves available
|
||||||
though a web site thus most store plugins will open using
|
though a web site thus most store plugins will open using
|
||||||
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
||||||
open a modal window and display the store website in a QWebView.
|
open a modal window and display the store website in a QWebView.
|
||||||
|
|
||||||
Sub-classes should implement and use the :meth:`genesis` if they require
|
Sub-classes should implement and use the :meth:`genesis` if they require
|
||||||
plugin specific initialization. They should not override or otherwise
|
plugin specific initialization. They should not override or otherwise
|
||||||
reimplement :meth:`__init__`.
|
reimplement :meth:`__init__`.
|
||||||
|
|
||||||
Once initialized, this plugin has access to the main calibre GUI via the
|
Once initialized, this plugin has access to the main calibre GUI via the
|
||||||
:attr:`gui` member. You can access other plugins by name, for example::
|
:attr:`gui` member. You can access other plugins by name, for example::
|
||||||
|
|
||||||
self.gui.istores['Amazon Kindle']
|
self.gui.istores['Amazon Kindle']
|
||||||
|
|
||||||
Plugin authors can use affiliate programs within their plugin. The
|
Plugin authors can use affiliate programs within their plugin. The
|
||||||
distribution of money earned from a store plugin is 70/30. 70% going
|
distribution of money earned from a store plugin is 70/30. 70% going
|
||||||
to the pluin author / maintainer and 30% going to the calibre project.
|
to the pluin author / maintainer and 30% going to the calibre project.
|
||||||
|
|
||||||
The easiest way to handle affiliate money payouts is to randomly select
|
The easiest way to handle affiliate money payouts is to randomly select
|
||||||
between the author's affiliate id and calibre's affiliate id so that
|
between the author's affiliate id and calibre's affiliate id so that
|
||||||
70% of the time the author's id is used.
|
70% of the time the author's id is used.
|
||||||
@ -49,61 +49,61 @@ class StorePlugin(object): # {{{
|
|||||||
self.gui = gui
|
self.gui = gui
|
||||||
self.name = name
|
self.name = name
|
||||||
self.base_plugin = None
|
self.base_plugin = None
|
||||||
|
|
||||||
def open(self, gui, parent=None, detail_item=None, external=False):
|
def open(self, gui, parent=None, detail_item=None, external=False):
|
||||||
'''
|
'''
|
||||||
Open the store.
|
Open the store.
|
||||||
|
|
||||||
:param gui: The main GUI. This will be used to have the job
|
:param gui: The main GUI. This will be used to have the job
|
||||||
system start downloading an item from the store.
|
system start downloading an item from the store.
|
||||||
|
|
||||||
:param parent: The parent of the store dialog. This is used
|
:param parent: The parent of the store dialog. This is used
|
||||||
to create modal dialogs.
|
to create modal dialogs.
|
||||||
|
|
||||||
:param detail_item: A plugin specific reference to an item
|
:param detail_item: A plugin specific reference to an item
|
||||||
in the store that the user should be shown.
|
in the store that the user should be shown.
|
||||||
|
|
||||||
:param external: When False open an internal dialog with the
|
:param external: When False open an internal dialog with the
|
||||||
store. When True open the users default browser to the store's
|
store. When True open the users default browser to the store's
|
||||||
web site. :param:`detail_item` should still be respected when external
|
web site. :param:`detail_item` should still be respected when external
|
||||||
is True.
|
is True.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
def search(self, query, max_results=10, timeout=60):
|
||||||
'''
|
'''
|
||||||
Searches the store for items matching query. This should
|
Searches the store for items matching query. This should
|
||||||
return items as a generator.
|
return items as a generator.
|
||||||
|
|
||||||
Don't be lazy with the search! Load as much data as possible in the
|
Don't be lazy with the search! Load as much data as possible in the
|
||||||
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
|
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
|
||||||
multiple pages to get all of the data then do so. However, if data (such as cover_url)
|
multiple pages to get all of the data then do so. However, if data (such as cover_url)
|
||||||
isn't available because the store does not display cover images then it's okay to
|
isn't available because the store does not display cover images then it's okay to
|
||||||
ignore it.
|
ignore it.
|
||||||
|
|
||||||
Also, by default search results can only include ebooks. A plugin can offer users
|
Also, by default search results can only include ebooks. A plugin can offer users
|
||||||
an option to include physical books in the search results but this must be
|
an option to include physical books in the search results but this must be
|
||||||
disabled by default.
|
disabled by default.
|
||||||
|
|
||||||
If a store doesn't provide search on it's own use something like a site specific
|
If a store doesn't provide search on it's own use something like a site specific
|
||||||
google search to get search results for this funtion.
|
google search to get search results for this funtion.
|
||||||
|
|
||||||
:param query: The string query search with.
|
:param query: The string query search with.
|
||||||
:param max_results: The maximum number of results to return.
|
:param max_results: The maximum number of results to return.
|
||||||
:param timeout: The maximum amount of time in seconds to spend download the search results.
|
:param timeout: The maximum amount of time in seconds to spend download the search results.
|
||||||
|
|
||||||
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
|
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
|
||||||
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
'''
|
'''
|
||||||
This is only useful for plugins that implement
|
This is only useful for plugins that implement
|
||||||
:attr:`config_widget` that is the only way to save
|
:attr:`config_widget` that is the only way to save
|
||||||
settings. This is used by plugins to get the saved
|
settings. This is used by plugins to get the saved
|
||||||
settings and apply when necessary.
|
settings and apply when necessary.
|
||||||
|
|
||||||
:return: A dictionary filled with the settings used
|
:return: A dictionary filled with the settings used
|
||||||
by this plugin.
|
by this plugin.
|
||||||
'''
|
'''
|
||||||
@ -117,23 +117,23 @@ class StorePlugin(object): # {{{
|
|||||||
Plugin specific initialization.
|
Plugin specific initialization.
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def config_widget(self):
|
def config_widget(self):
|
||||||
'''
|
'''
|
||||||
See :class:`calibre.customize.Plugin` for details.
|
See :class:`calibre.customize.Plugin` for details.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save_settings(self, config_widget):
|
def save_settings(self, config_widget):
|
||||||
'''
|
'''
|
||||||
See :class:`calibre.customize.Plugin` for details.
|
See :class:`calibre.customize.Plugin` for details.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
def customization_help(self, gui=False):
|
||||||
'''
|
'''
|
||||||
See :class:`calibre.customize.Plugin` for details.
|
See :class:`calibre.customize.Plugin` for details.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -21,14 +21,14 @@ from calibre.gui2.store import StorePlugin
|
|||||||
from calibre.gui2.store.search_result import SearchResult
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
|
||||||
class AmazonKindleStore(StorePlugin):
|
class AmazonKindleStore(StorePlugin):
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
'''
|
'''
|
||||||
Amazon comes with a number of difficulties.
|
Amazon comes with a number of difficulties.
|
||||||
|
|
||||||
QWebView has major issues with Amazon.com. The largest of
|
QWebView has major issues with Amazon.com. The largest of
|
||||||
issues is it simply doesn't work on a number of pages.
|
issues is it simply doesn't work on a number of pages.
|
||||||
|
|
||||||
When connecting to a number parts of Amazon.com (Kindle library
|
When connecting to a number parts of Amazon.com (Kindle library
|
||||||
for instance) QNetworkAccessManager fails to connect with a
|
for instance) QNetworkAccessManager fails to connect with a
|
||||||
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
||||||
@ -37,19 +37,19 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
the QNetworkAccessManager decides there was a NetworkError it
|
the QNetworkAccessManager decides there was a NetworkError it
|
||||||
does not download the page from Amazon. So I can't even set the
|
does not download the page from Amazon. So I can't even set the
|
||||||
HTML in the QWebView myself.
|
HTML in the QWebView myself.
|
||||||
|
|
||||||
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
|
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
|
||||||
open bug about the issue but it is not correct. We can set the
|
open bug about the issue but it is not correct. We can set the
|
||||||
useragent (Arora does) to something else and the above issue
|
useragent (Arora does) to something else and the above issue
|
||||||
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
|
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
|
||||||
gives a bit more information about the issue but as of now (27/Feb/2011)
|
gives a bit more information about the issue but as of now (27/Feb/2011)
|
||||||
there is no solution or work around.
|
there is no solution or work around.
|
||||||
|
|
||||||
We cannot change the The linkDelegationPolicy to allow us to avoid
|
We cannot change the The linkDelegationPolicy to allow us to avoid
|
||||||
QNetworkAccessManager because it only works links. Forms aren't
|
QNetworkAccessManager because it only works links. Forms aren't
|
||||||
included so the same issue persists on any part of the site (login)
|
included so the same issue persists on any part of the site (login)
|
||||||
that use a form to load a new page.
|
that use a form to load a new page.
|
||||||
|
|
||||||
Using an aStore was evaluated but I've decided against using it.
|
Using an aStore was evaluated but I've decided against using it.
|
||||||
There are three major issues with an aStore. Because checkout is
|
There are three major issues with an aStore. Because checkout is
|
||||||
handled by sending the user to Amazon we can't put it in a QWebView.
|
handled by sending the user to Amazon we can't put it in a QWebView.
|
||||||
@ -57,7 +57,7 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
nicer. Also, we cannot put the aStore in a QWebView and let it open the
|
nicer. Also, we cannot put the aStore in a QWebView and let it open the
|
||||||
redirection the users default browser because the cookies with the
|
redirection the users default browser because the cookies with the
|
||||||
shopping cart won't transfer.
|
shopping cart won't transfer.
|
||||||
|
|
||||||
Another issue with the aStore is how it handles the referral. It only
|
Another issue with the aStore is how it handles the referral. It only
|
||||||
counts the referral for the items in the shopping card / the item
|
counts the referral for the items in the shopping card / the item
|
||||||
that directed the user to Amazon. Kindle books do not use the shopping
|
that directed the user to Amazon. Kindle books do not use the shopping
|
||||||
@ -65,44 +65,44 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
instance we would only get referral credit for the one book that the
|
instance we would only get referral credit for the one book that the
|
||||||
aStore directs to Amazon that the user buys. Any other purchases we
|
aStore directs to Amazon that the user buys. Any other purchases we
|
||||||
won't get credit for.
|
won't get credit for.
|
||||||
|
|
||||||
The last issue with the aStore is performance. Even though it's an
|
The last issue with the aStore is performance. Even though it's an
|
||||||
Amazon site it's alow. So much slower than Amazon.com that it makes
|
Amazon site it's alow. So much slower than Amazon.com that it makes
|
||||||
me not want to browse books using it. The look and feel are lesser
|
me not want to browse books using it. The look and feel are lesser
|
||||||
issues. So is the fact that it almost seems like the purchase is
|
issues. So is the fact that it almost seems like the purchase is
|
||||||
with calibre. This can cause some support issues because we can't
|
with calibre. This can cause some support issues because we can't
|
||||||
do much for issues with Amazon.com purchase hiccups.
|
do much for issues with Amazon.com purchase hiccups.
|
||||||
|
|
||||||
Another option that was evaluated was the Product Advertising API.
|
Another option that was evaluated was the Product Advertising API.
|
||||||
The reasons against this are complexity. It would take a lot of work
|
The reasons against this are complexity. It would take a lot of work
|
||||||
to basically re-create Amazon.com within calibre. The Product
|
to basically re-create Amazon.com within calibre. The Product
|
||||||
Advertising API is also designed with being run on a server not
|
Advertising API is also designed with being run on a server not
|
||||||
in an app. The signing keys would have to be made avaliable to ever
|
in an app. The signing keys would have to be made avaliable to ever
|
||||||
calibre user which means bad things could be done with our account.
|
calibre user which means bad things could be done with our account.
|
||||||
|
|
||||||
The Product Advertising API also assumes the same browser for easy
|
The Product Advertising API also assumes the same browser for easy
|
||||||
shopping cart transfer to Amazon. With QWebView not working and there
|
shopping cart transfer to Amazon. With QWebView not working and there
|
||||||
not being an easy way to transfer cookies between a QWebView and the
|
not being an easy way to transfer cookies between a QWebView and the
|
||||||
users default browser this won't work well.
|
users default browser this won't work well.
|
||||||
|
|
||||||
We could create our own website on the calibre server and create an
|
We could create our own website on the calibre server and create an
|
||||||
Amazon Product Advertising API store. However, this goes back to the
|
Amazon Product Advertising API store. However, this goes back to the
|
||||||
complexity argument. Why spend the time recreating Amazon.com
|
complexity argument. Why spend the time recreating Amazon.com
|
||||||
|
|
||||||
The final and largest issue against using the Product Advertising API
|
The final and largest issue against using the Product Advertising API
|
||||||
is the Efficiency Guidelines:
|
is the Efficiency Guidelines:
|
||||||
|
|
||||||
"Each account used to access the Product Advertising API will be allowed
|
"Each account used to access the Product Advertising API will be allowed
|
||||||
an initial usage limit of 2,000 requests per hour. Each account will
|
an initial usage limit of 2,000 requests per hour. Each account will
|
||||||
receive an additional 500 requests per hour (up to a maximum of 25,000
|
receive an additional 500 requests per hour (up to a maximum of 25,000
|
||||||
requests per hour) for every $1 of shipped item revenue driven per hour
|
requests per hour) for every $1 of shipped item revenue driven per hour
|
||||||
in a trailing 30-day period. Usage thresholds are recalculated daily based
|
in a trailing 30-day period. Usage thresholds are recalculated daily based
|
||||||
on revenue performance."
|
on revenue performance."
|
||||||
|
|
||||||
With over two million users a limit of 2,000 request per hour could
|
With over two million users a limit of 2,000 request per hour could
|
||||||
render our store unusable for no other reason than Amazon rate
|
render our store unusable for no other reason than Amazon rate
|
||||||
limiting our traffic.
|
limiting our traffic.
|
||||||
|
|
||||||
The best (I use the term lightly here) solution is to open Amazon.com
|
The best (I use the term lightly here) solution is to open Amazon.com
|
||||||
in the users default browser and set the affiliate id as part of the url.
|
in the users default browser and set the affiliate id as part of the url.
|
||||||
'''
|
'''
|
||||||
@ -119,14 +119,14 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
def search(self, query, max_results=10, timeout=60):
|
def search(self, query, max_results=10, timeout=60):
|
||||||
url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query)
|
url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query)
|
||||||
br = browser()
|
br = browser()
|
||||||
|
|
||||||
counter = max_results
|
counter = max_results
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
doc = html.fromstring(f.read())
|
doc = html.fromstring(f.read())
|
||||||
for data in doc.xpath('//div[@class="productData"]'):
|
for data in doc.xpath('//div[@class="productData"]'):
|
||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Even though we are searching digital-text only Amazon will still
|
# Even though we are searching digital-text only Amazon will still
|
||||||
# put in results for non Kindle books (author pages). Se we need
|
# put in results for non Kindle books (author pages). Se we need
|
||||||
# to explicitly check if the item is a Kindle book and ignore it
|
# to explicitly check if the item is a Kindle book and ignore it
|
||||||
@ -134,7 +134,7 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
type = ''.join(data.xpath('//span[@class="format"]/text()'))
|
type = ''.join(data.xpath('//span[@class="format"]/text()'))
|
||||||
if 'kindle' not in type.lower():
|
if 'kindle' not in type.lower():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We must have an asin otherwise we can't easily reference the
|
# We must have an asin otherwise we can't easily reference the
|
||||||
# book later.
|
# book later.
|
||||||
asin_href = None
|
asin_href = None
|
||||||
@ -148,25 +148,25 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cover_url = ''
|
cover_url = ''
|
||||||
if asin_href:
|
if asin_href:
|
||||||
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
||||||
if cover_img:
|
if cover_img:
|
||||||
cover_url = cover_img[0]
|
cover_url = cover_img[0]
|
||||||
|
|
||||||
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
||||||
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
||||||
author = author.split('by')[-1]
|
author = author.split('by')[-1]
|
||||||
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
|
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
s.cover_url = cover_url
|
s.cover_url = cover_url
|
||||||
s.title = title.strip()
|
s.title = title.strip()
|
||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = asin.strip()
|
s.detail_item = asin.strip()
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
@ -6,7 +6,6 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
|
||||||
import urllib2
|
import urllib2
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ from calibre.gui2.store.search_result import SearchResult
|
|||||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
|
|
||||||
class BeWriteStore(BasicStoreConfig, StorePlugin):
|
class BeWriteStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
settings = self.get_settings()
|
settings = self.get_settings()
|
||||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
||||||
@ -42,9 +41,9 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
def search(self, query, max_results=10, timeout=60):
|
||||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(query)
|
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(query)
|
||||||
|
|
||||||
br = browser()
|
br = browser()
|
||||||
|
|
||||||
counter = max_results
|
counter = max_results
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
doc = html.fromstring(f.read())
|
doc = html.fromstring(f.read())
|
||||||
@ -55,12 +54,12 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
id = ''.join(data.xpath('.//a/@href'))
|
id = ''.join(data.xpath('.//a/@href'))
|
||||||
if not id:
|
if not id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
heading = ''.join(data.xpath('./td[2]//text()'))
|
heading = ''.join(data.xpath('./td[2]//text()'))
|
||||||
title, q, author = heading.partition('by ')
|
title, q, author = heading.partition('by ')
|
||||||
cover_url = ''
|
cover_url = ''
|
||||||
price = ''
|
price = ''
|
||||||
|
|
||||||
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
||||||
idata = html.fromstring(nf.read())
|
idata = html.fromstring(nf.read())
|
||||||
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
||||||
@ -68,14 +67,14 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
||||||
if cover_img:
|
if cover_img:
|
||||||
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
s.cover_url = cover_url.strip()
|
s.cover_url = cover_url.strip()
|
||||||
s.title = title.strip()
|
s.title = title.strip()
|
||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
@ -13,9 +13,8 @@ from random import shuffle
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QAbstractItemModel, QDialog, QTimer, QVariant, \
|
from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
|
||||||
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout, QHBoxLayout, \
|
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
|
||||||
QPushButton, QString, QByteArray
|
|
||||||
|
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.gui2 import NONE
|
from calibre.gui2 import NONE
|
||||||
@ -35,7 +34,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
def __init__(self, istores, *args):
|
def __init__(self, istores, *args):
|
||||||
QDialog.__init__(self, *args)
|
QDialog.__init__(self, *args)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.config = DynamicConfig('store_search')
|
self.config = DynamicConfig('store_search')
|
||||||
|
|
||||||
# We keep a cache of store plugins and reference them by name.
|
# We keep a cache of store plugins and reference them by name.
|
||||||
@ -44,7 +43,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
# Check for results and hung threads.
|
# Check for results and hung threads.
|
||||||
self.checker = QTimer()
|
self.checker = QTimer()
|
||||||
self.hang_check = 0
|
self.hang_check = 0
|
||||||
|
|
||||||
self.model = Matches()
|
self.model = Matches()
|
||||||
self.results_view.setModel(self.model)
|
self.results_view.setModel(self.model)
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
stores_group_layout.addWidget(cbox)
|
stores_group_layout.addWidget(cbox)
|
||||||
setattr(self, 'store_check_' + x, cbox)
|
setattr(self, 'store_check_' + x, cbox)
|
||||||
stores_group_layout.addStretch()
|
stores_group_layout.addStretch()
|
||||||
|
|
||||||
# Create and add the progress indicator
|
# Create and add the progress indicator
|
||||||
self.pi = ProgressIndicator(self, 24)
|
self.pi = ProgressIndicator(self, 24)
|
||||||
self.bottom_layout.insertWidget(0, self.pi)
|
self.bottom_layout.insertWidget(0, self.pi)
|
||||||
@ -71,9 +70,9 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
||||||
self.select_none_stores.clicked.connect(self.stores_select_none)
|
self.select_none_stores.clicked.connect(self.stores_select_none)
|
||||||
self.finished.connect(self.dialog_closed)
|
self.finished.connect(self.dialog_closed)
|
||||||
|
|
||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
def resize_columns(self):
|
def resize_columns(self):
|
||||||
total = 600
|
total = 600
|
||||||
# Cover
|
# Cover
|
||||||
@ -87,19 +86,19 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.results_view.setColumnWidth(3, int(total*.10))
|
self.results_view.setColumnWidth(3, int(total*.10))
|
||||||
# Store
|
# Store
|
||||||
self.results_view.setColumnWidth(4, int(total*.20))
|
self.results_view.setColumnWidth(4, int(total*.20))
|
||||||
|
|
||||||
def do_search(self, checked=False):
|
def do_search(self, checked=False):
|
||||||
# Stop all running threads.
|
# Stop all running threads.
|
||||||
self.checker.stop()
|
self.checker.stop()
|
||||||
self.search_pool.abort()
|
self.search_pool.abort()
|
||||||
# Clear the visible results.
|
# Clear the visible results.
|
||||||
self.results_view.model().clear_results()
|
self.results_view.model().clear_results()
|
||||||
|
|
||||||
# Don't start a search if there is nothing to search for.
|
# Don't start a search if there is nothing to search for.
|
||||||
query = unicode(self.search_edit.text())
|
query = unicode(self.search_edit.text())
|
||||||
if not query.strip():
|
if not query.strip():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Plugins are in alphebetic order. Randomize the
|
# Plugins are in alphebetic order. Randomize the
|
||||||
# order of plugin names. This way plugins closer
|
# order of plugin names. This way plugins closer
|
||||||
# to a don't have an unfair advantage over
|
# to a don't have an unfair advantage over
|
||||||
@ -117,12 +116,12 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.checker.start(100)
|
self.checker.start(100)
|
||||||
self.search_pool.start_threads()
|
self.search_pool.start_threads()
|
||||||
self.pi.startAnimation()
|
self.pi.startAnimation()
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
self.config['store_search_geometry'] = self.saveGeometry()
|
self.config['store_search_geometry'] = self.saveGeometry()
|
||||||
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
|
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
|
||||||
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||||
|
|
||||||
store_check = {}
|
store_check = {}
|
||||||
for n in self.store_plugins:
|
for n in self.store_plugins:
|
||||||
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
|
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
|
||||||
@ -132,11 +131,11 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
geometry = self.config['store_search_geometry']
|
geometry = self.config['store_search_geometry']
|
||||||
if geometry:
|
if geometry:
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
|
|
||||||
splitter_state = self.config['store_search_store_splitter_state']
|
splitter_state = self.config['store_search_store_splitter_state']
|
||||||
if splitter_state:
|
if splitter_state:
|
||||||
self.store_splitter.restoreState(splitter_state)
|
self.store_splitter.restoreState(splitter_state)
|
||||||
|
|
||||||
results_cwidth = self.config['store_search_results_view_column_width']
|
results_cwidth = self.config['store_search_results_view_column_width']
|
||||||
if results_cwidth:
|
if results_cwidth:
|
||||||
for i, x in enumerate(results_cwidth):
|
for i, x in enumerate(results_cwidth):
|
||||||
@ -145,7 +144,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.results_view.setColumnWidth(i, x)
|
self.results_view.setColumnWidth(i, x)
|
||||||
else:
|
else:
|
||||||
self.resize_columns()
|
self.resize_columns()
|
||||||
|
|
||||||
store_check = self.config['store_search_store_checked']
|
store_check = self.config['store_search_store_checked']
|
||||||
if store_check:
|
if store_check:
|
||||||
for n in store_check:
|
for n in store_check:
|
||||||
@ -165,7 +164,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
||||||
self.checker.stop()
|
self.checker.stop()
|
||||||
self.pi.stopAnimation()
|
self.pi.stopAnimation()
|
||||||
|
|
||||||
while self.search_pool.has_results():
|
while self.search_pool.has_results():
|
||||||
res = self.search_pool.get_result()
|
res = self.search_pool.get_result()
|
||||||
if res:
|
if res:
|
||||||
@ -189,15 +188,15 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
def stores_select_all(self):
|
def stores_select_all(self):
|
||||||
for check in self.get_store_checks():
|
for check in self.get_store_checks():
|
||||||
check.setChecked(True)
|
check.setChecked(True)
|
||||||
|
|
||||||
def stores_select_invert(self):
|
def stores_select_invert(self):
|
||||||
for check in self.get_store_checks():
|
for check in self.get_store_checks():
|
||||||
check.setChecked(not check.isChecked())
|
check.setChecked(not check.isChecked())
|
||||||
|
|
||||||
def stores_select_none(self):
|
def stores_select_none(self):
|
||||||
for check in self.get_store_checks():
|
for check in self.get_store_checks():
|
||||||
check.setChecked(False)
|
check.setChecked(False)
|
||||||
|
|
||||||
def dialog_closed(self, result):
|
def dialog_closed(self, result):
|
||||||
self.model.closing()
|
self.model.closing()
|
||||||
self.search_pool.abort()
|
self.search_pool.abort()
|
||||||
@ -208,46 +207,46 @@ class GenericDownloadThreadPool(object):
|
|||||||
'''
|
'''
|
||||||
add_task must be implemented in a subclass.
|
add_task must be implemented in a subclass.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, thread_type, thread_count):
|
def __init__(self, thread_type, thread_count):
|
||||||
self.thread_type = thread_type
|
self.thread_type = thread_type
|
||||||
self.thread_count = thread_count
|
self.thread_count = thread_count
|
||||||
|
|
||||||
self.tasks = Queue()
|
self.tasks = Queue()
|
||||||
self.results = Queue()
|
self.results = Queue()
|
||||||
self.threads = []
|
self.threads = []
|
||||||
|
|
||||||
def add_task(self):
|
def add_task(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def start_threads(self):
|
def start_threads(self):
|
||||||
for i in range(self.thread_count):
|
for i in range(self.thread_count):
|
||||||
t = self.thread_type(self.tasks, self.results)
|
t = self.thread_type(self.tasks, self.results)
|
||||||
self.threads.append(t)
|
self.threads.append(t)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
self.tasks = Queue()
|
self.tasks = Queue()
|
||||||
self.results = Queue()
|
self.results = Queue()
|
||||||
for t in self.threads:
|
for t in self.threads:
|
||||||
t.abort()
|
t.abort()
|
||||||
self.threads = []
|
self.threads = []
|
||||||
|
|
||||||
def has_tasks(self):
|
def has_tasks(self):
|
||||||
return not self.tasks.empty()
|
return not self.tasks.empty()
|
||||||
|
|
||||||
def get_result(self):
|
def get_result(self):
|
||||||
return self.results.get()
|
return self.results.get()
|
||||||
|
|
||||||
def get_result_no_wait(self):
|
def get_result_no_wait(self):
|
||||||
return self.results.get_nowait()
|
return self.results.get_nowait()
|
||||||
|
|
||||||
def result_count(self):
|
def result_count(self):
|
||||||
return len(self.results)
|
return len(self.results)
|
||||||
|
|
||||||
def has_results(self):
|
def has_results(self):
|
||||||
return not self.results.empty()
|
return not self.results.empty()
|
||||||
|
|
||||||
def threads_running(self):
|
def threads_running(self):
|
||||||
for t in self.threads:
|
for t in self.threads:
|
||||||
if t.is_alive():
|
if t.is_alive():
|
||||||
@ -260,7 +259,7 @@ class SearchThreadPool(GenericDownloadThreadPool):
|
|||||||
Threads will run until there is no work or
|
Threads will run until there is no work or
|
||||||
abort is called. Create and start new threads
|
abort is called. Create and start new threads
|
||||||
using start_threads(). Reset by calling abort().
|
using start_threads(). Reset by calling abort().
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
sp = SearchThreadPool(SearchThread, 3)
|
sp = SearchThreadPool(SearchThread, 3)
|
||||||
add tasks using add_task(...)
|
add tasks using add_task(...)
|
||||||
@ -270,13 +269,13 @@ class SearchThreadPool(GenericDownloadThreadPool):
|
|||||||
add tasks using add_task(...)
|
add tasks using add_task(...)
|
||||||
sp.start_threads()
|
sp.start_threads()
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def add_task(self, query, store_name, store_plugin, timeout):
|
def add_task(self, query, store_name, store_plugin, timeout):
|
||||||
self.tasks.put((query, store_name, store_plugin, timeout))
|
self.tasks.put((query, store_name, store_plugin, timeout))
|
||||||
|
|
||||||
|
|
||||||
class SearchThread(Thread):
|
class SearchThread(Thread):
|
||||||
|
|
||||||
def __init__(self, tasks, results):
|
def __init__(self, tasks, results):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
@ -286,7 +285,7 @@ class SearchThread(Thread):
|
|||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
self._run = False
|
self._run = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._run and not self.tasks.empty():
|
while self._run and not self.tasks.empty():
|
||||||
try:
|
try:
|
||||||
@ -305,7 +304,7 @@ class CoverThreadPool(GenericDownloadThreadPool):
|
|||||||
'''
|
'''
|
||||||
Once started all threads run until abort is called.
|
Once started all threads run until abort is called.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def add_task(self, search_result, update_callback, timeout=5):
|
def add_task(self, search_result, update_callback, timeout=5):
|
||||||
self.tasks.put((search_result, update_callback, timeout))
|
self.tasks.put((search_result, update_callback, timeout))
|
||||||
|
|
||||||
@ -318,12 +317,12 @@ class CoverThread(Thread):
|
|||||||
self.tasks = tasks
|
self.tasks = tasks
|
||||||
self.results = results
|
self.results = results
|
||||||
self._run = True
|
self._run = True
|
||||||
|
|
||||||
self.br = browser()
|
self.br = browser()
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
self._run = False
|
self._run = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._run:
|
while self._run:
|
||||||
try:
|
try:
|
||||||
@ -354,13 +353,13 @@ class Matches(QAbstractItemModel):
|
|||||||
|
|
||||||
def closing(self):
|
def closing(self):
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
|
|
||||||
def clear_results(self):
|
def clear_results(self):
|
||||||
self.matches = []
|
self.matches = []
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
self.cover_pool.start_threads()
|
self.cover_pool.start_threads()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def add_result(self, result):
|
def add_result(self, result):
|
||||||
self.layoutAboutToBeChanged.emit()
|
self.layoutAboutToBeChanged.emit()
|
||||||
self.matches.append(result)
|
self.matches.append(result)
|
||||||
@ -391,7 +390,7 @@ class Matches(QAbstractItemModel):
|
|||||||
|
|
||||||
def columnCount(self, *args):
|
def columnCount(self, *args):
|
||||||
return len(self.HEADERS)
|
return len(self.HEADERS)
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if role != Qt.DisplayRole:
|
if role != Qt.DisplayRole:
|
||||||
return NONE
|
return NONE
|
||||||
@ -434,7 +433,7 @@ class Matches(QAbstractItemModel):
|
|||||||
elif col == 3:
|
elif col == 3:
|
||||||
text = result.price
|
text = result.price
|
||||||
if len(text) < 3 or text[-3] not in ('.', ','):
|
if len(text) < 3 or text[-3] not in ('.', ','):
|
||||||
text += '00'
|
text += '00'
|
||||||
text = re.sub(r'\D', '', text)
|
text = re.sub(r'\D', '', text)
|
||||||
text = text.rjust(6, '0')
|
text = text.rjust(6, '0')
|
||||||
elif col == 4:
|
elif col == 4:
|
||||||
@ -444,7 +443,7 @@ class Matches(QAbstractItemModel):
|
|||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
if not self.matches:
|
if not self.matches:
|
||||||
return
|
return
|
||||||
descending = order == Qt.DescendingOrder
|
descending = order == Qt.DescendingOrder
|
||||||
self.matches.sort(None,
|
self.matches.sort(None,
|
||||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||||
descending)
|
descending)
|
||||||
|
@ -9,8 +9,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os
|
import os
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from PyQt4.Qt import QWebView, QWebPage, QNetworkCookieJar, QNetworkRequest, QString, \
|
from PyQt4.Qt import (QWebView, QWebPage, QNetworkCookieJar,
|
||||||
QFileDialog, QNetworkProxy
|
QFileDialog, QNetworkProxy)
|
||||||
|
|
||||||
from calibre import USER_AGENT, get_proxies, get_download_filename
|
from calibre import USER_AGENT, get_proxies, get_download_filename
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
@ -35,13 +35,13 @@ class NPWebView(QWebView):
|
|||||||
proxy.setPassword(proxy_parts.password)
|
proxy.setPassword(proxy_parts.password)
|
||||||
proxy.setHostName(proxy_parts.hostname)
|
proxy.setHostName(proxy_parts.hostname)
|
||||||
proxy.setPort(proxy_parts.port)
|
proxy.setPort(proxy_parts.port)
|
||||||
self.page().networkAccessManager().setProxy(proxy)
|
self.page().networkAccessManager().setProxy(proxy)
|
||||||
|
|
||||||
self.page().setForwardUnsupportedContent(True)
|
self.page().setForwardUnsupportedContent(True)
|
||||||
self.page().unsupportedContent.connect(self.start_download)
|
self.page().unsupportedContent.connect(self.start_download)
|
||||||
self.page().downloadRequested.connect(self.start_download)
|
self.page().downloadRequested.connect(self.start_download)
|
||||||
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
|
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
|
||||||
|
|
||||||
def createWindow(self, type):
|
def createWindow(self, type):
|
||||||
if type == QWebPage.WebBrowserWindow:
|
if type == QWebPage.WebBrowserWindow:
|
||||||
return self
|
return self
|
||||||
@ -50,17 +50,17 @@ class NPWebView(QWebView):
|
|||||||
|
|
||||||
def set_gui(self, gui):
|
def set_gui(self, gui):
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
|
|
||||||
def set_tags(self, tags):
|
def set_tags(self, tags):
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
|
||||||
def start_download(self, request):
|
def start_download(self, request):
|
||||||
if not self.gui:
|
if not self.gui:
|
||||||
return
|
return
|
||||||
|
|
||||||
url = unicode(request.url().toString())
|
url = unicode(request.url().toString())
|
||||||
cf = self.get_cookies()
|
cf = self.get_cookies()
|
||||||
|
|
||||||
filename = get_download_filename(url, cf)
|
filename = get_download_filename(url, cf)
|
||||||
ext = os.path.splitext(filename)[1][1:].lower()
|
ext = os.path.splitext(filename)[1][1:].lower()
|
||||||
if ext not in BOOK_EXTENSIONS:
|
if ext not in BOOK_EXTENSIONS:
|
||||||
@ -76,21 +76,21 @@ class NPWebView(QWebView):
|
|||||||
|
|
||||||
def ignore_ssl_errors(self, reply, errors):
|
def ignore_ssl_errors(self, reply, errors):
|
||||||
reply.ignoreSslErrors(errors)
|
reply.ignoreSslErrors(errors)
|
||||||
|
|
||||||
def get_cookies(self):
|
def get_cookies(self):
|
||||||
'''
|
'''
|
||||||
Writes QNetworkCookies to Mozilla cookie .txt file.
|
Writes QNetworkCookies to Mozilla cookie .txt file.
|
||||||
|
|
||||||
:return: The file path to the cookie file.
|
:return: The file path to the cookie file.
|
||||||
'''
|
'''
|
||||||
cf = PersistentTemporaryFile(suffix='.txt')
|
cf = PersistentTemporaryFile(suffix='.txt')
|
||||||
|
|
||||||
cf.write('# Netscape HTTP Cookie File\n\n')
|
cf.write('# Netscape HTTP Cookie File\n\n')
|
||||||
|
|
||||||
for c in self.page().networkAccessManager().cookieJar().allCookies():
|
for c in self.page().networkAccessManager().cookieJar().allCookies():
|
||||||
cookie = []
|
cookie = []
|
||||||
domain = unicode(c.domain())
|
domain = unicode(c.domain())
|
||||||
|
|
||||||
cookie.append(domain)
|
cookie.append(domain)
|
||||||
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
|
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
|
||||||
cookie.append(unicode(c.path()))
|
cookie.append(unicode(c.path()))
|
||||||
@ -98,15 +98,15 @@ class NPWebView(QWebView):
|
|||||||
cookie.append(unicode(c.expirationDate().toTime_t()))
|
cookie.append(unicode(c.expirationDate().toTime_t()))
|
||||||
cookie.append(unicode(c.name()))
|
cookie.append(unicode(c.name()))
|
||||||
cookie.append(unicode(c.value()))
|
cookie.append(unicode(c.value()))
|
||||||
|
|
||||||
cf.write('\t'.join(cookie))
|
cf.write('\t'.join(cookie))
|
||||||
cf.write('\n')
|
cf.write('\n')
|
||||||
|
|
||||||
cf.close()
|
cf.close()
|
||||||
return cf.name
|
return cf.name
|
||||||
|
|
||||||
|
|
||||||
class NPWebPage(QWebPage):
|
class NPWebPage(QWebPage):
|
||||||
|
|
||||||
def userAgentForUrl(self, url):
|
def userAgentForUrl(self, url):
|
||||||
return USER_AGENT
|
return USER_AGENT
|
||||||
|
@ -15,7 +15,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import (Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize,
|
from PyQt4.Qt import (Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize,
|
||||||
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,
|
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,
|
||||||
QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,
|
QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,
|
||||||
QWidget, QItemDelegate, QString, QLabel,
|
QWidget, QItemDelegate, QString, QLabel, QPushButton,
|
||||||
QShortcut, QKeySequence, SIGNAL, QMimeData, QToolButton)
|
QShortcut, QKeySequence, SIGNAL, QMimeData, QToolButton)
|
||||||
|
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
@ -1809,9 +1809,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
category_managers = (
|
|
||||||
)
|
|
||||||
|
|
||||||
class TagBrowserMixin(object): # {{{
|
class TagBrowserMixin(object): # {{{
|
||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
@ -1833,20 +1830,23 @@ class TagBrowserMixin(object): # {{{
|
|||||||
self.tags_view.restriction_error.connect(self.do_restriction_error,
|
self.tags_view.restriction_error.connect(self.do_restriction_error,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
for text, func, args in (
|
for text, func, args, cat_name in (
|
||||||
(_('Manage Authors'), self.do_author_sort_edit, (self,
|
(_('Manage Authors'),
|
||||||
None)),
|
self.do_author_sort_edit, (self, None), 'authors'),
|
||||||
(_('Manage Series'), self.do_tags_list_edit, (None,
|
(_('Manage Series'),
|
||||||
'series')),
|
self.do_tags_list_edit, (None, 'series'), 'series'),
|
||||||
(_('Manage Publishers'), self.do_tags_list_edit, (None,
|
(_('Manage Publishers'),
|
||||||
'publisher')),
|
self.do_tags_list_edit, (None, 'publisher'), 'publisher'),
|
||||||
(_('Manage Tags'), self.do_tags_list_edit, (None, 'tags')),
|
(_('Manage Tags'),
|
||||||
(_('Manage User Categories'),
|
self.do_tags_list_edit, (None, 'tags'), 'tags'),
|
||||||
self.do_edit_user_categories, (None,)),
|
(_('Manage User Categories'),
|
||||||
(_('Manage Saved Searches'), self.do_saved_search_edit,
|
self.do_edit_user_categories, (None,), 'user:'),
|
||||||
(None,))
|
(_('Manage Saved Searches'),
|
||||||
|
self.do_saved_search_edit, (None,), 'search')
|
||||||
):
|
):
|
||||||
self.manage_items_button.menu().addAction(text, partial(func, *args))
|
self.manage_items_button.menu().addAction(
|
||||||
|
QIcon(I(category_icon_map[cat_name])),
|
||||||
|
text, partial(func, *args))
|
||||||
|
|
||||||
def do_restriction_error(self):
|
def do_restriction_error(self):
|
||||||
error_dialog(self.tags_view, _('Invalid search restriction'),
|
error_dialog(self.tags_view, _('Invalid search restriction'),
|
||||||
@ -2166,11 +2166,9 @@ class TagBrowserWidget(QWidget): # {{{
|
|||||||
parent.tag_match.setStatusTip(parent.tag_match.toolTip())
|
parent.tag_match.setStatusTip(parent.tag_match.toolTip())
|
||||||
|
|
||||||
|
|
||||||
l = parent.manage_items_button = QToolButton(self)
|
l = parent.manage_items_button = QPushButton(self)
|
||||||
l.setIcon(QIcon(I('tags.png')))
|
l.setStyleSheet('QPushButton {text-align: left; }')
|
||||||
l.setText(_('Manage authors, tags, etc'))
|
l.setText(_('Manage authors, tags, etc'))
|
||||||
l.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
||||||
l.setPopupMode(l.InstantPopup)
|
|
||||||
l.setToolTip(_('All of these category_managers are available by right-clicking '
|
l.setToolTip(_('All of these category_managers are available by right-clicking '
|
||||||
'on items in the tag browser above'))
|
'on items in the tag browser above'))
|
||||||
l.m = QMenu()
|
l.m = QMenu()
|
||||||
|
@ -529,10 +529,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
action.location_selected(location)
|
action.location_selected(location)
|
||||||
if location == 'library':
|
if location == 'library':
|
||||||
self.search_restriction.setEnabled(True)
|
self.search_restriction.setEnabled(True)
|
||||||
self.search_options_button.setEnabled(True)
|
self.highlight_only_button.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
self.search_restriction.setEnabled(False)
|
self.search_restriction.setEnabled(False)
|
||||||
self.search_options_button.setEnabled(False)
|
self.highlight_only_button.setEnabled(False)
|
||||||
# Reset the view in case something changed while it was invisible
|
# Reset the view in case something changed while it was invisible
|
||||||
self.current_view().reset()
|
self.current_view().reset()
|
||||||
self.set_number_of_books_shown()
|
self.set_number_of_books_shown()
|
||||||
|
@ -426,7 +426,7 @@ def do_show_metadata(db, id, as_opf):
|
|||||||
mi = OPFCreator(os.getcwd(), mi)
|
mi = OPFCreator(os.getcwd(), mi)
|
||||||
mi.render(sys.stdout)
|
mi.render(sys.stdout)
|
||||||
else:
|
else:
|
||||||
print unicode(mi).encode(preferred_encoding)
|
prints(unicode(mi))
|
||||||
|
|
||||||
def show_metadata_option_parser():
|
def show_metadata_option_parser():
|
||||||
parser = get_parser(_(
|
parser = get_parser(_(
|
||||||
|
@ -854,7 +854,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
mi.uuid = row[fm['uuid']]
|
mi.uuid = row[fm['uuid']]
|
||||||
mi.title_sort = row[fm['sort']]
|
mi.title_sort = row[fm['sort']]
|
||||||
mi.last_modified = row[fm['last_modified']]
|
mi.last_modified = row[fm['last_modified']]
|
||||||
mi.size = row[fm['size']]
|
|
||||||
formats = row[fm['formats']]
|
formats = row[fm['formats']]
|
||||||
if not formats:
|
if not formats:
|
||||||
formats = None
|
formats = None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user