KG updates pre 0.7.15

This commit is contained in:
GRiker 2010-08-17 04:38:05 -07:00
commit 5db52d70af
25 changed files with 417 additions and 106 deletions

View File

@ -24,6 +24,7 @@ series_index_auto_increment = 'next'
# invert: use "fn ln" -> "ln, fn" (the original algorithm)
# copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
# nocomma : "fn ln" -> "ln fn" (without the comma)
author_sort_copy_method = 'invert'

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

View File

@ -0,0 +1,38 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
futurismic.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Futurismic(BasicNewsRecipe):
title = 'Futurismic'
__author__ = 'Darko Miletic'
description = 'Near-future science fiction and fact since 2001'
oldest_article = 15
max_articles_per_feed = 100
language = 'en'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = False
publication_type = 'blog'
extra_css = ' body{font-family: Arial,Verdana,sans-serif} '
conversion_options = {
'comment' : description
, 'tags' : 'blog, sf'
, 'publisher': 'Futurismic'
, 'language' : language
}
remove_attributes = ['width','height']
keep_only_tags = [dict(attrs={'class':['post','commentlist']})]
remove_tags = [dict(attrs={'class':['sociable','feedback','tagwords']})]
feeds = [(u'Posts', u'http://feeds2.feedburner.com/futurismic_feed')]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -657,9 +657,14 @@ class ActionEditCollections(InterfaceActionBase):
name = 'Edit Collections'
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
class ActionCopyToLibrary(InterfaceActionBase):
name = 'Copy To Library'
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary]
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
ActionCopyToLibrary]

View File

@ -55,9 +55,9 @@ class ANDROID(USBMS):
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD',
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -80,6 +80,13 @@ class HANLINV3(USBMS):
drives['carda'] = main
return drives
class SPECTRA(HANLINV3):
name = 'Spectra'
gui_name = 'Spectra'
PRODUCT_ID = [0xa4a5]
FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'djvu', 'pdf', 'rtf', 'txt']
class HANLINV5(HANLINV3):
name = 'Hanlin V5 driver'

View File

@ -38,7 +38,7 @@ def author_to_author_sort(author):
author = _bracket_pat.sub('', author).strip()
tokens = author.split()
tokens = tokens[-1:] + tokens[:-1]
if len(tokens) > 1:
if len(tokens) > 1 and method != 'nocomma':
tokens[0] += ','
return ' '.join(tokens)

View File

@ -26,8 +26,9 @@ class InterfaceAction(QObject):
If two :class:`InterfaceAction` objects have the same name, the one with higher
priority takes precedence.
Sub-classes should implement the :meth:`genesis` and
:meth:`location_selected` methods.
Sub-classes should implement the :meth:`genesis`, :meth:`library_moved`,
:meth:`location_selected` :meth:`shutting_down`
and :meth:`initialization_complete` methods.
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::
@ -108,3 +109,28 @@ class InterfaceAction(QObject):
'''
pass
def library_changed(self, db):
'''
Called whenever the current library is changed.
:param db: The LibraryDatabase corresponding to the current library.
'''
pass
def initialization_complete(self):
'''
Called once per action when the initialization of the main GUI is
completed.
'''
pass
def shutting_down(self):
'''
Called once per plugin when the main GUI is in the process of shutting
down. Release any used resources, but try not to block the shutdown for
long periods of time.
:return: False to halt the shutdown. You are responsible for telling
the user why the shutdown was halted.
'''
return True

View File

@ -97,10 +97,21 @@ class ChooseLibraryAction(InterfaceAction):
ac.triggered.connect(partial(self.qs_requested, i))
self.choose_menu.addAction(ac)
def library_used(self, db):
def library_name(self):
db = self.gui.library_view.model().db
path = db.library_path
if isbytestring(path):
path = path.decode(filesystem_encoding)
path = path.replace(os.sep, '/')
return self.stats.pretty(path)
def library_changed(self, db):
self.stats.library_used(db)
self.build_menus()
def initialization_complete(self):
self.library_changed(self.gui.library_view.model().db)
def build_menus(self):
db = self.gui.library_view.model().db
locations = list(self.stats.locations(db))

View File

@ -0,0 +1,21 @@
#!/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 PyQt4.Qt import QMenu
from calibre.gui2.actions import InterfaceAction
class CopyToLibraryAction(InterfaceAction):
name = 'Copy To Library'
action_spec = (_('Copy to library'), 'lt.png',
_('Copy selected books to the specified library'), None)
def genesis(self):
self.menu = QMenu(self.gui)

View File

@ -174,8 +174,14 @@ class EditMetadataAction(InterfaceAction):
_('No books selected'))
d.exec_()
return
if MetadataBulkDialog(self.gui, rows,
self.gui.library_view.model().db).changed:
# Prevent the TagView from updating due to signals from the database
self.gui.tags_view.blockSignals(True)
try:
changed = MetadataBulkDialog(self.gui, rows,
self.gui.library_view.model().db).changed
finally:
self.gui.tags_view.blockSignals(False)
if changed:
self.gui.library_view.model().resort(reset=False)
self.gui.library_view.model().research()
self.gui.tags_view.recount()

View File

@ -31,7 +31,12 @@ class FetchNewsAction(InterfaceAction):
self.qaction.setMenu(self.scheduler.news_menu)
self.qaction.triggered.connect(
self.scheduler.show_dialog)
self.database_changed = self.scheduler.database_changed
def library_changed(self, db):
self.scheduler.database_changed(db)
def initialization_complete(self):
self.connect_scheduler()
def connect_scheduler(self):
self.scheduler.delete_old_news.connect(

View File

@ -196,6 +196,7 @@ class CoverFlowMixin(object):
def show_cover_browser(self):
d = CBDialog(self, self.cover_flow)
d.addAction(self.cb_splitter.action_toggle)
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
d.show()

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \
QPushButton
QPushButton, QCoreApplication
from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
@ -406,13 +406,17 @@ class BulkBase(Base):
def commit(self, book_ids, notify=False):
if self.process_each_book():
for book_id in book_ids:
QCoreApplication.processEvents()
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.db.set_custom(book_id, self.getter(val), num=self.col_id, notify=notify)
new_val = self.getter(val)
if set(val) != new_val:
self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify)
else:
val = self.getter()
val = self.normalize_ui_val(val)
if val != self.initial_val:
for book_id in book_ids:
QCoreApplication.processEvents()
self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
class BulkBool(BulkBase, Bool):
@ -431,6 +435,7 @@ class BulkDateTime(BulkBase, DateTime):
pass
class BulkSeries(BulkBase):
def setup_ui(self, parent):
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
@ -456,6 +461,7 @@ class BulkSeries(BulkBase):
update_indices = self.idx_widget.checkState()
if val != '':
for book_id in book_ids:
QCoreApplication.processEvents()
if update_indices:
if tweaks['series_index_auto_increment'] == 'next':
s_index = self.db.get_next_cc_series_num_for\
@ -544,8 +550,9 @@ class BulkText(BulkBase):
ans = set(original_value)
ans -= set([v.strip() for v in
unicode(self.removing_widget.tags_box.text()).split(',')])
ans |= set([v.strip() for v in
unicode(self.adding_widget.text()).split(',')])
txt = unicode(self.adding_widget.text())
if txt:
ans |= set([v.strip() for v in txt.split(',')])
return ans # returning a set instead of a list works, for now at least.
val = unicode(self.widgets[1].currentText()).strip()
if not val:

View File

@ -48,11 +48,11 @@ class BookInfo(QDialog, Ui_BookInfo):
self.refresh(row)
def open_book_path(self, path):
if os.sep in unicode(path):
path = unicode(path)
if os.sep in path:
open_local_file(path)
else:
format = unicode(path)
path = self.view.model().db.format_abspath(self.current_row, format)
path = self.view.model().db.format_abspath(self.current_row, path)
if path is not None:
open_local_file(path)

View File

@ -57,7 +57,7 @@
<item>
<widget class="QCheckBox" name="fit_cover">
<property name="text">
<string>Fit &amp;cover to view</string>
<string>Fit &amp;cover within view</string>
</property>
</widget>
</item>

View File

@ -3,14 +3,15 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to edit metadata in bulk'''
from PyQt4.QtCore import SIGNAL, QObject
from PyQt4.QtGui import QDialog, QGridLayout
from PyQt4.Qt import SIGNAL, QObject, QDialog, QGridLayout, \
QCoreApplication
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, \
authors_to_string
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2.dialogs.progress import ProgressDialog
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
@ -25,10 +26,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
len(rows))
self.write_series = False
self.changed = False
QObject.connect(self.button_box, SIGNAL("accepted()"), self.sync)
self.tags.update_tags_cache(self.db.all_tags())
self.remove_tags.update_tags_cache(self.db.all_tags())
all_tags = self.db.all_tags()
self.tags.update_tags_cache(all_tags)
self.remove_tags.update_tags_cache(all_tags)
self.initialize_combos()
@ -102,59 +103,98 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.tags.update_tags_cache(self.db.all_tags())
self.remove_tags.update_tags_cache(self.db.all_tags())
def sync(self):
for id in self.ids:
def accept(self):
if len(self.ids) < 1:
return QDialog.accept(self)
pd = ProgressDialog(_('Working'),
_('Applying changes to %d books. This may take a while.')%len(self.ids),
0, 0, self, cancelable=False)
pd.setModal(True)
pd.show()
def upd():
QCoreApplication.processEvents()
try:
remove = unicode(self.remove_tags.text()).strip().split(',')
add = unicode(self.tags.text()).strip().split(',')
au = unicode(self.authors.text())
if au:
au = string_to_authors(au)
self.db.set_authors(id, au, notify=False)
if self.auto_author_sort.isChecked():
x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
aus = unicode(self.author_sort.text())
if aus and self.author_sort.isEnabled():
self.db.set_author_sort(id, aus, notify=False)
if self.rating.value() != -1:
self.db.set_rating(id, 2*self.rating.value(), notify=False)
do_aus = self.author_sort.isEnabled()
rating = self.rating.value()
pub = unicode(self.publisher.text())
if pub:
self.db.set_publisher(id, pub, notify=False)
remove_tags = unicode(self.remove_tags.text()).strip()
if remove_tags:
remove_tags = [i.strip() for i in remove_tags.split(',')]
self.db.unapply_tags(id, remove_tags, notify=False)
tags = unicode(self.tags.text()).strip()
if tags:
tags = map(lambda x: x.strip(), tags.split(','))
self.db.set_tags(id, tags, append=True, notify=False)
if self.write_series:
series = unicode(self.series.currentText()).strip()
next = self.db.get_next_series_num_for(series)
self.db.set_series(id, series, notify=False)
num = next if self.autonumber_series.isChecked() and series else 1.0
self.db.set_series_index(id, num, notify=False)
do_series = self.write_series
series = unicode(self.series.currentText()).strip()
do_autonumber = self.autonumber_series.isChecked()
do_remove_format = self.remove_format.currentIndex() > -1
remove_format = unicode(self.remove_format.currentText())
do_swap_ta = self.swap_title_and_author.isChecked()
do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked()
if self.remove_format.currentIndex() > -1:
self.db.remove_format(id, unicode(self.remove_format.currentText()), index_is_id=True, notify=False)
upd()
self.changed = bool(self.ids)
for id in self.ids:
upd()
if do_swap_ta:
title = self.db.title(id, index_is_id=True)
aum = self.db.authors(id, index_is_id=True)
if aum:
aum = [a.strip().replace('|', ',') for a in aum.split(',')]
new_title = authors_to_string(aum)
self.db.set_title(id, new_title, notify=False)
if title:
new_authors = string_to_authors(title)
self.db.set_authors(id, new_authors, notify=False)
upd()
if self.swap_title_and_author.isChecked():
title = self.db.title(id, index_is_id=True)
aum = self.db.authors(id, index_is_id=True)
if aum:
aum = [a.strip().replace('|', ',') for a in aum.split(',')]
new_title = authors_to_string(aum)
self.db.set_title(id, new_title, notify=False)
if title:
new_authors = string_to_authors(title)
self.db.set_authors(id, new_authors, notify=False)
if au:
self.db.set_authors(id, string_to_authors(au), notify=False)
upd()
if self.remove_conversion_settings.isChecked():
self.db.delete_conversion_options(id, 'PIPE')
if do_auto_author:
x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
upd()
self.changed = True
for w in getattr(self, 'custom_column_widgets', []):
w.commit(self.ids)
if aus and do_aus:
self.db.set_author_sort(id, aus, notify=False)
upd()
if rating != -1:
self.db.set_rating(id, 2*rating, notify=False)
if pub:
self.db.set_publisher(id, pub, notify=False)
upd()
if do_series:
next = self.db.get_next_series_num_for(series)
self.db.set_series(id, series, notify=False)
num = next if do_autonumber and series else 1.0
self.db.set_series_index(id, num, notify=False)
upd()
if do_remove_format:
self.db.remove_format(id, remove_format, index_is_id=True, notify=False)
upd()
if do_remove_conv:
self.db.delete_conversion_options(id, 'PIPE')
upd()
for w in getattr(self, 'custom_column_widgets', []):
w.commit(self.ids)
self.db.bulk_modify_tags(self.ids, add=add, remove=remove,
notify=False)
upd()
finally:
pd.hide()
return QDialog.accept(self)
def series_changed(self):

View File

@ -13,7 +13,8 @@ class ProgressDialog(QDialog, Ui_Dialog):
canceled_signal = pyqtSignal()
def __init__(self, title, msg='', min=0, max=99, parent=None):
def __init__(self, title, msg='', min=0, max=99, parent=None,
cancelable=True):
QDialog.__init__(self, parent)
self.setupUi(self)
self.setWindowTitle(title)
@ -26,6 +27,9 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.canceled = False
self.button_box.rejected.connect(self._canceled)
if not cancelable:
self.button_box.setVisible(False)
self.cancelable = cancelable
def set_msg(self, msg=''):
self.message.setText(msg)
@ -54,8 +58,14 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.title.setText(_('Aborting...'))
self.canceled_signal.emit()
def reject(self):
if not self.cancelable:
return
QDialog.reject(self)
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Escape:
self._canceled()
if self.cancelable:
self._canceled()
else:
QDialog.keyPressEvent(self, ev)

View File

@ -14,7 +14,8 @@ from Queue import Empty, Queue
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication, QAction, \
QByteArray
from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob
@ -281,6 +282,7 @@ class JobsButton(QFrame):
self.pi = ProgressIndicator(self, size)
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
self._jobs.mouseReleaseEvent = self.mouseReleaseEvent
self.shortcut = _('Shift+Alt+J')
if horizontal:
self.setLayout(QHBoxLayout())
@ -297,15 +299,24 @@ class JobsButton(QFrame):
self.layout().setMargin(0)
self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setCursor(Qt.PointingHandCursor)
self.setToolTip(_('Click to see list of active jobs.'))
b = _('Click to see list of jobs')
self.setToolTip(b + u' (%s)'%self.shortcut)
self.action_toggle = QAction(b, parent)
parent.addAction(self.action_toggle)
self.action_toggle.setShortcut(self.shortcut)
self.action_toggle.triggered.connect(self.toggle)
def initialize(self, jobs_dialog, job_manager):
self.jobs_dialog = jobs_dialog
job_manager.job_added.connect(self.job_added)
job_manager.job_done.connect(self.job_done)
self.jobs_dialog.addAction(self.action_toggle)
def mouseReleaseEvent(self, event):
self.toggle()
def toggle(self, *args):
if self.jobs_dialog.isVisible():
self.jobs_dialog.hide()
else:
@ -365,12 +376,26 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.jobs_view.doubleClicked.connect(self.show_job_details)
self.jobs_view.horizontalHeader().setMovable(True)
state = gprefs.get('jobs view column layout', None)
if state is not None:
try:
self.jobs_view.horizontalHeader().restoreState(bytes(state))
except:
pass
self.restore_state()
def restore_state(self):
try:
geom = gprefs.get('jobs_dialog_geometry', bytearray(''))
self.restoreGeometry(QByteArray(geom))
state = gprefs.get('jobs view column layout', bytearray(''))
self.jobs_view.horizontalHeader().restoreState(QByteArray(state))
except:
pass
def save_state(self):
try:
state = bytearray(self.jobs_view.horizontalHeader().saveState())
gprefs['jobs view column layout'] = state
geom = bytearray(self.saveGeometry())
gprefs['jobs_dialog_geometry'] = geom
except:
pass
def show_job_details(self, index):
row = index.row()
@ -394,9 +419,13 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.model.kill_all_jobs()
def closeEvent(self, e):
try:
state = bytearray(self.jobs_view.horizontalHeader().saveState())
gprefs['jobs view column layout'] = state
except:
pass
e.accept()
self.save_state()
return QDialog.closeEvent(self, e)
def show(self, *args):
self.restore_state()
return QDialog.show(self, *args)
def hide(self, *args):
self.save_state()
return QDialog.hide(self, *args)

View File

@ -78,6 +78,7 @@ class TagsView(QTreeView): # {{{
self.setAnimated(True)
self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self))
self.made_connections = False
def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories']
@ -90,12 +91,14 @@ class TagsView(QTreeView): # {{{
self.search_restriction = None
self.setModel(self._model)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
pop = config['sort_tags_by']
self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
if not self.made_connections:
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.made_connections = True
db.add_listener(self.database_changed)
def database_changed(self, event, ids):

View File

@ -132,7 +132,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
# Jobs Button {{{
self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager)
self.jobs_button = JobsButton(horizontal=True)
self.jobs_button = JobsButton(horizontal=True, parent=self)
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
# }}}
@ -249,9 +249,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.read_settings()
self.finalize_layout()
self.donate_button.start_animation()
self.set_window_title()
self.iactions['Fetch News'].connect_scheduler()
self.iactions['Choose Library'].library_used(self.library_view.model().db)
for ac in self.iactions.values():
ac.initialization_complete()
def start_content_server(self):
from calibre.library.server.main import start_threaded_server
@ -351,8 +352,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def library_moved(self, newloc):
if newloc is None: return
db = LibraryDatabase2(newloc)
@ -367,10 +366,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.saved_search.clear_to_help()
self.book_details.reset_info()
self.library_view.model().count_changed()
self.iactions['Fetch News'].database_changed(db)
prefs['library_path'] = self.library_path
self.iactions['Choose Library'].library_used(self.library_view.model().db)
db = self.library_view.model().db
for action in self.iactions.values():
action.library_changed(db)
self.set_window_title()
def set_window_title(self):
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())
def location_selected(self, location):
'''
@ -511,6 +514,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
def shutdown(self, write_settings=True):
for action in self.iactions.values():
if not action.shutting_down():
return
if write_settings:
self.write_settings()
self.check_messages_timer.stop()

View File

@ -871,14 +871,14 @@ class LayoutButton(QToolButton):
def set_state_to_show(self, *args):
self.setChecked(False)
label =_('Show')
self.setText(label + ' ' + self.label + ' ' + self.shortcut)
self.setText(label + ' ' + self.label + u' (%s)'%self.shortcut)
self.setToolTip(self.text())
self.setStatusTip(self.text())
def set_state_to_hide(self, *args):
self.setChecked(True)
label = _('Hide')
self.setText(label + ' ' + self.label+ ' ' + self.shortcut)
self.setText(label + ' ' + self.label+ u' (%s)'%self.shortcut)
self.setToolTip(self.text())
self.setStatusTip(self.text())
@ -941,7 +941,10 @@ class Splitter(QSplitter):
@property
def is_side_index_hidden(self):
sizes = list(self.sizes())
return sizes[self.side_index] == 0
try:
return sizes[self.side_index] == 0
except IndexError:
return True
@property
def save_name(self):

View File

@ -26,7 +26,7 @@ from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import
from calibre import isbytestring
from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks
@ -116,6 +116,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# so that various code taht connects directly will not complain about
# missing functions
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
# Store temporary tables in memory
self.conn.execute('pragma temp_store=2')
self.conn.commit()
@classmethod
def exists_at(cls, path):
@ -1369,6 +1372,80 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return set([])
return set([r[0] for r in result])
@classmethod
def cleanup_tags(cls, tags):
tags = [x.strip() for x in tags if x.strip()]
tags = [x.decode(preferred_encoding, 'replace') \
if isbytestring(x) else x for x in tags]
tags = [u' '.join(x.split()) for x in tags]
ans, seen = [], set([])
for tag in tags:
if tag.lower() not in seen:
seen.add(tag.lower())
ans.append(tag)
return ans
def bulk_modify_tags(self, ids, add=[], remove=[], notify=False):
add = self.cleanup_tags(add)
remove = self.cleanup_tags(remove)
remove = set(remove) - set(add)
if not ids or (not add and not remove):
return
# Add tags that do not already exist into the tag table
all_tags = self.all_tags()
lt = [t.lower() for t in all_tags]
new_tags = [t for t in add if t.lower() not in lt]
if new_tags:
self.conn.executemany('INSERT INTO tags(name) VALUES (?)', [(x,) for x in
new_tags])
# Create the temporary tables to store the ids for books and tags
# to be operated on
tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add',
'temp_bulk_tag_edit_remove')
drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in tables])
creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t
for t in tables])
self.conn.executescript(drops + creates)
# Populate the books temp table
self.conn.executemany(
'INSERT INTO temp_bulk_tag_edit_books VALUES (?)',
[(x,) for x in ids])
# Populate the add/remove tags temp tables
for table, tags in enumerate([add, remove]):
if not tags:
continue
table = tables[table+1]
insert = ('INSERT INTO %s(id) SELECT tags.id FROM tags WHERE name=?'
' COLLATE PYNOCASE LIMIT 1')
self.conn.executemany(insert%table, [(x,) for x in tags])
if remove:
self.conn.execute(
'''DELETE FROM books_tags_link WHERE
book IN (SELECT id FROM %s) AND
tag IN (SELECT id FROM %s)'''
% (tables[0], tables[2]))
if add:
self.conn.execute(
'''
INSERT INTO books_tags_link(book, tag) SELECT {0}.id, {1}.id FROM
{0}, {1}
'''.format(tables[0], tables[1])
)
self.conn.executescript(drops)
self.conn.commit()
for x in ids:
tags = u','.join(self.get_tags(x))
self.data.set(x, self.FIELD_MAP['tags'], tags, row_is_id=True)
if notify:
self.notify('metadata', ids)
def set_tags(self, id, tags, append=False, notify=True):
'''
@param tags: list of strings
@ -1378,10 +1455,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1')
otags = self.get_tags(id)
tags = [x.strip() for x in tags if x.strip()]
tags = [x.decode(preferred_encoding, 'replace') if not isinstance(x,
unicode) else x for x in tags]
tags = [u' '.join(x.split()) for x in tags]
tags = self.cleanup_tags(tags)
for tag in (set(tags)-otags):
tag = tag.strip()
if not tag:
@ -1407,7 +1481,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)',
(id, tid))
self.conn.commit()
tags = ','.join(self.get_tags(id))
tags = u','.join(self.get_tags(id))
self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True)
if notify:
self.notify('metadata', [id])

View File

@ -13,10 +13,12 @@ from threading import Thread
from Queue import Queue
from threading import RLock
from datetime import datetime
from functools import partial
from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat
from calibre import isbytestring
global_lock = RLock()
@ -98,6 +100,19 @@ def _author_to_author_sort(x):
if not x: return ''
return author_to_author_sort(x.replace('|', ','))
def pynocase(one, two, encoding='utf-8'):
if isbytestring(one):
try:
one = one.decode(encoding, 'replace')
except:
pass
if isbytestring(two):
try:
two = two.decode(encoding, 'replace')
except:
pass
return cmp(one.lower(), two.lower())
class DBThread(Thread):
CLOSE = '-------close---------'
@ -115,10 +130,13 @@ class DBThread(Thread):
def connect(self):
self.conn = sqlite.connect(self.path, factory=Connection,
detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
encoding = self.conn.execute('pragma encoding').fetchone()[0]
self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row)
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_collation('PYNOCASE', partial(pynocase,
encoding=encoding))
if tweaks['title_series_sorting'] == 'strictly_alphabetic':
self.conn.create_function('title_sort', 1, lambda x:x)
else: