mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Pull from trunk
This commit is contained in:
commit
d4f47b8e75
@ -4,6 +4,45 @@
|
||||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.1
|
||||
date: 2010-06-04
|
||||
|
||||
new features:
|
||||
- title: "Content server: Add option to control category groupiong in OPDS feeds"
|
||||
|
||||
- title: "Make the book details pane occupy the full lower part of the window"
|
||||
|
||||
- title: "Add true and false searches for date based columns"
|
||||
tickets: [5717]
|
||||
|
||||
bug fixes:
|
||||
- title: "iPad driver: Various bug fixes."
|
||||
|
||||
- title: "SONY driver: Fix Launcher partition being detected as storage card in linux"
|
||||
|
||||
- title: "Fix news downloading breaking on windows systems with local encoding other than UTF-8."
|
||||
|
||||
- title: "SONY driver: Fix problem caused by null titles"
|
||||
|
||||
- title: "Make the new splash screen not always stay on top"
|
||||
tickets: [5700]
|
||||
|
||||
- title: "When setting an image with transparent pixels as the book cover, overlay it on a white background first. Fixes transparent covers getting random backgrounds."
|
||||
|
||||
- title: "Content server: Fix stanza integration when entering the server URL my hand"
|
||||
|
||||
improved recipes:
|
||||
- Gizmodo
|
||||
- Vreme
|
||||
|
||||
|
||||
- version: 0.7.0
|
||||
date: 2010-06-04
|
||||
|
||||
new features:
|
||||
- title: "Go to http://calibre-ebook.com/new-in/seven to see what's new in 0.7.0"
|
||||
type: major
|
||||
|
||||
- version: 0.6.55
|
||||
date: 2010-05-28
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 820 B |
BIN
resources/images/news/haaretz_en.png
Normal file
BIN
resources/images/news/haaretz_en.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 712 B |
25
resources/recipes/cbc_canada.recipe
Normal file
25
resources/recipes/cbc_canada.recipe
Normal file
@ -0,0 +1,25 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1275798572(BasicNewsRecipe):
|
||||
title = u'CBC Canada'
|
||||
publisher = 'www.cbc.ca'
|
||||
language = 'en_CA'
|
||||
__author__ = 'rty'
|
||||
category = 'news'
|
||||
oldest_article = 4
|
||||
max_articles_per_feed = 100
|
||||
remove_javascript = True
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://www.cbc.ca/includes/gfx/cbcnews_logo_09.gif'
|
||||
cover_url = 'http://img692.imageshack.us/img692/2814/cbc.png'
|
||||
keep_only_tags = [dict(name='div', attrs={'id':['storyhead','storybody']})]
|
||||
remove_tags_after = dict(id=['socialtools'])
|
||||
feeds = [(u'Top Stories', u'http://rss.cbc.ca/lineup/topstories.xml'),
|
||||
(u'World', u'http://rss.cbc.ca/lineup/world.xml'),
|
||||
(u'National', u'http://rss.cbc.ca/lineup/canada.xml'),
|
||||
(u'Manitoba', u'http://rss.cbc.ca/lineup/canada-manitoba.xml'),
|
||||
(u'Politics', u'http://rss.cbc.ca/lineup/politics.xml'),
|
||||
(u'Tech & Science', u'http://rss.cbc.ca/lineup/technology.xml'),
|
||||
(u'Books', u'http://rss.cbc.ca/lineup/arts-books.xml')]
|
@ -5,7 +5,6 @@ __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
clarin.com
|
||||
'''
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Clarin(BasicNewsRecipe):
|
||||
@ -18,11 +17,12 @@ class Clarin(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
cover_url = strftime('http://www.clarin.com/diario/%Y/%m/%d/portada.jpg')
|
||||
encoding = 'cp1252'
|
||||
language = 'es'
|
||||
masthead_url = 'http://www.clarin.com/shared/v10/img/Hd/lg_Clarin.gif'
|
||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,"Times New Roman",Times,serif; font-size: xx-large} .Volan,.Pie,.Autor{ font-size: x-small} .Copete,.Hora{font-size: large} '
|
||||
encoding = 'utf8'
|
||||
language = 'es_AR'
|
||||
publication_type = 'newspaper'
|
||||
INDEX = 'http://www.clarin.com'
|
||||
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
|
||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,serif; font-size: xx-large} .hora{font-weight:bold} .hd p{font-size: small} .nombre-autor{color: #0F325A} '
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
@ -31,27 +31,32 @@ class Clarin(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(name='a' , attrs={'class':'Imp' })
|
||||
,dict(name='div' , attrs={'class':'Perma' })
|
||||
,dict(name='h1' , text='Imprimir' )
|
||||
]
|
||||
keep_only_tags = [dict(attrs={'class':['hd','mt']})]
|
||||
|
||||
feeds = [
|
||||
(u'Ultimo Momento', u'http://www.clarin.com/diario/hoy/um/sumariorss.xml')
|
||||
,(u'El Pais' , u'http://www.clarin.com/diario/hoy/elpais.xml' )
|
||||
,(u'Opinion' , u'http://www.clarin.com/diario/hoy/opinion.xml' )
|
||||
,(u'El Mundo' , u'http://www.clarin.com/diario/hoy/elmundo.xml' )
|
||||
,(u'Sociedad' , u'http://www.clarin.com/diario/hoy/sociedad.xml' )
|
||||
,(u'La Ciudad' , u'http://www.clarin.com/diario/hoy/laciudad.xml' )
|
||||
,(u'Policiales' , u'http://www.clarin.com/diario/hoy/policiales.xml' )
|
||||
,(u'Deportes' , u'http://www.clarin.com/diario/hoy/deportes.xml' )
|
||||
(u'Pagina principal', u'http://www.clarin.com/rss/' )
|
||||
,(u'Politica' , u'http://www.clarin.com/rss/politica/' )
|
||||
,(u'Deportes' , u'http://www.clarin.com/rss/deportes/' )
|
||||
,(u'Economia' , u'http://www.clarin.com/economia/' )
|
||||
,(u'Mundo' , u'http://www.clarin.com/rss/mundo/' )
|
||||
,(u'Espectaculos' , u'http://www.clarin.com/rss/espectaculos/')
|
||||
,(u'Sociedad' , u'http://www.clarin.com/rss/sociedad/' )
|
||||
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||
,(u'Policiales' , u'http://www.clarin.com/rss/policiales/' )
|
||||
,(u'Internet' , u'http://www.clarin.com/rss/internet/' )
|
||||
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
rest = url.partition('-0')[-1]
|
||||
lmain = rest.partition('.')[0]
|
||||
lurl = u'http://www.servicios.clarin.com/notas/jsp/clarin/v9/notas/imprimir.jsp?pagid=' + lmain
|
||||
return lurl
|
||||
return url + '?print=1'
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = None
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
cover_item = soup.find('div',attrs={'class':'bb-md bb-md-edicion_papel'})
|
||||
if cover_item:
|
||||
ap = cover_item.find('a',attrs={'href':'/edicion-impresa/'})
|
||||
if ap:
|
||||
cover_url = self.INDEX + ap.img['src']
|
||||
return cover_url
|
||||
|
||||
|
@ -17,7 +17,7 @@ class Gizmodo(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = True
|
||||
use_embedded_content = False
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/gizmodo.com/img/logo.png'
|
||||
extra_css = ' body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif} img{margin-bottom: 1em} '
|
||||
@ -29,9 +29,11 @@ class Gizmodo(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
remove_tags = [dict(name='div',attrs={'class':'feedflare'})]
|
||||
remove_tags_after = dict(name='div',attrs={'class':'feedflare'})
|
||||
remove_attributes = ['width','height']
|
||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/full')]
|
||||
|
||||
|
57
resources/recipes/haaretz_en.recipe
Normal file
57
resources/recipes/haaretz_en.recipe
Normal file
@ -0,0 +1,57 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
haaretz.com
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Haaretz_en(BasicNewsRecipe):
|
||||
title = 'Haaretz in English'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Haaretz.com, the online edition of Haaretz Newspaper in Israel, and analysis from Israel and the Middle East. Haaretz.com provides extensive and in-depth coverage of Israel, the Jewish World and the Middle East, including defense, diplomacy, the Arab-Israeli conflict, the peace process, Israeli politics, Jerusalem affairs, international relations, Iran, Iraq, Syria, Lebanon, the Palestinian Authority, the West Bank and the Gaza Strip, the Israeli business world and Jewish life in Israel and the Diaspora. '
|
||||
publisher = 'haaretz.com'
|
||||
category = 'news, politics, Israel'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'cp1252'
|
||||
use_embedded_content = False
|
||||
language = 'en_IL'
|
||||
publication_type = 'newspaper'
|
||||
remove_empty_feeds = True
|
||||
masthead_url = 'http://www.haaretz.com/images/logos/logoGrey.gif'
|
||||
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [dict(name='div', attrs={'class':['rightcol']}),dict(name='table')]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags_after = dict(attrs={'id':'innerArticle'})
|
||||
keep_only_tags = [dict(attrs={'id':'content'})]
|
||||
|
||||
|
||||
feeds = [
|
||||
(u'Opinion' , u'http://www.haaretz.com/cmlink/opinion-rss-1.209234?localLinksEnabled=false' )
|
||||
,(u'Defense and diplomacy' , u'http://www.haaretz.com/cmlink/defense-and-diplomacy-rss-1.208894?localLinksEnabled=false')
|
||||
,(u'National' , u'http://www.haaretz.com/cmlink/national-rss-1.208896?localLinksEnabled=false' )
|
||||
,(u'International' , u'http://www.haaretz.com/cmlink/international-rss-1.208898?localLinksEnabled=false' )
|
||||
,(u'Jewish World' , u'http://www.haaretz.com/cmlink/jewish-world-rss-1.209085?localLinksEnabled=false' )
|
||||
,(u'Business' , u'http://www.haaretz.com/cmlink/business-print-rss-1.264904?localLinksEnabled=false' )
|
||||
,(u'Real Estate' , u'http://www.haaretz.com/cmlink/real-estate-print-rss-1.264977?localLinksEnabled=false' )
|
||||
,(u'Features' , u'http://www.haaretz.com/cmlink/features-print-rss-1.264912?localLinksEnabled=false' )
|
||||
,(u'Arts and leisure' , u'http://www.haaretz.com/cmlink/arts-and-leisure-rss-1.286090?localLinksEnabled=false' )
|
||||
,(u'Books' , u'http://www.haaretz.com/cmlink/books-rss-1.264947?localLinksEnabled=false' )
|
||||
,(u'Food and Wine' , u'http://www.haaretz.com/cmlink/food-and-wine-print-rss-1.265034?localLinksEnabled=false' )
|
||||
,(u'Sports' , u'http://www.haaretz.com/cmlink/sports-rss-1.286092?localLinksEnabled=false' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return soup
|
@ -52,10 +52,12 @@ class Vreme(BasicNewsRecipe):
|
||||
def parse_index(self):
|
||||
articles = []
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
|
||||
cover_item = soup.find('div',attrs={'id':'najava'})
|
||||
if cover_item:
|
||||
self.cover_url = self.INDEX + cover_item.img['src']
|
||||
for item in soup.findAll(['h3','h4']):
|
||||
description = ''
|
||||
title_prefix = ''
|
||||
description = u''
|
||||
title_prefix = u''
|
||||
feed_link = item.find('a')
|
||||
if feed_link and feed_link.has_key('href') and feed_link['href'].startswith('/cms/view.php'):
|
||||
url = self.INDEX + feed_link['href']
|
||||
@ -67,7 +69,7 @@ class Vreme(BasicNewsRecipe):
|
||||
,'url' :url
|
||||
,'description':description
|
||||
})
|
||||
return [(soup.head.title.string, articles)]
|
||||
return [('Nedeljnik Vreme', articles)]
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['object','link'])
|
||||
@ -76,11 +78,3 @@ class Vreme(BasicNewsRecipe):
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '&print=yes'
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = None
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
cover_item = soup.find('div',attrs={'id':'najava'})
|
||||
if cover_item:
|
||||
cover_url = self.INDEX + cover_item.img['src']
|
||||
return cover_url
|
||||
|
@ -21,12 +21,16 @@ class weltDe(BasicNewsRecipe):
|
||||
no_stylesheets = True
|
||||
remove_stylesheets = True
|
||||
remove_javascript = True
|
||||
encoding = 'iso-8859-1'
|
||||
BasicNewsRecipe.summary_length = 200
|
||||
encoding = 'utf-8'
|
||||
html2epub_options = 'linearize_tables = True\nbase_font_size2=10'
|
||||
BasicNewsRecipe.summary_length = 100
|
||||
|
||||
|
||||
remove_tags = [dict(id='jumplinks'),
|
||||
dict(id='ad1'),
|
||||
dict(id='top'),
|
||||
dict(id='header'),
|
||||
dict(id='additionalNavWrapper'),
|
||||
dict(id='fullimage_index'),
|
||||
dict(id='additionalNav'),
|
||||
dict(id='printMenu'),
|
||||
@ -35,6 +39,8 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(id='servicesBox'),
|
||||
dict(id='servicesNav'),
|
||||
dict(id='ad2'),
|
||||
dict(id='banner_1'),
|
||||
dict(id='ssoInfoTop'),
|
||||
dict(id='brandingWrapper'),
|
||||
dict(id='links-intern'),
|
||||
dict(id='navigation'),
|
||||
@ -53,10 +59,22 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(id='xmsg_comment'),
|
||||
dict(id='additionalNavWrapper'),
|
||||
dict(id='imagebox'),
|
||||
dict(id='footerContainer'),
|
||||
#dict(id=''),
|
||||
dict(name='span'),
|
||||
dict(name='div', attrs={'class':'printURL'}),
|
||||
dict(name='ul', attrs={'class':'clear mainNavigation inline'}),
|
||||
dict(name='ul', attrs={'class':'inline'}),
|
||||
dict(name='ul', attrs={'class':'ubar'}),
|
||||
dict(name='hr', attrs={'class':'ubar'}),
|
||||
dict(name='li', attrs={'class':'counter'}),
|
||||
dict(name='li', attrs={'class':'browseBack'}),
|
||||
dict(name='li', attrs={'class':'browseNext'}),
|
||||
dict(name='li', attrs={'class':'selected'}),
|
||||
dict(name='div', attrs={'class':'floatLeft'}),
|
||||
dict(name='div', attrs={'class':'ad'}),
|
||||
dict(name='div', attrs={'class':'ftBarLeft'}),
|
||||
dict(name='div', attrs={'class':'clear additionalNav'}),
|
||||
dict(name='div', attrs={'class':'inlineBox inlineFurtherLinks'}),
|
||||
dict(name='div', attrs={'class':'inlineBox videoInlineBox'}),
|
||||
dict(name='div', attrs={'class':'inlineGallery'}),
|
||||
@ -65,6 +83,23 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(name='div', attrs={'class':'articleOptions clear'}),
|
||||
dict(name='div', attrs={'class':'noPrint galleryIndex'}),
|
||||
dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}),
|
||||
dict(name='div', attrs={'class':'clear module writeComment bgColor1'}),
|
||||
dict(name='div', attrs={'class':'clear module textGallery bgColor1'}),
|
||||
dict(name='div', attrs={'class':'clear module socialMedia bgColor1'}),
|
||||
dict(name='div', attrs={'class':'clear module continuativeLinks'}),
|
||||
dict(name='div', attrs={'class':'moreArtH3'}),
|
||||
dict(name='div', attrs={'class':'jqmWindow'}),
|
||||
dict(name='div', attrs={'class':'clear gap4'}),
|
||||
dict(name='div', attrs={'class':'hidden'}),
|
||||
dict(name='div', attrs={'class':'advertising'}),
|
||||
dict(name='div', attrs={'class':'ad adMarginBottom'}),
|
||||
dict(name='div', attrs={'class':'ad'}),
|
||||
dict(name='div', attrs={'class':'topLine'}),
|
||||
dict(name='div', attrs={'class':'toplineH2'}),
|
||||
dict(name='div', attrs={'class':'headLineH3'}),
|
||||
dict(name='div', attrs={'class':'print'}),
|
||||
dict(name='div', attrs={'class':'clear menu'}),
|
||||
dict(name='div', attrs={'class':'clear galleryContent'}),
|
||||
dict(name='p', attrs={'class':'jump'}),
|
||||
dict(name='a', attrs={'class':'commentLink'}),
|
||||
dict(name='h2', attrs={'class':'jumpHeading'}),
|
||||
@ -75,7 +110,7 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(name='table', attrs={'class':'textGallery'}),
|
||||
dict(name='li', attrs={'class':'active'})]
|
||||
|
||||
remove_tags_after = [dict(id='tw_link_widget')]
|
||||
remove_tags_after = [dict(name='div', attrs={'class':'clear departmentLine'})]
|
||||
|
||||
extra_css = '''
|
||||
h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;}
|
||||
@ -87,7 +122,6 @@ class weltDe(BasicNewsRecipe):
|
||||
.photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} '''
|
||||
|
||||
feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'),
|
||||
('Deutsche Dinge', 'http://www.welt.de/deutsche-dinge/?service=Rss'),
|
||||
('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'),
|
||||
('Finanzen', 'http://welt.de/finanzen/?service=Rss'),
|
||||
('Sport', 'http://welt.de/sport/?service=Rss'),
|
||||
@ -101,4 +135,5 @@ class weltDe(BasicNewsRecipe):
|
||||
|
||||
|
||||
def print_version(self, url):
|
||||
return url.replace ('.html', '.html?print=yes')
|
||||
return url.replace ('.html', '.html?print=true')
|
||||
|
||||
|
@ -41,6 +41,8 @@ mimetypes.add_type('application/vnd.palm', '.pdb')
|
||||
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
|
||||
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
|
||||
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
|
||||
mimetypes.add_type('application/x-cbz', '.cbz')
|
||||
mimetypes.add_type('application/x-cbr', '.cbr')
|
||||
mimetypes.add_type('image/wmf', '.wmf')
|
||||
guess_type = mimetypes.guess_type
|
||||
import cssutils
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.6.55'
|
||||
__version__ = '0.7.1'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,7 @@ __docformat__ = 'restructuredtext en'
|
||||
Device driver for the SONY devices
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import os, time, re
|
||||
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
from calibre.devices.prs505 import MEDIA_XML
|
||||
@ -66,6 +65,41 @@ class PRS505(USBMS):
|
||||
def windows_filter_pnp_id(self, pnp_id):
|
||||
return '_LAUNCHER' in pnp_id
|
||||
|
||||
def post_open_callback(self):
|
||||
|
||||
def write_cache(prefix):
|
||||
try:
|
||||
cachep = os.path.join(prefix, *(CACHE_XML.split('/')))
|
||||
if not os.path.exists(cachep):
|
||||
dname = os.path.dirname(cachep)
|
||||
if not os.path.exists(dname):
|
||||
try:
|
||||
os.makedirs(dname, mode=0777)
|
||||
except:
|
||||
time.sleep(5)
|
||||
os.makedirs(dname, mode=0777)
|
||||
with open(cachep, 'wb') as f:
|
||||
f.write(u'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cache xmlns="http://www.kinoma.com/FskCache/1">
|
||||
</cache>
|
||||
'''.encode('utf8'))
|
||||
return True
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
# Make sure we don't have the launcher partition
|
||||
# as one of the cards
|
||||
|
||||
if self._card_a_prefix is not None:
|
||||
if not write_cache(self._card_a_prefix):
|
||||
self._card_a_prefix = None
|
||||
if self._card_b_prefix is not None:
|
||||
if not write_cache(self._card_b_prefix):
|
||||
self._card_b_prefix = None
|
||||
|
||||
|
||||
def get_device_information(self, end_session=True):
|
||||
return (self.gui_name, '', '', '')
|
||||
|
||||
|
@ -415,10 +415,11 @@ class XMLCache(object):
|
||||
prints('\tmtime', strftime(os.path.getmtime(path)))
|
||||
record.set('date', date)
|
||||
record.set('size', str(os.stat(path).st_size))
|
||||
record.set('title', book.title)
|
||||
title = book.title if book.title else _('Unknown')
|
||||
record.set('title', title)
|
||||
ts = book.title_sort
|
||||
if not ts:
|
||||
ts = title_sort(book.title)
|
||||
ts = title_sort(title)
|
||||
record.set('titleSorter', ts)
|
||||
record.set('author', authors_to_string(book.authors))
|
||||
ext = os.path.splitext(path)[1]
|
||||
|
@ -44,7 +44,8 @@ def get_metadata_(src, encoding=None):
|
||||
author = match.group(2).replace(',', ';')
|
||||
|
||||
ent_pat = re.compile(r'&(\S+)?;')
|
||||
title = ent_pat.sub(entity_to_unicode, title)
|
||||
if title:
|
||||
title = ent_pat.sub(entity_to_unicode, title)
|
||||
if author:
|
||||
author = ent_pat.sub(entity_to_unicode, author)
|
||||
mi = MetaInformation(title, [author] if author else None)
|
||||
|
@ -1334,7 +1334,7 @@ class MobiWriter(object):
|
||||
item = self._oeb.manifest.hrefs[href]
|
||||
try:
|
||||
data = rescale_image(item.data, self._imagemax)
|
||||
except IOError:
|
||||
except:
|
||||
self._oeb.logger.warn('Bad image file %r' % item.href)
|
||||
continue
|
||||
self._records.append(data)
|
||||
|
@ -201,6 +201,11 @@ class CSSFlattener(object):
|
||||
tag = barename(node.tag)
|
||||
style = stylizer.style(node)
|
||||
cssdict = style.cssdict()
|
||||
try:
|
||||
font_size = style['font-size']
|
||||
except:
|
||||
font_size = self.sbase if self.sbase is not None else \
|
||||
self.context.source.fbase
|
||||
if 'align' in node.attrib:
|
||||
cssdict['text-align'] = node.attrib['align']
|
||||
del node.attrib['align']
|
||||
@ -219,13 +224,16 @@ class CSSFlattener(object):
|
||||
esize = 1
|
||||
if esize > 7:
|
||||
esize = 7
|
||||
cssdict['font-size'] = fnums[esize]
|
||||
font_size = fnums[esize]
|
||||
else:
|
||||
try:
|
||||
cssdict['font-size'] = fnums[force_int(size)]
|
||||
font_size = fnums[force_int(size)]
|
||||
except:
|
||||
cssdict['font-size'] = fnums[3]
|
||||
font_size = fnums[3]
|
||||
cssdict['font-size'] = '%.1fpt'%font_size
|
||||
del node.attrib['size']
|
||||
if 'face' in node.attrib:
|
||||
del node.attrib['face']
|
||||
if 'color' in node.attrib:
|
||||
cssdict['color'] = node.attrib['color']
|
||||
del node.attrib['color']
|
||||
@ -244,7 +252,7 @@ class CSSFlattener(object):
|
||||
cssdict['font-size'] = '%0.5fem'%(fsize/psize)
|
||||
psize = fsize
|
||||
elif 'font-size' in cssdict or tag == 'body':
|
||||
fsize = self.fmap[style['font-size']]
|
||||
fsize = self.fmap[font_size]
|
||||
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
|
||||
psize = fsize
|
||||
if cssdict:
|
||||
|
@ -222,6 +222,8 @@ class DBAdder(Thread):
|
||||
|
||||
class Adder(QObject):
|
||||
|
||||
ADD_TIMEOUT = 600 # seconds
|
||||
|
||||
def __init__(self, parent, db, callback, spare_server=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.pd = ProgressDialog(_('Adding...'), parent=parent)
|
||||
@ -328,7 +330,7 @@ class Adder(QObject):
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
if (time.time() - self.last_added_at) > 300:
|
||||
if (time.time() - self.last_added_at) > self.ADD_TIMEOUT:
|
||||
self.timer.stop()
|
||||
self.pd.hide()
|
||||
self.db_adder.end = True
|
||||
|
@ -445,6 +445,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.username.setText(opts.username)
|
||||
self.password.setText(opts.password if opts.password else '')
|
||||
self.opt_max_opds_items.setValue(opts.max_opds_items)
|
||||
self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items)
|
||||
self.auto_launch.setChecked(config['autolaunch_server'])
|
||||
self.systray_icon.setChecked(config['systray_icon'])
|
||||
self.sync_news.setChecked(config['upload_news_to_device'])
|
||||
@ -848,6 +849,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
sc.set('port', self.port.value())
|
||||
sc.set('max_cover', mcs)
|
||||
sc.set('max_opds_items', self.opt_max_opds_items.value())
|
||||
sc.set('max_opds_ungrouped_items',
|
||||
self.opt_max_opds_ungrouped_items.value())
|
||||
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
||||
config['upload_news_to_device'] = self.sync_news.isChecked()
|
||||
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
||||
|
@ -892,6 +892,26 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
|
||||
<property name="minimum">
|
||||
<number>25</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>Max. OPDS &ungrouped items:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_max_opds_ungrouped_items</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -3,14 +3,12 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
from PyQt4.QtGui import QDialog
|
||||
|
||||
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.constants import islinux
|
||||
|
||||
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from functools import partial
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog, QListWidgetItem
|
||||
|
||||
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
||||
from calibre.gui2 import question_dialog, error_dialog
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
|
||||
class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
|
||||
def tag_cmp(self, x, y):
|
||||
return cmp(x.lower(), y.lower())
|
||||
|
||||
def __init__(self, window, db, tag_to_match):
|
||||
def __init__(self, window, db, tag_to_match, category):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_TagListEditor.__init__(self)
|
||||
self.setupUi(self)
|
||||
@ -20,9 +20,28 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
self.to_delete = []
|
||||
self.db = db
|
||||
self.all_tags = {}
|
||||
for k,v in db.get_tags_with_ids():
|
||||
self.category = category
|
||||
if category == 'tags':
|
||||
result = db.get_tags_with_ids()
|
||||
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||
elif category == 'series':
|
||||
result = db.get_series_with_ids()
|
||||
compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower()))
|
||||
elif category == 'publisher':
|
||||
result = db.get_publishers_with_ids()
|
||||
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||
else: # should be a custom field
|
||||
self.cc_label = None
|
||||
if category in db.field_metadata:
|
||||
self.cc_label = db.field_metadata[category]['label']
|
||||
result = self.db.get_custom_items_with_ids(label=self.cc_label)
|
||||
else:
|
||||
result = []
|
||||
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||
|
||||
for k,v in result:
|
||||
self.all_tags[v] = k
|
||||
for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
|
||||
for tag in sorted(self.all_tags.keys(), cmp=compare):
|
||||
item = QListWidgetItem(tag)
|
||||
item.setData(Qt.UserRole, self.all_tags[tag])
|
||||
self.available_tags.addItem(item)
|
||||
@ -37,13 +56,18 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
|
||||
|
||||
def finish_editing(self, item):
|
||||
if item.text() != self.item_before_editing.text():
|
||||
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
|
||||
error_dialog(self, 'Tag already used',
|
||||
'The tag %s is already used.'%(item.text())).exec_()
|
||||
if not item.text():
|
||||
error_dialog(self, _('Item is blank'),
|
||||
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||
item.setText(self.item_before_editing.text())
|
||||
return
|
||||
id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
|
||||
if item.text() != self.item_before_editing.text():
|
||||
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
|
||||
error_dialog(self, _('Item already used'),
|
||||
_('The item %s is already used.')%(item.text())).exec_()
|
||||
item.setText(self.item_before_editing.text())
|
||||
return
|
||||
(id,ign) = self.item_before_editing.data(Qt.UserRole).toInt()
|
||||
self.to_rename[item.text()] = id
|
||||
|
||||
def rename_tag(self):
|
||||
@ -52,38 +76,53 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
|
||||
def _rename_tag(self, item):
|
||||
if item is None:
|
||||
error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_()
|
||||
error_dialog(self, _('No item selected'),
|
||||
_('You must select one item from the list of Available items.')).exec_()
|
||||
return
|
||||
self.item_before_editing = item.clone()
|
||||
item.setFlags (item.flags() | Qt.ItemIsEditable);
|
||||
self.available_tags.editItem(item)
|
||||
|
||||
def delete_tags(self, item=None):
|
||||
confirms, deletes = [], []
|
||||
items = self.available_tags.selectedItems() if item is None else [item]
|
||||
if not items:
|
||||
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
|
||||
deletes = self.available_tags.selectedItems() if item is None else [item]
|
||||
if not deletes:
|
||||
error_dialog(self, _('No items selected'),
|
||||
_('You must select at least one items from the list.')).exec_()
|
||||
return
|
||||
ct = ', '.join([unicode(item.text()) for item in deletes])
|
||||
if not question_dialog(self, _('Are your sure?'),
|
||||
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
|
||||
return
|
||||
for item in items:
|
||||
if self.db.is_tag_used(unicode(item.text())):
|
||||
confirms.append(item)
|
||||
else:
|
||||
deletes.append(item)
|
||||
if confirms:
|
||||
ct = ', '.join([unicode(item.text()) for item in confirms])
|
||||
if question_dialog(self, _('Are your sure?'),
|
||||
'<p>'+_('The following tags are used by one or more books. '
|
||||
'Are you certain you want to delete them?')+'<br>'+ct):
|
||||
deletes += confirms
|
||||
|
||||
for item in deletes:
|
||||
self.to_delete.append(item)
|
||||
(id,ign) = item.data(Qt.UserRole).toInt()
|
||||
self.to_delete.append(id)
|
||||
self.available_tags.takeItem(self.available_tags.row(item))
|
||||
|
||||
def accept(self):
|
||||
for text in self.to_rename:
|
||||
self.db.rename_tag(self.to_rename[text], unicode(text))
|
||||
for item in self.to_delete:
|
||||
self.db.delete_tag(unicode(item.text()))
|
||||
QDialog.accept(self)
|
||||
rename_func = None
|
||||
if self.category == 'tags':
|
||||
rename_func = self.db.rename_tag
|
||||
delete_func = self.db.delete_tag_using_id
|
||||
elif self.category == 'series':
|
||||
rename_func = self.db.rename_series
|
||||
delete_func = self.db.delete_series_using_id
|
||||
elif self.category == 'publisher':
|
||||
rename_func = self.db.rename_publisher
|
||||
delete_func = self.db.delete_publisher_using_id
|
||||
else:
|
||||
rename_func = partial(self.db.rename_custom_item, label=self.cc_label)
|
||||
delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label)
|
||||
|
||||
work_done = False
|
||||
if rename_func:
|
||||
for text in self.to_rename:
|
||||
work_done = True
|
||||
rename_func(id=self.to_rename[text], new_name=unicode(text))
|
||||
for item in self.to_delete:
|
||||
work_done = True
|
||||
delete_func(item)
|
||||
if not work_done:
|
||||
QDialog.reject(self)
|
||||
else:
|
||||
QDialog.accept(self)
|
||||
|
@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Tag Editor</string>
|
||||
<string>Category Editor</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
@ -25,7 +25,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Tags in use</string>
|
||||
<string>Items in use</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>available_tags</cstring>
|
||||
@ -54,7 +54,7 @@
|
||||
<item>
|
||||
<widget class="QToolButton" name="delete_button">
|
||||
<property name="toolTip">
|
||||
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
|
||||
<string>Delete item from database. This will unapply the item from all books and then remove it from the database.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
@ -74,7 +74,7 @@
|
||||
<item>
|
||||
<widget class="QToolButton" name="rename_button">
|
||||
<property name="toolTip">
|
||||
<string>Rename the tag everywhere it is used.</string>
|
||||
<string>Rename the item in every book where it is used.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
|
@ -213,7 +213,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.endInsertRows()
|
||||
self.count_changed()
|
||||
|
||||
def search(self, text, refinement, reset=True):
|
||||
def search(self, text, reset=True):
|
||||
try:
|
||||
self.db.search(text)
|
||||
except ParseException:
|
||||
@ -224,9 +224,10 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.clear_caches()
|
||||
self.reset()
|
||||
if self.last_search:
|
||||
# Do not issue search done for the null search. It is used to clear
|
||||
# the search and count records for restrictions
|
||||
self.searched.emit(True)
|
||||
|
||||
|
||||
def sort(self, col, order, reset=True):
|
||||
if not self.db:
|
||||
return
|
||||
@ -257,7 +258,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.sort(col, self.sorted_on[1], reset=reset)
|
||||
|
||||
def research(self, reset=True):
|
||||
self.search(self.last_search, False, reset=reset)
|
||||
self.search(self.last_search, reset=reset)
|
||||
|
||||
def columnCount(self, parent):
|
||||
if parent and parent.isValid():
|
||||
@ -730,6 +731,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
|
||||
def set_search_restriction(self, s):
|
||||
self.db.data.set_search_restriction(s)
|
||||
self.search('')
|
||||
return self.rowCount(None)
|
||||
|
||||
# }}}
|
||||
|
||||
@ -874,7 +877,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
return flags
|
||||
|
||||
|
||||
def search(self, text, refinement, reset=True):
|
||||
def search(self, text, reset=True):
|
||||
if not text or not text.strip():
|
||||
self.map = list(range(len(self.db)))
|
||||
else:
|
||||
@ -1086,7 +1089,6 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
idx = self.map[row]
|
||||
if cname == 'title' :
|
||||
self.db[idx].title = val
|
||||
self.db[idx].title_sorter = val
|
||||
elif cname == 'authors':
|
||||
self.db[idx].authors = string_to_authors(val)
|
||||
elif cname == 'collections':
|
||||
|
@ -75,6 +75,9 @@ class BooksView(QTableView): # {{{
|
||||
h.setSectionHidden(idx, True)
|
||||
elif action == 'show':
|
||||
h.setSectionHidden(idx, False)
|
||||
if h.sectionSize(idx) < 3:
|
||||
sz = h.sectionSizeHint(idx)
|
||||
h.resizeSection(idx, sz)
|
||||
elif action == 'ascending':
|
||||
self.sortByColumn(idx, Qt.AscendingOrder)
|
||||
elif action == 'descending':
|
||||
@ -257,6 +260,11 @@ class BooksView(QTableView): # {{{
|
||||
for col, alignment in state.get('column_alignment', {}).items():
|
||||
self._model.change_alignment(col, alignment)
|
||||
|
||||
for i in range(h.count()):
|
||||
if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
|
||||
sz = h.sectionSizeHint(i)
|
||||
h.resizeSection(i, sz)
|
||||
|
||||
def get_default_state(self):
|
||||
old_state = {
|
||||
'hidden_columns': [],
|
||||
@ -429,10 +437,6 @@ class BooksView(QTableView): # {{{
|
||||
self._search_done = search_done
|
||||
self._model.searched.connect(self.search_done)
|
||||
|
||||
def connect_to_restriction_set(self, tv):
|
||||
# must be synchronous (not queued)
|
||||
tv.restriction_set.connect(self._model.set_search_restriction)
|
||||
|
||||
def connect_to_book_display(self, bd):
|
||||
self._model.new_bookdisplay_data.connect(bd)
|
||||
|
||||
|
@ -152,7 +152,7 @@ class Main(MainWindow, Ui_MainWindow):
|
||||
self.stack.setCurrentIndex(1)
|
||||
self.renderer.start()
|
||||
|
||||
def find(self, search, refinement):
|
||||
def find(self, search):
|
||||
self.last_search = search
|
||||
try:
|
||||
self.document.search(search)
|
||||
|
@ -226,7 +226,7 @@ class GuiRunner(QObject):
|
||||
self.splash_pixmap = QPixmap()
|
||||
self.splash_pixmap.load(I('library.png'))
|
||||
self.splash_screen = QSplashScreen(self.splash_pixmap,
|
||||
Qt.SplashScreen|Qt.WindowStaysOnTopHint)
|
||||
Qt.SplashScreen)
|
||||
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
|
||||
__appname__)
|
||||
self.splash_screen.show()
|
||||
|
@ -28,7 +28,7 @@
|
||||
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
@ -305,78 +305,79 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="Splitter" name="vertical_splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="library">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="Splitter" name="horizontal_splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="TagsView" name="tags_view">
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Splitter" name="vertical_splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="library">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="Splitter" name="horizontal_splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="TagsView" name="tags_view">
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="popularity">
|
||||
<property name="text">
|
||||
<string>Sort by &popularity</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="tag_match">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match any</string>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="tag_match">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match all</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match any</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match all</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="edit_categories">
|
||||
<property name="toolTip">
|
||||
<string>Create, edit, and delete user categories</string>
|
||||
@ -385,10 +386,49 @@
|
||||
<string>Manage &user categories</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="BooksView" name="library_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="BooksView" name="library_view">
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="main_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="memory_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
@ -420,139 +460,107 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="card_a_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="card_a_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="card_b_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="card_b_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="main_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="memory_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SideBar" name="sidebar" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QWidget" name="card_a_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="card_a_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="card_b_memory">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="DeviceBooksView" name="card_b_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="StatusBar" name="status_bar" native="true"/>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SideBar" name="sidebar" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="StatusBar" name="status_bar" native="true"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
@ -57,7 +57,7 @@ class SearchBox2(QComboBox):
|
||||
INTERVAL = 1500 #: Time to wait before emitting search signal
|
||||
MAX_COUNT = 25
|
||||
|
||||
search = pyqtSignal(object, object)
|
||||
search = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QComboBox.__init__(self, parent)
|
||||
@ -97,8 +97,12 @@ class SearchBox2(QComboBox):
|
||||
self.help_state = False
|
||||
|
||||
def clear_to_help(self):
|
||||
self.search.emit('')
|
||||
self._in_a_search = False
|
||||
self.setEditText(self.help_text)
|
||||
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||
self.killTimer(self.timer)
|
||||
self.timer = None
|
||||
self.line_edit.home(False)
|
||||
self.help_state = True
|
||||
self.line_edit.setStyleSheet(
|
||||
@ -111,7 +115,6 @@ class SearchBox2(QComboBox):
|
||||
|
||||
def clear(self):
|
||||
self.clear_to_help()
|
||||
self.search.emit('', False)
|
||||
|
||||
def search_done(self, ok):
|
||||
if not unicode(self.currentText()).strip():
|
||||
@ -155,9 +158,8 @@ class SearchBox2(QComboBox):
|
||||
if not text or text == self.help_text:
|
||||
return self.clear()
|
||||
self.help_state = False
|
||||
refinement = text.startswith(self.prev_search) and ':' not in text
|
||||
self.prev_search = text
|
||||
self.search.emit(text, refinement)
|
||||
self.search.emit(text)
|
||||
|
||||
idx = self.findText(text, Qt.MatchFixedString)
|
||||
self.block_signals(True)
|
||||
@ -187,12 +189,15 @@ class SearchBox2(QComboBox):
|
||||
self.set_search_string(joiner.join(tags))
|
||||
|
||||
def set_search_string(self, txt):
|
||||
if not txt:
|
||||
self.clear_to_help()
|
||||
return
|
||||
self.normalize_state()
|
||||
self.setEditText(txt)
|
||||
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||
self.killTimer(self.timer)
|
||||
self.timer = None
|
||||
self.search.emit(txt, False)
|
||||
self.search.emit(txt)
|
||||
self.line_edit.end(False)
|
||||
self.initial_state = False
|
||||
|
||||
|
@ -17,15 +17,17 @@ from calibre.gui2 import config, NONE
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.library.field_metadata import TagsIcons
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.gui2 import error_dialog
|
||||
|
||||
class TagsView(QTreeView): # {{{
|
||||
|
||||
need_refresh = pyqtSignal()
|
||||
restriction_set = pyqtSignal(object)
|
||||
refresh_required = pyqtSignal()
|
||||
tags_marked = pyqtSignal(object, object)
|
||||
user_category_edit = pyqtSignal(object)
|
||||
tag_list_edit = pyqtSignal(object)
|
||||
tag_list_edit = pyqtSignal(object, object)
|
||||
saved_search_edit = pyqtSignal(object)
|
||||
tag_item_renamed = pyqtSignal()
|
||||
search_item_renamed = pyqtSignal()
|
||||
|
||||
def __init__(self, *args):
|
||||
QTreeView.__init__(self, *args)
|
||||
@ -34,26 +36,26 @@ class TagsView(QTreeView): # {{{
|
||||
self.setIconSize(QSize(30, 30))
|
||||
self.tag_match = None
|
||||
|
||||
def set_database(self, db, tag_match, popularity, restriction):
|
||||
def set_database(self, db, tag_match, popularity):
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
|
||||
self._model = TagsModel(db, parent=self,
|
||||
hidden_categories=self.hidden_categories,
|
||||
search_restriction=None)
|
||||
self.popularity = popularity
|
||||
self.restriction = restriction
|
||||
self.tag_match = tag_match
|
||||
self.db = db
|
||||
self.search_restriction = None
|
||||
self.setModel(self._model)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.clicked.connect(self.toggle)
|
||||
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||
self.popularity.setChecked(config['sort_by_popularity'])
|
||||
self.popularity.stateChanged.connect(self.sort_changed)
|
||||
self.restriction.activated[str].connect(self.search_restriction_set)
|
||||
self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
|
||||
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
|
||||
db.add_listener(self.database_changed)
|
||||
self.saved_searches_changed(recount=False)
|
||||
|
||||
def database_changed(self, event, ids):
|
||||
self.need_refresh.emit()
|
||||
self.refresh_required.emit()
|
||||
|
||||
@property
|
||||
def match_all(self):
|
||||
@ -64,34 +66,38 @@ class TagsView(QTreeView): # {{{
|
||||
self.model().refresh()
|
||||
# self.search_restriction_set()
|
||||
|
||||
def search_restriction_set(self, s):
|
||||
self.clear()
|
||||
if len(s) == 0:
|
||||
self.search_restriction = ''
|
||||
def set_search_restriction(self, s):
|
||||
if s:
|
||||
self.search_restriction = s
|
||||
else:
|
||||
self.search_restriction = 'search:"%s"' % unicode(s).strip()
|
||||
self.model().set_search_restriction(self.search_restriction)
|
||||
self.restriction_set.emit(self.search_restriction)
|
||||
self.recount() # Must happen after the emission of the restriction_set signal
|
||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
||||
self.search_restriction = None
|
||||
self.set_new_model()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# Swallow everything except leftButton so context menus work correctly
|
||||
if event.button() == Qt.LeftButton:
|
||||
QTreeView.mouseReleaseEvent(self, event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
# swallow these to avoid toggling and editing at the same time
|
||||
pass
|
||||
|
||||
def toggle(self, index):
|
||||
modifiers = int(QApplication.keyboardModifiers())
|
||||
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
||||
if self._model.toggle(index, exclusive):
|
||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
||||
|
||||
def context_menu_handler(self, action=None, category=None):
|
||||
def context_menu_handler(self, action=None, category=None,
|
||||
key=None, index=None):
|
||||
if not action:
|
||||
return
|
||||
try:
|
||||
if action == 'manage_tags':
|
||||
self.tag_list_edit.emit(category)
|
||||
if action == 'edit_item':
|
||||
self.edit(index)
|
||||
return
|
||||
if action == 'open_editor':
|
||||
self.tag_list_edit.emit(category, key)
|
||||
return
|
||||
if action == 'manage_categories':
|
||||
self.user_category_edit.emit(category)
|
||||
@ -117,29 +123,51 @@ class TagsView(QTreeView): # {{{
|
||||
item = index.internalPointer()
|
||||
tag_name = ''
|
||||
if item.type == TagTreeItem.TAG:
|
||||
tag_item = item
|
||||
tag_name = item.tag.name
|
||||
item = item.parent
|
||||
if item.type == TagTreeItem.CATEGORY:
|
||||
category = unicode(item.name.toString())
|
||||
self.context_menu = QMenu(self)
|
||||
self.context_menu.addAction(_('Hide %s') % category,
|
||||
partial(self.context_menu_handler, action='hide', category=category))
|
||||
key = item.category_key
|
||||
# Verify that we are working with a field that we know something about
|
||||
if key not in self.db.field_metadata:
|
||||
return True
|
||||
|
||||
if self.hidden_categories:
|
||||
self.context_menu = QMenu(self)
|
||||
# If the user right-clicked on an editable item, then offer
|
||||
# the possibility of renaming that item
|
||||
if tag_name and \
|
||||
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
|
||||
self.db.field_metadata[key]['is_custom']):
|
||||
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
|
||||
partial(self.context_menu_handler, action='edit_item',
|
||||
category=tag_item, index=index))
|
||||
self.context_menu.addSeparator()
|
||||
# Hide/Show/Restore categories
|
||||
self.context_menu.addAction(_('Hide category %s') % category,
|
||||
partial(self.context_menu_handler, action='hide', category=category))
|
||||
if self.hidden_categories:
|
||||
m = self.context_menu.addMenu(_('Show category'))
|
||||
for col in self.hidden_categories:
|
||||
for col in sorted(self.hidden_categories, cmp=lambda x,y: cmp(x.lower(), y.lower())):
|
||||
m.addAction(col,
|
||||
partial(self.context_menu_handler, action='show', category=col))
|
||||
self.context_menu.addSeparator()
|
||||
self.context_menu.addAction(_('Restore defaults'),
|
||||
self.context_menu.addAction(_('Show all categories'),
|
||||
partial(self.context_menu_handler, action='defaults'))
|
||||
|
||||
# Offer specific editors for tags/series/publishers/saved searches
|
||||
self.context_menu.addSeparator()
|
||||
self.context_menu.addAction(_('Manage Tags'),
|
||||
partial(self.context_menu_handler, action='manage_tags',
|
||||
category=tag_name))
|
||||
if key in ['tags', 'publisher', 'series'] or \
|
||||
self.db.field_metadata[key]['is_custom']:
|
||||
self.context_menu.addAction(_('Manage ') + category,
|
||||
partial(self.context_menu_handler, action='open_editor',
|
||||
category=tag_name, key=key))
|
||||
elif key == 'search':
|
||||
self.context_menu.addAction(_('Manage Saved Searches'),
|
||||
partial(self.context_menu_handler, action='manage_searches',
|
||||
category=tag_name))
|
||||
|
||||
# Always show the user categories editor
|
||||
self.context_menu.addSeparator()
|
||||
if category in prefs['user_categories'].keys():
|
||||
self.context_menu.addAction(_('Manage User Categories'),
|
||||
partial(self.context_menu_handler, action='manage_categories',
|
||||
@ -149,29 +177,12 @@ class TagsView(QTreeView): # {{{
|
||||
partial(self.context_menu_handler, action='manage_categories',
|
||||
category=None))
|
||||
|
||||
self.context_menu.addAction(_('Manage Saved Searches'),
|
||||
partial(self.context_menu_handler, action='manage_searches',
|
||||
category=tag_name))
|
||||
|
||||
self.context_menu.popup(self.mapToGlobal(point))
|
||||
return True
|
||||
|
||||
def clear(self):
|
||||
self.model().clear_state()
|
||||
|
||||
def saved_searches_changed(self, recount=True):
|
||||
p = prefs['saved_searches'].keys()
|
||||
p.sort()
|
||||
t = self.restriction.currentText()
|
||||
self.restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||
self.restriction.addItem('')
|
||||
for s in p:
|
||||
self.restriction.addItem(s)
|
||||
if t in p: # redo the current restriction, if there was one
|
||||
self.restriction.setCurrentIndex(self.restriction.findText(t))
|
||||
self.search_restriction_set(t)
|
||||
if recount:
|
||||
self.recount()
|
||||
if self.model():
|
||||
self.model().clear_state()
|
||||
|
||||
def recount(self, *args):
|
||||
ci = self.currentIndex()
|
||||
@ -193,7 +204,8 @@ class TagsView(QTreeView): # {{{
|
||||
# model. Reason: it is much easier than reconstructing the browser tree.
|
||||
def set_new_model(self):
|
||||
self._model = TagsModel(self.db, parent=self,
|
||||
hidden_categories=self.hidden_categories)
|
||||
hidden_categories=self.hidden_categories,
|
||||
search_restriction=self.search_restriction)
|
||||
self.setModel(self._model)
|
||||
# }}}
|
||||
|
||||
@ -203,7 +215,8 @@ class TagTreeItem(object): # {{{
|
||||
TAG = 1
|
||||
ROOT = 2
|
||||
|
||||
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None):
|
||||
def __init__(self, data=None, category_icon=None, icon_map=None,
|
||||
parent=None, tooltip=None, category_key=None):
|
||||
self.parent = parent
|
||||
self.children = []
|
||||
if self.parent is not None:
|
||||
@ -218,6 +231,7 @@ class TagTreeItem(object): # {{{
|
||||
self.bold_font = QFont()
|
||||
self.bold_font.setBold(True)
|
||||
self.bold_font = QVariant(self.bold_font)
|
||||
self.category_key = category_key
|
||||
elif self.type == self.TAG:
|
||||
icon_map[0] = data.icon
|
||||
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
||||
@ -263,6 +277,8 @@ class TagTreeItem(object): # {{{
|
||||
return QVariant('%s'%(self.tag.name))
|
||||
else:
|
||||
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
||||
if role == Qt.EditRole:
|
||||
return QVariant(self.tag.name)
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icon_state_map[self.tag.state]
|
||||
if role == Qt.ToolTipRole and self.tag.tooltip is not None:
|
||||
@ -277,7 +293,7 @@ class TagTreeItem(object): # {{{
|
||||
|
||||
class TagsModel(QAbstractItemModel): # {{{
|
||||
|
||||
def __init__(self, db, parent=None, hidden_categories=None):
|
||||
def __init__(self, db, parent, hidden_categories=None, search_restriction=None):
|
||||
QAbstractItemModel.__init__(self, parent)
|
||||
|
||||
# must do this here because 'QPixmap: Must construct a QApplication
|
||||
@ -297,9 +313,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
|
||||
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
||||
self.db = db
|
||||
self.tags_view = parent
|
||||
self.hidden_categories = hidden_categories
|
||||
self.search_restriction = ''
|
||||
self.ignore_next_search = 0
|
||||
self.search_restriction = search_restriction
|
||||
|
||||
# Reconstruct the user categories, putting them into metadata
|
||||
tb_cats = self.db.field_metadata
|
||||
@ -324,7 +340,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
c = TagTreeItem(parent=self.root_item,
|
||||
data=self.categories[i],
|
||||
category_icon=self.category_icon_map[r],
|
||||
tooltip=tt)
|
||||
tooltip=tt, category_key=r)
|
||||
for tag in data[r]:
|
||||
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
||||
|
||||
@ -335,18 +351,22 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
self.row_map = []
|
||||
self.categories = []
|
||||
|
||||
if len(self.search_restriction):
|
||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map,
|
||||
ids=self.db.search(self.search_restriction, return_matches=True))
|
||||
if self.search_restriction:
|
||||
data = self.db.get_categories(sort_on_count=sort,
|
||||
icon_map=self.category_icon_map,
|
||||
ids=self.db.search('', return_matches=True))
|
||||
else:
|
||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
|
||||
|
||||
tb_categories = self.db.field_metadata
|
||||
self.category_items = {}
|
||||
for category in tb_categories:
|
||||
if category in data: # They should always be there, but ...
|
||||
# make a map of sets of names per category for duplicate
|
||||
# checking when editing
|
||||
self.category_items[category] = set([tag.name for tag in data[category]])
|
||||
self.row_map.append(category)
|
||||
self.categories.append(tb_categories[category]['name'])
|
||||
|
||||
return data
|
||||
|
||||
def refresh(self):
|
||||
@ -382,11 +402,52 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
item = index.internalPointer()
|
||||
return item.data(role)
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if not index.isValid():
|
||||
return NONE
|
||||
val = unicode(value.toString())
|
||||
if not val:
|
||||
error_dialog(self.tags_view, _('Item is blank'),
|
||||
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||
return False
|
||||
item = index.internalPointer()
|
||||
key = item.parent.category_key
|
||||
# make certain we know about the category
|
||||
if key not in self.db.field_metadata:
|
||||
return
|
||||
if val in self.category_items[key]:
|
||||
error_dialog(self.tags_view, 'Duplicate item',
|
||||
_('The name %s is already used.')%val).exec_()
|
||||
return False
|
||||
oldval = item.tag.name
|
||||
if key == 'search':
|
||||
saved_searches.rename(unicode(item.data(role).toString()), val)
|
||||
self.tags_view.search_item_renamed.emit()
|
||||
else:
|
||||
if key == 'series':
|
||||
self.db.rename_series(item.tag.id, val)
|
||||
elif key == 'publisher':
|
||||
self.db.rename_publisher(item.tag.id, val)
|
||||
elif key == 'tags':
|
||||
self.db.rename_tag(item.tag.id, val)
|
||||
elif key == 'authors':
|
||||
self.db.rename_author(item.tag.id, val)
|
||||
elif self.db.field_metadata[key]['is_custom']:
|
||||
self.db.rename_custom_item(item.tag.id, val,
|
||||
label=self.db.field_metadata[key]['label'])
|
||||
self.tags_view.tag_item_renamed.emit()
|
||||
item.tag.name = val
|
||||
self.dataChanged.emit(index, index)
|
||||
# replace the old value in the duplicate detection map with the new one
|
||||
self.category_items[key].discard(oldval)
|
||||
self.category_items[key].add(val)
|
||||
return True
|
||||
|
||||
def headerData(self, *args):
|
||||
return NONE
|
||||
|
||||
def flags(self, *args):
|
||||
return Qt.ItemIsEnabled|Qt.ItemIsSelectable
|
||||
return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
|
||||
|
||||
def path_for_index(self, index):
|
||||
ans = []
|
||||
@ -464,12 +525,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
def clear_state(self):
|
||||
self.reset_all_states()
|
||||
|
||||
def reinit(self, *args, **kwargs):
|
||||
if self.ignore_next_search == 0:
|
||||
self.reset_all_states()
|
||||
else:
|
||||
self.ignore_next_search -= 1
|
||||
|
||||
def toggle(self, index, exclusive):
|
||||
if not index.isValid(): return False
|
||||
item = index.internalPointer()
|
||||
@ -477,7 +532,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
item.toggle()
|
||||
if exclusive:
|
||||
self.reset_all_states(except_=item.tag)
|
||||
self.ignore_next_search = 2
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
return False
|
||||
|
@ -160,9 +160,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.restriction_in_effect = False
|
||||
self.search.initialize('main_search_history', colorize=True,
|
||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
||||
self.search_clear()
|
||||
self.search.clear()
|
||||
|
||||
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
||||
help_text=_('Saved Searches'))
|
||||
@ -226,14 +226,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
||||
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
||||
self.connect(self.restore_action, SIGNAL('triggered()'),
|
||||
self.show_windows)
|
||||
self.show_windows)
|
||||
self.connect(self.action_show_book_details,
|
||||
SIGNAL('triggered(bool)'), self.show_book_info)
|
||||
SIGNAL('triggered(bool)'), self.show_book_info)
|
||||
self.connect(self.action_restart, SIGNAL('triggered()'),
|
||||
self.restart)
|
||||
self.connect(self.system_tray_icon,
|
||||
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
||||
self.system_tray_icon_activated)
|
||||
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
||||
self.system_tray_icon_activated)
|
||||
self.tool_bar.contextMenuEvent = self.no_op
|
||||
|
||||
####################### Start spare job server ########################
|
||||
@ -521,8 +521,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.search_done)),
|
||||
('connect_to_book_display',
|
||||
(self.status_bar.book_info.show_data,)),
|
||||
('connect_to_restriction_set',
|
||||
(self.tags_view,)),
|
||||
]:
|
||||
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
|
||||
getattr(view, func)(*args)
|
||||
@ -545,22 +543,22 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.cover_cache.start()
|
||||
self.library_view.model().cover_cache = self.cover_cache
|
||||
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
|
||||
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
|
||||
self.search_restriction.activated[str].connect(self.apply_search_restriction)
|
||||
self.tags_view.set_database(db, self.tag_match, self.popularity)
|
||||
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
||||
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
|
||||
self.tags_view.restriction_set.connect(x)
|
||||
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
||||
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
||||
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||
self.search.search.connect(self.tags_view.model().reinit)
|
||||
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
||||
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
|
||||
for x in (self.location_view.count_changed, self.tags_view.recount,
|
||||
self.restriction_count_changed):
|
||||
self.library_view.model().count_changed_signal.connect(x)
|
||||
|
||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||
self.connect(self.saved_search, SIGNAL('changed()'),
|
||||
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
||||
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
|
||||
self.saved_searches_changed()
|
||||
if not gprefs.get('quick_start_guide_added', False):
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||
@ -583,7 +581,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.search_restriction.setMinimumContentsLength(10)
|
||||
|
||||
|
||||
########################### Cover Flow ################################
|
||||
self.cover_flow = None
|
||||
if CoverFlow is not None:
|
||||
@ -623,7 +620,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.sidebar.job_done, Qt.QueuedConnection)
|
||||
|
||||
|
||||
|
||||
if config['autolaunch_server']:
|
||||
from calibre.library.server.main import start_threaded_server
|
||||
from calibre.library.server import server_config
|
||||
@ -660,19 +656,28 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.tags_view.set_new_model()
|
||||
self.tags_view.recount()
|
||||
|
||||
def do_tags_list_edit(self, tag):
|
||||
d = TagListEditor(self, self.library_view.model().db, tag)
|
||||
def do_tags_list_edit(self, tag, category):
|
||||
d = TagListEditor(self, self.library_view.model().db, tag, category)
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
# Clean up everything, as information could have changed for many books.
|
||||
self.library_view.model().refresh()
|
||||
self.tags_view.set_new_model()
|
||||
self.tags_view.recount()
|
||||
self.library_view.model().refresh()
|
||||
self.saved_search.clear_to_help()
|
||||
self.search.clear_to_help()
|
||||
|
||||
def do_tag_item_renamed(self):
|
||||
# Clean up library view and search
|
||||
self.library_view.model().refresh()
|
||||
self.saved_search.clear_to_help()
|
||||
self.search.clear_to_help()
|
||||
|
||||
def do_saved_search_edit(self, search):
|
||||
d = SavedSearchEditor(self, search)
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
self.tags_view.saved_searches_changed(recount=True)
|
||||
self.saved_searches_changed()
|
||||
self.saved_search.clear_to_help()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
@ -831,19 +836,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
sm.select(idx, sm.ClearAndSelect|sm.Rows)
|
||||
self.library_view.setCurrentIndex(idx)
|
||||
|
||||
|
||||
|
||||
'''
|
||||
Handling of the count of books in a restricted view requires that
|
||||
we capture the count after the initial restriction search. To so this,
|
||||
we require that the restriction_set signal be issued before the search signal,
|
||||
so that when the search_done happens and the count is displayed,
|
||||
we can grab the count. This works because the search box is cleared
|
||||
when a restriction is set, so that first search will find all books.
|
||||
|
||||
Adding and deleting books creates another complexity. When added, they are
|
||||
displayed regardless of whether they match the restriction. However, if they
|
||||
do not, they are removed at the next search. The counts must take this
|
||||
Restrictions.
|
||||
Adding and deleting books creates a complexity. When added, they are
|
||||
displayed regardless of whether they match a search restriction. However, if
|
||||
they do not, they are removed at the next search. The counts must take this
|
||||
behavior into effect.
|
||||
'''
|
||||
|
||||
@ -851,15 +848,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
|
||||
self.restriction_count_of_books_in_library = c
|
||||
if self.restriction_in_effect:
|
||||
self.set_number_of_books_shown(compute_count=False)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def mark_restriction_set(self, r):
|
||||
self.restriction_in_effect = False if r is None or not r else True
|
||||
def apply_search_restriction(self, r):
|
||||
r = unicode(r)
|
||||
if r is not None and r != '':
|
||||
self.restriction_in_effect = True
|
||||
restriction = "search:%s"%(r)
|
||||
else:
|
||||
self.restriction_in_effect = False
|
||||
restriction = ''
|
||||
self.restriction_count_of_books_in_view = \
|
||||
self.library_view.model().set_search_restriction(restriction)
|
||||
self.search.clear_to_help()
|
||||
self.saved_search.clear_to_help()
|
||||
self.tags_view.set_search_restriction(restriction)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def set_number_of_books_shown(self, compute_count):
|
||||
def set_number_of_books_shown(self):
|
||||
if self.current_view() == self.library_view and self.restriction_in_effect:
|
||||
if compute_count:
|
||||
self.restriction_count_of_books_in_view = self.current_view().row_count()
|
||||
t = _("({0} of {1})").format(self.current_view().row_count(),
|
||||
self.restriction_count_of_books_in_view)
|
||||
self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }')
|
||||
@ -873,18 +880,31 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.search_count.setText(t)
|
||||
|
||||
def search_box_cleared(self):
|
||||
self.set_number_of_books_shown(compute_count=True)
|
||||
self.tags_view.clear()
|
||||
self.saved_search.clear_to_help()
|
||||
|
||||
def search_clear(self):
|
||||
self.set_number_of_books_shown(compute_count=True)
|
||||
self.search.clear()
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def search_done(self, view, ok):
|
||||
if view is self.current_view():
|
||||
self.search.search_done(ok)
|
||||
self.set_number_of_books_shown(compute_count=False)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
def saved_searches_changed(self):
|
||||
p = prefs['saved_searches'].keys()
|
||||
p.sort()
|
||||
t = unicode(self.search_restriction.currentText())
|
||||
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||
self.search_restriction.addItem('')
|
||||
self.tags_view.recount()
|
||||
for s in p:
|
||||
self.search_restriction.addItem(s)
|
||||
if t:
|
||||
if t in p: # redo the current restriction, if there was one
|
||||
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
|
||||
# self.tags_view.set_search_restriction(t)
|
||||
else:
|
||||
self.search_restriction.setCurrentIndex(0)
|
||||
self.apply_search_restriction('')
|
||||
|
||||
def sync_cf_to_listview(self, current, previous):
|
||||
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
|
||||
@ -2293,14 +2313,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
def library_moved(self, newloc):
|
||||
if newloc is None: return
|
||||
db = LibraryDatabase2(newloc)
|
||||
self.library_path = newloc
|
||||
self.book_on_device(None, reset=True)
|
||||
db.set_book_on_device_func(self.book_on_device)
|
||||
self.library_view.set_database(db)
|
||||
self.tags_view.set_database(db, self.tag_match, self.popularity)
|
||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||
self.status_bar.clearMessage()
|
||||
self.search.clear_to_help()
|
||||
self.status_bar.reset_info()
|
||||
self.library_view.model().count_changed()
|
||||
prefs['library_path'] = self.library_path
|
||||
|
||||
############################################################################
|
||||
|
||||
@ -2347,7 +2370,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.search_restriction.setEnabled(False)
|
||||
for action in list(self.delete_menu.actions())[1:]:
|
||||
action.setEnabled(False)
|
||||
self.set_number_of_books_shown(compute_count=False)
|
||||
self.set_number_of_books_shown()
|
||||
|
||||
|
||||
def device_job_exception(self, job):
|
||||
|
@ -424,7 +424,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.set_bookmarks(self.iterator.bookmarks)
|
||||
|
||||
|
||||
def find(self, text, refinement, repeat=False, backwards=False):
|
||||
def find(self, text, repeat=False, backwards=False):
|
||||
if not text:
|
||||
self.view.search('')
|
||||
return self.search.search_done(False)
|
||||
|
@ -241,6 +241,24 @@ class ResultCache(SearchQueryParser):
|
||||
matches = set([])
|
||||
if len(query) < 2:
|
||||
return matches
|
||||
|
||||
if location == 'date':
|
||||
location = 'timestamp'
|
||||
loc = self.field_metadata[location]['rec_index']
|
||||
|
||||
if query == 'false':
|
||||
for item in self._data:
|
||||
if item is None: continue
|
||||
if item[loc] is None or item[loc] == UNDEFINED_DATE:
|
||||
matches.add(item[0])
|
||||
return matches
|
||||
if query == 'true':
|
||||
for item in self._data:
|
||||
if item is None: continue
|
||||
if item[loc] is not None and item[loc] != UNDEFINED_DATE:
|
||||
matches.add(item[0])
|
||||
return matches
|
||||
|
||||
relop = None
|
||||
for k in self.date_search_relops.keys():
|
||||
if query.startswith(k):
|
||||
@ -249,10 +267,6 @@ class ResultCache(SearchQueryParser):
|
||||
if relop is None:
|
||||
(p, relop) = self.date_search_relops['=']
|
||||
|
||||
if location == 'date':
|
||||
location = 'timestamp'
|
||||
loc = self.field_metadata[location]['rec_index']
|
||||
|
||||
if query == _('today'):
|
||||
qd = now()
|
||||
field_count = 3
|
||||
@ -301,7 +315,7 @@ class ResultCache(SearchQueryParser):
|
||||
if query == 'false':
|
||||
query = '0'
|
||||
elif query == 'true':
|
||||
query = '>0'
|
||||
query = '!=0'
|
||||
relop = None
|
||||
for k in self.numeric_search_relops.keys():
|
||||
if query.startswith(k):
|
||||
|
@ -171,6 +171,40 @@ class CustomColumns(object):
|
||||
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
||||
return ans
|
||||
|
||||
# convenience methods for tag editing
|
||||
def get_custom_items_with_ids(self, label=None, num=None):
|
||||
if label is not None:
|
||||
data = self.custom_column_label_map[label]
|
||||
if num is not None:
|
||||
data = self.custom_column_num_map[num]
|
||||
table,lt = self.custom_table_names(data['num'])
|
||||
if not data['normalized']:
|
||||
return []
|
||||
ans = self.conn.get('SELECT id, value FROM %s'%table)
|
||||
return ans
|
||||
|
||||
def rename_custom_item(self, id, new_name, label=None, num=None):
|
||||
if id:
|
||||
if label is not None:
|
||||
data = self.custom_column_label_map[label]
|
||||
if num is not None:
|
||||
data = self.custom_column_num_map[num]
|
||||
table,lt = self.custom_table_names(data['num'])
|
||||
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id))
|
||||
self.conn.commit()
|
||||
|
||||
def delete_custom_item_using_id(self, id, label=None, num=None):
|
||||
if id:
|
||||
if label is not None:
|
||||
data = self.custom_column_label_map[label]
|
||||
if num is not None:
|
||||
data = self.custom_column_num_map[num]
|
||||
table,lt = self.custom_table_names(data['num'])
|
||||
self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id,))
|
||||
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,))
|
||||
self.conn.commit()
|
||||
# end convenience methods
|
||||
|
||||
def all_custom(self, label=None, num=None):
|
||||
if label is not None:
|
||||
data = self.custom_column_label_map[label]
|
||||
|
@ -9,12 +9,6 @@ The database used to store ebook metadata
|
||||
import os, sys, shutil, cStringIO, glob,functools, traceback
|
||||
from itertools import repeat
|
||||
from math import floor
|
||||
try:
|
||||
from PIL import Image as PILImage
|
||||
PILImage
|
||||
except ImportError:
|
||||
import Image as PILImage
|
||||
|
||||
|
||||
from PyQt4.QtGui import QImage
|
||||
|
||||
@ -37,7 +31,7 @@ from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||
|
||||
from calibre.utils.magick_draw import save_cover_data_to
|
||||
|
||||
if iswindows:
|
||||
import calibre.utils.winshell as winshell
|
||||
@ -475,11 +469,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
if callable(getattr(data, 'save', None)):
|
||||
data.save(path)
|
||||
else:
|
||||
f = data
|
||||
if not callable(getattr(data, 'read', None)):
|
||||
f = cStringIO.StringIO(data)
|
||||
im = PILImage.open(f)
|
||||
im.convert('RGB').save(path, 'JPEG')
|
||||
if callable(getattr(data, 'read', None)):
|
||||
data = data.read()
|
||||
save_cover_data_to(data, path)
|
||||
|
||||
def book_on_device(self, id):
|
||||
if callable(self.book_on_device_func):
|
||||
@ -643,11 +635,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
'''
|
||||
Remove orphaned entries.
|
||||
'''
|
||||
st = 'DELETE FROM %(table)s WHERE (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=%(table)s.id) < 1;'
|
||||
self.conn.execute(st%dict(ltable='authors', table='authors', ltable_col='author'))
|
||||
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
|
||||
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
|
||||
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
|
||||
def doit(ltable, table, ltable_col):
|
||||
st = ('DELETE FROM books_%s_link WHERE (SELECT COUNT(id) '
|
||||
'FROM books WHERE id=book) < 1;')%ltable
|
||||
self.conn.execute(st)
|
||||
st = ('DELETE FROM %(table)s WHERE (SELECT COUNT(id) '
|
||||
'FROM books_%(ltable)s_link WHERE '
|
||||
'%(ltable_col)s=%(table)s.id) < 1;') % dict(
|
||||
ltable=ltable, table=table, ltable_col=ltable_col)
|
||||
self.conn.execute(st)
|
||||
|
||||
for ltable, table, ltable_col in [
|
||||
('authors', 'authors', 'author'),
|
||||
('publishers', 'publishers', 'publisher'),
|
||||
('tags', 'tags', 'tag'),
|
||||
('series', 'series', 'series')
|
||||
]:
|
||||
doit(ltable, table, ltable_col)
|
||||
|
||||
for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
|
||||
if not tag.strip():
|
||||
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
|
||||
@ -730,9 +735,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
||||
icon=icon, tooltip = tooltip)
|
||||
for r in data if item_not_zero_func(r)]
|
||||
if category == 'series':
|
||||
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name),
|
||||
title_sort(y.name)))
|
||||
if category == 'series' and not sort_on_count:
|
||||
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
|
||||
title_sort(y.name).lower()))
|
||||
|
||||
# We delayed computing the standard formats category because it does not
|
||||
# use a view, but is computed dynamically
|
||||
@ -985,19 +990,91 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
if notify:
|
||||
self.notify('metadata', [id])
|
||||
|
||||
# Convenience method for tags_list_editor
|
||||
# Convenience methods for tags_list_editor
|
||||
# Note: we generally do not need to refresh_ids because library_view will
|
||||
# refresh everything.
|
||||
def get_tags_with_ids(self):
|
||||
result = self.conn.get('SELECT * FROM tags')
|
||||
result = self.conn.get('SELECT id,name FROM tags')
|
||||
if not result:
|
||||
return {}
|
||||
r = []
|
||||
for k,v in result:
|
||||
r.append((k,v))
|
||||
return r
|
||||
return []
|
||||
return result
|
||||
|
||||
def rename_tag(self, id, new):
|
||||
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
|
||||
self.conn.commit()
|
||||
def rename_tag(self, id, new_name):
|
||||
if id:
|
||||
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id))
|
||||
self.conn.commit()
|
||||
|
||||
def delete_tag_using_id(self, id):
|
||||
if id:
|
||||
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,))
|
||||
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||
self.conn.commit()
|
||||
|
||||
def get_series_with_ids(self):
|
||||
result = self.conn.get('SELECT id,name FROM series')
|
||||
if not result:
|
||||
return []
|
||||
return result
|
||||
|
||||
def rename_series(self, id, new_name):
|
||||
if id:
|
||||
self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id))
|
||||
self.conn.commit()
|
||||
|
||||
def delete_series_using_id(self, id):
|
||||
if id:
|
||||
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
|
||||
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
|
||||
self.conn.execute('DELETE FROM series WHERE id=?', (id,))
|
||||
self.conn.commit()
|
||||
for (book_id,) in books:
|
||||
self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,))
|
||||
|
||||
def get_publishers_with_ids(self):
|
||||
result = self.conn.get('SELECT id,name FROM publishers')
|
||||
if not result:
|
||||
return []
|
||||
return result
|
||||
|
||||
def rename_publisher(self, id, new_name):
|
||||
if id:
|
||||
self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id))
|
||||
self.conn.commit()
|
||||
|
||||
def delete_publisher_using_id(self, id):
|
||||
if id:
|
||||
self.conn.execute('DELETE FROM books_publishers_link WHERE publisher=?', (id,))
|
||||
self.conn.execute('DELETE FROM publishers WHERE id=?', (id,))
|
||||
self.conn.commit()
|
||||
|
||||
# There is no editor for author, so we do not need get_authors_with_ids or
|
||||
# delete_author_using_id.
|
||||
def rename_author(self, id, new_name):
|
||||
if id:
|
||||
# Make sure that any commas in new_name are changed to '|'!
|
||||
new_name = new_name.replace(',', '|')
|
||||
self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, id))
|
||||
self.conn.commit()
|
||||
# now must fix up the books
|
||||
books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (id,))
|
||||
for (book_id,) in books:
|
||||
# First, must refresh the cache to see the new authors
|
||||
self.data.refresh_ids(self, [book_id])
|
||||
# now fix the filesystem paths
|
||||
self.set_path(book_id, index_is_id=True)
|
||||
# Next fix the author sort. Reset it to the default
|
||||
authors = self.conn.get('''
|
||||
SELECT authors.name
|
||||
FROM authors, books_authors_link as bl
|
||||
WHERE bl.book = ? and bl.author = authors.id
|
||||
''' , (book_id,))
|
||||
# unpack the double-list structure
|
||||
for i,aut in enumerate(authors):
|
||||
authors[i] = aut[0]
|
||||
ss = authors_to_sort_string(authors)
|
||||
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id))
|
||||
|
||||
# end convenience methods
|
||||
|
||||
def get_tags(self, id):
|
||||
result = self.conn.get(
|
||||
@ -1083,7 +1160,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
def set_series(self, id, series, notify=True):
|
||||
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
|
||||
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1')
|
||||
@ -1603,6 +1679,7 @@ books_series_link feeds
|
||||
|
||||
def check_integrity(self, callback):
|
||||
callback(0., _('Checking SQL integrity...'))
|
||||
self.clean()
|
||||
user_version = self.user_version
|
||||
sql = '\n'.join(self.conn.dump())
|
||||
self.conn.close()
|
||||
|
@ -195,11 +195,11 @@ class FieldMetadata(dict):
|
||||
'is_category':False}),
|
||||
('ondevice', {'table':None,
|
||||
'column':None,
|
||||
'datatype':'bool',
|
||||
'datatype':'text',
|
||||
'is_multiple':None,
|
||||
'kind':'field',
|
||||
'name':None,
|
||||
'search_terms':[],
|
||||
'search_terms':['ondevice'],
|
||||
'is_custom':False,
|
||||
'is_category':False}),
|
||||
('path', {'table':None,
|
||||
|
@ -38,6 +38,12 @@ def server_config(defaults=None):
|
||||
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
|
||||
help=_('The maximum number of matches to return per OPDS query. '
|
||||
'This affects Stanza, WordPlayer, etc. integration.'))
|
||||
c.add_opt('max_opds_ungrouped_items', ['--max-opds-ungrouped-items'],
|
||||
default=100,
|
||||
help=_('Group items in categories such as author/tags '
|
||||
'by first letter when there are more than this number '
|
||||
'of items. Default: %default. Set to a large number '
|
||||
'to disable grouping.'))
|
||||
return c
|
||||
|
||||
def main():
|
||||
|
@ -127,10 +127,7 @@ class ContentServer(object):
|
||||
cherrypy.log('User agent: '+ua)
|
||||
|
||||
if want_opds:
|
||||
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
|
||||
tagid=kwargs.get('tagid',None),
|
||||
seriesid=kwargs.get('seriesid',None),
|
||||
offset=kwargs.get('offset', 0))
|
||||
return self.opds(version=0)
|
||||
|
||||
if want_mobile:
|
||||
return self.mobile()
|
||||
|
@ -25,7 +25,7 @@ BASE_HREFS = {
|
||||
1 : '/opds',
|
||||
}
|
||||
|
||||
STANZA_FORMATS = frozenset(['epub', 'pdb'])
|
||||
STANZA_FORMATS = frozenset(['epub', 'pdb', 'pdf', 'cbr', 'cbz', 'djvu'])
|
||||
|
||||
def url_for(name, version, **kwargs):
|
||||
if not name.endswith('_'):
|
||||
@ -121,7 +121,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
|
||||
TITLE(item.text),
|
||||
ID(id_),
|
||||
UPDATED(updated),
|
||||
E.content(_('%d books')%item.count, type='text'),
|
||||
E.content(_('%d items')%item.count, type='text'),
|
||||
link
|
||||
)
|
||||
|
||||
@ -445,7 +445,7 @@ class OPDSServer(object):
|
||||
|
||||
id_ = 'calibre-category-feed:'+which
|
||||
|
||||
MAX_ITEMS = 50
|
||||
MAX_ITEMS = self.opts.max_opds_ungrouped_items
|
||||
|
||||
if len(items) <= MAX_ITEMS:
|
||||
max_items = self.opts.max_opds_items
|
||||
@ -459,8 +459,6 @@ class OPDSServer(object):
|
||||
self.text, self.count = text, count
|
||||
|
||||
starts = set([x.name[0] for x in items])
|
||||
if len(starts) > MAX_ITEMS:
|
||||
starts = set([x.name[:2] for x in items])
|
||||
category_groups = OrderedDict()
|
||||
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
||||
category_groups[x] = len([y for y in items if
|
||||
|
@ -8,16 +8,25 @@ Customizing |app|
|
||||
==================================
|
||||
|
||||
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
|
||||
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn how to
|
||||
use *plugins* to customize and control various aspects of |app|'s behavior.
|
||||
|
||||
Theer are different kinds of plugins, corresponding to different aspects of |app|. As more and more aspects of |app|
|
||||
are modularized, new plugin types will be added.
|
||||
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
|
||||
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
|
||||
use *plugins* to add funtionality to |app|.
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
|
||||
Environment variables
|
||||
-----------------------
|
||||
|
||||
* ``CALIBRE_CONFIG_DIRECTORY``
|
||||
* ``CALIBRE_OVERRIDE_DATABASE_PATH``
|
||||
* ``CALIBRE_DEVELOP_FROM``
|
||||
* ``CALIBRE_OVERRIDE_LANG``
|
||||
* ``SYSFS_PATH``
|
||||
* ``http_proxy``
|
||||
|
||||
|
||||
A Hello World plugin
|
||||
------------------------
|
||||
|
||||
|
@ -135,29 +135,18 @@ turned into a collection on the reader. Note that the PRS-500 does not support c
|
||||
How do I use |app| with my iPad/iPhone/iTouch?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can access your calibre library on a iPad/iPhone/iTouch over the air using the calibre content server.
|
||||
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
|
||||
|
||||
First perform the following steps in |app|
|
||||
|
||||
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under Preferences->General)
|
||||
* Set the output profile to iPad (this will work for iPhone/iPods as well), under Preferences->Conversion->Page Setup
|
||||
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
||||
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
||||
|
||||
For an iPad:
|
||||
Install the free Stanza reader app on your iPad/iPhone/iTouch using iTunes.
|
||||
|
||||
Install the ReadMe app on your iPad using iTunes. Open the Readme builtin browser and browse to::
|
||||
|
||||
http://192.168.1.2:8080/
|
||||
|
||||
Replace ``192.168.1.2`` with the local IP address of the computer running |app|. If you have changed the port the |app| content server is running on, you will have to change ``8080`` as well to the new port. The local IP address is the IP address you computer is assigned on your home network. A quick Google search will tell you how to find out your local IP address.
|
||||
|
||||
The books in your |app| library will be presented as a list, 25 entries at a time. Click the right arrow to go to the next 25. You can also type in the search box to find specific books. Just click on the EPUB link of the book you want and it will be downloaded into your ReadMe library.
|
||||
|
||||
For an iPhone/iTouch:
|
||||
|
||||
Install the free Stanza reader app on your iPhone/iTouch using iTunes.
|
||||
|
||||
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
||||
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPad/iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
||||
|
||||
http://192.168.1.2:8080/
|
||||
|
||||
@ -165,7 +154,12 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
|
||||
|
||||
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
||||
|
||||
Note that neither the Stanza, nor the ReadMe apps are in anyway associated with |app|.
|
||||
Alternative for the iPad
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As of |app| version 0.7.0, you can plugin your iPad into the computer using its charging cable, and |app| will detect it and show you a list of books on the iPad. You can then use the Send to device button to send books directly to iBooks on the iPad.
|
||||
|
||||
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
|
||||
|
||||
How do I use |app| with my Android phone?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -9,7 +9,7 @@ The Graphical User Interface *(GUI)* provides access to all
|
||||
library management and ebook format conversion features. The basic workflow
|
||||
for using |app| is to first add books to the library from your hard disk.
|
||||
|app| will automatically try to read metadata from the books and add them
|
||||
to its internal database. Once they are in the database, you can performa various
|
||||
to its internal database. Once they are in the database, you can perform a various
|
||||
:ref:`actions` on them that include conversion from one format to another,
|
||||
transfer to the reading device, viewing on your computer, editing metadata, including covers, etc.
|
||||
|
||||
@ -241,9 +241,9 @@ Now, you can access your saved search in the Tag Browser under "Searches". A sin
|
||||
|
||||
.. _configuration:
|
||||
|
||||
Configuration
|
||||
Preferences
|
||||
---------------
|
||||
The configuration dialog allows you to set some global defaults used by all of |app|. To access it, click the |cbi|.
|
||||
The Preferences dialog allows you to change the way various aspects of |app| work. To access it, click the |cbi|.
|
||||
|
||||
.. |cbi| image:: images/configuration.png
|
||||
|
||||
@ -251,7 +251,7 @@ The configuration dialog allows you to set some global defaults used by all of |
|
||||
|
||||
Guessing metadata from file names
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
In the :guilabel:`Advanced` section of the configuration dialog, you can specify a regularexpression that |app| will use to try and guess metadata from the names of ebook files
|
||||
In the :guilabel:`Add/Save` section of the configuration dialog, you can specify a regular expression that |app| will use to try and guess metadata from the names of ebook files
|
||||
that you add to the library. The default regular expression is::
|
||||
|
||||
title - author
|
||||
@ -265,18 +265,13 @@ will be interpreted to have the title: Foundation and Earth and author: Isaac As
|
||||
.. tip::
|
||||
If the filename does not contain the hyphen, the regular expression will fail.
|
||||
|
||||
.. tip::
|
||||
If you want to only use metadata guessed from filenames and not metadata read from the file itself, you can tell |app| to do this, via the configuration dialog, accessed by the button to the right
|
||||
of the search box.
|
||||
|
||||
.. _book_details:
|
||||
|
||||
Book Details
|
||||
-------------
|
||||
.. image:: images/book_details.png
|
||||
|
||||
The Book Details display shows you extra information and the cover for the currently selected book. THe comments section is truncated if the comments are too long. To see the full comments as well as
|
||||
a larger image of the cover, click anywhere in the Book Details area.
|
||||
The Book Details display shows you extra information and the cover for the currently selected book.
|
||||
|
||||
.. _jobs:
|
||||
|
||||
|
@ -111,6 +111,8 @@ Pre/post processing of downloaded HTML
|
||||
|
||||
.. automember:: BasicNewsRecipe.remove_javascript
|
||||
|
||||
.. automethod:: BasicNewsRecipe.prepreprocess_html
|
||||
|
||||
.. automethod:: BasicNewsRecipe.preprocess_html
|
||||
|
||||
.. automethod:: BasicNewsRecipe.postprocess_html
|
||||
|
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
@ -161,6 +161,19 @@ def create_text_arc(text, font_size, font=None, bgcolor='white'):
|
||||
p.MagickTrimImage(canvas, 0)
|
||||
return canvas
|
||||
|
||||
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
|
||||
border_color='white'):
|
||||
with p.ImageMagick():
|
||||
img = load_image(path_to_image)
|
||||
lwidth = p.MagickGetImageWidth(img)
|
||||
lheight = p.MagickGetImageHeight(img)
|
||||
canvas = create_canvas(lwidth+left+right, lheight+top+bottom,
|
||||
border_color)
|
||||
compose_image(canvas, img, left, top)
|
||||
p.DestroyMagickWand(img)
|
||||
with open(path_to_image, 'wb') as f:
|
||||
p.MagickWriteImage(canvas, f)
|
||||
p.DestroyMagickWand(canvas)
|
||||
|
||||
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
||||
bgcolor='white', output_format='png'):
|
||||
@ -199,6 +212,23 @@ def create_cover_page(top_lines, logo_path, width=590, height=750,
|
||||
p.DestroyMagickWand(canvas)
|
||||
return ans
|
||||
|
||||
def save_cover_data_to(data, path, bgcolor='white'):
|
||||
'''
|
||||
Saves image in data to path, in the format specified by the path
|
||||
extension. Composes the image onto a blank cancas so as to
|
||||
properly convert transparent images.
|
||||
'''
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
with p.ImageMagick():
|
||||
img = load_image(path)
|
||||
canvas = create_canvas(p.MagickGetImageWidth(img),
|
||||
p.MagickGetImageHeight(img), bgcolor)
|
||||
compose_image(canvas, img, 0, 0)
|
||||
p.MagickWriteImage(canvas, path)
|
||||
p.DestroyMagickWand(img)
|
||||
p.DestroyMagickWand(canvas)
|
||||
|
||||
def test():
|
||||
import subprocess
|
||||
with TemporaryFile('.png') as f:
|
||||
|
@ -52,6 +52,12 @@ class SavedSearchQueries(object):
|
||||
self.queries.pop(self.force_unicode(name), False)
|
||||
prefs[self.opt_name] = self.queries
|
||||
|
||||
def rename(self, old_name, new_name):
|
||||
self.queries[self.force_unicode(new_name)] = \
|
||||
self.queries.get(self.force_unicode(old_name), None)
|
||||
self.queries.pop(self.force_unicode(old_name), False)
|
||||
prefs[self.opt_name] = self.queries
|
||||
|
||||
def names(self):
|
||||
return sorted(self.queries.keys(),
|
||||
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||
|
@ -11,7 +11,7 @@ from lxml import html
|
||||
|
||||
from calibre.web.feeds.feedparser import parse
|
||||
from calibre.utils.logging import default_log
|
||||
from calibre import entity_to_unicode
|
||||
from calibre import entity_to_unicode, strftime
|
||||
from calibre.utils.date import dt_factory, utcnow, local_tz
|
||||
|
||||
class Article(object):
|
||||
@ -53,12 +53,17 @@ class Article(object):
|
||||
|
||||
@dynamic_property
|
||||
def formatted_date(self):
|
||||
|
||||
def fget(self):
|
||||
if self._formatted_date is None:
|
||||
self._formatted_date = self.localtime.strftime(" [%a, %d %b %H:%M]")
|
||||
self._formatted_date = strftime(" [%a, %d %b %H:%M]",
|
||||
t=self.localtime.timetuple())
|
||||
return self._formatted_date
|
||||
|
||||
def fset(self, val):
|
||||
self._formatted_date = val
|
||||
if isinstance(val, unicode):
|
||||
self._formatted_date = val
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@dynamic_property
|
||||
|
@ -267,7 +267,7 @@ class BasicNewsRecipe(Recipe):
|
||||
}
|
||||
|
||||
a.article {
|
||||
font-weight: bold;
|
||||
font-weight: bold; text-align:left;
|
||||
}
|
||||
|
||||
a.feed {
|
||||
@ -403,10 +403,25 @@ class BasicNewsRecipe(Recipe):
|
||||
return url
|
||||
return article.get('link', None)
|
||||
|
||||
def prepreprocess_html(self, soup):
|
||||
'''
|
||||
This method is called with the source of each downloaded :term:`HTML` file, before
|
||||
any of the cleanup attributes like remove_tags, keep_only_tags are
|
||||
applied. Note that preprocess_regexps will have already been applied.
|
||||
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
|
||||
It should return `soup` after processing it.
|
||||
|
||||
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
|
||||
instance containing the downloaded :term:`HTML`.
|
||||
'''
|
||||
return soup
|
||||
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
'''
|
||||
This method is called with the source of each downloaded :term:`HTML` file, before
|
||||
it is parsed for links and images.
|
||||
it is parsed for links and images. It is called after the cleanup as
|
||||
specified by remove_tags etc.
|
||||
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
|
||||
It should return `soup` after processing it.
|
||||
|
||||
@ -523,8 +538,8 @@ class BasicNewsRecipe(Recipe):
|
||||
Intended to be used to get article metadata like author/summary/etc.
|
||||
from the parsed HTML (soup).
|
||||
:param article: A object of class :class:`calibre.web.feeds.Article`.
|
||||
If you chane the sumamry, remeber to also change the
|
||||
text_summary
|
||||
If you change the summary, remember to also change the
|
||||
text_summary
|
||||
:param soup: Parsed HTML belonging to this article
|
||||
:param first: True iff the parsed HTML is the first page of the article.
|
||||
'''
|
||||
@ -603,7 +618,7 @@ class BasicNewsRecipe(Recipe):
|
||||
|
||||
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
|
||||
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
|
||||
'preprocess_html', 'remove_tags_after',
|
||||
'prepreprocess_html', 'preprocess_html', 'remove_tags_after',
|
||||
'remove_tags_before', 'is_link_wanted'):
|
||||
setattr(self.web2disk_options, extra, getattr(self, extra))
|
||||
self.web2disk_options.postprocess_html = self._postprocess_html
|
||||
@ -758,15 +773,15 @@ class BasicNewsRecipe(Recipe):
|
||||
if self.touchscreen:
|
||||
touchscreen_css = u'''
|
||||
.summary_headline {
|
||||
font-size:large; font-weight:bold; margin-top:0px; margin-bottom:0px;
|
||||
font-weight:bold; text-align:left;
|
||||
}
|
||||
|
||||
.summary_byline {
|
||||
font-size:small; margin-top:0px; margin-bottom:0px;
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
.summary_text {
|
||||
margin-top:0px; margin-bottom:0px;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.feed {
|
||||
@ -782,12 +797,6 @@ class BasicNewsRecipe(Recipe):
|
||||
border-width:thin;
|
||||
}
|
||||
|
||||
table.toc {
|
||||
font-size:large;
|
||||
}
|
||||
td.article_count {
|
||||
text-align:right;
|
||||
}
|
||||
'''
|
||||
|
||||
templ = templates.TouchscreenFeedTemplate()
|
||||
@ -1120,8 +1129,11 @@ class BasicNewsRecipe(Recipe):
|
||||
mi.publisher = __appname__
|
||||
mi.author_sort = __appname__
|
||||
if self.output_profile.name == 'iPad':
|
||||
mi.authors = [strftime('%A, %d %B %Y')]
|
||||
mi.author_sort = strftime('%Y-%m-%d')
|
||||
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||
mi = MetaInformation(self.short_title(), [date_as_author])
|
||||
mi.publisher = __appname__
|
||||
sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
||||
mi.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d'))
|
||||
mi.publication_type = 'periodical:'+self.publication_type
|
||||
mi.timestamp = nowf()
|
||||
mi.comments = self.description
|
||||
@ -1245,7 +1257,6 @@ class BasicNewsRecipe(Recipe):
|
||||
with nested(open(opf_path, 'wb'), open(ncx_path, 'wb')) as (opf_file, ncx_file):
|
||||
opf.render(opf_file, ncx_file)
|
||||
|
||||
|
||||
def article_downloaded(self, request, result):
|
||||
index = os.path.join(os.path.dirname(result[0]), 'index.html')
|
||||
if index != result[0]:
|
||||
|
@ -241,7 +241,7 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
||||
results.add(urn)
|
||||
return results
|
||||
|
||||
def search(self, query, refinement):
|
||||
def search(self, query):
|
||||
try:
|
||||
results = self.parse(unicode(query))
|
||||
if not results:
|
||||
|
@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from lxml import html, etree
|
||||
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
|
||||
STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
|
||||
STRONG, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
|
||||
TABLE, TD, TR
|
||||
|
||||
from calibre import preferred_encoding, strftime, isbytestring
|
||||
@ -120,6 +120,7 @@ class TouchscreenNavBarTemplate(Template):
|
||||
href = '%s%s/%s/index.html'%(prefix, up, next)
|
||||
navbar.text = '| '
|
||||
navbar.append(A('Next', href=href))
|
||||
|
||||
href = '%s../index.html#article_%d'%(prefix, art)
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
navbar.append(A('Section Menu', href=href))
|
||||
@ -130,6 +131,7 @@ class TouchscreenNavBarTemplate(Template):
|
||||
href = '%s../article_%d/index.html'%(prefix, art-1)
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
navbar.append(A('Previous', href=href))
|
||||
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
if not bottom:
|
||||
navbar.append(HR())
|
||||
@ -165,8 +167,14 @@ class TouchscreenIndexTemplate(Template):
|
||||
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
|
||||
if isinstance(datefmt, unicode):
|
||||
datefmt = datefmt.encode(preferred_encoding)
|
||||
date = strftime(datefmt)
|
||||
masthead_img = IMG(src=masthead,alt="masthead")
|
||||
date = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||
masthead_p = etree.Element("p")
|
||||
masthead_p.set("style","text-align:center")
|
||||
masthead_img = etree.Element("img")
|
||||
masthead_img.set("src",masthead)
|
||||
masthead_img.set("alt","masthead")
|
||||
masthead_p.append(masthead_img)
|
||||
|
||||
head = HEAD(TITLE(title))
|
||||
if style:
|
||||
head.append(STYLE(style, type='text/css'))
|
||||
@ -177,15 +185,13 @@ class TouchscreenIndexTemplate(Template):
|
||||
for i, feed in enumerate(feeds):
|
||||
if feed:
|
||||
tr = TR()
|
||||
tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i)))
|
||||
tr.append(TD( CLASS('article_count'),'%d' % len(feed.articles)))
|
||||
tr.append(TD( CLASS('calibre_rescale_120'), A(feed.title, href='feed_%d/index.html'%i)))
|
||||
tr.append(TD( '%s' % len(feed.articles), style="text-align:right"))
|
||||
toc.append(tr)
|
||||
|
||||
div = DIV(
|
||||
PT(masthead_img,style='text-align:center'),
|
||||
masthead_p,
|
||||
PT(date, style='text-align:center'),
|
||||
toc,
|
||||
CLASS('calibre_rescale_100'))
|
||||
toc)
|
||||
self.root = HTML(head, BODY(div))
|
||||
|
||||
class FeedTemplate(Template):
|
||||
@ -271,12 +277,15 @@ class TouchscreenFeedTemplate(Template):
|
||||
continue
|
||||
tr = TR()
|
||||
td = TD(
|
||||
A(article.title, CLASS('article calibre_rescale_100',
|
||||
A(article.title, CLASS('summary_headline','calibre_rescale_120',
|
||||
href=article.url))
|
||||
)
|
||||
if article.author:
|
||||
td.append(DIV(article.author,
|
||||
CLASS('summary_byline', 'calibre_rescale_100')))
|
||||
if article.summary:
|
||||
td.append(DIV(cutoff(article.text_summary),
|
||||
CLASS('article_description', 'calibre_rescale_80')))
|
||||
CLASS('summary_text', 'calibre_rescale_100')))
|
||||
tr.append(td)
|
||||
toc.append(tr)
|
||||
div.append(toc)
|
||||
|
@ -136,6 +136,7 @@ class RecursiveFetcher(object):
|
||||
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
|
||||
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
|
||||
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
|
||||
self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup)
|
||||
self.postprocess_html_ext= getattr(options, 'postprocess_html', None)
|
||||
self._is_link_wanted = getattr(options, 'is_link_wanted',
|
||||
default_is_link_wanted)
|
||||
@ -153,6 +154,8 @@ class RecursiveFetcher(object):
|
||||
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
|
||||
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
|
||||
|
||||
soup = self.prepreprocess_html_ext(soup)
|
||||
|
||||
if self.keep_only_tags:
|
||||
body = Tag(soup, 'body')
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user