mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
0.8.63
This commit is contained in:
commit
83bea25fe5
@ -19,6 +19,57 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
- version: 0.8.63
|
||||
date: 2012-08-02
|
||||
|
||||
new features:
|
||||
- title: "E-book viewer: Allow quick saving and loading of viewer settings as 'themes'."
|
||||
tickets: [1024611]
|
||||
|
||||
- title: "Ebook-viewer: Add a restore defaults button to the viewer preferences dialog"
|
||||
|
||||
- title: "E-book viewer: Add simple settings for text and background colors"
|
||||
|
||||
- title: "Add an entry to save to disk when right clicking a format in the book details panel"
|
||||
|
||||
- title: "ODT metadata: Read first image as the metadata cover from ODT files. Also allow ODT authors to set custom properties for extended metadata."
|
||||
|
||||
- title: "E-book viewer and PDF Output: Resize images that are longer than the page to fit onto a single page"
|
||||
|
||||
bug fixes:
|
||||
- title: "KF8 Output: Fix bug where some calibre generated KF8 files would cause the Amazon KF8 viewer on the Touch to go to into an infinite loop when using the next page function"
|
||||
tickets: [1026421]
|
||||
|
||||
- title: "News download: Add support for <img> tags that link to SVG images."
|
||||
tickets: [1031553]
|
||||
|
||||
- title: "Update podofo to 0.9.1 in all binary builds, to fix corruption of some PDFs when updating metadata."
|
||||
tickets: [1031086]
|
||||
|
||||
- title: "Catalog generation: Handle authors whose last name is a number."
|
||||
|
||||
- title: "KF8 Input: Handle html entities in the NCX toc entries correctly"
|
||||
|
||||
- title: "Fix a calibre crash that affected some windows installs"
|
||||
tickets: [1030234]
|
||||
|
||||
- title: "MOBI Output: Normalize unicode strings before writing to file, to workaround lack of support for non-normal unicode in Amazon's MOBI renderer."
|
||||
tickets: [1029825]
|
||||
|
||||
- title: "EPUB Input: Handle files that have duplicate entries in the spine"
|
||||
|
||||
- title: "Fix regression in Kobo driver that caused the on device column to not be updated after deleting books"
|
||||
|
||||
new recipes:
|
||||
- title: Dziennik Polski
|
||||
author: Gregorz Maj
|
||||
|
||||
- title: High Country Blogs
|
||||
author: Armin Geller
|
||||
|
||||
- title: Philosophy Now
|
||||
author: Rick Shang
|
||||
|
||||
- version: 0.8.62
|
||||
date: 2012-07-27
|
||||
|
||||
|
@ -710,3 +710,35 @@ EPUB from the ZIP file are::
|
||||
|
||||
Note that because this file explores the potential of EPUB, most of the advanced formatting is not going to work on readers less capable than |app|'s built-in EPUB viewer.
|
||||
|
||||
|
||||
Convert ODT documents
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|app| can directly convert ODT (OpenDocument Text) files. You should use styles to format your document and minimize the use of direct formatting.
|
||||
When inserting images into your document you need to anchor them to the paragraph, images anchored to a page will all end up in the front of the conversion.
|
||||
|
||||
To enable automatic detection of chapters, you need to mark them with the build-in styles called 'Heading 1', 'Heading 2', ..., 'Heading 6' ('Heading 1' equates to the HTML tag <h1>, 'Heading 2' to <h2> etc). When you convert in |app| you can enter which style you used into the 'Detect chapters at' box. Example:
|
||||
|
||||
* If you mark Chapters with style 'Heading 2', you have to set the 'Detect chapters at' box to ``//h:h2``
|
||||
* For a nested TOC with Sections marked with 'Heading 2' and the Chapters marked with 'Heading 3' you need to enter ``//h:h2|//h:h3``. On the Convert - TOC page set the 'Level 1 TOC' box to ``//h:h2`` and the 'Level 2 TOC' box to ``//h:h3``.
|
||||
|
||||
Well-known document properties (Title, Keywords, Description, Creator) are recognized and |app| will use the first image (not to small, and with good aspect-ratio) as the cover image.
|
||||
|
||||
There is also an advanced property conversion mode, which is activated by setting the custom property ``opf.metadata`` ('Yes or No' type) to Yes in your ODT document (File->Properties->Custom Properties).
|
||||
If this property is detected by |app|, the following custom properties are recognized (``opf.authors`` overrides document creator)::
|
||||
|
||||
opf.titlesort
|
||||
opf.authors
|
||||
opf.authorsort
|
||||
opf.publisher
|
||||
opf.pubdate
|
||||
opf.isbn
|
||||
opf.language
|
||||
opf.series
|
||||
opf.seriesindex
|
||||
|
||||
In addition to this, you can specify the picture to use as the cover by naming it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no picture with this name is found, the 'smart' method is used.
|
||||
As the cover detection might result in double covers in certain output formats, the process will remove the paragraph (only if the only content is the cover!) from the document. But this works only with the named picture!
|
||||
|
||||
To disable cover detection you can set the custom property ``opf.nocover`` ('Yes or No' type) to Yes in advanced mode.
|
||||
|
||||
|
@ -152,14 +152,17 @@ calibre is the directory that contains the src and resources sub-directories. En
|
||||
The next step is to create a bash script that will set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path of the src directory when running calibre in debug mode.
|
||||
|
||||
Create a plain text file::
|
||||
|
||||
#!/bin/sh
|
||||
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
|
||||
calibre-debug -g
|
||||
|
||||
Save this file as ``/usr/bin/calibre-develop``, then set its permissions so that it can be executed::
|
||||
|
||||
chmod +x /usr/bin/calibre-develop
|
||||
|
||||
Once you have done this, type::
|
||||
Once you have done this, run::
|
||||
|
||||
calibre-develop
|
||||
|
||||
You should see some diagnostic information in the Terminal window as calibre
|
||||
|
@ -30,7 +30,7 @@ Lets pick a couple of feeds that look interesting:
|
||||
#. Business Travel: http://feeds.portfolio.com/portfolio/businesstravel
|
||||
#. Tech Observer: http://feeds.portfolio.com/portfolio/thetechobserver
|
||||
|
||||
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 ebook, 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 ebook, you should right 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
|
||||
:align: center
|
||||
|
132
recipes/dziennik_polski.recipe
Normal file
132
recipes/dziennik_polski.recipe
Normal file
@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__license__='GPL v3'
|
||||
__author__='grzegorz.maj@dziennik.krakow.pl>'
|
||||
|
||||
'''
|
||||
http://dziennikpolski24.pl
|
||||
Author: grzegorz.maj@dziennik.krakow.pl
|
||||
'''
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class DziennikPolski24(BasicNewsRecipe):
|
||||
|
||||
title=u'Dziennik Polski'
|
||||
publisher=u'Grupa Polskapresse'
|
||||
|
||||
__author__='grzegorz.maj'
|
||||
description=u'Wiadomości z wydania Dziennika Polskiego'
|
||||
oldest_article=1
|
||||
max_articles_per_feed=50
|
||||
needs_subscription=True
|
||||
|
||||
remove_javascript=True
|
||||
no_stylesheets=True
|
||||
use_embedded_content=False
|
||||
remove_empty_feeds=True
|
||||
extra_css='.date{margin-top: 4em;} .logo_author{margin-left:0.5em;}'
|
||||
|
||||
publication_type='newspaper'
|
||||
cover_url='http://www.dziennikpolski24.pl/_p/images/logoDP24-b.gif'
|
||||
INDEX='http://dziennikpolski24.pl/'
|
||||
|
||||
encoding='utf-8'
|
||||
language='pl'
|
||||
|
||||
keep_only_tags=[
|
||||
|
||||
dict(name = 'div', attrs = {'class':['toolbar']})
|
||||
, dict(name = 'h1')
|
||||
, dict(name = 'h2', attrs = {'class':['teaser']})
|
||||
, dict(name = 'div', attrs = {'class':['picture']})
|
||||
, dict(name = 'div', attrs = {'id':['showContent']})
|
||||
, dict(name = 'div', attrs = {'class':['paging']})
|
||||
, dict(name = 'div', attrs = {'class':['wykupTresc']})
|
||||
]
|
||||
|
||||
remove_tags=[
|
||||
|
||||
]
|
||||
|
||||
feeds=[
|
||||
(u'Kraj', u'http://www.dziennikpolski24.pl/rss/feed/1151')
|
||||
, (u'Świat', u'http://www.dziennikpolski24.pl/rss/feed/1153')
|
||||
, (u'Gospodarka', u'http://www.dziennikpolski24.pl/rss/feed/1154')
|
||||
, (u'Małopolska', u'http://www.dziennikpolski24.pl/rss/feed/1155')
|
||||
, (u'Kultura', u'http://www.dziennikpolski24.pl/rss/feed/1156')
|
||||
, (u'Opinie', u'http://www.dziennikpolski24.pl/rss/feed/1158')
|
||||
, (u'Kronika Nowohucka', u'http://www.dziennikpolski24.pl/rss/feed/1656')
|
||||
, (u'Na bieżąco', u'http://www.dziennikpolski24.pl/rss/feed/1543')
|
||||
, (u'Londyn 2012', u'http://www.dziennikpolski24.pl/rss/feed/2545')
|
||||
, (u'Piłka nożna', u'http://www.dziennikpolski24.pl/rss/feed/2196')
|
||||
, (u'Siatkówka', u'http://www.dziennikpolski24.pl/rss/feed/2197')
|
||||
, (u'Koszykówka', u'http://www.dziennikpolski24.pl/rss/feed/2198')
|
||||
, (u'Tenis', u'http://www.dziennikpolski24.pl/rss/feed/2199')
|
||||
, (u'Formuła 1', u'http://www.dziennikpolski24.pl/rss/feed/2203')
|
||||
, (u'Lekkoatletyka', u'http://www.dziennikpolski24.pl/rss/feed/2204')
|
||||
, (u'Żużel', u'http://www.dziennikpolski24.pl/rss/feed/2200')
|
||||
, (u'Sporty motorowe', u'http://www.dziennikpolski24.pl/rss/feed/2206')
|
||||
, (u'Publicystyka sportowa', u'http://www.dziennikpolski24.pl/rss/feed/2201')
|
||||
, (u'Kolarstwo', u'http://www.dziennikpolski24.pl/rss/feed/2205')
|
||||
, (u'Inne', u'http://www.dziennikpolski24.pl/rss/feed/2202')
|
||||
, (u'Miasto Kraków', u'http://www.dziennikpolski24.pl/rss/feed/1784')
|
||||
, (u'Region nowosądecki', u'http://www.dziennikpolski24.pl/rss/feed/1795')
|
||||
, (u'Region Małopolski Zachodniej', u'http://www.dziennikpolski24.pl/rss/feed/1793')
|
||||
, (u'Region tarnowski', u'http://www.dziennikpolski24.pl/rss/feed/1797')
|
||||
, (u'Region podhalański', u'http://www.dziennikpolski24.pl/rss/feed/1789')
|
||||
, (u'Region olkuski', u'http://www.dziennikpolski24.pl/rss/feed/1670')
|
||||
, (u'Region miechowski', u'http://www.dziennikpolski24.pl/rss/feed/1806')
|
||||
, (u'Region podkrakowski', u'http://www.dziennikpolski24.pl/rss/feed/1787')
|
||||
, (u'Region proszowicki', u'http://www.dziennikpolski24.pl/rss/feed/1804')
|
||||
, (u'Region wielicki', u'http://www.dziennikpolski24.pl/rss/feed/1802')
|
||||
, (u'Region podbeskidzki', u'http://www.dziennikpolski24.pl/rss/feed/1791')
|
||||
, (u'Region myślenicki', u'http://www.dziennikpolski24.pl/rss/feed/1800')
|
||||
, (u'Autosalon', u'http://www.dziennikpolski24.pl/rss/feed/1294')
|
||||
, (u'Kariera', u'http://www.dziennikpolski24.pl/rss/feed/1289')
|
||||
, (u'Przegląd nieruchomości', u'http://www.dziennikpolski24.pl/rss/feed/1281')
|
||||
, (u'Magnes', u'http://www.dziennikpolski24.pl/rss/feed/1283')
|
||||
, (u'Magazyn Piątek', u'http://www.dziennikpolski24.pl/rss/feed/1293')
|
||||
, (u'Pejzaż rodzinny', u'http://www.dziennikpolski24.pl/rss/feed/1274')
|
||||
, (u'Podróże', u'http://www.dziennikpolski24.pl/rss/feed/1275')
|
||||
, (u'Konsument', u'http://www.dziennikpolski24.pl/rss/feed/1288')
|
||||
]
|
||||
|
||||
def append_page(self, soup, appendtag):
|
||||
loop=False
|
||||
tag=soup.find('div', attrs = {'class':'paging'})
|
||||
if tag:
|
||||
loop=True
|
||||
li_nks=tag.findAll('li')
|
||||
appendtag.find('div', attrs = {'class':'paging'}).extract()
|
||||
if appendtag.find('ul', attrs = {'class':'menuf'}):
|
||||
appendtag.find('ul', attrs = {'class':'menuf'}).extract()
|
||||
while loop:
|
||||
loop=False
|
||||
for li_nk in li_nks:
|
||||
link_tag=li_nk.contents[0].contents[0].string
|
||||
if u'następna' in link_tag:
|
||||
soup2=self.index_to_soup(self.INDEX+li_nk.contents[0]['href'])
|
||||
if soup2.find('div', attrs = {'id':'showContent'}):
|
||||
pagetext=soup2.find('div', attrs = {'id':'showContent'})
|
||||
pos=len(appendtag.contents)
|
||||
appendtag.insert(pos, pagetext)
|
||||
if soup2.find('div', attrs = {'class':'rightbar'}):
|
||||
pagecont=soup2.find('div', attrs = {'class':'rightbar'})
|
||||
tag=pagecont.find('div', attrs = {'class':'paging'})
|
||||
li_nks=tag.findAll('li')
|
||||
loop=True
|
||||
|
||||
def get_browser(self):
|
||||
br=BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('http://www.dziennikpolski24.pl/pl/moje-konto/950606-loguj.html')
|
||||
br.select_form(nr = 1)
|
||||
br["user_login[login]"]=self.username
|
||||
br['user_login[pass]']=self.password
|
||||
br.submit()
|
||||
return br
|
||||
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
self.append_page(soup, soup.body)
|
||||
return soup
|
||||
|
44
recipes/high_country_blogs.recipe
Normal file
44
recipes/high_country_blogs.recipe
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>, Armin Geller'
|
||||
|
||||
'''
|
||||
Fetch High Country News - Blogs
|
||||
'''
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class HighCountryNewsBlogs(BasicNewsRecipe):
|
||||
|
||||
title = u'High Country News - Blogs'
|
||||
description = u'High Country News - Blogs (RSS Version)'
|
||||
__author__ = 'Armin Geller' # 2012-08-01
|
||||
publisher = 'High Country News'
|
||||
category = 'news, politics, Germany'
|
||||
timefmt = ' [%a, %d %b %Y]'
|
||||
language = 'en'
|
||||
encoding = 'UTF-8'
|
||||
publication_type = 'newspaper'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
auto_cleanup = True
|
||||
remove_javascript = True
|
||||
use_embedded_content = False
|
||||
masthead_url = 'http://www.hcn.org/logo.jpg'
|
||||
cover_source = 'http://www.hcn.org'
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_source_soup = self.index_to_soup(self.cover_source)
|
||||
preview_image_div = cover_source_soup.find(attrs={'class':' portaltype-Plone Site content--hcn template-homepage_view'})
|
||||
return preview_image_div.div.img['src']
|
||||
|
||||
feeds = [
|
||||
(u'From the Blogs', u'http://feeds.feedburner.com/hcn/FromTheBlogs?format=xml'),
|
||||
|
||||
(u'Heard around the West', u'http://feeds.feedburner.com/hcn/heard?format=xml'),
|
||||
(u'The GOAT Blog', u'http://feeds.feedburner.com/hcn/goat?format=xml'),
|
||||
(u'The Range', u'http://feeds.feedburner.com/hcn/range?format=xml'),
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return url
|
||||
|
BIN
recipes/icons/dziennik_polski.png
Normal file
BIN
recipes/icons/dziennik_polski.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 757 B |
75
recipes/phillosophy_now.recipe
Normal file
75
recipes/phillosophy_now.recipe
Normal file
@ -0,0 +1,75 @@
|
||||
import re
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
from collections import OrderedDict
|
||||
|
||||
class PhilosophyNow(BasicNewsRecipe):
|
||||
|
||||
title = 'Philosophy Now'
|
||||
__author__ = 'Rick Shang'
|
||||
|
||||
description = '''Philosophy Now is a lively magazine for everyone
|
||||
interested in ideas. It isn't afraid to tackle all the major questions of
|
||||
life, the universe and everything. Published every two months, it tries to
|
||||
corrupt innocent citizens by convincing them that philosophy can be
|
||||
exciting, worthwhile and comprehensible, and also to provide some enjoyable
|
||||
reading matter for those already ensnared by the muse, such as philosophy
|
||||
students and academics.'''
|
||||
language = 'en'
|
||||
category = 'news'
|
||||
encoding = 'UTF-8'
|
||||
|
||||
keep_only_tags = [dict(attrs={'id':'fullMainColumn'})]
|
||||
remove_tags = [dict(attrs={'class':'articleTools'})]
|
||||
no_javascript = True
|
||||
no_stylesheets = True
|
||||
needs_subscription = True
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
br.open('https://philosophynow.org/auth/login')
|
||||
br.select_form(nr = 1)
|
||||
br['username'] = self.username
|
||||
br['password'] = self.password
|
||||
br.submit()
|
||||
return br
|
||||
|
||||
def parse_index(self):
|
||||
#Go to the issue
|
||||
soup0 = self.index_to_soup('http://philosophynow.org/')
|
||||
issue = soup0.find('div',attrs={'id':'navColumn'})
|
||||
|
||||
#Find date & cover
|
||||
cover = issue.find('div', attrs={'id':'cover'})
|
||||
date = self.tag_to_string(cover.find('h3')).strip()
|
||||
self.timefmt = u' [%s]'%date
|
||||
img=cover.find('img',src=True)['src']
|
||||
self.cover_url = 'http://philosophynow.org' + re.sub('medium','large',img)
|
||||
issuenum = re.sub('/media/images/covers/medium/issue','',img)
|
||||
issuenum = re.sub('.jpg','',issuenum)
|
||||
|
||||
#Go to the main body
|
||||
current_issue_url = 'http://philosophynow.org/issues/' + issuenum
|
||||
soup = self.index_to_soup(current_issue_url)
|
||||
div = soup.find ('div', attrs={'class':'articlesColumn'})
|
||||
|
||||
feeds = OrderedDict()
|
||||
|
||||
for post in div.findAll('h3'):
|
||||
articles = []
|
||||
a=post.find('a',href=True)
|
||||
if a is not None:
|
||||
url="http://philosophynow.org" + a['href']
|
||||
title=self.tag_to_string(a).strip()
|
||||
s=post.findPrevious('h4')
|
||||
section_title = self.tag_to_string(s).strip()
|
||||
d=post.findNext('p')
|
||||
desc = self.tag_to_string(d).strip()
|
||||
articles.append({'title':title, 'url':url, 'description':desc, 'date':''})
|
||||
|
||||
if articles:
|
||||
if section_title not in feeds:
|
||||
feeds[section_title] = []
|
||||
feeds[section_title] += articles
|
||||
ans = [(key, val) for key, val in feeds.iteritems()]
|
||||
return ans
|
||||
|
Binary file not shown.
@ -506,16 +506,6 @@ compile_gpm_templates = True
|
||||
# default_tweak_format = 'remember'
|
||||
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 = False
|
||||
enable_multicharacters_in_tag_browser = True
|
||||
|
||||
#: Do not preselect a completion when editing authors/tags/series/etc.
|
||||
# This means that you can make changes and press Enter and your changes will
|
||||
# not be overwritten by a matching completion. However, if you wish to use the
|
||||
|
BIN
resources/images/devices/galaxy_s3.png
Normal file
BIN
resources/images/devices/galaxy_s3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
@ -140,7 +140,7 @@ extensions = [
|
||||
['calibre/utils/podofo/podofo.cpp'],
|
||||
libraries=['podofo'],
|
||||
lib_dirs=[podofo_lib],
|
||||
inc_dirs=[podofo_inc],
|
||||
inc_dirs=[podofo_inc, os.path.dirname(podofo_inc)],
|
||||
optional=True,
|
||||
error=podofo_error),
|
||||
|
||||
|
@ -28,7 +28,10 @@ def is_vm_running(name):
|
||||
pat = '/%s/'%name
|
||||
pids= [pid for pid in os.listdir('/proc') if pid.isdigit()]
|
||||
for pid in pids:
|
||||
try:
|
||||
cmdline = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read()
|
||||
except IOError:
|
||||
continue # file went away
|
||||
if 'vmware-vmx' in cmdline and pat in cmdline:
|
||||
return True
|
||||
return False
|
||||
|
@ -32,7 +32,7 @@ binary_includes = [
|
||||
'/usr/lib/libunrar.so',
|
||||
'/usr/lib/libsqlite3.so.0',
|
||||
'/usr/lib/libmng.so.1',
|
||||
'/usr/lib/libpodofo.so.0.8.4',
|
||||
'/usr/lib/libpodofo.so.0.9.1',
|
||||
'/lib/libz.so.1',
|
||||
'/usr/lib/libtiff.so.5',
|
||||
'/lib/libbz2.so.1',
|
||||
|
@ -243,9 +243,6 @@ class Py2App(object):
|
||||
@flush
|
||||
def get_local_dependencies(self, path_to_lib):
|
||||
for x in self.get_dependencies(path_to_lib):
|
||||
if x.startswith('libpodofo'):
|
||||
yield x, x
|
||||
continue
|
||||
for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/',
|
||||
'/opt/local/lib/',
|
||||
SW+'/python/Python.framework/', SW+'/freetype/lib/'):
|
||||
@ -330,10 +327,6 @@ class Py2App(object):
|
||||
for f in glob.glob('src/calibre/plugins/*.so'):
|
||||
shutil.copy2(f, dest)
|
||||
self.fix_dependencies_in_lib(join(dest, basename(f)))
|
||||
if 'podofo' in f:
|
||||
self.change_dep('libpodofo.0.8.4.dylib',
|
||||
self.FID+'/'+'libpodofo.0.8.4.dylib', join(dest, basename(f)))
|
||||
|
||||
|
||||
@flush
|
||||
def create_plist(self):
|
||||
@ -380,7 +373,7 @@ class Py2App(object):
|
||||
@flush
|
||||
def add_podofo(self):
|
||||
info('\nAdding PoDoFo')
|
||||
pdf = join(SW, 'lib', 'libpodofo.0.8.4.dylib')
|
||||
pdf = join(SW, 'lib', 'libpodofo.0.9.1.dylib')
|
||||
self.install_dylib(pdf)
|
||||
|
||||
@flush
|
||||
|
@ -322,24 +322,7 @@ cp build/podofo-*/build/src/Release/podofo.exp lib/
|
||||
cp build/podofo-*/build/podofo_config.h include/podofo/
|
||||
cp -r build/podofo-*/src/* include/podofo/
|
||||
|
||||
You have to use >=0.8.2
|
||||
|
||||
The following patch (against -r1269) was required to get it to compile:
|
||||
|
||||
|
||||
Index: src/PdfFiltersPrivate.cpp
|
||||
===================================================================
|
||||
--- src/PdfFiltersPrivate.cpp (revision 1261)
|
||||
+++ src/PdfFiltersPrivate.cpp (working copy)
|
||||
@@ -1019,7 +1019,7 @@
|
||||
/*
|
||||
* Prepare for input from a memory buffer.
|
||||
*/
|
||||
-GLOBAL(void)
|
||||
+void
|
||||
jpeg_memory_src (j_decompress_ptr cinfo, const JOCTET * buffer, size_t bufsize)
|
||||
{
|
||||
my_src_ptr src;
|
||||
You have to use >=0.9.1
|
||||
|
||||
|
||||
ImageMagick
|
||||
|
@ -12,14 +12,14 @@ msgstr ""
|
||||
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
|
||||
"devel@lists.alioth.debian.org>\n"
|
||||
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
|
||||
"PO-Revision-Date: 2012-05-03 16:09+0000\n"
|
||||
"Last-Translator: Dídac Rios <didac@niorcs.com>\n"
|
||||
"PO-Revision-Date: 2012-07-23 10:54+0000\n"
|
||||
"Last-Translator: jmontane <Unknown>\n"
|
||||
"Language-Team: Catalan <linux@softcatala.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-05-04 04:47+0000\n"
|
||||
"X-Generator: Launchpad (build 15195)\n"
|
||||
"X-Launchpad-Export-Date: 2012-07-24 04:52+0000\n"
|
||||
"X-Generator: Launchpad (build 15668)\n"
|
||||
"Language: ca\n"
|
||||
|
||||
#. name for aaa
|
||||
@ -5612,7 +5612,7 @@ msgstr "Caixubi"
|
||||
|
||||
#. name for csc
|
||||
msgid "Catalan Sign Language"
|
||||
msgstr "Llenguatge de signes català"
|
||||
msgstr "Llengua de signes catalana"
|
||||
|
||||
#. name for csd
|
||||
msgid "Chiangmai Sign Language"
|
||||
@ -27348,7 +27348,7 @@ msgstr "Llenguatge de signes veneçolà"
|
||||
|
||||
#. name for vsv
|
||||
msgid "Valencian Sign Language"
|
||||
msgstr "Llenguatge de signes valencià"
|
||||
msgstr "Llengua de signes valenciana"
|
||||
|
||||
#. name for vto
|
||||
msgid "Vitou"
|
||||
|
@ -18,14 +18,14 @@ msgstr ""
|
||||
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
|
||||
"devel@lists.alioth.debian.org>\n"
|
||||
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
|
||||
"PO-Revision-Date: 2012-06-10 11:16+0000\n"
|
||||
"PO-Revision-Date: 2012-07-29 15:29+0000\n"
|
||||
"Last-Translator: SimonFS <simonschuette@arcor.de>\n"
|
||||
"Language-Team: German <debian-l10n-german@lists.debian.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-06-11 04:46+0000\n"
|
||||
"X-Generator: Launchpad (build 15376)\n"
|
||||
"X-Launchpad-Export-Date: 2012-07-30 04:54+0000\n"
|
||||
"X-Generator: Launchpad (build 15702)\n"
|
||||
"Language: de\n"
|
||||
|
||||
#. name for aaa
|
||||
@ -114,11 +114,11 @@ msgstr "Solong"
|
||||
|
||||
#. name for aax
|
||||
msgid "Mandobo Atas"
|
||||
msgstr ""
|
||||
msgstr "Mandobo Atas"
|
||||
|
||||
#. name for aaz
|
||||
msgid "Amarasi"
|
||||
msgstr ""
|
||||
msgstr "Amarasi"
|
||||
|
||||
# auch: Abbé, Abbey oder Abi
|
||||
#. name for aba
|
||||
@ -127,7 +127,7 @@ msgstr "Abé"
|
||||
|
||||
#. name for abb
|
||||
msgid "Bankon"
|
||||
msgstr ""
|
||||
msgstr "Bankon"
|
||||
|
||||
#. name for abc
|
||||
msgid "Ayta; Ambala"
|
||||
@ -135,7 +135,7 @@ msgstr ""
|
||||
|
||||
#. name for abd
|
||||
msgid "Manide"
|
||||
msgstr ""
|
||||
msgstr "Manide"
|
||||
|
||||
#. name for abe
|
||||
msgid "Abnaki; Western"
|
||||
@ -143,11 +143,11 @@ msgstr "Abnaki; Westlich"
|
||||
|
||||
#. name for abf
|
||||
msgid "Abai Sungai"
|
||||
msgstr ""
|
||||
msgstr "Abai Sungai"
|
||||
|
||||
#. name for abg
|
||||
msgid "Abaga"
|
||||
msgstr ""
|
||||
msgstr "Abaga"
|
||||
|
||||
#. name for abh
|
||||
msgid "Arabic; Tajiki"
|
||||
@ -171,7 +171,7 @@ msgstr ""
|
||||
|
||||
#. name for abm
|
||||
msgid "Abanyom"
|
||||
msgstr ""
|
||||
msgstr "Abanyom"
|
||||
|
||||
#. name for abn
|
||||
msgid "Abua"
|
||||
@ -219,23 +219,23 @@ msgstr ""
|
||||
|
||||
#. name for aby
|
||||
msgid "Aneme Wake"
|
||||
msgstr ""
|
||||
msgstr "Aneme Wake"
|
||||
|
||||
#. name for abz
|
||||
msgid "Abui"
|
||||
msgstr ""
|
||||
msgstr "Abui"
|
||||
|
||||
#. name for aca
|
||||
msgid "Achagua"
|
||||
msgstr ""
|
||||
msgstr "Achagua"
|
||||
|
||||
#. name for acb
|
||||
msgid "Áncá"
|
||||
msgstr ""
|
||||
msgstr "Áncá"
|
||||
|
||||
#. name for acd
|
||||
msgid "Gikyode"
|
||||
msgstr ""
|
||||
msgstr "Gikyode"
|
||||
|
||||
#. name for ace
|
||||
msgid "Achinese"
|
||||
@ -267,7 +267,7 @@ msgstr ""
|
||||
|
||||
#. name for acn
|
||||
msgid "Achang"
|
||||
msgstr ""
|
||||
msgstr "Achang"
|
||||
|
||||
#. name for acp
|
||||
msgid "Acipa; Eastern"
|
||||
@ -7064,7 +7064,7 @@ msgstr ""
|
||||
|
||||
#. name for egy
|
||||
msgid "Egyptian (Ancient)"
|
||||
msgstr "Ägyptisch"
|
||||
msgstr "Altägyptisch"
|
||||
|
||||
#. name for ehu
|
||||
msgid "Ehueun"
|
||||
@ -9241,7 +9241,7 @@ msgstr ""
|
||||
|
||||
#. name for hbo
|
||||
msgid "Hebrew; Ancient"
|
||||
msgstr ""
|
||||
msgstr "Althebräisch"
|
||||
|
||||
#. name for hbs
|
||||
msgid "Serbo-Croatian"
|
||||
@ -28694,7 +28694,7 @@ msgstr ""
|
||||
|
||||
#. name for xlg
|
||||
msgid "Ligurian (Ancient)"
|
||||
msgstr ""
|
||||
msgstr "Ligurisch"
|
||||
|
||||
#. name for xli
|
||||
msgid "Liburnian"
|
||||
@ -28762,7 +28762,7 @@ msgstr ""
|
||||
|
||||
#. name for xmk
|
||||
msgid "Macedonian; Ancient"
|
||||
msgstr ""
|
||||
msgstr "Altmazedonisch"
|
||||
|
||||
#. name for xml
|
||||
msgid "Malaysian Sign Language"
|
||||
@ -28826,7 +28826,7 @@ msgstr ""
|
||||
|
||||
#. name for xna
|
||||
msgid "North Arabian; Ancient"
|
||||
msgstr ""
|
||||
msgstr "Alt-Nordarabisch"
|
||||
|
||||
#. name for xnb
|
||||
msgid "Kanakanabu"
|
||||
|
@ -152,7 +152,7 @@ class Translations(POT): # {{{
|
||||
subprocess.check_call(['msgfmt', '-o', dest, iso639])
|
||||
elif locale not in ('en_GB', 'en_CA', 'en_AU', 'si', 'ur', 'sc',
|
||||
'ltg', 'nds', 'te', 'yi', 'fo', 'sq', 'ast', 'ml', 'ku',
|
||||
'fr_CA'):
|
||||
'fr_CA', 'him'):
|
||||
self.warn('No ISO 639 translations for locale:', locale)
|
||||
|
||||
self.write_stats()
|
||||
|
@ -201,7 +201,8 @@ def prints(*args, **kwargs):
|
||||
try:
|
||||
file.write(arg)
|
||||
except:
|
||||
file.write(repr(arg))
|
||||
import repr as reprlib
|
||||
file.write(reprlib.repr(arg))
|
||||
if i != len(args)-1:
|
||||
file.write(bytes(sep))
|
||||
file.write(bytes(end))
|
||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = u'calibre'
|
||||
numeric_version = (0, 8, 62)
|
||||
numeric_version = (0, 8, 63)
|
||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
|
@ -673,7 +673,7 @@ from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||
from calibre.devices.kobo.driver import KOBO
|
||||
from calibre.devices.bambook.driver import BAMBOOK
|
||||
from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX
|
||||
|
||||
from calibre.devices.smart_device_app.driver import SMART_DEVICE_APP
|
||||
|
||||
|
||||
# Order here matters. The first matched device is the one used.
|
||||
@ -746,6 +746,7 @@ plugins += [
|
||||
ITUNES,
|
||||
BOEYE_BEX,
|
||||
BOEYE_BDX,
|
||||
SMART_DEVICE_APP,
|
||||
USER_DEFINED,
|
||||
]
|
||||
# }}}
|
||||
|
@ -91,6 +91,37 @@ class DummyReporter(object):
|
||||
def __call__(self, percent, msg=''):
|
||||
pass
|
||||
|
||||
def gui_configuration_widget(name, parent, get_option_by_name,
|
||||
get_option_help, db, book_id, for_output=True):
|
||||
import importlib
|
||||
|
||||
def widget_factory(cls):
|
||||
return cls(parent, get_option_by_name,
|
||||
get_option_help, db, book_id)
|
||||
|
||||
if for_output:
|
||||
try:
|
||||
output_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = output_widget.PluginWidget
|
||||
pw.ICON = I('back.png')
|
||||
pw.HELP = _('Options specific to the output format.')
|
||||
return widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
input_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = input_widget.PluginWidget
|
||||
pw.ICON = I('forward.png')
|
||||
pw.HELP = _('Options specific to the input format.')
|
||||
return widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class InputFormatPlugin(Plugin):
|
||||
'''
|
||||
InputFormatPlugins are responsible for converting a document into
|
||||
@ -225,6 +256,17 @@ class InputFormatPlugin(Plugin):
|
||||
'''
|
||||
pass
|
||||
|
||||
def gui_configuration_widget(self, parent, get_option_by_name,
|
||||
get_option_help, db, book_id=None):
|
||||
'''
|
||||
Called to create the widget used for configuring this plugin in the
|
||||
calibre GUI. The widget must be an instance of the PluginWidget class.
|
||||
See the builting input plugins for examples.
|
||||
'''
|
||||
name = self.name.lower().replace(' ', '_')
|
||||
return gui_configuration_widget(name, parent, get_option_by_name,
|
||||
get_option_help, db, book_id, for_output=False)
|
||||
|
||||
|
||||
class OutputFormatPlugin(Plugin):
|
||||
'''
|
||||
@ -308,4 +350,16 @@ class OutputFormatPlugin(Plugin):
|
||||
'''
|
||||
pass
|
||||
|
||||
def gui_configuration_widget(self, parent, get_option_by_name,
|
||||
get_option_help, db, book_id=None):
|
||||
'''
|
||||
Called to create the widget used for configuring this plugin in the
|
||||
calibre GUI. The widget must be an instance of the PluginWidget class.
|
||||
See the builtin output plugins for examples.
|
||||
'''
|
||||
name = self.name.lower().replace(' ', '_')
|
||||
return gui_configuration_widget(name, parent, get_option_by_name,
|
||||
get_option_help, db, book_id, for_output=True)
|
||||
|
||||
|
||||
|
||||
|
@ -213,7 +213,7 @@ class ANDROID(USBMS):
|
||||
'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER',
|
||||
'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX',
|
||||
'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE',
|
||||
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER']
|
||||
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
||||
@ -223,7 +223,7 @@ class ANDROID(USBMS):
|
||||
'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC',
|
||||
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875',
|
||||
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
|
||||
'USB_FLASH_DRIVER']
|
||||
'USB_FLASH_DRIVER', 'ANDROID']
|
||||
|
||||
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||
|
||||
|
@ -13,7 +13,6 @@ import datetime, os, re, sys, json, hashlib
|
||||
from calibre.devices.kindle.bookmark import Bookmark
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
from calibre import strftime
|
||||
from calibre.utils.logging import default_log
|
||||
|
||||
'''
|
||||
Notes on collections:
|
||||
@ -389,6 +388,7 @@ class KINDLE2(KINDLE):
|
||||
self.upload_apnx(path, filename, metadata, filepath)
|
||||
|
||||
def upload_kindle_thumbnail(self, metadata, filepath):
|
||||
from calibre.utils.logging import default_log
|
||||
coverdata = getattr(metadata, 'thumbnail', None)
|
||||
if not coverdata or not coverdata[2]:
|
||||
return
|
||||
|
@ -461,7 +461,7 @@ class KOBO(USBMS):
|
||||
self.report_progress(1.0, _('Removing books from device...'))
|
||||
|
||||
def remove_books_from_metadata(self, paths, booklists):
|
||||
if self.modify_datbase_check("remove_books_from_metatata") == False:
|
||||
if self.modify_database_check("remove_books_from_metatata") == False:
|
||||
return
|
||||
|
||||
for i, path in enumerate(paths):
|
||||
|
9
src/calibre/devices/smart_device_app/__init__.py
Normal file
9
src/calibre/devices/smart_device_app/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
|
911
src/calibre/devices/smart_device_app/driver.py
Normal file
911
src/calibre/devices/smart_device_app/driver.py
Normal file
@ -0,0 +1,911 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
'''
|
||||
Created on 29 Jun 2012
|
||||
|
||||
@author: charles
|
||||
'''
|
||||
import socket, select, json, inspect, os, traceback, time, sys, random
|
||||
import hashlib, threading
|
||||
from base64 import b64encode, b64decode
|
||||
from functools import wraps
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import numeric_version, DEBUG
|
||||
from calibre.devices.errors import OpenFeedback
|
||||
from calibre.devices.interface import DevicePlugin
|
||||
from calibre.devices.usbms.books import Book, BookList
|
||||
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.ebooks.metadata.book.json_codec import JsonCodec
|
||||
from calibre.library import current_library_name
|
||||
from calibre.utils.ipc import eintr_retry_call
|
||||
from calibre.utils.config import from_json, tweaks
|
||||
from calibre.utils.date import isoformat, now
|
||||
from calibre.utils.filenames import ascii_filename as sanitize, shorten_components_to
|
||||
from calibre.utils.mdns import (publish as publish_zeroconf, unpublish as
|
||||
unpublish_zeroconf)
|
||||
|
||||
def synchronous(tlockname):
|
||||
"""A decorator to place an instance based lock around a method """
|
||||
|
||||
def _synched(func):
|
||||
@wraps(func)
|
||||
def _synchronizer(self, *args, **kwargs):
|
||||
with self.__getattribute__(tlockname):
|
||||
return func(self, *args, **kwargs)
|
||||
return _synchronizer
|
||||
return _synched
|
||||
|
||||
def do_zeroconf(f, port):
|
||||
f('calibre smart device client',
|
||||
'_calibresmartdeviceapp._tcp', port, {})
|
||||
|
||||
|
||||
class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
||||
name = 'SmartDevice App Interface'
|
||||
gui_name = _('SmartDevice')
|
||||
icon = I('devices/galaxy_s3.png')
|
||||
description = _('Communicate with Smart Device apps')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Charles Haley'
|
||||
version = (0, 0, 1)
|
||||
|
||||
# Invalid USB vendor information so the scanner will never match
|
||||
VENDOR_ID = [0xffff]
|
||||
PRODUCT_ID = [0xffff]
|
||||
BCD = [0xffff]
|
||||
|
||||
FORMATS = list(BOOK_EXTENSIONS)
|
||||
ALL_FORMATS = list(BOOK_EXTENSIONS)
|
||||
HIDE_FORMATS_CONFIG_BOX = True
|
||||
USER_CAN_ADD_NEW_FORMATS = False
|
||||
DEVICE_PLUGBOARD_NAME = 'SMART_DEVICE_APP'
|
||||
CAN_SET_METADATA = []
|
||||
CAN_DO_DEVICE_DB_PLUGBOARD = False
|
||||
SUPPORTS_SUB_DIRS = False
|
||||
MUST_READ_METADATA = True
|
||||
NEWS_IN_FOLDER = False
|
||||
SUPPORTS_USE_AUTHOR_SORT = False
|
||||
WANTS_UPDATED_THUMBNAILS = True
|
||||
MAX_PATH_LEN = 100
|
||||
THUMBNAIL_HEIGHT = 160
|
||||
PREFIX = ''
|
||||
|
||||
# Some network protocol constants
|
||||
BASE_PACKET_LEN = 4096
|
||||
PROTOCOL_VERSION = 1
|
||||
MAX_CLIENT_COMM_TIMEOUT = 60.0 # Wait at most N seconds for an answer
|
||||
|
||||
opcodes = {
|
||||
'NOOP' : 12,
|
||||
'OK' : 0,
|
||||
'BOOK_DATA' : 10,
|
||||
'BOOK_DONE' : 11,
|
||||
'DELETE_BOOK' : 13,
|
||||
'DISPLAY_MESSAGE' : 17,
|
||||
'FREE_SPACE' : 5,
|
||||
'GET_BOOK_FILE_SEGMENT' : 14,
|
||||
'GET_BOOK_METADATA' : 15,
|
||||
'GET_BOOK_COUNT' : 6,
|
||||
'GET_DEVICE_INFORMATION' : 3,
|
||||
'GET_INITIALIZATION_INFO': 9,
|
||||
'SEND_BOOKLISTS' : 7,
|
||||
'SEND_BOOK' : 8,
|
||||
'SEND_BOOK_METADATA' : 16,
|
||||
'SET_CALIBRE_DEVICE_INFO': 1,
|
||||
'SET_CALIBRE_DEVICE_NAME': 2,
|
||||
'TOTAL_SPACE' : 4,
|
||||
}
|
||||
reverse_opcodes = dict([(v, k) for k,v in opcodes.iteritems()])
|
||||
|
||||
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = [
|
||||
_('Enable connections at startup') + ':::<p>' +
|
||||
_('Check this box to allow connections when calibre starts') + '</p>',
|
||||
'',
|
||||
_('Security password') + ':::<p>' +
|
||||
_('Enter a password that the device app must use to connect to calibre') + '</p>',
|
||||
'',
|
||||
_('Use fixed network port') + ':::<p>' +
|
||||
_('If checked, use the port number in the "Port" box, otherwise '
|
||||
'the driver will pick a random port') + '</p>',
|
||||
_('Port') + ':::<p>' +
|
||||
_('Enter the port number the driver is to use if the "fixed port" box is checked') + '</p>',
|
||||
_('Print extra debug information') + ':::<p>' +
|
||||
_('Check this box if requested when reporting problems') + '</p>',
|
||||
]
|
||||
EXTRA_CUSTOMIZATION_DEFAULT = [
|
||||
False,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
False, '9090',
|
||||
False,
|
||||
]
|
||||
OPT_AUTOSTART = 0
|
||||
OPT_PASSWORD = 2
|
||||
OPT_USE_PORT = 4
|
||||
OPT_PORT_NUMBER = 5
|
||||
OPT_EXTRA_DEBUG = 6
|
||||
|
||||
def __init__(self, path):
|
||||
self.sync_lock = threading.RLock()
|
||||
self.noop_counter = 0
|
||||
self.debug_start_time = time.time()
|
||||
self.debug_time = time.time()
|
||||
|
||||
def _debug(self, *args):
|
||||
if not DEBUG:
|
||||
return
|
||||
total_elapsed = time.time() - self.debug_start_time
|
||||
elapsed = time.time() - self.debug_time
|
||||
print('SMART_DEV (%7.2f:%7.3f) %s'%(total_elapsed, elapsed,
|
||||
inspect.stack()[1][3]), end='')
|
||||
for a in args:
|
||||
try:
|
||||
prints('', a, end='')
|
||||
except:
|
||||
prints('', 'value too long', end='')
|
||||
print()
|
||||
self.debug_time = time.time()
|
||||
|
||||
# Various methods required by the plugin architecture
|
||||
@classmethod
|
||||
def _default_save_template(cls):
|
||||
from calibre.library.save_to_disk import config
|
||||
st = cls.SAVE_TEMPLATE if cls.SAVE_TEMPLATE else \
|
||||
config().parse().send_template
|
||||
if st:
|
||||
st = os.path.basename(st)
|
||||
return st
|
||||
|
||||
@classmethod
|
||||
def save_template(cls):
|
||||
st = cls.settings().save_template
|
||||
if st:
|
||||
st = os.path.basename(st)
|
||||
else:
|
||||
st = cls._default_save_template()
|
||||
return st
|
||||
|
||||
# local utilities
|
||||
|
||||
# copied from USBMS. Perhaps this could be a classmethod in usbms?
|
||||
def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None):
|
||||
import uuid
|
||||
if not isinstance(dinfo, dict):
|
||||
dinfo = {}
|
||||
if dinfo.get('device_store_uuid', None) is None:
|
||||
dinfo['device_store_uuid'] = unicode(uuid.uuid4())
|
||||
if dinfo.get('device_name') is None:
|
||||
dinfo['device_name'] = self.get_gui_name()
|
||||
if name is not None:
|
||||
dinfo['device_name'] = name
|
||||
dinfo['location_code'] = location_code
|
||||
dinfo['last_library_uuid'] = getattr(self, 'current_library_uuid', None)
|
||||
dinfo['calibre_version'] = '.'.join([unicode(i) for i in numeric_version])
|
||||
dinfo['date_last_connected'] = isoformat(now())
|
||||
dinfo['prefix'] = self.PREFIX
|
||||
return dinfo
|
||||
|
||||
# copied with changes from USBMS.Device. In particular, we needed to
|
||||
# remove the 'path' argument and all its uses. Also removed the calls to
|
||||
# filename_callback and sanitize_path_components
|
||||
def _create_upload_path(self, mdata, fname, create_dirs=True):
|
||||
maxlen = self.MAX_PATH_LEN
|
||||
|
||||
special_tag = None
|
||||
if mdata.tags:
|
||||
for t in mdata.tags:
|
||||
if t.startswith(_('News')) or t.startswith('/'):
|
||||
special_tag = t
|
||||
break
|
||||
|
||||
settings = self.settings()
|
||||
template = self.save_template()
|
||||
if mdata.tags and _('News') in mdata.tags:
|
||||
try:
|
||||
p = mdata.pubdate
|
||||
date = (p.year, p.month, p.day)
|
||||
except:
|
||||
today = time.localtime()
|
||||
date = (today[0], today[1], today[2])
|
||||
template = "{title}_%d-%d-%d" % date
|
||||
use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs
|
||||
|
||||
fname = sanitize(fname)
|
||||
ext = os.path.splitext(fname)[1]
|
||||
|
||||
from calibre.library.save_to_disk import get_components
|
||||
from calibre.library.save_to_disk import config
|
||||
opts = config().parse()
|
||||
if not isinstance(template, unicode):
|
||||
template = template.decode('utf-8')
|
||||
app_id = str(getattr(mdata, 'application_id', ''))
|
||||
id_ = mdata.get('id', fname)
|
||||
extra_components = get_components(template, mdata, id_,
|
||||
timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1)
|
||||
if not extra_components:
|
||||
extra_components.append(sanitize(fname))
|
||||
else:
|
||||
extra_components[-1] = sanitize(extra_components[-1]+ext)
|
||||
|
||||
if extra_components[-1] and extra_components[-1][0] in ('.', '_'):
|
||||
extra_components[-1] = 'x' + extra_components[-1][1:]
|
||||
|
||||
if special_tag is not None:
|
||||
name = extra_components[-1]
|
||||
extra_components = []
|
||||
tag = special_tag
|
||||
if tag.startswith(_('News')):
|
||||
if self.NEWS_IN_FOLDER:
|
||||
extra_components.append('News')
|
||||
else:
|
||||
for c in tag.split('/'):
|
||||
c = sanitize(c)
|
||||
if not c: continue
|
||||
extra_components.append(c)
|
||||
extra_components.append(name)
|
||||
|
||||
if not use_subdirs:
|
||||
# Leave this stuff here in case we later decide to use subdirs
|
||||
extra_components = extra_components[-1:]
|
||||
|
||||
def remove_trailing_periods(x):
|
||||
ans = x
|
||||
while ans.endswith('.'):
|
||||
ans = ans[:-1].strip()
|
||||
if not ans:
|
||||
ans = 'x'
|
||||
return ans
|
||||
|
||||
extra_components = list(map(remove_trailing_periods, extra_components))
|
||||
components = shorten_components_to(maxlen, extra_components)
|
||||
filepath = os.path.join(*components)
|
||||
return filepath
|
||||
|
||||
def _strip_prefix(self, path):
|
||||
if self.PREFIX and path.startswith(self.PREFIX):
|
||||
return path[len(self.PREFIX):]
|
||||
return path
|
||||
|
||||
# JSON booklist encode & decode
|
||||
|
||||
# If the argument is a booklist or contains a book, use the metadata json
|
||||
# codec to first convert it to a string dict
|
||||
def _json_encode(self, op, arg):
|
||||
res = {}
|
||||
for k,v in arg.iteritems():
|
||||
if isinstance(v, (Book, Metadata)):
|
||||
res[k] = self.json_codec.encode_book_metadata(v)
|
||||
series = v.get('series', None)
|
||||
if series:
|
||||
tsorder = tweaks['save_template_title_series_sorting']
|
||||
series = title_sort(v.get('series', ''), order=tsorder)
|
||||
else:
|
||||
series = ''
|
||||
res[k]['_series_sort_'] = series
|
||||
else:
|
||||
res[k] = v
|
||||
return json.dumps([op, res], encoding='utf-8')
|
||||
|
||||
# Network functions
|
||||
def _read_string_from_net(self):
|
||||
data = bytes(0)
|
||||
while True:
|
||||
dex = data.find(b'[')
|
||||
if dex >= 0:
|
||||
break
|
||||
# recv seems to return a pointer into some internal buffer.
|
||||
# Things get trashed if we don't make a copy of the data.
|
||||
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
|
||||
v = self.device_socket.recv(self.BASE_PACKET_LEN)
|
||||
self.device_socket.settimeout(None)
|
||||
if len(v) == 0:
|
||||
return '' # documentation says the socket is broken permanently.
|
||||
data += v
|
||||
total_len = int(data[:dex])
|
||||
data = data[dex:]
|
||||
pos = len(data)
|
||||
while pos < total_len:
|
||||
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
|
||||
v = self.device_socket.recv(total_len - pos)
|
||||
self.device_socket.settimeout(None)
|
||||
if len(v) == 0:
|
||||
return '' # documentation says the socket is broken permanently.
|
||||
data += v
|
||||
pos += len(v)
|
||||
return data
|
||||
|
||||
def _call_client(self, op, arg, print_debug_info=True):
|
||||
if op != 'NOOP':
|
||||
self.noop_counter = 0
|
||||
extra_debug = self.settings().extra_customization[self.OPT_EXTRA_DEBUG]
|
||||
if print_debug_info or extra_debug:
|
||||
if extra_debug:
|
||||
self._debug(op, arg)
|
||||
else:
|
||||
self._debug(op)
|
||||
if self.device_socket is None:
|
||||
return None, None
|
||||
try:
|
||||
s = self._json_encode(self.opcodes[op], arg)
|
||||
if print_debug_info and extra_debug:
|
||||
self._debug('send string', s)
|
||||
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
|
||||
self.device_socket.sendall(('%d' % len(s))+s)
|
||||
self.device_socket.settimeout(None)
|
||||
v = self._read_string_from_net()
|
||||
if print_debug_info and extra_debug:
|
||||
self._debug('received string', v)
|
||||
if v:
|
||||
v = json.loads(v, object_hook=from_json)
|
||||
if print_debug_info and extra_debug:
|
||||
self._debug('receive after decode') #, v)
|
||||
return (self.reverse_opcodes[v[0]], v[1])
|
||||
self._debug('protocol error -- empty json string')
|
||||
except socket.timeout:
|
||||
self._debug('timeout communicating with device')
|
||||
self.device_socket.close()
|
||||
self.device_socket = None
|
||||
self.is_connected = False
|
||||
raise IOError(_('Device did not respond in reasonable time'))
|
||||
except socket.error:
|
||||
self._debug('device went away')
|
||||
self.device_socket.close()
|
||||
self.device_socket = None
|
||||
self.is_connected = False
|
||||
raise IOError(_('Device closed the network connection'))
|
||||
except:
|
||||
self._debug('other exception')
|
||||
traceback.print_exc()
|
||||
self.device_socket.close()
|
||||
self.device_socket = None
|
||||
self.is_connected = False
|
||||
raise
|
||||
raise IOError('Device responded with incorrect information')
|
||||
|
||||
# Write a file as a series of base64-encoded strings.
|
||||
def _put_file(self, infile, lpath, book_metadata, this_book, total_books):
|
||||
close_ = False
|
||||
if not hasattr(infile, 'read'):
|
||||
infile, close_ = open(infile, 'rb'), True
|
||||
infile.seek(0, os.SEEK_END)
|
||||
length = infile.tell()
|
||||
book_metadata.size = length
|
||||
infile.seek(0)
|
||||
self._debug(lpath, length)
|
||||
self._call_client('SEND_BOOK', {'lpath': lpath, 'length': length,
|
||||
'metadata': book_metadata, 'thisBook': this_book,
|
||||
'totalBooks': total_books}, print_debug_info=False)
|
||||
self._set_known_metadata(book_metadata)
|
||||
pos = 0
|
||||
failed = False
|
||||
with infile:
|
||||
while True:
|
||||
b = infile.read(self.max_book_packet_len)
|
||||
blen = len(b)
|
||||
if not b:
|
||||
break;
|
||||
b = b64encode(b)
|
||||
opcode, result = self._call_client('BOOK_DATA',
|
||||
{'lpath': lpath, 'position': pos, 'data': b},
|
||||
print_debug_info=False)
|
||||
pos += blen
|
||||
if opcode != 'OK':
|
||||
self._debug('protocol error', opcode)
|
||||
failed = True
|
||||
break
|
||||
self._call_client('BOOK_DONE', {'lpath': lpath})
|
||||
self.time = None
|
||||
if close_:
|
||||
infile.close()
|
||||
return -1 if failed else length
|
||||
|
||||
def _get_smartdevice_option_number(self, opt_string):
|
||||
if opt_string == 'password':
|
||||
return self.OPT_PASSWORD
|
||||
elif opt_string == 'autostart':
|
||||
return self.OPT_AUTOSTART
|
||||
else:
|
||||
return None
|
||||
|
||||
def _compare_metadata(self, mi1, mi2):
|
||||
for key in SERIALIZABLE_FIELDS:
|
||||
if key in ['cover', 'mime']:
|
||||
continue
|
||||
if key == 'user_metadata':
|
||||
meta1 = mi1.get_all_user_metadata(make_copy=False)
|
||||
meta2 = mi1.get_all_user_metadata(make_copy=False)
|
||||
if meta1 != meta2:
|
||||
self._debug('custom metadata different')
|
||||
return False
|
||||
for ckey in meta1:
|
||||
if mi1.get(ckey) != mi2.get(ckey):
|
||||
self._debug(ckey, mi1.get(ckey), mi2.get(ckey))
|
||||
return False
|
||||
elif mi1.get(key, None) != mi2.get(key, None):
|
||||
self._debug(key, mi1.get(key), mi2.get(key))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _metadata_already_on_device(self, book):
|
||||
v = self.known_metadata.get(book.lpath, None)
|
||||
if v is not None:
|
||||
return self._compare_metadata(book, v)
|
||||
return False
|
||||
|
||||
def _set_known_metadata(self, book, remove=False):
|
||||
lpath = book.lpath
|
||||
if remove:
|
||||
self.known_metadata[lpath] = None
|
||||
else:
|
||||
self.known_metadata[lpath] = book.deepcopy()
|
||||
|
||||
# The public interface methods.
|
||||
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def is_usb_connected(self, devices_on_system, debug=False, only_presence=False):
|
||||
if getattr(self, 'listen_socket', None) is None:
|
||||
self.is_connected = False
|
||||
if self.is_connected:
|
||||
self.noop_counter += 1
|
||||
if only_presence and (self.noop_counter % 5) != 1:
|
||||
try:
|
||||
ans = select.select((self.device_socket,), (), (), 0)
|
||||
if len(ans[0]) == 0:
|
||||
return (True, self)
|
||||
# The socket indicates that something is there. Given the
|
||||
# protocol, this can only be a disconnect notification. Fall
|
||||
# through and actually try to talk to the client.
|
||||
# This will usually toss an exception if the socket is gone.
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if self._call_client('NOOP', dict())[0] is None:
|
||||
self.is_connected = False
|
||||
except:
|
||||
self.is_connected = False
|
||||
if not self.is_connected:
|
||||
self.device_socket.close()
|
||||
return (self.is_connected, self)
|
||||
if getattr(self, 'listen_socket', None) is not None:
|
||||
ans = select.select((self.listen_socket,), (), (), 0)
|
||||
if len(ans[0]) > 0:
|
||||
# timeout in 10 ms to detect rare case where the socket went
|
||||
# way between the select and the accept
|
||||
try:
|
||||
self.device_socket = None
|
||||
self.listen_socket.settimeout(0.010)
|
||||
self.device_socket, ign = eintr_retry_call(
|
||||
self.listen_socket.accept)
|
||||
self.listen_socket.settimeout(None)
|
||||
self.device_socket.settimeout(None)
|
||||
self.is_connected = True
|
||||
except socket.timeout:
|
||||
if self.device_socket is not None:
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
except socket.error:
|
||||
x = sys.exc_info()[1]
|
||||
self._debug('unexpected socket exception', x.args[0])
|
||||
if self.device_socket is not None:
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
raise
|
||||
return (True, self)
|
||||
return (False, None)
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def open(self, connected_device, library_uuid):
|
||||
self._debug()
|
||||
self.current_library_uuid = library_uuid
|
||||
self.current_library_name = current_library_name()
|
||||
try:
|
||||
password = self.settings().extra_customization[self.OPT_PASSWORD]
|
||||
if password:
|
||||
challenge = isoformat(now())
|
||||
hasher = hashlib.new('sha1')
|
||||
hasher.update(password.encode('UTF-8'))
|
||||
hasher.update(challenge.encode('UTF-8'))
|
||||
hash_digest = hasher.hexdigest()
|
||||
else:
|
||||
challenge = ''
|
||||
hash_digest = ''
|
||||
opcode, result = self._call_client('GET_INITIALIZATION_INFO',
|
||||
{'serverProtocolVersion': self.PROTOCOL_VERSION,
|
||||
'validExtensions': self.ALL_FORMATS,
|
||||
'passwordChallenge': challenge,
|
||||
'currentLibraryName': self.current_library_name,
|
||||
'currentLibraryUUID': library_uuid})
|
||||
if opcode != 'OK':
|
||||
# Something wrong with the return. Close the socket
|
||||
# and continue.
|
||||
self._debug('Protocol error - Opcode not OK')
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
return False
|
||||
if not result.get('versionOK', False):
|
||||
# protocol mismatch
|
||||
self._debug('Protocol error - protocol version mismatch')
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
return False
|
||||
if result.get('maxBookContentPacketLen', 0) <= 0:
|
||||
# protocol mismatch
|
||||
self._debug('Protocol error - bogus book packet length')
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
return False
|
||||
self.max_book_packet_len = result.get('maxBookContentPacketLen',
|
||||
self.BASE_PACKET_LEN)
|
||||
exts = result.get('acceptedExtensions', None)
|
||||
if exts is None or not isinstance(exts, list) or len(exts) == 0:
|
||||
self._debug('Protocol error - bogus accepted extensions')
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
return False
|
||||
self.FORMATS = exts
|
||||
if password:
|
||||
returned_hash = result.get('passwordHash', None)
|
||||
if result.get('passwordHash', None) is None:
|
||||
# protocol mismatch
|
||||
self._debug('Protocol error - missing password hash')
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
return False
|
||||
if returned_hash != hash_digest:
|
||||
# bad password
|
||||
self._debug('password mismatch')
|
||||
self._call_client("DISPLAY_MESSAGE", {'messageKind':1})
|
||||
self.is_connected = False
|
||||
self.device_socket.close()
|
||||
raise OpenFeedback('Incorrect password supplied')
|
||||
return True
|
||||
except socket.timeout:
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
except socket.error:
|
||||
x = sys.exc_info()[1]
|
||||
self._debug('unexpected socket exception', x.args[0])
|
||||
self.device_socket.close()
|
||||
self.is_connected = False
|
||||
raise
|
||||
return False
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def get_device_information(self, end_session=True):
|
||||
self._debug()
|
||||
self.report_progress(1.0, _('Get device information...'))
|
||||
opcode, result = self._call_client('GET_DEVICE_INFORMATION', dict())
|
||||
if opcode == 'OK':
|
||||
self.driveinfo = result['device_info']
|
||||
self._update_driveinfo_record(self.driveinfo, self.PREFIX, 'main')
|
||||
self._call_client('SET_CALIBRE_DEVICE_INFO', self.driveinfo)
|
||||
return (self.get_gui_name(), result['device_version'],
|
||||
result['version'], '', {'main':self.driveinfo})
|
||||
return (self.get_gui_name(), '', '', '')
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def set_driveinfo_name(self, location_code, name):
|
||||
self._update_driveinfo_record(self.driveinfo, "main", name)
|
||||
self._call_client('SET_CALIBRE_DEVICE_NAME',
|
||||
{'location_code': 'main', 'name':name})
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||
detected_device=None) :
|
||||
self._debug()
|
||||
self.set_progress_reporter(report_progress)
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def set_progress_reporter(self, report_progress):
|
||||
self._debug()
|
||||
self.report_progress = report_progress
|
||||
if self.report_progress is None:
|
||||
self.report_progress = lambda x, y: x
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def card_prefix(self, end_session=True):
|
||||
self._debug()
|
||||
return (None, None)
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def total_space(self, end_session=True):
|
||||
self._debug()
|
||||
opcode, result = self._call_client('TOTAL_SPACE', {})
|
||||
if opcode == 'OK':
|
||||
return (result['total_space_on_device'], 0, 0)
|
||||
# protocol error if we get here
|
||||
return (0, 0, 0)
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def free_space(self, end_session=True):
|
||||
self._debug()
|
||||
opcode, result = self._call_client('FREE_SPACE', {})
|
||||
if opcode == 'OK':
|
||||
return (result['free_space_on_device'], 0, 0)
|
||||
# protocol error if we get here
|
||||
return (0, 0, 0)
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def books(self, oncard=None, end_session=True):
|
||||
self._debug(oncard)
|
||||
if oncard is not None:
|
||||
return BookList(None, None, None)
|
||||
opcode, result = self._call_client('GET_BOOK_COUNT', {})
|
||||
bl = BookList(None, self.PREFIX, self.settings)
|
||||
if opcode == 'OK':
|
||||
count = result['count']
|
||||
for i in range(0, count):
|
||||
self._debug('retrieve metadata book', i)
|
||||
opcode, result = self._call_client('GET_BOOK_METADATA', {'index': i},
|
||||
print_debug_info=False)
|
||||
if opcode == 'OK':
|
||||
if '_series_sort_' in result:
|
||||
del result['_series_sort_']
|
||||
book = self.json_codec.raw_to_book(result, Book, self.PREFIX)
|
||||
self._set_known_metadata(book)
|
||||
bl.add_book(book, replace_metadata=True)
|
||||
else:
|
||||
raise IOError(_('Protocol error -- book metadata not returned'))
|
||||
return bl
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
self._debug()
|
||||
# If we ever do device_db plugboards, this is where it will go. We will
|
||||
# probably need to send two booklists, one with calibre's data that is
|
||||
# given back by "books", and one that has been plugboarded.
|
||||
self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]) } )
|
||||
for i,book in enumerate(booklists[0]):
|
||||
if not self._metadata_already_on_device(book):
|
||||
self._set_known_metadata(book)
|
||||
self._debug('syncing book', book.lpath)
|
||||
opcode, result = self._call_client('SEND_BOOK_METADATA',
|
||||
{'index': i, 'data': book},
|
||||
print_debug_info=False)
|
||||
if opcode != 'OK':
|
||||
self._debug('protocol error', opcode, i)
|
||||
raise IOError(_('Protocol error -- sync_booklists'))
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def eject(self):
|
||||
self._debug()
|
||||
if self.device_socket:
|
||||
self.device_socket.close()
|
||||
self.device_socket = None
|
||||
self.is_connected = False
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def post_yank_cleanup(self):
|
||||
self._debug()
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def upload_books(self, files, names, on_card=None, end_session=True,
|
||||
metadata=None):
|
||||
self._debug(names)
|
||||
|
||||
paths = []
|
||||
names = iter(names)
|
||||
metadata = iter(metadata)
|
||||
|
||||
for i, infile in enumerate(files):
|
||||
mdata, fname = metadata.next(), names.next()
|
||||
lpath = self._create_upload_path(mdata, fname, create_dirs=False)
|
||||
if not hasattr(infile, 'read'):
|
||||
infile = USBMS.normalize_path(infile)
|
||||
book = Book(self.PREFIX, lpath, other=mdata)
|
||||
length = self._put_file(infile, lpath, book, i, len(files))
|
||||
if length < 0:
|
||||
raise IOError(_('Sending book %s to device failed') % lpath)
|
||||
paths.append((lpath, length))
|
||||
# No need to deal with covers. The client will get the thumbnails
|
||||
# in the mi structure
|
||||
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
|
||||
|
||||
self.report_progress(1.0, _('Transferring books to device...'))
|
||||
self._debug('finished uploading %d books'%(len(files)))
|
||||
return paths
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def add_books_to_metadata(self, locations, metadata, booklists):
|
||||
self._debug('adding metadata for %d books'%(len(metadata)))
|
||||
|
||||
metadata = iter(metadata)
|
||||
for i, location in enumerate(locations):
|
||||
self.report_progress((i+1) / float(len(locations)),
|
||||
_('Adding books to device metadata listing...'))
|
||||
info = metadata.next()
|
||||
lpath = location[0]
|
||||
length = location[1]
|
||||
lpath = self._strip_prefix(lpath)
|
||||
book = Book(self.PREFIX, lpath, other=info)
|
||||
if book.size is None:
|
||||
book.size = length
|
||||
b = booklists[0].add_book(book, replace_metadata=True)
|
||||
if b:
|
||||
b._new_book = True
|
||||
self.report_progress(1.0, _('Adding books to device metadata listing...'))
|
||||
self._debug('finished adding metadata')
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def delete_books(self, paths, end_session=True):
|
||||
self._debug(paths)
|
||||
for path in paths:
|
||||
# the path has the prefix on it (I think)
|
||||
path = self._strip_prefix(path)
|
||||
opcode, result = self._call_client('DELETE_BOOK', {'lpath': path})
|
||||
if opcode == 'OK':
|
||||
self._debug('removed book with UUID', result['uuid'])
|
||||
else:
|
||||
raise IOError(_('Protocol error - delete books'))
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def remove_books_from_metadata(self, paths, booklists):
|
||||
self._debug(paths)
|
||||
for i, path in enumerate(paths):
|
||||
path = self._strip_prefix(path)
|
||||
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
|
||||
for bl in booklists:
|
||||
for book in bl:
|
||||
if path == book.path:
|
||||
bl.remove_book(book)
|
||||
self._set_known_metadata(book, remove=True)
|
||||
self.report_progress(1.0, _('Removing books from device metadata listing...'))
|
||||
self._debug('finished removing metadata for %d books'%(len(paths)))
|
||||
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def get_file(self, path, outfile, end_session=True):
|
||||
self._debug(path)
|
||||
eof = False
|
||||
position = 0
|
||||
while not eof:
|
||||
opcode, result = self._call_client('GET_BOOK_FILE_SEGMENT',
|
||||
{'lpath' : path, 'position': position},
|
||||
print_debug_info=False )
|
||||
if opcode == 'OK':
|
||||
if not result['eof']:
|
||||
data = b64decode(result['data'])
|
||||
if len(data) != result['next_position'] - position:
|
||||
self._debug('position mismatch', result['next_position'], position)
|
||||
position = result['next_position']
|
||||
outfile.write(data)
|
||||
else:
|
||||
eof = True
|
||||
else:
|
||||
raise IOError(_('request for book data failed'))
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def set_plugboards(self, plugboards, pb_func):
|
||||
self._debug()
|
||||
self.plugboards = plugboards
|
||||
self.plugboard_func = pb_func
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def startup(self):
|
||||
self.listen_socket = None
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def startup_on_demand(self):
|
||||
if getattr(self, 'listen_socket', None) is not None:
|
||||
# we are already running
|
||||
return
|
||||
if len(self.opcodes) != len(self.reverse_opcodes):
|
||||
self._debug(self.opcodes, self.reverse_opcodes)
|
||||
self.is_connected = False
|
||||
self.listen_socket = None
|
||||
self.device_socket = None
|
||||
self.json_codec = JsonCodec()
|
||||
self.known_metadata = {}
|
||||
self.debug_time = time.time()
|
||||
self.debug_start_time = time.time()
|
||||
self.max_book_packet_len = 0
|
||||
self.noop_counter = 0
|
||||
try:
|
||||
self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
except:
|
||||
self._debug('creation of listen socket failed')
|
||||
return
|
||||
|
||||
i = 0
|
||||
while i < 100: # try up to 100 random port numbers
|
||||
if self.settings().extra_customization[self.OPT_USE_PORT]:
|
||||
i = 100
|
||||
try:
|
||||
port = int(self.settings().extra_customization[self.OPT_PORT_NUMBER])
|
||||
except:
|
||||
port = 0
|
||||
else:
|
||||
i += 1
|
||||
port = random.randint(8192, 32000)
|
||||
try:
|
||||
self._debug('try port', port)
|
||||
self.listen_socket.bind(('', port))
|
||||
break
|
||||
except socket.error:
|
||||
port = 0
|
||||
except:
|
||||
self._debug('Unknown exception while allocating listen socket')
|
||||
traceback.print_exc()
|
||||
raise
|
||||
if port == 0:
|
||||
self._debug('Failed to allocate a port');
|
||||
self.listen_socket.close()
|
||||
self.listen_socket = None
|
||||
self.is_connected = False
|
||||
return
|
||||
|
||||
try:
|
||||
self.listen_socket.listen(0)
|
||||
except:
|
||||
self._debug('listen on socket failed', port)
|
||||
self.listen_socket.close()
|
||||
self.listen_socket = None
|
||||
self.is_connected = False
|
||||
return
|
||||
|
||||
try:
|
||||
do_zeroconf(publish_zeroconf, port)
|
||||
except:
|
||||
self._debug('registration with bonjour failed')
|
||||
self.listen_socket.close()
|
||||
self.listen_socket = None
|
||||
self.is_connected = False
|
||||
return
|
||||
|
||||
self._debug('listening on port', port)
|
||||
self.port = port
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def shutdown(self):
|
||||
if getattr(self, 'listen_socket', None) is not None:
|
||||
do_zeroconf(unpublish_zeroconf, self.port)
|
||||
self.listen_socket.close()
|
||||
self.listen_socket = None
|
||||
self.is_connected = False
|
||||
|
||||
# Methods for dynamic control
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def is_dynamically_controllable(self):
|
||||
return 'smartdevice'
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def start_plugin(self):
|
||||
self.startup_on_demand()
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def stop_plugin(self):
|
||||
self.shutdown()
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def get_option(self, opt_string, default=None):
|
||||
opt = self._get_smartdevice_option_number(opt_string)
|
||||
if opt is not None:
|
||||
return self.settings().extra_customization[opt]
|
||||
return default
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def set_option(self, opt_string, value):
|
||||
opt = self._get_smartdevice_option_number(opt_string)
|
||||
if opt is not None:
|
||||
config = self._configProxy()
|
||||
ec = config['extra_customization']
|
||||
ec[opt] = value
|
||||
config['extra_customization'] = ec
|
||||
|
||||
@synchronous('sync_lock')
|
||||
def is_running(self):
|
||||
return getattr(self, 'listen_socket', None) is not None
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
#
|
||||
# Copyright (C) 2006 Søren Roug, European Environment Agency
|
||||
#
|
||||
# This is free software. You may redistribute it under the terms
|
||||
@ -17,12 +19,20 @@
|
||||
#
|
||||
# Contributor(s):
|
||||
#
|
||||
from __future__ import division
|
||||
|
||||
import zipfile, re
|
||||
import xml.sax.saxutils
|
||||
from cStringIO import StringIO
|
||||
|
||||
from odf.namespaces import OFFICENS, DCNS, METANS
|
||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||
from odf.opendocument import load as odLoad
|
||||
from odf.draw import Image as odImage, Frame as odFrame
|
||||
|
||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors, check_isbn
|
||||
from calibre.utils.magick.draw import identify_data
|
||||
from calibre.utils.date import parse_date
|
||||
from calibre.utils.localization import canonicalize_lang
|
||||
|
||||
whitespace = re.compile(r'\s+')
|
||||
|
||||
@ -125,6 +135,10 @@ class odfmetaparser(xml.sax.saxutils.XMLGenerator):
|
||||
else:
|
||||
texttag = self._tag
|
||||
self.seenfields[texttag] = self.data()
|
||||
# OpenOffice has the habit to capitalize custom properties, so we add a
|
||||
# lowercase version for easy access
|
||||
if texttag[:4].lower() == u'opf.':
|
||||
self.seenfields[texttag.lower()] = self.data()
|
||||
|
||||
if field in self.deletefields:
|
||||
self.output.dowrite = True
|
||||
@ -141,7 +155,7 @@ class odfmetaparser(xml.sax.saxutils.XMLGenerator):
|
||||
def data(self):
|
||||
return normalize(''.join(self._data))
|
||||
|
||||
def get_metadata(stream):
|
||||
def get_metadata(stream, extract_cover=True):
|
||||
zin = zipfile.ZipFile(stream, 'r')
|
||||
odfs = odfmetaparser()
|
||||
parser = xml.sax.make_parser()
|
||||
@ -162,7 +176,90 @@ def get_metadata(stream):
|
||||
if data.has_key('language'):
|
||||
mi.language = data['language']
|
||||
if data.get('keywords', ''):
|
||||
mi.tags = data['keywords'].split(',')
|
||||
mi.tags = [x.strip() for x in data['keywords'].split(',') if x.strip()]
|
||||
opfmeta = False # we need this later for the cover
|
||||
opfnocover = False
|
||||
if data.get('opf.metadata','') == 'true':
|
||||
# custom metadata contains OPF information
|
||||
opfmeta = True
|
||||
if data.get('opf.titlesort', ''):
|
||||
mi.title_sort = data['opf.titlesort']
|
||||
if data.get('opf.authors', ''):
|
||||
mi.authors = string_to_authors(data['opf.authors'])
|
||||
if data.get('opf.authorsort', ''):
|
||||
mi.author_sort = data['opf.authorsort']
|
||||
if data.get('opf.isbn', ''):
|
||||
isbn = check_isbn(data['opf.isbn'])
|
||||
if isbn is not None:
|
||||
mi.isbn = isbn
|
||||
if data.get('opf.publisher', ''):
|
||||
mi.publisher = data['opf.publisher']
|
||||
if data.get('opf.pubdate', ''):
|
||||
mi.pubdate = parse_date(data['opf.pubdate'], assume_utc=True)
|
||||
if data.get('opf.series', ''):
|
||||
mi.series = data['opf.series']
|
||||
if data.get('opf.seriesindex', ''):
|
||||
try:
|
||||
mi.series_index = float(data['opf.seriesindex'])
|
||||
except ValueError:
|
||||
mi.series_index = 1.0
|
||||
if data.get('opf.language', ''):
|
||||
cl = canonicalize_lang(data['opf.language'])
|
||||
if cl:
|
||||
mi.languages = [cl]
|
||||
opfnocover = data.get('opf.nocover', 'false') == 'true'
|
||||
if not opfnocover:
|
||||
try:
|
||||
read_cover(stream, zin, mi, opfmeta, extract_cover)
|
||||
except:
|
||||
pass # Do not let an error reading the cover prevent reading other data
|
||||
|
||||
return mi
|
||||
|
||||
def read_cover(stream, zin, mi, opfmeta, extract_cover):
|
||||
# search for an draw:image in a draw:frame with the name 'opf.cover'
|
||||
# if opf.metadata prop is false, just use the first image that
|
||||
# has a proper size (borrowed from docx)
|
||||
otext = odLoad(stream)
|
||||
cover_href = None
|
||||
cover_data = None
|
||||
cover_frame = None
|
||||
for frm in otext.topnode.getElementsByType(odFrame):
|
||||
img = frm.getElementsByType(odImage)
|
||||
if len(img) > 0: # there should be only one
|
||||
i_href = img[0].getAttribute('href')
|
||||
try:
|
||||
raw = zin.read(i_href)
|
||||
except KeyError:
|
||||
continue
|
||||
try:
|
||||
width, height, fmt = identify_data(raw)
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
if opfmeta and frm.getAttribute('name').lower() == u'opf.cover':
|
||||
cover_href = i_href
|
||||
cover_data = (fmt, raw)
|
||||
cover_frame = frm.getAttribute('name') # could have upper case
|
||||
break
|
||||
if cover_href is None and 0.8 <= height/width <= 1.8 and height*width >= 12000:
|
||||
cover_href = i_href
|
||||
cover_data = (fmt, raw)
|
||||
if not opfmeta:
|
||||
break
|
||||
|
||||
if cover_href is not None:
|
||||
mi.cover = cover_href
|
||||
mi.odf_cover_frame = cover_frame
|
||||
if extract_cover:
|
||||
if not cover_data:
|
||||
raw = zin.read(cover_href)
|
||||
try:
|
||||
width, height, fmt = identify_data(raw)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
cover_data = (fmt, raw)
|
||||
mi.cover_data = cover_data
|
||||
|
||||
|
@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
from calibre import replace_entities
|
||||
from calibre.ebooks.metadata.toc import TOC
|
||||
from calibre.ebooks.mobi.reader.headers import NULL_INDEX
|
||||
from calibre.ebooks.mobi.reader.index import read_index
|
||||
@ -88,7 +89,8 @@ def build_toc(index_entries):
|
||||
for lvl in sorted(levels):
|
||||
for item in level_map[lvl]:
|
||||
parent = num_map[item['parent']]
|
||||
child = parent.add_item(item['href'], item['idtag'], item['text'])
|
||||
child = parent.add_item(item['href'], item['idtag'],
|
||||
replace_entities(item['text'], encoding=None))
|
||||
num_map[item['num']] = child
|
||||
|
||||
# Set play orders in depth first order
|
||||
|
@ -11,8 +11,9 @@ import re
|
||||
|
||||
from calibre.ebooks.oeb.base import (OEB_DOCS, XHTML, XHTML_NS, XML_NS,
|
||||
namespace, prefixname, urlnormalize)
|
||||
from calibre.ebooks import normalize
|
||||
from calibre.ebooks.mobi.mobiml import MBP_NS
|
||||
from calibre.ebooks.mobi.utils import is_guide_ref_start, utf8_text
|
||||
from calibre.ebooks.mobi.utils import is_guide_ref_start
|
||||
|
||||
from collections import defaultdict
|
||||
from urlparse import urldefrag
|
||||
@ -355,7 +356,7 @@ class Serializer(object):
|
||||
text = text.replace(u'\u00AD', '') # Soft-hyphen
|
||||
if quot:
|
||||
text = text.replace('"', '"')
|
||||
self.buf.write(utf8_text(text))
|
||||
self.buf.write(normalize(text).encode('utf-8'))
|
||||
|
||||
def fixup_links(self):
|
||||
'''
|
||||
|
@ -76,15 +76,13 @@ def tostring(raw, **kwargs):
|
||||
|
||||
class Chunk(object):
|
||||
|
||||
def __init__(self, raw, parent_tag):
|
||||
def __init__(self, raw, selector):
|
||||
self.raw = raw
|
||||
self.starts_tags = []
|
||||
self.ends_tags = []
|
||||
self.insert_pos = None
|
||||
self.parent_tag = parent_tag
|
||||
self.parent_is_body = False
|
||||
self.is_last_chunk = False
|
||||
self.is_first_chunk = False
|
||||
self.selector = "%s-//*[@aid='%s']"%selector
|
||||
|
||||
def __len__(self):
|
||||
return len(self.raw)
|
||||
@ -97,11 +95,6 @@ class Chunk(object):
|
||||
return 'Chunk(len=%r insert_pos=%r starts_tags=%r ends_tags=%r)'%(
|
||||
len(self.raw), self.insert_pos, self.starts_tags, self.ends_tags)
|
||||
|
||||
@property
|
||||
def selector(self):
|
||||
typ = 'S' if (self.is_last_chunk and not self.parent_is_body) else 'P'
|
||||
return "%s-//*[@aid='%s']"%(typ, self.parent_tag)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
class Skeleton(object):
|
||||
@ -251,13 +244,13 @@ class Chunker(object):
|
||||
|
||||
def step_into_tag(self, tag, chunks):
|
||||
aid = tag.get('aid')
|
||||
is_body = tag.tag == 'body'
|
||||
self.chunk_selector = ('P', aid)
|
||||
|
||||
first_chunk_idx = len(chunks)
|
||||
|
||||
# First handle any text
|
||||
if tag.text and tag.text.strip(): # Leave pure whitespace in the skel
|
||||
chunks.extend(self.chunk_up_text(tag.text, aid))
|
||||
chunks.extend(self.chunk_up_text(tag.text))
|
||||
tag.text = None
|
||||
|
||||
# Now loop over children
|
||||
@ -266,21 +259,21 @@ class Chunker(object):
|
||||
if child.tag == etree.Entity:
|
||||
chunks.append(raw)
|
||||
if child.tail:
|
||||
chunks.extend(self.chunk_up_text(child.tail, aid))
|
||||
chunks.extend(self.chunk_up_text(child.tail))
|
||||
continue
|
||||
raw = close_self_closing_tags(raw)
|
||||
if len(raw) > CHUNK_SIZE and child.get('aid', None):
|
||||
self.step_into_tag(child, chunks)
|
||||
if child.tail and child.tail.strip(): # Leave pure whitespace
|
||||
chunks.extend(self.chunk_up_text(child.tail, aid))
|
||||
chunks.extend(self.chunk_up_text(child.tail))
|
||||
child.tail = None
|
||||
else:
|
||||
if len(raw) > CHUNK_SIZE:
|
||||
self.log.warn('Tag %s has no aid and a too large chunk'
|
||||
' size. Adding anyway.'%child.tag)
|
||||
chunks.append(Chunk(raw, aid))
|
||||
chunks.append(Chunk(raw, self.chunk_selector))
|
||||
if child.tail:
|
||||
chunks.extend(self.chunk_up_text(child.tail, aid))
|
||||
chunks.extend(self.chunk_up_text(child.tail))
|
||||
tag.remove(child)
|
||||
|
||||
if len(chunks) <= first_chunk_idx and chunks:
|
||||
@ -293,12 +286,9 @@ class Chunker(object):
|
||||
my_chunks = chunks[first_chunk_idx:]
|
||||
if my_chunks:
|
||||
my_chunks[0].is_first_chunk = True
|
||||
my_chunks[-1].is_last_chunk = True
|
||||
if is_body:
|
||||
for chunk in my_chunks:
|
||||
chunk.parent_is_body = True
|
||||
self.chunk_selector = ('S', aid)
|
||||
|
||||
def chunk_up_text(self, text, parent_tag):
|
||||
def chunk_up_text(self, text):
|
||||
text = text.encode('utf-8')
|
||||
ans = []
|
||||
|
||||
@ -314,7 +304,7 @@ class Chunker(object):
|
||||
while rest:
|
||||
start, rest = split_multibyte_text(rest)
|
||||
ans.append(b'<span class="AmznBigTextBlock">' + start + '</span>')
|
||||
return [Chunk(x, parent_tag) for x in ans]
|
||||
return [Chunk(x, self.chunk_selector) for x in ans]
|
||||
|
||||
def merge_small_chunks(self, chunks):
|
||||
ans = chunks[:1]
|
||||
|
@ -10,6 +10,9 @@ import os
|
||||
|
||||
from lxml import etree
|
||||
from odf.odf2xhtml import ODF2XHTML
|
||||
from odf.opendocument import load as odLoad
|
||||
from odf.draw import Frame as odFrame, Image as odImage
|
||||
from odf.namespaces import TEXTNS as odTEXTNS
|
||||
|
||||
from calibre import CurrentDir, walk
|
||||
|
||||
@ -138,22 +141,84 @@ class Extract(ODF2XHTML):
|
||||
r.selectorText = '.'+replace_name
|
||||
return sheet.cssText, sel_map
|
||||
|
||||
def search_page_img(self, mi, log):
|
||||
for frm in self.document.topnode.getElementsByType(odFrame):
|
||||
try:
|
||||
if frm.getAttrNS(odTEXTNS,u'anchor-type') == 'page':
|
||||
log.warn('Document has Pictures anchored to Page, will all end up before first page!')
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def filter_cover(self, mi, log):
|
||||
# filter the Element tree (remove the detected cover)
|
||||
if mi.cover and mi.odf_cover_frame:
|
||||
for frm in self.document.topnode.getElementsByType(odFrame):
|
||||
# search the right frame
|
||||
if frm.getAttribute('name') == mi.odf_cover_frame:
|
||||
img = frm.getElementsByType(odImage)
|
||||
# only one draw:image allowed in the draw:frame
|
||||
if len(img) == 1 and img[0].getAttribute('href') == mi.cover:
|
||||
# ok, this is the right frame with the right image
|
||||
# check if there are more childs
|
||||
if len(frm.childNodes) != 1:
|
||||
break
|
||||
# check if the parent paragraph more childs
|
||||
para = frm.parentNode
|
||||
if para.tagName != 'text:p' or len(para.childNodes) != 1:
|
||||
break
|
||||
# now it should be safe to remove the text:p
|
||||
parent = para.parentNode
|
||||
parent.removeChild(para)
|
||||
log("Removed cover image paragraph from document...")
|
||||
break
|
||||
|
||||
def filter_load(self, odffile, mi, log):
|
||||
""" This is an adaption from ODF2XHTML. It adds a step between
|
||||
load and parse of the document where the Element tree can be
|
||||
modified.
|
||||
"""
|
||||
# first load the odf structure
|
||||
self.lines = []
|
||||
self._wfunc = self._wlines
|
||||
if isinstance(odffile, basestring) \
|
||||
or hasattr(odffile, 'read'): # Added by Kovid
|
||||
self.document = odLoad(odffile)
|
||||
else:
|
||||
self.document = odffile
|
||||
# filter stuff
|
||||
self.search_page_img(mi, log)
|
||||
try:
|
||||
self.filter_cover(mi, log)
|
||||
except:
|
||||
pass
|
||||
# parse the modified tree and generate xhtml
|
||||
self._walknode(self.document.topnode)
|
||||
|
||||
def __call__(self, stream, odir, log):
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.ebooks.metadata.odt import get_metadata
|
||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||
|
||||
|
||||
if not os.path.exists(odir):
|
||||
os.makedirs(odir)
|
||||
with CurrentDir(odir):
|
||||
log('Extracting ODT file...')
|
||||
html = self.odf2xhtml(stream)
|
||||
stream.seek(0)
|
||||
mi = get_metadata(stream, 'odt')
|
||||
if not mi.title:
|
||||
mi.title = _('Unknown')
|
||||
if not mi.authors:
|
||||
mi.authors = [_('Unknown')]
|
||||
self.filter_load(stream, mi, log)
|
||||
html = self.xhtml()
|
||||
# A blanket img specification like this causes problems
|
||||
# with EPUB output as the containing element often has
|
||||
# an absolute height and width set that is larger than
|
||||
# the available screen real estate
|
||||
html = html.replace('img { width: 100%; height: 100%; }', '')
|
||||
# odf2xhtml creates empty title tag
|
||||
html = html.replace('<title></title>','<title>%s</title>'%(mi.title,))
|
||||
try:
|
||||
html = self.fix_markup(html, log)
|
||||
except:
|
||||
@ -162,12 +227,6 @@ class Extract(ODF2XHTML):
|
||||
f.write(html.encode('utf-8'))
|
||||
zf = ZipFile(stream, 'r')
|
||||
self.extract_pictures(zf)
|
||||
stream.seek(0)
|
||||
mi = get_metadata(stream, 'odt')
|
||||
if not mi.title:
|
||||
mi.title = _('Unknown')
|
||||
if not mi.authors:
|
||||
mi.authors = [_('Unknown')]
|
||||
opf = OPFCreator(os.path.abspath(os.getcwdu()), mi)
|
||||
opf.create_manifest([(os.path.abspath(f), None) for f in
|
||||
walk(os.getcwdu())])
|
||||
|
@ -15,7 +15,7 @@ from calibre.constants import iswindows, isosx
|
||||
from calibre.customize.ui import is_disabled
|
||||
from calibre.devices.bambook.driver import BAMBOOK
|
||||
from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog
|
||||
from calibre.gui2 import info_dialog
|
||||
from calibre.gui2 import info_dialog, question_dialog
|
||||
|
||||
class ShareConnMenu(QMenu): # {{{
|
||||
|
||||
@ -28,6 +28,9 @@ class ShareConnMenu(QMenu): # {{{
|
||||
control_smartdevice = pyqtSignal()
|
||||
dont_add_to = frozenset(['context-menu-device'])
|
||||
|
||||
DEVICE_MSGS = [_('Start wireless device connection'),
|
||||
_('Stop wireless device connection')]
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QMenu.__init__(self, parent)
|
||||
mitem = self.addAction(QIcon(I('devices/folder.png')), _('Connect to folder'))
|
||||
@ -59,8 +62,8 @@ class ShareConnMenu(QMenu): # {{{
|
||||
self.toggle_server_action.triggered.connect(lambda x:
|
||||
self.toggle_server.emit())
|
||||
self.control_smartdevice_action = \
|
||||
self.addAction(QIcon(I('dot_green.png')),
|
||||
_('Control Smart Device Connections'))
|
||||
self.addAction(QIcon(I('dot_red.png')),
|
||||
self.DEVICE_MSGS[0])
|
||||
self.control_smartdevice_action.triggered.connect(lambda x:
|
||||
self.control_smartdevice.emit())
|
||||
self.addSeparator()
|
||||
@ -215,17 +218,33 @@ class ConnectShareAction(InterfaceAction):
|
||||
self.stopping_msg.accept()
|
||||
|
||||
def control_smartdevice(self):
|
||||
dm = self.gui.device_manager
|
||||
running = dm.is_running('smartdevice')
|
||||
if running:
|
||||
dm.stop_plugin('smartdevice')
|
||||
if dm.get_option('smartdevice', 'autostart'):
|
||||
if not question_dialog(self.gui, _('Disable autostart'),
|
||||
_('Do you want wireless device connections to be'
|
||||
' started automatically when calibre starts?')):
|
||||
dm.set_option('smartdevice', 'autostart', False)
|
||||
else:
|
||||
sd_dialog = SmartdeviceDialog(self.gui)
|
||||
sd_dialog.exec_()
|
||||
self.set_smartdevice_icon()
|
||||
self.set_smartdevice_action_state()
|
||||
|
||||
def check_smartdevice_menus(self):
|
||||
if not self.gui.device_manager.is_enabled('smartdevice'):
|
||||
self.share_conn_menu.hide_smartdevice_menus()
|
||||
|
||||
def set_smartdevice_icon(self):
|
||||
def set_smartdevice_action_state(self):
|
||||
from calibre.utils.mdns import get_external_ip
|
||||
running = self.gui.device_manager.is_running('smartdevice')
|
||||
if running:
|
||||
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png')))
|
||||
if not running:
|
||||
text = self.share_conn_menu.DEVICE_MSGS[0]
|
||||
else:
|
||||
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png')))
|
||||
text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s]'%get_external_ip()
|
||||
icon = 'green' if running else 'red'
|
||||
ac = self.share_conn_menu.control_smartdevice_action
|
||||
ac.setIcon(QIcon(I('dot_%s.png'%icon)))
|
||||
ac.setText(text)
|
||||
|
||||
|
@ -73,7 +73,9 @@ class SaveToDiskAction(InterfaceAction):
|
||||
self.save_to_disk(False, single_dir=True,
|
||||
single_format=prefs['output_format'])
|
||||
|
||||
def save_to_disk(self, checked, single_dir=False, single_format=None):
|
||||
def save_to_disk(self, checked, single_dir=False, single_format=None,
|
||||
rows=None, write_opf=None, save_cover=None):
|
||||
if rows is None:
|
||||
rows = self.gui.current_view().selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
return error_dialog(self.gui, _('Cannot save to disk'),
|
||||
@ -105,6 +107,10 @@ class SaveToDiskAction(InterfaceAction):
|
||||
opts.write_opf = False
|
||||
opts.template = opts.send_template
|
||||
opts.single_dir = single_dir
|
||||
if write_opf is not None:
|
||||
opts.write_opf = write_opf
|
||||
if save_cover is not None:
|
||||
opts.save_cover = save_cover
|
||||
self._saver = Saver(self.gui, self.gui.library_view.model().db,
|
||||
Dispatcher(self._books_saved), rows, path, opts,
|
||||
spare_server=self.gui.spare_server)
|
||||
@ -114,6 +120,13 @@ class SaveToDiskAction(InterfaceAction):
|
||||
self.gui.device_manager.save_books(
|
||||
Dispatcher(self.books_saved), paths, path)
|
||||
|
||||
def save_library_format_by_ids(self, book_ids, fmt, single_dir=True):
|
||||
if isinstance(book_ids, int):
|
||||
book_ids = [book_ids]
|
||||
rows = list(self.gui.library_view.ids_to_rows(book_ids).itervalues())
|
||||
rows = [self.gui.library_view.model().index(r, 0) for r in rows]
|
||||
self.save_to_disk(True, single_dir=single_dir, single_format=fmt,
|
||||
rows=rows, write_opf=False, save_cover=False)
|
||||
|
||||
def _books_saved(self, path, failures, error):
|
||||
self._saver = None
|
||||
|
@ -383,6 +383,7 @@ class BookInfo(QWebView):
|
||||
|
||||
link_clicked = pyqtSignal(object)
|
||||
remove_format = pyqtSignal(int, object)
|
||||
save_format = pyqtSignal(int, object)
|
||||
|
||||
def __init__(self, vertical, parent=None):
|
||||
QWebView.__init__(self, parent)
|
||||
@ -396,16 +397,23 @@ class BookInfo(QWebView):
|
||||
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||
self.page().setPalette(palette)
|
||||
self.css = P('templates/book_details.css', data=True).decode('utf-8')
|
||||
self.remove_format_action = QAction(QIcon(I('trash.png')),
|
||||
'', self)
|
||||
self.remove_format_action.current_fmt = None
|
||||
self.remove_format_action.triggered.connect(self.remove_format_triggerred)
|
||||
for x, icon in [('remove', 'trash.png'), ('save', 'save.png')]:
|
||||
ac = QAction(QIcon(I(icon)), '', self)
|
||||
ac.current_fmt = None
|
||||
ac.triggered.connect(getattr(self, '%s_format_triggerred'%x))
|
||||
setattr(self, '%s_format_action'%x, ac)
|
||||
|
||||
def remove_format_triggerred(self):
|
||||
f = self.remove_format_action.current_fmt
|
||||
def context_action_triggered(self, which):
|
||||
f = getattr(self, '%s_format_action'%which).current_fmt
|
||||
if f:
|
||||
book_id, fmt = f
|
||||
self.remove_format.emit(book_id, fmt)
|
||||
getattr(self, '%s_format'%which).emit(book_id, fmt)
|
||||
|
||||
def remove_format_triggerred(self):
|
||||
self.context_action_triggered('remove')
|
||||
|
||||
def save_format_triggerred(self):
|
||||
self.context_action_triggered('save')
|
||||
|
||||
def link_activated(self, link):
|
||||
self._link_clicked = True
|
||||
@ -449,10 +457,12 @@ class BookInfo(QWebView):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
self.remove_format_action.current_fmt = (book_id, fmt)
|
||||
self.remove_format_action.setText(_('Delete the %s format')%parts[
|
||||
2])
|
||||
menu.addAction(self.remove_format_action)
|
||||
for a, t in [('remove', _('Delete the %s format')),
|
||||
('save', _('Save the %s format to disk'))]:
|
||||
ac = getattr(self, '%s_format_action'%a)
|
||||
ac.current_fmt = (book_id, fmt)
|
||||
ac.setText(t%parts[2])
|
||||
menu.addAction(ac)
|
||||
if len(menu.actions()) > 0:
|
||||
menu.exec_(ev.globalPos())
|
||||
|
||||
@ -551,6 +561,7 @@ class BookDetails(QWidget): # {{{
|
||||
open_containing_folder = pyqtSignal(int)
|
||||
view_specific_format = pyqtSignal(int, object)
|
||||
remove_specific_format = pyqtSignal(int, object)
|
||||
save_specific_format = pyqtSignal(int, object)
|
||||
remote_file_dropped = pyqtSignal(object, object)
|
||||
files_dropped = pyqtSignal(object, object)
|
||||
cover_changed = pyqtSignal(object, object)
|
||||
@ -618,6 +629,7 @@ class BookDetails(QWidget): # {{{
|
||||
self._layout.addWidget(self.book_info)
|
||||
self.book_info.link_clicked.connect(self.handle_click)
|
||||
self.book_info.remove_format.connect(self.remove_specific_format)
|
||||
self.book_info.save_format.connect(self.save_specific_format)
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def handle_click(self, link):
|
||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import re, os
|
||||
|
||||
from lxml import html
|
||||
import sip
|
||||
|
||||
from PyQt4.Qt import (QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit,
|
||||
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl,
|
||||
@ -42,6 +43,7 @@ class PageAction(QAction): # {{{
|
||||
self.page_action.trigger()
|
||||
|
||||
def update_state(self, *args):
|
||||
if sip.isdeleted(self) or sip.isdeleted(self.page_action): return
|
||||
if self.isCheckable():
|
||||
self.setChecked(self.page_action.isChecked())
|
||||
self.setEnabled(self.page_action.isEnabled())
|
||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import shutil, importlib
|
||||
import shutil
|
||||
|
||||
from PyQt4.Qt import QString, SIGNAL
|
||||
|
||||
@ -86,17 +86,9 @@ class BulkConfig(Config):
|
||||
sd = widget_factory(StructureDetectionWidget)
|
||||
toc = widget_factory(TOCWidget)
|
||||
|
||||
output_widget = None
|
||||
name = self.plumber.output_plugin.name.lower().replace(' ', '_')
|
||||
try:
|
||||
output_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = output_widget.PluginWidget
|
||||
pw.ICON = I('back.png')
|
||||
pw.HELP = _('Options specific to the output format.')
|
||||
output_widget = widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
output_widget = self.plumber.output_plugin.gui_configuration_widget(
|
||||
self.stack, self.plumber.get_option_by_name,
|
||||
self.plumber.get_option_help, self.db)
|
||||
|
||||
while True:
|
||||
c = self.stack.currentWidget()
|
||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import cPickle, shutil, importlib
|
||||
import cPickle, shutil
|
||||
|
||||
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
||||
|
||||
@ -187,29 +187,12 @@ class Config(ResizableDialog, Ui_Dialog):
|
||||
toc = widget_factory(TOCWidget)
|
||||
debug = widget_factory(DebugWidget)
|
||||
|
||||
output_widget = None
|
||||
name = self.plumber.output_plugin.name.lower().replace(' ', '_')
|
||||
try:
|
||||
output_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = output_widget.PluginWidget
|
||||
pw.ICON = I('back.png')
|
||||
pw.HELP = _('Options specific to the output format.')
|
||||
output_widget = widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
input_widget = None
|
||||
name = self.plumber.input_plugin.name.lower().replace(' ', '_')
|
||||
try:
|
||||
input_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = input_widget.PluginWidget
|
||||
pw.ICON = I('forward.png')
|
||||
pw.HELP = _('Options specific to the input format.')
|
||||
input_widget = widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
output_widget = self.plumber.output_plugin.gui_configuration_widget(
|
||||
self.stack, self.plumber.get_option_by_name,
|
||||
self.plumber.get_option_help, self.db, self.book_id)
|
||||
input_widget = self.plumber.input_plugin.gui_configuration_widget(
|
||||
self.stack, self.plumber.get_option_by_name,
|
||||
self.plumber.get_option_help, self.db, self.book_id)
|
||||
while True:
|
||||
c = self.stack.currentWidget()
|
||||
if not c: break
|
||||
|
@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from PyQt4.QtGui import QDialog, QLineEdit
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.Qt import (QDialog, QLineEdit, Qt)
|
||||
|
||||
from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog
|
||||
|
||||
@ -13,68 +16,38 @@ class SmartdeviceDialog(QDialog, Ui_Dialog):
|
||||
Ui_Dialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.msg.setText(
|
||||
_('This dialog starts and stops the smart device app interface. '
|
||||
'When you start the interface, you might see some messages from '
|
||||
'your computer\'s firewall or anti-virus manager asking you '
|
||||
'if it is OK for calibre to connect to the network. <B>Please '
|
||||
'answer yes</b>. If you do not, the app will not work. It will '
|
||||
'be unable to connect to calibre.'))
|
||||
|
||||
self.password_box.setToolTip('<p>' +
|
||||
_('Use a password if calibre is running on a network that '
|
||||
'is not secure. For example, if you run calibre on a laptop, '
|
||||
'use that laptop in an airport, and want to connect your '
|
||||
'smart device to calibre, you should use a password.') + '</p>')
|
||||
|
||||
self.run_box.setToolTip('<p>' +
|
||||
_('Check this box to allow calibre to accept connections from the '
|
||||
'smart device. Uncheck the box to prevent connections.') + '</p>')
|
||||
|
||||
self.autostart_box.setToolTip('<p>' +
|
||||
_('Check this box if you want calibre to automatically start the '
|
||||
'smart device interface when calibre starts. You should not do '
|
||||
'this if you are using a network that is not secure and you '
|
||||
'are not setting a password.') + '</p>')
|
||||
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password)
|
||||
self.autostart_box.stateChanged.connect(self.autostart_changed)
|
||||
self.show_password.stateChanged[int].connect(self.toggle_password)
|
||||
|
||||
self.device_manager = parent.device_manager
|
||||
if self.device_manager.is_running('smartdevice'):
|
||||
self.run_box.setChecked(True)
|
||||
else:
|
||||
self.run_box.setChecked(False)
|
||||
|
||||
if self.device_manager.get_option('smartdevice', 'autostart'):
|
||||
self.autostart_box.setChecked(True)
|
||||
self.run_box.setChecked(True)
|
||||
self.run_box.setEnabled(False)
|
||||
|
||||
pw = self.device_manager.get_option('smartdevice', 'password')
|
||||
if pw:
|
||||
self.password_box.setText(pw)
|
||||
|
||||
def autostart_changed(self):
|
||||
if self.autostart_box.isChecked():
|
||||
self.run_box.setChecked(True)
|
||||
self.run_box.setEnabled(False)
|
||||
else:
|
||||
self.run_box.setEnabled(True)
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def toggle_password(self, state):
|
||||
if state == Qt.Unchecked:
|
||||
self.password_box.setEchoMode(QLineEdit.Password)
|
||||
else:
|
||||
self.password_box.setEchoMode(QLineEdit.Normal)
|
||||
self.password_box.setEchoMode(QLineEdit.Password if state ==
|
||||
Qt.Unchecked else QLineEdit.Normal)
|
||||
|
||||
def accept(self):
|
||||
self.device_manager.set_option('smartdevice', 'password',
|
||||
unicode(self.password_box.text()))
|
||||
self.device_manager.set_option('smartdevice', 'autostart',
|
||||
self.autostart_box.isChecked())
|
||||
if self.run_box.isChecked():
|
||||
self.device_manager.start_plugin('smartdevice')
|
||||
else:
|
||||
self.device_manager.stop_plugin('smartdevice')
|
||||
|
||||
QDialog.accept(self)
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>209</height>
|
||||
<width>612</width>
|
||||
<height>226</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -15,23 +15,26 @@
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
|
||||
<normaloff>:/images/devices/galaxy_s3.png</normaloff>:/images/devices/galaxy_s3.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="4" column="1">
|
||||
<widget class="QCheckBox" name="autostart_box">
|
||||
<property name="text">
|
||||
<string>&Automatically allow connections at startup</string>
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="msg">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="label_43">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel { margin-bottom: 1ex; }</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><p>Start wireless device connections.
|
||||
<p>You may see some messages from your computer's firewall or anti-virus manager asking you if it is OK for calibre to connect to the network. <b>Please answer yes</b>. If you do not, wireless connections will not work.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -51,27 +54,10 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="run_box">
|
||||
<property name="text">
|
||||
<string>&Allow connections</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="msg">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Password:</string>
|
||||
<string>Optional &password:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>password_box</cstring>
|
||||
@ -85,7 +71,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="3">
|
||||
<item row="4" column="0" colspan="3">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@ -95,6 +81,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="3">
|
||||
<widget class="QCheckBox" name="autostart_box">
|
||||
<property name="text">
|
||||
<string>&Automatically allow connections at calibre startup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
|
@ -135,21 +135,22 @@ def dnd_has_extension(md, extensions):
|
||||
prints('Debugging DND event')
|
||||
for f in md.formats():
|
||||
f = unicode(f)
|
||||
prints(f, repr(data_as_string(f, md))[:300], '\n')
|
||||
raw = data_as_string(f, md)
|
||||
prints(f, len(raw), repr(raw[:300]), '\n')
|
||||
print ()
|
||||
if has_firefox_ext(md, extensions):
|
||||
return True
|
||||
if md.hasUrls():
|
||||
urls = [unicode(u.toString()) for u in
|
||||
md.urls()]
|
||||
purls = [urlparse(u) for u in urls]
|
||||
paths = [u2p(x) for x in purls]
|
||||
paths = [urlparse(u).path for u in urls]
|
||||
exts = frozenset([posixpath.splitext(u)[1][1:].lower() for u in
|
||||
paths if u])
|
||||
if DEBUG:
|
||||
prints('URLS:', urls)
|
||||
prints('Paths:', paths)
|
||||
prints('Extensions:', exts)
|
||||
|
||||
exts = frozenset([posixpath.splitext(u)[1][1:].lower() for u in
|
||||
paths])
|
||||
return bool(exts.intersection(frozenset(extensions)))
|
||||
return False
|
||||
|
||||
|
@ -267,6 +267,8 @@ class LayoutMixin(object): # {{{
|
||||
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
|
||||
self.book_details.remove_specific_format.connect(
|
||||
self.iactions['Remove Books'].remove_format_by_id)
|
||||
self.book_details.save_specific_format.connect(
|
||||
self.iactions['Save To Disk'].save_library_format_by_ids)
|
||||
|
||||
m = self.library_view.model()
|
||||
if m.rowCount(None) > 0:
|
||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os, itertools, operator
|
||||
from functools import partial
|
||||
from future_builtins import map
|
||||
from collections import OrderedDict
|
||||
|
||||
from PyQt4.Qt import (QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal,
|
||||
QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication,
|
||||
@ -793,6 +794,17 @@ class BooksView(QTableView): # {{{
|
||||
sm = self.selectionModel()
|
||||
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
||||
|
||||
def ids_to_rows(self, ids):
|
||||
row_map = OrderedDict()
|
||||
ids = frozenset(ids)
|
||||
m = self.model()
|
||||
for row in xrange(m.rowCount(QModelIndex())):
|
||||
if len(row_map) >= len(ids): break
|
||||
c = m.id(row)
|
||||
if c in ids:
|
||||
row_map[c] = row
|
||||
return row_map
|
||||
|
||||
def select_rows(self, identifiers, using_ids=True, change_current=True,
|
||||
scroll=True):
|
||||
'''
|
||||
|
@ -32,6 +32,8 @@ class SonyStore(BasicStoreConfig, StorePlugin):
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
else:
|
||||
open_url(QUrl('http://ebookstore.sony.com'))
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://ebookstore.sony.com/search?keyword=%s'%urllib.quote_plus(
|
||||
|
@ -9,7 +9,6 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback, cPickle, copy
|
||||
from itertools import repeat
|
||||
|
||||
from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt,
|
||||
QMimeData, QModelIndex, pyqtSignal, QObject)
|
||||
@ -17,7 +16,7 @@ from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt,
|
||||
from calibre.gui2 import NONE, gprefs, config, error_dialog
|
||||
from calibre.library.database2 import Tag
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.icu import sort_key, lower, strcmp, contractions
|
||||
from calibre.utils.icu import sort_key, lower, strcmp, collation_order
|
||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.utils.formatter import EvalFormatter
|
||||
@ -258,16 +257,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
self.hidden_categories.add(cat)
|
||||
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._run_rebuild()
|
||||
self.endResetModel()
|
||||
@ -416,53 +405,23 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
tt = key if in_uc 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 = {}
|
||||
# Build a list of 'equal' first letters by noticing changes
|
||||
# in ICU's 'ordinal' for the first letter. In this case, the
|
||||
# first letter can actually be more than one letter long.
|
||||
cl_list = [None] * len(data[key])
|
||||
last_ordnum = 0
|
||||
for idx,tag in enumerate(data[key]):
|
||||
if not tag.sort:
|
||||
c = ' '
|
||||
else:
|
||||
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:
|
||||
chardict[c] = [idx, idx]
|
||||
else:
|
||||
chardict[c][1] = idx
|
||||
|
||||
# sort the ranges to facilitate detecting overlap
|
||||
if len(chardict) == 1 and ' ' in chardict:
|
||||
# The category could not be partitioned.
|
||||
collapse_model = 'disable'
|
||||
else:
|
||||
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
|
||||
|
||||
if len(data[key]) > 0:
|
||||
c = tag.sort
|
||||
ordnum, ordlen = collation_order(c)
|
||||
if last_ordnum != ordnum:
|
||||
last_c = icu_upper(c[0:ordlen])
|
||||
last_ordnum = ordnum
|
||||
cl_list[idx] = last_c
|
||||
top_level_component = 'z' + data[key][0].original_name
|
||||
else:
|
||||
top_level_component = ''
|
||||
|
||||
last_idx = -collapse
|
||||
category_is_hierarchical = not (
|
||||
key in ['authors', 'publisher', 'news', 'formats', 'rating'] or
|
||||
|
@ -339,14 +339,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
if config['autolaunch_server']:
|
||||
self.start_content_server()
|
||||
|
||||
smartdevice_actions = self.iactions['Connect Share']
|
||||
smartdevice_actions.check_smartdevice_menus()
|
||||
smartdevice_action = self.iactions['Connect Share']
|
||||
smartdevice_action.check_smartdevice_menus()
|
||||
if self.device_manager.get_option('smartdevice', 'autostart'):
|
||||
try:
|
||||
self.device_manager.start_plugin('smartdevice')
|
||||
except:
|
||||
pass
|
||||
smartdevice_actions.set_smartdevice_icon()
|
||||
smartdevice_action.set_smartdevice_action_state()
|
||||
|
||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||
|
||||
|
@ -8,11 +8,13 @@ __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import zipfile
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QFont, QVariant, QDialog
|
||||
from PyQt4.Qt import (QFont, QVariant, QDialog, Qt, QColor, QColorDialog,
|
||||
QMenu, QInputDialog)
|
||||
|
||||
from calibre.constants import iswindows, isxp
|
||||
from calibre.utils.config import Config, StringConfig
|
||||
from calibre.utils.config import Config, StringConfig, JSONConfig
|
||||
from calibre.gui2.shortcuts import ShortcutConfig
|
||||
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
||||
from calibre.utils.localization import get_language
|
||||
@ -58,6 +60,8 @@ def config(defaults=None):
|
||||
c.add_opt('top_margin', default=20)
|
||||
c.add_opt('side_margin', default=40)
|
||||
c.add_opt('bottom_margin', default=20)
|
||||
c.add_opt('text_color', default=None)
|
||||
c.add_opt('background_color', default=None)
|
||||
|
||||
fonts = c.add_group('FONTS', _('Font options'))
|
||||
fonts('serif_family', default='Times New Roman' if iswindows else 'Liberation Serif',
|
||||
@ -78,7 +82,92 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
for x in ('text', 'background'):
|
||||
getattr(self, 'change_%s_color_button'%x).clicked.connect(
|
||||
partial(self.change_color, x, reset=False))
|
||||
getattr(self, 'reset_%s_color_button'%x).clicked.connect(
|
||||
partial(self.change_color, x, reset=True))
|
||||
self.css.setToolTip(_('Set the user CSS stylesheet. This can be used to customize the look of all books.'))
|
||||
|
||||
self.shortcuts = shortcuts
|
||||
self.shortcut_config = ShortcutConfig(shortcuts, parent=self)
|
||||
bb = self.buttonBox
|
||||
bb.button(bb.RestoreDefaults).clicked.connect(self.restore_defaults)
|
||||
|
||||
with zipfile.ZipFile(P('viewer/hyphenate/patterns.zip',
|
||||
allow_user_override=False), 'r') as zf:
|
||||
pats = [x.split('.')[0].replace('-', '_') for x in zf.namelist()]
|
||||
names = list(map(get_language, pats))
|
||||
pmap = {}
|
||||
for i in range(len(pats)):
|
||||
pmap[names[i]] = pats[i]
|
||||
for x in sorted(names):
|
||||
self.hyphenate_default_lang.addItem(x, QVariant(pmap[x]))
|
||||
self.hyphenate_pats = pats
|
||||
self.hyphenate_names = names
|
||||
p = self.tabs.widget(1)
|
||||
p.layout().addWidget(self.shortcut_config)
|
||||
|
||||
if isxp:
|
||||
self.hyphenate.setVisible(False)
|
||||
self.hyphenate_default_lang.setVisible(False)
|
||||
self.hyphenate_label.setVisible(False)
|
||||
|
||||
self.themes = JSONConfig('viewer_themes')
|
||||
self.save_theme_button.clicked.connect(self.save_theme)
|
||||
self.load_theme_button.m = m = QMenu()
|
||||
self.load_theme_button.setMenu(m)
|
||||
m.triggered.connect(self.load_theme)
|
||||
self.delete_theme_button.m = m = QMenu()
|
||||
self.delete_theme_button.setMenu(m)
|
||||
m.triggered.connect(self.delete_theme)
|
||||
|
||||
opts = config().parse()
|
||||
self.load_options(opts)
|
||||
self.init_load_themes()
|
||||
|
||||
def save_theme(self):
|
||||
themename, ok = QInputDialog.getText(self, _('Theme name'),
|
||||
_('Choose a name for this theme'))
|
||||
if not ok: return
|
||||
themename = unicode(themename).strip()
|
||||
if not themename: return
|
||||
c = config('')
|
||||
c.add_opt('theme_name_xxx', default=themename)
|
||||
self.save_options(c)
|
||||
self.themes['theme_'+themename] = c.src
|
||||
self.init_load_themes()
|
||||
self.theming_message.setText(_('Saved settings as the theme named: %s')%
|
||||
themename)
|
||||
|
||||
def init_load_themes(self):
|
||||
for x in ('load', 'delete'):
|
||||
m = getattr(self, '%s_theme_button'%x).menu()
|
||||
m.clear()
|
||||
for x in self.themes.iterkeys():
|
||||
title = x[len('theme_'):]
|
||||
ac = m.addAction(title)
|
||||
ac.theme_id = x
|
||||
|
||||
def load_theme(self, ac):
|
||||
theme = ac.theme_id
|
||||
raw = self.themes[theme]
|
||||
self.load_options(config(raw).parse())
|
||||
self.theming_message.setText(_('Loaded settings from the theme %s')%
|
||||
theme[len('theme_'):])
|
||||
|
||||
def delete_theme(self, ac):
|
||||
theme = ac.theme_id
|
||||
del self.themes[theme]
|
||||
self.init_load_themes()
|
||||
self.theming_message.setText(_('Deleted the theme named: %s')%
|
||||
theme[len('theme_'):])
|
||||
|
||||
def restore_defaults(self):
|
||||
opts = config('').parse()
|
||||
self.load_options(opts)
|
||||
|
||||
def load_options(self, opts):
|
||||
self.opt_remember_window_size.setChecked(opts.remember_window_size)
|
||||
self.opt_remember_current_page.setChecked(opts.remember_current_page)
|
||||
self.opt_wheel_flips_pages.setChecked(opts.wheel_flips_pages)
|
||||
@ -94,19 +183,11 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
self.mono_family.setCurrentFont(QFont(opts.mono_family))
|
||||
self.default_font_size.setValue(opts.default_font_size)
|
||||
self.mono_font_size.setValue(opts.mono_font_size)
|
||||
self.standard_font.setCurrentIndex({'serif':0, 'sans':1, 'mono':2}[opts.standard_font])
|
||||
self.standard_font.setCurrentIndex(
|
||||
{'serif':0, 'sans':1, 'mono':2}[opts.standard_font])
|
||||
self.css.setPlainText(opts.user_css)
|
||||
self.css.setToolTip(_('Set the user CSS stylesheet. This can be used to customize the look of all books.'))
|
||||
self.max_fs_width.setValue(opts.max_fs_width)
|
||||
with zipfile.ZipFile(P('viewer/hyphenate/patterns.zip',
|
||||
allow_user_override=False), 'r') as zf:
|
||||
pats = [x.split('.')[0].replace('-', '_') for x in zf.namelist()]
|
||||
names = list(map(get_language, pats))
|
||||
pmap = {}
|
||||
for i in range(len(pats)):
|
||||
pmap[names[i]] = pats[i]
|
||||
for x in sorted(names):
|
||||
self.hyphenate_default_lang.addItem(x, QVariant(pmap[x]))
|
||||
pats, names = self.hyphenate_pats, self.hyphenate_names
|
||||
try:
|
||||
idx = pats.index(opts.hyphenate_default_lang)
|
||||
except ValueError:
|
||||
@ -115,21 +196,42 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
self.hyphenate_default_lang.setCurrentIndex(idx)
|
||||
self.hyphenate.setChecked(opts.hyphenate)
|
||||
self.hyphenate_default_lang.setEnabled(opts.hyphenate)
|
||||
self.shortcuts = shortcuts
|
||||
self.shortcut_config = ShortcutConfig(shortcuts, parent=self)
|
||||
p = self.tabs.widget(1)
|
||||
p.layout().addWidget(self.shortcut_config)
|
||||
self.opt_fit_images.setChecked(opts.fit_images)
|
||||
if isxp:
|
||||
self.hyphenate.setVisible(False)
|
||||
self.hyphenate_default_lang.setVisible(False)
|
||||
self.hyphenate_label.setVisible(False)
|
||||
self.opt_fullscreen_clock.setChecked(opts.fullscreen_clock)
|
||||
self.opt_cols_per_screen.setValue(opts.cols_per_screen)
|
||||
self.opt_override_book_margins.setChecked(not opts.use_book_margins)
|
||||
for x in ('top', 'bottom', 'side'):
|
||||
getattr(self, 'opt_%s_margin'%x).setValue(getattr(opts,
|
||||
x+'_margin'))
|
||||
for x in ('text', 'background'):
|
||||
setattr(self, 'current_%s_color'%x, getattr(opts, '%s_color'%x))
|
||||
self.update_sample_colors()
|
||||
|
||||
def change_color(self, which, reset=False):
|
||||
if reset:
|
||||
setattr(self, 'current_%s_color'%which, None)
|
||||
else:
|
||||
initial = getattr(self, 'current_%s_color'%which)
|
||||
if initial:
|
||||
initial = QColor(initial)
|
||||
else:
|
||||
initial = Qt.black if which == 'text' else Qt.white
|
||||
title = (_('Choose text color') if which == 'text' else
|
||||
_('Choose background color'))
|
||||
col = QColorDialog.getColor(initial, self,
|
||||
title, QColorDialog.ShowAlphaChannel)
|
||||
if col.isValid():
|
||||
name = unicode(col.name())
|
||||
setattr(self, 'current_%s_color'%which, name)
|
||||
self.update_sample_colors()
|
||||
|
||||
def update_sample_colors(self):
|
||||
for x in ('text', 'background'):
|
||||
val = getattr(self, 'current_%s_color'%x)
|
||||
if not val: val = 'inherit' if x == 'text' else 'transparent'
|
||||
ss = 'QLabel { %s: %s }'%('background-color' if x == 'background'
|
||||
else 'color', val)
|
||||
getattr(self, '%s_color_sample'%x).setStyleSheet(ss)
|
||||
|
||||
def accept(self, *args):
|
||||
if self.shortcut_config.is_editing:
|
||||
@ -139,13 +241,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
' first complete that, by clicking outside the '
|
||||
' shortcut editing box.'), show=True)
|
||||
return
|
||||
c = config()
|
||||
self.save_options(config())
|
||||
return QDialog.accept(self, *args)
|
||||
|
||||
def save_options(self, c):
|
||||
c.set('serif_family', unicode(self.serif_family.currentFont().family()))
|
||||
c.set('sans_family', unicode(self.sans_family.currentFont().family()))
|
||||
c.set('mono_family', unicode(self.mono_family.currentFont().family()))
|
||||
c.set('default_font_size', self.default_font_size.value())
|
||||
c.set('mono_font_size', self.mono_font_size.value())
|
||||
c.set('standard_font', {0:'serif', 1:'sans', 2:'mono'}[self.standard_font.currentIndex()])
|
||||
c.set('standard_font', {0:'serif', 1:'sans', 2:'mono'}[
|
||||
self.standard_font.currentIndex()])
|
||||
c.set('user_css', unicode(self.css.toPlainText()))
|
||||
c.set('remember_window_size', self.opt_remember_window_size.isChecked())
|
||||
c.set('fit_images', self.opt_fit_images.isChecked())
|
||||
@ -165,9 +271,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
c.set('cols_per_screen', int(self.opt_cols_per_screen.value()))
|
||||
c.set('use_book_margins', not
|
||||
self.opt_override_book_margins.isChecked())
|
||||
c.set('text_color', self.current_text_color)
|
||||
c.set('background_color', self.current_background_color)
|
||||
for x in ('top', 'bottom', 'side'):
|
||||
c.set(x+'_margin', int(getattr(self, 'opt_%s_margin'%x).value()))
|
||||
return QDialog.accept(self, *args)
|
||||
|
||||
|
||||
|
||||
|
@ -18,6 +18,16 @@
|
||||
<normaloff>:/images/config.png</normaloff>:/images/config.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="0">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QTabWidget" name="tabs">
|
||||
<property name="currentIndex">
|
||||
@ -58,7 +68,7 @@ QToolBox::tab:hover {
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>811</width>
|
||||
<height>384</height>
|
||||
<height>352</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -207,8 +217,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>811</width>
|
||||
<height>384</height>
|
||||
<width>397</width>
|
||||
<height>232</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -337,8 +347,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>811</width>
|
||||
<height>384</height>
|
||||
<width>313</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -380,13 +390,92 @@ QToolBox::tab:hover {
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_6">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>351</width>
|
||||
<height>76</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<string>Colors and backgrounds</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout" name="formLayout_6">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_14">
|
||||
<property name="text">
|
||||
<string>Background color:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="background_color_sample">
|
||||
<property name="text">
|
||||
<string>Sample</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="change_background_color_button">
|
||||
<property name="text">
|
||||
<string>Change</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="reset_background_color_button">
|
||||
<property name="text">
|
||||
<string>Reset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>Text color:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="text_color_sample">
|
||||
<property name="text">
|
||||
<string>Sample</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="change_text_color_button">
|
||||
<property name="text">
|
||||
<string>Change</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="reset_text_color_button">
|
||||
<property name="text">
|
||||
<string>Reset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>811</width>
|
||||
<height>384</height>
|
||||
<width>410</width>
|
||||
<height>120</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -456,8 +545,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>811</width>
|
||||
<height>384</height>
|
||||
<width>352</width>
|
||||
<height>123</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -548,16 +637,131 @@ QToolBox::tab:hover {
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_4">
|
||||
<attribute name="title">
|
||||
<string>&Theming</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout" name="formLayout_7">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::ExpandingFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>You can save and load the viewer settings as <i>themes</i></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<property name="text">
|
||||
<string>Save current settings as a theme:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>save_theme_button</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QToolButton" name="save_theme_button">
|
||||
<property name="text">
|
||||
<string>&Save</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/save.png</normaloff>:/images/save.png</iconset>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<property name="text">
|
||||
<string>Load a previously saved theme:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>load_theme_button</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QToolButton" name="load_theme_button">
|
||||
<property name="text">
|
||||
<string>&Load</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::InstantPopup</enum>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_21">
|
||||
<property name="text">
|
||||
<string>Delete a saved theme:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QToolButton" name="delete_theme_button">
|
||||
<property name="text">
|
||||
<string>&Delete</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::InstantPopup</enum>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<widget class="QLabel" name="theming_message">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -115,8 +115,16 @@ class Document(QWebPage): # {{{
|
||||
mf.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||
|
||||
def set_user_stylesheet(self):
|
||||
raw = config().parse().user_css
|
||||
raw = '::selection {background:#ffff00; color:#000;}\nbody {background-color: white;}\n'+raw
|
||||
opts = config().parse()
|
||||
bg = opts.background_color or 'white'
|
||||
brules = ['background-color: %s !important'%bg]
|
||||
if opts.text_color:
|
||||
brules += ['color: %s !important'%opts.text_color]
|
||||
prefix = '''
|
||||
body { %s }
|
||||
'''%('; '.join(brules))
|
||||
raw = prefix + opts.user_css
|
||||
raw = '::selection {background:#ffff00; color:#000;}\n'+raw
|
||||
data = 'data:text/css;charset=utf-8;base64,'
|
||||
data += b64encode(raw.encode('utf-8'))
|
||||
self.settings().setUserStyleSheetUrl(QUrl(data))
|
||||
|
@ -507,8 +507,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.clock_label.setVisible(True)
|
||||
self.clock_label.setText('99:99 AA')
|
||||
self.clock_timer.start(1000)
|
||||
self.clock_label.setStyleSheet(self.clock_label_style%
|
||||
tuple(self.view.document.colors()))
|
||||
self.clock_label.setStyleSheet(self.clock_label_style%(
|
||||
'rgba(0, 0, 0, 0)', self.view.document.colors()[1]))
|
||||
self.clock_label.resize(self.clock_label.sizeHint())
|
||||
sw = QApplication.desktop().screenGeometry(self.view)
|
||||
self.clock_label.move(sw.width() - self.vertical_scrollbar.width() - 15
|
||||
|
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
20502
src/calibre/translations/him.po
Normal file
20502
src/calibre/translations/him.po
Normal file
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
Loading…
x
Reference in New Issue
Block a user