Pull from trunk

This commit is contained in:
Kovid Goyal 2010-06-06 23:14:25 -06:00
commit d4f47b8e75
71 changed files with 44529 additions and 27329 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

View 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')]

View File

@ -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

View File

@ -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')]

View 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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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, '', '', '')

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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 &amp;ungrouped items:</string>
</property>
<property name="buddy">
<cstring>opt_max_opds_ungrouped_items</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>

View File

@ -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):

View File

@ -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)

View File

@ -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>

View File

@ -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':

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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 &amp;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 &amp;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>

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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]

View File

@ -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()

View File

@ -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,

View File

@ -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():

View File

@ -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()

View File

@ -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

View File

@ -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
------------------------

View File

@ -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?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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()))

View File

@ -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

View File

@ -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]:

View File

@ -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:

View File

@ -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)

View File

@ -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: