diff --git a/resources/images/format-fill-color.png b/resources/images/format-fill-color.png new file mode 100644 index 0000000000..946bead5da Binary files /dev/null and b/resources/images/format-fill-color.png differ diff --git a/resources/images/format-text-color.png b/resources/images/format-text-color.png new file mode 100644 index 0000000000..2ec27d559d Binary files /dev/null and b/resources/images/format-text-color.png differ diff --git a/resources/images/format-text-heading.png b/resources/images/format-text-heading.png new file mode 100644 index 0000000000..970acb7d60 Binary files /dev/null and b/resources/images/format-text-heading.png differ diff --git a/resources/images/insert-link.png b/resources/images/insert-link.png new file mode 100644 index 0000000000..b9e335d320 Binary files /dev/null and b/resources/images/insert-link.png differ diff --git a/resources/recipes/cnd.recipe b/resources/recipes/cnd.recipe new file mode 100644 index 0000000000..0e8206d07a --- /dev/null +++ b/resources/recipes/cnd.recipe @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2010, Derek Liang ' +''' +cnd.org +''' +import re + +from calibre.web.feeds.news import BasicNewsRecipe + +class TheCND(BasicNewsRecipe): + + title = 'CND' + __author__ = 'Derek Liang' + description = '' + INDEX = 'http://cnd.org' + language = 'zh' + conversion_options = {'linearize_tables':True} + + remove_tags_before = dict(name='div', id='articleHead') + remove_tags_after = dict(id='copyright') + remove_tags = [dict(name='table', attrs={'align':'right'}), dict(name='img', attrs={'src':'http://my.cnd.org/images/logo.gif'}), dict(name='hr', attrs={}), dict(name='small', attrs={})] + no_stylesheets = True + + preprocess_regexps = [(re.compile(r'', re.DOTALL), lambda m: '')] + + def print_version(self, url): + if url.find('news/article.php') >= 0: + return re.sub("^[^=]*", "http://my.cnd.org/modules/news/print.php?storyid", url) + else: + return re.sub("^[^=]*", "http://my.cnd.org/modules/wfsection/print.php?articleid", url) + + def parse_index(self): + soup = self.index_to_soup(self.INDEX) + + feeds = [] + articles = {} + + for a in soup.findAll('a', attrs={'target':'_cnd'}): + url = a['href'] + if url.find('article.php') < 0 : + continue + if url.startswith('/'): + url = 'http://cnd.org'+url + title = self.tag_to_string(a) + self.log('\tFound article: ', title, 'at', url) + date = a.nextSibling + if (date is not None) and len(date)>2: + if not articles.has_key(date): + articles[date] = [] + articles[date].append({'title':title, 'url':url, 'description': '', 'date':''}) + self.log('\t\tAppend to : ', date) + + self.log('log articles', articles) + mostCurrent = sorted(articles).pop() + self.title = 'CND ' + mostCurrent + + feeds.append((self.title, articles[mostCurrent])) + + return feeds + + def populate_article_metadata(self, article, soup, first): + header = soup.find('h3') + self.log('header: ' + self.tag_to_string(header)) + pass + diff --git a/resources/recipes/ecotrend.recipe b/resources/recipes/ecotrend.recipe new file mode 100644 index 0000000000..679f190e96 --- /dev/null +++ b/resources/recipes/ecotrend.recipe @@ -0,0 +1,42 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +globaleconomicanalysis.blogspot.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class GlobalEconomicAnalysis(BasicNewsRecipe): + title = "Mish's Global Economic Trend Analysis" + __author__ = 'Darko Miletic' + description = 'Thoughts on the global economy, housing, gold, silver, interest rates, oil, energy, China, commodities, the dollar, Euro, Renminbi, Yen, inflation, deflation, stagflation, precious metals, emerging markets, and policy decisions that affect the global markets.' + publisher = 'Mike Shedlock' + category = 'news, politics, economy, banking' + oldest_article = 7 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'utf8' + use_embedded_content = True + language = 'en' + remove_empty_feeds = True + publication_type = 'blog' + masthead_url = 'http://www.pagina12.com.ar/commons/imgs/logo-home.gif' + extra_css = """ + body{font-family: Arial,Helvetica,sans-serif } + img{margin-bottom: 0.4em; display:block} + """ + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + remove_tags = [ + dict(name=['meta','link','iframe','object','embed']) + ,dict(attrs={'class':'blogger-post-footer'}) + ] + remove_attributes=['border'] + + feeds = [(u'Articles', u'http://feeds2.feedburner.com/MishsGlobalEconomicTrendAnalysis')] diff --git a/resources/recipes/gva_be.recipe b/resources/recipes/gva_be.recipe index 34c4122394..f42bd23417 100644 --- a/resources/recipes/gva_be.recipe +++ b/resources/recipes/gva_be.recipe @@ -40,13 +40,12 @@ class GazetvanAntwerpen(BasicNewsRecipe): remove_tags_after = dict(name='span', attrs={'class':'author'}) feeds = [ - (u'Overzicht & Blikvanger', u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/overview/overzicht' ) + (u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' ) + ,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' ) ,(u'Stad & Regio' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/stadenregio' ) ,(u'Economie' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/economie' ) - ,(u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' ) - ,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' ) ,(u'Media & Cultur' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur') - ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur') + ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/wetenschap' ) ,(u'Sport' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/sport' ) ] diff --git a/resources/recipes/wenxuecity-znjy.recipe b/resources/recipes/wenxuecity-znjy.recipe new file mode 100644 index 0000000000..ecce80222e --- /dev/null +++ b/resources/recipes/wenxuecity-znjy.recipe @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2010, Derek Liang ' +''' +wenxuecity.com +''' +import re + +from calibre.web.feeds.news import BasicNewsRecipe + +class TheCND(BasicNewsRecipe): + + title = 'wenxuecity - znjy' + __author__ = 'Derek Liang' + description = '' + INDEX = 'http://bbs.wenxuecity.com/znjy/?elite=1' + language = 'zh' + conversion_options = {'linearize_tables':True} + + remove_tags_before = dict(name='div', id='message') + remove_tags_after = dict(name='div', id='message') + remove_tags = [dict(name='div', id='postmeta'), dict(name='div', id='footer')] + no_stylesheets = True + + preprocess_regexps = [(re.compile(r'', re.DOTALL), lambda m: '')] + + def print_version(self, url): + return url + '?print' + + def parse_index(self): + soup = self.index_to_soup(self.INDEX) + + feeds = [] + articles = {} + + for a in soup.findAll('a', attrs={'class':'post'}): + url = a['href'] + if url.startswith('/'): + url = 'http://bbs.wenxuecity.com'+url + title = self.tag_to_string(a) + self.log('\tFound article: ', title, ' at:', url) + dateReg = re.search( '(\d\d?)/(\d\d?)/(\d\d)', self.tag_to_string(a.parent) ) + date = '%(y)s/%(m)02d/%(d)02d' % {'y' : dateReg.group(3), 'm' : int(dateReg.group(1)), 'd' : int(dateReg.group(2)) } + if not articles.has_key(date): + articles[date] = [] + articles[date].append({'title':title, 'url':url, 'description': '', 'date':''}) + self.log('\t\tAppend to : ', date) + + self.log('log articles', articles) + mostCurrent = sorted(articles).pop() + self.title = '文学城 - 子女教育 - ' + mostCurrent + + feeds.append((self.title, articles[mostCurrent])) + + return feeds + + def populate_article_metadata(self, article, soup, first): + header = soup.find('h3') + self.log('header: ' + self.tag_to_string(header)) + pass + diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py index df2c1d6480..bd8463b1a7 100644 --- a/setup/installer/linux/freeze2.py +++ b/setup/installer/linux/freeze2.py @@ -318,7 +318,11 @@ class LinuxFreeze(Command): import codecs def set_default_encoding(): - locale.setlocale(locale.LC_ALL, '') + try: + locale.setlocale(locale.LC_ALL, '') + except: + print 'WARNING: Failed to set default libc locale, using en_US.UTF-8' + locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') enc = locale.getdefaultlocale()[1] if not enc: enc = locale.nl_langinfo(locale.CODESET) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index af2948cf82..2c095d6f7b 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -605,8 +605,9 @@ class Device(DeviceConfig, DevicePlugin): main, carda, cardb = self.find_device_nodes() if main is None: - raise DeviceError(_('Unable to detect the %s disk drive. Your ' - ' kernel is probably exporting a deprecated version of SYSFS.') + raise DeviceError(_('Unable to detect the %s disk drive. Either ' + 'the device has already been ejected, or your ' + 'kernel is exporting a deprecated version of SYSFS.') %self.__class__.__name__) self._linux_mount_map = {} diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 24dd1d3e5c..27973b5f5b 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -97,10 +97,15 @@ class DeleteAction(InterfaceAction): for action in list(self.delete_menu.actions())[1:]: action.setEnabled(enabled) - def _get_selected_formats(self, msg): + def _get_selected_formats(self, msg, ids): from calibre.gui2.dialogs.select_formats import SelectFormats - fmts = self.gui.library_view.model().db.all_formats() - d = SelectFormats([x.lower() for x in fmts], msg, parent=self.gui) + fmts = set([]) + db = self.gui.library_view.model().db + for x in ids: + fmts_ = db.formats(x, index_is_id=True, verify_formats=False) + if fmts_: + fmts.update(frozenset([x.lower() for x in fmts_.split(',')])) + d = SelectFormats(list(sorted(fmts)), msg, parent=self.gui) if d.exec_() != d.Accepted: return None return d.selected_formats @@ -118,7 +123,7 @@ class DeleteAction(InterfaceAction): if not ids: return fmts = self._get_selected_formats( - _('Choose formats to be deleted')) + _('Choose formats to be deleted'), ids) if not fmts: return for id in ids: @@ -136,7 +141,7 @@ class DeleteAction(InterfaceAction): if not ids: return fmts = self._get_selected_formats( - '

'+_('Choose formats not to be deleted')) + '

'+_('Choose formats not to be deleted'), ids) if fmts is None: return for id in ids: diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 547dd51848..d19c97e87b 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -5,18 +5,19 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import re, os from lxml import html from lxml.html import soupparser from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \ - QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, \ - QSyntaxHighlighter, QColor, QChar + QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, QUrl, \ + QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog from PyQt4.QtWebKit import QWebView from calibre.ebooks.chardet import xml_to_unicode from calibre import xml_replace_entities - +from calibre.gui2 import open_url class PageAction(QAction): # {{{ @@ -44,6 +45,18 @@ class PageAction(QAction): # {{{ # }}} +class BlockStyleAction(QAction): # {{{ + + def __init__(self, text, name, view): + QAction.__init__(self, text, view) + self._name = name + self.triggered.connect(self.apply_style) + + def apply_style(self, *args): + self.parent().exec_command('formatBlock', self._name) + +# }}} + class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): @@ -90,14 +103,109 @@ class EditorWidget(QWebView): # {{{ ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_'+name, ac) + self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'), + self) + self.action_color.triggered.connect(self.foreground_color) + + self.action_background = QAction(QIcon(I('format-fill-color')), + _('Background color'), self) + self.action_background.triggered.connect(self.background_color) + + self.action_block_style = QAction(QIcon(I('format-text-heading')), + _('Style text block'), self) + self.action_block_style.setToolTip( + _('Style the selected text block')) + self.block_style_menu = QMenu(self) + self.action_block_style.setMenu(self.block_style_menu) + self.block_style_actions = [] + for text, name in [ + (_('Normal'), 'p'), + (_('Heading') +' 1', 'h1'), + (_('Heading') +' 2', 'h2'), + (_('Heading') +' 3', 'h3'), + (_('Heading') +' 4', 'h4'), + (_('Heading') +' 5', 'h5'), + (_('Heading') +' 6', 'h6'), + (_('Pre-formatted'), 'pre'), + (_('Blockquote'), 'blockquote'), + (_('Address'), 'address'), + ]: + ac = BlockStyleAction(text, name, self) + self.block_style_menu.addAction(ac) + self.block_style_actions.append(ac) + + self.action_insert_link = QAction(QIcon(I('insert-link.png')), + _('Insert link'), self) + self.action_insert_link.triggered.connect(self.insert_link) + + self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) + self.page().linkClicked.connect(self.link_clicked) + + def link_clicked(self, url): + open_url(url) + + def foreground_color(self): + col = QColorDialog.getColor(Qt.black, self, + _('Choose foreground color'), QColorDialog.ShowAlphaChannel) + if col.isValid(): + self.exec_command('foreColor', unicode(col.name())) + + def background_color(self): + col = QColorDialog.getColor(Qt.white, self, + _('Choose background color'), QColorDialog.ShowAlphaChannel) + if col.isValid(): + self.exec_command('hiliteColor', unicode(col.name())) + + def insert_link(self, *args): + link, ok = QInputDialog.getText(self, _('Create link'), + _('Enter URL')) + if not ok: + return + url = self.parse_link(unicode(link)) + if url.isValid(): + url = unicode(url.toString()) + self.exec_command('createLink', url) + + def parse_link(self, link): + link = link.strip() + has_schema = re.match(r'^[a-zA-Z]+:', link) + if has_schema is not None: + url = QUrl(link, QUrl.TolerantMode) + if url.isValid(): + return url + if os.path.exists(link): + return QUrl.fromLocalFile(link) + + if has_schema is None: + first, _, rest = link.partition('.') + prefix = 'http' + if first == 'ftp': + prefix = 'ftp' + url = QUrl(prefix +'://'+link, QUrl.TolerantMode) + if url.isValid(): + return url + + return QUrl(link, QUrl.TolerantMode) + def sizeHint(self): return QSize(150, 150) + def exec_command(self, cmd, arg=None): + frame = self.page().mainFrame() + if arg is not None: + js = 'document.execCommand("%s", false, "%s");' % (cmd, arg) + else: + js = 'document.execCommand("%s", false, null);' % cmd + frame.evaluateJavaScript(js) + @dynamic_property def html(self): def fget(self): ans = u'' + check = unicode(self.page().mainFrame().toPlainText()).strip() + if not check: + return ans try: raw = unicode(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, @@ -348,7 +456,7 @@ class Highlighter(QSyntaxHighlighter): # }}} -class Editor(QWidget): +class Editor(QWidget): # {{{ def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -404,6 +512,15 @@ class Editor(QWidget): self.toolbar1.addAction(ac) self.toolbar1.addSeparator() + self.toolbar1.addAction(self.editor.action_color) + self.toolbar1.addAction(self.editor.action_background) + self.toolbar1.addSeparator() + + self.toolbar1.addAction(self.editor.action_block_style) + w = self.toolbar1.widgetForAction(self.editor.action_block_style) + w.setPopupMode(w.InstantPopup) + self.toolbar1.addAction(self.editor.action_insert_link) + self.code_edit.textChanged.connect(self.code_dirtied) self.editor.page().contentsChanged.connect(self.wyswyg_dirtied) @@ -411,18 +528,21 @@ class Editor(QWidget): def html(self): def fset(self, v): self.editor.html = v - return property(fget=lambda self:self.editor.html, fset=fset) + def fget(self): + self.tabs.setCurrentIndex(0) + return self.editor.html + return property(fget=fget, fset=fset) def change_tab(self, index): #print 'reloading:', (index and self.wyswyg_dirty) or (not index and # self.source_dirty) if index == 1: # changing to code view if self.wyswyg_dirty: - self.code_edit.setPlainText(self.html) + self.code_edit.setPlainText(self.editor.html) self.wyswyg_dirty = False elif index == 0: #changing to wyswyg if self.source_dirty: - self.html = unicode(self.code_edit.toPlainText()) + self.editor.html = unicode(self.code_edit.toPlainText()) self.source_dirty = False def wyswyg_dirtied(self, *args): @@ -431,6 +551,8 @@ class Editor(QWidget): def code_dirtied(self, *args): self.source_dirty = True +# }}} + if __name__ == '__main__': app = QApplication([]) w = Editor() diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index 14aca24db5..be9c5b4658 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -11,7 +11,6 @@ from PyQt4.Qt import Qt from calibre.gui2.convert.mobi_output_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2.widgets import FontFamilyModel -from calibre.utils.fonts import fontconfig font_family_model = None @@ -28,6 +27,7 @@ class PluginWidget(Widget, Ui_Form): 'mobi_ignore_margins', 'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc'] ) + from calibre.utils.fonts import fontconfig self.db, self.book_id = db, book_id global font_family_model diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 4a6acf0a5e..dc691c4ffe 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' '''Dialog to edit metadata in bulk''' -import re +import re, os from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \ pyqtSignal, QDialogButtonBox @@ -13,12 +13,41 @@ 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.ebooks.metadata.book.base import composite_formatter +from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2 import error_dialog from calibre.gui2.progress_indicator import ProgressIndicator from calibre.utils.config import dynamic from calibre.utils.titlecase import titlecase from calibre.utils.icu import sort_key, capitalize +from calibre.utils.config import prefs +from calibre.utils.magick.draw import identify_data + +def get_cover_data(path): + old = prefs['read_file_metadata'] + if not old: + prefs['read_file_metadata'] = True + cdata = area = None + + try: + mi = get_metadata(open(path, 'rb'), + os.path.splitext(path)[1][1:].lower()) + if mi.cover and os.access(mi.cover, os.R_OK): + cdata = open(mi.cover).read() + elif mi.cover_data[1] is not None: + cdata = mi.cover_data[1] + if cdata: + width, height, fmt = identify_data(cdata) + area = width*height + except: + cdata = area = None + + if old != prefs['read_file_metadata']: + prefs['read_file_metadata'] = old + + return cdata, area + + class MyBlockingBusy(QDialog): @@ -147,6 +176,20 @@ class MyBlockingBusy(QDialog): cdata = calibre_cover(mi.title, mi.format_field('authors')[-1], series_string=series_string) self.db.set_cover(id, cdata) + elif cover_action == 'fromfmt': + fmts = self.db.formats(id, index_is_id=True, verify_formats=False) + if fmts: + covers = [] + for fmt in fmts.split(','): + fmt = self.db.format_abspath(id, fmt, index_is_id=True) + if not fmt: continue + cdata, area = get_cover_data(fmt) + if cdata: + covers.append((cdata, area)) + covers.sort(key=lambda x: x[1]) + if covers: + self.db.set_cover(id, covers[-1][0]) + covers = [] elif self.current_phase == 2: # All of these just affect the DB, so we can tolerate a total rollback if do_auto_author: @@ -714,6 +757,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): cover_action = 'remove' elif self.cover_generate.isChecked(): cover_action = 'generate' + elif self.cover_from_fmt.isChecked(): + cover_action = 'fromfmt' args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, do_autonumber, do_remove_format, remove_format, do_swap_ta, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 8422c84ccb..dca7abc82c 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -414,6 +414,13 @@ Future conversion of these books will use the default settings. + + + + Set from &ebook file(s) + + + @@ -710,8 +717,8 @@ nothing should be put between the original text and the inserted text 0 0 - 122 - 38 + 726 + 334 diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 7c5a9f95d4..9cb9f7bbbc 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -34,6 +34,7 @@ from calibre.customize.ui import run_plugins_on_import, get_isbndb_key from calibre.gui2.preferences.social import SocialMetadata from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre import strftime +from calibre.library.comments import comments_to_html class CoverFetcher(Thread): # {{{ @@ -195,7 +196,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): _file + _(" is not a valid picture")) d.exec_() else: - self.cover_path.setText(_file) self.cover.setPixmap(pix) self.update_cover_tooltip() self.cover_changed = True @@ -409,7 +409,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if mi.series_index is not None: self.series_index.setValue(float(mi.series_index)) if mi.comments and mi.comments.strip(): - self.comments.setPlainText(mi.comments) + comments = comments_to_html(mi.comments) + self.comments.html = comments def sync_formats(self): @@ -556,7 +557,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if rating > 0: self.rating.setValue(int(rating/2.)) comments = self.db.comments(row) - self.comments.setPlainText(comments if comments else '') + if comments and comments.strip(): + comments = comments_to_html(comments) + self.comments.html = comments cover = self.db.cover(row) pubdate = db.pubdate(self.id, index_is_id=True) self.pubdate.setDate(QDate(pubdate.year, pubdate.month, @@ -806,10 +809,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.pubdate.setDate(QDate(dt.year, dt.month, dt.day)) summ = book.comments if summ: - prefix = unicode(self.comments.toPlainText()) + prefix = self.comment.html if prefix: prefix += '\n' - self.comments.setPlainText(prefix + summ) + self.comments.html = prefix + comments_to_html(summ) if book.rating is not None: self.rating.setValue(int(book.rating)) if book.tags: @@ -899,7 +902,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.db.set_series_index(self.id, self.series_index.value(), notify=False, commit=False) self.db.set_comment(self.id, - unicode(self.comments.toPlainText()).strip(), + self.comments.html, notify=False, commit=False) d = self.pubdate.date() d = qt_to_dt(d) @@ -936,16 +939,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): QDialog.reject(self, *args) def read_state(self): - wg = dynamic.get('metasingle_window_geometry', None) - ss = dynamic.get('metasingle_splitter_state', None) + wg = dynamic.get('metasingle_window_geometry2', None) + ss = dynamic.get('metasingle_splitter_state2', None) if wg is not None: self.restoreGeometry(wg) if ss is not None: self.splitter.restoreState(ss) def save_state(self): - dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry())) - dynamic.set('metasingle_splitter_state', + dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry())) + dynamic.set('metasingle_splitter_state2', bytes(self.splitter.saveState())) def break_cycles(self): diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 0355dc0fe6..dfa8c45797 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -6,8 +6,8 @@ 0 0 - 887 - 750 + 994 + 726 @@ -43,8 +43,8 @@ 0 0 - 879 - 711 + 986 + 687 @@ -66,8 +66,8 @@ &Basic metadata - - + + Qt::Horizontal @@ -495,29 +495,132 @@ Using this button to create author sort will change author sort from red to gree + + + + - - - &Comments + + + + 0 + 10 + - - - - - true - - - false + + Book Cover + + + + + + + 0 + 100 + + + + + 6 + + + QLayout::SetMaximumSize + + + 0 + + + + + Change &cover image: + + + cover_button + + + + + + + 6 + + + 0 + + + + + &Browse + + + + :/images/document_open.png:/images/document_open.png + + + + + + + Remove border (if any) from cover + + + T&rim + + + + :/images/trim.png:/images/trim.png + + + + + + + Reset cover to default + + + &Remove + + + + :/images/trash.png:/images/trash.png + + + + + + + + + + + + + Download co&ver + + + + + + + Generate a default cover based on the title and author + + + &Generate cover + + + + + - - + + @@ -546,6 +649,12 @@ Using this button to create author sort will change author sort from red to gree 140 + + + 100 + 0 + + QAbstractItemView::DropOnly @@ -644,129 +753,22 @@ Using this button to create author sort will change author sort from red to gree - + - + 0 10 - Book Cover + &Comments - + + + 0 + - - - - 0 - 100 - - - - - - - - 6 - - - QLayout::SetMaximumSize - - - 0 - - - - - Change &cover image: - - - cover_path - - - - - - - 6 - - - 0 - - - - - true - - - - - - - &Browse - - - - :/images/document_open.png:/images/document_open.png - - - - - - - Remove border (if any) from cover - - - T&rim - - - - :/images/trim.png:/images/trim.png - - - Qt::ToolButtonTextBesideIcon - - - - - - - Reset cover to default - - - ... - - - - :/images/trash.png:/images/trash.png - - - - - - - - - - - - - Download co&ver - - - - - - - Generate a default cover based on the title and author - - - &Generate cover - - - - + @@ -828,6 +830,12 @@ Using this button to create author sort will change author sort from red to gree

calibre/gui2/widgets.h
1 + + Editor + QWidget +
calibre/gui2/comments_editor.h
+ 1 +
title @@ -848,13 +856,11 @@ Using this button to create author sort will change author sort from red to gree date pubdate fetch_metadata_button - comments button_set_cover button_set_metadata formats add_format_button remove_format_button - cover_path cover_button trim_cover_button reset_cover diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index f7b8a8d401..25f69b1558 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -650,7 +650,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_table_of_contents.setDisabled(not self.iterator.toc) self.current_book_has_toc = bool(self.iterator.toc) self.current_title = title - self.setWindowTitle(self.base_window_title+' - '+title) + self.setWindowTitle(self.base_window_title+' - '+title + + ' [%s]'%os.path.splitext(pathtoebook)[1][1:].upper()) self.pos.setMaximum(sum(self.iterator.pages)) self.pos.setSuffix(' / %d'%sum(self.iterator.pages)) self.vertical_scrollbar.setMinimum(100) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 8ca1df917c..b2d8e4b8fd 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -19,7 +19,6 @@ from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs from calibre.constants import isosx from calibre.gui2.filename_pattern_ui import Ui_Form from calibre import fit_image -from calibre.utils.fonts import fontconfig from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.utils.config import prefs, XMLConfig @@ -283,6 +282,7 @@ class FontFamilyModel(QAbstractListModel): def __init__(self, *args): QAbstractListModel.__init__(self, *args) + from calibre.utils.fonts import fontconfig try: self.families = fontconfig.find_font_families() except: diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 5492c86fa9..8af70d5675 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -5,7 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, os +import re, os, posixpath import cherrypy @@ -88,17 +88,24 @@ class ContentServer(object): def static(self, name): 'Serves static content' name = name.lower() - cherrypy.response.headers['Content-Type'] = { + fname = posixpath.basename(name) + try: + cherrypy.response.headers['Content-Type'] = { 'js' : 'text/javascript', 'css' : 'text/css', 'png' : 'image/png', 'gif' : 'image/gif', 'html' : 'text/html', - '' : 'application/octet-stream', - }[name.rpartition('.')[-1].lower()] + }[fname.rpartition('.')[-1].lower()] + except KeyError: + raise cherrypy.HTTPError(404, '%r not a valid resource type'%name) cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time) - path = P('content_server/'+name) - if not os.path.exists(path): + basedir = os.path.abspath(P('content_server')) + path = os.path.join(basedir, name.replace('/', os.sep)) + path = os.path.abspath(path) + if not path.startswith(basedir): + raise cherrypy.HTTPError(403, 'Access to %s is forbidden'%name) + if not os.path.exists(path) or not os.path.isfile(path): raise cherrypy.HTTPError(404, '%s not found'%name) if self.opts.develop: lm = fromtimestamp(os.stat(path).st_mtime) diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index 5cab0c4920..3db3a1b285 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -7,15 +7,19 @@ __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, sys -from threading import Thread -from calibre.constants import plugins, iswindows +from calibre.constants import plugins, iswindows, islinux, isfreebsd _fc, _fc_err = plugins['fontconfig'] if _fc is None: raise RuntimeError('Failed to load fontconfig with error:'+_fc_err) +if islinux or isfreebsd: + Thread = object +else: + from threading import Thread + class FontConfig(Thread): def __init__(self): @@ -45,7 +49,8 @@ class FontConfig(Thread): self.failed = True def wait(self): - self.join() + if not (islinux or isfreebsd): + self.join() if self.failed: raise RuntimeError('Failed to initialize fontconfig') @@ -144,7 +149,13 @@ class FontConfig(Thread): return fonts if all else (fonts[0] if fonts else None) fontconfig = FontConfig() -fontconfig.start() +if islinux or isfreebsd: + # On X11 Qt also uses fontconfig, so initialization must happen in the + # main thread. In any case on X11 initializing fontconfig should be very + # fast + fontconfig.run() +else: + fontconfig.start() def test(): from pprint import pprint;