This commit is contained in:
GRiker 2012-07-06 11:19:35 -06:00
commit e5151089fe
121 changed files with 16902 additions and 14527 deletions

View File

@ -19,6 +19,57 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.8.59
date: 2012-07-06
new features:
- title: "Drivers for Samsung SGH-T989 and Sony Ericsson Sola"
tickets: [1021365]
- title: "Conversion pipeline: When removing the first image, also remove the html file the image is found in, if that file has no other content. Allows this option to be used to remove covers from EPUB files without leaving behind a blank page."
- title: "Content server: Add a navigation panel at the bottom of each page."
tickets: [1020225]
- title: "calibredb: Add a backup_metadata command to manually run the backup to opf from the command line"
- title: "User defined driver: Add option to swap main memory and card a."
tickets: [1020056]
- title: "Add new option to the series_index_auto_increment tweak, no_change, that causes calibre not to change the series_index when the series is changed"
bug fixes:
- title: "PDF Output: Resize large images so that they do not get off at the right edge of the page."
- title: "On linux ensure that WM_CLASS for the main calibre GUI is set to 'calibre-gui' to match the name of the calibre-gui.desktop file. This is apparently required by the GNOME 3 shell."
tickets: [1020297]
- title: "Update ICU in all builds to version 49.1"
- title: "Tag browser: Fix regression that broke drag and drop between user categories in the tag browser"
- title: "When copying to library and deleting after copy, do not place deleted files in recycle bin, as this is redundant and slow (they have already been copied into another library)"
- title: "Fix yes/no fields with value of No not showing up in the book details panel"
- title: "Catalogs: Better sorting for non English languages"
tickets: [930882]
- title: "Get Books: Fix Foyles UK, Weightless books, ebooks.com and ozon.ru"
- title: "CHM Input: Fix handling of chm files that split their html into multiple sub-directories."
tickets: [1018792]
improved recipes:
- FHM UK
- The Age
- weblogs_ssl
- Heraldo.es
new recipes:
- title: CATO Institute and Heritage Foundation
author: _reader
- version: 0.8.58 - version: 0.8.58
date: 2012-06-29 date: 2012-06-29

View File

@ -15,7 +15,7 @@ Here, we will teach you how to create your own plugins to add new features to |a
:depth: 2 :depth: 2
:local: :local:
.. note:: This only applies to calibre releases >= 0.7.53 .. note:: This only applies to calibre releases >= 0.8.60
Anatomy of a |app| plugin Anatomy of a |app| plugin
--------------------------- ---------------------------
@ -32,11 +32,15 @@ and enter the following Python code into it:
.. literalinclude:: plugin_examples/helloworld/__init__.py .. literalinclude:: plugin_examples/helloworld/__init__.py
:lines: 10- :lines: 10-
That's all. To add this code to |app| as a plugin, simply create a zip file with:: That's all. To add this code to |app| as a plugin, simply run the following in
the directory in which you created :file:`__init__.py`::
zip plugin.zip __init__.py calibre-customize -b .
Add this plugin to |app| via :guilabel:`Preferences->Plugins`. .. note::
On OS X you have to first install the |app| command line tools, by
going to :guilabel:`Preferences->Miscellaneous` and clicking the
:guilabel:`Install command line tools` button.
You can download the Hello World plugin from You can download the Hello World plugin from
`helloworld_plugin.zip <http://calibre-ebook.com/downloads/helloworld_plugin.zip>`_. `helloworld_plugin.zip <http://calibre-ebook.com/downloads/helloworld_plugin.zip>`_.
@ -191,14 +195,12 @@ When running from the command line, debug output will be printed to the console,
You can insert print statements anywhere in your plugin code, they will be output in debug mode. Remember, this is python, you really shouldn't need anything more than print statements to debug ;) I developed all of |app| using just this debugging technique. You can insert print statements anywhere in your plugin code, they will be output in debug mode. Remember, this is python, you really shouldn't need anything more than print statements to debug ;) I developed all of |app| using just this debugging technique.
It can get tiresome to keep re-adding a plugin to calibre to test small changes. The plugin zip files are stored in the calibre config directory in plugins/ (goto Preferences->Misc and click open config directory to see the config directory). You can quickly test changes to your plugin by using the following command
line::
Once you've located the zip file of your plugin you can then directly update it with your changes instead of re-adding it each time. To do so from the command line, in the directory that contains your plugin source code, use:: calibre -s; calibre-customize -b /path/to/your/plugin/directory; calibre
calibre -s; zip -r /path/to/plugin/zip/file.zip *; calibre This will shutdown a running calibre, wait for the shutdown to complete, then update your plugin in |app| and relaunch |app|.
This will shutdown a running calibre. Wait for the shutdown to complete, then update your plugin files and relaunch calibre.
It relies on the freely available zip command line tool.
More plugin examples More plugin examples
---------------------- ----------------------

75
recipes/cato.recipe Normal file
View File

@ -0,0 +1,75 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class CATOInstitute(BasicNewsRecipe):
title = u'The CATO Institute'
description = "The Cato Institute is a public policy research organization — a think tank — \
dedicated to the principles of individual liberty, limited government, free markets and peace.\
Its scholars and analysts conduct independent, nonpartisan research on a wide range of policy issues."
__author__ = '_reader'
__date__ = '05 July 2012'
__version__ = '1.0'
cover_url = 'http://www.cato.org/images/logo.jpg'
masthead_url = 'http://www.cato.org/images/logo.jpg'
language = 'en'
oldest_article = 30 #days
max_articles_per_feed = 100
needs_subscription = False
publisher = 'CATO Institute'
category = 'commentary'
tags = 'commentary'
publication_type = 'blog'
no_stylesheets = True
use_embedded_content = False
encoding = None
simultaneous_downloads = 10
recursions = 0
remove_javascript = True
remove_empty_feeds = True
auto_cleanup = True
conversion_options = {
'comments' : description,
'tags' : tags,
'language' : language,
'publisher' : publisher,
'authors' : publisher,
'smarten_punctuation' : True
}
feeds = [
(u'Cato Recent Op-Eds', u'http://feeds.cato.org/CatoRecentOpeds'),
(u'Cato Homepage Headlines', u'http://feeds.cato.org/CatoHomepageHeadlines'),
(u'Cato Media Updates', u'http://feeds.cato.org/CatoMediaUpdates'),
(u'Cato@Liberty', u'http://feeds.cato.org/Cato-at-liberty'),
(u'Cato Unbound', u'http://feeds.feedburner.com/cato-unbound'),
(u'Education and Child Policy', u'http://www.cato.org/rss/ra.xml?name=education-child-policy'),
(u'Finance, Banking & Monetary Policy', u'http://www.cato.org/rss/ra.xml?name=finance-banking-monetary-policy'),
(u'Government and Politics', u'http://www.cato.org/rss/ra.xml?name=government-politics'),
(u'International Economics & Development', u'http://www.cato.org/rss/ra.xml?name=international-economics-development'),
(u'Political Philosophy', u'http://www.cato.org/rss/ra.xml?name=political-philosophy'),
(u'Social Security', u'http://www.cato.org/rss/ra.xml?name=social-security'),
(u'Telecom, Internet & Information Policy', u'http://www.cato.org/rss/ra.xml?name=telecom-internet-information-policy'),
(u'Energy and Environment', u'http://www.cato.org/rss/ra.xml?name=energy-environment'),
(u'Foreign Policy and National Security', u'http://www.cato.org/rss/ra.xml?name=foreign-policy-national-security'),
(u'Health Care', u'http://www.cato.org/rss/ra.xml?name=health-care'),
(u'Law and Civil Liberties', u'http://www.cato.org/rss/ra.xml?name=law-civil-liberties'),
(u'Regulatory Studies', u'http://www.cato.org/rss/ra.xml?name=regulatory-studies'),
(u'Tax and Budget Policy', u'http://www.cato.org/rss/ra.xml?name=tax-budget-policy'),
(u'Trade and Immigration', u'http://www.cato.org/rss/ra.xml?name=trade-immigration')
]
def print_version(self,url):
R_unbound = re.compile(r'(^.*cato-unbound.*)(\/\?utm_source.*$)' , re.DOTALL | re.IGNORECASE ) #CATO Unbound
R_pubs = re.compile(r'(^.*\/publications\/.*$)' , re.DOTALL | re.IGNORECASE ) #CATO Publications
if re.match(R_unbound, url):
printURL = r'\g<1>' + '/print/'
elif re.match(R_pubs, url):
printURL = url + '?print'
else:
printURL = url + '/print/'
return printURL

View File

@ -0,0 +1,71 @@
from calibre.web.feeds.news import BasicNewsRecipe
class HeritageFoundation(BasicNewsRecipe):
title = u'The Heritage Foundation'
description = 'Founded in 1973, The Heritage Foundation is a research and educational institution—a think tank—\
whose mission is to formulate and promote conservative public policies based on the principles of free enterprise, limited government, \
individual freedom, traditional American values, and a strong national defense.'
__author__ = '_reader'
__date__ = '05 July 2012'
__version__ = '1.0'
oldest_article = 30
max_articles_per_feed = 100
publisher = 'The Heritage Foundation'
category = 'commentary'
tags = 'commentary'
language = 'en'
publication_type = 'blog'
cover_url = 'http://www.heritage.org/static/images/logo.jpg'
masthead_url = 'http://www.heritage.org/static/images/logo.jpg'
encoding = None
use_embedded_content = False
no_stylesheets = True
remove_javascript = True
recursions = 0
remove_empty_feeds = True
auto_cleanup = True
conversion_options = {
'comments' : description,
'tags' : tags,
'language' : language,
'publisher' : publisher,
'authors' : publisher,
'smarten_punctuation' : True
}
feeds = [
(u'Agriculture', u'http://www.heritage.org/static/RSS/Agriculture.xml'),
(u'Alliances', u'http://www.heritage.org/static/RSS/Alliances.xml'),
(u'Arms Control and Non-Proliferation', u'http://www.heritage.org/static/RSS/Arms-Control-and-Non-Proliferation.xml'),
(u'Budget and Spending', u'http://www.heritage.org/static/RSS/Budget-and-Spending.xml'),
(u'Economic Freedom', u'http://www.heritage.org/static/RSS/Economic-Freedom.xml'),
(u'Economy', u'http://www.heritage.org/static/RSS/Economy.xml'),
(u'Education', u'http://www.heritage.org/static/RSS/Education.xml'),
(u'Energy and Environment', u'http://www.heritage.org/static/RSS/Energy-and-Environment.xml'),
(u'Family and Marriage', u'http://www.heritage.org/static/RSS/Family-And-Marriage.xml'),
(u'Foreign Aid and Development', u'http://www.heritage.org/static/RSS/Foreign-Aid-and-Development.xml'),
(u'Health Care', u'http://www.heritage.org/static/RSS/Health-Care.xml'),
(u'Homeland Security', u'http://www.heritage.org/static/RSS/Homeland-Security.xml'),
(u'Housing', u'http://www.heritage.org/static/RSS/Housing.xml'),
(u'Immigration', u'http://www.heritage.org/static/RSS/Immigration.xml'),
(u'International Conflicts', u'http://www.heritage.org/static/RSS/International-Conflicts.xml'),
(u'International Law', u'http://www.heritage.org/static/RSS/International-Law.xml'),
(u'Labor', u'http://www.heritage.org/static/RSS/Labor.xml'),
(u'Legal Issues', u'http://www.heritage.org/static/RSS/Legal.xml'),
(u'Missile Defense', u'http://www.heritage.org/static/RSS/Missile-Defense.xml'),
(u'National Security and Defense', u'http://www.heritage.org/static/RSS/National-Security-and-Defense.xml'),
(u'Political Thought', u'http://www.heritage.org/static/RSS/Political-Thought.xml'),
(u'Public Diplomacy', u'http://www.heritage.org/static/RSS/Public-Diplomacy.xml'),
(u'Regulation', u'http://www.heritage.org/static/RSS/Regulation.xml'),
(u'Religion and Civil Society', u'http://www.heritage.org/static/RSS/Religion-and-Civil-Society.xml'),
(u'Retirement Security', u'http://www.heritage.org/static/RSS/Retirement-Security.xml'),
(u'Space Policy', u'http://www.heritage.org/static/RSS/Space-Policy.xml'),
(u'Taxes', u'http://www.heritage.org/static/RSS/Taxes.xml'),
(u'Terrorism', u'http://www.heritage.org/static/RSS/Terrorism.xml'),
(u'Trade', u'http://www.heritage.org/static/RSS/Trade.xml'),
(u'Transportation', u'http://www.heritage.org/static/RSS/Transportation.xml'),
(u'Welfare', u'http://www.heritage.org/static/RSS/Welfare.xml'),
(u'Worldwide Freedom and Human Rights', u'http://www.heritage.org/static/RSS/Worldwide-Freedom-and-Human-Rights.xml'),
]

View File

@ -1,59 +1,100 @@
import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
class AdvancedUserRecipe1335532466(BasicNewsRecipe): class RichmondTimesDispatch(BasicNewsRecipe):
title = u'Richmond Times-Dispatch' title = u'Richmond Times-Dispatch'
description = 'News from Richmond, Virginia, USA' description = "The Richmond Times-Dispatch is the primary daily newspaper in Richmond, \
__author__ = 'jde' the capital of Virginia, United States, as well as the Virginia cities of Petersburg, \
Chester. Hopewell, Colonial Heights, Charlottesville, Lynchburg, Waynesboro, \
and is also a default paper for rural regions of the state. \
The RTD has published in some form for more than 150 years."
__author__ = '_reader'
__date__ = '05 July 2012'
__version__ = '1.4'
cover_url = 'http://static2.dukecms.com/va_tn/timesdispatch_com/site-media/img/icons/logo252x97.png' cover_url = 'http://static2.dukecms.com/va_tn/timesdispatch_com/site-media/img/icons/logo252x97.png'
masthead_url = 'http://static2.dukecms.com/va_tn/timesdispatch_com/site-media/img/icons/logo252x97.png'
language = 'en' language = 'en'
encoding = 'utf8' oldest_article = 1.5 #days
oldest_article = 1 #days max_articles_per_feed = 100
max_articles_per_feed = 25
needs_subscription = False needs_subscription = False
remove_javascript = True publisher = 'timesdispatch.com'
recursions = 0 category = 'news, commentary'
use_embedded_content = False tags = 'news'
publication_type = 'newspaper'
no_stylesheets = True no_stylesheets = True
auto_cleanup = True use_embedded_content= False
encoding = None
simultaneous_downloads = 20
recursions = 0
remove_javascript = True
remove_empty_feeds = True
auto_cleanup = False
feeds = [ conversion_options = {
'comments' : description,
'tags' : tags,
'language' : language,
'publisher' : publisher,
'authors' : publisher,
'smarten_punctuation' : True
}
('News', remove_tags_before = dict(id='hnews hentry item')
'http://www2.timesdispatch.com/list/feed/rss/news-archive'),
('Breaking News',
'http://www2.timesdispatch.com/list/feed/rss/breaking-news'),
('National News',
'http://www2.timesdispatch.com/list/feed/rss/national-news'),
('Local News',
'http://www2.timesdispatch.com/list/feed/rss/local-news'),
('Business',
'http://www2.timesdispatch.com/list/feed/rss/business'),
('Local Business',
'http://www2.timesdispatch.com/list/feed/rss/local-business'),
('Politics',
'http://www2.timesdispatch.com/list/feed/rss/politics'),
('Virginia Politics',
'http://www2.timesdispatch.com/list/feed/rss/virginia-politics'),
('Editorials',
'http://www2.timesdispatch.com/list/feed/rss/editorial-desk'),
('Columnists and Blogs',
'http://www2.timesdispatch.com/list/feed/rss/news-columnists-blogs'),
('Opinion Columnists',
'http://www2.timesdispatch.com/list/feed/rss/opinion-editorial-columnists'),
('Letters to the Editor',
'http://www2.timesdispatch.com/list/feed/rss/opinion-letters'),
('Traffic',
'http://www2.timesdispatch.com/list/feed/rss/traffic'),
('Sports',
'http://www2.timesdispatch.com/list/feed/rss/sports2'),
('Entertainment/Life',
'http://www2.timesdispatch.com/list/feed/rss/entertainment'),
('Movies',
'http://www2.timesdispatch.com/list/feed/rss/movies'),
('Music',
'http://www2.timesdispatch.com/list/feed/rss/music'),
('Dining & Food',
'http://www2.timesdispatch.com/list/feed/rss/dining'),
remove_tags_after = dict(name='hr')
remove_tags = [
dict(name='div', attrs={'id':['mg_hd', 'mg_ft', 'sr_b', 'comments_left', 'comments_right']})
,dict(name='div', attrs={'class':['bottom_social','article_bottom']})
,dict(name='table', attrs={'class':['ap-mediabox-table', 'ap-htmltable-table', 'ap-photogallery-table', 'ap-htmlfragment-table']})
] ]
preprocess_regexps = [
(re.compile(r'<table class="ap-story-table hnews hentry item".*?<td class="ap-story-td">', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<p>\s*http://www2.timesdispatch.*?</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<p>\s*<img src="http://static2.dukecms.*?</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<p>\s*<a href="http://www2.timesdispatch.*?</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<hr.*?>', re.DOTALL|re.IGNORECASE), lambda match: ''), #strip <hr /> line break
(re.compile(r'<a\s*rel="item-license.*?Use</a>.', re.DOTALL|re.IGNORECASE), lambda match: ''), #strip <hr /> line break
(re.compile(r'<small>\s*Richmond Times-Dispatch.*?</small>', re.DOTALL|re.IGNORECASE), lambda match: ''), #strip <hr /> line break
]
feeds = [
('News', 'http://www2.timesdispatch.com/list/feed/rss/news-archive'),
('Breaking News', 'http://www2.timesdispatch.com/list/feed/rss/breaking-news'),
('National News', 'http://www2.timesdispatch.com/list/feed/rss/national-news'),
('Local News', 'http://www2.timesdispatch.com/list/feed/rss/local-news'),
('Business', 'http://www2.timesdispatch.com/list/feed/rss/business'),
('Local Business', 'http://www2.timesdispatch.com/list/feed/rss/local-business'),
('Politics', 'http://www2.timesdispatch.com/list/feed/rss/politics'),
('Virginia Politics', 'http://www2.timesdispatch.com/list/feed/rss/virginia-politics'),
('Sports', 'http://www2.timesdispatch.com/list/feed/rss/sports2'),
('Health', 'http://www2.timesdispatch.com/feed/rss/lifestyles/health_med_fit/'),
('Entertainment/Life', 'http://www2.timesdispatch.com/list/feed/rss/entertainment'),
('Arts/Theatre', 'http://www2.timesdispatch.com/feed/rss/entertainment/arts_theatre/'),
('Movies', 'http://www2.timesdispatch.com/list/feed/rss/movies'),
('Music', 'http://www2.timesdispatch.com/list/feed/rss/music'),
('Dining & Food', 'http://www2.timesdispatch.com/list/feed/rss/dining'),
('Home & Garden', 'http://www2.timesdispatch.com/list/feed/rss/home-and-garden/'),
#inactive('Travel', 'http://www2.timesdispatch.com/feed/rss/travel/'),
('Opinion', 'http://www2.timesdispatch.com/feed/rss/news/opinion/'),
('Editorials', 'http://www2.timesdispatch.com/list/feed/rss/editorial-desk'),
('Columnists and Blogs', 'http://www2.timesdispatch.com/list/feed/rss/news-columnists-blogs'),
('Opinion Columnists', 'http://www2.timesdispatch.com/list/feed/rss/opinion-editorial-columnists'),
('Letters to the Editor', 'http://www2.timesdispatch.com/list/feed/rss/opinion-letters'),
('Traffic', 'http://www2.timesdispatch.com/list/feed/rss/traffic'),
]
def print_version(self,url):
article_num = re.sub(r'(^.*)\-([0-9]{4,10})\/$', r'\g<2>', url)
ap_pat = re.compile('http')
#print '\nDEBUG>>>>>>>>: article_num: ', article_num
#print 'DEBUG>>>>>>>>: ap_pat.search(article_num): ', ap_pat.search(article_num)
if ap_pat.search(article_num): #AP article, no print url
#print 'DEBUG>>>>>>>>: AP URL: ', url
return url
else:
printURL = 'http://www2.timesdispatch.com/member-center/share-this/print/?content=ar' + article_num
return printURL

16
recipes/warentest.recipe Normal file
View File

@ -0,0 +1,16 @@
from calibre.web.feeds.news import BasicNewsRecipe
class Warentest(BasicNewsRecipe):
title = u'Warentest'
language = 'de'
description = 'Stiftung Warentest is a German consumer organisation and foundation involved in investigating and comparing goods and services in an unbiased way'
__author__ = 'asdfdsfksd'
needs_subscription = False
max_articles_per_feed = 100
auto_cleanup = True
feeds = [(u'Test', u'http://www.test.de/rss/alles/')]
def get_cover_url(self):
return 'http://www.test.de/img/pp/logo.png'

Binary file not shown.

View File

@ -515,3 +515,13 @@ compile_gpm_templates = True
# default_tweak_format = 'remember' # default_tweak_format = 'remember'
default_tweak_format = None default_tweak_format = None
#: Enable multi-character first-letters in the tag browser
# Some languages have letters that can be represented by multiple characters.
# For example, Czech has a 'character' "ch" that sorts between "h" and "i".
# If this tweak is True, then the tag browser will take these characters into
# consideration when partitioning by first letter.
# Examples:
# enable_multicharacters_in_tag_browser = True
# enable_multicharacters_in_tag_browser = True
enable_multicharacters_in_tag_browser = True

View File

@ -34,6 +34,7 @@ if iswindows:
MT = os.path.join(os.path.dirname(p), 'bin', 'mt.exe') MT = os.path.join(os.path.dirname(p), 'bin', 'mt.exe')
MT = os.path.join(SDK, 'bin', 'mt.exe') MT = os.path.join(SDK, 'bin', 'mt.exe')
os.environ['QMAKESPEC'] = 'win32-msvc' os.environ['QMAKESPEC'] = 'win32-msvc'
ICU = r'Q:\icu'
QMAKE = '/Volumes/sw/qt/bin/qmake' if isosx else 'qmake' QMAKE = '/Volumes/sw/qt/bin/qmake' if isosx else 'qmake'
if find_executable('qmake-qt4'): if find_executable('qmake-qt4'):
@ -97,8 +98,9 @@ if iswindows:
prefix = r'C:\cygwin\home\kovid\sw' prefix = r'C:\cygwin\home\kovid\sw'
sw_inc_dir = os.path.join(prefix, 'include') sw_inc_dir = os.path.join(prefix, 'include')
sw_lib_dir = os.path.join(prefix, 'lib') sw_lib_dir = os.path.join(prefix, 'lib')
icu_inc_dirs = [sw_inc_dir] icu_inc_dirs = [os.path.join(ICU, 'source', 'common'), os.path.join(ICU,
icu_lib_dirs = [sw_lib_dir] 'source', 'i18n')]
icu_lib_dirs = [os.path.join(ICU, 'source', 'lib')]
sqlite_inc_dirs = [sw_inc_dir] sqlite_inc_dirs = [sw_inc_dir]
fc_inc = os.path.join(sw_inc_dir, 'fontconfig') fc_inc = os.path.join(sw_inc_dir, 'fontconfig')
fc_lib = sw_lib_dir fc_lib = sw_lib_dir

View File

@ -54,10 +54,10 @@ binary_includes = [
'/lib/libreadline.so.6', '/lib/libreadline.so.6',
'/usr/lib/libchm.so.0', '/usr/lib/libchm.so.0',
'/usr/lib/liblcms2.so.2', '/usr/lib/liblcms2.so.2',
'/usr/lib/libicudata.so.46', '/usr/lib/libicudata.so.49',
'/usr/lib/libicui18n.so.46', '/usr/lib/libicui18n.so.49',
'/usr/lib/libicuuc.so.46', '/usr/lib/libicuuc.so.49',
'/usr/lib/libicuio.so.46', '/usr/lib/libicuio.so.49',
] ]
binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS] binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS]

View File

@ -13,6 +13,7 @@ from setup import (Command, modules, functions, basenames, __version__,
from setup.build_environment import msvc, MT, RC from setup.build_environment import msvc, MT, RC
from setup.installer.windows.wix import WixMixIn from setup.installer.windows.wix import WixMixIn
ICU_DIR = r'Q:\icu'
OPENSSL_DIR = r'Q:\openssl' OPENSSL_DIR = r'Q:\openssl'
QT_DIR = 'Q:\\Qt\\4.8.2' QT_DIR = 'Q:\\Qt\\4.8.2'
QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns'] QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns']
@ -147,6 +148,8 @@ class Win32Freeze(Command, WixMixIn):
ignore=shutil.ignore_patterns('msvc*.dll', 'Microsoft.*')) ignore=shutil.ignore_patterns('msvc*.dll', 'Microsoft.*'))
for x in glob.glob(self.j(OPENSSL_DIR, 'bin', '*.dll')): for x in glob.glob(self.j(OPENSSL_DIR, 'bin', '*.dll')):
shutil.copy2(x, self.dll_dir) shutil.copy2(x, self.dll_dir)
for x in glob.glob(self.j(ICU_DIR, 'source', 'lib', '*.dll')):
shutil.copy2(x, self.dll_dir)
for x in QT_DLLS: for x in QT_DLLS:
x += '4.dll' x += '4.dll'
if not x.startswith('phonon'): x = 'Qt'+x if not x.startswith('phonon'): x = 'Qt'+x

View File

@ -131,11 +131,22 @@ calibre-debug -c "import _imaging, _imagingmath, _imagingft, _imagingcms"
ICU ICU
------- -------
Download the win32 msvc9 binary from http://www.icu-project.org/download/4.4.html Download the win32 source .zip from http://www.icu-project.org/download
Note that 4.4 is the last version of ICU that can be compiled (is precompiled) with msvc9 Extract to q:\icu
Put the dlls into sw/bin and the unicode dir into sw/include and the contents of lib int sw/lib Add Q:\icu\bin to PATH and reboot
In a Visual Studio Command Prompt
cd to <ICU>\source
Run set PATH=%PATH%;c:\cygwin\bin
Run dos2unix on configure and runConfigureICU
Run bash ./runConfigureICU Cygwin/MSVC
Run make (note that you must have GNU make installed in cygwin)
Optionally run make test
Libunrar Libunrar
---------- ----------

View File

@ -40,7 +40,7 @@ class Stage2(Command):
class Stage3(Command): class Stage3(Command):
description = 'Stage 3 of the publish process' description = 'Stage 3 of the publish process'
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist'] sub_commands = ['upload_user_manual', 'upload_demo', 'sdist', 'tag_release']
class Stage4(Command): class Stage4(Command):
@ -50,7 +50,7 @@ class Stage4(Command):
class Stage5(Command): class Stage5(Command):
description = 'Stage 5 of the publish process' description = 'Stage 5 of the publish process'
sub_commands = ['tag_release', 'upload_to_server'] sub_commands = ['upload_to_server']
def run(self, opts): def run(self, opts):
subprocess.check_call('rm -rf build/* dist/*', shell=True) subprocess.check_call('rm -rf build/* dist/*', shell=True)

View File

@ -4,7 +4,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__ = u'calibre' __appname__ = u'calibre'
numeric_version = (0, 8, 58) numeric_version = (0, 8, 59)
__version__ = u'.'.join(map(unicode, numeric_version)) __version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>" __author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -1203,7 +1203,7 @@ class StoreAmazonFRKindleStore(StoreBase):
description = u'Tous les ebooks Kindle' description = u'Tous les ebooks Kindle'
actual_plugin = 'calibre.gui2.store.stores.amazon_fr_plugin:AmazonFRKindleStore' actual_plugin = 'calibre.gui2.store.stores.amazon_fr_plugin:AmazonFRKindleStore'
headquarters = 'DE' headquarters = 'FR'
formats = ['KINDLE'] formats = ['KINDLE']
affiliate = True affiliate = True

View File

@ -497,6 +497,7 @@ def initialize_plugin(plugin, path_to_zip_file):
%tb) + '\n'+tb) %tb) + '\n'+tb)
def has_external_plugins(): def has_external_plugins():
'True if there are updateable (zip file based) plugins'
return bool(config['plugins']) return bool(config['plugins'])
def initialize_plugins(perf=False): def initialize_plugins(perf=False):
@ -554,6 +555,23 @@ def initialized_plugins():
# CLI {{{ # CLI {{{
def build_plugin(path):
from calibre import prints
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.zipfile import ZipFile, ZIP_STORED
path = type(u'')(path)
names = frozenset(os.listdir(path))
if u'__init__.py' not in names:
prints(path, ' is not a valid plugin')
raise SystemExit(1)
t = PersistentTemporaryFile(u'.zip')
with ZipFile(t, u'w', ZIP_STORED) as zf:
zf.add_dir(path)
t.close()
plugin = add_plugin(t.name)
os.remove(t.name)
prints(u'Plugin updated:', plugin.name, plugin.version)
def option_parser(): def option_parser():
parser = OptionParser(usage=_('''\ parser = OptionParser(usage=_('''\
%prog options %prog options
@ -562,6 +580,10 @@ def option_parser():
''')) '''))
parser.add_option('-a', '--add-plugin', default=None, parser.add_option('-a', '--add-plugin', default=None,
help=_('Add a plugin by specifying the path to the zip file containing it.')) help=_('Add a plugin by specifying the path to the zip file containing it.'))
parser.add_option('-b', '--build-plugin', default=None,
help=_('For plugin developers: Path to the directory where you are'
' developing the plugin. This command will automatically zip '
'up the plugin and update it in calibre.'))
parser.add_option('-r', '--remove-plugin', default=None, parser.add_option('-r', '--remove-plugin', default=None,
help=_('Remove a custom plugin by name. Has no effect on builtin plugins')) help=_('Remove a custom plugin by name. Has no effect on builtin plugins'))
parser.add_option('--customize-plugin', default=None, parser.add_option('--customize-plugin', default=None,
@ -583,6 +605,8 @@ def main(args=sys.argv):
if opts.add_plugin is not None: if opts.add_plugin is not None:
plugin = add_plugin(opts.add_plugin) plugin = add_plugin(opts.add_plugin)
print 'Plugin added:', plugin.name, plugin.version print 'Plugin added:', plugin.name, plugin.version
if opts.build_plugin is not None:
build_plugin(opts.build_plugin)
if opts.remove_plugin is not None: if opts.remove_plugin is not None:
if remove_plugin(opts.remove_plugin): if remove_plugin(opts.remove_plugin):
print 'Plugin removed' print 'Plugin removed'

View File

@ -72,6 +72,7 @@ class ANDROID(USBMS):
# Sony Ericsson # Sony Ericsson
0xfce : { 0xfce : {
0xa173 : [0x216],
0xd12e : [0x0100], 0xd12e : [0x0100],
0xe156 : [0x226], 0xe156 : [0x226],
0xe15d : [0x226], 0xe15d : [0x226],
@ -99,7 +100,7 @@ class ANDROID(USBMS):
0x681c : [0x0222, 0x0223, 0x0224, 0x0400], 0x681c : [0x0222, 0x0223, 0x0224, 0x0400],
0x6640 : [0x0100], 0x6640 : [0x0100],
0x685b : [0x0400, 0x0226], 0x685b : [0x0400, 0x0226],
0x685e : [0x0400], 0x685e : [0x0400, 0x226],
0x6860 : [0x0400], 0x6860 : [0x0400],
0x6863 : [0x226], 0x6863 : [0x226],
0x6877 : [0x0400], 0x6877 : [0x0400],
@ -208,7 +209,7 @@ class ANDROID(USBMS):
'XT910', 'BOOK_A10', 'USB_2.0_DRIVER', 'I9100T', 'P999DW', 'XT910', 'BOOK_A10', 'USB_2.0_DRIVER', 'I9100T', 'P999DW',
'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER',
'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX',
'THINKPAD_TABLET'] 'THINKPAD_TABLET', 'SGH-T989']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -217,7 +218,7 @@ class ANDROID(USBMS):
'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD', 'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD',
'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC',
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX'] 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD']
OSX_MAIN_MEM = 'Android Device Main Memory' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -159,16 +159,29 @@ def get_udisks(ver=None):
return u return u
return UDisks2() if ver == 2 else UDisks() return UDisks2() if ver == 2 else UDisks()
def mount(node_path): def get_udisks1():
u = None
try:
u = UDisks() u = UDisks()
except NoUDisks1:
try:
u = UDisks2()
except NoUDisks2:
pass
if u is None:
raise EnvironmentError('UDisks not available on your system')
return u
def mount(node_path):
u = get_udisks1()
u.mount(node_path) u.mount(node_path)
def eject(node_path): def eject(node_path):
u = UDisks() u = get_udisks1()
u.eject(node_path) u.eject(node_path)
def umount(node_path): def umount(node_path):
u = UDisks() u = get_udisks1()
u.unmount(node_path) u.unmount(node_path)
def test_udisks(ver=None): def test_udisks(ver=None):

View File

@ -66,6 +66,9 @@ class USER_DEFINED(USBMS):
_('Card A folder') + ':::<p>' + _('Card A folder') + ':::<p>' +
_('Enter the folder where the books are to be stored. This folder ' _('Enter the folder where the books are to be stored. This folder '
'is prepended to any send_to_device template') + '</p>', 'is prepended to any send_to_device template') + '</p>',
_('Swap main and card A') + ':::<p>' +
_('Check this box if the device\'s main memory is being seen as '
'card a and the card is being seen as main memory') + '</p>',
] ]
EXTRA_CUSTOMIZATION_DEFAULT = [ EXTRA_CUSTOMIZATION_DEFAULT = [
'0xffff', '0xffff',
@ -78,16 +81,19 @@ class USER_DEFINED(USBMS):
'', '',
'', '',
'', '',
False,
] ]
OPT_USB_VENDOR_ID = 0 OPT_USB_VENDOR_ID = 0
OPT_USB_PRODUCT_ID = 1 OPT_USB_PRODUCT_ID = 1
OPT_USB_REVISION_ID = 2 OPT_USB_REVISION_ID = 2
# OPT 3 isn't used
OPT_USB_WINDOWS_MM_VEN_ID = 4 OPT_USB_WINDOWS_MM_VEN_ID = 4
OPT_USB_WINDOWS_MM_ID = 5 OPT_USB_WINDOWS_MM_ID = 5
OPT_USB_WINDOWS_CA_VEN_ID = 6 OPT_USB_WINDOWS_CA_VEN_ID = 6
OPT_USB_WINDOWS_CA_ID = 7 OPT_USB_WINDOWS_CA_ID = 7
OPT_MAIN_MEM_FOLDER = 8 OPT_MAIN_MEM_FOLDER = 8
OPT_CARD_A_FOLDER = 9 OPT_CARD_A_FOLDER = 9
OPT_SWAP_MAIN_AND_CARD = 10
def initialize(self): def initialize(self):
self.plugin_needs_delayed_initialization = True self.plugin_needs_delayed_initialization = True
@ -113,4 +119,41 @@ class USER_DEFINED(USBMS):
traceback.print_exc() traceback.print_exc()
self.plugin_needs_delayed_initialization = False self.plugin_needs_delayed_initialization = False
def windows_sort_drives(self, drives):
if len(drives) < 2: return drives
e = self.settings().extra_customization
if not e[self.OPT_SWAP_MAIN_AND_CARD]:
return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda:
drives['main'] = carda
drives['carda'] = main
return drives
def linux_swap_drives(self, drives):
if len(drives) < 2 or not drives[1] or not drives[2]: return drives
e = self.settings().extra_customization
if not e[self.OPT_SWAP_MAIN_AND_CARD]:
return drives
drives = list(drives)
t = drives[0]
drives[0] = drives[1]
drives[1] = t
return tuple(drives)
def osx_sort_names(self, names):
if len(names) < 2: return names
e = self.settings().extra_customization
if not e[self.OPT_SWAP_MAIN_AND_CARD]:
return names
main = names.get('main', None)
card = names.get('carda', None)
if main is not None and card is not None:
names['main'] = card
names['carda'] = main
return names

View File

@ -6,48 +6,7 @@
Released under the GPLv3 License Released under the GPLv3 License
### ###
log = (args...) -> # {{{ log = window.calibre_utils.log
if args
msg = args.join(' ')
if window?.console?.log
window.console.log(msg)
else if process?.stdout?.write
process.stdout.write(msg + '\n')
# }}}
window_scroll_pos = (win=window) -> # {{{
if typeof(win.pageXOffset) == 'number'
x = win.pageXOffset
y = win.pageYOffset
else # IE < 9
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
x = document.body.scrollLeft
y = document.body.scrollTop
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
y = document.documentElement.scrollTop
x = document.documentElement.scrollLeft
return [x, y]
# }}}
viewport_to_document = (x, y, doc=window?.document) -> # {{{
until doc == window.document
# We are in a frame
frame = doc.defaultView.frameElement
rect = frame.getBoundingClientRect()
x += rect.left
y += rect.top
doc = frame.ownerDocument
win = doc.defaultView
[wx, wy] = window_scroll_pos(win)
x += wx
y += wy
return [x, y]
# }}}
absleft = (elem) -> # {{{
r = elem.getBoundingClientRect()
return viewport_to_document(r.left, 0, elem.ownerDocument)[0]
# }}}
class PagedDisplay class PagedDisplay
# This class is a namespace to expose functions via the # This class is a namespace to expose functions via the
@ -75,6 +34,7 @@ class PagedDisplay
this.cols_per_screen = cols_per_screen this.cols_per_screen = cols_per_screen
layout: () -> layout: () ->
# start_time = new Date().getTime()
body_style = window.getComputedStyle(document.body) body_style = window.getComputedStyle(document.body)
# When laying body out in columns, webkit bleeds the top margin of the # When laying body out in columns, webkit bleeds the top margin of the
# first block element out above the columns, leading to an extra top # first block element out above the columns, leading to an extra top
@ -160,8 +120,30 @@ class PagedDisplay
this.in_paged_mode = true this.in_paged_mode = true
this.current_margin_side = sm this.current_margin_side = sm
# log('Time to layout:', new Date().getTime() - start_time)
return sm return sm
fit_images: () ->
# Ensure no images are wider than the available width in a column. Note
# that this method use getBoundingClientRect() which means it will
# force a relayout if the render tree is dirty.
images = []
for img in document.getElementsByTagName('img')
previously_limited = calibre_utils.retrieve(img, 'width-limited', false)
br = img.getBoundingClientRect()
left = calibre_utils.viewport_to_document(br.left, 0, doc=img.ownerDocument)[0]
col = this.column_at(left) * this.page_width
rleft = left - col - this.current_margin_side
width = br.right - br.left
rright = rleft + width
col_width = this.page_width - 2*this.current_margin_side
if previously_limited or rright > col_width
images.push([img, col_width - rleft])
for [img, max_width] in images
img.style.setProperty('max-width', max_width+'px')
calibre_utils.store(img, 'width-limited', true)
scroll_to_pos: (frac) -> scroll_to_pos: (frac) ->
# Scroll to the position represented by frac (number between 0 and 1) # Scroll to the position represented by frac (number between 0 and 1)
xpos = Math.floor(document.body.scrollWidth * frac) xpos = Math.floor(document.body.scrollWidth * frac)
@ -297,7 +279,7 @@ class PagedDisplay
elem.scrollIntoView() elem.scrollIntoView()
if this.in_paged_mode if this.in_paged_mode
# Ensure we are scrolled to the column containing elem # Ensure we are scrolled to the column containing elem
this.scroll_to_xpos(absleft(elem) + 5) this.scroll_to_xpos(calibre_utils.absleft(elem) + 5)
snap_to_selection: () -> snap_to_selection: () ->
# Ensure that the viewport is positioned at the start of the column # Ensure that the viewport is positioned at the start of the column
@ -306,7 +288,7 @@ class PagedDisplay
sel = window.getSelection() sel = window.getSelection()
r = sel.getRangeAt(0).getBoundingClientRect() r = sel.getRangeAt(0).getBoundingClientRect()
node = sel.anchorNode node = sel.anchorNode
left = viewport_to_document(r.left, r.top, doc=node.ownerDocument)[0] left = calibre_utils.viewport_to_document(r.left, r.top, doc=node.ownerDocument)[0]
# Ensure we are scrolled to the column containing the start of the # Ensure we are scrolled to the column containing the start of the
# selection # selection
@ -365,5 +347,5 @@ if window?
window.paged_display = new PagedDisplay() window.paged_display = new PagedDisplay()
# TODO: # TODO:
# Resizing of images
# Highlight on jump_to_anchor # Highlight on jump_to_anchor
# Handle document specified margins and allow them to be overridden

View File

@ -0,0 +1,96 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
Released under the GPLv3 License
###
class CalibreUtils
# This class is a namespace to expose functions via the
# window.calibre_utils object.
constructor: () ->
if not this instanceof arguments.callee
throw new Error('CalibreUtils constructor called as function')
this.dom_attr = 'calibre_f3fa75ca98eb4413a4ee413f20f60226'
this.dom_data = []
# Data API {{{
retrieve: (node, key, def=null) ->
# Retrieve data previously stored on node (a DOM node) with key (a
# string). If no such data is found then return the value of def.
idx = parseInt(node.getAttribute(this.dom_attr))
if isNaN(idx)
return def
data = this.dom_data[idx]
if not data.hasOwnProperty(key)
return def
return data[key]
store: (node, key, val) ->
# Store arbitrary javscript object val on DOM node node with key (a
# string). This can be later retrieved by the retrieve method.
idx = parseInt(node.getAttribute(this.dom_attr))
if isNaN(idx)
idx = this.dom_data.length
node.setAttribute(this.dom_attr, idx+'')
this.dom_data.push({})
this.dom_data[idx][key] = val
# }}}
log: (args...) -> # {{{
# Output args to the window.console object. args are automatically
# coerced to strings
if args
msg = args.join(' ')
if window?.console?.log
window.console.log(msg)
else if process?.stdout?.write
process.stdout.write(msg + '\n')
# }}}
window_scroll_pos: (win=window) -> # {{{
# The current scroll position of the browser window
if typeof(win.pageXOffset) == 'number'
x = win.pageXOffset
y = win.pageYOffset
else # IE < 9
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
x = document.body.scrollLeft
y = document.body.scrollTop
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
y = document.documentElement.scrollTop
x = document.documentElement.scrollLeft
return [x, y]
# }}}
viewport_to_document: (x, y, doc=window?.document) -> # {{{
# Convert x, y from the viewport (window) co-ordinate system to the
# document (body) co-ordinate system
until doc == window.document
# We are in a frame
frame = doc.defaultView.frameElement
rect = frame.getBoundingClientRect()
x += rect.left
y += rect.top
doc = frame.ownerDocument
win = doc.defaultView
[wx, wy] = this.window_scroll_pos(win)
x += wx
y += wy
return [x, y]
# }}}
absleft: (elem) -> # {{{
# The left edge of elem in document co-ords. Works in all
# circumstances, including column layout. Note that this will cause
# a relayout if the render tree is dirty.
r = elem.getBoundingClientRect()
return this.viewport_to_document(r.left, 0, elem.ownerDocument)[0]
# }}}
if window?
window.calibre_utils = new CalibreUtils()

View File

@ -117,7 +117,6 @@ class PDFMetadata(object):
if len(oeb_metadata.creator) >= 1: if len(oeb_metadata.creator) >= 1:
self.author = authors_to_string([x.value for x in oeb_metadata.creator]) self.author = authors_to_string([x.value for x in oeb_metadata.creator])
class PDFWriter(QObject): # {{{ class PDFWriter(QObject): # {{{
def __init__(self, opts, log, cover_data=None): def __init__(self, opts, log, cover_data=None):
@ -185,8 +184,8 @@ class PDFWriter(QObject): # {{{
from PyQt4.Qt import QSize, QPainter from PyQt4.Qt import QSize, QPainter
if self.paged_js is None: if self.paged_js is None:
from calibre.utils.resources import compiled_coffeescript from calibre.utils.resources import compiled_coffeescript
self.paged_js = compiled_coffeescript('ebooks.oeb.display.paged', self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
dynamic=False) self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
printer = get_pdf_printer(self.opts, output_file_name=outpath) printer = get_pdf_printer(self.opts, output_file_name=outpath)
painter = QPainter(printer) painter = QPainter(printer)
zoomx = printer.logicalDpiX()/self.view.logicalDpiX() zoomx = printer.logicalDpiX()/self.view.logicalDpiX()
@ -202,6 +201,7 @@ class PDFWriter(QObject): # {{{
document.body.style.backgroundColor = "white"; document.body.style.backgroundColor = "white";
paged_display.set_geometry(1, 0, 0, 0); paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout(); paged_display.layout();
paged_display.fit_images();
''') ''')
mf = self.view.page().mainFrame() mf = self.view.page().mainFrame()
while True: while True:

View File

@ -736,6 +736,9 @@ class Application(QApplication):
def __init__(self, args, force_calibre_style=False): def __init__(self, args, force_calibre_style=False):
self.file_event_hook = None self.file_event_hook = None
if islinux and args[0].endswith(u'calibre'):
args = list(args)
args[0] += '-gui'
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
QApplication.__init__(self, qargs) QApplication.__init__(self, qargs)
global gui_thread, qt_app global gui_thread, qt_app

View File

@ -216,7 +216,8 @@ class CopyToLibraryAction(InterfaceAction):
if ci.isValid(): if ci.isValid():
row = ci.row() row = ci.row()
v.model().delete_books_by_id(self.worker.processed) v.model().delete_books_by_id(self.worker.processed,
permanent=True)
self.gui.iactions['Remove Books'].library_ids_deleted( self.gui.iactions['Remove Books'].library_ids_deleted(
self.worker.processed, row) self.worker.processed, row)

View File

@ -104,8 +104,11 @@ def render_data(mi, use_roman_numbers=True, all_fields=False):
field = 'title_sort' field = 'title_sort'
if all_fields: if all_fields:
display = True display = True
if (not display or not metadata or mi.is_null(field) or if metadata['datatype'] == 'bool':
field == 'comments'): isnull = mi.get(field) is None
else:
isnull = mi.is_null(field)
if (not display or not metadata or isnull or field == 'comments'):
continue continue
name = metadata['name'] name = metadata['name']
if not name: if not name:

View File

@ -165,8 +165,18 @@ class MultiCompleteComboBox(EnComboBox):
def showPopup(self): def showPopup(self):
c = self.le._completer c = self.le._completer
v = c.currentCompletion()
c.setCompletionPrefix('') c.setCompletionPrefix('')
c.complete() c.complete()
cs = c.caseSensitivity()
i = 0
while c.setCurrentRow(i):
cr = c.currentIndex().data().toString()
if cr.startsWith(v, cs):
c.popup().setCurrentIndex(c.currentIndex())
return
i += 1
c.setCurrentRow(0)
def update_items_cache(self, complete_items): def update_items_cache(self, complete_items):
self.lineEdit().update_items_cache(complete_items) self.lineEdit().update_items_cache(complete_items)

View File

@ -220,16 +220,15 @@ class BooksModel(QAbstractTableModel): # {{{
self.count_changed() self.count_changed()
self.reset() self.reset()
def delete_books(self, indices): def delete_books(self, indices, permanent=False):
ids = map(self.id, indices) ids = map(self.id, indices)
for id in ids: self.delete_books_by_id(ids, permanent=permanent)
self.db.delete_book(id, notify=False)
self.books_deleted()
return ids return ids
def delete_books_by_id(self, ids): def delete_books_by_id(self, ids, permanent=False):
for id in ids: for id in ids:
self.db.delete_book(id) self.db.delete_book(id, permanent=permanent, do_clean=False)
self.db.clean()
self.books_deleted() self.books_deleted()
def books_added(self, num): def books_added(self, num):

View File

@ -13,6 +13,7 @@ from calibre.gui2.preferences.search_ui import Ui_Form
from calibre.gui2 import config, error_dialog from calibre.gui2 import config, error_dialog
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.library.caches import set_use_primary_find_in_search
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
@ -26,6 +27,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('search_as_you_type', config) r('search_as_you_type', config)
r('highlight_search_matches', config) r('highlight_search_matches', config)
r('limit_search_columns', prefs) r('limit_search_columns', prefs)
r('use_primary_find_in_search', prefs)
r('limit_search_columns_to', prefs, setting=CommaSeparatedList) r('limit_search_columns_to', prefs, setting=CommaSeparatedList)
fl = db.field_metadata.get_search_terms() fl = db.field_metadata.get_search_terms()
self.opt_limit_search_columns_to.update_items_cache(fl) self.opt_limit_search_columns_to.update_items_cache(fl)
@ -222,6 +224,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
return ConfigWidgetBase.commit(self) return ConfigWidgetBase.commit(self)
def refresh_gui(self, gui): def refresh_gui(self, gui):
set_use_primary_find_in_search(prefs['use_primary_find_in_search'])
gui.set_highlight_only_button_icon() gui.set_highlight_only_button_icon()
if self.muc_changed: if self.muc_changed:
gui.tags_view.recount() gui.tags_view.recount()

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>670</width> <width>670</width>
<height>556</height> <height>663</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -21,14 +21,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="0" column="1">
<widget class="QCheckBox" name="opt_use_primary_find_in_search">
<property name="text">
<string>Unaccented characters match accented characters</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_highlight_search_matches"> <widget class="QCheckBox" name="opt_highlight_search_matches">
<property name="text"> <property name="text">
<string>&amp;Highlight search results instead of restricting the book list to the results</string> <string>&amp;Highlight search results instead of restricting the book list to the results</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>What to search by default</string> <string>What to search by default</string>
@ -77,17 +84,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="6" column="0" colspan="2">
<widget class="QPushButton" name="clear_history_button">
<property name="toolTip">
<string>Clear search histories from all over calibre. Including the book list, e-book viewer, fetch news dialog, etc.</string>
</property>
<property name="text">
<string>Clear search &amp;histories</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="groupBox_2"> <widget class="QGroupBox" name="groupBox_2">
<property name="title"> <property name="title">
<string>Grouped Search Terms</string> <string>Grouped Search Terms</string>
@ -107,12 +104,6 @@
</item> </item>
<item> <item>
<widget class="QComboBox" name="gst_names"> <widget class="QComboBox" name="gst_names">
<property name="editable">
<bool>true</bool>
</property>
<property name="minimumContentsLength">
<number>10</number>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Contains the names of the currently-defined group search terms. <string>Contains the names of the currently-defined group search terms.
Create a new name by entering it into the empty box, then Create a new name by entering it into the empty box, then
@ -120,6 +111,12 @@ pressing Save. Rename a search term by selecting it then
changing the name and pressing Save. Change the value of changing the name and pressing Save. Change the value of
a search term by changing the value box then pressing Save.</string> a search term by changing the value box then pressing Save.</string>
</property> </property>
<property name="editable">
<bool>true</bool>
</property>
<property name="minimumContentsLength">
<number>10</number>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -201,7 +198,17 @@ to be shown as user categories</string>
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="5" column="0" colspan="2">
<widget class="QPushButton" name="clear_history_button">
<property name="toolTip">
<string>Clear search histories from all over calibre. Including the book list, e-book viewer, fetch news dialog, etc.</string>
</property>
<property name="text">
<string>Clear search &amp;histories</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox22"> <widget class="QGroupBox" name="groupBox22">
<property name="title"> <property name="title">
<string>What to search when searching similar books</string> <string>What to search when searching similar books</string>
@ -211,7 +218,7 @@ to be shown as user categories</string>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>&lt;p&gt;When you search for similar books by right clicking the <string>&lt;p&gt;When you search for similar books by right clicking the
book and selecting "Similar books...", book and selecting &quot;Similar books...&quot;,
calibre constructs a search using the column lookup names specified below. calibre constructs a search using the column lookup names specified below.
By changing the lookup name to a grouped search term you can By changing the lookup name to a grouped search term you can
search multiple columns at once.&lt;/p&gt;</string> search multiple columns at once.&lt;/p&gt;</string>
@ -239,8 +246,7 @@ to be shown as user categories</string>
</widget> </widget>
</item> </item>
<item row="1" column="2"> <item row="1" column="2">
<widget class="QComboBox" name="opt_similar_authors_match_kind"> <widget class="QComboBox" name="opt_similar_authors_match_kind"/>
</widget>
</item> </item>
<item row="1" column="3"> <item row="1" column="3">
<widget class="QLabel" name="label_222"> <widget class="QLabel" name="label_222">
@ -260,8 +266,7 @@ to be shown as user categories</string>
</widget> </widget>
</item> </item>
<item row="1" column="5"> <item row="1" column="5">
<widget class="QComboBox" name="opt_similar_series_match_kind"> <widget class="QComboBox" name="opt_similar_series_match_kind"/>
</widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_223"> <widget class="QLabel" name="label_223">
@ -271,12 +276,10 @@ to be shown as user categories</string>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="similar_tags_search_key"> <widget class="QComboBox" name="similar_tags_search_key"/>
</widget>
</item> </item>
<item row="2" column="2"> <item row="2" column="2">
<widget class="QComboBox" name="opt_similar_tags_match_kind"> <widget class="QComboBox" name="opt_similar_tags_match_kind"/>
</widget>
</item> </item>
<item row="2" column="3"> <item row="2" column="3">
<widget class="QLabel" name="label_224"> <widget class="QLabel" name="label_224">
@ -286,12 +289,10 @@ to be shown as user categories</string>
</widget> </widget>
</item> </item>
<item row="2" column="4"> <item row="2" column="4">
<widget class="QComboBox" name="similar_publisher_search_key"> <widget class="QComboBox" name="similar_publisher_search_key"/>
</widget>
</item> </item>
<item row="2" column="5"> <item row="2" column="5">
<widget class="QComboBox" name="opt_similar_publisher_match_kind"> <widget class="QComboBox" name="opt_similar_publisher_match_kind"/>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -5,7 +5,9 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from functools import partial
import textwrap import textwrap
from collections import OrderedDict
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
from calibre.gui2.preferences.tweaks_ui import Ui_Form from calibre.gui2.preferences.tweaks_ui import Ui_Form
@ -18,8 +20,8 @@ from calibre.utils.search_query_parser import (ParseException,
SearchQueryParser) SearchQueryParser)
from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle, from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle,
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, QApplication,
QVBoxLayout, QPlainTextEdit, QLabel, QModelIndex) QVBoxLayout, QPlainTextEdit, QLabel, QModelIndex, QMenu, QIcon)
ROOT = QModelIndex() ROOT = QModelIndex()
@ -46,10 +48,12 @@ class Tweak(object): # {{{
if self.doc: if self.doc:
self.doc = translate(self.doc) self.doc = translate(self.doc)
self.var_names = var_names self.var_names = var_names
self.default_values = {} if self.var_names:
self.doc = u"%s: %s\n\n%s"%(_('ID'), self.var_names[0], self.doc)
self.default_values = OrderedDict()
for x in var_names: for x in var_names:
self.default_values[x] = defaults[x] self.default_values[x] = defaults[x]
self.custom_values = {} self.custom_values = OrderedDict()
for x in var_names: for x in var_names:
if x in custom: if x in custom:
self.custom_values[x] = custom[x] self.custom_values[x] = custom[x]
@ -243,7 +247,8 @@ class Tweaks(QAbstractListModel, SearchQueryParser): # {{{
query = lower(query) query = lower(query)
for r in candidates: for r in candidates:
dat = self.data(self.index(r), Qt.UserRole) dat = self.data(self.index(r), Qt.UserRole)
if query in lower(dat.name):# or query in lower(dat.doc): var_names = u' '.join(dat.default_values)
if query in lower(dat.name) or query in lower(var_names):
ans.add(r) ans.add(r)
return ans return ans
@ -325,7 +330,29 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.search.initialize('tweaks_search_history', help_text= self.search.initialize('tweaks_search_history', help_text=
_('Search for tweak')) _('Search for tweak'))
self.search.search.connect(self.find) self.search.search.connect(self.find)
self.view.setContextMenuPolicy(Qt.CustomContextMenu)
self.view.customContextMenuRequested.connect(self.show_context_menu)
self.copy_icon = QIcon(I('edit-copy.png'))
def show_context_menu(self, point):
idx = self.tweaks_view.currentIndex()
if not idx.isValid():
return True
tweak = self.tweaks.data(idx, Qt.UserRole)
self.context_menu = QMenu(self)
self.context_menu.addAction(self.copy_icon,
_('Copy to clipboard'),
partial(self.copy_item_to_clipboard,
val=u"%s (%s: %s)"%(tweak.name,
_('ID'),
tweak.var_names[0])))
self.context_menu.popup(self.mapToGlobal(point))
return True
def copy_item_to_clipboard(self, val):
cb = QApplication.clipboard();
cb.clear()
cb.setText(val)
def plugin_tweaks(self): def plugin_tweaks(self):
raw = self.tweaks.plugin_tweaks_string raw = self.tweaks.plugin_tweaks_string
@ -441,7 +468,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([]) app = QApplication([])
#Tweaks() #Tweaks()
#test_widget #test_widget

View File

@ -6,7 +6,7 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>' __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import urllib2, re import urllib2
from contextlib import closing from contextlib import closing
from lxml import html from lxml import html
@ -40,32 +40,27 @@ class FoylesUKStore(BasicStoreConfig, StorePlugin):
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):
url = 'http://www.foyles.co.uk/Public/Shop/Search.aspx?fFacetId=1015&searchBy=1&quick=true&term=' + urllib2.quote(query) url = 'http://ebooks.foyles.co.uk/search_for-' + urllib2.quote(query)
br = browser() br = browser()
counter = max_results counter = max_results
with closing(br.open(url, timeout=timeout)) as f: with closing(br.open(url, timeout=timeout)) as f:
doc = html.fromstring(f.read()) doc = html.fromstring(f.read())
for data in doc.xpath('//table[contains(@id, "MainContent")]/tr/td/div[contains(@class, "Item")]'): for data in doc.xpath('//div[@class="doc-item"]'):
if counter <= 0: if counter <= 0:
break break
id = ''.join(data.xpath('.//a[@class="Title"]/@href')).strip() id_ = ''.join(data.xpath('.//p[@class="doc-cover"]/a/@href')).strip()
if not id: if not id_:
continue continue
# filter out the audio books cover_url = ''.join(data.xpath('.//p[@class="doc-cover"]/a/img/@src'))
if not data.xpath('boolean(.//div[@class="Relative"]/ul/li[contains(text(), "ePub")])'): title = ''.join(data.xpath('.//span[@class="title"]/a/text()'))
continue author = ', '.join(data.xpath('.//span[@class="author"]/span[@class="author"]/text()'))
price = ''.join(data.xpath('.//span[@class="price"]/text()'))
cover_url = ''.join(data.xpath('.//a[@class="Jacket"]/img/@src')) format_ = ''.join(data.xpath('.//p[@class="doc-meta-format"]/span[last()]/text()'))
title = ''.join(data.xpath('.//a[@class="Title"]/text()')) format_, ign, drm = format_.partition(' ')
author = ', '.join(data.xpath('.//span[@class="Author"]/text()')) drm = SearchResult.DRM_LOCKED if 'DRM' in drm else SearchResult.DRM_UNLOCKED
price = ''.join(data.xpath('./ul/li[@class="Strong"]/text()'))
mo = re.search('£[\d\.]+', price)
if mo is None:
continue
price = mo.group(0)
counter -= 1 counter -= 1
@ -74,8 +69,8 @@ class FoylesUKStore(BasicStoreConfig, StorePlugin):
s.title = title.strip() s.title = title.strip()
s.author = author.strip() s.author = author.strip()
s.price = price s.price = price
s.detail_item = id s.detail_item = id_
s.drm = SearchResult.DRM_LOCKED s.drm = drm
s.formats = 'ePub' s.formats = format_
yield s yield s

View File

@ -17,7 +17,7 @@ from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt,
from calibre.gui2 import NONE, gprefs, config, error_dialog from calibre.gui2 import NONE, gprefs, config, error_dialog
from calibre.library.database2 import Tag from calibre.library.database2 import Tag
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key, lower, strcmp from calibre.utils.icu import sort_key, lower, strcmp, contractions
from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.utils.formatter import EvalFormatter from calibre.utils.formatter import EvalFormatter
@ -258,6 +258,16 @@ class TagsModel(QAbstractItemModel): # {{{
self.hidden_categories.add(cat) self.hidden_categories.add(cat)
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
conts = contractions()
if len(conts) == 0 or not tweaks['enable_multicharacters_in_tag_browser']:
self.do_contraction = False
else:
self.do_contraction = True
nconts = set()
for s in conts:
nconts.add(icu_upper(s))
self.contraction_set = frozenset(nconts)
self.db = db self.db = db
self._run_rebuild() self._run_rebuild()
self.endResetModel() self.endResetModel()
@ -335,7 +345,6 @@ class TagsModel(QAbstractItemModel): # {{{
node.is_gst = is_gst node.is_gst = is_gst
if not is_gst: if not is_gst:
node.tag.is_hierarchical = '5state' node.tag.is_hierarchical = '5state'
if not is_gst:
tree_root[p] = {} tree_root[p] = {}
tree_root = tree_root[p] tree_root = tree_root[p]
else: else:
@ -417,7 +426,14 @@ class TagsModel(QAbstractItemModel): # {{{
if not tag.sort: if not tag.sort:
c = ' ' c = ' '
else: else:
c = icu_upper(tag.sort[0]) if not self.do_contraction:
c = icu_upper(tag.sort)[0]
else:
v = icu_upper(tag.sort)
c = v[0]
for s in self.contraction_set:
if len(s) > len(c) and v.startswith(s):
c = s
if c not in chardict: if c not in chardict:
chardict[c] = [idx, idx] chardict[c] = [idx, idx]
else: else:
@ -519,7 +535,7 @@ class TagsModel(QAbstractItemModel): # {{{
# category display order is important here. The following works # category display order is important here. The following works
# only if all the non-user categories are displayed before the # only if all the non-user categories are displayed before the
# user categories # user categories
if category_is_hierarchical: if category_is_hierarchical or tag.is_hierarchical:
components = get_name_components(tag.original_name) components = get_name_components(tag.original_name)
else: else:
components = [tag.original_name] components = [tag.original_name]
@ -581,6 +597,14 @@ class TagsModel(QAbstractItemModel): # {{{
return [(t.tag.id, t.tag.original_name, t.tag.count) return [(t.tag.id, t.tag.original_name, t.tag.count)
for t in cat.child_tags() if t.tag.count > 0] for t in cat.child_tags() if t.tag.count > 0]
def is_in_user_category(self, index):
if not index.isValid():
return False
p = self.get_node(index)
while p.type != TagTreeItem.CATEGORY:
p = p.parent
return p.tag.category.startswith('@')
# Drag'n Drop {{{ # Drag'n Drop {{{
def mimeTypes(self): def mimeTypes(self):
return ["application/calibre+from_library", return ["application/calibre+from_library",
@ -646,13 +670,13 @@ class TagsModel(QAbstractItemModel): # {{{
action is Qt.CopyAction or Qt.MoveAction action is Qt.CopyAction or Qt.MoveAction
''' '''
def process_source_node(user_cats, src_parent, src_parent_is_gst, def process_source_node(user_cats, src_parent, src_parent_is_gst,
is_uc, dest_key, node): is_uc, dest_key, idx):
''' '''
Copy/move an item and all its children to the destination Copy/move an item and all its children to the destination
''' '''
copied = False copied = False
src_name = node.tag.original_name src_name = idx.tag.original_name
src_cat = node.tag.category src_cat = idx.tag.category
# 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:
@ -675,7 +699,7 @@ 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: for c in idx.children:
copied = process_source_node(user_cats, src_parent, src_parent_is_gst, copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
is_uc, dest_key, c) is_uc, dest_key, c)
return copied return copied
@ -696,11 +720,11 @@ class TagsModel(QAbstractItemModel): # {{{
if dest_key not in user_cats: if dest_key not in user_cats:
continue continue
node = self.index_for_path(path) idx = self.index_for_path(path)
if node: if idx.isValid():
process_source_node(user_cats, src_parent, src_parent_is_gst, process_source_node(user_cats, src_parent, src_parent_is_gst,
is_uc, dest_key, is_uc, dest_key,
self.get_node(node)) self.get_node(idx))
self.db.prefs.set('user_categories', user_cats) self.db.prefs.set('user_categories', user_cats)
self.refresh_required.emit() self.refresh_required.emit()
@ -1139,6 +1163,8 @@ class TagsModel(QAbstractItemModel): # {{{
return QModelIndex() return QModelIndex()
ans = self.createIndex(parent_item.row(), 0, parent_item) ans = self.createIndex(parent_item.row(), 0, parent_item)
if not ans.isValid():
return QModelIndex()
return ans return ans
def rowCount(self, parent): def rowCount(self, parent):

View File

@ -12,7 +12,8 @@ from functools import partial
from itertools import izip from itertools import izip
from PyQt4.Qt import (QStyledItemDelegate, Qt, QTreeView, pyqtSignal, QSize, from PyQt4.Qt import (QStyledItemDelegate, Qt, QTreeView, pyqtSignal, QSize,
QIcon, QApplication, QMenu, QPoint, QModelIndex, QToolTip, QCursor) QIcon, QApplication, QMenu, QPoint, QModelIndex, QToolTip, QCursor,
QDrag)
from calibre.gui2.tag_browser.model import (TagTreeItem, TAG_SEARCH_STATES, from calibre.gui2.tag_browser.model import (TagTreeItem, TAG_SEARCH_STATES,
TagsModel) TagsModel)
@ -101,6 +102,7 @@ class TagsView(QTreeView): # {{{
self.setDragEnabled(True) self.setDragEnabled(True)
self.setDragDropMode(self.DragDrop) self.setDragDropMode(self.DragDrop)
self.setDropIndicatorShown(True) self.setDropIndicatorShown(True)
self.in_drag_drop = False
self.setAutoExpandDelay(500) self.setAutoExpandDelay(500)
self.pane_is_visible = False self.pane_is_visible = False
self.search_icon = QIcon(I('search.png')) self.search_icon = QIcon(I('search.png'))
@ -232,10 +234,35 @@ class TagsView(QTreeView): # {{{
s = s if s else None s = s if s else None
self._model.set_search_restriction(s) self._model.set_search_restriction(s)
def mouseMoveEvent(self, event):
dex = self.indexAt(event.pos())
if self.in_drag_drop or not dex.isValid():
QTreeView.mouseMoveEvent(self, event)
return
# Must deal with odd case where the node being dragged is 'virtual',
# created to form a hierarchy. We can't really drag this node, but in
# addition we can't allow drag recognition to notice going over some
# other node and grabbing that one. So we set in_drag_drop to prevent
# this from happening, turning it off when the user lifts the button.
self.in_drag_drop = True
if not self._model.flags(dex) & Qt.ItemIsDragEnabled:
QTreeView.mouseMoveEvent(self, event)
return
md = self._model.mimeData([dex])
pixmap = dex.data(Qt.DecorationRole).toPyObject().pixmap(25, 25)
drag = QDrag(self)
drag.setPixmap(pixmap)
drag.setMimeData(md)
if self._model.is_in_user_category(dex):
drag.exec_(Qt.CopyAction|Qt.MoveAction, Qt.CopyAction)
else:
drag.exec_(Qt.CopyAction)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
# Swallow everything except leftButton so context menus work correctly # Swallow everything except leftButton so context menus work correctly
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton or self.in_drag_drop:
QTreeView.mouseReleaseEvent(self, event) QTreeView.mouseReleaseEvent(self, event)
self.in_drag_drop = False
def mouseDoubleClickEvent(self, event): def mouseDoubleClickEvent(self, event):
# swallow these to avoid toggling and editing at the same time # swallow these to avoid toggling and editing at the same time
@ -622,11 +649,13 @@ class TagsView(QTreeView): # {{{
path = self.model().path_for_index(ci) if self.is_visible(ci) else None path = self.model().path_for_index(ci) if self.is_visible(ci) else None
expanded_categories, state_map = self.get_state() expanded_categories, state_map = self.get_state()
self._model.rebuild_node_tree(state_map=state_map) self._model.rebuild_node_tree(state_map=state_map)
self.blockSignals(True)
for category in expanded_categories: for category in expanded_categories:
idx = self._model.index_for_category(category) idx = self._model.index_for_category(category)
if idx is not None and idx.isValid(): if idx is not None and idx.isValid():
self.expand(idx) self.expand(idx)
self.show_item_at_path(path) self.show_item_at_path(path)
self.blockSignals(False)
def show_item_at_path(self, path, box=False, def show_item_at_path(self, path, box=False,
position=QTreeView.PositionAtCenter): position=QTreeView.PositionAtCenter):
@ -639,12 +668,19 @@ class TagsView(QTreeView): # {{{
self.show_item_at_index(self._model.index_for_path(path), box=box, self.show_item_at_index(self._model.index_for_path(path), box=box,
position=position) position=position)
def expand_parent(self, idx):
# Needed otherwise Qt sometimes segfaults if the node is buried in a
# collapsed, off screen hierarchy. To be safe, we expand from the
# outermost in
p = self._model.parent(idx)
if p.isValid():
self.expand_parent(p)
self.expand(idx)
def show_item_at_index(self, idx, box=False, def show_item_at_index(self, idx, box=False,
position=QTreeView.PositionAtCenter): position=QTreeView.PositionAtCenter):
if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item: if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item:
self.expand(self._model.parent(idx)) # Needed otherwise Qt sometimes segfaults if the self.expand_parent(idx)
# node is buried in a collapsed, off
# screen hierarchy
self.setCurrentIndex(idx) self.setCurrentIndex(idx)
self.scrollTo(idx, position) self.scrollTo(idx, position)
if box: if box:

View File

@ -136,7 +136,7 @@ class Document(QWebPage): # {{{
self.max_fs_width = min(opts.max_fs_width, screen_width-50) self.max_fs_width = min(opts.max_fs_width, screen_width-50)
def fit_images(self): def fit_images(self):
if self.do_fit_images: if self.do_fit_images and not self.in_paged_mode:
self.javascript('setup_image_scaling_handlers()') self.javascript('setup_image_scaling_handlers()')
def add_window_objects(self): def add_window_objects(self):
@ -219,6 +219,7 @@ class Document(QWebPage): # {{{
if scroll_width > self.window_width: if scroll_width > self.window_width:
sz.setWidth(scroll_width+side_margin) sz.setWidth(scroll_width+side_margin)
self.setPreferredContentsSize(sz) self.setPreferredContentsSize(sz)
self.javascript('window.paged_display.fit_images()')
@property @property
def column_boundaries(self): def column_boundaries(self):

View File

@ -31,10 +31,11 @@ class JavaScriptLoader(object):
'cfi':'ebooks.oeb.display.cfi', 'cfi':'ebooks.oeb.display.cfi',
'indexing':'ebooks.oeb.display.indexing', 'indexing':'ebooks.oeb.display.indexing',
'paged':'ebooks.oeb.display.paged', 'paged':'ebooks.oeb.display.paged',
'utils':'ebooks.oeb.display.utils',
} }
ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images', ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images',
'hyphenation', 'hyphenator', 'cfi', 'indexing', 'paged') 'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged')
def __init__(self, dynamic_coffeescript=False): def __init__(self, dynamic_coffeescript=False):

View File

@ -26,8 +26,8 @@ class Printing(QObject):
for x in (Qt.Horizontal, Qt.Vertical): for x in (Qt.Horizontal, Qt.Vertical):
mf.setScrollBarPolicy(x, Qt.ScrollBarAlwaysOff) mf.setScrollBarPolicy(x, Qt.ScrollBarAlwaysOff)
self.view.loadFinished.connect(self.load_finished) self.view.loadFinished.connect(self.load_finished)
self.paged_js = compiled_coffeescript('ebooks.oeb.display.paged', self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
dynamic=False) self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
def load_finished(self, ok): def load_finished(self, ok):
self.loaded_ok = ok self.loaded_ok = ok
@ -70,6 +70,7 @@ class Printing(QObject):
document.body.style.backgroundColor = "white"; document.body.style.backgroundColor = "white";
paged_display.set_geometry(1, 0, 0, 0); paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout(); paged_display.layout();
paged_display.fit_images();
''') ''')
while True: while True:

View File

@ -15,11 +15,11 @@ from calibre.utils.config import tweaks, prefs
from calibre.utils.date import parse_date, now, UNDEFINED_DATE, clean_date_for_sort from calibre.utils.date import parse_date, now, UNDEFINED_DATE, clean_date_for_sort
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
from calibre.utils.localization import (canonicalize_lang, lang_map, get_udc, from calibre.utils.localization import (canonicalize_lang, lang_map, get_udc)
get_lang)
from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre import prints from calibre import prints
from calibre.utils.icu import primary_find
class MetadataBackup(Thread): # {{{ class MetadataBackup(Thread): # {{{
''' '''
@ -118,7 +118,15 @@ class MetadataBackup(Thread): # {{{
# }}} # }}}
### Global utility function for get_match here and in gui2/library.py ### Global utility function for get_match here and in gui2/library.py
# This is a global for performance
pref_use_primary_find_in_search = False
def set_use_primary_find_in_search(toWhat):
global pref_use_primary_find_in_search
pref_use_primary_find_in_search = toWhat
CONTAINS_MATCH = 0 CONTAINS_MATCH = 0
EQUALS_MATCH = 1 EQUALS_MATCH = 1
REGEXP_MATCH = 2 REGEXP_MATCH = 2
@ -130,8 +138,8 @@ def _match(query, value, matchkind):
else: else:
internal_match_ok = False internal_match_ok = False
for t in value: for t in value:
t = icu_lower(t)
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
t = icu_lower(t)
if (matchkind == EQUALS_MATCH): if (matchkind == EQUALS_MATCH):
if internal_match_ok: if internal_match_ok:
if query == t: if query == t:
@ -147,9 +155,13 @@ def _match(query, value, matchkind):
return True return True
elif query == t: elif query == t:
return True return True
elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I|re.UNICODE)) or ### search unanchored elif matchkind == REGEXP_MATCH:
(matchkind == CONTAINS_MATCH and query in t)): return re.search(query, t, re.I|re.UNICODE)
return True elif matchkind == CONTAINS_MATCH:
if pref_use_primary_find_in_search:
return primary_find(query, t)[0] != -1
else:
return query in t
except re.error: except re.error:
pass pass
return False return False
@ -226,10 +238,6 @@ class ResultCache(SearchQueryParser): # {{{
''' '''
def __init__(self, FIELD_MAP, field_metadata, db_prefs=None): def __init__(self, FIELD_MAP, field_metadata, db_prefs=None):
self.FIELD_MAP = FIELD_MAP self.FIELD_MAP = FIELD_MAP
l = get_lang()
asciize_author_names = l and l.lower() in ('en', 'eng')
if not asciize_author_names:
self.ascii_name = lambda x: False
self.db_prefs = db_prefs self.db_prefs = db_prefs
self.composites = {} self.composites = {}
self.udc = get_udc() self.udc = get_udc()
@ -249,6 +257,9 @@ class ResultCache(SearchQueryParser): # {{{
SearchQueryParser.__init__(self, self.all_search_locations, optimize=True) SearchQueryParser.__init__(self, self.all_search_locations, optimize=True)
self.build_date_relop_dict() self.build_date_relop_dict()
self.build_numeric_relop_dict() self.build_numeric_relop_dict()
# Do this here so the var get updated when a library changes
global pref_use_primary_find_in_search
pref_use_primary_find_in_search = prefs['use_primary_find_in_search']
def break_cycles(self): def break_cycles(self):
self._data = self.field_metadata = self.FIELD_MAP = \ self._data = self.field_metadata = self.FIELD_MAP = \
@ -279,15 +290,6 @@ class ResultCache(SearchQueryParser): # {{{
# Search functions {{{ # Search functions {{{
def ascii_name(self, name):
try:
ans = self.udc.decode(name)
if ans == name:
ans = False
except:
ans = False
return ans
def universal_set(self): def universal_set(self):
return set([i[0] for i in self._data if i is not None]) return set([i[0] for i in self._data if i is not None])
@ -649,8 +651,10 @@ class ResultCache(SearchQueryParser): # {{{
else: else:
invert = False invert = False
for loc in location: for loc in location:
matches |= self.get_matches(loc, query, m = self.get_matches(loc, query,
candidates=candidates, allow_recursion=False) candidates=candidates, allow_recursion=False)
matches |= m
candidates -= m
if invert: if invert:
matches = self.universal_set() - matches matches = self.universal_set() - matches
return matches return matches
@ -667,8 +671,10 @@ class ResultCache(SearchQueryParser): # {{{
if terms: if terms:
for l in terms: for l in terms:
try: try:
matches |= self.get_matches(l, query, m = self.get_matches(l, query,
candidates=candidates, allow_recursion=allow_recursion) candidates=candidates, allow_recursion=allow_recursion)
matches |= m
candidates -= m
except: except:
pass pass
return matches return matches
@ -761,8 +767,6 @@ class ResultCache(SearchQueryParser): # {{{
else: else:
q = query q = query
au_loc = self.FIELD_MAP['authors']
for id_ in candidates: for id_ in candidates:
item = self._data[id_] item = self._data[id_]
if item is None: continue if item is None: continue
@ -805,9 +809,6 @@ class ResultCache(SearchQueryParser): # {{{
if loc not in exclude_fields: # time for text matching if loc not in exclude_fields: # time for text matching
if is_multiple_cols[loc] is not None: if is_multiple_cols[loc] is not None:
vals = [v.strip() for v in item[loc].split(is_multiple_cols[loc])] vals = [v.strip() for v in item[loc].split(is_multiple_cols[loc])]
if loc == au_loc:
vals += filter(None, map(self.ascii_name,
vals))
else: else:
vals = [item[loc]] ### make into list to make _match happy vals = [item[loc]] ### make into list to make _match happy
if _match(q, vals, matchkind): if _match(q, vals, matchkind):

View File

@ -999,6 +999,55 @@ def command_saved_searches(args, dbpath):
return 0 return 0
def backup_metadata_option_parser():
parser = get_parser(_('''\
%prog backup_metadata [options]
Backup the metadata stored in the database into individual OPF files in each
books directory. This normally happens automatically, but you can run this
command to force re-generation of the OPF files, with the --all option.
Note that there is normally no need to do this, as the OPF files are backed up
automatically, every time metadata is changed.
'''))
parser.add_option('--all', default=False, action='store_true',
help=_('Normally, this command only operates on books that have'
' out of date OPF files. This option makes it operate on all'
' books.'))
return parser
class BackupProgress(object):
def __init__(self):
self.total = 0
self.count = 0
def __call__(self, book_id, mi, ok):
if mi is True:
self.total = book_id
else:
self.count += 1
prints(u'%.1f%% %s - %s'%((self.count*100)/float(self.total),
book_id, mi.title))
def command_backup_metadata(args, dbpath):
parser = backup_metadata_option_parser()
opts, args = parser.parse_args(args)
if len(args) != 0:
parser.print_help()
return 1
if opts.library_path is not None:
dbpath = opts.library_path
if isbytestring(dbpath):
dbpath = dbpath.decode(preferred_encoding)
db = LibraryDatabase2(dbpath)
book_ids = None
if opts.all:
book_ids = db.all_ids()
db.dump_metadata(book_ids=book_ids, callback=BackupProgress())
def check_library_option_parser(): def check_library_option_parser():
from calibre.library.check_library import CHECKS from calibre.library.check_library import CHECKS
parser = get_parser(_('''\ parser = get_parser(_('''\
@ -1275,7 +1324,7 @@ COMMANDS = ('list', 'add', 'remove', 'add_format', 'remove_format',
'show_metadata', 'set_metadata', 'export', 'catalog', 'show_metadata', 'set_metadata', 'export', 'catalog',
'saved_searches', 'add_custom_column', 'custom_columns', 'saved_searches', 'add_custom_column', 'custom_columns',
'remove_custom_column', 'set_custom', 'restore_database', 'remove_custom_column', 'set_custom', 'restore_database',
'check_library', 'list_categories') 'check_library', 'list_categories', 'backup_metadata')
def option_parser(): def option_parser():

View File

@ -808,18 +808,30 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
pass pass
def dump_metadata(self, book_ids=None, remove_from_dirtied=True, def dump_metadata(self, book_ids=None, remove_from_dirtied=True,
commit=True): commit=True, callback=None):
''' '''
Write metadata for each record to an individual OPF file Write metadata for each record to an individual OPF file. If callback
is not None, it is called once at the start with the number of book_ids
being processed. And once for every book_id, with arguments (book_id,
mi, ok).
''' '''
if book_ids is None: if book_ids is None:
book_ids = [x[0] for x in self.conn.get( book_ids = [x[0] for x in self.conn.get(
'SELECT book FROM metadata_dirtied', all=True)] 'SELECT book FROM metadata_dirtied', all=True)]
if callback is not None:
book_ids = tuple(book_ids)
callback(len(book_ids), True, False)
for book_id in book_ids: for book_id in book_ids:
if not self.data.has_id(book_id): if not self.data.has_id(book_id):
if callback is not None:
callback(book_id, None, False)
continue continue
path, mi, sequence = self.get_metadata_for_dump(book_id) path, mi, sequence = self.get_metadata_for_dump(book_id)
if path is None: if path is None:
if callback is not None:
callback(book_id, mi, False)
continue continue
try: try:
raw = metadata_to_opf(mi) raw = metadata_to_opf(mi)
@ -829,6 +841,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.clear_dirtied(book_id, sequence) self.clear_dirtied(book_id, sequence)
except: except:
pass pass
if callback is not None:
callback(book_id, mi, True)
if commit: if commit:
self.conn.commit() self.conn.commit()
@ -1411,7 +1425,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
opath = self.format_abspath(book_id, nfmt, index_is_id=True) opath = self.format_abspath(book_id, nfmt, index_is_id=True)
return fmt if opath is None else nfmt return fmt if opath is None else nfmt
def delete_book(self, id, notify=True, commit=True, permanent=False): def delete_book(self, id, notify=True, commit=True, permanent=False,
do_clean=True):
''' '''
Removes book from the result cache and the underlying database. Removes book from the result cache and the underlying database.
If you set commit to False, you must call clean() manually afterwards If you set commit to False, you must call clean() manually afterwards
@ -1428,6 +1443,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM books WHERE id=?', (id,)) self.conn.execute('DELETE FROM books WHERE id=?', (id,))
if commit: if commit:
self.conn.commit() self.conn.commit()
if do_clean:
self.clean() self.clean()
self.data.books_deleted([id]) self.data.books_deleted([id])
if notify: if notify:

View File

@ -97,6 +97,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
search_box = build_search_box(num, search, sort, order, prefix) search_box = build_search_box(num, search, sort, order, prefix)
navigation = build_navigation(start, num, total, prefix+url_base) navigation = build_navigation(start, num, total, prefix+url_base)
navigation2 = build_navigation(start, num, total, prefix+url_base)
bookt = TABLE(id='listing') bookt = TABLE(id='listing')
body = BODY( body = BODY(
@ -104,7 +105,9 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
search_box, search_box,
navigation, navigation,
HR(CLASS('spacer')), HR(CLASS('spacer')),
bookt bookt,
HR(CLASS('spacer')),
navigation2
) )
# Book list {{{ # Book list {{{
@ -155,7 +158,6 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
bookt.append(TR(thumbnail, data)) bookt.append(TR(thumbnail, data))
# }}} # }}}
body.append(HR())
body.append(DIV( body.append(DIV(
A(_('Switch to the full interface (non-mobile interface)'), A(_('Switch to the full interface (non-mobile interface)'),
href=prefix+"/browse", href=prefix+"/browse",

View File

@ -354,8 +354,8 @@ class PostInstall:
check_call('xdg-icon-resource install --noupdate --context mimetypes --size 128 calibre-lrf.png text-lrs', shell=True) check_call('xdg-icon-resource install --noupdate --context mimetypes --size 128 calibre-lrf.png text-lrs', shell=True)
self.icon_resources.append(('mimetypes', 'application-lrs', self.icon_resources.append(('mimetypes', 'application-lrs',
'128')) '128'))
render_img('lt.png', 'calibre-gui.png') render_img('lt.png', 'calibre-gui.png', width=256, height=256)
check_call('xdg-icon-resource install --noupdate --size 128 calibre-gui.png calibre-gui', shell=True) check_call('xdg-icon-resource install --noupdate --size 256 calibre-gui.png calibre-gui', shell=True)
self.icon_resources.append(('apps', 'calibre-gui', '128')) self.icon_resources.append(('apps', 'calibre-gui', '128'))
render_img('viewer.png', 'calibre-viewer.png') render_img('viewer.png', 'calibre-viewer.png')
check_call('xdg-icon-resource install --size 128 calibre-viewer.png calibre-viewer', shell=True) check_call('xdg-icon-resource install --size 128 calibre-viewer.png calibre-viewer', shell=True)

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

Some files were not shown because too many files have changed in this diff Show More