Sync to trunk.

This commit is contained in:
John Schember 2011-02-25 22:05:06 -05:00
commit 7d9b44d7de
83 changed files with 23549 additions and 21307 deletions

View File

@ -19,6 +19,81 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.7.47
date: 2011-02-25
new features:
- title: "Tag Browser: Support the creation of nested User Categories"
description: "See http://calibre-ebook.com/user_manual/gui.html#tag-browser for details"
type: major
- title: "Disable Kent District Library plugin to download series information. The website could not handle the load calibre's 2 million users put on it. You can manually re-enable it if you really want series information, but it is very slow"
- title: "Drivers for the Wexler T7001, Archos 7, Wink and Xperia X10"
- title: "Comic Input: Add option to not add links to individual pages to the Table of Contents when converting CBC files"
- title: "EPUB Output: Try to ensure that the cover image always has an id='cover' to workaround Nook cover reading bug."
tickets: [8182]
- title: "ODT input: Update odfpy library to latest version, adds support for bookmarks"
- title: "EPUB Output: Remove unnecessary CSS page breaks as they confuse the latest release of iBooks"
bug fixes:
- title: "Fix regression in 0.7.46 that broke creating date and composite custom columns"
- title: "Linux binary build: Fix ImageMagick trying to load system modules instead of bundled modules"
- title: "Kobo driver: Handle missing firmware version file"
- title: "ODT Input: Do not force the background color to white."
tickets: [9118]
- title: "MOBI Input: Do not speciy text-align for every paragraph. Fixes text-align inheritance issues for newer MOBIs with nested divs."
tickets: [9098]
- title: "EPUB Output: Do not set the file-as attribute on title elements in the OPF as the current OPF spec does not support file-as. Instead use a calibre extension to OPF."
tickets: [9109]
- title: "Content server: Fix regression that broke browsing User Categories via OPDS"
tickets: [9090]
- title: "Update the book details panel after adding books incase automerge is turned on and the current book is affected"
tickets: [9073]
- title: "FB2 Output: Fix paragraph spacing sometime incorrect."
tickets: [8927]
- title: "Tag Browser: Fix generation of search query for authors with quote characters in their names"
tickets: [9071]
- title: "Fix bug that could cause download of cover/social metadata from Amazon to sometimes fail"
- title: "LRF Input: Workaround for broken LRF files from BookDesigner that have incomplete TextStyle elements"
improved recipes:
- Le Monde
- Gizmodo
- Lifehacker
- ESPN
- Adevarul
- gsp.ro
- Ming Pao
new recipes:
- title: "Flickr Blog"
author: Ricardo Jurado
- title: "Various Romanian news sources"
author: Silviu Coatara
- title: "Osnews.pl and SwiatKindle"
author: Mori
- title: "Roger Ebert Journal"
author: Shane Erstad
- version: 0.7.46 - version: 0.7.46
date: 2011-02-18 date: 2011-02-18

View File

@ -108,9 +108,11 @@ function init() {
function toplevel_layout() { function toplevel_layout() {
var last = $(".toplevel li").last(); var last = $(".toplevel li").last();
var title = $('.toplevel h3').first(); var title = $('.toplevel h3').first();
if (title && title.position()) {
var bottom = last.position().top + last.height() - title.position().top; var bottom = last.position().top + last.height() - title.position().top;
$("#main").height(Math.max(200, bottom+75)); $("#main").height(Math.max(200, bottom+75));
} }
}
function toplevel() { function toplevel() {
$(".sort_select").hide(); $(".sort_select").hide();

View File

@ -16,14 +16,9 @@ class Deadspin(BasicNewsRecipe):
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
encoding = 'utf-8' encoding = 'utf-8'
use_embedded_content = False use_embedded_content = True
language = 'en' language = 'en'
masthead_url = 'http://cache.gawkerassets.com/assets/deadspin.com/img/logo.png' masthead_url = 'http://cache.gawkerassets.com/assets/deadspin.com/img/logo.png'
extra_css = '''
body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif}
img{margin-bottom: 1em}
h1{font-family :Arial,Helvetica,sans-serif; font-size:large}
'''
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
@ -31,13 +26,11 @@ class Deadspin(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
remove_attributes = ['width','height'] remove_tags = [
keep_only_tags = [dict(attrs={'class':'content permalink'})] {'class': 'feedflare'},
remove_tags_before = dict(name='h1') ]
remove_tags = [dict(attrs={'class':'contactinfo'})]
remove_tags_after = dict(attrs={'class':'contactinfo'})
feeds = [(u'Articles', u'http://feeds.gawker.com/deadspin/full')] feeds = [(u'Articles', u'http://feeds.gawker.com/deadspin/vip?format=xml')]
def preprocess_html(self, soup): def preprocess_html(self, soup):
return self.adeify_images(soup) return self.adeify_images(soup)

View File

@ -16,14 +16,10 @@ class Gawker(BasicNewsRecipe):
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
encoding = 'utf-8' encoding = 'utf-8'
use_embedded_content = False use_embedded_content = True
language = 'en' language = 'en'
masthead_url = 'http://cache.gawkerassets.com/assets/gawker.com/img/logo.png' masthead_url = 'http://cache.gawkerassets.com/assets/gawker.com/img/logo.png'
extra_css = '''
body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif}
img{margin-bottom: 1em}
h1{font-family :Arial,Helvetica,sans-serif; font-size:large}
'''
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
@ -31,13 +27,11 @@ class Gawker(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
remove_attributes = ['width','height'] remove_tags = [
keep_only_tags = [dict(attrs={'class':'content permalink'})] {'class': 'feedflare'},
remove_tags_before = dict(name='h1') ]
remove_tags = [dict(attrs={'class':'contactinfo'})]
remove_tags_after = dict(attrs={'class':'contactinfo'})
feeds = [(u'Articles', u'http://feeds.gawker.com/gawker/full')] feeds = [(u'Articles', u'http://feeds.gawker.com/gawker/vip?format=xml')]
def preprocess_html(self, soup): def preprocess_html(self, soup):
return self.adeify_images(soup) return self.adeify_images(soup)

View File

@ -1,10 +1,15 @@
__license__ = 'GPL v3'
__copyright__ = '2011'
'''
lemonde.fr
'''
import re import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
class LeMonde(BasicNewsRecipe): class LeMonde(BasicNewsRecipe):
title = 'Le Monde' title = 'Le Monde'
__author__ = 'veezh' __author__ = 'veezh'
description = u'Actualit\xe9s' description = 'Actualités'
oldest_article = 1 oldest_article = 1
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
@ -12,9 +17,23 @@ class LeMonde(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
encoding = 'cp1252' encoding = 'cp1252'
publisher = 'lemonde.fr' publisher = 'lemonde.fr'
category = 'news, France, world'
language = 'fr' language = 'fr'
#publication_type = 'newsportal'
extra_css = '''
h1{font-size:130%;}
.ariane{font-size:xx-small;}
.source{font-size:xx-small;}
#.href{font-size:xx-small;}
.LM_caption{color:#666666; font-size:x-small;}
#.main-article-info{font-family:Arial,Helvetica,sans-serif;}
#full-contents{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
'''
#preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
conversion_options = { conversion_options = {
'comments' : description 'comments' : description
,'tags' : category
,'language' : language ,'language' : language
,'publisher' : publisher ,'publisher' : publisher
,'linearize_tables': True ,'linearize_tables': True
@ -32,15 +51,28 @@ class LeMonde(BasicNewsRecipe):
return soup return soup
preprocess_regexps = [ preprocess_regexps = [
(re.compile(r'([0-9])%'), lambda m: m.group(1) + '&nbsp;%'),
(re.compile(r'([0-9])([0-9])([0-9]) ([0-9])([0-9])([0-9])'), lambda m: m.group(1) + m.group(2) + m.group(3) + '&nbsp;' + m.group(4) + m.group(5) + m.group(6)),
(re.compile(r'([0-9]) ([0-9])([0-9])([0-9])'), lambda m: m.group(1) + '&nbsp;' + m.group(2) + m.group(3) + m.group(4)),
(re.compile(r'<span>'), lambda match: ' <span>'),
(re.compile(r'\("'), lambda match: '(&laquo;&nbsp;'),
(re.compile(r'"\)'), lambda match: '&nbsp;&raquo;)'),
(re.compile(r'&ldquo;'), lambda match: '(&laquo;&nbsp;'),
(re.compile(r'&rdquo;'), lambda match: '&nbsp;&raquo;)'),
(re.compile(r'>\''), lambda match: '>&lsquo;'),
(re.compile(r' \''), lambda match: ' &lsquo;'), (re.compile(r' \''), lambda match: ' &lsquo;'),
(re.compile(r'\''), lambda match: '&rsquo;'), (re.compile(r'\''), lambda match: '&rsquo;'),
(re.compile(r'"<'), lambda match: '&nbsp;&raquo;<'), (re.compile(r'"<em>'), lambda match: '<em>&laquo;&nbsp;'),
(re.compile(r'"<em>"</em><em>'), lambda match: '<em>&laquo;&nbsp;'),
(re.compile(r'"<a href='), lambda match: '&laquo;&nbsp;<a href='),
(re.compile(r'</em>"'), lambda match: '&nbsp;&raquo;</em>'),
(re.compile(r'</a>"'), lambda match: '&nbsp;&raquo;</a>'),
(re.compile(r'"</'), lambda match: '&nbsp;&raquo;</'),
(re.compile(r'>"'), lambda match: '>&laquo;&nbsp;'), (re.compile(r'>"'), lambda match: '>&laquo;&nbsp;'),
(re.compile(r'"<'), lambda match: '&nbsp;&raquo;<'),
(re.compile(r'&rsquo;"'), lambda match: '&rsquo;«&nbsp;'), (re.compile(r'&rsquo;"'), lambda match: '&rsquo;«&nbsp;'),
(re.compile(r' "'), lambda match: ' &laquo;&nbsp;'), (re.compile(r' "'), lambda match: ' &laquo;&nbsp;'),
(re.compile(r'" '), lambda match: '&nbsp;&raquo; '), (re.compile(r'" '), lambda match: '&nbsp;&raquo; '),
(re.compile(r'\("'), lambda match: '(&laquo;&nbsp;'),
(re.compile(r'"\)'), lambda match: '&nbsp;&raquo;)'),
(re.compile(r'"\.'), lambda match: '&nbsp;&raquo;.'), (re.compile(r'"\.'), lambda match: '&nbsp;&raquo;.'),
(re.compile(r'",'), lambda match: '&nbsp;&raquo;,'), (re.compile(r'",'), lambda match: '&nbsp;&raquo;,'),
(re.compile(r'"\?'), lambda match: '&nbsp;&raquo;?'), (re.compile(r'"\?'), lambda match: '&nbsp;&raquo;?'),
@ -56,8 +88,14 @@ class LeMonde(BasicNewsRecipe):
(re.compile(r' %'), lambda match: '&nbsp;%'), (re.compile(r' %'), lambda match: '&nbsp;%'),
(re.compile(r'\.jpg&nbsp;&raquo; border='), lambda match: '.jpg'), (re.compile(r'\.jpg&nbsp;&raquo; border='), lambda match: '.jpg'),
(re.compile(r'\.png&nbsp;&raquo; border='), lambda match: '.png'), (re.compile(r'\.png&nbsp;&raquo; border='), lambda match: '.png'),
(re.compile(r' &ndash; '), lambda match: '&nbsp;&ndash; '),
(re.compile(r' '), lambda match: '&nbsp;&ndash; '),
(re.compile(r' - '), lambda match: '&nbsp;&ndash; '),
(re.compile(r' -,'), lambda match: '&nbsp;&ndash;,'),
(re.compile(r'&raquo;:'), lambda match: '&raquo;&nbsp;:'),
] ]
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'class':['contenu']}) dict(name='div', attrs={'class':['contenu']})
] ]
@ -65,11 +103,15 @@ class LeMonde(BasicNewsRecipe):
remove_tags_after = [dict(id='appel_temoignage')] remove_tags_after = [dict(id='appel_temoignage')]
def get_article_url(self, article): def get_article_url(self, article):
link = article.get('link') url = article.get('guid', None)
if 'blog' not in link: if '/chat/' in url or '.blog' in url or '/video/' in url or '/sport/' in url or '/portfolio/' in url or '/visuel/' in url :
return link url = None
return url
# def get_article_url(self, article):
# link = article.get('link')
# if 'blog' not in link and ('chat' not in link):
# return link
feeds = [ feeds = [
('A la une', 'http://www.lemonde.fr/rss/une.xml'), ('A la une', 'http://www.lemonde.fr/rss/une.xml'),
@ -94,3 +136,4 @@ class LeMonde(BasicNewsRecipe):
cover_url = link_item.img['src'] cover_url = link_item.img['src']
return cover_url return cover_url

View File

@ -3,22 +3,20 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = u'2011, Tomasz Dlugosz <tomek3d@gmail.com>' __copyright__ = u'2011, Tomasz Dlugosz <tomek3d@gmail.com>'
''' '''
swiatkindle.pl swiatczytnikow.pl
''' '''
import re import re
from calibre.web.feeds.news import BasicNewsRecipe class swiatczytnikow(BasicNewsRecipe):
title = u'Swiat Czytnikow'
class swiatkindle(BasicNewsRecipe): description = u'Czytniki e-książek w Polsce. Jak wybrać, kupić i korzystać z Amazon Kindle i innych'
title = u'Swiat Kindle'
description = u'Blog o czytniku Amazon Kindle. Wersje, ksi\u0105\u017cki, kupowanie i korzystanie w Polsce'
language = 'pl' language = 'pl'
__author__ = u'Tomasz D\u0142ugosz' __author__ = u'Tomasz D\u0142ugosz'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
feeds = [(u'\u015awiat Kindle - wpisy', u'http://swiatkindle.pl/feed')] feeds = [(u'Świat Czytników - wpisy', u'http://swiatczytnikow.pl/feed')]
remove_tags = [dict(name = 'ul', attrs = {'class' : 'similar-posts'})] remove_tags = [dict(name = 'ul', attrs = {'class' : 'similar-posts'})]

View File

@ -2,9 +2,9 @@
# -*- coding: utf-8 mode: python -*- # -*- coding: utf-8 mode: python -*-
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Steffen Siebert <calibre at steffensiebert.de>' __copyright__ = '2010-2011, Steffen Siebert <calibre at steffensiebert.de>'
__docformat__ = 'restructuredtext de' __docformat__ = 'restructuredtext de'
__version__ = '1.1' __version__ = '1.2'
""" """
Die Zeit EPUB Die Zeit EPUB
@ -13,21 +13,43 @@ Die Zeit EPUB
import os, urllib2, zipfile, re import os, urllib2, zipfile, re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import walk
class ZeitEPUBAbo(BasicNewsRecipe): class ZeitEPUBAbo(BasicNewsRecipe):
title = u'Zeit Online Premium' title = u'Die Zeit'
description = u'Das EPUB Abo der Zeit (needs subscription)' description = u'Das EPUB Abo der Zeit (needs subscription)'
language = 'de' language = 'de'
lang = 'de-DE' lang = 'de-DE'
__author__ = 'Steffen Siebert' __author__ = 'Steffen Siebert and Tobias Isenberg'
needs_subscription = True needs_subscription = True
conversion_options = { conversion_options = {
'no_default_epub_cover' : True 'no_default_epub_cover' : True,
# fixing the wrong left margin
'mobi_ignore_margins' : True,
} }
preprocess_regexps = [
# filtering for correct dashes
(re.compile(r' - '), lambda match: ' '), # regular "Gedankenstrich"
(re.compile(r' -,'), lambda match: ' ,'), # "Gedankenstrich" before a comma
(re.compile(r'(?<=\d)-(?=\d)'), lambda match: ''), # number-number
# filtering for unicode characters that are missing on the Kindle,
# try to replace them with meaningful work-arounds
(re.compile(u'\u2080'), lambda match: '<span style="font-size: 50%;">0</span>'), # subscript-0
(re.compile(u'\u2081'), lambda match: '<span style="font-size: 50%;">1</span>'), # subscript-1
(re.compile(u'\u2082'), lambda match: '<span style="font-size: 50%;">2</span>'), # subscript-2
(re.compile(u'\u2083'), lambda match: '<span style="font-size: 50%;">3</span>'), # subscript-3
(re.compile(u'\u2084'), lambda match: '<span style="font-size: 50%;">4</span>'), # subscript-4
(re.compile(u'\u2085'), lambda match: '<span style="font-size: 50%;">5</span>'), # subscript-5
(re.compile(u'\u2086'), lambda match: '<span style="font-size: 50%;">6</span>'), # subscript-6
(re.compile(u'\u2087'), lambda match: '<span style="font-size: 50%;">7</span>'), # subscript-7
(re.compile(u'\u2088'), lambda match: '<span style="font-size: 50%;">8</span>'), # subscript-8
(re.compile(u'\u2089'), lambda match: '<span style="font-size: 50%;">9</span>'), # subscript-9
]
def build_index(self): def build_index(self):
domain = "http://premium.zeit.de" domain = "http://premium.zeit.de"
url = domain + "/abovorteile/cgi-bin/_er_member/p4z.fpl?ER_Do=getUserData&ER_NextTemplate=login_ok" url = domain + "/abovorteile/cgi-bin/_er_member/p4z.fpl?ER_Do=getUserData&ER_NextTemplate=login_ok"
@ -55,9 +77,36 @@ class ZeitEPUBAbo(BasicNewsRecipe):
zfile.extractall(self.output_dir) zfile.extractall(self.output_dir)
tmp.close() tmp.close()
index = os.path.join(self.output_dir, 'content.opf') index = os.path.join(self.output_dir, 'content.opf')
self.report_progress(1,_('epub downloaded and extracted')) self.report_progress(1,_('epub downloaded and extracted'))
# doing regular expression filtering
for path in walk('.'):
(shortname, extension) = os.path.splitext(path)
if extension.lower() in ('.html', '.htm', '.xhtml'):
with open(path, 'r+b') as f:
raw = f.read()
raw = raw.decode('utf-8')
for pat, func in self.preprocess_regexps:
raw = pat.sub(func, raw)
f.seek(0)
f.truncate()
f.write(raw.encode('utf-8'))
# adding real cover
self.report_progress(0,_('trying to download cover image (titlepage)'))
self.download_cover()
self.conversion_options["cover"] = self.cover_path
return index return index
# getting url of the cover
def get_cover_url(self):
try:
inhalt = self.index_to_soup('http://www.zeit.de/inhalt')
cover_url = inhalt.find('div', attrs={'class':'singlearchive clearfix'}).img['src'].replace('icon_','')
except:
cover_url = 'http://images.zeit.de/bilder/titelseiten_zeit/1946/001_001.jpg'
return cover_url

View File

@ -7,7 +7,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, os, shutil, platform, subprocess, stat, py_compile, glob, \ import sys, os, shutil, platform, subprocess, stat, py_compile, glob, \
textwrap, tarfile textwrap, tarfile, re
from setup import Command, modules, basenames, functions, __version__, \ from setup import Command, modules, basenames, functions, __version__, \
__appname__ __appname__
@ -19,7 +19,7 @@ SITE_PACKAGES = ['IPython', 'PIL', 'dateutil', 'dns', 'PyQt4', 'mechanize',
QTDIR = '/usr/lib/qt4' QTDIR = '/usr/lib/qt4'
QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit', 'QtDBus') QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit', 'QtDBus')
MAGICK_PREFIX = '/usr'
binary_includes = [ binary_includes = [
'/usr/bin/pdftohtml', '/usr/bin/pdftohtml',
'/usr/lib/libwmflite-0.2.so.7', '/usr/lib/libwmflite-0.2.so.7',
@ -41,8 +41,8 @@ binary_includes = [
'/usr/lib/libgthread-2.0.so.0', '/usr/lib/libgthread-2.0.so.0',
'/usr/lib/libpng14.so.14', '/usr/lib/libpng14.so.14',
'/usr/lib/libexslt.so.0', '/usr/lib/libexslt.so.0',
'/usr/lib/libMagickWand.so.4', MAGICK_PREFIX+'/lib/libMagickWand.so.4',
'/usr/lib/libMagickCore.so.4', MAGICK_PREFIX+'/lib/libMagickCore.so.4',
'/usr/lib/libgcrypt.so.11', '/usr/lib/libgcrypt.so.11',
'/usr/lib/libgpg-error.so.0', '/usr/lib/libgpg-error.so.0',
'/usr/lib/libphonon.so.4', '/usr/lib/libphonon.so.4',
@ -116,9 +116,25 @@ class LinuxFreeze(Command):
if x not in ('designer', 'sqldrivers', 'codecs'): if x not in ('designer', 'sqldrivers', 'codecs'):
shutil.copytree(y, self.j(dest, x)) shutil.copytree(y, self.j(dest, x))
im = glob.glob('/usr/lib/ImageMagick-*')[0] im = glob.glob(MAGICK_PREFIX + '/lib/ImageMagick-*')[-1]
dest = self.j(self.lib_dir, 'ImageMagick') self.magick_base = os.path.basename(im)
dest = self.j(self.lib_dir, self.magick_base)
shutil.copytree(im, dest, ignore=shutil.ignore_patterns('*.a')) shutil.copytree(im, dest, ignore=shutil.ignore_patterns('*.a'))
from calibre import walk
for x in walk(dest):
if x.endswith('.la'):
raw = open(x).read()
raw = re.sub('libdir=.*', '', raw)
open(x, 'wb').write(raw)
dest = self.j(dest, 'config')
src = self.j(MAGICK_PREFIX, 'share', self.magick_base, 'config')
for x in glob.glob(src+'/*'):
d = self.j(dest, os.path.basename(x))
if os.path.isdir(x):
shutil.copytree(x, d)
else:
shutil.copyfile(x, d)
def compile_mount_helper(self): def compile_mount_helper(self):
self.info('Compiling mount helper...') self.info('Compiling mount helper...')
@ -278,9 +294,10 @@ class LinuxFreeze(Command):
base=`dirname $path` base=`dirname $path`
lib=$base/lib lib=$base/lib
export LD_LIBRARY_PATH=$lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=$lib:$LD_LIBRARY_PATH
export MAGICK_CONFIGURE_PATH=$lib/ImageMagick/config export MAGICK_HOME=$base
export MAGICK_CODER_MODULE_PATH=$lib/ImageMagick/modules-Q16/coders export MAGICK_CONFIGURE_PATH=$lib/{1}/config
export MAGICK_CODER_FILTER_PATH=$lib/ImageMagick/modules-Q16/filters export MAGICK_CODER_MODULE_PATH=$lib/{1}/modules-Q16/coders
export MAGICK_CODER_FILTER_PATH=$lib/{1}/modules-Q16/filters
$base/bin/{0} "$@" $base/bin/{0} "$@"
''') ''')
@ -292,7 +309,7 @@ class LinuxFreeze(Command):
exe = self.j(self.bin_dir, bname) exe = self.j(self.bin_dir, bname)
sh = self.j(self.base, bname) sh = self.j(self.base, bname)
with open(sh, 'wb') as f: with open(sh, 'wb') as f:
f.write(launcher.format(bname)) f.write(launcher.format(bname, self.magick_base))
os.chmod(sh, os.chmod(sh,
stat.S_IREAD|stat.S_IEXEC|stat.S_IWRITE|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) stat.S_IREAD|stat.S_IEXEC|stat.S_IWRITE|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.46' __version__ = '0.7.47'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

@ -62,6 +62,7 @@ class ANDROID(USBMS):
# Archos # Archos
0x0e79 : { 0x0e79 : {
0x1400 : [0x0222, 0x0216], 0x1400 : [0x0222, 0x0216],
0x1408 : [0x0222, 0x0216],
0x1419 : [0x0216], 0x1419 : [0x0216],
0x1420 : [0x0216], 0x1420 : [0x0216],
0x1422 : [0x0216] 0x1422 : [0x0216]

View File

@ -9,6 +9,8 @@ __docformat__ = 'restructuredtext en'
import os import os
from calibre.devices.usbms.driver import USBMS from calibre.devices.usbms.driver import USBMS
from calibre import prints
prints
class PALMPRE(USBMS): class PALMPRE(USBMS):
@ -268,5 +270,36 @@ class NEXTBOOK(USBMS):
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
VENDOR_NAME = 'NEXT2' VENDOR_NAME = 'NEXT2'
WINDOWS_MAIN_MEM = '1.0.14' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '1.0.14'
SUPPORTS_SUB_DIRS = True
'''
def upload_cover(self, path, filename, metadata, filepath):
if metadata.thumbnail and metadata.thumbnail[-1]:
path = path.replace('/', os.sep)
is_main = path.startswith(self._main_prefix)
prefix = None
if is_main:
prefix = self._main_prefix
else:
if self._card_a_prefix and \
path.startswith(self._card_a_prefix):
prefix = self._card_a_prefix
elif self._card_b_prefix and \
path.startswith(self._card_b_prefix):
prefix = self._card_b_prefix
if prefix is None:
prints('WARNING: Failed to find prefix for:', filepath)
return
thumbnail_dir = os.path.join(prefix, '.Cover')
relpath = os.path.relpath(filepath, prefix)
if relpath.startswith('..\\'):
relpath = relpath[3:]
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
if not os.path.exists(thumbnail_dir):
os.makedirs(thumbnail_dir)
with open(os.path.join(thumbnail_dir, filename+'.jpg'), 'wb') as f:
f.write(metadata.thumbnail[-1])
'''

View File

@ -171,6 +171,13 @@ class TagCategories(QDialog, Ui_TagCategories):
cat_name = unicode(self.input_box.text()).strip() cat_name = unicode(self.input_box.text()).strip()
if cat_name == '': if cat_name == '':
return False return False
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
if len(comps) == 0 or '.'.join(comps) != cat_name:
error_dialog(self, _('Invalid name'),
_('That name contains leading or trailing periods, '
'multiple periods in a row or spaces before '
'or after periods.')).exec_()
return False
for c in self.categories: for c in self.categories:
if strcmp(c, cat_name) == 0: if strcmp(c, cat_name) == 0:
error_dialog(self, _('Name already used'), error_dialog(self, _('Name already used'),
@ -193,6 +200,14 @@ class TagCategories(QDialog, Ui_TagCategories):
return False return False
if not self.current_cat_name: if not self.current_cat_name:
return False return False
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
if len(comps) == 0 or '.'.join(comps) != cat_name:
error_dialog(self, _('Invalid name'),
_('That name contains leading or trailing periods, '
'multiple periods in a row or spaces before '
'or after periods.')).exec_()
return False
for c in self.categories: for c in self.categories:
if strcmp(c, cat_name) == 0: if strcmp(c, cat_name) == 0:
error_dialog(self, _('Name already used'), error_dialog(self, _('Name already used'),

View File

@ -9,7 +9,7 @@ Browsing book collection by tags.
import traceback, copy, cPickle import traceback, copy, cPickle
from itertools import izip from itertools import izip, repeat
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
@ -22,7 +22,7 @@ from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE, gprefs from calibre.gui2 import config, NONE, gprefs
from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key, upper, lower, strcmp from calibre.utils.icu import sort_key, lower, strcmp
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.formatter import eval_formatter from calibre.utils.formatter import eval_formatter
from calibre.gui2 import error_dialog, question_dialog from calibre.gui2 import error_dialog, question_dialog
@ -32,6 +32,9 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
from calibre.gui2.widgets import HistoryLineEdit from calibre.gui2.widgets import HistoryLineEdit
def original_name(t):
return getattr(t, 'original_name', t.name)
class TagDelegate(QItemDelegate): # {{{ class TagDelegate(QItemDelegate): # {{{
def paint(self, painter, option, index): def paint(self, painter, option, index):
@ -82,7 +85,7 @@ class TagsView(QTreeView): # {{{
author_sort_edit = pyqtSignal(object, object) author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal() tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal()
drag_drop_finished = pyqtSignal(object, object) drag_drop_finished = pyqtSignal(object)
def __init__(self, parent=None): def __init__(self, parent=None):
QTreeView.__init__(self, parent=None) QTreeView.__init__(self, parent=None)
@ -228,9 +231,13 @@ class TagsView(QTreeView): # {{{
self._toggle(index, set_to=search_state) self._toggle(index, set_to=search_state)
return return
if action == 'add_to_category': if action == 'add_to_category':
self.add_item_to_user_cat.emit(category, tag = index.tag
getattr(index, 'original_name', index.name), if len(index.children) > 0:
index.category) for c in index.children:
self.add_item_to_user_cat.emit(category, original_name(c.tag),
c.tag.category)
self.add_item_to_user_cat.emit(category, original_name(tag),
tag.category)
return return
if action == 'add_subcategory': if action == 'add_subcategory':
self.add_subcategory.emit(key) self.add_subcategory.emit(key)
@ -242,8 +249,12 @@ class TagsView(QTreeView): # {{{
self.delete_user_category.emit(key) self.delete_user_category.emit(key)
return return
if action == 'delete_item_from_user_category': if action == 'delete_item_from_user_category':
self.del_item_from_user_cat.emit(key, tag = index.tag
getattr(index, 'original_name', index.name), index.category) if len(index.children) > 0:
for c in index.children:
self.del_item_from_user_cat.emit(key, original_name(c.tag),
c.tag.category)
self.del_item_from_user_cat.emit(key, original_name(tag), tag.category)
return return
if action == 'manage_searches': if action == 'manage_searches':
self.saved_search_edit.emit(category) self.saved_search_edit.emit(category)
@ -278,8 +289,8 @@ class TagsView(QTreeView): # {{{
tag = None tag = None
if item.type == TagTreeItem.TAG: if item.type == TagTreeItem.TAG:
tag_item = item
tag = item.tag tag = item.tag
can_edit = getattr(tag, 'can_edit', True)
while item.type != TagTreeItem.CATEGORY: while item.type != TagTreeItem.CATEGORY:
item = item.parent item = item.parent
@ -297,7 +308,7 @@ class TagsView(QTreeView): # {{{
if tag: if tag:
# If the user right-clicked on an editable item, then offer # If the user right-clicked on an editable item, then offer
# the possibility of renaming that item. # the possibility of renaming that item.
if can_edit: if tag.is_editable:
# Add the 'rename' items # Add the 'rename' items
self.context_menu.addAction(_('Rename %s')%tag.name, self.context_menu.addAction(_('Rename %s')%tag.name,
partial(self.context_menu_handler, action='edit_item', partial(self.context_menu_handler, action='edit_item',
@ -317,8 +328,7 @@ class TagsView(QTreeView): # {{{
m.addAction(self.user_category_icon, n, m.addAction(self.user_category_icon, n,
partial(self.context_menu_handler, partial(self.context_menu_handler,
'add_to_category', 'add_to_category',
category='.'.join(p), category='.'.join(p), index=tag_item))
index=tag))
if len(tree_dict[k]): if len(tree_dict[k]):
tm = m.addMenu(self.user_category_icon, tm = m.addMenu(self.user_category_icon,
_('Children of %s')%n) _('Children of %s')%n)
@ -331,7 +341,7 @@ class TagsView(QTreeView): # {{{
_('Remove %s from category %s')%(tag.name, item.py_name), _('Remove %s from category %s')%(tag.name, item.py_name),
partial(self.context_menu_handler, partial(self.context_menu_handler,
action='delete_item_from_user_category', action='delete_item_from_user_category',
key = key, index = tag)) key = key, index = tag_item))
# Add the search for value items # Add the search for value items
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for %s')%tag.name, _('Search for %s')%tag.name,
@ -345,7 +355,7 @@ class TagsView(QTreeView): # {{{
index=index)) index=index))
self.context_menu.addSeparator() self.context_menu.addSeparator()
elif key.startswith('@') and not item.is_gst: elif key.startswith('@') and not item.is_gst:
if item.can_edit: if item.can_be_edited:
self.context_menu.addAction(self.user_category_icon, self.context_menu.addAction(self.user_category_icon,
_('Rename %s')%item.py_name, _('Rename %s')%item.py_name,
partial(self.context_menu_handler, action='edit_item', partial(self.context_menu_handler, action='edit_item',
@ -386,8 +396,8 @@ class TagsView(QTreeView): # {{{
self.db.field_metadata[key]['is_custom']: self.db.field_metadata[key]['is_custom']:
self.context_menu.addAction(_('Manage %s')%category, self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='open_editor', partial(self.context_menu_handler, action='open_editor',
category=getattr(tag, 'original_name', tag.name) category=original_name(tag) if tag else None,
if tag else None, key=key)) key=key))
elif key == 'authors': elif key == 'authors':
self.context_menu.addAction(_('Manage %s')%category, self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='edit_author_sort')) partial(self.context_menu_handler, action='edit_author_sort'))
@ -524,9 +534,11 @@ class TagTreeItem(object): # {{{
ROOT = 2 ROOT = 2
def __init__(self, data=None, category_icon=None, icon_map=None, def __init__(self, data=None, category_icon=None, icon_map=None,
parent=None, tooltip=None, category_key=None): parent=None, tooltip=None, category_key=None, temporary=False):
self.parent = parent self.parent = parent
self.children = [] self.children = []
self.id_set = set()
self.is_gst = False
self.boxed = False self.boxed = False
if self.parent is not None: if self.parent is not None:
self.parent.append(self) self.parent.append(self)
@ -541,6 +553,7 @@ class TagTreeItem(object): # {{{
self.bold_font.setBold(True) self.bold_font.setBold(True)
self.bold_font = QVariant(self.bold_font) self.bold_font = QVariant(self.bold_font)
self.category_key = category_key self.category_key = category_key
self.temporary = temporary
elif self.type == self.TAG: elif self.type == self.TAG:
icon_map[0] = data.icon icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
@ -597,18 +610,20 @@ class TagTreeItem(object): # {{{
p = self p = self
while p.parent.type != self.ROOT: while p.parent.type != self.ROOT:
p = p.parent p = p.parent
if p.category_key.startswith('@'): if not tag.is_hierarchical:
name = getattr(tag, 'original_name', tag.name) name = original_name(tag)
else: else:
name = tag.name name = tag.name
tt_author = False tt_author = False
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
if tag.count == 0: count = len(self.id_set)
count = count if count > 0 else tag.count
if count == 0:
return QVariant('%s'%(name)) return QVariant('%s'%(name))
else: else:
return QVariant('[%d] %s'%(tag.count, name)) return QVariant('[%d] %s'%(count, name))
if role == Qt.EditRole: if role == Qt.EditRole:
return QVariant(getattr(tag, 'original_name', tag.name)) return QVariant(original_name(tag))
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icon_state_map[tag.state] return self.icon_state_map[tag.state]
if role == Qt.ToolTipRole: if role == Qt.ToolTipRole:
@ -671,7 +686,9 @@ class TagsModel(QAbstractItemModel): # {{{
self.filter_categories_by = filter_categories_by self.filter_categories_by = filter_categories_by
self.collapse_model = collapse_model self.collapse_model = collapse_model
# get_node_tree cannot return None here, because row_map is empty # get_node_tree cannot return None here, because row_map is empty. Note
# that get_node_tree can indirectly change the user_categories dict.
data = self.get_node_tree(config['sort_tags_by']) data = self.get_node_tree(config['sort_tags_by'])
gst = db.prefs.get('grouped_search_terms', {}) gst = db.prefs.get('grouped_search_terms', {})
self.root_item = TagTreeItem() self.root_item = TagTreeItem()
@ -693,7 +710,7 @@ class TagsModel(QAbstractItemModel): # {{{
tt = _(u'The lookup/search name is "{0}"').format(r) tt = _(u'The lookup/search name is "{0}"').format(r)
if r.startswith('@'): if r.startswith('@'):
path_parts = [p.strip() for p in r.split('.') if p.strip()] path_parts = [p for p in r.split('.')]
path = '' path = ''
last_category_node = self.root_item last_category_node = self.root_item
tree_root = self.category_node_tree tree_root = self.category_node_tree
@ -708,7 +725,7 @@ class TagsModel(QAbstractItemModel): # {{{
last_category_node = node last_category_node = node
category_node_map[path] = node category_node_map[path] = node
self.category_nodes.append(node) self.category_nodes.append(node)
node.can_edit = (not is_gst) and (i == (len(path_parts)-1)) node.can_be_edited = (not is_gst) and (i == (len(path_parts)-1))
node.is_gst = is_gst node.is_gst = is_gst
if not is_gst: if not is_gst:
tree_root[p] = {} tree_root[p] = {}
@ -741,6 +758,7 @@ class TagsModel(QAbstractItemModel): # {{{
if idx.isValid(): if idx.isValid():
# get some useful serializable data # get some useful serializable data
node = idx.internalPointer() node = idx.internalPointer()
path = self.path_for_index(idx)
if node.type == TagTreeItem.CATEGORY: if node.type == TagTreeItem.CATEGORY:
d = (node.type, node.py_name, node.category_key) d = (node.type, node.py_name, node.category_key)
else: else:
@ -748,8 +766,8 @@ class TagsModel(QAbstractItemModel): # {{{
p = node p = node
while p.type != TagTreeItem.CATEGORY: while p.type != TagTreeItem.CATEGORY:
p = p.parent p = p.parent
d = (node.type, p.category_key, p.is_gst, d = (node.type, p.category_key, p.is_gst, original_name(t),
getattr(t, 'original_name', t.name), t.category, t.id) t.category, path)
data.append(d) data.append(d)
else: else:
data.append(None) data.append(None)
@ -788,37 +806,30 @@ class TagsModel(QAbstractItemModel): # {{{
''' '''
src is a list of tuples representing items to copy. The tuple is src is a list of tuples representing items to copy. The tuple is
(type, containing category key, category key is global search term, (type, containing category key, category key is global search term,
full name, category key, id) full name, category key, path to node)
The 'id' member is ignored, and can be None.
The type must be TagTreeItem.TAG The type must be TagTreeItem.TAG
dest is the TagTreeItem node to receive the items dest is the TagTreeItem node to receive the items
action is Qt.CopyAction or Qt.MoveAction action is Qt.CopyAction or Qt.MoveAction
''' '''
user_cats = self.db.prefs.get('user_categories', {}) def process_source_node(user_cats, src_parent, src_parent_is_gst,
parent_node = None is_uc, dest_key, node):
copied_node = None '''
for s in src: Copy/move an item and all its children to the destination
src_parent, src_parent_is_gst, src_name, src_cat = s[1:5] '''
parent_node = src_parent copied = False
if src_parent.startswith('@'): src_name = original_name(node.tag)
is_uc = True src_cat = node.tag.category
src_parent = src_parent[1:]
else:
is_uc = False
dest_key = dest.category_key[1:]
if dest_key not in user_cats:
continue
new_cat = []
# delete the item if the source is a user category and action is move # delete the item if the source is a user category and action is move
if is_uc and not src_parent_is_gst and src_parent in user_cats and \ if is_uc and not src_parent_is_gst and src_parent in user_cats and \
action == Qt.MoveAction: action == Qt.MoveAction:
new_cat = []
for tup in user_cats[src_parent]: for tup in user_cats[src_parent]:
if src_name == tup[0] and src_cat == tup[1]: if src_name == tup[0] and src_cat == tup[1]:
continue continue
new_cat.append(list(tup)) new_cat.append(list(tup))
user_cats[src_parent] = new_cat user_cats[src_parent] = new_cat
else: else:
copied_node = (src_parent, src_name) copied = True
# Now add the item to the destination user category # Now add the item to the destination user category
add_it = True add_it = True
@ -830,18 +841,53 @@ class TagsModel(QAbstractItemModel): # {{{
if add_it: if add_it:
user_cats[dest_key].append([src_name, src_cat, 0]) user_cats[dest_key].append([src_name, src_cat, 0])
for c in node.children:
copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
is_uc, dest_key, c)
return copied
user_cats = self.db.prefs.get('user_categories', {})
parent_node = None
copied = False
path = None
for s in src:
src_parent, src_parent_is_gst = s[1:3]
path = s[5]
parent_node = src_parent
if src_parent.startswith('@'):
is_uc = True
src_parent = src_parent[1:]
else:
is_uc = False
dest_key = dest.category_key[1:]
if dest_key not in user_cats:
continue
node = self.index_for_path(path)
if node:
copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
is_uc, dest_key, node.internalPointer())
self.db.prefs.set('user_categories', user_cats) self.db.prefs.set('user_categories', user_cats)
self.tags_view.recount() self.tags_view.recount()
# Scroll to the item copied. If it was moved, scroll to the parent
if parent_node is not None: if parent_node is not None:
self.clear_boxed()
m = self.tags_view.model() m = self.tags_view.model()
if copied_node is not None: if not copied:
path = m.find_item_node(parent_node, copied_node[1], None, p = path[-1]
equals_match=True) if p == 0:
else:
path = m.find_category_node(parent_node) path = m.find_category_node(parent_node)
else:
path[-1] = p - 1
idx = m.index_for_path(path) idx = m.index_for_path(path)
self.tags_view.setExpanded(idx, True) self.tags_view.setExpanded(idx, True)
if idx.internalPointer().type == TagTreeItem.TAG:
m.show_item_at_index(idx, box=True)
else:
m.show_item_at_index(idx) m.show_item_at_index(idx)
return True return True
@ -875,7 +921,7 @@ class TagsModel(QAbstractItemModel): # {{{
def handle_user_category_drop(self, on_node, ids, column): def handle_user_category_drop(self, on_node, ids, column):
categories = self.db.prefs.get('user_categories', {}) categories = self.db.prefs.get('user_categories', {})
category = categories.get(on_node.category_key[:-1], None) category = categories.get(on_node.category_key[1:], None)
if category is None: if category is None:
return return
fm_src = self.db.metadata_for_field(column) fm_src = self.db.metadata_for_field(column)
@ -909,9 +955,9 @@ class TagsModel(QAbstractItemModel): # {{{
break break
else: else:
category.append([val, column, vmap[val]]) category.append([val, column, vmap[val]])
categories[on_node.category_key[:-1]] = category categories[on_node.category_key[1:]] = category
self.db.prefs.set('user_categories', categories) self.db.prefs.set('user_categories', categories)
self.drag_drop_finished.emit(None, True) self.tags_view.recount()
def handle_drop(self, on_node, ids): def handle_drop(self, on_node, ids):
#print 'Dropped ids:', ids, on_node.tag #print 'Dropped ids:', ids, on_node.tag
@ -929,7 +975,7 @@ class TagsModel(QAbstractItemModel): # {{{
fm = self.db.metadata_for_field(key) fm = self.db.metadata_for_field(key)
is_multiple = fm['is_multiple'] is_multiple = fm['is_multiple']
val = on_node.tag.name val = original_name(on_node.tag)
for id in ids: for id in ids:
mi = self.db.get_metadata(id, index_is_id=True) mi = self.db.get_metadata(id, index_is_id=True)
@ -961,7 +1007,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.db.set_metadata(id, mi, set_title=False, self.db.set_metadata(id, mi, set_title=False,
set_authors=set_authors, commit=False) set_authors=set_authors, commit=False)
self.db.commit() self.db.commit()
self.drag_drop_finished.emit(ids, False) self.drag_drop_finished.emit(ids)
def set_search_restriction(self, s): def set_search_restriction(self, s):
self.search_restriction = s self.search_restriction = s
@ -1044,24 +1090,60 @@ class TagsModel(QAbstractItemModel): # {{{
else: else:
collapse_model = 'partition' collapse_model = 'partition'
collapse_template = tweaks['categories_collapsed_popularity_template'] collapse_template = tweaks['categories_collapsed_popularity_template']
collapse_letter = collapse_letter_sk = None
def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): def process_one_node(category, state_map):
collapse_letter = None
category_index = self.createIndex(category.row(), 0, category) category_index = self.createIndex(category.row(), 0, category)
category_node = category_index.internalPointer() category_node = category_index.internalPointer()
key = category_node.category_key key = category_node.category_key
if key not in data: if key not in data:
return ((collapse_letter, collapse_letter_sk)) return
cat_len = len(data[key]) cat_len = len(data[key])
if cat_len <= 0: if cat_len <= 0:
return ((collapse_letter, collapse_letter_sk)) return
category_child_map = {}
fm = self.db.field_metadata[key] fm = self.db.field_metadata[key]
clear_rating = True if key not in self.categories_with_ratings and \ clear_rating = True if key not in self.categories_with_ratings and \
not fm['is_custom'] and \ not fm['is_custom'] and \
not fm['kind'] == 'user' \ not fm['kind'] == 'user' \
else False else False
tt = key if fm['kind'] == 'user' else None tt = key if fm['kind'] == 'user' else None
if collapse_model == 'first letter':
# Build a list of 'equal' first letters by looking for
# overlapping ranges. If a range overlaps another, then the
# letters are assumed to be equivalent. ICU collating is complex
# beyond belief. This mechanism lets us determine the logical
# first character from ICU's standpoint.
chardict = {}
for idx,tag in enumerate(data[key]):
if not tag.sort:
c = ' '
else:
c = icu_upper(tag.sort[0])
if c not in chardict:
chardict[c] = [idx, idx]
else:
chardict[c][1] = idx
# sort the ranges to facilitate detecting overlap
ranges = sorted([(v[0], v[1], c) for c,v in chardict.items()])
# Create a list of 'first letters' to use for each item in
# the category. The list is generated using the ranges. Overlaps
# are filled with the character that first occurs.
cl_list = list(repeat(None, len(data[key])))
for t in ranges:
start = t[0]
c = t[2]
if cl_list[start] is None:
nc = c
else:
nc = cl_list[start]
for i in range(start, t[1]+1):
cl_list[i] = nc
for idx,tag in enumerate(data[key]): for idx,tag in enumerate(data[key]):
if clear_rating: if clear_rating:
tag.avg_rating = None tag.avg_rating = None
@ -1078,72 +1160,77 @@ class TagsModel(QAbstractItemModel): # {{{
name = eval_formatter.safe_format(collapse_template, name = eval_formatter.safe_format(collapse_template,
d, 'TAG_VIEW', None) d, 'TAG_VIEW', None)
self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1) self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1)
sub_cat = TagTreeItem(parent=category, sub_cat = TagTreeItem(parent=category, data = name,
data = name, tooltip = None, tooltip = None, temporary=True,
category_icon = category_node.icon, category_icon = category_node.icon,
category_key=category_node.category_key) category_key=category_node.category_key)
self.endInsertRows() self.endInsertRows()
else: else: # by 'first letter'
ts = tag.sort cl = cl_list[idx]
if not ts: if cl != collapse_letter:
ts = ' ' collapse_letter = cl
try:
sk = sort_key(ts)[0]
except:
sk = ts[0]
if sk != collapse_letter_sk:
collapse_letter = upper(ts[0])
try:
collapse_letter_sk = sort_key(collapse_letter)[0]
except:
collapse_letter_sk = collapse_letter
sub_cat = TagTreeItem(parent=category, sub_cat = TagTreeItem(parent=category,
data = collapse_letter, data = collapse_letter,
category_icon = category_node.icon, category_icon = category_node.icon,
tooltip = None, tooltip = None, temporary=True,
category_key=category_node.category_key) category_key=category_node.category_key)
node_parent = sub_cat node_parent = sub_cat
else: else:
node_parent = category node_parent = category
components = [t for t in tag.name.split('.')] # category display order is important here. The following works
if key in ['authors', 'publisher', 'news', 'formats', 'rating'] or \ # only of all the non-user categories are displayed before the
key not in self.db.prefs.get('categories_using_hierarchy', []) or\ # user categories
len(components) == 1 or \ components = [t.strip() for t in original_name(tag).split('.')
fm['kind'] == 'user': if t.strip()]
if len(components) == 0 or '.'.join(components) != original_name(tag):
components = [original_name(tag)]
in_uc = fm['kind'] == 'user'
if (not tag.is_hierarchical) and (in_uc or
key in ['authors', 'publisher', 'news', 'formats', 'rating'] or
key not in self.db.prefs.get('categories_using_hierarchy', []) or
len(components) == 1):
self.beginInsertRows(category_index, 999999, 1) self.beginInsertRows(category_index, 999999, 1)
TagTreeItem(parent=node_parent, data=tag, tooltip=tt, n = TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
icon_map=self.icon_state_map) icon_map=self.icon_state_map)
if tag.id_set is not None:
n.id_set |= tag.id_set
category_child_map[tag.name, tag.category] = n
self.endInsertRows() self.endInsertRows()
tag.can_edit = key != 'formats' and (key == 'news' or \ tag.is_editable = key != 'formats' and (key == 'news' or \
self.db.field_metadata[tag.category]['datatype'] in \ self.db.field_metadata[tag.category]['datatype'] in \
['text', 'series', 'enumeration']) ['text', 'series', 'enumeration'])
else: else:
for i,comp in enumerate(components): for i,comp in enumerate(components):
child_map = dict([(t.tag.name, t) for t in node_parent.children if i == 0:
child_map = category_child_map
else:
child_map = dict([((t.tag.name, t.tag.category), t)
for t in node_parent.children
if t.type != TagTreeItem.CATEGORY]) if t.type != TagTreeItem.CATEGORY])
if comp in child_map: if (comp,tag.category) in child_map:
node_parent = child_map[comp] node_parent = child_map[(comp,tag.category)]
node_parent.tag.count += tag.count node_parent.tag.is_hierarchical = True
node_parent.tag.use_prefix = True
else: else:
if i < len(components)-1: if i < len(components)-1:
t = copy.copy(tag) t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1]) t.original_name = '.'.join(components[:i+1])
t.can_edit = False t.is_editable = False
else: else:
t = tag t = tag
if not in_uc:
t.original_name = t.name t.original_name = t.name
t.can_edit = True t.is_editable = True
t.use_prefix = True t.is_hierarchical = True
t.name = comp t.name = comp
self.beginInsertRows(category_index, 999999, 1) self.beginInsertRows(category_index, 999999, 1)
node_parent = TagTreeItem(parent=node_parent, data=t, node_parent = TagTreeItem(parent=node_parent, data=t,
tooltip=tt, icon_map=self.icon_state_map) tooltip=tt, icon_map=self.icon_state_map)
child_map[(comp,tag.category)] = node_parent
self.endInsertRows() self.endInsertRows()
# This id_set must not be None
return ((collapse_letter, collapse_letter_sk)) node_parent.id_set |= tag.id_set
return
for category in self.category_nodes: for category in self.category_nodes:
if len(category.children) > 0: if len(category.children) > 0:
@ -1151,7 +1238,11 @@ class TagsModel(QAbstractItemModel): # {{{
states = [c.tag.state for c in category.child_tags()] states = [c.tag.state for c in category.child_tags()]
names = [(c.tag.name, c.tag.category) for c in category.child_tags()] names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
state_map = dict(izip(names, states)) state_map = dict(izip(names, states))
ctags = [c for c in child_map if c.type == TagTreeItem.CATEGORY] # temporary sub-categories (the partitioning ones) must follow
# the permanent sub-categories. This will happen naturally if
# the temp ones are added by process_node
ctags = [c for c in child_map if
c.type == TagTreeItem.CATEGORY and not c.temporary]
start = len(ctags) start = len(ctags)
self.beginRemoveRows(self.createIndex(category.row(), 0, category), self.beginRemoveRows(self.createIndex(category.row(), 0, category),
start, len(child_map)-1) start, len(child_map)-1)
@ -1160,8 +1251,7 @@ class TagsModel(QAbstractItemModel): # {{{
else: else:
state_map = {} state_map = {}
collapse_letter, collapse_letter_sk = process_one_node(category, process_one_node(category, state_map)
state_map, collapse_letter, collapse_letter_sk)
return True return True
def columnCount(self, parent): def columnCount(self, parent):
@ -1180,13 +1270,19 @@ class TagsModel(QAbstractItemModel): # {{{
# working with the last item and that item is deleted, in which case # working with the last item and that item is deleted, in which case
# we position at the parent label # we position at the parent label
path = index.model().path_for_index(index) path = index.model().path_for_index(index)
val = unicode(value.toString()) val = unicode(value.toString()).strip()
if not val: if not val:
error_dialog(self.tags_view, _('Item is blank'), error_dialog(self.tags_view, _('Item is blank'),
_('An item cannot be set to nothing. Delete it instead.')).exec_() _('An item cannot be set to nothing. Delete it instead.')).exec_()
return False return False
item = index.internalPointer() item = index.internalPointer()
if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'): if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'):
if val.find('.') >= 0:
error_dialog(self.tags_view, _('Rename user category'),
_('You cannot use periods in the name when '
'renaming user categories'), show=True)
return False
user_cats = self.db.prefs.get('user_categories', {}) user_cats = self.db.prefs.get('user_categories', {})
ckey = item.category_key[1:] ckey = item.category_key[1:]
dotpos = ckey.rfind('.') dotpos = ckey.rfind('.')
@ -1199,7 +1295,7 @@ class TagsModel(QAbstractItemModel): # {{{
if len(c) == len(ckey): if len(c) == len(ckey):
if nkey in user_cats: if nkey in user_cats:
error_dialog(self.tags_view, _('Rename user category'), error_dialog(self.tags_view, _('Rename user category'),
_('The name %s is already used'%nkey), show=True) _('The name %s is already used')%nkey, show=True)
return False return False
user_cats[nkey] = user_cats[ckey] user_cats[nkey] = user_cats[ckey]
del user_cats[ckey] del user_cats[ckey]
@ -1219,7 +1315,7 @@ class TagsModel(QAbstractItemModel): # {{{
return True return True
key = item.tag.category key = item.tag.category
name = getattr(item.tag, 'original_name', item.tag.name) name = original_name(item.tag)
# make certain we know about the item's category # make certain we know about the item's category
if key not in self.db.field_metadata: if key not in self.db.field_metadata:
return False return False
@ -1306,7 +1402,7 @@ class TagsModel(QAbstractItemModel): # {{{
if index.isValid(): if index.isValid():
node = self.data(index, Qt.UserRole) node = self.data(index, Qt.UserRole)
if node.type == TagTreeItem.TAG: if node.type == TagTreeItem.TAG:
if getattr(node.tag, 'can_edit', True): if node.tag.is_editable:
ans |= Qt.ItemIsDragEnabled ans |= Qt.ItemIsDragEnabled
fm = self.db.metadata_for_field(node.tag.category) fm = self.db.metadata_for_field(node.tag.category)
if node.tag.category in \ if node.tag.category in \
@ -1438,8 +1534,8 @@ class TagsModel(QAbstractItemModel): # {{{
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name))) ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else: else:
name = getattr(tag, 'original_name', tag.name) name = original_name(tag)
use_prefix = getattr(tag, 'use_prefix', False) use_prefix = tag.is_hierarchical
if category == 'tags': if category == 'tags':
if name in tags_seen: if name in tags_seen:
continue continue
@ -1477,7 +1573,7 @@ class TagsModel(QAbstractItemModel): # {{{
tag = tag_item.tag tag = tag_item.tag
if tag is None: if tag is None:
return False return False
name = getattr(tag, 'original_name', tag.name) name = original_name(tag)
if (equals_match and strcmp(name, txt) == 0) or \ if (equals_match and strcmp(name, txt) == 0) or \
(not equals_match and lower(name).find(txt) >= 0): (not equals_match and lower(name).find(txt) >= 0):
self.path_found = path self.path_found = path
@ -1559,11 +1655,16 @@ class TagsModel(QAbstractItemModel): # {{{
if tag_item.boxed: if tag_item.boxed:
tag_item.boxed = False tag_item.boxed = False
self.dataChanged.emit(tag_index, tag_index) self.dataChanged.emit(tag_index, tag_index)
for i,c in enumerate(tag_item.children):
process_tag(self.index(i, 0, tag_index), c)
def process_level(category_index): def process_level(category_index):
for j in xrange(self.rowCount(category_index)): for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index) tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer() tag_item = tag_index.internalPointer()
if tag_item.boxed:
tag_item.boxed = False
self.dataChanged.emit(tag_index, tag_index)
if tag_item.type == TagTreeItem.CATEGORY: if tag_item.type == TagTreeItem.CATEGORY:
process_level(tag_index) process_level(tag_index)
else: else:
@ -1703,8 +1804,9 @@ class TagBrowserMixin(object): # {{{
db = self.library_view.model().db db = self.library_view.model().db
user_cats = db.prefs.get('user_categories', {}) user_cats = db.prefs.get('user_categories', {})
if dest_category.startswith('@'): if dest_category and dest_category.startswith('@'):
dest_category = dest_category[1:] dest_category = dest_category[1:]
if dest_category not in user_cats: if dest_category not in user_cats:
return error_dialog(self.tags_view, _('Add to user category'), return error_dialog(self.tags_view, _('Add to user category'),
_('A user category %s does not exist')%dest_category, show=True) _('A user category %s does not exist')%dest_category, show=True)
@ -1810,10 +1912,7 @@ class TagBrowserMixin(object): # {{{
self.library_view.model().refresh() self.library_view.model().refresh()
self.tags_view.recount() self.tags_view.recount()
def drag_drop_finished(self, ids, is_category): def drag_drop_finished(self, ids):
if is_category:
self.tags_view.recount()
else:
self.library_view.model().refresh_ids(ids) self.library_view.model().refresh_ids(ids)
# }}} # }}}

View File

@ -46,11 +46,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object): class Tag(object):
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None, def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
tooltip=None, icon=None, category=None): tooltip=None, icon=None, category=None, id_set=None):
self.name = name self.name = name
self.id = id self.id = id
self.count = count self.count = count
self.state = state self.state = state
self.is_hierarchical = False
self.is_editable = True
self.id_set = id_set
self.avg_rating = avg/2.0 if avg is not None else 0 self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort self.sort = sort
if self.avg_rating > 0: if self.avg_rating > 0:
@ -1160,6 +1163,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.n = name self.n = name
self.s = sort self.s = sort
self.c = 0 self.c = 0
self.id_set = set()
self.rt = 0 self.rt = 0
self.rc = 0 self.rc = 0
self.id = None self.id = None
@ -1177,6 +1181,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\ return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
(self.n, self.s, self.c, self.rt, self.rc, self.id) (self.n, self.s, self.c, self.rt, self.rc, self.id)
def clean_user_categories(self):
user_cats = self.prefs.get('user_categories', {})
new_cats = {}
for k in user_cats:
comps = [c.strip() for c in k.split('.') if c.strip()]
if len(comps) == 0:
i = 1
while True:
if unicode(i) not in user_cats:
new_cats[unicode(i)] = user_cats[k]
break
i += 1
else:
new_cats['.'.join(comps)] = user_cats[k]
self.prefs.set('user_categories', new_cats)
return new_cats
def get_categories(self, sort='name', ids=None, icon_map=None): def get_categories(self, sort='name', ids=None, icon_map=None):
#start = last = time.clock() #start = last = time.clock()
@ -1264,6 +1284,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
item = tag_class(val, sort_val) item = tag_class(val, sort_val)
tcategories[cat][val] = item tcategories[cat][val] = item
item.c += 1 item.c += 1
item.id_set.add(book[0])
item.id = item_id item.id = item_id
if rating > 0: if rating > 0:
item.rt += rating item.rt += rating
@ -1281,6 +1302,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
item = tag_class(val, sort_val) item = tag_class(val, sort_val)
tcategories[cat][val] = item tcategories[cat][val] = item
item.c += 1 item.c += 1
item.id_set.add(book[0])
item.id = item_id item.id = item_id
if rating > 0: if rating > 0:
item.rt += rating item.rt += rating
@ -1368,7 +1390,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id, categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
avg=avgr(r), sort=r.s, icon=icon, avg=avgr(r), sort=r.s, icon=icon,
tooltip=tooltip, category=category) tooltip=tooltip, category=category,
id_set=r.id_set)
for r in items] for r in items]
#print 'end phase "tags list":', time.clock() - last, 'seconds' #print 'end phase "tags list":', time.clock() - last, 'seconds'
@ -1377,6 +1400,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# Needed for legacy databases that have multiple ratings that # Needed for legacy databases that have multiple ratings that
# map to n stars # map to n stars
for r in categories['rating']: for r in categories['rating']:
r.id_set = None
for x in categories['rating']: for x in categories['rating']:
if r.name == x.name and r.id != x.id: if r.name == x.name and r.id != x.id:
r.count = r.count + x.count r.count = r.count + x.count
@ -1413,7 +1437,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories['formats'].sort(key = lambda x:x.name) categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. #### #### Now do the user-defined categories. ####
user_categories = dict.copy(self.prefs['user_categories']) user_categories = dict.copy(self.clean_user_categories())
# We want to use same node in the user category as in the source # We want to use same node in the user category as in the source
# category. To do that, we need to find the original Tag node. There is # category. To do that, we need to find the original Tag node. There is

View File

@ -342,6 +342,7 @@ class BrowseServer(object):
return category_meta[x]['name'].lower() return category_meta[x]['name'].lower()
displayed_custom_fields = custom_fields_to_display(self.db) displayed_custom_fields = custom_fields_to_display(self.db)
uc_displayed = set()
for category in sorted(categories, key=lambda x: sort_key(getter(x))): for category in sorted(categories, key=lambda x: sort_key(getter(x))):
if len(categories[category]) == 0: if len(categories[category]) == 0:
continue continue
@ -361,6 +362,18 @@ class BrowseServer(object):
icon = category_icon_map['user:'] icon = category_icon_map['user:']
else: else:
icon = 'blank.png' icon = 'blank.png'
if meta['kind'] == 'user':
dot = category.find('.')
if dot > 0:
cat = category[:dot]
if cat not in uc_displayed:
cats.append((meta['name'][:dot-1], cat, icon))
uc_displayed.add(cat)
else:
cats.append((meta['name'], category, icon))
uc_displayed.add(category)
else:
cats.append((meta['name'], category, icon)) cats.append((meta['name'], category, icon))
cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}">&nbsp;</a>' cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}">&nbsp;</a>'
@ -394,13 +407,59 @@ class BrowseServer(object):
category_name = category_meta[category]['name'] category_name = category_meta[category]['name']
datatype = category_meta[category]['datatype'] datatype = category_meta[category]['datatype']
# See if we have any sub-categories to display. As we find them, add
# them to the displayed set to avoid showing the same item twice
uc_displayed = set()
cats = []
for ucat in sorted(categories.keys(), key=sort_key):
if len(categories[ucat]) == 0:
continue
if category == 'formats':
continue
meta = category_meta.get(ucat, None)
if meta is None:
continue
if meta['kind'] != 'user':
continue
cat_len = len(category)
if not (len(ucat) > cat_len and ucat.startswith(category+'.')):
continue
icon = category_icon_map['user:']
# we have a subcategory. Find any further dots (further subcats)
cat_len += 1
cat = ucat[cat_len:]
dot = cat.find('.')
if dot > 0:
# More subcats
cat = cat[:dot]
if cat not in uc_displayed:
cats.append((cat, ucat[:cat_len+dot], icon))
uc_displayed.add(cat)
else:
# This is the end of the chain
cats.append((cat, ucat, icon))
uc_displayed.add(cat)
cats = u'\n\n'.join(
[(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}">&nbsp;</a>'
u'<img src="{3}{src}" alt="{0}" />'
u'<span class="label">{0}</span>'
u'</li>')
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
self.opts.url_prefix, src='/browse/icon/'+z)
for x, y, z in cats])
if cats:
cats = (u'\n<div class="toplevel">\n'
'{0}</div>').format(cats)
script = 'toplevel();'
else:
script = 'true'
# Now do the category items
items = categories[category] items = categories[category]
sort = self.browse_sort_categories(items, sort) sort = self.browse_sort_categories(items, sort)
script = 'true' if not cats and len(items) == 1:
if len(items) == 1:
# Only one item in category, go directly to book list # Only one item in category, go directly to book list
prefix = '' if self.is_wsgi else self.opts.url_prefix prefix = '' if self.is_wsgi else self.opts.url_prefix
html = get_category_items(category, items, html = get_category_items(category, items,
@ -443,6 +502,9 @@ class BrowseServer(object):
if cats:
script = 'toplevel();category(%s);'%script
else:
script = 'category(%s);'%script script = 'category(%s);'%script
main = u''' main = u'''
@ -453,7 +515,7 @@ class BrowseServer(object):
{1} {1}
</div> </div>
'''.format( '''.format(
xml(_('Browsing by')+': ' + category_name), items, xml(_('Browsing by')+': ' + category_name), cats + items,
xml(_('Up'), True), self.opts.url_prefix) xml(_('Up'), True), self.opts.url_prefix)
return self.browse_template(sort).format(title=category_name, return self.browse_template(sort).format(title=category_name,

View File

@ -198,7 +198,7 @@ def cli_docs(app):
documented_cmds = [] documented_cmds = []
undocumented_cmds = [] undocumented_cmds = []
for script in entry_points['console_scripts']: for script in entry_points['console_scripts'] + entry_points['gui_scripts']:
module = script[script.index('=')+1:script.index(':')].strip() module = script[script.index('=')+1:script.index(':')].strip()
cmd = script[:script.index('=')].strip() cmd = script[:script.index('=')].strip()
if cmd in ('calibre-complete', 'calibre-parallel'): continue if cmd in ('calibre-complete', 'calibre-parallel'): continue

View File

@ -27,6 +27,7 @@ Actions
.. image:: images/actions.png .. image:: images/actions.png
:alt: The Actions Toolbar :alt: The Actions Toolbar
:align: center
The actions toolbar provides convenient shortcuts to commonly used actions. Most of the action buttons have little arrows next to them. By clicking the arrows, you can perform variations on the default action. Please note that the actions toolbar will look slightly different depending on whether you have an ebook reader attached to your computer. The actions toolbar provides convenient shortcuts to commonly used actions. Most of the action buttons have little arrows next to them. By clicking the arrows, you can perform variations on the default action. Please note that the actions toolbar will look slightly different depending on whether you have an ebook reader attached to your computer.
@ -39,6 +40,7 @@ The actions toolbar provides convenient shortcuts to commonly used actions. Most
Add books Add books
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
.. |adbi| image:: images/add_books.png .. |adbi| image:: images/add_books.png
:class: float-right-img
|adbi| The :guilabel:`Add books` action has five variations, accessed by the clicking the down arrow on the right side of the button. |adbi| The :guilabel:`Add books` action has five variations, accessed by the clicking the down arrow on the right side of the button.
@ -63,6 +65,7 @@ To add an additional format for an existing book, use the :ref:`edit_meta_inform
Edit metadata Edit metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. |emii| image:: images/edit_meta_information.png .. |emii| image:: images/edit_meta_information.png
:class: float-right-img
|emii| The :guilabel:`Edit metadata` action has six variations, which can be accessed by clicking the down arrow on the right side of the button. |emii| The :guilabel:`Edit metadata` action has six variations, which can be accessed by clicking the down arrow on the right side of the button.
@ -80,6 +83,7 @@ Edit metadata
Convert e-books Convert e-books
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
.. |cei| image:: images/convert_ebooks.png .. |cei| image:: images/convert_ebooks.png
:class: float-right-img
|cei| Ebooks can be converted from a number of formats into whatever format your e-book reader prefers. |cei| Ebooks can be converted from a number of formats into whatever format your e-book reader prefers.
Note that ebooks you purchase will typically have `Digital Rights Management <http://bugs.calibre-ebook.com/wiki/DRM>`_ *(DRM)*. Note that ebooks you purchase will typically have `Digital Rights Management <http://bugs.calibre-ebook.com/wiki/DRM>`_ *(DRM)*.
@ -106,6 +110,7 @@ The :guilabel:`Convert E-books` action has three variations, accessed by the arr
View View
~~~~~~~~~~~ ~~~~~~~~~~~
.. |vi| image:: images/view.png .. |vi| image:: images/view.png
:class: float-right-img
|vi| The :guilabel:`View` action displays the book in an ebook viewer program. |app| has a builtin viewer for the most e-book formats. |vi| The :guilabel:`View` action displays the book in an ebook viewer program. |app| has a builtin viewer for the most e-book formats.
For other formats it uses the default operating system application. You can configure which formats should open with the internal viewer via For other formats it uses the default operating system application. You can configure which formats should open with the internal viewer via
@ -118,6 +123,7 @@ on the right of the :guilabel:`View` button.
Send to device Send to device
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
.. |stdi| image:: images/send_to_device.png .. |stdi| image:: images/send_to_device.png
:class: float-right-img
|stdi| The :guilabel:`Send to device` action has eight variations, accessed by clicking the down arrow on the right of the button. |stdi| The :guilabel:`Send to device` action has eight variations, accessed by clicking the down arrow on the right of the button.
@ -138,6 +144,7 @@ You can control the file name and folder structure of files sent to the device b
Fetch news Fetch news
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. |fni| image:: images/fetch_news.png .. |fni| image:: images/fetch_news.png
:class: float-right-img
|fni| The :guilabel:`Fetch news` action downloads news from various websites and converts it into an ebook that can be read on your ebook reader. Normally, the newly created ebook is added to your ebook library, but if an ebook reader is connected at the time the download finishes, the news is also uploaded to the reader automatically. |fni| The :guilabel:`Fetch news` action downloads news from various websites and converts it into an ebook that can be read on your ebook reader. Normally, the newly created ebook is added to your ebook library, but if an ebook reader is connected at the time the download finishes, the news is also uploaded to the reader automatically.
@ -155,6 +162,7 @@ The :guilabel:`Fetch news` action has three variations, accessed by clicking the
Library Library
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. |lii| image:: images/library.png .. |lii| image:: images/library.png
:class: float-right-img
|lii| The :guilabel: `Library` action allows you to create, switch between, rename or delete a Library. |app| allows you to create as many libraries as you wish. You coudl for instance create a fiction library, a non fiction library, a foreign language library a project library, basically any structure that suits your needs. Libraries are the highest organizational structure within |app|, each library has its own set of books, tags, categories and base storage location. |lii| The :guilabel: `Library` action allows you to create, switch between, rename or delete a Library. |app| allows you to create as many libraries as you wish. You coudl for instance create a fiction library, a non fiction library, a foreign language library a project library, basically any structure that suits your needs. Libraries are the highest organizational structure within |app|, each library has its own set of books, tags, categories and base storage location.
@ -169,6 +177,7 @@ Library
Device Device
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. |dvi| image:: images/device.png .. |dvi| image:: images/device.png
:class: float-right-img
|dvi| The :guilabel:`Device` action allows you to view the books in the main memory or storage cards of your device, or to eject the device (detach it from |app|). |dvi| The :guilabel:`Device` action allows you to view the books in the main memory or storage cards of your device, or to eject the device (detach it from |app|).
This icon shows up automatically on the main |app| toolbar when you connect a supported device. You can click on it to see the books on your device. You can also drag and drop books from your |app| library onto the icon to transfer them to your device. Conversely, you can drag and drop books from your device onto the |app| icon on the toolbar to transfer books from your device to the |app| library. This icon shows up automatically on the main |app| toolbar when you connect a supported device. You can click on it to see the books on your device. You can also drag and drop books from your |app| library onto the icon to transfer them to your device. Conversely, you can drag and drop books from your device onto the |app| icon on the toolbar to transfer books from your device to the |app| library.
@ -179,6 +188,7 @@ This icon shows up automatically on the main |app| toolbar when you connect a su
Save to disk Save to disk
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
.. |svdi| image:: images/save_to_disk.png .. |svdi| image:: images/save_to_disk.png
:class: float-right-img
|svdi| The :guilabel:`Save to disk` action has five variations, accessed by the arrow next to the button. |svdi| The :guilabel:`Save to disk` action has five variations, accessed by the arrow next to the button.
@ -212,6 +222,7 @@ Save to disk
Connect/Share Connect/Share
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. |csi| image:: images/connect_share.png .. |csi| image:: images/connect_share.png
:class: float-right-img
|csi| The :guilabel:`Connect/Share` action allows you to manually connect to a device or folder on your computer, it also allows you to set up you |app| library for access via a web browser, or email. |csi| The :guilabel:`Connect/Share` action allows you to manually connect to a device or folder on your computer, it also allows you to set up you |app| library for access via a web browser, or email.
@ -230,6 +241,7 @@ Connect/Share
Remove books Remove books
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
.. |rbi| image:: images/remove_books.png .. |rbi| image:: images/remove_books.png
:class: float-right-img
|rbi| The :guilabel:`Remove books` action **deletes books permanently**, so use it with care. It is *context sensitive*, i.e. it depends on which :ref:`catalog <catalogs>` you have selected. If you have selected the :guilabel:`Library`, books will be removed from the library. If you have selected the ebook reader device, the books will be removed from the device. To remove only a particular format for a given book use the :ref:`edit_meta_information` action. Remove books also has five variations which can be accessed by clicking the down arrow on the right side of the button. |rbi| The :guilabel:`Remove books` action **deletes books permanently**, so use it with care. It is *context sensitive*, i.e. it depends on which :ref:`catalog <catalogs>` you have selected. If you have selected the :guilabel:`Library`, books will be removed from the library. If you have selected the ebook reader device, the books will be removed from the device. To remove only a particular format for a given book use the :ref:`edit_meta_information` action. Remove books also has five variations which can be accessed by clicking the down arrow on the right side of the button.
@ -259,6 +271,7 @@ The Preferences Action allows you to change the way various aspects of |app| wor
Catalogs Catalogs
---------- ----------
.. image:: images/catalogs.png .. image:: images/catalogs.png
:align: center
A *catalog* is a collection of books. |app| can manage two types of different catalogs: A *catalog* is a collection of books. |app| can manage two types of different catalogs:
@ -274,6 +287,7 @@ Many operations, like Adding books, deleting, viewing, etc. are context sensitiv
Search & Sort Search & Sort
--------------- ---------------
.. image:: images/search_sort.png .. image:: images/search_sort.png
:align: center
The Search & Sort section allows you to perform several powerful actions on your book collections. The Search & Sort section allows you to perform several powerful actions on your book collections.
@ -375,6 +389,7 @@ Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the col
:align: middle :align: middle
.. figure:: images/search.png .. figure:: images/search.png
:align: center
:guilabel:`Advanced Search Dialog` :guilabel:`Advanced Search Dialog`
@ -408,6 +423,7 @@ will be interpreted to have the title: Foundation and Earth and author: Isaac As
Book Details Book Details
------------- -------------
.. image:: images/book_details.png .. image:: images/book_details.png
:align: center
The Book Details display shows you extra information and the cover for the currently selected book. The Book Details display shows you extra information and the cover for the currently selected book.
@ -418,11 +434,14 @@ The Book Details display shows you extra information and the cover for the curre
Tag Browser Tag Browser
------------- -------------
.. image:: images/tag_browser.png .. image:: images/tag_browser.png
:class: float-left-img
The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any Item in the Tag Browser, for example, the Author name, Isaac Asimov, then the list of books to the right is restricted to books by that author. Clicking once again on Isaac Asimov will restrict the list of books to books not by Isaac Asimov. A third click will remove the restriction. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could Hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. It is a good way to learn how to construct basic search expressions. The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any Item in the Tag Browser, for example, the Author name, Isaac Asimov, then the list of books to the right is restricted to books by that author. Clicking once again on Isaac Asimov will restrict the list of books to books not by Isaac Asimov. A third click will remove the restriction. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could Hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. It is a good way to learn how to construct basic search expressions.
There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose to hide it or rename it or open a "Manage x" dialog that allows you to manage items of that kind. For example the "Manage Authors" dialog allows you to rename authors and control how their names are sorted. There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose to hide it or rename it or open a "Manage x" dialog that allows you to manage items of that kind. For example the "Manage Authors" dialog allows you to rename authors and control how their names are sorted.
Items in the Tag browser have their icons partially colored. The amount of color depends on the average rating of the books in that category. So for example if the books by Isaac Asimov have an average of four stars, the icon for Isaac Asimov in the Tag Browser will be 4/5th colored. You can hover your mouse over the icon to see the average rating.
For convenience, you can drag and drop books from the book list to items in the Tag Browser and that item will be automatically applied to the dropped books. For example, dragging a book to Isaac Asimov will set the author of that book to Isaac Asimov or dragging it to the tag History will add the tag History to its tags. For convenience, you can drag and drop books from the book list to items in the Tag Browser and that item will be automatically applied to the dropped books. For example, dragging a book to Isaac Asimov will set the author of that book to Isaac Asimov or dragging it to the tag History will add the tag History to its tags.
The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the user categories editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories act like built-in categories; you can click on items to search for them. You can search for all items in a category by right-clicking on the category name and choosing "Search for books in ...". The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the user categories editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories act like built-in categories; you can click on items to search for them. You can search for all items in a category by right-clicking on the category name and choosing "Search for books in ...".
@ -433,10 +452,13 @@ It is also possible to create hierarchies inside some of the built-in categories
You can drag and drop items in the Tag browser onto user categories to add them to that category. You can drag and drop items in the Tag browser onto user categories to add them to that category.
You can control how items are sorted in the Tag browser via the box at the bottom of the Tag Browser. You can choose to sort by name, average rating or popularity (popularity is the number of books with an item in your library; for example; the popularity of Isaac Asimov is the number of book sin your library by Isaac Asimov).
Jobs Jobs
----- -----
.. image:: images/jobs.png .. image:: images/jobs.png
:class: float-left-img
The Jobs panel shows you the number of currently running jobs. Jobs are tasks that run in a separate process, they include converting ebooks and talking to your reader device. You can click on the jobs panel to access the list of jobs. Once a job has completed, by double-clicking it in the list, you can see a detailed log from that job. This is useful to debug jobs that may not have completed successfully. The Jobs panel shows you the number of currently running jobs. Jobs are tasks that run in a separate process, they include converting ebooks and talking to your reader device. You can click on the jobs panel to access the list of jobs. Once a job has completed, by double-clicking it in the list, you can see a detailed log from that job. This is useful to debug jobs that may not have completed successfully.

View File

@ -33,6 +33,7 @@ Lets pick a couple of feeds that look interesting:
I got the URLs by clicking the little orange RSS icon next to each feed name. To make |app| download the feeds and convert them into an e-book, you should click the :guilabel:`Fetch news` button and then the :guilabel:`Add a custom news source` menu item. A dialog similar to that shown below should open up. I got the URLs by clicking the little orange RSS icon next to each feed name. To make |app| download the feeds and convert them into an e-book, you should click the :guilabel:`Fetch news` button and then the :guilabel:`Add a custom news source` menu item. A dialog similar to that shown below should open up.
.. image:: images/custom_news.png .. image:: images/custom_news.png
:align: center
First enter ``Portfolio`` into the :guilabel:`Recipe title` field. This will be the title of the e-book that will be created from the articles in the above feeds. First enter ``Portfolio`` into the :guilabel:`Recipe title` field. This will be the title of the e-book that will be created from the articles in the above feeds.
@ -81,6 +82,7 @@ So it looks like to get the print version, we need to prefix every article URL w
Now in the :guilabel:`Advanced Mode` of the Custom news sources dialog, you should see something like (remember to select *The BBC* recipe before switching to advanced mode): Now in the :guilabel:`Advanced Mode` of the Custom news sources dialog, you should see something like (remember to select *The BBC* recipe before switching to advanced mode):
.. image:: images/bbc_advanced.png .. image:: images/bbc_advanced.png
:align: center
You can see that the fields from the :guilabel:`Basic mode` have been translated to python code in a straightforward manner. We need to add instructions to this recipe to use the print version of the articles. All that's needed is to add the following two lines: You can see that the fields from the :guilabel:`Basic mode` have been translated to python code in a straightforward manner. We need to add instructions to this recipe to use the print version of the articles. All that's needed is to add the following two lines:
@ -92,6 +94,7 @@ You can see that the fields from the :guilabel:`Basic mode` have been translated
This is python, so indentation is important. After you've added the lines, it should look like: This is python, so indentation is important. After you've added the lines, it should look like:
.. image:: images/bbc_altered.png .. image:: images/bbc_altered.png
:align: center
In the above, ``def print_version(self, url)`` defines a *method* that is called by |app| for every article. ``url`` is the URL of the original article. What ``print_version`` does is take that url and replace it with the new URL that points to the print version of the article. To learn about `python <http://www.python.org>`_ see the `tutorial <http://docs.python.org/tut/>`_. In the above, ``def print_version(self, url)`` defines a *method* that is called by |app| for every article. ``url`` is the URL of the original article. What ``print_version`` does is take that url and replace it with the new URL that points to the print version of the article. To learn about `python <http://www.python.org>`_ see the `tutorial <http://docs.python.org/tut/>`_.
@ -109,6 +112,7 @@ The recipe now looks like:
.. _bbc1: .. _bbc1:
.. image:: images/bbc_altered1.png .. image:: images/bbc_altered1.png
:align: center
The new version looks pretty good. If you're a perfectionist, you'll want to read the next section, which deals with actually modifying the downloaded content. The new version looks pretty good. If you're a perfectionist, you'll want to read the next section, which deals with actually modifying the downloaded content.

View File

@ -1,4 +1,59 @@
{% extends "!layout.html" %} {% extends "!layout.html" %}
{% block extrahead %}
{% if not embedded %}
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-20736318-1']);
_gaq.push(['_setDomainName', '.calibre-ebook.com']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
{% endif %}
<style type="text/css">
.float-left-img { float: left; margin-right: 1em; margin-bottom: 1em }
.float-right-img { float: right; margin-left: 1em; margin-bottom: 1em }
</style>
{% endblock %}
{% block document %}
<div class="documentwrapper">
{%- if render_sidebar %}
<div class="bodywrapper">
{%- endif %}
<div class="body">
{% if not embedded %}
<div id="ad-container" style="text-align:center">
<script type="text/javascript"><!--
google_ad_client = "ca-pub-2595272032872519";
/* User Manual horizontal */
google_ad_slot = "3131592704";
google_ad_width = 728;
google_ad_height = 90;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>
{% endif %}
{% block body %} {% endblock %}
</div>
{%- if render_sidebar %}
</div>
{%- endif %}
</div>
{% endblock %}
{% block sidebarlogo %} {% block sidebarlogo %}
<p class="logo"> <p class="logo">
<a href="http://calibre-ebook.com"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></a> <a href="http://calibre-ebook.com"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></a>
@ -12,3 +67,4 @@
<hr/> <hr/>
{% endblock %} {% endblock %}

View File

@ -66,6 +66,7 @@ reference mode. You can turn it on by clicking the Reference Mode button |refmi|
mouse over a paragraph, calibre will display a unique number made up of the section and paragraph numbers. mouse over a paragraph, calibre will display a unique number made up of the section and paragraph numbers.
.. image:: images/ref_mode.png .. image:: images/ref_mode.png
:align: center
You can use this number to unambiguously refer to parts of the books when discussing it with friends or referring to it You can use this number to unambiguously refer to parts of the books when discussing it with friends or referring to it
in other works. You can enter these numbers in the box marked Go to at the top of the window to go to a particular in other works. You can enter these numbers in the box marked Go to at the top of the window to go to a particular

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff