diff --git a/Changelog.yaml b/Changelog.yaml index edeb60febe..6b41c53e30 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -4,6 +4,83 @@ # for important features/bug fixes. # Also, each release can have new and improved recipes. +- version: 0.7.18 + date: 2010-09-10 + + new features: + - title: "All new Preferences dialog, with nicer layout and the ability to restore settings to defaults" + type: major + + - title: "Add series info when available to generated cover. Also auto-resize the logo on the cover to ensure all text fits" + tickets: [6724] + + - title: "On device column: Now indicates when multiple copies of the same book are present on the device" + + - title: "Driver for the Gemei GM2000" + + - title: "Extract fb2 files from zip container when adding to calibre. Can be disables by disabling the Archive Extract file type plugin." + tickets: [6739] + + - title: "Switch to using raster icons for a small speedup in startup time" + + bug fixes: + - title: "On device column: Fix matching bug when multiple books in the library have the same title and author" + + - title: "Content server: Use /mobile version for Kindle browser" + + - title: "E-book viewer: When adding a bookmark, a default name is generated" + tickets: [6450] + + - title: "Hide visible menus before clearing toolbar." + tickets: [6706] + + - title: "Batch conversion: Don't overwrite the insert page break before setting" + tickets: [6729] + + - title: "Catalog generation: Fixed improper title display in catalog when title contains ':'. Added 'ondevice' field to CSV/XML catalog output (only with connected device|folder|iTunes). Added optional 'Series' section to generated catalogs with hyperlinks between books and series. Tweaks to catalog formatting." + + - title: "Fix regression when checking database integrity with custom columns introduced in 0.7.17" + + - title: "Sending email: Ignore geenric records when trying to resolve domain" + tickets: [6723] + + - title: "Fix a bug where the open state of the splitter was not being saved on shutdown if the splitter had been closed at startup and was opened by dragging the center line" + + - title: "MOBI Output: Fix bug generating index when chapter names contained non ASCII characters" + tickets: [6595] + + - title: "PDF Input: Fix handling of more non ascii characters" + + - title: "Content server: Triple AJAX timeout for main book list to 30 seconds" + + - title: "Use ImageMagick instead of Qt to generate thumbnails when sending covers to device. Should fix corrupted nook covers on some windows installs" + + - title: "FB2 Output: Improve creation of sections and fix a couple of bugs that could result in invalid output" + + new recipes: + - title: "Journal Gazette" + author: cynvision + + - title: "Milenio" + author: bmsleight + + - title: "Winnipeg Free Press" + author: buyo + + - title: "Buckmasters in the kitchen, The Walrus Magazine and Kansas City Star" + author: Tony Stegall + + - title: "Europa Sur" + author: "Darko Miletic" + + + improved recipes: + - El Pais + - La Jornada + - nrcnext + - WSJ (free) + + - version: 0.7.17 date: 2010-09-03 diff --git a/imgsrc/column.svg b/imgsrc/column.svg index 4d6f4b809e..ac2f026473 100644 --- a/imgsrc/column.svg +++ b/imgsrc/column.svg @@ -1,61 +1,211 @@ -Capitello modanatura modanature moulure mouluresbuildingArchitetto Francesco RollandinArchitetto Francesco RollandinArchitetto Francesco Rollandinimage/svg+xmlen - - + - - +Capitello modanatura modanature moulure mouluresbuildingArchitetto Francesco RollandinArchitetto Francesco RollandinArchitetto Francesco Rollandinimage/svg+xmlen + + - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + \ No newline at end of file diff --git a/imgsrc/drawer.svg b/imgsrc/drawer.svg index 679bca53b2..22e54a1537 100644 --- a/imgsrc/drawer.svg +++ b/imgsrc/drawer.svg @@ -1,2679 +1,2508 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index 4f9ca9ac41..4c5045d378 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -6,7 +6,7 @@ p.title { text-align:center; font-style:italic; font-size:xx-large; - border-bottom: solid black 4px; + border-bottom: solid black 2px; } p.author { @@ -17,6 +17,15 @@ p.author { font-size:large; } +p.author_index { + font-size:large; + font-weight:bold; + text-align:left; + margin-top:0px; + margin-bottom:-2px; + text-indent: 0em; + } + p.tags { margin-top:0em; margin-bottom:0em; @@ -47,19 +56,12 @@ p.letter_index { margin-bottom:0px; } -p.author_index { - font-size:large; - text-align:left; - margin-top:0px; - margin-bottom:0px; - text-indent: 0em; - } - p.series { - text-align: left; - margin-top:0px; + font-style:italic; + margin-top:2px; margin-bottom:0px; margin-left:2em; + text-align:left; text-indent:-2em; } @@ -87,11 +89,13 @@ p.date_read { text-indent:-6em; } -hr.series_divider { - width:50%; - margin-left:1em; - margin-top:0em; - margin-bottom:0em; +hr.description_divider { + width:90%; + margin-left:5%; + border-top: solid white 0px; + border-right: solid white 0px; + border-bottom: solid black 1px; + border-left: solid white 0px; } hr.annotations_divider { diff --git a/resources/images/column.png b/resources/images/column.png index ae26292b25..0a5f184601 100644 Binary files a/resources/images/column.png and b/resources/images/column.png differ diff --git a/resources/images/drawer.png b/resources/images/drawer.png index 356de02a88..dd33c2ad6e 100644 Binary files a/resources/images/drawer.png and b/resources/images/drawer.png differ diff --git a/resources/images/news/journalgazette.png b/resources/images/news/journalgazette.png new file mode 100644 index 0000000000..f9b3316cab Binary files /dev/null and b/resources/images/news/journalgazette.png differ diff --git a/resources/images/news/kstar.png b/resources/images/news/kstar.png new file mode 100644 index 0000000000..03eced9c44 Binary files /dev/null and b/resources/images/news/kstar.png differ diff --git a/resources/images/news/walrusmag.png b/resources/images/news/walrusmag.png new file mode 100644 index 0000000000..17030d94b8 Binary files /dev/null and b/resources/images/news/walrusmag.png differ diff --git a/resources/images/swap.png b/resources/images/swap.png index 60b8803d95..e5aeb60e22 100644 Binary files a/resources/images/swap.png and b/resources/images/swap.png differ diff --git a/resources/recipes/buckmasters.recipe b/resources/recipes/buckmasters.recipe new file mode 100644 index 0000000000..e3d852fa07 --- /dev/null +++ b/resources/recipes/buckmasters.recipe @@ -0,0 +1,42 @@ +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import Tag + +class AdvancedUserRecipe1282101454(BasicNewsRecipe): + title = 'BuckMasters In The Kitchen' + language = 'en' + __author__ = 'TonytheBookworm & Starson17' + description = 'Learn how to cook all those outdoor varments' + publisher = 'BuckMasters.com' + category = 'food,cooking,recipes' + oldest_article = 365 + max_articles_per_feed = 100 + conversion_options = {'linearize_tables' : True} + masthead_url = 'http://www.buckmasters.com/Portals/_default/Skins/BM_10/images/header_bg.jpg' + keep_only_tags = [ + dict(name='table', attrs={'class':['containermaster_black']}) + ] + remove_tags_after = [dict(name='div', attrs={'align':['left']})] + feeds = [ + ('Recipes', 'http://www.buckmasters.com/DesktopModules/DnnForge%20-%20NewsArticles/RSS.aspx?TabID=292&ModuleID=658&MaxCount=25'), + ] + + def preprocess_html(self, soup): + item = soup.find('a', attrs={'class':['MenuTopSelected']}) + if item: + item.parent.extract() + for img_tag in soup.findAll('img'): + parent_tag = img_tag.parent + if parent_tag.name == 'a': + new_tag = Tag(soup,'p') + new_tag.insert(0,img_tag) + parent_tag.replaceWith(new_tag) + elif parent_tag.name == 'p': + if not self.tag_to_string(parent_tag) == '': + new_div = Tag(soup,'div') + new_tag = Tag(soup,'p') + new_tag.insert(0,img_tag) + parent_tag.replaceWith(new_div) + new_div.insert(0,new_tag) + new_div.insert(1,parent_tag) + return soup + diff --git a/resources/recipes/el_pais.recipe b/resources/recipes/el_pais.recipe index c953a4dd95..1e2164b2af 100644 --- a/resources/recipes/el_pais.recipe +++ b/resources/recipes/el_pais.recipe @@ -1,8 +1,8 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__author__ = 'Lorenzo Vigentini, based on earlier version by Kovid Goyal' +__author__ = 'Jordi Balcells, based on an earlier version by Lorenzo Vigentini & Kovid Goyal' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' -description = 'Main daily newspaper from Spain - v1.02 (10, January 2010)' +description = 'Main daily newspaper from Spain - v1.03 (03, September 2010)' __docformat__ = 'restructuredtext en' ''' @@ -12,12 +12,12 @@ elpais.es from calibre.web.feeds.news import BasicNewsRecipe class ElPais(BasicNewsRecipe): - __author__ = 'Kovid Goyal & Lorenzo Vigentini' + __author__ = 'Kovid Goyal & Lorenzo Vigentini & Jordi Balcells' description = 'Main daily newspaper from Spain' cover_url = 'http://www.elpais.com/im/tit_logo_global.gif' title = u'El Pais' - publisher = 'Ediciones El Pais SL' + publisher = u'Ediciones El Pa\xeds SL' category = 'News, politics, culture, economy, general interest' language = 'es' @@ -32,7 +32,8 @@ class ElPais(BasicNewsRecipe): remove_javascript = True no_stylesheets = True - keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia','cabecera_noticia_reportaje','contenido_noticia','caja_despiece','presentacion']})] + keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia','cabecera_noticia_reportaje','cabecera_noticia_opinion','contenido_noticia','caja_despiece','presentacion']})] + extra_css = ''' p{style:normal size:12 serif} @@ -40,25 +41,29 @@ class ElPais(BasicNewsRecipe): remove_tags = [ dict(name='div', attrs={'class':['zona_superior','pie_enlaces_inferiores','contorno_f','ampliar']}), - dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos','info_complementa','info_relacionada']}), - dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas']}) + dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos','info_complementa','info_relacionada','buscador_m','nav_ant_sig']}), + dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas','calendar']}), + dict(name='p', attrs={'class':'nav_meses'}), + dict(attrs={'class':['enlaces_m','miniaturas_m']}) ] feeds = [ (u'Titulares de portada', u'http://www.elpais.com/rss/feed.html?feedId=1022'), (u'Internacional', u'http://www.elpais.com/rss/feed.html?feedId=1001'), - (u'Espana', u'http://www.elpais.com/rss/feed.html?feedId=1002'), + (u'Espa\xf1a', u'http://www.elpais.com/rss/feed.html?feedId=1002'), (u'Deportes', u'http://www.elpais.com/rss/feed.html?feedId=1007'), - (u'Economia', u'http://www.elpais.com/rss/feed.html?feedId=1006'), - (u'Politica', u'http://www.elpais.com/rss/feed.html?feedId=17073'), - (u'Tecnologia', u'http://www.elpais.com/rss/feed.html?feedId=1005'), + (u'Econom\xeda', u'http://www.elpais.com/rss/feed.html?feedId=1006'), + (u'Pol\xedtica', u'http://www.elpais.com/rss/feed.html?feedId=17073'), + (u'Tecnolog\xeda', u'http://www.elpais.com/rss/feed.html?feedId=1005'), (u'Cultura', u'http://www.elpais.com/rss/feed.html?feedId=1008'), (u'Gente', u'http://www.elpais.com/rss/feed.html?feedId=1009'), (u'Sociedad', u'http://www.elpais.com/rss/feed.html?feedId=1004'), - (u'Opinion', u'http://www.elpais.com/rss/feed.html?feedId=1003'), + (u'Opini\xf3n', u'http://www.elpais.com/rss/feed.html?feedId=1003'), (u'Ciencia', u'http://www.elpais.com/rss/feed.html?feedId=17068'), (u'Justicia y leyes', u'http://www.elpais.com/rss/feed.html?feedId=17069'), - ] + (u'Medio ambiente', u'http://www.elpais.com/rss/feed.html?feedId=17071'), + (u'Vi\xf1etas', u'http://www.elpais.com/rss/feed.html?feedId=17058') + ] def print_version(self, url): url = url+'?print=1' diff --git a/resources/recipes/journalgazette.recipe b/resources/recipes/journalgazette.recipe new file mode 100644 index 0000000000..406917f5ce --- /dev/null +++ b/resources/recipes/journalgazette.recipe @@ -0,0 +1,64 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__author__ = 'somedayson & TonytheBookworm, revised by Cynthia Clavey' +__copyright__ = '2010, Cynthia Clavey cynvision@yahoo.com' +__version__ = '1.02' +__date__ = '05, september 2010' +__docformat__ = 'restructuredtext en' +from calibre.web.feeds.recipes import BasicNewsRecipe + +class AdvancedUserRecipe1283666183(BasicNewsRecipe): + title = u'Journal Gazette Ft. Wayne IN' + __author__ = 'cynvision' + oldest_article = 1 + max_articles_per_feed = 8 + no_stylesheets = True + remove_javascript = True + use_embedded_content = False + keep_only_tags = [dict(name='div', attrs={'id':'mainContent'})] + extra_css = '#copyinfo { font-size: 6 ;} \n #photocredit { font-size: 6 ;} \n .pubinfo { font-size: 6 ;}' + masthead_url = 'http://www.journalgazette.net/img/icons/jgmini.gif' +# cover_url = 'http://www.journalgazette.net/img/icons/jgmini.gif' + encoding = 'cp1252' + + feeds = [(u'Opinion', u'http://journalgazette.net/apps/pbcs.dll/section?Category=EDIT&template=blogrss&mime=xml'), + (u'Local News',u'http://journalgazette.net/apps/pbcs.dll/section?Category=LOCAL&template=blogrss&mime=xml') , + (u'Sports',u'http://journalgazette.net/apps/pbcs.dll/section?Category=SPORTS&template=blogrss&mime=xml' ), + (u'Features',u'http://journalgazette.net/apps/pbcs.dll/section?Category=FEAT&template=blogrss&mime=xml'), + (u'Business',u'http://journalgazette.net/apps/pbcs.dll/section?Category=BIZ&template=blogrss&mime=xml'), + (u'Ice Chips',u'http://journalgazette.net/apps/pbcs.dll/section?Category=BLOGS11&template=blogrss&mime=xml '), + (u'Entertainment',u'http://journalgazette.net/apps/pbcs.dll/section?Category=ENT&template=blogrss&mime=xml'), + (u'Food',u'http://journalgazette.net/apps/pbcs.dll/section?Category=FOOD&template=blogrss&mime=xml') + ] + + + + + def print_version(self, url): + split1 = url.split("/") + #print 'THE SPLIT IS: ', split1 + #url1 = split1[0] + #url2 = split1[1] + url3 = split1[2] + #url4 = split1[3] + url5 = split1[4] + url6 = split1[5] + url7 = split1[6] + #url8 = split1[7] + + #need to convert to print_version + #originalversion is : http://www.journalgazette.net/article/20100905/EDIT10/309059959/1021/EDIT + #printversion should be: http://www.journalgazette.net/apps/pbcs.dll/article?AID=/20100905/EDIT10/309059959/-1/EDIT01&template=printart + #results of the split + #THE SPLIT IS: [u'http:', u'', u'www.journalgazette.net', u'article', u'20100905', u'EDIT10', u'309059959', u'1021', u'EDIT'] + + + + print_url = 'http://' + url3 + '/apps/pbcs.dll/article?AID=/' + url5 + '/' + url6 + '/' + url7 + '/-1/EDIT01&template=printart' + #print 'THIS URL WILL PRINT: ', print_url # this is a test string to see what the url is it will return + return print_url + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + return soup diff --git a/resources/recipes/kstar.recipe b/resources/recipes/kstar.recipe new file mode 100644 index 0000000000..fe44bfd679 --- /dev/null +++ b/resources/recipes/kstar.recipe @@ -0,0 +1,58 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1282101454(BasicNewsRecipe): + title = 'Kansascity Star' + language = 'en' + __author__ = 'TonytheBookworm' + description = 'www.kansascity.com feed' + publisher = 'Tony Stegall' + category = 'news, politics, USA, kansascity' + oldest_article = 7 + max_articles_per_feed = 100 + no_stylesheets = True + + masthead_url = 'http://media.kansascity.com/images/site_logo_340x60.gif' + keep_only_tags = [ + dict(attrs={'id':['storyTitle','sub_headline','byLine']}) + ,dict(name='div', attrs={'id':['storyDate-Links','storyBody']}) + + ] + feeds = [ + ('Kansas News', 'http://www.kansascity.com/105/index.xml'), + + ] + + + + + def print_version(self, url): + split1 = url.split("/") + #url1 = split1[0] + #url2 = split1[1] + url3 = split1[2] + url4 = split1[3] + url5 = split1[4] + url6 = split1[5] + url7 = split1[6] + url8 = split1[7] + + + #example of link to convert + #Original link: http://www.kansascity.com/2010/09/04/2199362/lees-summit-school-appears-to.html + #print version: http://www.kansascity.com/2010/09/04/v-print/2199362/lees-summit-school-appears-to.html + + print_url = 'http://' + url3 + '/' + url4 + '/' + url5 + '/' + url6 + '/v-print/' + url7 + '/' + url8 + + return print_url + + + + + + + + + + + + diff --git a/resources/recipes/la_jornada.recipe b/resources/recipes/la_jornada.recipe index 2e1a3bb50d..afeae08201 100644 --- a/resources/recipes/la_jornada.recipe +++ b/resources/recipes/la_jornada.recipe @@ -1,15 +1,16 @@ __license__ = 'GPL v3' -__copyright__ = '2010, Darko Miletic ' +__copyright__ = '2010, Darko Miletic , Rogelio Domínguez ' ''' www.jornada.unam.mx ''' +import re from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe class LaJornada_mx(BasicNewsRecipe): title = 'La Jornada (Mexico)' - __author__ = 'Darko Miletic' + __author__ = 'Darko Miletic/Rogelio Domínguez' description = 'Noticias del diario mexicano La Jornada' publisher = 'DEMOS, Desarrollo de Medios, S.A. de C.V.' category = 'news, Mexico' @@ -20,12 +21,26 @@ class LaJornada_mx(BasicNewsRecipe): use_embedded_content = False language = 'es' remove_empty_feeds = True - cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/planitas/portadita.jpg") + cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/portada.pdf") masthead_url = 'http://www.jornada.unam.mx/v7.0/imagenes/la-jornada-trans.png' + publication_type = 'newspaper' extra_css = """ body{font-family: "Times New Roman",serif } .cabeza{font-size: xx-large; font-weight: bold } - .credito-articulo{font-size: 1.3em} + .documentFirstHeading{font-size: xx-large; font-weight: bold } + .credito-articulo{font-variant: small-caps; font-weight: bold } + .foto{text-align: center} + .pie-foto{font-size: 0.9em} + .credito{font-weight: bold; margin-left: 1em} + .credito-autor{font-variant: small-caps; font-weight: bold } + .credito-titulo{text-align: right} + .hemero{text-align: right; font-size: 0.9em; margin-bottom: 0.5em } + .loc{font-weight: bold} + .carton{text-align: center} + .credit{font-weight: bold} + .text{margin-top: 1.4em} + p.inicial{display: inline; font-size: xx-large; font-weight: bold} + p.s-s{display: inline; text-indent: 0} """ conversion_options = { @@ -35,15 +50,21 @@ class LaJornada_mx(BasicNewsRecipe): , 'language' : language } + preprocess_regexps = [ + (re.compile( r'
(.*)

' + ,re.DOTALL|re.IGNORECASE) + ,lambda match: '

' + match.group(1) + '

') + ] + keep_only_tags = [ - dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','text']}) + dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','credito-articulo','text','carton']}) ,dict(name='div', attrs={'id':'renderComments'}) ] - remove_tags = [dict(name='div', attrs={'class':'buttonbar'})] + remove_tags = [dict(name='div', attrs={'class':['buttonbar','comment-cont']})] feeds = [ - (u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' ) - ,(u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' ) + (u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' ) + ,(u'Cartones' , u'http://www.jornada.unam.mx/rss/cartones.xml' ) ,(u'Politica' , u'http://www.jornada.unam.mx/rss/politica.xml' ) ,(u'Economia' , u'http://www.jornada.unam.mx/rss/economia.xml' ) ,(u'Mundo' , u'http://www.jornada.unam.mx/rss/mundo.xml' ) @@ -55,6 +76,7 @@ class LaJornada_mx(BasicNewsRecipe): ,(u'Gastronomia' , u'http://www.jornada.unam.mx/rss/gastronomia.xml' ) ,(u'Espectaculos' , u'http://www.jornada.unam.mx/rss/espectaculos.xml' ) ,(u'Deportes' , u'http://www.jornada.unam.mx/rss/deportes.xml' ) + ,(u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' ) ] def preprocess_html(self, soup): @@ -62,3 +84,7 @@ class LaJornada_mx(BasicNewsRecipe): del item['style'] return soup + def get_article_url(self, article): + rurl = article.get('link', None) + return rurl.rpartition('&partner=')[0] + diff --git a/resources/recipes/ncrnext.recipe b/resources/recipes/ncrnext.recipe index e03da301fa..6585cc9665 100644 --- a/resources/recipes/ncrnext.recipe +++ b/resources/recipes/ncrnext.recipe @@ -22,10 +22,19 @@ class NrcNextRecipe(BasicNewsRecipe): remove_tags = [] remove_tags.append(dict(name = 'div', attrs = {'class' : 'meta'})) + remove_tags.append(dict(name = 'p', attrs = {'class' : 'meta'})) remove_tags.append(dict(name = 'div', attrs = {'class' : 'datumlabel'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'sharing-is-caring'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'navigation'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'reageer'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'comment odd alt thread-odd thread-alt depth-1 reactie '})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'comment even thread-even depth-1 reactie '})) remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats single'})) remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats onderwerpen'})) remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats rubrieken'})) + remove_tags.append(dict(name = 'h3', attrs = {'class' : 'reacties'})) + + extra_css = ''' body {font-family: verdana, arial, helvetica, geneva, sans-serif; text-align: left;} @@ -41,20 +50,18 @@ class NrcNextRecipe(BasicNewsRecipe): feeds[u'koken'] = u'http://www.nrcnext.nl/koken/' feeds[u'geld & werk'] = u'http://www.nrcnext.nl/geld-en-werk/' feeds[u'vandaag'] = u'http://www.nrcnext.nl' - feeds[u'city life in afrika'] = u'http://www.nrcnext.nl/city-life-in-afrika/' + # feeds[u'city life in afrika'] = u'http://www.nrcnext.nl/city-life-in-afrika/' answer = [] articles = {} indices = [] for index, feed in feeds.items() : soup = self.index_to_soup(feed) - - for post in soup.findAll(True, attrs={'class' : 'post'}) : + for post in soup.findAll(True, attrs={'class' : 'post '}) : # Find the links to the actual articles and rember the location they're pointing to and the title a = post.find('a', attrs={'rel' : 'bookmark'}) href = a['href'] title = self.tag_to_string(a) - if index == 'columnisten' : # In this feed/page articles can be written by more than one author. # It is nice to see their names in the titles. @@ -74,7 +81,8 @@ class NrcNextRecipe(BasicNewsRecipe): indices.append(index) # Now, sort the temporary list of feeds in the order they appear on the website - indices = self.sort_index_by(indices, {u'columnisten' : 1, u'koken' : 3, u'geld & werk' : 2, u'vandaag' : 0, u'city life in afrika' : 4}) + # indices = self.sort_index_by(indices, {u'columnisten' : 1, u'koken' : 3, u'geld & werk' : 2, u'vandaag' : 0, u'city life in afrika' : 4}) + indices = self.sort_index_by(indices, {u'columnisten' : 1, u'koken' : 3, u'geld & werk' : 2, u'vandaag' : 0}) # Apply this sort order to the actual list of feeds and articles answer = [(key, articles[key]) for key in indices if articles.has_key(key)] diff --git a/resources/recipes/nejm.recipe b/resources/recipes/nejm.recipe index 415377b5c2..c860413926 100644 --- a/resources/recipes/nejm.recipe +++ b/resources/recipes/nejm.recipe @@ -26,13 +26,13 @@ class NYTimes(BasicNewsRecipe): #TO LOGIN def get_browser(self): br = BasicNewsRecipe.get_browser() - br.open('http://content.nejm.org/cgi/login?uri=/') - br.select_form(nr=0) - br['username'] = self.username - br['code'] = self.password + br.open('http://www.nejm.org/action/showLogin?uri=http://www.nejm.org/') + br.select_form(name='frmLogin') + br['login'] = self.username + br['password'] = self.password response = br.submit() raw = response.read() - if 'Welcome' not in raw: + if '>Sign Out<' not in raw: raise Exception('Login failed. Check your username and password') return br diff --git a/resources/recipes/taz.recipe b/resources/recipes/taz.recipe index 530fa7d6b7..45b414f3cd 100644 --- a/resources/recipes/taz.recipe +++ b/resources/recipes/taz.recipe @@ -13,50 +13,51 @@ from calibre.web.feeds.news import BasicNewsRecipe class TazDigiabo(BasicNewsRecipe): - title = u'Taz Digiabo' - description = u'Das EPUB DigiAbo der Taz' - language = 'de' - lang = 'de-DE' + title = u'Taz Digiabo' + description = u'Das EPUB DigiAbo der Taz' + language = 'de' + lang = 'de-DE' - __author__ = 'Lars Jacob' - needs_subscription = True + __author__ = 'Lars Jacob' + needs_subscription = True - conversion_options = { - 'no_default_epub_cover' : True - } + conversion_options = { + 'no_default_epub_cover' : True + } - def build_index(self): - if self.username is not None and self.password is not None: - domain = "http://www.taz.de" + def build_index(self): + if self.username is not None and self.password is not None: + domain = "http://www.taz.de" - url = domain + "/epub/" + url = domain + "/epub/" - auth_handler = urllib2.HTTPBasicAuthHandler() - auth_handler.add_password(realm='TAZ-ABO', - uri=url, - user=self.username, - passwd=self.password) - opener = urllib2.build_opener(auth_handler) - urllib2.install_opener(opener) + auth_handler = urllib2.HTTPBasicAuthHandler() + auth_handler.add_password(realm='TAZ-ABO', + uri=url, + user=self.username, + passwd=self.password) + opener = urllib2.build_opener(auth_handler) + urllib2.install_opener(opener) - try: - f = urllib2.urlopen(url) - except urllib2.HTTPError: - self.report_progress(0,_('Can\'t login to download issue')) - return + try: + f = urllib2.urlopen(url) + except urllib2.HTTPError: + self.report_progress(0,_('Can\'t login to download issue')) + raise ValueError('Failed to login, check your username and' + ' password') - tmp = tempfile.TemporaryFile() - self.report_progress(0,_('downloading epub')) - tmp.write(f.read()) + tmp = tempfile.TemporaryFile() + self.report_progress(0,_('downloading epub')) + tmp.write(f.read()) - zfile = zipfile.ZipFile(tmp, 'r') - self.report_progress(0,_('extracting epub')) + zfile = zipfile.ZipFile(tmp, 'r') + self.report_progress(0,_('extracting epub')) - zfile.extractall(self.output_dir) + zfile.extractall(self.output_dir) - tmp.close() - index = os.path.join(self.output_dir, 'content.opf') + tmp.close() + index = os.path.join(self.output_dir, 'content.opf') - self.report_progress(1,_('epub downloaded and extracted')) + self.report_progress(1,_('epub downloaded and extracted')) - return index + return index diff --git a/resources/recipes/walrusmag.recipe b/resources/recipes/walrusmag.recipe new file mode 100644 index 0000000000..5c10100fe4 --- /dev/null +++ b/resources/recipes/walrusmag.recipe @@ -0,0 +1,46 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1282101454(BasicNewsRecipe): + title = 'The Walrus Mag' + language = 'en' + __author__ = 'TonytheBookworm' + description = 'national general interest magazine about Canada and its place in the world' + publisher = 'Tony Stegall' + category = 'Canada, news' + oldest_article = 365 + max_articles_per_feed = 100 + + masthead_url = 'http://www.walrusmagazine.com/images/wordmark.png' + keep_only_tags = [ + dict(name='h1'), + dict(name='div', attrs={'id':['prbody']}) + # ,dict(attrs={'id':['cxArticleText','cxArticleBodyText']}) + ] + feeds = [ + ('Walrus Magazine', 'http://feeds.feedburner.com/WalrusFeatureArticles?format=xml'), + + ] + + + + + def print_version(self, url): + split1 = url.split("/articles/") + #print 'THE SPLIT IS: ', split1 + url1 = split1[0] + #print 'url1 is: ',url1 + url2 = split1[1] + #print 'url2 is: ',url2 + + + #need to convert to print_version + #originalversion is : http://www.walrusmagazine.com/articles/2010.09-frontier-no-one-can-hear-you-scream/ + #printversion should be: http://www.walrusmagazine.com/print/2010.09-frontier-no-one-can-hear-you-scream/ + + + + + print_url = url1 + '/print/' + url2 + #print 'THIS URL WILL PRINT: ', print_url # this is a test string to see what the url is it will return + return print_url + diff --git a/resources/recipes/wsj_free.recipe b/resources/recipes/wsj_free.recipe new file mode 100644 index 0000000000..7f3664f1c4 --- /dev/null +++ b/resources/recipes/wsj_free.recipe @@ -0,0 +1,153 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +from calibre.web.feeds.news import BasicNewsRecipe +import copy + +class WallStreetJournal(BasicNewsRecipe): + + title = 'Wall Street Journal (free)' + __author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17' + description = 'News and current affairs' + language = 'en' + cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG' + max_articles_per_feed = 1000 + timefmt = ' [%a, %b %d, %Y]' + no_stylesheets = True + + extra_css = '''h1{color:#093D72 ; font-size:large ; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; } + h2{color:#474537; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;} + .subhead{color:gray; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;} + .insettipUnit {color:#666666; font-family:Arial,Sans-serif;font-size:xx-small } + .targetCaption{ font-size:x-small; color:#333333; font-family:Arial,Helvetica,sans-serif} + .article{font-family :Arial,Helvetica,sans-serif; font-size:x-small} + .tagline {color:#333333; font-size:xx-small} + .dateStamp {color:#666666; font-family:Arial,Helvetica,sans-serif} + h3{color:blue ;font-family:Arial,Helvetica,sans-serif; font-size:xx-small} + .byline{color:blue;font-family:Arial,Helvetica,sans-serif; font-size:xx-small} + h6{color:#333333; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small;font-style:italic; } + .paperLocation{color:#666666; font-size:xx-small}''' + + remove_tags_before = dict(name='h1') + remove_tags = [ + dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]), + {'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]}, + dict(name='div', attrs={'data-flash-settings':True}), + {'class':['insetContent embedType-interactive insetCol3wide','insetCol6wide','insettipUnit']}, + dict(rel='shortcut icon'), + ] + remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},] + + def postprocess_html(self, soup, first): + for tag in soup.findAll(name=['table', 'tr', 'td']): + tag.name = 'div' + + for tag in soup.findAll('div', dict(id=["articleThumbnail_1", "articleThumbnail_2", "articleThumbnail_3", "articleThumbnail_4", "articleThumbnail_5", "articleThumbnail_6", "articleThumbnail_7"])): + tag.extract() + + return soup + + def wsj_get_index(self): + return self.index_to_soup('http://online.wsj.com/itp') + + def wsj_add_feed(self,feeds,title,url): + self.log('Found section:', title) + if url.endswith('whatsnews'): + articles = self.wsj_find_wn_articles(url) + else: + articles = self.wsj_find_articles(url) + if articles: + feeds.append((title, articles)) + return feeds + + def parse_index(self): + soup = self.wsj_get_index() + + date = soup.find('span', attrs={'class':'date-date'}) + if date is not None: + self.timefmt = ' [%s]'%self.tag_to_string(date) + + feeds = [] + div = soup.find('div', attrs={'class':'itpHeader'}) + div = div.find('ul', attrs={'class':'tab'}) + for a in div.findAll('a', href=lambda x: x and '/itp/' in x): + pageone = a['href'].endswith('pageone') + if pageone: + title = 'Front Section' + url = 'http://online.wsj.com' + a['href'] + feeds = self.wsj_add_feed(feeds,title,url) + title = 'What''s News' + url = url.replace('pageone','whatsnews') + feeds = self.wsj_add_feed(feeds,title,url) + else: + title = self.tag_to_string(a) + url = 'http://online.wsj.com' + a['href'] + feeds = self.wsj_add_feed(feeds,title,url) + return feeds + + def wsj_find_wn_articles(self, url): + soup = self.index_to_soup(url) + articles = [] + + whats_news = soup.find('div', attrs={'class':lambda x: x and 'whatsNews-simple' in x}) + if whats_news is not None: + for a in whats_news.findAll('a', href=lambda x: x and '/article/' in x): + container = a.findParent(['p']) + meta = a.find(attrs={'class':'meta_sectionName'}) + if meta is not None: + meta.extract() + title = self.tag_to_string(a).strip() + url = a['href'] + desc = '' + if container is not None: + desc = self.tag_to_string(container) + + articles.append({'title':title, 'url':url, + 'description':desc, 'date':''}) + + self.log('\tFound WN article:', title) + + return articles + + def wsj_find_articles(self, url): + soup = self.index_to_soup(url) + + whats_news = soup.find('div', attrs={'class':lambda x: x and 'whatsNews-simple' in x}) + if whats_news is not None: + whats_news.extract() + + articles = [] + + flavorarea = soup.find('div', attrs={'class':lambda x: x and 'ahed' in x}) + if flavorarea is not None: + flavorstory = flavorarea.find('a', href=lambda x: x and x.startswith('/article')) + if flavorstory is not None: + flavorstory['class'] = 'mjLinkItem' + metapage = soup.find('span', attrs={'class':lambda x: x and 'meta_sectionName' in x}) + if metapage is not None: + flavorstory.append( copy.copy(metapage) ) #metapage should always be A1 because that should be first on the page + + for a in soup.findAll('a', attrs={'class':'mjLinkItem'}, href=True): + container = a.findParent(['li', 'div']) + meta = a.find(attrs={'class':'meta_sectionName'}) + if meta is not None: + meta.extract() + title = self.tag_to_string(a).strip() + ' [%s]'%self.tag_to_string(meta) + url = 'http://online.wsj.com'+a['href'] + desc = '' + p = container.find('p') + if p is not None: + desc = self.tag_to_string(p) + + articles.append({'title':title, 'url':url, + 'description':desc, 'date':''}) + + self.log('\tFound article:', title) + + return articles + + def cleanup(self): + self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com') + diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 3e583e6f24..c72265b1fa 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = 'calibre' -__version__ = '0.7.17' +__version__ = '0.7.18' __author__ = "Kovid Goyal " import re diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 91052bbc2b..d89a7c45a3 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -294,7 +294,7 @@ class CatalogPlugin(Plugin): # {{{ # Return a list of requested fields, with opts.sort_by first all_fields = set( ['author_sort','authors','comments','cover','formats', - 'id','isbn','pubdate','publisher','rating', + 'id','isbn','ondevice','pubdate','publisher','rating', 'series_index','series','size','tags','timestamp', 'title','uuid']) @@ -306,6 +306,9 @@ class CatalogPlugin(Plugin): # {{{ else: fields = list(all_fields) + if not opts.connected_device['is_device_connected'] and 'ondevice' in fields: + fields.pop(int(fields.index('ondevice'))) + fields.sort() if opts.sort_by and opts.sort_by in fields: fields.insert(0,fields.pop(int(fields.index(opts.sort_by)))) @@ -371,6 +374,13 @@ class InterfaceActionBase(Plugin): # {{{ class PreferencesPlugin(Plugin): # {{{ + ''' + A plugin representing a widget displayed in the Preferences dialog. + + This plugin has only one important method :meth:`create_widget`. The + various fields of the plugin control how it is categorized in the UI. + ''' + supported_platforms = ['windows', 'osx', 'linux'] author = 'Kovid Goyal' type = _('Preferences') @@ -393,13 +403,21 @@ class PreferencesPlugin(Plugin): # {{{ #: The category name displayed to the user for this plugin gui_category = None + #: The name displayed to the user for this plugin gui_name = None + #: The icon for this plugin, should be an absolute path + icon = None + + #: The description used for tooltips and the like + description = None + def create_widget(self, parent=None): ''' Create and return the actual Qt widget used for setting this group of - preferences. The widget must implement the ConfigWidgetInterface. + preferences. The widget must implement the + :class:`calibre.gui2.preferences.ConfigWidgetInterface`. The default implementation uses :attr:`config_widget` to instantiate the widget. diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 732c5dbc76..4c87236e71 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -461,7 +461,7 @@ from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK from calibre.devices.edge.driver import EDGE from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS from calibre.devices.sne.driver import SNE -from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN +from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, GEMEI from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.kobo.driver import KOBO @@ -570,6 +570,7 @@ plugins += [ KOGAN, PDNOVEL, SPECTRA, + GEMEI, ITUNES, ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ @@ -678,78 +679,181 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, class LookAndFeel(PreferencesPlugin): name = 'Look & Feel' + icon = I('lookfeel.png') gui_name = _('Look and Feel') category = 'Interface' gui_category = _('Interface') category_order = 1 name_order = 1 config_widget = 'calibre.gui2.preferences.look_feel' + description = _('Adjust the look and feel of the calibre interface' + ' to suit your tastes') class Behavior(PreferencesPlugin): name = 'Behavior' + icon = I('config.png') gui_name = _('Behavior') category = 'Interface' gui_category = _('Interface') category_order = 1 name_order = 2 config_widget = 'calibre.gui2.preferences.behavior' + description = _('Change the way calibre behaves') class Columns(PreferencesPlugin): name = 'Custom Columns' + icon = I('column.png') gui_name = _('Add your own columns') category = 'Interface' gui_category = _('Interface') category_order = 1 name_order = 3 config_widget = 'calibre.gui2.preferences.columns' + description = _('Add/remove your own columns to the calibre book list') class Toolbar(PreferencesPlugin): name = 'Toolbar' + icon = I('wizard.png') gui_name = _('Customize the toolbar') category = 'Interface' gui_category = _('Interface') category_order = 1 name_order = 4 config_widget = 'calibre.gui2.preferences.toolbar' + description = _('Customize the toolbars and context menus, changing which' + ' actions are available in each') class InputOptions(PreferencesPlugin): name = 'Input Options' + icon = I('arrow-down.png') gui_name = _('Input Options') category = 'Conversion' gui_category = _('Conversion') category_order = 2 name_order = 1 config_widget = 'calibre.gui2.preferences.conversion:InputOptions' + description = _('Set conversion options specific to each input format') class CommonOptions(PreferencesPlugin): name = 'Common Options' + icon = I('convert.png') gui_name = _('Common Options') category = 'Conversion' gui_category = _('Conversion') category_order = 2 name_order = 2 config_widget = 'calibre.gui2.preferences.conversion:CommonOptions' + description = _('Set conversion options common to all formats') class OutputOptions(PreferencesPlugin): name = 'Output Options' + icon = I('arrow-up.png') gui_name = _('Output Options') category = 'Conversion' gui_category = _('Conversion') category_order = 2 name_order = 3 config_widget = 'calibre.gui2.preferences.conversion:OutputOptions' + description = _('Set conversion options specific to each output format') class Adding(PreferencesPlugin): name = 'Adding' + icon = I('add_book.png') gui_name = _('Adding books') category = 'Import/Export' gui_category = _('Import/Export') category_order = 3 name_order = 1 config_widget = 'calibre.gui2.preferences.adding' + description = _('Control how calibre reads metadata from files when ' + 'adding books') + +class Saving(PreferencesPlugin): + name = 'Saving' + icon = I('save.png') + gui_name = _('Saving books to disk') + category = 'Import/Export' + gui_category = _('Import/Export') + category_order = 3 + name_order = 2 + config_widget = 'calibre.gui2.preferences.saving' + description = _('Control how calibre exports files from its database ' + 'to disk when using Save to disk') + +class Sending(PreferencesPlugin): + name = 'Sending' + icon = I('sync.png') + gui_name = _('Sending books to devices') + category = 'Import/Export' + gui_category = _('Import/Export') + category_order = 3 + name_order = 3 + config_widget = 'calibre.gui2.preferences.sending' + description = _('Control how calibre transfers files to your ' + 'ebook reader') + +class Email(PreferencesPlugin): + name = 'Email' + icon = I('mail.png') + gui_name = _('Sharing books by email') + category = 'Sharing' + gui_category = _('Sharing') + category_order = 4 + name_order = 1 + config_widget = 'calibre.gui2.preferences.emailp' + description = _('Setup sharing of books via email. Can be used ' + 'for automatic sending of downloaded news to your devices') + +class Server(PreferencesPlugin): + name = 'Server' + icon = I('network-server.png') + gui_name = _('Sharing over the net') + category = 'Sharing' + gui_category = _('Sharing') + category_order = 4 + name_order = 2 + config_widget = 'calibre.gui2.preferences.server' + description = _('Setup the calibre Content Server which will ' + 'give you access to your calibre library from anywhere, ' + 'on any device, over the internet') + +class Plugins(PreferencesPlugin): + name = 'Plugins' + icon = I('plugins.png') + gui_name = _('Plugins') + category = 'Advanced' + gui_category = _('Advanced') + category_order = 5 + name_order = 1 + config_widget = 'calibre.gui2.preferences.plugins' + description = _('Add/remove/customize various bits of calibre ' + 'functionality') + +class Tweaks(PreferencesPlugin): + name = 'Tweaks' + icon = I('drawer.png') + gui_name = _('Tweaks') + category = 'Advanced' + gui_category = _('Advanced') + category_order = 5 + name_order = 2 + config_widget = 'calibre.gui2.preferences.tweaks' + description = _('Fine tune how calibre behaves in various contexts') + +class Misc(PreferencesPlugin): + name = 'Misc' + icon = I('exec.png') + gui_name = _('Miscellaneous') + category = 'Advanced' + gui_category = _('Advanced') + category_order = 5 + name_order = 3 + config_widget = 'calibre.gui2.preferences.misc' + description = _('Miscellaneous advanced configuration') plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions, - CommonOptions, OutputOptions, Adding] + CommonOptions, OutputOptions, Adding, Saving, Sending, Email, Server, + Plugins, Tweaks, Misc] #}}} diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 916c88f203..e318d368ff 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -82,7 +82,7 @@ class ITUNES(DriverBase): ''' name = 'Apple device interface' - gui_name = 'Apple device' + gui_name = _('Apple device') icon = I('devices/ipad.png') description = _('Communicate with iTunes/iBooks.') supported_platforms = ['osx','windows'] @@ -2303,9 +2303,9 @@ class ITUNES(DriverBase): # Delete existing from Device|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata for book in self.cached_books: - if self.cached_books[book]['uuid'] == metadata.uuid and \ - self.cached_books[book]['title'] == metadata.title and \ - self.cached_books[book]['author'] == metadata.authors[0]: + if self.cached_books[book]['uuid'] == metadata.uuid or \ + (self.cached_books[book]['title'] == metadata.title and \ + self.cached_books[book]['author'] == metadata.authors[0]): self.update_list.append(self.cached_books[book]) self._remove_from_device(self.cached_books[book]) if DEBUG: @@ -2322,9 +2322,9 @@ class ITUNES(DriverBase): # Delete existing from Library|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata for book in self.cached_books: - if self.cached_books[book]['uuid'] == metadata.uuid and \ - self.cached_books[book]['title'] == metadata.title and \ - self.cached_books[book]['author'] == metadata.authors[0]: + if self.cached_books[book]['uuid'] == metadata.uuid or \ + (self.cached_books[book]['title'] == metadata.title and \ + self.cached_books[book]['author'] == metadata.authors[0]): self.update_list.append(self.cached_books[book]) self._remove_from_iTunes(self.cached_books[book]) if DEBUG: @@ -2488,7 +2488,8 @@ class ITUNES(DriverBase): zf_opf.close() # If 'News' in tags, tweak the title/author for friendlier display in iBooks - if _('News') in metadata.tags: + if _('News') in metadata.tags or \ + _('Catalog') in metadata.tags: if metadata.title.find('[') > 0: metadata.title = metadata.title[:metadata.title.find('[')-1] date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y')) diff --git a/src/calibre/devices/jetbook/driver.py b/src/calibre/devices/jetbook/driver.py index 0d2e35e4fc..6ee1c07464 100644 --- a/src/calibre/devices/jetbook/driver.py +++ b/src/calibre/devices/jetbook/driver.py @@ -87,7 +87,7 @@ class MIBUK(USBMS): name = 'MiBuk Wolder Device Interface' description = _('Communicate with the MiBuk Wolder reader.') author = 'Kovid Goyal' - supported_platforms = ['windows', 'osx', 'linux'] + supported_platforms = ['windows'] FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'txt', 'rtf', 'pdf'] diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py index 9da99d75c8..496162d668 100644 --- a/src/calibre/devices/kobo/books.py +++ b/src/calibre/devices/kobo/books.py @@ -44,16 +44,17 @@ class Book(MetaInformation): self.mime = mime self.size = size # will be set later if None - try: - if ContentType == '6': - self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") - else: - self.datetime = time.gmtime(os.path.getctime(self.path)) - except: - self.datetime = time.gmtime() - if thumbnail_name is not None: - self.thumbnail = ImageWrapper(thumbnail_name) + if ContentType == '6': + self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") + else: + try: + self.datetime = time.gmtime(os.path.getctime(self.path)) + except: + self.datetime = time.gmtime() + + if thumbnail_name is not None: + self.thumbnail = ImageWrapper(thumbnail_name) self.tags = [] if other: self.smart_update(other) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 5e1c752c76..f24e00143b 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -106,11 +106,14 @@ class KOBO(USBMS): changed = True bl[idx].device_collections = playlist_map.get(lpath, []) else: - book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID) + if ContentType == '6': + book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576) + else: + book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID) # print 'Update booklist' + book.device_collections = playlist_map.get(book.lpath, []) if bl.add_book(book, replace_metadata=False): changed = True - book.device_collections = playlist_map.get(book.lpath, []) except: # Probably a path encoding error import traceback traceback.print_exc() @@ -231,21 +234,9 @@ class KOBO(USBMS): path = self.normalize_path(path) # print "Delete file normalized path: " + path extension = os.path.splitext(path)[1] - - if extension == '.kobo': - # Kobo books do not have book files. They do have some images though - #print "kobo book" - ContentType = 6 - ContentID = self.contentid_from_path(path, ContentType) - elif extension == '.pdf' or extension == '.epub': - # print "ePub or pdf" - ContentType = 16 - #print "Path: " + path - ContentID = self.contentid_from_path(path, ContentType) - # print "ContentID: " + ContentID - else: # if extension == '.html' or extension == '.txt': - ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored - ContentID = self.contentid_from_path(path, ContentType) + ContentType = self.get_content_type_from_extension(extension) + + ContentID = self.contentid_from_path(path, ContentType) ImageID = self.delete_via_sql(ContentID, ContentType) #print " We would now delete the Images for" + ImageID @@ -343,6 +334,17 @@ class KOBO(USBMS): ContentID = ContentID.replace("\\", '/') return ContentID + def get_content_type_from_extension(self, extension): + if extension == '.kobo': + # Kobo books do not have book files. They do have some images though + #print "kobo book" + ContentType = 6 + elif extension == '.pdf' or extension == '.epub': + # print "ePub or pdf" + ContentType = 16 + else: # if extension == '.html' or extension == '.txt': + ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored + return ContentType def path_from_contentid(self, ContentID, ContentType, oncard): path = ContentID diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index 7e8f5fbdd3..bb75ceabd1 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -108,4 +108,23 @@ class PDNOVEL(USBMS): with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile: coverfile.write(coverdata[2]) +class GEMEI(USBMS): + name = 'Gemei Device Interface' + gui_name = 'GM2000' + description = _('Communicate with the GM2000') + author = 'Kovid Goyal' + supported_platforms = ['windows', 'osx', 'linux'] + + # Ordered list of supported formats + FORMATS = ['epub', 'chm', 'html', 'pdb', 'pdf', 'txt'] + + VENDOR_ID = [0x07c4] + PRODUCT_ID = [0xa4a5] + BCD = None + + VENDOR_NAME = 'CHINA' + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'CHIP' + + EBOOK_DIR_MAIN = 'eBooks' + SUPPORTS_SUB_DIRS = True diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py index f05d9c0311..879f86d66a 100644 --- a/src/calibre/devices/prs505/sony_cache.py +++ b/src/calibre/devices/prs505/sony_cache.py @@ -89,6 +89,10 @@ class XMLCache(object): raw, strip_encoding_pats=True, assume_utf8=True, verbose=DEBUG)[0], parser=parser) + if self.roots[source_id] is None: + raise Exception(('The SONY database at %s is corrupted. Try ' + ' disconnecting and reconnecting your reader.')%path) + # }}} recs = self.roots[0].xpath('//*[local-name()="records"]') diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 6596e9b1a2..624b277e61 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -138,3 +138,11 @@ def check_ebook_format(stream, current_guess): stream.seek(0) return ans +def calibre_cover(title, author_string, series_string=None, + output_format='jpg', title_size=46, author_size=36): + from calibre.utils.magick.draw import create_cover_page, TextLine + lines = [TextLine(title, title_size), TextLine(author_string, author_size)] + if series_string: + lines.append(TextLine(series_string, author_size)) + return create_cover_page(lines, I('library.png'), output_format='jpg') + diff --git a/src/calibre/ebooks/metadata/archive.py b/src/calibre/ebooks/metadata/archive.py index 624c2ad5e5..f5982406ea 100644 --- a/src/calibre/ebooks/metadata/archive.py +++ b/src/calibre/ebooks/metadata/archive.py @@ -70,7 +70,7 @@ class ArchiveExtract(FileTypePlugin): fname = fnames[0] ext = os.path.splitext(fname)[1][1:] if ext.lower() not in ('lit', 'epub', 'mobi', 'prc', 'rtf', 'pdf', - 'mp3', 'pdb', 'azw', 'azw1'): + 'mp3', 'pdb', 'azw', 'azw1', 'fb2'): return archive of = self.temporary_file('_archive_extract.'+ext) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index f93b614ef2..2d9de7f780 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -1105,7 +1105,8 @@ class OPFCreator(MetaInformation): spine.set('toc', 'ncx') if self.spine is not None: for ref in self.spine: - spine.append(E.itemref(idref=ref.id)) + if ref.id is not None: + spine.append(E.itemref(idref=ref.id)) guide = E.guide() if self.guide is not None: for ref in self.guide: diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 10f1ee32d6..5d5de7b153 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -1695,12 +1695,11 @@ class MobiWriter(object): header.write(pack('>I', 1)) # 0x1c - 0x1f : Text encoding ? - # header.write(pack('>I', 650001)) - # GR: This needs to be either 0xFDE9 or 0x4E4 - header.write(pack('>I', 0xFDE9)) + # GR: Language encoding for NCX entries (latin_1) + header.write(pack('>I', 0x4e4)) - # 0x20 - 0x23 : Language code? - header.write(iana2mobi(str(self._oeb.metadata.language[0]))) + # 0x20 - 0x23 : Mimicking kindleGen + header.write(pack('>I', 0xFFFFFFFF)) # 0x24 - 0x27 : Number of TOC entries in INDX1 header.write(pack('>I', indxt_count + 1)) @@ -1795,13 +1794,12 @@ class MobiWriter(object): self._oeb.log.debug('Index records dumped to', t) def _clean_text_value(self, text): - if text is not None and text.strip() : - text = text.strip() - if not isinstance(text, unicode): - text = text.decode('utf-8', 'replace') - text = text.encode('utf-8') - else : - text = "(none)".encode('utf-8') + if not text: + text = u'(none)' + text = text.strip() + if not isinstance(text, unicode): + text = text.decode('utf-8', 'replace') + text = text.encode('cp1252','replace') return text def _add_to_ctoc(self, ctoc_str, record_offset): @@ -2151,6 +2149,26 @@ class MobiWriter(object): indxt.write(decint(self._ctoc_map[index]['titleOffset'], DECINT_FORWARD)) # vwi title offset in CNCX indxt.write(decint(0, DECINT_FORWARD)) # unknown byte + def _write_subchapter_node(self, indxt, indices, index, offset, length, count): + # This style works without a parent chapter, mimicking what KindleGen does, + # using a value of 0x0B for parentIndex + # Writes an INDX1 NCXEntry of entryType 0x1F - subchapter + if self.opts.verbose > 2: + # *** GR: Turn this off while I'm developing my code + #self._oeb.log.debug('Writing TOC node to IDXT:', node.title, 'href:', node.href) + pass + + pos = 0xc0 + indxt.tell() + indices.write(pack('>H', pos)) # Save the offset for IDXTIndices + name = "%04X"%count + indxt.write(chr(len(name)) + name) # Write the name + indxt.write(INDXT['subchapter']) # entryType [0x0F | 0xDF | 0xFF | 0x3F] + indxt.write(decint(offset, DECINT_FORWARD)) # offset + indxt.write(decint(length, DECINT_FORWARD)) # length + indxt.write(decint(self._ctoc_map[index]['titleOffset'], DECINT_FORWARD)) # vwi title offset in CNCX + indxt.write(decint(0, DECINT_FORWARD)) # unknown byte + indxt.write(decint(0xb, DECINT_FORWARD)) # parentIndex - null + def _compute_offset_length(self, i, node, entries) : h = node.href if h not in self._id_offsets: diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index 83b7b5d3c1..59b42df68a 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -89,19 +89,22 @@ class CoverManager(object): ''' Create a generic cover for books that dont have a cover ''' - from calibre.ebooks.metadata import authors_to_string + from calibre.ebooks.metadata import authors_to_string, fmt_sidx if self.no_default_cover: return None self.log('Generating default cover') m = self.oeb.metadata title = unicode(m.title[0]) authors = [unicode(x) for x in m.creator if x.role == 'aut'] + series_string = None + if m.series and m.series_index: + series_string = _('Book %s of %s')%( + fmt_sidx(m.series_index[0], use_roman=True), m.series[0]) try: - from calibre.utils.magick.draw import create_cover_page, TextLine - lines = [TextLine(title, 44), TextLine(authors_to_string(authors), - 32)] - img_data = create_cover_page(lines, I('library.png')) + from calibre.ebooks import calibre_cover + img_data = calibre_cover(title, authors_to_string(authors), + series_string=series_string) id, href = self.oeb.manifest.generate('cover_image', 'cover_image.jpg') item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0], diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index 6feaec978d..77fa4755c1 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -26,14 +26,18 @@ class GenerateCatalogAction(InterfaceAction): rows = xrange(self.gui.library_view.model().rowCount(QModelIndex())) ids = map(self.gui.library_view.model().id, rows) - dbspec = None if not ids: return error_dialog(self.gui, _('No books selected'), _('No books selected to generate catalog for'), show=True) + db = self.gui.library_view.model().db + dbspec = {} + for id in ids: + dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)} + # Calling gui2.tools:generate_catalog() - ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager.device) + ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager) if ret is None: return diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 955c751d19..79406da40c 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -177,7 +177,16 @@ class ChooseLibraryAction(InterfaceAction): return error_dialog(self.gui, _('Already exists'), _('The folder %s already exists. Delete it first.') % newloc, show=True) - os.rename(loc, newloc) + try: + os.rename(loc, newloc) + except: + import traceback + error_dialog(self.gui, _('Rename failed'), + _('Failed to rename the library at %s. ' + 'The most common cause for this is if one of the files' + ' in the library is open in another program.') % loc, + det_msg=traceback.format_exc(), show=True) + return self.stats.rename(location, newloc) self.build_menus() diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index e50131afe3..0343c6df84 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -159,7 +159,7 @@ class DeleteAction(InterfaceAction): if self.gui.stack.currentIndex() == 0: if not confirm('

'+_('The selected books will be ' 'permanently deleted and the files ' - 'removed from your computer. Are you sure?') + 'removed from your calibre library. Are you sure?') +'

', 'library_delete_books', self.gui): return ci = view.currentIndex() diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 6b8c303fbe..744ab20d10 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -122,7 +122,7 @@ class ConnectShareAction(InterfaceAction): self.share_conn_menu.toggle_server.connect(self.toggle_content_server) self.share_conn_menu.config_email.connect(partial( self.gui.iactions['Preferences'].do_config, - initial_category='email')) + initial_plugin=('Sharing', 'Email'))) self.qaction.setMenu(self.share_conn_menu) self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder) self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 7a1e589d2a..f0232d9859 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -51,7 +51,8 @@ class EditMetadataAction(InterfaceAction): self.merge_books) mb.addSeparator() mb.addAction(_('Merge into first selected book - keep others'), - partial(self.merge_books, safe_merge=True)) + partial(self.merge_books, safe_merge=True), + Qt.AltModifier+Qt.Key_M) self.merge_menu = mb self.action_merge.setMenu(mb) md.addSeparator() diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py index 9173cd8144..d9957bd70d 100644 --- a/src/calibre/gui2/actions/preferences.py +++ b/src/calibre/gui2/actions/preferences.py @@ -8,8 +8,8 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import QIcon, QMenu from calibre.gui2.actions import InterfaceAction -from calibre.gui2.dialogs.config import ConfigDialog -from calibre.gui2 import error_dialog, config +from calibre.gui2.preferences.main import Preferences +from calibre.gui2 import error_dialog class PreferencesAction(InterfaceAction): @@ -28,7 +28,7 @@ class PreferencesAction(InterfaceAction): x.triggered.connect(self.do_config) - def do_config(self, checked=False, initial_category='general'): + def do_config(self, checked=False, initial_plugin=None): if self.gui.job_manager.has_jobs(): d = error_dialog(self.gui, _('Cannot configure'), _('Cannot configure while there are running jobs.')) @@ -39,26 +39,7 @@ class PreferencesAction(InterfaceAction): _('Cannot configure before calibre is restarted.')) d.exec_() return - d = ConfigDialog(self.gui, self.gui.library_view, - server=self.gui.content_server, initial_category=initial_category) - - d.exec_() - self.gui.content_server = d.server - if self.gui.content_server is not None: - self.gui.content_server.state_callback = \ - self.Dispatcher(self.gui.iactions['Connect Share'].content_server_state_changed) - self.gui.content_server.state_callback(self.gui.content_server.is_running) - - if d.result() == d.Accepted: - self.gui.search.search_as_you_type(config['search_as_you_type']) - self.gui.tags_view.set_new_model() # in case columns changed - self.gui.iactions['Save To Disk'].reread_prefs() - self.gui.tags_view.recount() - self.gui.create_device_menu() - self.gui.set_device_menu_items_state(bool(self.gui.device_connected)) - self.gui.tool_bar.build_bar() - self.gui.build_context_menus() - self.gui.tool_bar.apply_settings() - + d = Preferences(self.gui, initial_plugin=initial_plugin) + d.show() diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 5acda0948c..1f0fdae7e2 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -19,6 +19,7 @@ class PluginWidget(QWidget,Ui_Form): OPTION_FIELDS = [('exclude_genre','\[.+\]'), ('exclude_tags','~,'+_('Catalog')), ('generate_titles', True), + ('generate_series', True), ('generate_recently_added', True), ('note_tag','*'), ('numbers_as_text', False), @@ -40,7 +41,7 @@ class PluginWidget(QWidget,Ui_Form): # Update dialog fields from stored options for opt in self.OPTION_FIELDS: opt_value = gprefs.get(self.name + '_' + opt[0], opt[1]) - if opt[0] in ['numbers_as_text','generate_titles','generate_recently_added']: + if opt[0] in ['numbers_as_text','generate_titles','generate_series','generate_recently_added']: getattr(self, opt[0]).setChecked(opt_value) else: getattr(self, opt[0]).setText(opt_value) @@ -52,13 +53,13 @@ class PluginWidget(QWidget,Ui_Form): # others store as lists opts_dict = {} for opt in self.OPTION_FIELDS: - if opt[0] in ['numbers_as_text','generate_titles','generate_recently_added']: + if opt[0] in ['numbers_as_text','generate_titles','generate_series','generate_recently_added']: opt_value = getattr(self,opt[0]).isChecked() else: opt_value = unicode(getattr(self, opt[0]).text()) gprefs.set(self.name + '_' + opt[0], opt_value) - if opt[0] in ['exclude_genre','numbers_as_text','generate_titles','generate_recently_added']: + if opt[0] in ['exclude_genre','numbers_as_text','generate_titles','generate_series','generate_recently_added']: opts_dict[opt[0]] = opt_value else: opts_dict[opt[0]] = opt_value.split(',') diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index cdf91eed6f..fa6b53e3a4 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -108,20 +108,27 @@ - + Include 'Recently Added' Section - + Sort numbers as text + + + + Include 'Series' Section + + + diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py index f0fdf9a9a1..1affea4e2a 100644 --- a/src/calibre/gui2/convert/bulk.py +++ b/src/calibre/gui2/convert/bulk.py @@ -58,7 +58,8 @@ class BulkConfig(Config): output_path = 'dummy.'+output_format log = Log() log.outputs = [] - self.plumber = Plumber(input_path, output_path, log) + self.plumber = Plumber(input_path, output_path, log, + merge_plugin_recs=False) def widget_factory(cls): return cls(self.stack, self.plumber.get_option_by_name, diff --git a/src/calibre/gui2/convert/gui_conversion.py b/src/calibre/gui2/convert/gui_conversion.py index 148dc328ad..69dabe28b8 100644 --- a/src/calibre/gui2/convert/gui_conversion.py +++ b/src/calibre/gui2/convert/gui_conversion.py @@ -27,13 +27,9 @@ def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, conne notification=DummyReporter(), log=None): if log is None: log = Log() - if dbspec is None: - from calibre.utils.config import prefs - from calibre.library.database2 import LibraryDatabase2 - dbpath = prefs['library_path'] - db = LibraryDatabase2(dbpath) - else: # To be implemented in the future - pass + from calibre.library import db + db = db() + db.catalog_plugin_on_device_temp_mapping = dbspec # Create a minimal OptionParser that we can append to parser = OptionParser() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 5fb8b1028b..45c78ce6da 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -760,8 +760,8 @@ class DeviceMixin(object): # {{{ def refresh_ondevice_info(self, device_connected, reset_only = False): ''' - Force the library view to refresh, taking into consideration - books information + Force the library view to refresh, taking into consideration new + device books information ''' self.book_on_device(None, reset=True) if reset_only: @@ -791,12 +791,14 @@ class DeviceMixin(object): # {{{ self.booklists()) model.paths_deleted(paths) self.upload_booklists() - # Clear the ondevice info so it will be recomputed + # Force recomputation the library's ondevice info. We need to call + # set_books_in_library even though books were not added because + # the deleted book might have been an exact match. + self.set_books_in_library(self.booklists(), reset=True) self.book_on_device(None, None, reset=True) - # We want to reset all the ondevice flags in the library. Use a big - # hammer, so we don't need to worry about whether some succeeded or not - self.library_view.model().refresh() - + # We need to reset the ondevice flags in the library. Use a big hammer, + # so we don't need to worry about whether some succeeded or not. + self.refresh_ondevice_info(device_connected=True, reset_only=False) def dispatch_sync_event(self, dest, delete, specific): rows = self.library_view.selectionModel().selectedRows() @@ -1286,8 +1288,14 @@ class DeviceMixin(object): # {{{ books_to_be_deleted = memory[1] self.library_view.model().delete_books_by_id(books_to_be_deleted) - self.set_books_in_library(self.booklists(), - reset=bool(books_to_be_deleted)) + # There are some cases where sending a book to the device overwrites a + # book already there with a different book. This happens frequently in + # news. When this happens, the book match indication will be wrong + # because the UUID changed. Force both the device and the library view + # to refresh the flags. + self.set_books_in_library(self.booklists(), reset=True) + self.book_on_device(None, reset=True) + self.refresh_ondevice_info(device_connected = True) view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view view.model().resort(reset=False) @@ -1295,112 +1303,108 @@ class DeviceMixin(object): # {{{ for f in files: getattr(f, 'close', lambda : True)() - self.book_on_device(None, reset=True) - if metadata: - changed = set([]) - for mi in metadata: - id_ = getattr(mi, 'application_id', None) - if id_ is not None: - changed.add(id_) - if changed: - self.library_view.model().refresh_ids(list(changed)) - def book_on_device(self, id, format=None, reset=False): - loc = [None, None, None] + ''' + Return an indication of whether the given book represented by its db id + is on the currently connected device. It returns a 5 element list. The + first three elements represent memory locations main, carda, and cardb, + and are true if the book is identifiably in that memory. The fourth + is a count of how many instances of the book were found across all + the memory locations. The fifth is a set of paths to the + matching books on the device. + ''' + loc = [None, None, None, 0, set([])] if reset: - self.book_db_title_cache = None - self.book_db_uuid_cache = None + self.book_db_id_cache = None + self.book_db_id_counts = None + self.book_db_uuid_path_map = None return - if self.book_db_title_cache is None: - self.book_db_title_cache = [] - self.book_db_uuid_cache = [] + if not hasattr(self, 'db_book_uuid_cache'): + return loc + + if self.book_db_id_cache is None: + self.book_db_id_cache = [] + self.book_db_id_counts = {} + self.book_db_uuid_path_map = {} for i, l in enumerate(self.booklists()): - self.book_db_title_cache.append({}) - self.book_db_uuid_cache.append(set()) + self.book_db_id_cache.append(set()) for book in l: - book_title = book.title.lower() if book.title else '' - book_title = re.sub('(?u)\W|[_]', '', book_title) - if book_title not in self.book_db_title_cache[i]: - self.book_db_title_cache[i][book_title] = \ - {'authors':set(), 'db_ids':set(), 'uuids':set()} - book_authors = authors_to_string(book.authors).lower() - book_authors = re.sub('(?u)\W|[_]', '', book_authors) - self.book_db_title_cache[i][book_title]['authors'].add(book_authors) db_id = getattr(book, 'application_id', None) if db_id is None: db_id = book.db_id if db_id is not None: - self.book_db_title_cache[i][book_title]['db_ids'].add(db_id) - uuid = getattr(book, 'uuid', None) - if uuid is not None: - self.book_db_uuid_cache[i].add(uuid) + # increment the count of books on the device with this + # db_id. + self.book_db_id_cache[i].add(db_id) + if db_id not in self.book_db_uuid_path_map: + self.book_db_uuid_path_map[db_id] = set() + if getattr(book, 'lpath', False): + self.book_db_uuid_path_map[db_id].add(book.lpath) + c = self.book_db_id_counts.get(db_id, 0) + self.book_db_id_counts[db_id] = c + 1 - mi = self.library_view.model().db.get_metadata(id, index_is_id=True) for i, l in enumerate(self.booklists()): - if mi.uuid in self.book_db_uuid_cache[i]: + if id in self.book_db_id_cache[i]: loc[i] = True - continue - db_title = re.sub('(?u)\W|[_]', '', mi.title.lower()) - cache = self.book_db_title_cache[i].get(db_title, None) - if cache: - if id in cache['db_ids']: - loc[i] = True - continue - if mi.authors and \ - re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \ - in cache['authors']: - loc[i] = True - continue - # Also check author sort, because it can be used as author in - # some formats - if mi.author_sort and \ - re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \ - in cache['authors']: - loc[i] = True - continue + loc[3] = self.book_db_id_counts.get(id, 0) + loc[4] |= self.book_db_uuid_path_map[id] return loc def set_books_in_library(self, booklists, reset=False): + ''' + Set the ondevice indications in the device database. + This method should be called before book_on_device is called, because + it sets the application_id for matched books. Book_on_device uses that + to both speed up matching and to count matches. + ''' + + string_pat = re.compile('(?u)\W|[_]') + def clean_string(x): + x = x.lower() if x else '' + return string_pat.sub('', x) + # Force a reset if the caches are not initialized if reset or not hasattr(self, 'db_book_title_cache'): # It might be possible to get here without having initialized the # library view. In this case, simply give up - if not hasattr(self, 'library_view') or self.library_view is None: - return - db = getattr(self.library_view.model(), 'db', None) - if db is None: + try: + db = self.library_view.model().db + except: return # Build a cache (map) of the library, so the search isn't On**2 self.db_book_title_cache = {} self.db_book_uuid_cache = {} for id in db.data.iterallids(): mi = db.get_metadata(id, index_is_id=True) - title = re.sub('(?u)\W|[_]', '', mi.title.lower()) + title = clean_string(mi.title) if title not in self.db_book_title_cache: self.db_book_title_cache[title] = \ {'authors':{}, 'author_sort':{}, 'db_ids':{}} + # If there are multiple books in the library with the same title + # and author, then remember the last one. That is OK, because as + # we can't tell the difference between the books, one is as good + # as another. if mi.authors: - authors = authors_to_string(mi.authors).lower() - authors = re.sub('(?u)\W|[_]', '', authors) + authors = clean_string(authors_to_string(mi.authors)) self.db_book_title_cache[title]['authors'][authors] = mi if mi.author_sort: - aus = mi.author_sort.lower() - aus = re.sub('(?u)\W|[_]', '', aus) + aus = clean_string(mi.author_sort) self.db_book_title_cache[title]['author_sort'][aus] = mi self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi self.db_book_uuid_cache[mi.uuid] = mi # Now iterate through all the books on the device, setting the - # in_library field Fastest and most accurate key is the uuid. Second is - # the application_id, which is really the db key, but as this can - # accidentally match across libraries we also verify the title. The - # db_id exists on Sony devices. Fallback is title and author match + # in_library field. If the UUID matches a book in the library, then + # do not consider that book for other matching. In all cases set + # the application_id to the db_id of the matching book. This value + # will be used by books_on_device to indicate matches. update_metadata = prefs['manage_device_metadata'] == 'on_connect' for booklist in booklists: for book in booklist: + book.in_library = None if getattr(book, 'uuid', None) in self.db_book_uuid_cache: if update_metadata: book.smart_update(self.db_book_uuid_cache[book.uuid], @@ -1410,39 +1414,56 @@ class DeviceMixin(object): # {{{ book.application_id = \ self.db_book_uuid_cache[book.uuid].application_id continue - - book_title = book.title.lower() if book.title else '' - book_title = re.sub('(?u)\W|[_]', '', book_title) - book.in_library = None + # No UUID exact match. Try metadata matching. + book_title = clean_string(book.title) d = self.db_book_title_cache.get(book_title, None) if d is not None: + # At this point we know that the title matches. The book + # will match if any of the db_id, author, or author_sort + # also match. if getattr(book, 'application_id', None) in d['db_ids']: book.in_library = True + # app_id already matches a db_id. No need to set it. if update_metadata: book.smart_update(d['db_ids'][book.application_id], replace_metadata=True) continue - if book.db_id in d['db_ids']: + # Sonys know their db_id independent of the application_id + # in the metadata cache. Check that as well. + if getattr(book, 'db_id', None) in d['db_ids']: book.in_library = True + book.application_id = \ + d['db_ids'][book.db_id].application_id if update_metadata: book.smart_update(d['db_ids'][book.db_id], replace_metadata=True) continue + # We now know that the application_id is not right. Set it + # to None to prevent book_on_device from accidentally + # matching on it. It will be set to a correct value below if + # the book is matched with one in the library + book.application_id = None if book.authors: # Compare against both author and author sort, because # either can appear as the author - book_authors = authors_to_string(book.authors).lower() - book_authors = re.sub('(?u)\W|[_]', '', book_authors) + book_authors = clean_string(authors_to_string(book.authors)) if book_authors in d['authors']: book.in_library = True + book.application_id = \ + d['authors'][book_authors].application_id if update_metadata: book.smart_update(d['authors'][book_authors], replace_metadata=True) elif book_authors in d['author_sort']: book.in_library = True + book.application_id = \ + d['author_sort'][book_authors].application_id if update_metadata: book.smart_update(d['author_sort'][book_authors], replace_metadata=True) + else: + # Book definitely not matched. Clear its application ID + book.application_id = None # Set author_sort if it isn't already asort = getattr(book, 'author_sort', None) if not asort and book.authors: diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py deleted file mode 100644 index 067a49de2e..0000000000 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ /dev/null @@ -1,1028 +0,0 @@ -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' - -import os, re, time, textwrap, copy, sys - -from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \ - QVBoxLayout, QLabel, QPlainTextEdit, \ - QStringListModel, QAbstractItemModel, QFont, \ - SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \ - QModelIndex, QAbstractTableModel, \ - QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \ - QProgressDialog - -from calibre.constants import iswindows, isosx -from calibre.gui2.dialogs.config.config_ui import Ui_Dialog -from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn -from calibre.gui2.dialogs.config.toolbar import ToolbarLayout -from calibre.gui2 import error_dialog, config, gprefs, \ - open_url, open_local_file, \ - ALL_COLUMNS, NONE, info_dialog, choose_files, \ - warning_dialog, ResizableDialog, question_dialog -from calibre.utils.config import prefs, read_raw_tweaks, write_tweaks -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.oeb.iterator import is_supported -from calibre.library.server import server_config -from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ - disable_plugin, customize_plugin, \ - plugin_customization, add_plugin, \ - remove_plugin, all_input_formats, \ - input_format_plugins, \ - output_format_plugins, available_output_formats -from calibre.utils.smtp import config as smtp_prefs -from calibre.gui2.convert import config_widget_for_input_plugin -from calibre.gui2.convert.look_and_feel import LookAndFeelWidget -from calibre.gui2.convert.page_setup import PageSetupWidget -from calibre.gui2.convert.structure_detection import StructureDetectionWidget -from calibre.ebooks.conversion.plumber import Plumber -from calibre.utils.logging import Log -from calibre.gui2.convert.toc import TOCWidget -from calibre.utils.search_query_parser import saved_searches - - -class ConfigTabs(QTabWidget): - - def __init__(self, parent): - QTabWidget.__init__(self, parent) - log = Log() - log.outputs = [] - - self.plumber = Plumber('dummy.epub', 'dummy.epub', log, dummy=True, - merge_plugin_recs=False) - - def widget_factory(cls): - return cls(self, self.plumber.get_option_by_name, - self.plumber.get_option_help, None, None) - - lf = widget_factory(LookAndFeelWidget) - ps = widget_factory(PageSetupWidget) - sd = widget_factory(StructureDetectionWidget) - toc = widget_factory(TOCWidget) - - self.widgets = [lf, ps, sd, toc] - - for plugin in input_format_plugins(): - pw = config_widget_for_input_plugin(plugin) - if pw is not None: - pw.ICON = I('forward.png') - self.widgets.append(widget_factory(pw)) - - for plugin in output_format_plugins(): - name = plugin.name.lower().replace(' ', '_') - try: - output_widget = __import__('calibre.gui2.convert.'+name, - fromlist=[1]) - pw = output_widget.PluginWidget - pw.ICON = I('forward.png') - self.widgets.append(widget_factory(pw)) - except ImportError: - continue - - for i, widget in enumerate(self.widgets): - self.addTab(widget, widget.TITLE.replace('\n', ' ').replace('&', - '&&')) - self.setTabToolTip(i, widget.HELP if widget.HELP else widget.TITLE) - self.setUsesScrollButtons(True) - - def commit(self): - for widget in self.widgets: - if not widget.pre_commit_check(): - return False - widget.commit(save_defaults=True) - return True - -class PluginModel(QAbstractItemModel): - - def __init__(self, *args): - QAbstractItemModel.__init__(self, *args) - self.icon = QVariant(QIcon(I('plugins.png'))) - p = QIcon(self.icon).pixmap(32, 32, QIcon.Disabled, QIcon.On) - self.disabled_icon = QVariant(QIcon(p)) - self._p = p - self.populate() - - def populate(self): - self._data = {} - for plugin in initialized_plugins(): - if plugin.type not in self._data: - self._data[plugin.type] = [plugin] - else: - self._data[plugin.type].append(plugin) - self.categories = sorted(self._data.keys()) - - for plugins in self._data.values(): - plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower())) - - def index(self, row, column, parent): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - if parent.isValid(): - return self.createIndex(row, column, 1+parent.row()) - else: - return self.createIndex(row, column, 0) - - def parent(self, index): - if not index.isValid() or index.internalId() == 0: - return QModelIndex() - return self.createIndex(index.internalId()-1, 0, 0) - - def rowCount(self, parent): - if not parent.isValid(): - return len(self.categories) - if parent.internalId() == 0: - category = self.categories[parent.row()] - return len(self._data[category]) - return 0 - - def columnCount(self, parent): - return 1 - - def index_to_plugin(self, index): - category = self.categories[index.parent().row()] - return self._data[category][index.row()] - - def plugin_to_index(self, plugin): - for i, category in enumerate(self.categories): - parent = self.index(i, 0, QModelIndex()) - for j, p in enumerate(self._data[category]): - if plugin == p: - return self.index(j, 0, parent) - return QModelIndex() - - def refresh_plugin(self, plugin, rescan=False): - if rescan: - self.populate() - idx = self.plugin_to_index(plugin) - self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx) - - def flags(self, index): - if not index.isValid(): - return 0 - if index.internalId() == 0: - return Qt.ItemIsEnabled - flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled - return flags - - def data(self, index, role): - if not index.isValid(): - return NONE - if index.internalId() == 0: - if role == Qt.DisplayRole: - category = self.categories[index.row()] - return QVariant(_("%(plugin_type)s %(plugins)s")%\ - dict(plugin_type=category, plugins=_('plugins'))) - else: - plugin = self.index_to_plugin(index) - if role == Qt.DisplayRole: - ver = '.'.join(map(str, plugin.version)) - desc = '\n'.join(textwrap.wrap(plugin.description, 50)) - ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), plugin.author, desc) - c = plugin_customization(plugin) - if c: - ans += _('\nCustomization: ')+c - return QVariant(ans) - if role == Qt.DecorationRole: - return self.disabled_icon if is_disabled(plugin) else self.icon - if role == Qt.ForegroundRole and is_disabled(plugin): - return QVariant(QBrush(Qt.gray)) - if role == Qt.UserRole: - return plugin - return NONE - - - -class CategoryModel(QStringListModel): - - CATEGORIES = [ - ('general', _('General'), 'dialog_information.png'), - ('interface', _('Interface'), 'lookfeel.png'), - ('conversion', _('Conversion'), 'convert.png'), - ('email', _('Email\nDelivery'), 'mail.png'), - ('add/save', _('Add/Save'), 'save.png'), - ('advanced', _('Advanced'), 'view.png'), - ('server', _('Content\nServer'), 'network-server.png'), - ('plugins', _('Plugins'), 'plugins.png'), - ] - - def __init__(self, *args): - QStringListModel.__init__(self, *args) - self.setStringList([x[1] for x in self.CATEGORIES]) - - def data(self, index, role): - if role == Qt.DecorationRole: - return QVariant(QIcon(I(self.CATEGORIES[index.row()][2]))) - return QStringListModel.data(self, index, role) - - def index_for_name(self, name): - for i, x in enumerate(self.CATEGORIES): - if x[0] == name: - return self.index(i) - return self.index(0) - -class EmailAccounts(QAbstractTableModel): - - def __init__(self, accounts): - QAbstractTableModel.__init__(self) - self.accounts = accounts - self.account_order = sorted(self.accounts.keys()) - self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')]) - self.default_font = QFont() - self.default_font.setBold(True) - self.default_font = QVariant(self.default_font) - self.tooltips =[NONE] + map(QVariant, - [_('Formats to email. The first matching format will be sent.'), - '

'+_('If checked, downloaded news will be automatically ' - 'mailed
to this email address ' - '(provided it is in one of the listed formats).')]) - - def rowCount(self, *args): - return len(self.account_order) - - def columnCount(self, *args): - return 3 - - def headerData(self, section, orientation, role): - if role == Qt.DisplayRole and orientation == Qt.Horizontal: - return self.headers[section] - return NONE - - def data(self, index, role): - row, col = index.row(), index.column() - if row < 0 or row >= self.rowCount(): - return NONE - account = self.account_order[row] - if role == Qt.UserRole: - return (account, self.accounts[account]) - if role == Qt.ToolTipRole: - return self.tooltips[col] - if role in [Qt.DisplayRole, Qt.EditRole]: - if col == 0: - return QVariant(account) - if col == 1: - return QVariant(self.accounts[account][0]) - if role == Qt.FontRole and self.accounts[account][2]: - return self.default_font - if role == Qt.CheckStateRole and col == 2: - return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked) - return NONE - - def flags(self, index): - if index.column() == 2: - return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable - else: - return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable - - def setData(self, index, value, role): - if not index.isValid(): - return False - row, col = index.row(), index.column() - account = self.account_order[row] - if col == 2: - self.accounts[account][1] ^= True - elif col == 1: - self.accounts[account][0] = unicode(value.toString()).upper() - else: - na = unicode(value.toString()) - from email.utils import parseaddr - addr = parseaddr(na)[-1] - if not addr: - return False - self.accounts[na] = self.accounts.pop(account) - self.account_order[row] = na - if '@kindle.com' in addr: - self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1' - - self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), - self.index(index.row(), 0), self.index(index.row(), 2)) - return True - - def make_default(self, index): - if index.isValid(): - row = index.row() - for x in self.accounts.values(): - x[2] = False - self.accounts[self.account_order[row]][2] = True - self.reset() - - def add(self): - x = _('new email address') - y = x - c = 0 - while y in self.accounts: - c += 1 - y = x + str(c) - auto_send = len(self.accounts) < 1 - self.accounts[y] = ['MOBI, EPUB', auto_send, - len(self.account_order) == 0] - self.account_order = sorted(self.accounts.keys()) - self.reset() - return self.index(self.account_order.index(y), 0) - - def remove(self, index): - if index.isValid(): - row = index.row() - account = self.account_order[row] - self.accounts.pop(account) - self.account_order = sorted(self.accounts.keys()) - has_default = False - for account in self.account_order: - if self.accounts[account][2]: - has_default = True - break - if not has_default and self.account_order: - self.accounts[self.account_order[0]][2] = True - - self.reset() - - -class ConfigDialog(ResizableDialog, Ui_Dialog): - - def category_current_changed(self, n, p): - self.stackedWidget.setCurrentIndex(n.row()) - - def __init__(self, parent, library_view, server=None, - initial_category='general'): - ResizableDialog.__init__(self, parent) - self._category_model = CategoryModel() - - self.category_view.currentChanged = self.category_current_changed - self.category_view.setModel(self._category_model) - self.parent = parent - self.library_view = library_view - self.model = library_view.model() - self.db = self.model.db - self.server = server - self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact) - - input_map = prefs['input_format_order'] - all_formats = set() - for fmt in all_input_formats().union(set(['ZIP', 'RAR'])): - all_formats.add(fmt.upper()) - for format in input_map + list(all_formats.difference(input_map)): - item = QListWidgetItem(format, self.input_order) - item.setData(Qt.UserRole, QVariant(format)) - item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) - - self.connect(self.input_up, SIGNAL('clicked()'), self.up_input) - self.connect(self.input_down, SIGNAL('clicked()'), self.down_input) - - rn = config['use_roman_numerals_for_series_number'] - self.timeout.setValue(prefs['network_timeout']) - self.roman_numerals.setChecked(rn) - self.new_version_notification.setChecked(config['new_version_notification']) - - # Set up columns - colmap = list(self.model.column_map) - state = self.library_view.get_state() - hidden_cols = state['hidden_columns'] - positions = state['column_positions'] - colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y])) - self.custcols = copy.deepcopy(self.db.field_metadata.get_custom_field_metadata()) - for col in colmap: - item = QListWidgetItem(self.model.headers[col], self.columns) - item.setData(Qt.UserRole, QVariant(col)) - flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable - if col != 'ondevice': - flags |= Qt.ItemIsUserCheckable - item.setFlags(flags) - if col != 'ondevice': - item.setCheckState(Qt.Unchecked if col in hidden_cols else - Qt.Checked) - self.column_up.clicked.connect(self.up_column) - self.column_down.clicked.connect(self.down_column) - self.del_custcol_button.clicked.connect(self.del_custcol) - self.add_custcol_button.clicked.connect(self.add_custcol) - self.edit_custcol_button.clicked.connect(self.edit_custcol) - - output_formats = sorted(available_output_formats()) - output_formats.remove('oeb') - for f in output_formats: - self.output_format.addItem(f.upper()) - default_index = \ - self.output_format.findText(prefs['output_format'].upper()) - self.output_format.setCurrentIndex(default_index if default_index != -1 else 0) - - - self.cover_browse.setValue(config['cover_flow_queue_length']) - self.systray_notifications.setChecked(not config['disable_tray_notification']) - from calibre.utils.localization import available_translations, \ - get_language, get_lang - lang = get_lang() - if lang is None or lang not in available_translations(): - lang = 'en' - self.language.addItem(get_language(lang), QVariant(lang)) - items = [(l, get_language(l)) for l in available_translations() \ - if l != lang] - if lang != 'en': - items.append(('en', get_language('en'))) - items.sort(cmp=lambda x, y: cmp(x[1], y[1])) - for item in items: - self.language.addItem(item[1], QVariant(item[0])) - - exts = set([]) - for ext in BOOK_EXTENSIONS: - ext = ext.lower() - ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) - if ext == 'lrf' or is_supported('book.'+ext): - exts.add(ext) - - for ext in sorted(exts): - self.viewer.addItem(ext.upper()) - self.viewer.item(self.viewer.count()-1).setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable) - self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) - self.viewer.sortItems() - self.start.setEnabled(not getattr(self.server, 'is_running', False)) - self.test.setEnabled(not self.start.isEnabled()) - self.stop.setDisabled(self.start.isEnabled()) - self.connect(self.start, SIGNAL('clicked()'), self.start_server) - self.connect(self.view_logs, SIGNAL('clicked()'), self.view_server_logs) - self.connect(self.stop, SIGNAL('clicked()'), self.stop_server) - self.connect(self.test, SIGNAL('clicked()'), self.test_server) - self.connect(self.show_server_password, SIGNAL('stateChanged(int)'), - lambda s: self.password.setEchoMode(self.password.Normal if s == Qt.Checked else self.password.Password)) - self.password.setEchoMode(self.password.Password) - opts = server_config().parse() - self.max_cover_size.setText(opts.max_cover) - self.port.setValue(opts.port) - 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']) - self.delete_news.setChecked(config['delete_news_from_library_on_upload']) - p = {'normal':0, 'high':1, 'low':2}[prefs['worker_process_priority']] - self.priority.setCurrentIndex(p) - self.priority.setVisible(iswindows) - self.priority_label.setVisible(iswindows) - self.new_book_tags.setText(', '.join(prefs['new_book_tags'])) - self._plugin_model = PluginModel() - self.plugin_view.setModel(self._plugin_model) - self.plugin_view.setStyleSheet( - "QTreeView::item { padding-bottom: 10px;}") - self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle')) - self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize')) - self.connect(self.remove_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='remove')) - self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin) - self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin) - self.connect(self.button_osx_symlinks, SIGNAL('clicked()'), - self.create_symlinks) - self.button_osx_symlinks.setVisible(isosx) - self.separate_cover_flow.setChecked(config['separate_cover_flow']) - self.setup_email_page() - self.delete_news.setEnabled(bool(self.sync_news.isChecked())) - self.connect(self.sync_news, SIGNAL('toggled(bool)'), - self.delete_news.setEnabled) - self.setup_conversion_options() - self.opt_worker_limit.setValue(config['worker_limit']) - self.connect(self.button_open_config_dir, SIGNAL('clicked()'), - self.open_config_dir) - self.opt_get_social_metadata.setChecked(config['get_social_metadata']) - self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata']) - self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit']) - self.device_detection_button.clicked.connect(self.debug_device_detection) - self.port.editingFinished.connect(self.check_port_value) - self.search_as_you_type.setChecked(config['search_as_you_type']) - self.show_avg_rating.setChecked(config['show_avg_rating']) - self.show_splash_screen.setChecked(gprefs.get('show_splash_screen', - True)) - li = None - for i, z in enumerate([('wide', _('Wide')), - ('narrow', _('Narrow'))]): - x, y = z - self.opt_gui_layout.addItem(y, QVariant(x)) - if x == config['gui_layout']: - li = i - self.opt_gui_layout.setCurrentIndex(li) - restrictions = sorted(saved_searches().names(), - cmp=lambda x,y: cmp(x.lower(), y.lower())) - restrictions.insert(0, '') - for x in ('gui', 'cs'): - w = getattr(self, 'opt_%s_restriction'%x) - w.addItems(restrictions) - idx = w.findText(self.db.prefs.get(x+'_restriction', '')) - w.setCurrentIndex(0 if idx < 0 else idx) - self.opt_disable_animations.setChecked(config['disable_animations']) - idx = 0 - for i, x in enumerate([(_('Small'), 'small'), (_('Medium'), 'medium'), - (_('Large'), 'large')]): - if x[1] == gprefs.get('toolbar_icon_size', 'medium'): - idx = i - self.opt_toolbar_icon_size.addItem(x[0], x[1]) - self.opt_toolbar_icon_size.setCurrentIndex(idx) - idx = 0 - for i, x in enumerate([(_('Automatic'), 'auto'), (_('Always'), 'always'), - (_('Never'), 'never')]): - if x[1] == gprefs.get('toolbar_text', 'auto'): - idx = i - self.opt_toolbar_text.addItem(x[0], x[1]) - self.opt_toolbar_text.setCurrentIndex(idx) - self.reset_confirmation_button.clicked.connect(self.reset_confirmation) - - deft, curt = read_raw_tweaks() - self.current_tweaks.setPlainText(curt.decode('utf-8')) - self.default_tweaks.setPlainText(deft.decode('utf-8')) - self.restore_tweaks_to_default_button.clicked.connect(self.restore_tweaks_to_default) - self.toolbar_cm_widget = ToolbarLayout(parent, parent) - self.toolbar_cm_tab.addTab(self.toolbar_cm_widget, - _('Toolbars/Context menus')) - - self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category)) - - def restore_tweaks_to_default(self, *args): - deft, curt = read_raw_tweaks() - self.current_tweaks.setPlainText(deft.decode('utf-8')) - - - def reset_confirmation(self): - from calibre.gui2 import dynamic - for key in dynamic.keys(): - if key.endswith('_again') and dynamic[key] is False: - dynamic[key] = True - info_dialog(self, _('Done'), - _('Confirmation dialogs have all been reset'), show=True) - - def check_port_value(self, *args): - port = self.port.value() - if port < 1025: - warning_dialog(self, _('System port selected'), '

'+ - _('The value %d you have chosen for the content ' - 'server port is a system port. Your operating ' - 'system may not allow the server to run on this ' - 'port. To be safe choose a port number larger than ' - '1024.')%port, show=True) - - def debug_device_detection(self): - from calibre.gui2.dialogs.config.device_debug import DebugDevice - d = DebugDevice(self) - d.exec_() - - def open_config_dir(self): - from calibre.utils.config import config_dir - open_local_file(config_dir) - - def create_symlinks(self): - from calibre.utils.osx_symlinks import create_symlinks - loc, paths = create_symlinks() - if loc is None: - error_dialog(self, _('Error'), - _('Failed to install command line tools.'), - det_msg=paths, show=True) - else: - info_dialog(self, _('Command line tools installed'), - '

'+_('Command line tools installed in')+' '+loc+ - '
'+ _('If you move calibre.app, you have to re-install ' - 'the command line tools.'), - det_msg='\n'.join(paths), show=True) - - def setup_conversion_options(self): - self.conversion_options = ConfigTabs(self) - self.stackedWidget.insertWidget(2, self.conversion_options) - - def setup_email_page(self): - def x(): - if self._email_accounts.account_order: - return self._email_accounts.account_order[0] - self.send_email_widget.initialize(x) - opts = self.send_email_widget.smtp_opts - self._email_accounts = EmailAccounts(opts.accounts) - self.email_view.setModel(self._email_accounts) - - self.connect(self.email_add, SIGNAL('clicked(bool)'), - self.add_email_account) - self.connect(self.email_make_default, SIGNAL('clicked(bool)'), - lambda c: self._email_accounts.make_default(self.email_view.currentIndex())) - self.email_view.resizeColumnsToContents() - self.connect(self.email_remove, SIGNAL('clicked()'), - self.remove_email_account) - - def add_email_account(self, checked): - index = self._email_accounts.add() - self.email_view.setCurrentIndex(index) - self.email_view.resizeColumnsToContents() - self.email_view.edit(index) - - def remove_email_account(self, *args): - idx = self.email_view.currentIndex() - self._email_accounts.remove(idx) - - def set_email_settings(self): - to_set = bool(self._email_accounts.accounts) - if not self.send_email_widget.set_email_settings(to_set): - return False - conf = smtp_prefs() - conf.set('accounts', self._email_accounts.accounts) - return True - - - def add_plugin(self): - path = unicode(self.plugin_path.text()) - if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'): - add_plugin(path) - self._plugin_model.populate() - self._plugin_model.reset() - else: - error_dialog(self, _('No valid plugin path'), - _('%s is not a valid plugin path')%path).exec_() - - def find_plugin(self): - path = choose_files(self, 'choose plugin dialog', _('Choose plugin'), - filters=[('Plugins', ['zip'])], all_files=False, - select_only_single_file=True) - if path: - self.plugin_path.setText(path[0]) - - def modify_plugin(self, op=''): - index = self.plugin_view.currentIndex() - if index.isValid(): - plugin = self._plugin_model.index_to_plugin(index) - if op == 'toggle': - if not plugin.can_be_disabled: - error_dialog(self,_('Plugin cannot be disabled'), - _('The plugin: %s cannot be disabled')%plugin.name).exec_() - return - if is_disabled(plugin): - enable_plugin(plugin) - else: - disable_plugin(plugin) - self._plugin_model.refresh_plugin(plugin) - if op == 'customize': - if not plugin.is_customizable(): - info_dialog(self, _('Plugin not customizable'), - _('Plugin: %s does not need customization')%plugin.name).exec_() - return - config_dialog = QDialog(self) - button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - v = QVBoxLayout(config_dialog) - - config_dialog.connect(button_box, SIGNAL('accepted()'), config_dialog.accept) - config_dialog.connect(button_box, SIGNAL('rejected()'), config_dialog.reject) - config_dialog.setWindowTitle(_('Customize') + ' ' + plugin.name) - - if hasattr(plugin, 'config_widget'): - config_widget = plugin.config_widget() - v.addWidget(config_widget) - v.addWidget(button_box) - config_dialog.exec_() - - if config_dialog.result() == QDialog.Accepted: - plugin.save_settings(config_widget) - self._plugin_model.refresh_plugin(plugin) - else: - help_text = plugin.customization_help(gui=True) - help_text = QLabel(help_text, config_dialog) - help_text.setWordWrap(True) - help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse - | Qt.LinksAccessibleByKeyboard) - help_text.setOpenExternalLinks(True) - v.addWidget(help_text) - sc = plugin_customization(plugin) - if not sc: - sc = '' - sc = sc.strip() - sc = QLineEdit(sc, config_dialog) - v.addWidget(sc) - v.addWidget(button_box) - config_dialog.exec_() - - if config_dialog.result() == QDialog.Accepted: - sc = unicode(sc.text()).strip() - customize_plugin(plugin, sc) - - self._plugin_model.refresh_plugin(plugin) - elif op == 'remove': - if remove_plugin(plugin): - self._plugin_model.populate() - self._plugin_model.reset() - else: - error_dialog(self, _('Cannot remove builtin plugin'), - plugin.name + _(' cannot be removed. It is a ' - 'builtin plugin. Try disabling it instead.')).exec_() - - def up_input(self): - idx = self.input_order.currentRow() - if idx > 0: - self.input_order.insertItem(idx-1, self.input_order.takeItem(idx)) - self.input_order.setCurrentRow(idx-1) - - def set_tweaks(self): - raw = unicode(self.current_tweaks.toPlainText()).encode('utf-8') - try: - exec raw - except: - import traceback - error_dialog(self, _('Invalid tweaks'), - _('The tweaks you entered are invalid, try resetting the' - ' tweaks to default and changing them one by one until' - ' you find the invalid setting.'), - det_msg=traceback.format_exc(), show=True) - return False - write_tweaks(raw) - return True - - def down_input(self): - idx = self.input_order.currentRow() - if idx < self.input_order.count()-1: - self.input_order.insertItem(idx+1, self.input_order.takeItem(idx)) - self.input_order.setCurrentRow(idx+1) - - # Column settings {{{ - def up_column(self): - idx = self.columns.currentRow() - if idx > 0: - self.columns.insertItem(idx-1, self.columns.takeItem(idx)) - self.columns.setCurrentRow(idx-1) - - def down_column(self): - idx = self.columns.currentRow() - if idx < self.columns.count()-1: - self.columns.insertItem(idx+1, self.columns.takeItem(idx)) - self.columns.setCurrentRow(idx+1) - - def del_custcol(self): - idx = self.columns.currentRow() - if idx < 0: - return error_dialog(self, '', _('You must select a column to delete it'), - show=True) - col = unicode(self.columns.item(idx).data(Qt.UserRole).toString()) - if col not in self.custcols: - return error_dialog(self, '', - _('The selected column is not a custom column'), show=True) - if not question_dialog(self, _('Are you sure?'), - _('Do you really want to delete column %s and all its data?') % - self.custcols[col]['name'], show_copy_button=False): - return - self.columns.item(idx).setCheckState(False) - self.columns.takeItem(idx) - self.custcols[col]['*deleteme'] = True - return - - def add_custcol(self): - CreateCustomColumn(self, False, self.model.orig_headers, ALL_COLUMNS) - - def edit_custcol(self): - CreateCustomColumn(self, True, self.model.orig_headers, ALL_COLUMNS) - - def apply_custom_column_changes(self): - config_cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString())\ - for i in range(self.columns.count())] - if not config_cols: - config_cols = ['title'] - removed_cols = set(self.model.column_map) - set(config_cols) - hidden_cols = set([unicode(self.columns.item(i).data(Qt.UserRole).toString())\ - for i in range(self.columns.count()) \ - if self.columns.item(i).checkState()==Qt.Unchecked]) - hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols - hidden_cols = list(hidden_cols.intersection(set(self.model.column_map))) - if 'ondevice' in hidden_cols: - hidden_cols.remove('ondevice') - def col_pos(x, y): - xidx = config_cols.index(x) if x in config_cols else sys.maxint - yidx = config_cols.index(y) if y in config_cols else sys.maxint - return cmp(xidx, yidx) - positions = {} - for i, col in enumerate((sorted(self.model.column_map, cmp=col_pos))): - positions[col] = i - state = {'hidden_columns': hidden_cols, 'column_positions':positions} - self.library_view.apply_state(state) - self.library_view.save_state() - - must_restart = False - for c in self.custcols: - if self.custcols[c]['colnum'] is None: - self.db.create_custom_column( - label=self.custcols[c]['label'], - name=self.custcols[c]['name'], - datatype=self.custcols[c]['datatype'], - is_multiple=self.custcols[c]['is_multiple'], - display = self.custcols[c]['display']) - must_restart = True - elif '*deleteme' in self.custcols[c]: - self.db.delete_custom_column(label=self.custcols[c]['label']) - must_restart = True - elif '*edited' in self.custcols[c]: - cc = self.custcols[c] - self.db.set_custom_column_metadata(cc['colnum'], name=cc['name'], - label=cc['label'], - display = self.custcols[c]['display']) - if '*must_restart' in self.custcols[c]: - must_restart = True - return must_restart - # }}} - - def view_server_logs(self): - from calibre.library.server import log_access_file, log_error_file - d = QDialog(self) - d.resize(QSize(800, 600)) - layout = QVBoxLayout() - d.setLayout(layout) - layout.addWidget(QLabel(_('Error log:'))) - el = QPlainTextEdit(d) - layout.addWidget(el) - try: - el.setPlainText(open(log_error_file, 'rb').read().decode('utf8', 'replace')) - except IOError: - el.setPlainText('No error log found') - layout.addWidget(QLabel(_('Access log:'))) - al = QPlainTextEdit(d) - layout.addWidget(al) - try: - al.setPlainText(open(log_access_file, 'rb').read().decode('utf8', 'replace')) - except IOError: - al.setPlainText('No access log found') - bx = QDialogButtonBox(QDialogButtonBox.Ok) - layout.addWidget(bx) - self.connect(bx, SIGNAL('accepted()'), d.accept) - d.show() - - def set_server_options(self): - c = server_config() - c.set('port', self.port.value()) - c.set('username', unicode(self.username.text()).strip()) - p = unicode(self.password.text()).strip() - if not p: - p = None - c.set('password', p) - - def start_server(self): - self.set_server_options() - from calibre.library.server.main import start_threaded_server - self.server = start_threaded_server(self.db, server_config().parse()) - while not self.server.is_running and self.server.exception is None: - time.sleep(1) - if self.server.exception is not None: - error_dialog(self, _('Failed to start content server'), - unicode(self.server.exception)).exec_() - return - self.start.setEnabled(False) - self.test.setEnabled(True) - self.stop.setEnabled(True) - - def stop_server(self): - from calibre.library.server.main import stop_threaded_server - stop_threaded_server(self.server) - self.server = None - self.start.setEnabled(True) - self.test.setEnabled(False) - self.stop.setEnabled(False) - - def test_server(self): - open_url(QUrl('http://127.0.0.1:'+str(self.port.value()))) - - def compact(self, toggled): - d = CheckIntegrity(self.db, self) - d.exec_() - - def accept(self): - mcs = unicode(self.max_cover_size.text()).strip() - if not re.match(r'\d+x\d+', mcs): - error_dialog(self, _('Invalid size'), - _('The size %s is invalid. must be of the form widthxheight')%mcs).exec_() - return - if not self.set_email_settings(): - return - if not self.conversion_options.commit(): - return - if not self.add_save.save_settings(): - return - if not self.set_tweaks(): - return - wl = self.opt_worker_limit.value() - if wl%2 != 0: - wl += 1 - self.toolbar_cm_widget.commit() - config['worker_limit'] = wl - - config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked()) - config['new_version_notification'] = bool(self.new_version_notification.isChecked()) - prefs['network_timeout'] = int(self.timeout.value()) - input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())] - prefs['input_format_order'] = input_cols - - must_restart = self.apply_custom_column_changes() - - config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked()) - config['disable_tray_notification'] = not self.systray_notifications.isChecked() - p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()] - prefs['worker_process_priority'] = p - nbt = [x.strip() for x in - unicode(self.new_book_tags.text()).strip().split(',')] - prefs['new_book_tags'] = [x for x in nbt if x] - prefs['output_format'] = unicode(self.output_format.currentText()).upper() - config['cover_flow_queue_length'] = self.cover_browse.value() - prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) - config['systray_icon'] = self.systray_icon.checkState() == Qt.Checked - config['autolaunch_server'] = self.auto_launch.isChecked() - sc = server_config() - sc.set('username', unicode(self.username.text()).strip()) - sc.set('password', unicode(self.password.text()).strip()) - 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() - config['show_avg_rating'] = self.show_avg_rating.isChecked() - config['get_social_metadata'] = self.opt_get_social_metadata.isChecked() - config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked() - config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked()) - config['disable_animations'] = bool(self.opt_disable_animations.isChecked()) - gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked()) - for x in ('toolbar_icon_size', 'toolbar_text'): - w = getattr(self, 'opt_'+x) - data = w.itemData(w.currentIndex()).toString() - gprefs[x] = unicode(data) - fmts = [] - for i in range(self.viewer.count()): - if self.viewer.item(i).checkState() == Qt.Checked: - fmts.append(str(self.viewer.item(i).text())) - config['internally_viewed_formats'] = fmts - val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString() - config['gui_layout'] = unicode(val) - for x in ('gui', 'cs'): - w = getattr(self, 'opt_%s_restriction'%x) - self.db.prefs.set(x+'_restriction', unicode(w.currentText())) - - if must_restart: - warning_dialog(self, _('Must restart'), - _('The changes you made require that Calibre be ' - 'restarted. Please restart as soon as practical.'), - show=True, show_copy_button=False) - self.parent.must_restart_before_config = True - QDialog.accept(self) - -class VacThread(QThread): - - def __init__(self, parent, db): - QThread.__init__(self, parent) - self.db = db - self._parent = parent - - def run(self): - err = bad = None - try: - bad = self.db.check_integrity(self.callback) - except: - import traceback - err = traceback.format_exc() - self.emit(SIGNAL('check_done(PyQt_PyObject, PyQt_PyObject)'), bad, err) - - def callback(self, progress, msg): - self.emit(SIGNAL('callback(PyQt_PyObject,PyQt_PyObject)'), progress, - msg) - -class CheckIntegrity(QProgressDialog): - - def __init__(self, db, parent=None): - QProgressDialog.__init__(self, parent) - self.db = db - self.setCancelButton(None) - self.setMinimum(0) - self.setMaximum(100) - self.setWindowTitle(_('Checking database integrity')) - self.setAutoReset(False) - self.setValue(0) - - self.vthread = VacThread(self, db) - self.connect(self.vthread, SIGNAL('check_done(PyQt_PyObject,PyQt_PyObject)'), - self.check_done, - Qt.QueuedConnection) - self.connect(self.vthread, - SIGNAL('callback(PyQt_PyObject,PyQt_PyObject)'), - self.callback, Qt.QueuedConnection) - self.vthread.start() - - def callback(self, progress, msg): - self.setLabelText(msg) - self.setValue(int(100*progress)) - - def check_done(self, bad, err): - if err: - error_dialog(self, _('Error'), - _('Failed to check database integrity'), - det_msg=err, show=True) - elif bad: - titles = [self.db.title(x, index_is_id=True) for x in bad] - det_msg = '\n'.join(titles) - warning_dialog(self, _('Some inconsistencies found'), - _('The following books had formats listed in the ' - 'database that are not actually available. ' - 'The entries for the formats have been removed. ' - 'You should check them manually. This can ' - 'happen if you manipulate the files in the ' - 'library folder directly.'), det_msg=det_msg, show=True) - self.reset() - - - -if __name__ == '__main__': - from calibre.library.database2 import LibraryDatabase2 - from PyQt4.Qt import QApplication - app = QApplication([]) - d=ConfigDialog(None, LibraryDatabase2('/tmp')) - d.show() - app.exec_() diff --git a/src/calibre/gui2/dialogs/config/add_save.py b/src/calibre/gui2/dialogs/config/add_save.py deleted file mode 100644 index 8eb6cf7bd0..0000000000 --- a/src/calibre/gui2/dialogs/config/add_save.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import with_statement - -__license__ = 'GPL v3' -__copyright__ = '2009, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -import textwrap - -from PyQt4.Qt import QTabWidget - -from calibre.gui2.dialogs.config.add_save_ui import Ui_TabWidget -from calibre.library.save_to_disk import config -from calibre.utils.config import prefs -from calibre.gui2.widgets import FilenamePattern - -class AddSave(QTabWidget, Ui_TabWidget): - - def __init__(self, parent=None): - QTabWidget.__init__(self, parent) - self.setupUi(self) - while self.count() > 3: - self.removeTab(3) - c = config() - opts = c.parse() - for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf', - 'replace_whitespace', 'to_lowercase'): - g = getattr(self, 'opt_'+x) - g.setChecked(getattr(opts, x)) - help = '\n'.join(textwrap.wrap(c.get_option(x).help, 75)) - g.setToolTip(help) - g.setWhatsThis(help) - - for x in ('formats', 'timefmt'): - g = getattr(self, 'opt_'+x) - g.setText(getattr(opts, x)) - help = '\n'.join(textwrap.wrap(c.get_option(x).help, 75)) - g.setToolTip(help) - g.setWhatsThis(help) - - - self.opt_read_metadata_from_filename.setChecked(not prefs['read_file_metadata']) - self.filename_pattern = FilenamePattern(self) - self.metadata_box.layout().insertWidget(0, self.filename_pattern) - self.opt_swap_author_names.setChecked(prefs['swap_author_names']) - self.opt_add_formats_to_existing.setChecked(prefs['add_formats_to_existing']) - if prefs['manage_device_metadata'] == 'manual': - self.manage_device_metadata.setCurrentIndex(0) - elif prefs['manage_device_metadata'] == 'on_send': - self.manage_device_metadata.setCurrentIndex(1) - else: - self.manage_device_metadata.setCurrentIndex(2) - help = '\n'.join(textwrap.wrap(c.get_option('template').help, 75)) - self.save_template.initialize('save_to_disk', opts.template, help) - self.send_template.initialize('send_to_device', opts.send_template, help) - - def validate(self): - return self.save_template.validate() and self.send_template.validate() - - def save_settings(self): - if not self.validate(): - return False - c = config() - for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf', - 'replace_whitespace', 'to_lowercase'): - c.set(x, getattr(self, 'opt_'+x).isChecked()) - for x in ('formats', 'timefmt'): - val = unicode(getattr(self, 'opt_'+x).text()).strip() - if x == 'formats' and not val: - val = 'all' - c.set(x, val) - self.save_template.save_settings(c, 'template') - self.send_template.save_settings(c, 'send_template') - prefs['read_file_metadata'] = not bool(self.opt_read_metadata_from_filename.isChecked()) - pattern = self.filename_pattern.commit() - prefs['filename_pattern'] = pattern - prefs['swap_author_names'] = bool(self.opt_swap_author_names.isChecked()) - prefs['add_formats_to_existing'] = bool(self.opt_add_formats_to_existing.isChecked()) - if self.manage_device_metadata.currentIndex() == 0: - prefs['manage_device_metadata'] = 'manual' - elif self.manage_device_metadata.currentIndex() == 1: - prefs['manage_device_metadata'] = 'on_send' - else: - prefs['manage_device_metadata'] = 'on_connect' - return True - -if __name__ == '__main__': - from PyQt4.Qt import QApplication - app=QApplication([]) - a = AddSave() - a.show() - app.exec_() - a.save_settings() - diff --git a/src/calibre/gui2/dialogs/config/add_save.ui b/src/calibre/gui2/dialogs/config/add_save.ui deleted file mode 100644 index c8ee0419a9..0000000000 --- a/src/calibre/gui2/dialogs/config/add_save.ui +++ /dev/null @@ -1,280 +0,0 @@ - - - TabWidget - - - - 0 - 0 - 671 - 516 - - - - TabWidget - - - 0 - - - - &Adding books - - - - - - Here you can control how calibre will read metadata from the files you add to it. calibre can either read metadata from the contents of the file, or from the filename. - - - true - - - - - - - Read metadata only from &file name - - - - - - - Swap the firstname and lastname of the author. This affects only metadata read from file names. - - - &Swap author firstname and lastname - - - - - - - If an existing book with a similar title and author is found that does not have the format being added, the format is added -to the existing book, instead of creating a new entry. If the existing book already has the format, then it is silently ignored. - -Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact. - - - If books with similar titles and authors found, &merge the new files automatically - - - - - - - &Configure metadata from file name - - - - - - Qt::Vertical - - - - 20 - 363 - - - - - - - - - - - - &Saving books - - - - - - Here you can control how calibre will save your books when you click the Save to Disk button: - - - true - - - - - - - Save &cover separately - - - - - - - Update &metadata in saved copies - - - - - - - Save metadata in &OPF file - - - - - - - Convert non-English characters to &English equivalents - - - - - - - Format &dates as: - - - opt_timefmt - - - - - - - - - - File &formats to save: - - - opt_formats - - - - - - - - - - Replace space with &underscores - - - - - - - Change paths to &lowercase - - - - - - - - - - - Sending to &device - - - - - - - 0 - 0 - - - - Metadata &management: - - - manage_device_metadata - - - - - - - - 0 - 0 - - - - - Manual management - - - - - Only on send - - - - - Automatic management - - - - - - - - Qt::Horizontal - - - - 313 - 20 - - - - - - - - <li><b>Manual Management</b>: Calibre updates the metadata and adds collections only when a book is sent. With this option, calibre will never remove a collection.</li> -<li><b>Only on send</b>: Calibre updates metadata and adds/removes collections for a book only when it is sent to the device. </li> -<li><b>Automatic management</b>: Calibre automatically keeps metadata on the device in sync with the calibre library, on every connect</li></ul> - - - true - - - - - - - - - - - - - - Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Plugins - - - true - - - - - - - - - - - - SaveTemplate - QWidget -

calibre/gui2/dialogs/config/save_template.h
- 1 - - - - - diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui deleted file mode 100644 index 3dc30cbf36..0000000000 --- a/src/calibre/gui2/dialogs/config/config.ui +++ /dev/null @@ -1,1398 +0,0 @@ - - - Kovid Goyal - Dialog - - - - 0 - 0 - 1001 - 730 - - - - Preferences - - - - :/images/config.png:/images/config.png - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - QAbstractItemView::NoEditTriggers - - - true - - - false - - - - 48 - 48 - - - - QAbstractItemView::ScrollPerItem - - - QAbstractItemView::ScrollPerPixel - - - QListView::TopToBottom - - - 20 - - - QListView::ListMode - - - - - - - - 1 - 0 - - - - QFrame::Plain - - - 0 - - - true - - - - - 0 - 0 - 725 - 683 - - - - - 0 - - - - - - 100 - 0 - - - - 0 - - - 0 - - - - - - - Show notification when &new version is available - - - - - - - Download &social metadata (tags/ratings/etc.) by default - - - - - - - &Overwrite author and title by default when fetching metadata - - - - - - - - - Default network &timeout: - - - timeout - - - - - - - Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information) - - - seconds - - - 2 - - - 120 - - - 5 - - - - - - - - - - Choose &language (requires restart): - - - language - - - - - - - - Normal - - - - - High - - - - - Low - - - - - - - - Job &priority: - - - priority - - - - - - - Preferred &output format: - - - output_format - - - - - - - - - - Tags to apply when adding a book: - - - new_book_tags - - - - - - - A comma-separated list of tags that will be applied to books added to the library - - - - - - - - - Reset all disabled &confirmation dialogs - - - - - - - Preferred &input format order: - - - - - - - - true - - - QAbstractItemView::SelectRows - - - - - - - - - ... - - - - :/images/arrow-up.png:/images/arrow-up.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - ... - - - - :/images/arrow-down.png:/images/arrow-down.png - - - - - - - - - - - - - - - - - - 0 - - - - &Miscellaneous - - - - - - User Interface &layout (needs restart): - - - opt_gui_layout - - - - - - - - 250 - 16777215 - - - - - - - - &Number of covers to show in browse mode (needs restart): - - - cover_browse - - - - - - - - - - Restriction to apply when the current library is opened: - - - opt_gui_restriction - - - - - - - - 250 - 16777215 - - - - Apply this restriction on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 15 - - - - - - - Disable all animations. Useful if you have a slow/old computer. - - - Disable &animations - - - - - - - &Toolbar - - - - - - - - - &Icon size: - - - opt_toolbar_icon_size - - - - - - - - - - Show &text under icons: - - - opt_toolbar_text - - - - - - - - - - &Delete news from library when it is automatically sent to reader - - - - - - - - - Select visible &columns in library view - - - - - - - - true - - - QAbstractItemView::SelectRows - - - - - - - - - ... - - - - :/images/arrow-up.png:/images/arrow-up.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Remove a user-defined column - - - ... - - - - :/images/minus.png:/images/minus.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Add a user-defined column - - - ... - - - - :/images/plus.png:/images/plus.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Edit settings of a user-defined column - - - ... - - - - :/images/edit_input.png:/images/edit_input.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - ... - - - - :/images/arrow-down.png:/images/arrow-down.png - - - - - - - - - - - - - - Use internal &viewer for: - - - - - - true - - - QAbstractItemView::NoSelection - - - - - - - - - - - - Search as you type - - - true - - - - - - - Use &Roman numerals for series - - - true - - - - - - - Enable system &tray icon (needs restart) - - - - - - - Show &average ratings in the tags browser - - - true - - - - - - - Automatically send downloaded &news to ebook reader - - - - - - - Show &splash screen at startup - - - - - - - Show cover &browser in a separate window (needs restart) - - - - - - - Show &notifications in system tray - - - - - - - - - - - - - - - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - - - - - - Add an email address to which to send books - - - &Add email - - - - :/images/plus.png:/images/plus.png - - - - 24 - 24 - - - - Qt::ToolButtonTextUnderIcon - - - - - - - Make &default - - - - - - - &Remove email - - - - :/images/minus.png:/images/minus.png - - - - 24 - 24 - - - - Qt::ToolButtonTextUnderIcon - - - - - - - - - - - calibre can send your books to you (or your reader) by email. Emails will be automatically sent for downloaded news to all email addresses that have Auto-send checked. - - - true - - - - - - - - - - - - - - - - - - - - - 0 - - - - &Miscellaneous - - - - - - &Maximum number of waiting worker processes (needs restart): - - - opt_worker_limit - - - - - - - 2 - - - 10000 - - - 2 - - - - - - - Limit the max. simultaneous jobs to the available CPU &cores - - - - - - - Qt::Vertical - - - - 20 - 79 - - - - - - - - Debug &device detection - - - - - - - Qt::Vertical - - - - 20 - 80 - - - - - - - - &Check database integrity - - - - - - - Qt::Vertical - - - - 20 - 79 - - - - - - - - Open calibre &configuration directory - - - - - - - Qt::Vertical - - - - 20 - 80 - - - - - - - - &Install command line tools - - - - - - - Qt::Vertical - - - - 20 - 79 - - - - - - - - - &Tweaks - - - - - - Values for the tweaks are shown below. Edit them to change the behavior of calibre - - - true - - - - - - - All available tweaks - - - - - - true - - - - - - - - - - &Current tweaks - - - - - - - - - - - - &Restore to defaults - - - - - - - - - - - - - - - calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart. - - - true - - - - - - - - - Server &port: - - - port - - - - - - - 65535 - - - 8080 - - - - - - - &Username: - - - username - - - - - - - - - - &Password: - - - password - - - - - - - If you leave the password blank, anyone will be able to access your book collection using the web interface. - - - - - - - The maximum size (widthxheight) for displayed covers. Larger covers are resized. - - - - - - - - - - Max. &cover size: - - - max_cover_size - - - - - - - &Show password - - - - - - - Max. &OPDS items per query: - - - opt_max_opds_items - - - - - - - 10 - - - 10000 - - - - - - - 25 - - - 1000000 - - - - - - - Max. OPDS &ungrouped items: - - - opt_max_opds_ungrouped_items - - - - - - - Restriction (saved search) to apply: - - - - - - - This restriction (based on a saved search) will restrict the books the content server makes available to those matching the search. This setting is per library (i.e. you can have a different restriction per library). - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 20 - - - - - - - - - - - &Start Server - - - - - - - St&op Server - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - &Test Server - - - - - - - - - Run server &automatically on startup - - - - - - - View &server logs - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - <p>Remember to leave calibre running as the server only runs as long as calibre is running. -<p>Stanza should see your calibre collection automatically. If not, try adding the URL http://myhostname:8080 as a new catalog in the Stanza reader on your iPhone. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Here you can customize the behavior of Calibre by controlling what plugins it uses. - - - true - - - - - - - true - - - - 32 - 32 - - - - true - - - true - - - true - - - - - - - - - Enable/&Disable plugin - - - - - - - &Customize plugin - - - - - - - &Remove plugin - - - - - - - - - Add new plugin - - - - - - - - Plugin &file: - - - plugin_path - - - - - - - - - - ... - - - - :/images/document_open.png:/images/document_open.png - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - &Add - - - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - SendEmail - QWidget -
calibre/gui2/wizard/send_email.h
- 1 -
- - AddSave - QTabWidget -
calibre/gui2/dialogs/config/add_save.h
- 1 -
-
- - - - - - buttonBox - accepted() - Dialog - accept() - - - 239 - 558 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 307 - 558 - - - 286 - 274 - - - - -
diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.py b/src/calibre/gui2/dialogs/config/create_custom_column.py deleted file mode 100644 index c0f17b8cba..0000000000 --- a/src/calibre/gui2/dialogs/config/create_custom_column.py +++ /dev/null @@ -1,173 +0,0 @@ -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal ' - -'''Dialog to create a new custom column''' - -import re -from functools import partial - -from PyQt4.QtCore import SIGNAL -from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant - -from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn -from calibre.gui2 import error_dialog - -class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): - - column_types = { - 0:{'datatype':'text', - 'text':_('Text, column shown in the tag browser'), - 'is_multiple':False}, - 1:{'datatype':'*text', - 'text':_('Comma separated text, like tags, shown in the tag browser'), - 'is_multiple':True}, - 2:{'datatype':'comments', - 'text':_('Long text, like comments, not shown in the tag browser'), - 'is_multiple':False}, - 3:{'datatype':'series', - 'text':_('Text column for keeping series-like information'), - 'is_multiple':False}, - 4:{'datatype':'datetime', - 'text':_('Date'), 'is_multiple':False}, - 5:{'datatype':'float', - 'text':_('Floating point numbers'), 'is_multiple':False}, - 6:{'datatype':'int', - 'text':_('Integers'), 'is_multiple':False}, - 7:{'datatype':'rating', - 'text':_('Ratings, shown with stars'), - 'is_multiple':False}, - 8:{'datatype':'bool', - 'text':_('Yes/No'), 'is_multiple':False}, - } - - def __init__(self, parent, editing, standard_colheads, standard_colnames): - QDialog.__init__(self, parent) - Ui_QCreateCustomColumn.__init__(self) - self.setupUi(self) - # Remove help icon on title bar - icon = self.windowIcon() - self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) - self.setWindowIcon(icon) - - self.simple_error = partial(error_dialog, self, show=True, - show_copy_button=False) - self.connect(self.button_box, SIGNAL("accepted()"), self.accept) - self.connect(self.button_box, SIGNAL("rejected()"), self.reject) - self.parent = parent - self.editing_col = editing - self.standard_colheads = standard_colheads - self.standard_colnames = standard_colnames - for t in self.column_types: - self.column_type_box.addItem(self.column_types[t]['text']) - self.column_type_box.currentIndexChanged.connect(self.datatype_changed) - if not self.editing_col: - self.datatype_changed() - self.exec_() - return - idx = parent.columns.currentRow() - if idx < 0: - self.simple_error(_('No column selected'), - _('No column has been selected')) - return - col = unicode(parent.columns.item(idx).data(Qt.UserRole).toString()) - if col not in parent.custcols: - self.simple_error('', _('Selected column is not a user-defined column')) - return - - c = parent.custcols[col] - self.column_name_box.setText(c['label']) - self.column_heading_box.setText(c['name']) - ct = c['datatype'] if not c['is_multiple'] else '*text' - self.orig_column_number = c['colnum'] - self.orig_column_name = col - column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types)) - self.column_type_box.setCurrentIndex(column_numbers[ct]) - self.column_type_box.setEnabled(False) - if ct == 'datetime': - if c['display'].get('date_format', None): - self.date_format_box.setText(c['display'].get('date_format', '')) - self.datatype_changed() - self.exec_() - - def datatype_changed(self, *args): - try: - col_type = self.column_types[self.column_type_box.currentIndex()]['datatype'] - except: - col_type = None - df_visible = col_type == 'datetime' - for x in ('box', 'default_label', 'label'): - getattr(self, 'date_format_'+x).setVisible(df_visible) - - - def accept(self): - col = unicode(self.column_name_box.text()) - if not col: - return self.simple_error('', _('No lookup name was provided')) - if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col: - return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter')) - if col.endswith('_index'): - return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.')) - col_heading = unicode(self.column_heading_box.text()) - col_type = self.column_types[self.column_type_box.currentIndex()]['datatype'] - if col_type == '*text': - col_type='text' - is_multiple = True - else: - is_multiple = False - if not col_heading: - return self.simple_error('', _('No column heading was provided')) - bad_col = False - if col in self.parent.custcols: - if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number: - bad_col = True - if bad_col: - return self.simple_error('', _('The lookup name %s is already used')%col) - bad_head = False - for t in self.parent.custcols: - if self.parent.custcols[t]['name'] == col_heading: - if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number: - bad_head = True - for t in self.standard_colheads: - if self.standard_colheads[t] == col_heading: - bad_head = True - if bad_head: - return self.simple_error('', _('The heading %s is already used')%col_heading) - - date_format = {} - if col_type == 'datetime': - if self.date_format_box.text(): - date_format = {'date_format':unicode(self.date_format_box.text())} - else: - date_format = {'date_format': None} - - key = self.parent.db.field_metadata.custom_field_prefix+col - if not self.editing_col: - self.parent.db.field_metadata - self.parent.custcols[key] = { - 'label':col, - 'name':col_heading, - 'datatype':col_type, - 'editable':True, - 'display':date_format, - 'normalized':None, - 'colnum':None, - 'is_multiple':is_multiple, - } - item = QListWidgetItem(col_heading, self.parent.columns) - item.setData(Qt.UserRole, QVariant(key)) - item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) - item.setCheckState(Qt.Checked) - else: - idx = self.parent.columns.currentRow() - item = self.parent.columns.item(idx) - item.setData(Qt.UserRole, QVariant(key)) - item.setText(col_heading) - self.parent.custcols[self.orig_column_name]['label'] = col - self.parent.custcols[self.orig_column_name]['name'] = col_heading - self.parent.custcols[self.orig_column_name]['display'].update(date_format) - self.parent.custcols[self.orig_column_name]['*edited'] = True - self.parent.custcols[self.orig_column_name]['*must_restart'] = True - QDialog.accept(self) - - def reject(self): - QDialog.reject(self) diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.ui b/src/calibre/gui2/dialogs/config/create_custom_column.ui deleted file mode 100644 index 5cb9494845..0000000000 --- a/src/calibre/gui2/dialogs/config/create_custom_column.ui +++ /dev/null @@ -1,191 +0,0 @@ - - - QCreateCustomColumn - - - Qt::ApplicationModal - - - - 0 - 0 - 528 - 199 - - - - - 0 - 0 - - - - Create or edit custom columns - - - - - - QLayout::SetDefaultConstraint - - - 5 - - - - - 0 - - - - - &Lookup name - - - column_name_box - - - - - - - Column &heading - - - column_heading_box - - - - - - - - 20 - 0 - - - - Used for searching the column. Must contain only digits and lower case letters. - - - - - - - Column heading in the library view and category name in the tag browser - - - - - - - Column &type - - - column_type_box - - - - - - - - 0 - 0 - - - - - 70 - 0 - - - - What kind of information will be kept in the column. - - - - - - - - - - 0 - 0 - - - - <p>Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.</p> -<p>For example: -<ul> -<li> ddd, d MMM yyyy gives Mon, 5 Jan 2010<li> -<li>dd MMMM yy gives 05 January 10</li> -</ul> - - - - - - - Use MMM yyyy for month + year, yyyy for year only - - - Default: dd MMM yyyy. - - - - - - - - - Format for &dates - - - date_format_box - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - true - - - - - - - - 75 - true - - - - Create or edit custom columns - - - - - - - - - column_name_box - column_heading_box - column_type_box - date_format_box - button_box - - - - diff --git a/src/calibre/gui2/dialogs/config/toolbar.py b/src/calibre/gui2/dialogs/config/toolbar.py deleted file mode 100644 index 62e249d073..0000000000 --- a/src/calibre/gui2/dialogs/config/toolbar.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -from functools import partial - -from PyQt4.Qt import QWidget, QAbstractListModel, Qt, QIcon, \ - QVariant, QItemSelectionModel - -from calibre.gui2.dialogs.config.toolbar_ui import Ui_Form -from calibre.gui2 import gprefs, NONE, warning_dialog - - -class FakeAction(object): - - def __init__(self, name, icon, tooltip=None, - dont_add_to=frozenset([]), dont_remove_from=frozenset([])): - self.name = name - self.action_spec = (name, icon, tooltip, None) - self.dont_remove_from = dont_remove_from - self.dont_add_to = dont_add_to - -class BaseModel(QAbstractListModel): - - def name_to_action(self, name, gui): - if name == 'Donate': - return FakeAction(name, 'donate.png', - dont_add_to=frozenset(['context-menu', - 'context-menu-device'])) - if name == 'Location Manager': - return FakeAction(name, None, - _('Switch between library and device views'), - dont_remove_from=set(['toolbar-device'])) - if name is None: - return FakeAction('--- '+_('Separator')+' ---', None) - return gui.iactions[name] - - def rowCount(self, parent): - return len(self._data) - - def data(self, index, role): - row = index.row() - action = self._data[row].action_spec - if role == Qt.DisplayRole: - text = action[0] - text = text.replace('&', '') - if text == _('%d books'): - text = _('Choose library') - return QVariant(text) - if role == Qt.DecorationRole: - ic = action[1] - if ic is None: - ic = 'blank.png' - return QVariant(QIcon(I(ic))) - if role == Qt.ToolTipRole and action[2] is not None: - return QVariant(action[2]) - return NONE - - def names(self, indexes): - rows = [i.row() for i in indexes] - ans = [] - for i in rows: - n = self._data[i].name - if n.startswith('---'): - n = None - ans.append(n) - return ans - - -class AllModel(BaseModel): - - def __init__(self, key, gui): - BaseModel.__init__(self) - self.gprefs_name = 'action-layout-'+key - current = gprefs[self.gprefs_name] - self.gui = gui - self.key = key - self._data = self.get_all_actions(current) - - def get_all_actions(self, current): - all = list(self.gui.iactions.keys()) + ['Donate'] - all = [x for x in all if x not in current] + [None] - all = [self.name_to_action(x, self.gui) for x in all] - all = [x for x in all if self.key not in x.dont_add_to] - all.sort() - return all - - def add(self, names): - actions = [] - for name in names: - if name is None or name.startswith('---'): continue - actions.append(self.name_to_action(name, self.gui)) - self._data.extend(actions) - self._data.sort() - self.reset() - - def remove(self, indices, allowed): - rows = [i.row() for i in indices] - remove = set([]) - for row in rows: - ac = self._data[row] - if ac.name.startswith('---'): continue - if ac.name in allowed: - remove.add(row) - ndata = [] - for i, ac in enumerate(self._data): - if i not in remove: - ndata.append(ac) - self._data = ndata - self.reset() - - def restore_defaults(self): - current = gprefs.defaults[self.gprefs_name] - self._data = self.get_all_actions(current) - self.reset() - -class CurrentModel(BaseModel): - - def __init__(self, key, gui): - BaseModel.__init__(self) - self.gprefs_name = 'action-layout-'+key - current = gprefs[self.gprefs_name] - self._data = [self.name_to_action(x, gui) for x in current] - self.key = key - self.gui = gui - - def move(self, idx, delta): - row = idx.row() - if row < 0 or row >= len(self._data): - return - nrow = row + delta - if nrow < 0 or nrow >= len(self._data): - return - t = self._data[row] - self._data[row] = self._data[nrow] - self._data[nrow] = t - ni = self.index(nrow) - self.dataChanged.emit(idx, idx) - self.dataChanged.emit(ni, ni) - return ni - - def add(self, names): - actions = [] - reject = set([]) - for name in names: - ac = self.name_to_action(name, self.gui) - if self.key in ac.dont_add_to: - reject.add(ac) - else: - actions.append(ac) - - self._data.extend(actions) - self.reset() - return reject - - def remove(self, indices): - rows = [i.row() for i in indices] - remove, rejected = set([]), set([]) - for row in rows: - ac = self._data[row] - if self.key in ac.dont_remove_from: - rejected.add(ac) - continue - remove.add(row) - ndata = [] - for i, ac in enumerate(self._data): - if i not in remove: - ndata.append(ac) - self._data = ndata - self.reset() - return rejected - - def commit(self): - old = gprefs[self.gprefs_name] - new = [] - for x in self._data: - n = x.name - if n.startswith('---'): - n = None - new.append(n) - new = tuple(new) - if new != old: - defaults = gprefs.defaults[self.gprefs_name] - if defaults == new: - del gprefs[self.gprefs_name] - else: - gprefs[self.gprefs_name] = new - - def restore_defaults(self): - current = gprefs.defaults[self.gprefs_name] - self._data = [self.name_to_action(x, self.gui) for x in current] - self.reset() - - -class ToolbarLayout(QWidget, Ui_Form): - - LOCATIONS = [ - ('toolbar', _('The main toolbar')), - ('toolbar-device', _('The main toolbar when a device is connected')), - ('context-menu', _('The context menu for the books in the ' - 'calibre library')), - ('context-menu-device', _('The context menu for the books on ' - 'the device')) - ] - - def __init__(self, gui, parent=None): - QWidget.__init__(self, parent) - self.setupUi(self) - - self.models = {} - for key, text in self.LOCATIONS: - self.what.addItem(text, key) - all_model = AllModel(key, gui) - current_model = CurrentModel(key, gui) - self.models[key] = (all_model, current_model) - self.what.setCurrentIndex(0) - self.what.currentIndexChanged[int].connect(self.what_changed) - self.what_changed(0) - - self.add_action_button.clicked.connect(self.add_action) - self.remove_action_button.clicked.connect(self.remove_action) - self.restore_defaults_button.clicked.connect(self.restore_defaults) - self.action_up_button.clicked.connect(partial(self.move, -1)) - self.action_down_button.clicked.connect(partial(self.move, 1)) - - def what_changed(self, idx): - key = unicode(self.what.itemData(idx).toString()) - self.all_actions.setModel(self.models[key][0]) - self.current_actions.setModel(self.models[key][1]) - - def add_action(self, *args): - x = self.all_actions.selectionModel().selectedIndexes() - names = self.all_actions.model().names(x) - if names: - not_added = self.current_actions.model().add(names) - ns = set([x.name for x in not_added]) - added = set(names) - ns - self.all_actions.model().remove(x, added) - if not_added: - warning_dialog(self, _('Cannot add'), - _('Cannot add the actions %s to this location') % - ','.join([a.action_spec[0] for a in not_added]), - show=True) - if added: - ca = self.current_actions - idx = ca.model().index(ca.model().rowCount(None)-1) - ca.scrollTo(idx) - - def remove_action(self, *args): - x = self.current_actions.selectionModel().selectedIndexes() - names = self.current_actions.model().names(x) - if names: - not_removed = self.current_actions.model().remove(x) - ns = set([x.name for x in not_removed]) - removed = set(names) - ns - self.all_actions.model().add(removed) - if not_removed: - warning_dialog(self, _('Cannot remove'), - _('Cannot remove the actions %s from this location') % - ','.join([a.action_spec[0] for a in not_removed]), - show=True) - - def move(self, delta, *args): - ci = self.current_actions.currentIndex() - m = self.current_actions.model() - if ci.isValid(): - ni = m.move(ci, delta) - if ni is not None: - self.current_actions.setCurrentIndex(ni) - self.current_actions.selectionModel().select(ni, - QItemSelectionModel.ClearAndSelect) - - def commit(self): - for am, cm in self.models.values(): - cm.commit() - - def restore_defaults(self, *args): - for am, cm in self.models.values(): - cm.restore_defaults() - am.restore_defaults() - - -if __name__ == '__main__': - from PyQt4.Qt import QApplication - from calibre.gui2.ui import Main - app=QApplication([]) - m = Main(None) - a = ToolbarLayout(m) - a.show() - app.exec_() - a.commit() - diff --git a/src/calibre/gui2/dialogs/config/toolbar.ui b/src/calibre/gui2/dialogs/config/toolbar.ui deleted file mode 100644 index 7ae98371ff..0000000000 --- a/src/calibre/gui2/dialogs/config/toolbar.ui +++ /dev/null @@ -1,223 +0,0 @@ - - - Form - - - - 0 - 0 - 831 - 553 - - - - Form - - - - - - Customize the actions in: - - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 20 - - - - - - - A&vailable actions - - - - - - QAbstractItemView::MultiSelection - - - - 32 - 32 - - - - 10 - - - true - - - - - - - - - - &Current actions - - - - - - QAbstractItemView::MultiSelection - - - - 32 - 32 - - - - 10 - - - true - - - - - - - - - Move selected action up - - - ... - - - - :/images/arrow-up.png:/images/arrow-up.png - - - - 24 - 24 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Move selected action down - - - ... - - - - :/images/arrow-down.png:/images/arrow-down.png - - - - 24 - 24 - - - - Ctrl+S - - - - - - - - - - - - - - Add selected actions to toolbar - - - ... - - - - :/images/forward.png:/images/forward.png - - - - 24 - 24 - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 40 - - - - - - - - Remove selected actions from toolbar - - - ... - - - - :/images/back.png:/images/back.png - - - - 24 - 24 - - - - - - - - - - Restore to &default - - - - - - - - - - diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 860785c49e..3d79b01c14 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -30,7 +30,7 @@ from calibre.ebooks.metadata import MetaInformation from calibre.utils.config import prefs, tweaks from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp from calibre.customize.ui import run_plugins_on_import, get_isbndb_key -from calibre.gui2.dialogs.config.social import SocialMetadata +from calibre.gui2.preferences.social import SocialMetadata from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre import strftime @@ -144,15 +144,23 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.cover_data = cover def generate_cover(self, *args): - from calibre.utils.magick.draw import create_cover_page, TextLine + from calibre.ebooks import calibre_cover + from calibre.ebooks.metadata import fmt_sidx + from calibre.gui2 import config title = unicode(self.title.text()).strip() author = unicode(self.authors.text()).strip() if not title or not author: return error_dialog(self, _('Specify title and author'), _('You must specify a title and author before generating ' 'a cover'), show=True) - lines = [TextLine(title, 44), TextLine(author, 32)] - self.cover_data = create_cover_page(lines, I('library.png')) + series = unicode(self.series.text()).strip() + series_string = None + if series: + series_string = _('Book %s of %s')%( + fmt_sidx(self.series_index.value(), + use_roman=config['use_roman_numerals_for_series_number']), series) + self.cover_data = calibre_cover(title, author, + series_string=series_string) pix = QPixmap() pix.loadFromData(self.cover_data) self.cover.setPixmap(pix) diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 4ea68f539d..7184192eba 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -646,7 +646,7 @@ - Download &cover + Download co&ver diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 51b17426b8..223efcf95b 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -32,6 +32,9 @@ class LibraryViewMixin(object): # {{{ def __init__(self, db): self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection) + self.library_view.add_column_signal.connect(partial(self.iactions['Preferences'].do_config, + initial_plugin=('Interface', 'Custom Columns')), + type=Qt.QueuedConnection) for func, args in [ ('connect_to_search_box', (self.search, self.search_done)), @@ -145,20 +148,23 @@ class StatusBar(QStatusBar): # {{{ self._font = QFont() self._font.setBold(True) self.setFont(self._font) + self.defmsg = QLabel(self.default_message) + self.defmsg.setFont(self._font) + self.addWidget(self.defmsg) def initialize(self, systray=None): self.systray = systray self.notifier = get_notifier(systray) - self.messageChanged.connect(self.message_changed, - type=Qt.QueuedConnection) - self.message_changed('') def device_connected(self, devname): self.device_string = _('Connected ') + devname + self.defmsg.setText(self.default_message + ' ..::.. ' + + self.device_string) self.clearMessage() def device_disconnected(self): self.device_string = '' + self.defmsg.setText(self.default_message) self.clearMessage() def new_version_available(self, ver, url): @@ -188,15 +194,6 @@ class StatusBar(QStatusBar): # {{{ def clear_message(self): self.clearMessage() - def message_changed(self, msg): - if not msg or msg.isEmpty() or msg.isNull() or \ - not unicode(msg).strip(): - extra = '' - if self.device_string: - extra = ' ..::.. ' + self.device_string - self.showMessage(self.default_message + extra) - - # }}} class LayoutMixin(object): # {{{ @@ -258,7 +255,9 @@ class LayoutMixin(object): # {{{ getattr(self, x+'_view').save_state() for x in ('cb', 'tb', 'bd'): - getattr(self, x+'_splitter').save_state() + s = getattr(self, x+'_splitter') + s.update_desired_state() + s.save_state() def read_layout_settings(self): # View states are restored automatically when set_database is called diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 68f093da95..58d5267c8e 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -215,6 +215,7 @@ class ToolBar(QToolBar): # {{{ self.location_manager.locations_changed.connect(self.build_bar) donate.setAutoRaise(True) donate.setCursor(Qt.PointingHandCursor) + self.added_actions = [] self.build_bar() self.preferred_width = self.sizeHint().width() @@ -237,7 +238,13 @@ class ToolBar(QToolBar): # {{{ actions = '-device' if showing_device else '' actions = gprefs['action-layout-toolbar'+actions] + for ac in self.added_actions: + m = ac.menu() + if m is not None: + m.setVisible(False) + self.clear() + self.added_actions = [] for what in actions: if what is None: @@ -245,6 +252,7 @@ class ToolBar(QToolBar): # {{{ elif what == 'Location Manager': for ac in self.location_manager.available_actions: self.addAction(ac) + self.added_actions.append(ac) self.setup_tool_button(ac, QToolButton.MenuButtonPopup) elif what == 'Donate': self.d_widget = QWidget() @@ -255,6 +263,7 @@ class ToolBar(QToolBar): # {{{ elif what in self.gui.iactions: action = self.gui.iactions[what] self.addAction(action.qaction) + self.added_actions.append(action.qaction) self.setup_tool_button(action.qaction, action.popup_type) def setup_tool_button(self, ac, menu_mode=None): diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index ae9487f801..bb47508531 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -121,10 +121,11 @@ class BooksModel(QAbstractTableModel): # {{{ def set_device_connected(self, is_connected): self.device_connected = is_connected self.db.refresh_ondevice() + self.refresh() + self.research() if is_connected and self.sorted_on[0] == 'ondevice': self.resort() - def set_book_on_device_func(self, func): self.book_on_device = func diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 966180467c..d67d286aeb 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -9,7 +9,7 @@ import os from functools import partial from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \ - QModelIndex + QModelIndex, QIcon from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ @@ -23,6 +23,7 @@ from calibre.gui2.library import DEFAULT_SORT class BooksView(QTableView): # {{{ files_dropped = pyqtSignal(object) + add_column_signal = pyqtSignal() def __init__(self, parent, modelcls=BooksModel): QTableView.__init__(self, parent) @@ -54,6 +55,7 @@ class BooksView(QTableView): # {{{ self.selectionModel().currentRowChanged.connect(self._model.current_changed) # {{{ Column Header setup + self.can_add_columns = True self.was_restored = False self.column_header = self.horizontalHeader() self.column_header.setMovable(True) @@ -93,6 +95,8 @@ class BooksView(QTableView): # {{{ self.sortByColumn(idx, Qt.DescendingOrder) elif action == 'defaults': self.apply_state(self.get_default_state()) + elif action == 'addcustcol': + self.add_column_signal.emit() elif action.startswith('align_'): alignment = action.partition('_')[-1] self._model.change_alignment(column, alignment) @@ -166,6 +170,13 @@ class BooksView(QTableView): # {{{ partial(self.column_header_context_handler, action='defaults', column=col)) + if self.can_add_columns: + self.column_header_context_menu.addAction( + QIcon(I('column.png')), + _('Add your own columns'), + partial(self.column_header_context_handler, + action='addcustcol', column=col)) + self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos)) # }}} @@ -494,6 +505,7 @@ class DeviceBooksView(BooksView): # {{{ def __init__(self, parent): BooksView.__init__(self, parent, DeviceBooksModel) + self.can_add_columns = False self.columns_resized = False self.resize_on_select = False self.rating_delegate = None diff --git a/src/calibre/gui2/preferences/__init__.py b/src/calibre/gui2/preferences/__init__.py index fd26344a2e..7267716ea8 100644 --- a/src/calibre/gui2/preferences/__init__.py +++ b/src/calibre/gui2/preferences/__init__.py @@ -5,25 +5,61 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import textwrap + from PyQt4.Qt import QWidget, pyqtSignal, QCheckBox, QAbstractSpinBox, \ QLineEdit, QComboBox, QVariant from calibre.customize.ui import preferences_plugins +from calibre.utils.config import ConfigProxy class AbortCommit(Exception): pass class ConfigWidgetInterface(object): + ''' + This class defines the interface that all widgets displayed in the + Preferences dialog must implement. See :class:`ConfigWidgetBase` for + a base class that implements this interface and defines various conveninece + methods as well. + ''' + + #: This signal must be emitted whenever the user changes a value in this + #: widget changed_signal = None + #: Set to True iff the :meth:`restore_to_defaults` method is implemented. + supports_restoring_to_defaults = True + + #: The tooltip for the Restore to defaults button + restore_defaults_desc = _('Restore settings to default values. ' + 'You have to click Apply to actually save the default settings.') + + #: If True the Preferences dialog will not allow the user to set any more + #: preferences. Only has effect if :meth:`commit` returns True. + restart_critical = False + def genesis(self, gui): + ''' + Called once before the widget is displayed, should perform any + necessary setup. + + :param gui: The main calibre graphical user interface + ''' raise NotImplementedError() def initialize(self): + ''' + Should set all config values to their initial values (the values + stored in the config files). + ''' raise NotImplementedError() def restore_defaults(self): + ''' + Should set all config values to their defaults. + ''' pass def commit(self): @@ -31,10 +67,19 @@ class ConfigWidgetInterface(object): Save any changed settings. Return True if the changes require a restart, False otherwise. Raise an :class:`AbortCommit` exception to indicate that an error occurred. You are responsible for giving the - suer feedback about what the error is and how to correct it. + user feedback about what the error is and how to correct it. ''' return False + def refresh_gui(self, gui): + ''' + Called once after this widget is committed. Responsible for causing the + gui to reread any changed settings. Note that by default the GUI + re-initializes various elements anyway, so most widgets won't need to + use this method. + ''' + pass + class Setting(object): def __init__(self, name, config_obj, widget, gui_name=None, @@ -65,6 +110,20 @@ class Setting(object): else: raise ValueError('Unknown data type') + if isinstance(self.config_obj, ConfigProxy) and \ + not unicode(self.gui_obj.toolTip()): + h = self.config_obj.help(self.name) + if h: + self.gui_obj.setToolTip(h) + tt = unicode(self.gui_obj.toolTip()) + if tt: + if not unicode(self.gui_obj.whatsThis()): + self.gui_obj.setWhatsThis(tt) + if not unicode(self.gui_obj.statusTip()): + self.gui_obj.setStatusTip(tt) + tt = '\n'.join(textwrap.wrap(tt, 70)) + self.gui_obj.setToolTip(tt) + def changed(self, *args): self.widget.changed_signal.emit() @@ -120,7 +179,7 @@ class Setting(object): elif self.datatype == 'number': val = self.gui_obj.value() elif self.datatype == 'string': - val = unicode(self.gui_name.text()).strip() + val = unicode(self.gui_obj.text()).strip() if self.empty_string_is_None and not val: val = None elif self.datatype == 'choice': @@ -147,7 +206,23 @@ class CommaSeparatedList(Setting): class ConfigWidgetBase(QWidget, ConfigWidgetInterface): + ''' + Base class that contains code to easily add standard config widgets like + checkboxes, combo boxes, text fields and so on. See the :meth:`register` + method. + + This class automatically handles change notification, resetting to default, + translation between gui objects and config objects, etc. for registered + settings. + + If your config widget inherits from this class but includes setting that + are not registered, you should override the :class:`ConfigWidgetInterface` methods + and call the base class methods inside the overrides. + ''' + changed_signal = pyqtSignal() + supports_restoring_to_defaults = True + restart_critical = False def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -156,9 +231,25 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface): self.settings = {} def register(self, name, config_obj, gui_name=None, choices=None, - restart_required=False, setting=Setting): + restart_required=False, empty_string_is_None=True, setting=Setting): + ''' + Register a setting. + + :param name: The setting name + :param config: The config object that reads/writes the setting + :param gui_name: The name of the GUI object that presents an interface + to change the setting. By default it is assumed to be + ``'opt_' + name``. + :param choices: If this setting is a multiple choice (combobox) based + setting, the list of choices. The list is a list of two + element tuples of the form: ``[(gui name, value), ...]`` + :param setting: The class responsible for managing this setting. The + default class handles almost all cases, so this param + is rarely used. + ''' setting = setting(name, config_obj, self, gui_name=gui_name, - choices=choices, restart_required=restart_required) + choices=choices, restart_required=restart_required, + empty_string_is_None=empty_string_is_None) return self.register_setting(setting) def register_setting(self, setting): @@ -182,7 +273,6 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface): setting.restore_defaults() - def get_plugin(category, name): for plugin in preferences_plugins(): if plugin.category == category and plugin.name == name: @@ -191,13 +281,27 @@ def get_plugin(category, name): 'No Preferences Plugin with category: %s and name: %s found' % (category, name)) -def test_widget(category, name, gui=None): # {{{ +# Testing {{{ + +def init_gui(): + from calibre.gui2.ui import Main + from calibre.gui2.main import option_parser + from calibre.library import db + parser = option_parser() + opts, args = parser.parse_args([]) + actions = tuple(Main.create_application_menubar()) + db = db() + gui = Main(opts) + gui.initialize(db.library_path, db, None, actions, show_gui=False) + return gui + +def test_widget(category, name, gui=None): from PyQt4.Qt import QDialog, QVBoxLayout, QDialogButtonBox class Dialog(QDialog): def set_widget(self, w): self.w = w def accept(self): try: - self.w.commit() + self.restart_required = self.w.commit() except AbortCommit: return QDialog.accept(self) @@ -213,6 +317,7 @@ def test_widget(category, name, gui=None): # {{{ w = pl.create_widget(d) d.set_widget(w) bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults) + bb.button(bb.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults) bb.button(bb.Apply).setEnabled(False) bb.button(bb.Apply).clicked.connect(d.accept) w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True)) @@ -222,24 +327,28 @@ def test_widget(category, name, gui=None): # {{{ l.addWidget(bb) mygui = gui is None if gui is None: - from calibre.gui2.ui import Main - from calibre.gui2.main import option_parser - from calibre.library import db - parser = option_parser() - opts, args = parser.parse_args([]) - actions = tuple(Main.create_application_menubar()) - db = db() - gui = Main(opts) - gui.initialize(db.library_path, db, None, actions, show_gui=False) + gui = init_gui() + mygui = True w.genesis(gui) w.initialize() - restart_required = False - if d.exec_() == QDialog.Accepted: - restart_required = w.commit() - if restart_required: + d.exec_() + if getattr(d, 'restart_required', False): from calibre.gui2 import warning_dialog warning_dialog(gui, 'Restart required', 'Restart required', show=True) if mygui: gui.shutdown() + +def test_all(): + from PyQt4.Qt import QApplication + app = QApplication([]) + app + gui = init_gui() + for plugin in preferences_plugins(): + test_widget(plugin.category, plugin.name, gui=gui) + gui.shutdown() + +if __name__ == '__main__': + test_all() # }}} + diff --git a/src/calibre/gui2/preferences/columns.py b/src/calibre/gui2/preferences/columns.py index 973e214c2f..c1b9230f42 100644 --- a/src/calibre/gui2/preferences/columns.py +++ b/src/calibre/gui2/preferences/columns.py @@ -16,6 +16,8 @@ from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS class ConfigWidget(ConfigWidgetBase, Ui_Form): + restart_critical = True + def genesis(self, gui): self.gui = gui db = self.gui.library_view.model().db @@ -103,7 +105,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): return self.opt_columns.item(idx).setCheckState(False) self.opt_columns.takeItem(idx) - self.custcols[col]['*deleteme'] = True + if self.custcols[col]['colnum'] is None: + del self.custcols[col] # A newly-added column was deleted + else: + self.custcols[col]['*deleteme'] = True self.changed_signal.emit() def add_custcol(self): diff --git a/src/calibre/gui2/preferences/conversion.py b/src/calibre/gui2/preferences/conversion.py index 2950f58c02..0063d4a341 100644 --- a/src/calibre/gui2/preferences/conversion.py +++ b/src/calibre/gui2/preferences/conversion.py @@ -34,6 +34,10 @@ class Model(QStringListModel): class Base(ConfigWidgetBase, Ui_Form): + restore_defaults_desc = _('Restore settings to default values. ' + 'Only settings for the currently selected section ' + 'are restored.') + def genesis(self, gui): log = Log() log.outputs = [] @@ -108,6 +112,6 @@ if __name__ == '__main__': from PyQt4.Qt import QApplication app = QApplication([]) #test_widget('Conversion', 'Input Options') - #test_widget('Conversion', 'Common Options') - test_widget('Conversion', 'Output Options') + test_widget('Conversion', 'Common Options') + #test_widget('Conversion', 'Output Options') diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 9cad1293a9..e8ab8707e2 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -161,7 +161,6 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): else: idx = self.parent.opt_columns.currentRow() item = self.parent.opt_columns.item(idx) - item.setData(Qt.UserRole, QVariant(key)) item.setText(col_heading) self.parent.custcols[self.orig_column_name]['label'] = col self.parent.custcols[self.orig_column_name]['name'] = col_heading diff --git a/src/calibre/gui2/dialogs/config/device_debug.py b/src/calibre/gui2/preferences/device_debug.py similarity index 100% rename from src/calibre/gui2/dialogs/config/device_debug.py rename to src/calibre/gui2/preferences/device_debug.py diff --git a/src/calibre/gui2/preferences/email.ui b/src/calibre/gui2/preferences/email.ui new file mode 100644 index 0000000000..2ffeb756d9 --- /dev/null +++ b/src/calibre/gui2/preferences/email.ui @@ -0,0 +1,112 @@ + + + Form + + + + 0 + 0 + 701 + 494 + + + + Form + + + + + + calibre can send your books to you (or your reader) by email. Emails will be automatically sent for downloaded news to all email addresses that have Auto-send checked. + + + true + + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + + + + + + Add an email address to which to send books + + + &Add email + + + + :/images/plus.png:/images/plus.png + + + + 24 + 24 + + + + Qt::ToolButtonTextUnderIcon + + + + + + + Make &default + + + + + + + &Remove email + + + + :/images/minus.png:/images/minus.png + + + + 24 + 24 + + + + Qt::ToolButtonTextUnderIcon + + + + + + + + + + + + + + + SendEmail + QWidget +
calibre/gui2/wizard/send_email.h
+ 1 +
+
+ + + + +
diff --git a/src/calibre/gui2/preferences/emailp.py b/src/calibre/gui2/preferences/emailp.py new file mode 100644 index 0000000000..f0b079e209 --- /dev/null +++ b/src/calibre/gui2/preferences/emailp.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QAbstractTableModel, QVariant, QFont, Qt + + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ + AbortCommit +from calibre.gui2.preferences.email_ui import Ui_Form +from calibre.utils.config import ConfigProxy +from calibre.gui2 import NONE +from calibre.utils.smtp import config as smtp_prefs + +class EmailAccounts(QAbstractTableModel): # {{{ + + def __init__(self, accounts): + QAbstractTableModel.__init__(self) + self.accounts = accounts + self.account_order = sorted(self.accounts.keys()) + self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')]) + self.default_font = QFont() + self.default_font.setBold(True) + self.default_font = QVariant(self.default_font) + self.tooltips =[NONE] + map(QVariant, + [_('Formats to email. The first matching format will be sent.'), + '

'+_('If checked, downloaded news will be automatically ' + 'mailed
to this email address ' + '(provided it is in one of the listed formats).')]) + + def rowCount(self, *args): + return len(self.account_order) + + def columnCount(self, *args): + return 3 + + def headerData(self, section, orientation, role): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.headers[section] + return NONE + + def data(self, index, role): + row, col = index.row(), index.column() + if row < 0 or row >= self.rowCount(): + return NONE + account = self.account_order[row] + if role == Qt.UserRole: + return (account, self.accounts[account]) + if role == Qt.ToolTipRole: + return self.tooltips[col] + if role in [Qt.DisplayRole, Qt.EditRole]: + if col == 0: + return QVariant(account) + if col == 1: + return QVariant(self.accounts[account][0]) + if role == Qt.FontRole and self.accounts[account][2]: + return self.default_font + if role == Qt.CheckStateRole and col == 2: + return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked) + return NONE + + def flags(self, index): + if index.column() == 2: + return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable + else: + return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable + + def setData(self, index, value, role): + if not index.isValid(): + return False + row, col = index.row(), index.column() + account = self.account_order[row] + if col == 2: + self.accounts[account][1] ^= True + elif col == 1: + self.accounts[account][0] = unicode(value.toString()).upper() + else: + na = unicode(value.toString()) + from email.utils import parseaddr + addr = parseaddr(na)[-1] + if not addr: + return False + self.accounts[na] = self.accounts.pop(account) + self.account_order[row] = na + if '@kindle.com' in addr: + self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1' + + self.dataChanged.emit( + self.index(index.row(), 0), self.index(index.row(), 2)) + return True + + def make_default(self, index): + if index.isValid(): + row = index.row() + for x in self.accounts.values(): + x[2] = False + self.accounts[self.account_order[row]][2] = True + self.reset() + + def add(self): + x = _('new email address') + y = x + c = 0 + while y in self.accounts: + c += 1 + y = x + str(c) + auto_send = len(self.accounts) < 1 + self.accounts[y] = ['MOBI, EPUB', auto_send, + len(self.account_order) == 0] + self.account_order = sorted(self.accounts.keys()) + self.reset() + return self.index(self.account_order.index(y), 0) + + def remove(self, index): + if index.isValid(): + row = index.row() + account = self.account_order[row] + self.accounts.pop(account) + self.account_order = sorted(self.accounts.keys()) + has_default = False + for account in self.account_order: + if self.accounts[account][2]: + has_default = True + break + if not has_default and self.account_order: + self.accounts[self.account_order[0]][2] = True + + self.reset() + +# }}} + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + supports_restoring_to_defaults = False + + def genesis(self, gui): + self.gui = gui + self.proxy = ConfigProxy(smtp_prefs()) + + self.send_email_widget.initialize(self.preferred_to_address) + self.send_email_widget.changed_signal.connect(self.changed_signal.emit) + opts = self.send_email_widget.smtp_opts + self._email_accounts = EmailAccounts(opts.accounts) + self._email_accounts.dataChanged.connect(lambda x,y: + self.changed_signal.emit()) + self.email_view.setModel(self._email_accounts) + + self.email_add.clicked.connect(self.add_email_account) + self.email_make_default.clicked.connect(self.make_default) + self.email_view.resizeColumnsToContents() + self.email_remove.clicked.connect(self.remove_email_account) + + def preferred_to_address(self): + if self._email_accounts.account_order: + return self._email_accounts.account_order[0] + + def initialize(self): + ConfigWidgetBase.initialize(self) + # Initializing all done in genesis + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + # No defaults to restore to + + def commit(self): + to_set = bool(self._email_accounts.accounts) + if not self.send_email_widget.set_email_settings(to_set): + raise AbortCommit('abort') + self.proxy['accounts'] = self._email_accounts.accounts + return ConfigWidgetBase.commit(self) + + def make_default(self, *args): + self._email_accounts.make_default(self.email_view.currentIndex()) + self.changed_signal.emit() + + def add_email_account(self, *args): + index = self._email_accounts.add() + self.email_view.setCurrentIndex(index) + self.email_view.resizeColumnsToContents() + self.email_view.edit(index) + self.changed_signal.emit() + + def remove_email_account(self, *args): + idx = self.email_view.currentIndex() + self._email_accounts.remove(idx) + self.changed_signal.emit() + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Sharing', 'Email') + diff --git a/src/calibre/gui2/dialogs/config/history.py b/src/calibre/gui2/preferences/history.py similarity index 70% rename from src/calibre/gui2/dialogs/config/history.py rename to src/calibre/gui2/preferences/history.py index 74b88a4380..c1c68642f6 100644 --- a/src/calibre/gui2/dialogs/config/history.py +++ b/src/calibre/gui2/preferences/history.py @@ -6,6 +6,8 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import textwrap + from PyQt4.Qt import QComboBox, QStringList, Qt from calibre.gui2 import config as gui_conf @@ -17,15 +19,22 @@ class HistoryBox(QComboBox): self.setEditable(True) def initialize(self, opt_name, default, help=None): - history = gui_conf[opt_name] - if default not in history: - history.append(default) - self.addItems(QStringList(history)) - self.setCurrentIndex(self.findText(default, Qt.MatchFixedString)) - if help is not None: + self.opt_name = opt_name + self.set_value(default) + if help: + self.setStatusTip(help) + help = '\n'.join(textwrap.wrap(help)) self.setToolTip(help) self.setWhatsThis(help) + def set_value(self, val): + history = gui_conf[self.opt_name] + if val not in history: + history.append(val) + self.clear() + self.addItems(QStringList(history)) + self.setCurrentIndex(self.findText(val, Qt.MatchFixedString)) + def save_history(self, opt_name): history = [unicode(self.itemText(i)) for i in range(self.count())] ct = self.text() diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index a0805e8915..f30b2fddbb 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -23,7 +23,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('gui_layout', config, restart_required=True, choices= [(_('Wide'), 'wide'), (_('Narrow'), 'narrow')]) - r('cover_flow_queue_length', config) + r('cover_flow_queue_length', config, restart_required=True) lang = get_lang() if lang is None or lang not in available_translations(): @@ -55,6 +55,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): (_('Never'), 'never')] r('toolbar_text', gprefs, choices=choices) + def refresh_gui(self, gui): + gui.search.search_as_you_type(config['search_as_you_type']) + + if __name__ == '__main__': from PyQt4.Qt import QApplication app = QApplication([]) diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py new file mode 100644 index 0000000000..6653fe2b67 --- /dev/null +++ b/src/calibre/gui2/preferences/main.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import textwrap +from functools import partial + +from PyQt4.Qt import QMainWindow, Qt, QIcon, QStatusBar, QFont, QWidget, \ + QScrollArea, QStackedWidget, QVBoxLayout, QLabel, QFrame, QKeySequence, \ + QToolBar, QSize, pyqtSignal, QPixmap, QToolButton, QAction, \ + QDialogButtonBox, QHBoxLayout + +from calibre.constants import __appname__, __version__, islinux +from calibre.gui2 import gprefs, min_available_height, available_width, \ + warning_dialog +from calibre.gui2.preferences import init_gui, AbortCommit, get_plugin +from calibre.customize.ui import preferences_plugins +from calibre.utils.ordered_dict import OrderedDict + +ICON_SIZE = 32 + +class StatusBar(QStatusBar): # {{{ + + def __init__(self, parent=None): + QStatusBar.__init__(self, parent) + self.default_message = __appname__ + ' ' + _('version') + ' ' + \ + __version__ + ' ' + _('created by Kovid Goyal') + self.device_string = '' + self._font = QFont() + self._font.setBold(True) + self.setFont(self._font) + + self.w = QLabel(self.default_message) + self.w.setFont(self._font) + self.addWidget(self.w) + +# }}} + +class BarTitle(QWidget): # {{{ + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self._layout = QHBoxLayout() + self.setLayout(self._layout) + self._layout.addStretch(10) + self.icon = QLabel('') + self._layout.addWidget(self.icon) + self.title = QLabel('') + self.title.setStyleSheet('QLabel { font-weight: bold }') + self.title.setAlignment(Qt.AlignLeft | Qt.AlignCenter) + self._layout.addWidget(self.title) + self._layout.addStretch(10) + + def show_plugin(self, plugin): + self.pmap = QPixmap(plugin.icon).scaled(ICON_SIZE, ICON_SIZE, + Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.icon.setPixmap(self.pmap) + self.title.setText(plugin.gui_name) + tt = plugin.description + self.setStatusTip(tt) + tt = textwrap.fill(tt) + self.setToolTip(tt) + self.setWhatsThis(tt) + +# }}} + +class Category(QWidget): # {{{ + + plugin_activated = pyqtSignal(object) + + def __init__(self, name, plugins, parent=None): + QWidget.__init__(self, parent) + self._layout = QVBoxLayout() + self.setLayout(self._layout) + self.label = QLabel(name) + self.sep = QFrame(self) + self.bf = QFont() + self.bf.setBold(True) + self.label.setFont(self.bf) + self.sep.setFrameShape(QFrame.HLine) + self._layout.addWidget(self.label) + self._layout.addWidget(self.sep) + + self.plugins = plugins + + self.bar = QToolBar(self) + self.bar.setIconSize(QSize(48, 48)) + self.bar.setMovable(False) + self.bar.setFloatable(False) + self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + self._layout.addWidget(self.bar) + self.actions = [] + for p in plugins: + target = partial(self.triggered, p) + ac = self.bar.addAction(QIcon(p.icon), p.gui_name, target) + ac.setToolTip(textwrap.fill(p.description)) + ac.setWhatsThis(textwrap.fill(p.description)) + ac.setStatusTip(p.description) + self.actions.append(ac) + w = self.bar.widgetForAction(ac) + w.setStyleSheet('QToolButton { margin-right: 20px; min-width: 100px }') + w.setCursor(Qt.PointingHandCursor) + w.setAutoRaise(True) + + def triggered(self, plugin, *args): + self.plugin_activated.emit(plugin) + +# }}} + +class Browser(QScrollArea): # {{{ + + show_plugin = pyqtSignal(object) + + def __init__(self, parent=None): + QScrollArea.__init__(self, parent) + self.setWidgetResizable(True) + + category_map = {} + for plugin in preferences_plugins(): + if plugin.category not in category_map: + category_map[plugin.category] = plugin.category_order + if category_map[plugin.category] < plugin.category_order: + category_map[plugin.category] = plugin.category_order + + categories = list(category_map.keys()) + categories.sort(cmp=lambda x, y: cmp(category_map[x], category_map[y])) + + self.category_map = OrderedDict() + for c in categories: + self.category_map[c] = [] + + for plugin in preferences_plugins(): + self.category_map[plugin.category].append(plugin) + + for plugins in self.category_map.values(): + plugins.sort(cmp=lambda x, y: cmp(x.name_order, y.name_order)) + + self.widgets = [] + self._layout = QVBoxLayout() + self.container = QWidget(self) + self.container.setLayout(self._layout) + self.setWidget(self.container) + + for name, plugins in self.category_map.items(): + w = Category(name, plugins, self) + self.widgets.append(w) + self._layout.addWidget(w) + w.plugin_activated.connect(self.show_plugin.emit) + + +# }}} + +class Preferences(QMainWindow): + + def __init__(self, gui, initial_plugin=None): + QMainWindow.__init__(self, gui) + self.gui = gui + self.must_restart = False + self.committed = False + + self.resize(900, 720) + nh, nw = min_available_height()-25, available_width()-10 + if nh < 0: + nh = 800 + if nw < 0: + nw = 600 + nh = min(self.height(), nh) + nw = min(self.width(), nw) + self.resize(nw, nh) + self.esc_action = QAction(self) + self.addAction(self.esc_action) + self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape)) + self.esc_action.triggered.connect(self.esc) + + geom = gprefs.get('preferences_window_geometry', None) + if geom is not None: + self.restoreGeometry(geom) + + # Center + if islinux: + self.move(gui.rect().center() - self.rect().center()) + + self.setWindowModality(Qt.WindowModal) + self.setWindowTitle(__appname__ + ' - ' + _('Preferences')) + self.setWindowIcon(QIcon(I('config.png'))) + + self.status_bar = StatusBar(self) + self.setStatusBar(self.status_bar) + + self.stack = QStackedWidget(self) + self.cw = QWidget(self) + self.cw.setLayout(QVBoxLayout()) + self.cw.layout().addWidget(self.stack) + self.bb = QDialogButtonBox(QDialogButtonBox.Close) + self.cw.layout().addWidget(self.bb) + self.bb.rejected.connect(self.close, type=Qt.QueuedConnection) + self.setCentralWidget(self.cw) + self.browser = Browser(self) + self.browser.show_plugin.connect(self.show_plugin) + self.stack.addWidget(self.browser) + self.scroll_area = QScrollArea(self) + self.stack.addWidget(self.scroll_area) + self.scroll_area.setWidgetResizable(True) + + self.bar = QToolBar(self) + self.addToolBar(self.bar) + self.bar.setVisible(False) + self.bar.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) + self.bar.setMovable(False) + self.bar.setFloatable(False) + self.bar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.apply_action = self.bar.addAction(QIcon(I('ok.png')), _('&Apply'), + self.commit) + self.cancel_action = self.bar.addAction(QIcon(I('window-close.png')), + _('&Cancel'), self.cancel) + self.bar_title = BarTitle(self.bar) + self.bar.addWidget(self.bar_title) + self.restore_action = self.bar.addAction(QIcon(I('clear_left.png')), + _('Restore &defaults'), self.restore_defaults) + for ac, tt in [('apply', _('Save changes')), + ('cancel', _('Cancel and return to overview'))]: + ac = getattr(self, ac+'_action') + ac.setToolTip(tt) + ac.setWhatsThis(tt) + ac.setStatusTip(tt) + + for ch in self.bar.children(): + if isinstance(ch, QToolButton): + ch.setCursor(Qt.PointingHandCursor) + ch.setAutoRaise(True) + + self.stack.setCurrentIndex(0) + + if initial_plugin is not None: + category, name = initial_plugin + plugin = get_plugin(category, name) + if plugin is not None: + self.show_plugin(plugin) + + + def show_plugin(self, plugin): + self.showing_widget = plugin.create_widget(self.scroll_area) + self.showing_widget.genesis(self.gui) + self.showing_widget.initialize() + self.scroll_area.setWidget(self.showing_widget) + self.stack.setCurrentIndex(1) + self.showing_widget.show() + self.setWindowTitle(__appname__ + ' - ' + _('Preferences') + ' - ' + + plugin.gui_name) + self.apply_action.setEnabled(False) + self.showing_widget.changed_signal.connect(lambda : + self.apply_action.setEnabled(True)) + self.restore_action.setEnabled(self.showing_widget.supports_restoring_to_defaults) + tt = self.showing_widget.restore_defaults_desc + if not self.restore_action.isEnabled(): + tt = _('Restoring to defaults not supported for') + ' ' + \ + plugin.gui_name + self.restore_action.setToolTip(textwrap.fill(tt)) + self.restore_action.setWhatsThis(textwrap.fill(tt)) + self.restore_action.setStatusTip(tt) + self.bar_title.show_plugin(plugin) + self.setWindowIcon(QIcon(plugin.icon)) + self.bar.setVisible(True) + self.bb.setVisible(False) + + + def hide_plugin(self): + self.showing_widget = QWidget(self.scroll_area) + self.scroll_area.setWidget(self.showing_widget) + self.setWindowTitle(__appname__ + ' - ' + _('Preferences')) + self.bar.setVisible(False) + self.stack.setCurrentIndex(0) + self.setWindowIcon(QIcon(I('config.png'))) + self.bb.setVisible(True) + + def esc(self, *args): + if self.stack.currentIndex() == 1: + self.hide_plugin() + elif self.stack.currentIndex() == 0: + self.close() + + def commit(self, *args): + try: + must_restart = self.showing_widget.commit() + except AbortCommit: + return + rc = self.showing_widget.restart_critical + self.committed = True + if must_restart: + self.must_restart = True + msg = _('Some of the changes you made require a restart.' + ' Please restart calibre as soon as possible.') + if rc: + msg = _('The changes you have made require calibre be ' + 'restarted immediately. You will not be allowed ' + 'set any more preferences, until you restart.') + + + warning_dialog(self, _('Restart needed'), msg, show=True, + show_copy_button=False) + self.showing_widget.refresh_gui(self.gui) + self.hide_plugin() + if must_restart and rc: + self.close() + + + def cancel(self, *args): + self.hide_plugin() + + def restore_defaults(self, *args): + self.showing_widget.restore_defaults() + + def closeEvent(self, *args): + gprefs.set('preferences_window_geometry', + bytearray(self.saveGeometry())) + if self.committed: + self.gui.must_restart_before_config = self.must_restart + self.gui.tags_view.set_new_model() # in case columns changed + self.gui.tags_view.recount() + self.gui.create_device_menu() + self.gui.set_device_menu_items_state(bool(self.gui.device_connected)) + self.gui.tool_bar.build_bar() + self.gui.build_context_menus() + self.gui.tool_bar.apply_settings() + + return QMainWindow.closeEvent(self, *args) + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + app + gui = init_gui() + + p = Preferences(gui) + p.show() + app.exec_() + gui.shutdown() diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py new file mode 100644 index 0000000000..eae79fdfc0 --- /dev/null +++ b/src/calibre/gui2/preferences/misc.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QProgressDialog, QThread, Qt, pyqtSignal + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget +from calibre.gui2.preferences.misc_ui import Ui_Form +from calibre.gui2 import error_dialog, config, warning_dialog, \ + open_local_file, info_dialog +from calibre.constants import isosx + +# Check Integrity {{{ + +class VacThread(QThread): + + check_done = pyqtSignal(object, object) + callback = pyqtSignal(object, object) + + def __init__(self, parent, db): + QThread.__init__(self, parent) + self.db = db + self._parent = parent + + def run(self): + err = bad = None + try: + bad = self.db.check_integrity(self.callbackf) + except: + import traceback + err = traceback.format_exc() + self.check_done.emit(bad, err) + + def callbackf(self, progress, msg): + self.callback.emit(progress, msg) + + +class CheckIntegrity(QProgressDialog): + + def __init__(self, db, parent=None): + QProgressDialog.__init__(self, parent) + self.db = db + self.setCancelButton(None) + self.setMinimum(0) + self.setMaximum(100) + self.setWindowTitle(_('Checking database integrity')) + self.setAutoReset(False) + self.setValue(0) + + self.vthread = VacThread(self, db) + self.vthread.check_done.connect(self.check_done, + type=Qt.QueuedConnection) + self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection) + self.vthread.start() + + def callback(self, progress, msg): + self.setLabelText(msg) + self.setValue(int(100*progress)) + + def check_done(self, bad, err): + if err: + error_dialog(self, _('Error'), + _('Failed to check database integrity'), + det_msg=err, show=True) + elif bad: + titles = [self.db.title(x, index_is_id=True) for x in bad] + det_msg = '\n'.join(titles) + warning_dialog(self, _('Some inconsistencies found'), + _('The following books had formats listed in the ' + 'database that are not actually available. ' + 'The entries for the formats have been removed. ' + 'You should check them manually. This can ' + 'happen if you manipulate the files in the ' + 'library folder directly.'), det_msg=det_msg, show=True) + self.reset() + +# }}} + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + r = self.register + r('worker_limit', config, restart_required=True) + r('enforce_cpu_limit', config, restart_required=True) + self.device_detection_button.clicked.connect(self.debug_device_detection) + self.compact_button.clicked.connect(self.compact) + self.button_open_config_dir.clicked.connect(self.open_config_dir) + self.button_osx_symlinks.clicked.connect(self.create_symlinks) + self.button_osx_symlinks.setVisible(isosx) + + def debug_device_detection(self, *args): + from calibre.gui2.preferences.device_debug import DebugDevice + d = DebugDevice(self) + d.exec_() + + def compact(self, *args): + d = CheckIntegrity(self.gui.library_view.model().db, self) + d.exec_() + + def open_config_dir(self, *args): + from calibre.utils.config import config_dir + open_local_file(config_dir) + + def create_symlinks(self): + from calibre.utils.osx_symlinks import create_symlinks + loc, paths = create_symlinks() + if loc is None: + error_dialog(self, _('Error'), + _('Failed to install command line tools.'), + det_msg=paths, show=True) + else: + info_dialog(self, _('Command line tools installed'), + '

'+_('Command line tools installed in')+' '+loc+ + '
'+ _('If you move calibre.app, you have to re-install ' + 'the command line tools.'), + det_msg='\n'.join(paths), show=True) + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Advanced', 'Misc') + diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui new file mode 100644 index 0000000000..f8582a3675 --- /dev/null +++ b/src/calibre/gui2/preferences/misc.ui @@ -0,0 +1,144 @@ + + + Form + + + + 0 + 0 + 502 + 314 + + + + Form + + + + + + &Maximum number of waiting worker processes (needs restart): + + + opt_worker_limit + + + + + + + 2 + + + 10000 + + + 2 + + + + + + + Limit the max. simultaneous jobs to the available CPU &cores + + + + + + + Qt::Vertical + + + + 20 + 18 + + + + + + + + Debug &device detection + + + + + + + Qt::Vertical + + + + 20 + 19 + + + + + + + + &Check database integrity + + + + + + + Qt::Vertical + + + + 20 + 18 + + + + + + + + Open calibre &configuration directory + + + + + + + Qt::Vertical + + + + 20 + 19 + + + + + + + + &Install command line tools + + + + + + + Qt::Vertical + + + + 20 + 18 + + + + + + + + + diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py new file mode 100644 index 0000000000..a26553db1c --- /dev/null +++ b/src/calibre/gui2/preferences/plugins.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import textwrap, os + +from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \ + QBrush, QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QLineEdit + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget +from calibre.gui2.preferences.plugins_ui import Ui_Form +from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ + disable_plugin, customize_plugin, \ + plugin_customization, add_plugin, \ + remove_plugin +from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files + +class PluginModel(QAbstractItemModel): # {{{ + + def __init__(self, *args): + QAbstractItemModel.__init__(self, *args) + self.icon = QVariant(QIcon(I('plugins.png'))) + p = QIcon(self.icon).pixmap(32, 32, QIcon.Disabled, QIcon.On) + self.disabled_icon = QVariant(QIcon(p)) + self._p = p + self.populate() + + def populate(self): + self._data = {} + for plugin in initialized_plugins(): + if plugin.type not in self._data: + self._data[plugin.type] = [plugin] + else: + self._data[plugin.type].append(plugin) + self.categories = sorted(self._data.keys()) + + for plugins in self._data.values(): + plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower())) + + def index(self, row, column, parent): + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if parent.isValid(): + return self.createIndex(row, column, 1+parent.row()) + else: + return self.createIndex(row, column, 0) + + def parent(self, index): + if not index.isValid() or index.internalId() == 0: + return QModelIndex() + return self.createIndex(index.internalId()-1, 0, 0) + + def rowCount(self, parent): + if not parent.isValid(): + return len(self.categories) + if parent.internalId() == 0: + category = self.categories[parent.row()] + return len(self._data[category]) + return 0 + + def columnCount(self, parent): + return 1 + + def index_to_plugin(self, index): + category = self.categories[index.parent().row()] + return self._data[category][index.row()] + + def plugin_to_index(self, plugin): + for i, category in enumerate(self.categories): + parent = self.index(i, 0, QModelIndex()) + for j, p in enumerate(self._data[category]): + if plugin == p: + return self.index(j, 0, parent) + return QModelIndex() + + def refresh_plugin(self, plugin, rescan=False): + if rescan: + self.populate() + idx = self.plugin_to_index(plugin) + self.dataChanged.emit(idx, idx) + + def flags(self, index): + if not index.isValid(): + return 0 + if index.internalId() == 0: + return Qt.ItemIsEnabled + flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled + return flags + + def data(self, index, role): + if not index.isValid(): + return NONE + if index.internalId() == 0: + if role == Qt.DisplayRole: + category = self.categories[index.row()] + return QVariant(_("%(plugin_type)s %(plugins)s")%\ + dict(plugin_type=category, plugins=_('plugins'))) + else: + plugin = self.index_to_plugin(index) + if role == Qt.DisplayRole: + ver = '.'.join(map(str, plugin.version)) + desc = '\n'.join(textwrap.wrap(plugin.description, 50)) + ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), plugin.author, desc) + c = plugin_customization(plugin) + if c: + ans += _('\nCustomization: ')+c + return QVariant(ans) + if role == Qt.DecorationRole: + return self.disabled_icon if is_disabled(plugin) else self.icon + if role == Qt.ForegroundRole and is_disabled(plugin): + return QVariant(QBrush(Qt.gray)) + if role == Qt.UserRole: + return plugin + return NONE + +# }}} + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + supports_restoring_to_defaults = False + + def genesis(self, gui): + self.gui = gui + self._plugin_model = PluginModel() + self.plugin_view.setModel(self._plugin_model) + self.plugin_view.setStyleSheet( + "QTreeView::item { padding-bottom: 10px;}") + self.toggle_plugin_button.clicked.connect(self.toggle_plugin) + self.customize_plugin_button.clicked.connect(self.customize_plugin) + self.remove_plugin_button.clicked.connect(self.remove_plugin) + self.button_plugin_browse.clicked.connect(self.find_plugin) + self.button_plugin_add.clicked.connect(self.add_plugin) + + def toggle_plugin(self, *args): + self.modify_plugin(op='toggle') + + def customize_plugin(self, *args): + self.modify_plugin(op='customize') + + def remove_plugin(self, *args): + self.modify_plugin(op='remove') + + def add_plugin(self): + path = unicode(self.plugin_path.text()) + if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'): + add_plugin(path) + self._plugin_model.populate() + self._plugin_model.reset() + self.changed_signal.emit() + else: + error_dialog(self, _('No valid plugin path'), + _('%s is not a valid plugin path')%path).exec_() + + def find_plugin(self): + path = choose_files(self, 'choose plugin dialog', _('Choose plugin'), + filters=[('Plugins', ['zip'])], all_files=False, + select_only_single_file=True) + if path: + self.plugin_path.setText(path[0]) + + def modify_plugin(self, op=''): + index = self.plugin_view.currentIndex() + if index.isValid(): + plugin = self._plugin_model.index_to_plugin(index) + if op == 'toggle': + if not plugin.can_be_disabled: + error_dialog(self,_('Plugin cannot be disabled'), + _('The plugin: %s cannot be disabled')%plugin.name).exec_() + return + if is_disabled(plugin): + enable_plugin(plugin) + else: + disable_plugin(plugin) + self._plugin_model.refresh_plugin(plugin) + self.changed_signal.emit() + if op == 'customize': + if not plugin.is_customizable(): + info_dialog(self, _('Plugin not customizable'), + _('Plugin: %s does not need customization')%plugin.name).exec_() + return + self.changed_signal.emit() + + config_dialog = QDialog(self) + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + v = QVBoxLayout(config_dialog) + + button_box.accepted.connect(config_dialog.accept) + button_box.rejected.connect(config_dialog.reject) + config_dialog.setWindowTitle(_('Customize') + ' ' + plugin.name) + + if hasattr(plugin, 'config_widget'): + config_widget = plugin.config_widget() + v.addWidget(config_widget) + v.addWidget(button_box) + config_dialog.exec_() + + if config_dialog.result() == QDialog.Accepted: + plugin.save_settings(config_widget) + self._plugin_model.refresh_plugin(plugin) + else: + help_text = plugin.customization_help(gui=True) + help_text = QLabel(help_text, config_dialog) + help_text.setWordWrap(True) + help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse + | Qt.LinksAccessibleByKeyboard) + help_text.setOpenExternalLinks(True) + v.addWidget(help_text) + sc = plugin_customization(plugin) + if not sc: + sc = '' + sc = sc.strip() + sc = QLineEdit(sc, config_dialog) + v.addWidget(sc) + v.addWidget(button_box) + config_dialog.exec_() + + if config_dialog.result() == QDialog.Accepted: + sc = unicode(sc.text()).strip() + customize_plugin(plugin, sc) + + self._plugin_model.refresh_plugin(plugin) + elif op == 'remove': + if remove_plugin(plugin): + self._plugin_model.populate() + self._plugin_model.reset() + self.changed_signal.emit() + else: + error_dialog(self, _('Cannot remove builtin plugin'), + plugin.name + _(' cannot be removed. It is a ' + 'builtin plugin. Try disabling it instead.')).exec_() + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Advanced', 'Plugins') + diff --git a/src/calibre/gui2/preferences/plugins.ui b/src/calibre/gui2/preferences/plugins.ui new file mode 100644 index 0000000000..8979867bbc --- /dev/null +++ b/src/calibre/gui2/preferences/plugins.ui @@ -0,0 +1,141 @@ + + + Form + + + + 0 + 0 + 723 + 540 + + + + Form + + + + + + Here you can customize the behavior of Calibre by controlling what plugins it uses. + + + true + + + + + + + true + + + + 32 + 32 + + + + true + + + true + + + true + + + + + + + + + Enable/&Disable plugin + + + + + + + &Customize plugin + + + + + + + &Remove plugin + + + + + + + + + Add new plugin + + + + + + + + Plugin &file: + + + plugin_path + + + + + + + + + + ... + + + + :/images/document_open.png:/images/document_open.png + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Add + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/dialogs/config/save_template.py b/src/calibre/gui2/preferences/save_template.py similarity index 80% rename from src/calibre/gui2/dialogs/config/save_template.py rename to src/calibre/gui2/preferences/save_template.py index 71eb15f4aa..d325ac42ff 100644 --- a/src/calibre/gui2/dialogs/config/save_template.py +++ b/src/calibre/gui2/preferences/save_template.py @@ -6,15 +6,17 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QWidget +from PyQt4.Qt import QWidget, pyqtSignal from calibre.gui2 import error_dialog -from calibre.gui2.dialogs.config.save_template_ui import Ui_Form +from calibre.gui2.preferences.save_template_ui import Ui_Form from calibre.library.save_to_disk import FORMAT_ARG_DESCS, \ preprocess_template class SaveTemplate(QWidget, Ui_Form): + changed_signal = pyqtSignal() + def __init__(self, *args): QWidget.__init__(self, *args) Ui_Form.__init__(self) @@ -31,8 +33,13 @@ class SaveTemplate(QWidget, Ui_Form): self.opt_template.initialize(name+'_template_history', default, help) + self.opt_template.editTextChanged.connect(self.changed) + self.opt_template.currentIndexChanged.connect(self.changed) self.option_name = name + def changed(self, *args): + self.changed_signal.emit() + def validate(self): tmpl = preprocess_template(self.opt_template.text()) fa = {} @@ -47,6 +54,9 @@ class SaveTemplate(QWidget, Ui_Form): return False return True + def set_value(self, val): + self.opt_template.set_value(val) + def save_settings(self, config, name): val = unicode(self.opt_template.text()) config.set(name, val) diff --git a/src/calibre/gui2/dialogs/config/save_template.ui b/src/calibre/gui2/preferences/save_template.ui similarity index 96% rename from src/calibre/gui2/dialogs/config/save_template.ui rename to src/calibre/gui2/preferences/save_template.ui index 02506891bb..0d7422abcf 100644 --- a/src/calibre/gui2/dialogs/config/save_template.ui +++ b/src/calibre/gui2/preferences/save_template.ui @@ -52,7 +52,7 @@ HistoryBox QComboBox -

calibre/gui2/dialogs/config/history.h
+
calibre/gui2/preferences/history.h
diff --git a/src/calibre/gui2/preferences/saving.py b/src/calibre/gui2/preferences/saving.py new file mode 100644 index 0000000000..e4b6a33917 --- /dev/null +++ b/src/calibre/gui2/preferences/saving.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ + AbortCommit +from calibre.gui2.preferences.saving_ui import Ui_Form +from calibre.utils.config import ConfigProxy +from calibre.library.save_to_disk import config + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + self.proxy = ConfigProxy(config()) + + r = self.register + + for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf', + 'replace_whitespace', 'to_lowercase', 'formats', 'timefmt'): + r(x, self.proxy) + + self.save_template.changed_signal.connect(self.changed_signal.emit) + + + def initialize(self): + ConfigWidgetBase.initialize(self) + self.save_template.blockSignals(True) + self.save_template.initialize('save_to_disk', self.proxy['template'], + self.proxy.help('template')) + self.save_template.blockSignals(False) + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + self.save_template.set_value(self.proxy.defaults['template']) + + def commit(self): + if not self.save_template.validate(): + raise AbortCommit('abort') + self.save_template.save_settings(self.proxy, 'template') + return ConfigWidgetBase.commit(self) + + def refresh_gui(self, gui): + gui.iactions['Save To Disk'].reread_prefs() + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Import/Export', 'Saving') + diff --git a/src/calibre/gui2/preferences/saving.ui b/src/calibre/gui2/preferences/saving.ui new file mode 100644 index 0000000000..e4f5aaeb47 --- /dev/null +++ b/src/calibre/gui2/preferences/saving.ui @@ -0,0 +1,110 @@ + + + Form + + + + 0 + 0 + 707 + 340 + + + + Form + + + + + + Here you can control how calibre will save your books when you click the Save to Disk button: + + + true + + + + + + + Save &cover separately + + + + + + + Replace space with &underscores + + + + + + + Update &metadata in saved copies + + + + + + + Change paths to &lowercase + + + + + + + Format &dates as: + + + opt_timefmt + + + + + + + + + + File &formats to save: + + + opt_formats + + + + + + + + + + + + + Convert non-English characters to &English equivalents + + + + + + + Save metadata in &OPF file + + + + + + + + SaveTemplate + QWidget +
calibre/gui2/preferences/save_template.h
+ 1 +
+
+ + +
diff --git a/src/calibre/gui2/preferences/sending.py b/src/calibre/gui2/preferences/sending.py new file mode 100644 index 0000000000..748c6b2a2d --- /dev/null +++ b/src/calibre/gui2/preferences/sending.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ + AbortCommit +from calibre.gui2.preferences.sending_ui import Ui_Form +from calibre.utils.config import ConfigProxy +from calibre.library.save_to_disk import config +from calibre.utils.config import prefs + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + self.proxy = ConfigProxy(config()) + + r = self.register + + choices = [(_('Manual management'), 'manual'), + (_('Only on send'), 'on_send'), + (_('Automatic management'), 'on_connect')] + r('manage_device_metadata', prefs, choices=choices) + + self.send_template.changed_signal.connect(self.changed_signal.emit) + + + def initialize(self): + ConfigWidgetBase.initialize(self) + self.send_template.blockSignals(True) + self.send_template.initialize('send_to_device', self.proxy['send_template'], + self.proxy.help('send_template')) + self.send_template.blockSignals(False) + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + self.send_template.set_value(self.proxy.defaults['send_template']) + + def commit(self): + if not self.send_template.validate(): + raise AbortCommit('abort') + self.send_template.save_settings(self.proxy, 'send_template') + return ConfigWidgetBase.commit(self) + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Import/Export', 'Sending') + diff --git a/src/calibre/gui2/preferences/sending.ui b/src/calibre/gui2/preferences/sending.ui new file mode 100644 index 0000000000..e064646afd --- /dev/null +++ b/src/calibre/gui2/preferences/sending.ui @@ -0,0 +1,108 @@ + + + Form + + + + 0 + 0 + 807 + 331 + + + + Form + + + + + + + 0 + 0 + + + + Metadata &management: + + + opt_manage_device_metadata + + + + + + + + 0 + 0 + + + + + Manual management + + + + + Only on send + + + + + Automatic management + + + + + + + + Qt::Horizontal + + + + 457 + 20 + + + + + + + + <li><b>Manual management</b>: Calibre updates the metadata and adds collections only when a book is sent. With this option, calibre will never remove a collection.</li> +<li><b>Only on send</b>: Calibre updates metadata and adds/removes collections for a book only when it is sent to the device. </li> +<li><b>Automatic management</b>: Calibre automatically keeps metadata on the device in sync with the calibre library, on every connect</li></ul> + + + true + + + + + + + Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Advanced->Plugins + + + true + + + + + + + + + + + SaveTemplate + QWidget +
calibre/gui2/preferences/save_template.h
+ 1 +
+
+ + +
diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py new file mode 100644 index 0000000000..4db1244d75 --- /dev/null +++ b/src/calibre/gui2/preferences/server.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import time + +from PyQt4.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \ + QPlainTextEdit, QDialogButtonBox + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget +from calibre.gui2.preferences.server_ui import Ui_Form +from calibre.utils.search_query_parser import saved_searches +from calibre.library.server import server_config +from calibre.utils.config import ConfigProxy +from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ + Dispatcher + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + self.proxy = ConfigProxy(server_config()) + db = self.db = gui.library_view.model().db + self.server = self.gui.content_server + + r = self.register + + r('port', self.proxy) + r('username', self.proxy) + r('password', self.proxy) + r('max_cover', self.proxy) + r('max_opds_items', self.proxy) + r('max_opds_ungrouped_items', self.proxy) + + self.show_server_password.stateChanged[int].connect( + lambda s: self.opt_password.setEchoMode( + self.opt_password.Normal if s == Qt.Checked + else self.opt_password.Password)) + self.opt_password.setEchoMode(self.opt_password.Password) + + restrictions = sorted(saved_searches().names(), + cmp=lambda x,y: cmp(x.lower(), y.lower())) + choices = [('', '')] + [(x, x) for x in restrictions] + r('cs_restriction', db.prefs, choices=choices) + + self.start_button.setEnabled(not getattr(self.server, 'is_running', False)) + self.test_button.setEnabled(not self.start_button.isEnabled()) + self.stop_button.setDisabled(self.start_button.isEnabled()) + self.start_button.clicked.connect(self.start_server) + self.stop_button.clicked.connect(self.stop_server) + self.test_button.clicked.connect(self.test_server) + self.view_logs.clicked.connect(self.view_server_logs) + + r('autolaunch_server', config) + + def set_server_options(self): + c = self.proxy + c.set('port', self.opt_port.value()) + c.set('username', unicode(self.opt_username.text()).strip()) + p = unicode(self.opt_password.text()).strip() + if not p: + p = None + c.set('password', p) + + def start_server(self): + self.set_server_options() + from calibre.library.server.main import start_threaded_server + self.server = start_threaded_server(self.db, server_config().parse()) + while not self.server.is_running and self.server.exception is None: + time.sleep(1) + if self.server.exception is not None: + error_dialog(self, _('Failed to start content server'), + unicode(self.server.exception)).exec_() + return + self.start_button.setEnabled(False) + self.test_button.setEnabled(True) + self.stop_button.setEnabled(True) + + def stop_server(self): + from calibre.library.server.main import stop_threaded_server + stop_threaded_server(self.server) + self.server = None + self.start_button.setEnabled(True) + self.test_button.setEnabled(False) + self.stop_button.setEnabled(False) + + def test_server(self): + open_url(QUrl('http://127.0.0.1:'+str(self.opt_port.value()))) + + def view_server_logs(self): + from calibre.library.server import log_access_file, log_error_file + d = QDialog(self) + d.resize(QSize(800, 600)) + layout = QVBoxLayout() + d.setLayout(layout) + layout.addWidget(QLabel(_('Error log:'))) + el = QPlainTextEdit(d) + layout.addWidget(el) + try: + el.setPlainText(open(log_error_file, 'rb').read().decode('utf8', 'replace')) + except IOError: + el.setPlainText('No error log found') + layout.addWidget(QLabel(_('Access log:'))) + al = QPlainTextEdit(d) + layout.addWidget(al) + try: + al.setPlainText(open(log_access_file, 'rb').read().decode('utf8', 'replace')) + except IOError: + al.setPlainText('No access log found') + bx = QDialogButtonBox(QDialogButtonBox.Ok) + layout.addWidget(bx) + bx.accepted.connect(d.accept) + d.show() + + def commit(self): + ConfigWidgetBase.commit(self) + warning_dialog(self, _('Restart needed'), + _('You need to restart the server for changes to' + ' take effect'), show=True) + return False + + def refresh_gui(self, gui): + gui.content_server = self.server + if gui.content_server is not None: + gui.content_server.state_callback = \ + Dispatcher(gui.iactions['Connect Share'].content_server_state_changed) + gui.content_server.state_callback(gui.content_server.is_running) + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Sharing', 'Server') + diff --git a/src/calibre/gui2/preferences/server.ui b/src/calibre/gui2/preferences/server.ui new file mode 100644 index 0000000000..d043f6f52c --- /dev/null +++ b/src/calibre/gui2/preferences/server.ui @@ -0,0 +1,261 @@ + + + Form + + + + 0 + 0 + 641 + 563 + + + + Form + + + + + + + + Server &port: + + + opt_port + + + + + + + 65535 + + + 8080 + + + + + + + &Username: + + + opt_username + + + + + + + + + + &Password: + + + opt_password + + + + + + + If you leave the password blank, anyone will be able to access your book collection using the web interface. + + + + + + + The maximum size (widthxheight) for displayed covers. Larger covers are resized. + + + + + + + + + + Max. &cover size: + + + opt_max_cover + + + + + + + &Show password + + + + + + + Max. &OPDS items per query: + + + opt_max_opds_items + + + + + + + 10 + + + 10000 + + + + + + + 25 + + + 1000000 + + + + + + + Max. OPDS &ungrouped items: + + + opt_max_opds_ungrouped_items + + + + + + + Restriction (saved search) to apply: + + + + + + + This restriction (based on a saved search) will restrict the books the content server makes available to those matching the search. This setting is per library (i.e. you can have a different restriction per library). + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 20 + + + + + + + + + + + &Start Server + + + + + + + St&op Server + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Test Server + + + + + + + + + calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart. + + + true + + + + + + + Run server &automatically on startup + + + + + + + View &server logs + + + + + + + Qt::Vertical + + + + 20 + 36 + + + + + + + + <p>Remember to leave calibre running as the server only runs as long as calibre is running. +<p>Stanza should see your calibre collection automatically. If not, try adding the URL http://myhostname:8080 as a new catalog in the Stanza reader on your iPhone. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on. + + + true + + + + + + + Qt::Vertical + + + + 20 + 37 + + + + + + + + + diff --git a/src/calibre/gui2/dialogs/config/social.py b/src/calibre/gui2/preferences/social.py similarity index 100% rename from src/calibre/gui2/dialogs/config/social.py rename to src/calibre/gui2/preferences/social.py diff --git a/src/calibre/gui2/preferences/tweaks.py b/src/calibre/gui2/preferences/tweaks.py new file mode 100644 index 0000000000..2bd765986d --- /dev/null +++ b/src/calibre/gui2/preferences/tweaks.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit +from calibre.gui2.preferences.tweaks_ui import Ui_Form +from calibre.gui2 import error_dialog +from calibre.utils.config import read_raw_tweaks, write_tweaks + + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + self.current_tweaks.textChanged.connect(self.changed) + + def changed(self, *args): + self.changed_signal.emit() + + def initialize(self): + deft, curt = read_raw_tweaks() + self.current_tweaks.blockSignals(True) + self.current_tweaks.setPlainText(curt.decode('utf-8')) + self.current_tweaks.blockSignals(False) + + self.default_tweaks.setPlainText(deft.decode('utf-8')) + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + deft, curt = read_raw_tweaks() + self.current_tweaks.setPlainText(deft.decode('utf-8')) + + + def commit(self): + raw = unicode(self.current_tweaks.toPlainText()).encode('utf-8') + try: + exec raw + except: + import traceback + error_dialog(self, _('Invalid tweaks'), + _('The tweaks you entered are invalid, try resetting the' + ' tweaks to default and changing them one by one until' + ' you find the invalid setting.'), + det_msg=traceback.format_exc(), show=True) + raise AbortCommit('abort') + write_tweaks(raw) + ConfigWidgetBase.commit(self) + return True + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Advanced', 'Tweaks') + diff --git a/src/calibre/gui2/preferences/tweaks.ui b/src/calibre/gui2/preferences/tweaks.ui new file mode 100644 index 0000000000..8546873552 --- /dev/null +++ b/src/calibre/gui2/preferences/tweaks.ui @@ -0,0 +1,59 @@ + + + Form + + + + 0 + 0 + 660 + 351 + + + + Form + + + + + + Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect after a restart of calibre. + + + true + + + + + + + All available tweaks + + + + + + true + + + + + + + + + + &Current tweaks + + + + + + + + + + + + + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 72f0efd9bf..519d533ff6 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -424,10 +424,8 @@ class TagsModel(QAbstractItemModel): # {{{ self.categories = [] # Reconstruct the user categories, putting them into metadata + self.db.field_metadata.remove_dynamic_categories() tb_cats = self.db.field_metadata - for k in tb_cats.keys(): - if tb_cats[k]['kind'] in ['user', 'search']: - del tb_cats[k] for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys()): cat_name = user_cat+':' # add the ':' to avoid name collision tb_cats.add_user_category(label=cat_name, name=user_cat) @@ -514,7 +512,8 @@ class TagsModel(QAbstractItemModel): # {{{ _('The saved search name %s is already used.')%val).exec_() return False saved_searches().rename(unicode(item.data(role).toString()), val) - self.tags_view.search_item_renamed.emit() + item.tag.name = val + self.tags_view.search_item_renamed.emit() # Does a refresh else: if key == 'series': self.db.rename_series(item.tag.id, val) @@ -528,8 +527,8 @@ class TagsModel(QAbstractItemModel): # {{{ 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.refresh() # Should work, because no categories can have disappeared + item.tag.name = val + self.refresh() # Should work, because no categories can have disappeared if path: idx = self.index_for_path(path) if idx.isValid(): @@ -671,7 +670,7 @@ class TagBrowserMixin(object): # {{{ self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) self.tags_view.author_sort_edit.connect(self.do_author_sort_edit) 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) + self.tags_view.search_item_renamed.connect(self.saved_searches_changed) self.edit_categories.clicked.connect(lambda x: self.do_user_categories_edit()) diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index caef82ab81..7a516bb4ff 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -238,7 +238,7 @@ def fetch_scheduled_recipe(arg): return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt] -def generate_catalog(parent, dbspec, ids, device): +def generate_catalog(parent, dbspec, ids, device_manager): from calibre.gui2.dialogs.catalog import Catalog # Build the Catalog dialog in gui2.dialogs.catalog @@ -252,9 +252,18 @@ def generate_catalog(parent, dbspec, ids, device): # Profile the connected device # Parallel initialization in calibre.library.cli:command_catalog() - connected_device = { 'storage':None,'serial':None,'save_template':None,'name':None} + connected_device = { + 'is_device_connected': device_manager.is_device_connected, + 'kind': device_manager.connected_device_kind, + 'name': None, + 'save_template': None, + 'serial': None, + 'storage': None + } - if device: + if device_manager.is_device_connected: + device = device_manager.device + connected_device['name'] = device.gui_name try: storage = [] if device._main_prefix: @@ -263,11 +272,10 @@ def generate_catalog(parent, dbspec, ids, device): storage.append(os.path.join(device._card_a_prefix, device.EBOOK_DIR_CARD_A)) if device._card_b_prefix: storage.append(os.path.join(device._card_b_prefix, device.EBOOK_DIR_CARD_B)) - connected_device = { 'storage': storage, - 'serial': device.detected_device.serial if \ - hasattr(device.detected_device,'serial') else None, - 'save_template': device.save_template(), - 'name': device.gui_name} + connected_device['storage'] = storage + connected_device['serial'] = device.detected_device.serial if \ + hasattr(device.detected_device,'serial') else None + connected_device['save_template'] = device.save_template() except: pass diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 84f5a1a2c9..f40eed1fcd 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -522,6 +522,16 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ def shutdown(self, write_settings=True): + try: + db = self.library_view.model().db + cf = db.clean + except: + pass + else: + cf() + # Save the current field_metadata for applications like calibre2opds + # Goes here, because if cf is valid, db is valid. + db.prefs['field_metadata'] = db.field_metadata.all_metadata() for action in self.iactions.values(): if not action.shutting_down(): return diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index c8f1f62856..79f4c29998 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -173,6 +173,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.pending_anchor = None self.pending_reference = None self.pending_bookmark = None + self.existing_bookmarks= [] self.selected_text = None self.read_settings() self.dictionary_box.hide() @@ -415,15 +416,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_font_size_smaller.setEnabled(self.view.multiplier() > 0.2) self.set_page_number(frac) - def bookmark(self, *args): - title, ok = QInputDialog.getText(self, _('Add bookmark'), _('Enter title for bookmark:')) - title = unicode(title).strip() - if ok and title: - pos = self.view.bookmark() - bookmark = '%d#%s'%(self.current_index, pos) - self.iterator.add_bookmark((title, bookmark)) - self.set_bookmarks(self.iterator.bookmarks) - def find(self, text, repeat=False, backwards=False): if not text: @@ -539,15 +531,34 @@ class EbookViewer(MainWindow, Ui_EbookViewer): getattr(self, o).setEnabled(False) self.setCursor(Qt.BusyCursor) + def bookmark(self, *args): + num = 1 + bm = None + while True: + bm = _('Bookmark #%d')%num + if bm not in self.existing_bookmarks: + break + num += 1 + title, ok = QInputDialog.getText(self, _('Add bookmark'), + _('Enter title for bookmark:'), text=bm) + title = unicode(title).strip() + if ok and title: + pos = self.view.bookmark() + bookmark = '%d#%s'%(self.current_index, pos) + self.iterator.add_bookmark((title, bookmark)) + self.set_bookmarks(self.iterator.bookmarks) + def set_bookmarks(self, bookmarks): self.bookmarks_menu.clear() self.bookmarks_menu.addAction(_("Manage Bookmarks"), self.manage_bookmarks) self.bookmarks_menu.addSeparator() current_page = None + self.existing_bookmarks = [] for bm in bookmarks: if bm[0] == 'calibre_current_page_bookmark': current_page = bm else: + self.existing_bookmarks.append(bm[0]) self.bookmarks_menu.addAction(bm[0], partial(self.goto_bookmark, bm)) return current_page diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 0237da0a09..60224aefc7 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -1032,6 +1032,9 @@ class Splitter(QSplitter): # Public API {{{ + def update_desired_state(self): + self.desired_show = not self.is_side_index_hidden + def save_state(self): if self.count() > 1: gprefs[self.save_name+'_state'] = self.get_state() diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 504c426359..61b9c9f934 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' import cStringIO, sys from binascii import hexlify, unhexlify -from PyQt4.Qt import QWidget, SIGNAL, QDialog, Qt +from PyQt4.Qt import QWidget, pyqtSignal, QDialog, Qt from calibre.gui2.wizard.send_email_ui import Ui_Form from calibre.utils.smtp import config as smtp_prefs @@ -24,7 +24,7 @@ class TestEmail(QDialog, TE_Dialog): self.setupUi(self) opts = smtp_prefs().parse() self.test_func = parent.test_email_settings - self.connect(self.test_button, SIGNAL('clicked(bool)'), self.test) + self.test_button.clicked.connect(self.test) self.from_.setText(unicode(self.from_.text())%opts.from_) if pa: self.to.setText(pa) @@ -33,7 +33,7 @@ class TestEmail(QDialog, TE_Dialog): (opts.relay_username, unhexlify(opts.relay_password), opts.relay_host, opts.relay_port, opts.encryption)) - def test(self): + def test(self, *args): self.log.setPlainText(_('Sending...')) self.test_button.setEnabled(False) try: @@ -47,6 +47,8 @@ class TestEmail(QDialog, TE_Dialog): class SendEmail(QWidget, Ui_Form): + changed_signal = pyqtSignal() + def __init__(self, parent=None): QWidget.__init__(self, parent) self.setupUi(self) @@ -57,23 +59,31 @@ class SendEmail(QWidget, Ui_Form): self.smtp_opts = opts if opts.from_: self.email_from.setText(opts.from_) + self.email_from.textChanged.connect(self.changed) if opts.relay_host: self.relay_host.setText(opts.relay_host) + self.relay_host.textChanged.connect(self.changed) self.relay_port.setValue(opts.relay_port) + self.relay_port.valueChanged.connect(self.changed) if opts.relay_username: self.relay_username.setText(opts.relay_username) + self.relay_username.textChanged.connect(self.changed) if opts.relay_password: self.relay_password.setText(unhexlify(opts.relay_password)) + self.relay_password.textChanged.connect(self.changed) (self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True) - self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'), - self.create_gmail_relay) - self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'), - lambda - state:self.relay_password.setEchoMode(self.relay_password.Password if - state == 0 else self.relay_password.Normal)) - self.connect(self.test_email_button, SIGNAL('clicked(bool)'), - self.test_email) + self.relay_tls.toggled.connect(self.changed) + self.relay_use_gmail.clicked.connect( + self.create_gmail_relay) + self.relay_show_password.stateChanged.connect( + lambda state : self.relay_password.setEchoMode( + self.relay_password.Password if + state == 0 else self.relay_password.Normal)) + self.test_email_button.clicked.connect(self.test_email) + + def changed(self, *args): + self.changed_signal.emit() def test_email(self, *args): pa = self.preferred_to_address() diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 4637730116..bd2160aff1 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -20,7 +20,7 @@ from calibre.utils.date import isoformat, now as nowf from calibre.utils.logging import default_log as log FIELDS = ['all', 'author_sort', 'authors', 'comments', - 'cover', 'formats', 'id', 'isbn', 'pubdate', 'publisher', 'rating', + 'cover', 'formats', 'id', 'isbn', 'ondevice', 'pubdate', 'publisher', 'rating', 'series_index', 'series', 'size', 'tags', 'timestamp', 'title', 'uuid'] @@ -67,6 +67,8 @@ class CSV_XML(CatalogPlugin): if opts.verbose: opts_dict = vars(opts) log("%s(): Generating %s" % (self.name,self.fmt)) + if opts.connected_device['is_device_connected']: + log(" connected_device: %s" % opts.connected_device['name']) if opts_dict['search_text']: log(" --search='%s'" % opts_dict['search_text']) @@ -81,7 +83,6 @@ class CSV_XML(CatalogPlugin): else: log(" Fields: %s" % opts_dict['fields']) - # If a list of ids are provided, don't use search_text if opts.ids: opts.search_text = None @@ -95,6 +96,11 @@ class CSV_XML(CatalogPlugin): # Get the requested output fields as a list fields = self.get_output_fields(opts) + # If connected device, add 'On Device' values to data + if opts.connected_device['is_device_connected'] and 'ondevice' in fields: + for entry in data: + entry['ondevice'] = db.catalog_plugin_on_device_temp_mapping[entry['id']]['ondevice'] + if self.fmt == 'csv': outfile = codecs.open(path_to_output, 'w', 'utf8') @@ -140,10 +146,10 @@ class CSV_XML(CatalogPlugin): root.append(record) for field in ('id', 'uuid', 'title', 'publisher', 'rating', 'size', - 'isbn'): + 'isbn','ondevice'): if field in fields: val = r[field] - if val is None: + if not val: continue if not isinstance(val, (str, unicode)): val = unicode(val) @@ -561,6 +567,13 @@ class EPUB_MOBI(CatalogPlugin): help=_("Include 'Titles' section in catalog.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), + Option('--generate-series', + default=False, + dest='generate_series', + action = 'store_true', + help=_("Include 'Series' section in catalog.\n" + "Default: '%default'\n" + "Applies to: ePub, MOBI output formats")), Option('--generate-recently-added', default=False, dest='generate_recently_added', @@ -886,304 +899,307 @@ class EPUB_MOBI(CatalogPlugin): self.__totalSteps += 2 if self.generateRecentlyRead: self.__totalSteps += 2 + if self.opts.generate_series: + self.__totalSteps += 2 + # Accessors - ''' - @dynamic_property - def xxxx(self): - def fget(self): - return self.__ - def fset(self, val): - self.__ = val - return property(fget=fget, fset=fset) - ''' + if True: + ''' + @dynamic_property + def xxxx(self): + def fget(self): + return self.__ + def fset(self, val): + self.__ = val + return property(fget=fget, fset=fset) + ''' + @dynamic_property + def authorClip(self): + def fget(self): + return self.__authorClip + def fset(self, val): + self.__authorClip = val + return property(fget=fget, fset=fset) + @dynamic_property + def authors(self): + def fget(self): + return self.__authors + def fset(self, val): + self.__authors = val + return property(fget=fget, fset=fset) + @dynamic_property + def basename(self): + def fget(self): + return self.__basename + def fset(self, val): + self.__basename = val + return property(fget=fget, fset=fset) + @dynamic_property + def bookmarked_books(self): + def fget(self): + return self.__bookmarked_books + def fset(self, val): + self.__bookmarked_books = val + return property(fget=fget, fset=fset) + @dynamic_property + def booksByAuthor(self): + def fget(self): + return self.__booksByAuthor + def fset(self, val): + self.__booksByAuthor = val + return property(fget=fget, fset=fset) + @dynamic_property + def booksByDateRead(self): + def fget(self): + return self.__booksByDateRead + def fset(self, val): + self.__booksByDateRead = val + return property(fget=fget, fset=fset) + @dynamic_property + def booksByTitle(self): + def fget(self): + return self.__booksByTitle + def fset(self, val): + self.__booksByTitle = val + return property(fget=fget, fset=fset) + @dynamic_property + def booksByTitle_noSeriesPrefix(self): + def fget(self): + return self.__booksByTitle_noSeriesPrefix + def fset(self, val): + self.__booksByTitle_noSeriesPrefix = val + return property(fget=fget, fset=fset) + @dynamic_property + def catalogPath(self): + def fget(self): + return self.__catalogPath + def fset(self, val): + self.__catalogPath = val + return property(fget=fget, fset=fset) + @dynamic_property + def contentDir(self): + def fget(self): + return self.__contentDir + def fset(self, val): + self.__contentDir = val + return property(fget=fget, fset=fset) + @dynamic_property + def currentStep(self): + def fget(self): + return self.__currentStep + def fset(self, val): + self.__currentStep = val + return property(fget=fget, fset=fset) + @dynamic_property + def creator(self): + def fget(self): + return self.__creator + def fset(self, val): + self.__creator = val + return property(fget=fget, fset=fset) + @dynamic_property + def db(self): + def fget(self): + return self.__db + return property(fget=fget) + @dynamic_property + def descriptionClip(self): + def fget(self): + return self.__descriptionClip + def fset(self, val): + self.__descriptionClip = val + return property(fget=fget, fset=fset) + @dynamic_property + def error(self): + def fget(self): + return self.__error + return property(fget=fget) + @dynamic_property + def generateForKindle(self): + def fget(self): + return self.__generateForKindle + def fset(self, val): + self.__generateForKindle = val + return property(fget=fget, fset=fset) + @dynamic_property + def generateRecentlyRead(self): + def fget(self): + return self.__generateRecentlyRead + def fset(self, val): + self.__generateRecentlyRead = val + return property(fget=fget, fset=fset) + @dynamic_property + def genres(self): + def fget(self): + return self.__genres + def fset(self, val): + self.__genres = val + return property(fget=fget, fset=fset) + @dynamic_property + def genre_tags_dict(self): + def fget(self): + return self.__genre_tags_dict + def fset(self, val): + self.__genre_tags_dict = val + return property(fget=fget, fset=fset) + @dynamic_property + def htmlFileList(self): + def fget(self): + return self.__htmlFileList + def fset(self, val): + self.__htmlFileList = val + return property(fget=fget, fset=fset) + @dynamic_property + def libraryPath(self): + def fget(self): + return self.__libraryPath + def fset(self, val): + self.__libraryPath = val + return property(fget=fget, fset=fset) + @dynamic_property + def markerTags(self): + def fget(self): + return self.__markerTags + def fset(self, val): + self.__markerTags = val + return property(fget=fget, fset=fset) + @dynamic_property + def ncxSoup(self): + def fget(self): + return self.__ncxSoup + def fset(self, val): + self.__ncxSoup = val + return property(fget=fget, fset=fset) + @dynamic_property + def opts(self): + def fget(self): + return self.__opts + return property(fget=fget) + @dynamic_property + def playOrder(self): + def fget(self): + return self.__playOrder + def fset(self,val): + self.__playOrder = val + return property(fget=fget, fset=fset) + @dynamic_property + def plugin(self): + def fget(self): + return self.__plugin + return property(fget=fget) + @dynamic_property + def progressInt(self): + def fget(self): + return self.__progressInt + def fset(self, val): + self.__progressInt = val + return property(fget=fget, fset=fset) + @dynamic_property + def progressString(self): + def fget(self): + return self.__progressString + def fset(self, val): + self.__progressString = val + return property(fget=fget, fset=fset) + @dynamic_property + def reporter(self): + def fget(self): + return self.__reporter + def fset(self, val): + self.__reporter = val + return property(fget=fget, fset=fset) + @dynamic_property + def stylesheet(self): + def fget(self): + return self.__stylesheet + def fset(self, val): + self.__stylesheet = val + return property(fget=fget, fset=fset) + @dynamic_property + def thumbs(self): + def fget(self): + return self.__thumbs + def fset(self, val): + self.__thumbs = val + return property(fget=fget, fset=fset) + def thumbWidth(self): + def fget(self): + return self.__thumbWidth + def fset(self, val): + self.__thumbWidth = val + return property(fget=fget, fset=fset) + def thumbHeight(self): + def fget(self): + return self.__thumbHeight + def fset(self, val): + self.__thumbHeight = val + return property(fget=fget, fset=fset) + @dynamic_property + def title(self): + def fget(self): + return self.__title + def fset(self, val): + self.__title = val + return property(fget=fget, fset=fset) + @dynamic_property + def totalSteps(self): + def fget(self): + return self.__totalSteps + return property(fget=fget) + @dynamic_property + def useSeriesPrefixInTitlesSection(self): + def fget(self): + return self.__useSeriesPrefixInTitlesSection + def fset(self, val): + self.__useSeriesPrefixInTitlesSection = val + return property(fget=fget, fset=fset) + @dynamic_property + def verbose(self): + def fget(self): + return self.__verbose + def fset(self, val): + self.__verbose = val + return property(fget=fget, fset=fset) - @dynamic_property - def authorClip(self): - def fget(self): - return self.__authorClip - def fset(self, val): - self.__authorClip = val - return property(fget=fget, fset=fset) - @dynamic_property - def authors(self): - def fget(self): - return self.__authors - def fset(self, val): - self.__authors = val - return property(fget=fget, fset=fset) - @dynamic_property - def basename(self): - def fget(self): - return self.__basename - def fset(self, val): - self.__basename = val - return property(fget=fget, fset=fset) - @dynamic_property - def bookmarked_books(self): - def fget(self): - return self.__bookmarked_books - def fset(self, val): - self.__bookmarked_books = val - return property(fget=fget, fset=fset) - @dynamic_property - def booksByAuthor(self): - def fget(self): - return self.__booksByAuthor - def fset(self, val): - self.__booksByAuthor = val - return property(fget=fget, fset=fset) - @dynamic_property - def booksByDateRead(self): - def fget(self): - return self.__booksByDateRead - def fset(self, val): - self.__booksByDateRead = val - return property(fget=fget, fset=fset) - @dynamic_property - def booksByTitle(self): - def fget(self): - return self.__booksByTitle - def fset(self, val): - self.__booksByTitle = val - return property(fget=fget, fset=fset) - @dynamic_property - def booksByTitle_noSeriesPrefix(self): - def fget(self): - return self.__booksByTitle_noSeriesPrefix - def fset(self, val): - self.__booksByTitle_noSeriesPrefix = val - return property(fget=fget, fset=fset) - @dynamic_property - def catalogPath(self): - def fget(self): - return self.__catalogPath - def fset(self, val): - self.__catalogPath = val - return property(fget=fget, fset=fset) - @dynamic_property - def contentDir(self): - def fget(self): - return self.__contentDir - def fset(self, val): - self.__contentDir = val - return property(fget=fget, fset=fset) - @dynamic_property - def currentStep(self): - def fget(self): - return self.__currentStep - def fset(self, val): - self.__currentStep = val - return property(fget=fget, fset=fset) - @dynamic_property - def creator(self): - def fget(self): - return self.__creator - def fset(self, val): - self.__creator = val - return property(fget=fget, fset=fset) - @dynamic_property - def db(self): - def fget(self): - return self.__db - return property(fget=fget) - @dynamic_property - def descriptionClip(self): - def fget(self): - return self.__descriptionClip - def fset(self, val): - self.__descriptionClip = val - return property(fget=fget, fset=fset) - @dynamic_property - def error(self): - def fget(self): - return self.__error - return property(fget=fget) - @dynamic_property - def generateForKindle(self): - def fget(self): - return self.__generateForKindle - def fset(self, val): - self.__generateForKindle = val - return property(fget=fget, fset=fset) - @dynamic_property - def generateRecentlyRead(self): - def fget(self): - return self.__generateRecentlyRead - def fset(self, val): - self.__generateRecentlyRead = val - return property(fget=fget, fset=fset) - @dynamic_property - def genres(self): - def fget(self): - return self.__genres - def fset(self, val): - self.__genres = val - return property(fget=fget, fset=fset) - @dynamic_property - def genre_tags_dict(self): - def fget(self): - return self.__genre_tags_dict - def fset(self, val): - self.__genre_tags_dict = val - return property(fget=fget, fset=fset) - @dynamic_property - def htmlFileList(self): - def fget(self): - return self.__htmlFileList - def fset(self, val): - self.__htmlFileList = val - return property(fget=fget, fset=fset) - @dynamic_property - def libraryPath(self): - def fget(self): - return self.__libraryPath - def fset(self, val): - self.__libraryPath = val - return property(fget=fget, fset=fset) - @dynamic_property - def markerTags(self): - def fget(self): - return self.__markerTags - def fset(self, val): - self.__markerTags = val - return property(fget=fget, fset=fset) - @dynamic_property - def ncxSoup(self): - def fget(self): - return self.__ncxSoup - def fset(self, val): - self.__ncxSoup = val - return property(fget=fget, fset=fset) - @dynamic_property - def opts(self): - def fget(self): - return self.__opts - return property(fget=fget) - @dynamic_property - def playOrder(self): - def fget(self): - return self.__playOrder - def fset(self,val): - self.__playOrder = val - return property(fget=fget, fset=fset) - @dynamic_property - def plugin(self): - def fget(self): - return self.__plugin - return property(fget=fget) - @dynamic_property - def progressInt(self): - def fget(self): - return self.__progressInt - def fset(self, val): - self.__progressInt = val - return property(fget=fget, fset=fset) - @dynamic_property - def progressString(self): - def fget(self): - return self.__progressString - def fset(self, val): - self.__progressString = val - return property(fget=fget, fset=fset) - @dynamic_property - def reporter(self): - def fget(self): - return self.__reporter - def fset(self, val): - self.__reporter = val - return property(fget=fget, fset=fset) - @dynamic_property - def stylesheet(self): - def fget(self): - return self.__stylesheet - def fset(self, val): - self.__stylesheet = val - return property(fget=fget, fset=fset) - @dynamic_property - def thumbs(self): - def fget(self): - return self.__thumbs - def fset(self, val): - self.__thumbs = val - return property(fget=fget, fset=fset) - def thumbWidth(self): - def fget(self): - return self.__thumbWidth - def fset(self, val): - self.__thumbWidth = val - return property(fget=fget, fset=fset) - def thumbHeight(self): - def fget(self): - return self.__thumbHeight - def fset(self, val): - self.__thumbHeight = val - return property(fget=fget, fset=fset) - @dynamic_property - def title(self): - def fget(self): - return self.__title - def fset(self, val): - self.__title = val - return property(fget=fget, fset=fset) - @dynamic_property - def totalSteps(self): - def fget(self): - return self.__totalSteps - return property(fget=fget) - @dynamic_property - def useSeriesPrefixInTitlesSection(self): - def fget(self): - return self.__useSeriesPrefixInTitlesSection - def fset(self, val): - self.__useSeriesPrefixInTitlesSection = val - return property(fget=fget, fset=fset) - @dynamic_property - def verbose(self): - def fget(self): - return self.__verbose - def fset(self, val): - self.__verbose = val - return property(fget=fget, fset=fset) - - @dynamic_property - def NOT_READ_SYMBOL(self): - def fget(self): - return '' if self.generateForKindle else \ - '%s' % self.opts.read_tag - return property(fget=fget) - @dynamic_property - def READING_SYMBOL(self): - def fget(self): - return '' if self.generateForKindle else \ - '%s' % self.opts.read_tag - return property(fget=fget) - @dynamic_property - def READ_SYMBOL(self): - def fget(self): - return '' if self.generateForKindle else \ - '%s' % self.opts.read_tag - return property(fget=fget) - @dynamic_property - def FULL_RATING_SYMBOL(self): - def fget(self): - return "★" if self.generateForKindle else "*" - return property(fget=fget) - @dynamic_property - def EMPTY_RATING_SYMBOL(self): - def fget(self): - return "☆" if self.generateForKindle else ' ' - return property(fget=fget) - @dynamic_property - def READ_PROGRESS_SYMBOL(self): - def fget(self): - return "▪" if self.generateForKindle else '+' - return property(fget=fget) - @dynamic_property - def UNREAD_PROGRESS_SYMBOL(self): - def fget(self): - return "▫" if self.generateForKindle else '-' - return property(fget=fget) + @dynamic_property + def NOT_READ_SYMBOL(self): + def fget(self): + return '' if self.generateForKindle else \ + '%s' % self.opts.read_tag + return property(fget=fget) + @dynamic_property + def READING_SYMBOL(self): + def fget(self): + return '' if self.generateForKindle else \ + '%s' % self.opts.read_tag + return property(fget=fget) + @dynamic_property + def READ_SYMBOL(self): + def fget(self): + return '' if self.generateForKindle else \ + '%s' % self.opts.read_tag + return property(fget=fget) + @dynamic_property + def FULL_RATING_SYMBOL(self): + def fget(self): + return "★" if self.generateForKindle else "*" + return property(fget=fget) + @dynamic_property + def EMPTY_RATING_SYMBOL(self): + def fget(self): + return "☆" if self.generateForKindle else ' ' + return property(fget=fget) + @dynamic_property + def READ_PROGRESS_SYMBOL(self): + def fget(self): + return "▪" if self.generateForKindle else '+' + return property(fget=fget) + @dynamic_property + def UNREAD_PROGRESS_SYMBOL(self): + def fget(self): + return "▫" if self.generateForKindle else '-' + return property(fget=fget) # Methods def buildSources(self): @@ -1196,6 +1212,8 @@ class EPUB_MOBI(CatalogPlugin): self.generateHTMLByAuthor() if self.opts.generate_titles: self.generateHTMLByTitle() + if self.opts.generate_series: + self.generateHTMLBySeries() if self.opts.generate_recently_added: self.generateHTMLByDateAdded() if self.generateRecentlyRead: @@ -1206,15 +1224,17 @@ class EPUB_MOBI(CatalogPlugin): self.generateOPF() self.generateNCXHeader() - self.generateNCXDescriptions("Descriptions") self.generateNCXByAuthor("Authors") if self.opts.generate_titles: self.generateNCXByTitle("Titles") + if self.opts.generate_series: + self.generateNCXBySeries("Series") if self.opts.generate_recently_added: self.generateNCXByDateAdded("Recently Added") if self.generateRecentlyRead: self.generateNCXByDateRead("Recently Read") self.generateNCXByGenre("Genres") + self.generateNCXDescriptions("Descriptions") self.writeNCX() return True @@ -1569,13 +1589,26 @@ class EPUB_MOBI(CatalogPlugin): emTag = Tag(soup, "em") if title['series']: # title
series series_index - brTag = Tag(soup,'br') - title_tokens = title['title'].split(': ') - emTag.insert(0, escape(NavigableString(title_tokens[1]))) - emTag.insert(1, brTag) - smallTag = Tag(soup,'small') - smallTag.insert(0, escape(NavigableString(title_tokens[0]))) - emTag.insert(2, smallTag) + if self.opts.generate_series: + brTag = Tag(soup,'br') + title_tokens = list(title['title'].partition(':')) + emTag.insert(0, escape(NavigableString(title_tokens[2].strip()))) + emTag.insert(1, brTag) + smallTag = Tag(soup,'small') + aTag = Tag(soup,'a') + aTag['href'] = "%s.html#%s_series" % ('BySeries', + re.sub('\W','',title['series']).lower()) + aTag.insert(0, title_tokens[0]) + smallTag.insert(0, aTag) + emTag.insert(2, smallTag) + else: + brTag = Tag(soup,'br') + title_tokens = list(title['title'].partition(':')) + emTag.insert(0, escape(NavigableString(title_tokens[2].strip()))) + emTag.insert(1, brTag) + smallTag = Tag(soup,'small') + smallTag.insert(0, escape(NavigableString(title_tokens[0]))) + emTag.insert(2, smallTag) else: emTag.insert(0, NavigableString(escape(title['title']))) titleTag = body.find(attrs={'class':'title'}) @@ -1724,17 +1757,17 @@ class EPUB_MOBI(CatalogPlugin): body.insert(btc, aTag) btc += 1 - ''' - # We don't need this because the Kindle shows section titles - #

By Title

- h2Tag = Tag(soup, "h2") - aTag = Tag(soup, "a") - aTag['name'] = "bytitle" - h2Tag.insert(0,aTag) - h2Tag.insert(1,NavigableString('By Title (%d)' % len(self.booksByTitle))) - body.insert(btc,h2Tag) - btc += 1 - ''' + if not self.__generateForKindle: + # We don't need this because the Kindle shows section titles + #

By Title

+ pTag = Tag(soup, "p") + pTag['class'] = 'title' + aTag = Tag(soup, "a") + aTag['name'] = "bytitle" + pTag.insert(0,aTag) + pTag.insert(1,NavigableString('Titles')) + body.insert(btc,pTag) + btc += 1 #

#

@@ -1742,13 +1775,13 @@ class EPUB_MOBI(CatalogPlugin): dtc = 0 current_letter = "" - # 2/14/10 7:11 AM Experimental: re-sort title list without leading series/series_index + # Re-sort title list without leading series/series_index if not self.useSeriesPrefixInTitlesSection: nspt = deepcopy(self.booksByTitle) for book in nspt: if book['series']: - tokens = book['title'].split(': ') - book['title'] = '%s (%s)' % (tokens[1], tokens[0]) + tokens = book['title'].partition(':') + book['title'] = '%s (%s)' % (tokens[2].strip(), tokens[0]) book['title_sort'] = self.generateSortTitle(book['title']) nspt = sorted(nspt, key=lambda x:(x['title_sort'].upper(), x['title_sort'].upper())) @@ -1835,7 +1868,7 @@ class EPUB_MOBI(CatalogPlugin): # Write books by author A-Z self.updateProgressFullStep("'Authors'") - friendly_name = "By Author" + friendly_name = "Authors" soup = self.generateHTMLEmptyHeader(friendly_name) body = soup.find('body') @@ -1906,15 +1939,14 @@ class EPUB_MOBI(CatalogPlugin): current_series = None pAuthorTag = Tag(soup, "p") pAuthorTag['class'] = "author_index" - emTag = Tag(soup, "em") aTag = Tag(soup, "a") aTag['name'] = "%s" % self.generateAuthorAnchor(current_author) aTag.insert(0,NavigableString(current_author)) - emTag.insert(0,aTag) - pAuthorTag.insert(0,emTag) + pAuthorTag.insert(0,aTag) divTag.insert(dtc,pAuthorTag) dtc += 1 + ''' # Insert an


between non-series and series if not current_series and non_series_books and book['series']: # Insert an
@@ -1922,6 +1954,7 @@ class EPUB_MOBI(CatalogPlugin): hrTag['class'] = "series_divider" divTag.insert(dtc,hrTag) dtc += 1 + ''' # Check for series if book['series'] and book['series'] != current_series: @@ -1929,7 +1962,18 @@ class EPUB_MOBI(CatalogPlugin): current_series = book['series'] pSeriesTag = Tag(soup,'p') pSeriesTag['class'] = "series" - pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + book['series'])) + + if self.opts.generate_series: + aTag = Tag(soup,'a') + aTag['href'] = "%s.html#%s_series" % ('BySeries', + re.sub('\W','',book['series']).lower()) + aTag.insert(0, book['series']) + #pSeriesTag.insert(0, NavigableString(self.NOT_READ_SYMBOL)) + pSeriesTag.insert(0, aTag) + else: + #pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series'])) + pSeriesTag.insert(0,NavigableString('%s' % book['series'])) + divTag.insert(dtc,pSeriesTag) dtc += 1 if current_series and not book['series']: @@ -1957,30 +2001,34 @@ class EPUB_MOBI(CatalogPlugin): aTag = Tag(soup, "a") aTag['href'] = "book_%d.html" % (int(float(book['id']))) - # Use series, series index if avail else just title + # Use series, series index if avail else just title, + year of publication if current_series: - aTag.insert(0,escape(book['title'][len(book['series'])+1:])) + aTag.insert(0,'%s (%s)' % (escape(book['title'][len(book['series'])+1:]), + book['date'].split()[1])) else: - aTag.insert(0,escape(book['title'])) + aTag.insert(0,'%s (%s)' % (escape(book['title']), + book['date'].split()[1])) non_series_books += 1 pBookTag.insert(ptc, aTag) ptc += 1 + divTag.insert(dtc, pBookTag) dtc += 1 - ''' - # Insert the

tag with book_count at the head - #

By Author

- h2Tag = Tag(soup, "h2") - aTag = Tag(soup, "a") - anchor_name = friendly_name.lower() - aTag['name'] = anchor_name.replace(" ","") - h2Tag.insert(0,aTag) - h2Tag.insert(1,NavigableString('%s (%d)' % (friendly_name, book_count))) - body.insert(btc,h2Tag) - btc += 1 - ''' + if not self.__generateForKindle: + # Insert the

tag with book_count at the head + #

By Author

+ pTag = Tag(soup, "p") + pTag['class'] = 'title' + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + pTag.insert(0,aTag) + #h2Tag.insert(1,NavigableString('%s (%d)' % (friendly_name, book_count))) + pTag.insert(1,NavigableString('%s' % (friendly_name))) + body.insert(btc,pTag) + btc += 1 # Add the divTag to the body body.insert(btc, divTag) @@ -2023,15 +2071,14 @@ class EPUB_MOBI(CatalogPlugin): current_series = None pAuthorTag = Tag(soup, "p") pAuthorTag['class'] = "author_index" - emTag = Tag(soup, "em") aTag = Tag(soup, "a") aTag['name'] = "%s" % self.generateAuthorAnchor(current_author) aTag.insert(0,NavigableString(current_author)) - emTag.insert(0,aTag) - pAuthorTag.insert(0,emTag) + pAuthorTag.insert(0,aTag) divTag.insert(dtc,pAuthorTag) dtc += 1 + ''' # Insert an
between non-series and series if not current_series and non_series_books and new_entry['series']: # Insert an
@@ -2039,6 +2086,7 @@ class EPUB_MOBI(CatalogPlugin): hrTag['class'] = "series_divider" divTag.insert(dtc,hrTag) dtc += 1 + ''' # Check for series if new_entry['series'] and new_entry['series'] != current_series: @@ -2046,7 +2094,14 @@ class EPUB_MOBI(CatalogPlugin): current_series = new_entry['series'] pSeriesTag = Tag(soup,'p') pSeriesTag['class'] = "series" - pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + new_entry['series'])) + if self.opts.generate_series: + aTag = Tag(soup,'a') + aTag['href'] = "%s.html#%s_series" % ('BySeries', + re.sub('\W','',new_entry['series']).lower()) + aTag.insert(0, new_entry['series']) + pSeriesTag.insert(0, aTag) + else: + pSeriesTag.insert(0,NavigableString('%s' % new_entry['series'])) divTag.insert(dtc,pSeriesTag) dtc += 1 if current_series and not new_entry['series']: @@ -2160,18 +2215,18 @@ class EPUB_MOBI(CatalogPlugin): aTag['name'] = anchor_name.replace(" ","") body.insert(btc, aTag) btc += 1 - ''' - # We don't need this because the kindle inserts section titles - #

By Author

- h2Tag = Tag(soup, "h2") - aTag = Tag(soup, "a") - anchor_name = friendly_name.lower() - aTag['name'] = anchor_name.replace(" ","") - h2Tag.insert(0,aTag) - h2Tag.insert(1,NavigableString('%s' % friendly_name)) - body.insert(btc,h2Tag) - btc += 1 - ''' + + if not self.__generateForKindle: + #

By Author

+ pTag = Tag(soup, "p") + pTag['class'] = 'title' + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + pTag.insert(0,aTag) + pTag.insert(1,NavigableString('%s' % friendly_name)) + body.insert(btc,pTag) + btc += 1 #

#

@@ -2186,14 +2241,13 @@ class EPUB_MOBI(CatalogPlugin): nspt = deepcopy(self.booksByTitle) for book in nspt: if book['series']: - tokens = book['title'].split(': ') - book['title'] = '%s (%s)' % (tokens[1], tokens[0]) + tokens = book['title'].partition(':') + book['title'] = '%s (%s)' % (tokens[2].strip(), tokens[0]) book['title_sort'] = self.generateSortTitle(book['title']) self.booksByDateRange = sorted(nspt, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) date_range_list = [] today_time = nowf().replace(hour=23, minute=59, second=59) - books_added_in_date_range = False for (i, date) in enumerate(self.DATE_RANGE): date_range_limit = self.DATE_RANGE[i] if i: @@ -2206,18 +2260,20 @@ class EPUB_MOBI(CatalogPlugin): delta = today_time-book_time if delta.days <= date_range_limit: date_range_list.append(book) - books_added_in_date_range = True else: break dtc = add_books_to_HTML_by_date_range(date_range_list, date_range, dtc) date_range_list = [book] + ''' if books_added_in_date_range: # Add an


separating date ranges from months hrTag = Tag(soup,'hr') + hrTag['class'] = "description_divider" divTag.insert(dtc,hrTag) dtc += 1 + ''' # >>>> Books by month <<<< # Sort titles case-insensitive for by month using series prefix @@ -2437,6 +2493,174 @@ class EPUB_MOBI(CatalogPlugin): outfile.close() self.htmlFileList.append("content/ByDateRead.html") + def generateHTMLBySeries(self): + ''' + Generate a list of series + ''' + self.updateProgressFullStep("Fetching series") + + self.opts.sort_by = 'series' + + # Merge opts.exclude_tags with opts.search_text + # Updated to use exact match syntax + empty_exclude_tags = False if len(self.opts.exclude_tags) else True + search_phrase = 'series:true ' + if not empty_exclude_tags: + exclude_tags = self.opts.exclude_tags.split(',') + search_terms = [] + for tag in exclude_tags: + search_terms.append("tag:=%s" % tag) + search_phrase += "not (%s)" % " or ".join(search_terms) + + # If a list of ids are provided, don't use search_text + if self.opts.ids: + self.opts.search_text = search_phrase + else: + if self.opts.search_text: + self.opts.search_text += " " + search_phrase + else: + self.opts.search_text = search_phrase + + # Fetch the database as a dictionary + self.booksBySeries = self.plugin.search_sort_db(self.db, self.opts) + + friendly_name = "Series" + + soup = self.generateHTMLEmptyHeader(friendly_name) + body = soup.find('body') + + btc = 0 + + # Insert section tag + aTag = Tag(soup,'a') + aTag['name'] = 'section_start' + body.insert(btc, aTag) + btc += 1 + + # Insert the anchor + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + body.insert(btc, aTag) + btc += 1 + + #

+ #

+ divTag = Tag(soup, "div") + dtc = 0 + current_letter = "" + current_series = None + + # Loop through booksBySeries + series_count = 0 + for book in self.booksBySeries: + # Check for initial letter change + sort_title = self.generateSortTitle(book['series']) + if self.letter_or_symbol(sort_title[0].upper()) != current_letter : + ''' + # Start a new letter - anchor only, hidden + current_letter = book['author_sort'][0].upper() + aTag = Tag(soup, "a") + aTag['name'] = "%sseries" % current_letter + divTag.insert(dtc, aTag) + dtc += 1 + ''' + # Start a new letter with Index letter + current_letter = self.letter_or_symbol(sort_title[0].upper()) + pIndexTag = Tag(soup, "p") + pIndexTag['class'] = "letter_index" + aTag = Tag(soup, "a") + aTag['name'] = "%s_series" % self.letter_or_symbol(current_letter) + pIndexTag.insert(0,aTag) + pIndexTag.insert(1,NavigableString(self.letter_or_symbol(sort_title[0].upper()))) + divTag.insert(dtc,pIndexTag) + dtc += 1 + + # Check for series change + if book['series'] != current_series: + # Start a new series + series_count += 1 + current_series = book['series'] + pSeriesTag = Tag(soup,'p') + pSeriesTag['class'] = "series" + aTag = Tag(soup, 'a') + aTag['name'] = "%s_series" % re.sub('\W','',book['series']).lower() + pSeriesTag.insert(0,aTag) + pSeriesTag.insert(1,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series'])) + divTag.insert(dtc,pSeriesTag) + dtc += 1 + + # Add books + pBookTag = Tag(soup, "p") + ptc = 0 + + # book with read/reading/unread symbol + if 'read' in book and book['read']: + # check mark + pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) + pBookTag['class'] = "read_book" + ptc += 1 + elif book['id'] in self.bookmarked_books: + pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) + pBookTag['class'] = "read_book" + ptc += 1 + else: + # hidden check mark + pBookTag['class'] = "unread_book" + pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) + ptc += 1 + + aTag = Tag(soup, "a") + aTag['href'] = "book_%d.html" % (int(float(book['id']))) + # Use series, series index if avail else just title + #aTag.insert(0,'%d. %s · %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors']))) + + # Link to book + aTag.insert(0,'%d. %s (%s)' % (book['series_index'], + escape(book['title']), + strftime(u'%Y', book['pubdate'].timetuple()))) + pBookTag.insert(ptc, aTag) + ptc += 1 + + # · + pBookTag.insert(ptc, NavigableString(' · ')) + ptc += 1 + + # Link to author + aTag = Tag(soup, "a") + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", + self.generateAuthorAnchor(escape(' & '.join(book['authors'])))) + aTag.insert(0, NavigableString(' & '.join(book['authors']))) + pBookTag.insert(ptc, aTag) + ptc += 1 + + divTag.insert(dtc, pBookTag) + dtc += 1 + + if not self.__generateForKindle: + # Insert the

tag with book_count at the head + #

By Series

+ pTag = Tag(soup, "p") + pTag['class'] = 'title' + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + pTag.insert(0,aTag) + #h2Tag.insert(1,NavigableString('%s (%d)' % (friendly_name, series_count))) + pTag.insert(1,NavigableString('%s' % friendly_name)) + body.insert(btc,pTag) + btc += 1 + + # Add the divTag to the body + body.insert(btc, divTag) + + # Write the generated file to contentdir + outfile_spec = "%s/BySeries.html" % (self.contentDir) + outfile = open(outfile_spec, 'w') + outfile.write(soup.prettify()) + outfile.close() + self.htmlFileList.append("content/BySeries.html") + def generateHTMLByTags(self): # Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ... # Note that special tags - ~+*[] - have already been filtered from books[] @@ -2683,22 +2907,7 @@ class EPUB_MOBI(CatalogPlugin): # HTML files - add books to manifest and spine sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \ else self.booksByTitle - for book in sort_descriptions_by: - # manifest - itemTag = Tag(soup, "item") - itemTag['href'] = "content/book_%d.html" % int(book['id']) - itemTag['id'] = "book%d" % int(book['id']) - itemTag['media-type'] = "application/xhtml+xml" - manifest.insert(mtc, itemTag) - mtc += 1 - - # spine - itemrefTag = Tag(soup, "itemref") - itemrefTag['idref'] = "book%d" % int(book['id']) - spine.insert(stc, itemrefTag) - stc += 1 - - # Add other html_files to manifest and spine + # Add html_files to manifest and spine for file in self.htmlFileList: itemTag = Tag(soup, "item") @@ -2734,6 +2943,21 @@ class EPUB_MOBI(CatalogPlugin): spine.insert(stc, itemrefTag) stc += 1 + for book in sort_descriptions_by: + # manifest + itemTag = Tag(soup, "item") + itemTag['href'] = "content/book_%d.html" % int(book['id']) + itemTag['id'] = "book%d" % int(book['id']) + itemTag['media-type'] = "application/xhtml+xml" + manifest.insert(mtc, itemTag) + mtc += 1 + + # spine + itemrefTag = Tag(soup, "itemref") + itemrefTag['idref'] = "book%d" % int(book['id']) + spine.insert(stc, itemrefTag) + stc += 1 + # Guide referenceTag = Tag(soup, "reference") referenceTag['type'] = 'masthead' @@ -2769,7 +2993,8 @@ class EPUB_MOBI(CatalogPlugin): navLabelTag.insert(0, textTag) navPointTag.insert(0, navLabelTag) contentTag = Tag(soup, 'content') - contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id']) + #contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id']) + contentTag['src'] = "content/ByAlphaAuthor.html" navPointTag.insert(1, contentTag) cmiTag = Tag(soup, '%s' % 'calibre:meta-img') cmiTag['name'] = "mastheadImage" @@ -2821,15 +3046,15 @@ class EPUB_MOBI(CatalogPlugin): navLabelTag = Tag(ncx_soup, "navLabel") textTag = Tag(ncx_soup, "text") if book['series']: - tokens = book['title'].split(': ') + tokens = list(book['title'].partition(':')) if self.generateForKindle: # Don't include Author for Kindle textTag.insert(0, NavigableString(self.formatNCXText('%s (%s)' % \ - (tokens[1], tokens[0]), dest='title'))) + (tokens[2].strip(), tokens[0]), dest='title'))) else: # Include Author for non-Kindle textTag.insert(0, NavigableString(self.formatNCXText('%s · %s (%s)' % \ - (tokens[1], book['author'], tokens[0]), dest='title'))) + (tokens[2].strip(), book['author'], tokens[0]), dest='title'))) else: if self.generateForKindle: # Don't include Author for Kindle @@ -2882,6 +3107,98 @@ class EPUB_MOBI(CatalogPlugin): self.ncxSoup = ncx_soup + def generateNCXBySeries(self, tocTitle): + self.updateProgressFullStep("NCX 'Series'") + + def add_to_series_by_letter(current_series_list): + current_series_list = " • ".join(current_series_list) + current_series_list = self.formatNCXText(current_series_list, dest="description") + series_by_letter.append(current_series_list) + + soup = self.ncxSoup + output = "BySeries" + body = soup.find("navPoint") + btc = len(body.contents) + + # --- Construct the 'Books By Series' section --- + navPointTag = Tag(soup, 'navPoint') + navPointTag['class'] = "section" + navPointTag['id'] = "byseries-ID" + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + textTag.insert(0, NavigableString(tocTitle)) + navLabelTag.insert(0, textTag) + nptc = 0 + navPointTag.insert(nptc, navLabelTag) + nptc += 1 + contentTag = Tag(soup,"content") + contentTag['src'] = "content/%s.html#section_start" % (output) + navPointTag.insert(nptc, contentTag) + nptc += 1 + + series_by_letter = [] + + # Loop over the series titles, find start of each letter, add description_preview_count books + # Special switch for using different title list + title_list = self.booksBySeries + current_letter = self.letter_or_symbol(title_list[0]['series'][0]) + title_letters = [current_letter] + current_series_list = [] + current_series = "" + for book in title_list: + sort_title = self.generateSortTitle(book['series']) + if self.letter_or_symbol(sort_title[0]) != current_letter: + # Save the old list + add_to_series_by_letter(current_series_list) + + # Start the new list + current_letter = self.letter_or_symbol(sort_title[0]) + title_letters.append(current_letter) + current_series = book['series'] + current_series_list = [book['series']] + else: + if len(current_series_list) < self.descriptionClip and \ + book['series'] != current_series : + current_series = book['series'] + current_series_list.append(book['series']) + + # Add the last book list + add_to_series_by_letter(current_series_list) + + # Add *article* entries for each populated series title letter + for (i,books) in enumerate(series_by_letter): + navPointByLetterTag = Tag(soup, 'navPoint') + navPointByLetterTag['class'] = "article" + navPointByLetterTag['id'] = "%sSeries-ID" % (title_letters[i].upper()) + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + textTag.insert(0, NavigableString(u"Series beginning with %s" % \ + (title_letters[i] if len(title_letters[i])>1 else "'" + title_letters[i] + "'"))) + navLabelTag.insert(0, textTag) + navPointByLetterTag.insert(0,navLabelTag) + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/%s.html#%s_series" % (output, title_letters[i]) + navPointByLetterTag.insert(1,contentTag) + + if self.generateForKindle: + cmTag = Tag(soup, '%s' % 'calibre:meta') + cmTag['name'] = "description" + cmTag.insert(0, NavigableString(self.formatNCXText(books, dest='description'))) + navPointByLetterTag.insert(2, cmTag) + + navPointTag.insert(nptc, navPointByLetterTag) + nptc += 1 + + # Add this section to the body + body.insert(btc, navPointTag) + btc += 1 + + self.ncxSoup = soup + def generateNCXByTitle(self, tocTitle): self.updateProgressFullStep("NCX 'Titles'") @@ -3713,7 +4030,7 @@ class EPUB_MOBI(CatalogPlugin): btc += 1 titleTag = body.find(attrs={'class':'title'}) - titleTag.insert(0,NavigableString('%s' % escape(self.getFriendlyGenreTag(genre)))) + titleTag.insert(0,NavigableString('%s' % escape(self.getFriendlyGenreTag(genre)))) # Insert the books by author list divTag = body.find(attrs={'class':'authors'}) @@ -3729,15 +4046,14 @@ class EPUB_MOBI(CatalogPlugin): current_series = None pAuthorTag = Tag(soup, "p") pAuthorTag['class'] = "author_index" - emTag = Tag(soup, "em") aTag = Tag(soup, "a") aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) aTag.insert(0, book['author']) - emTag.insert(0,aTag) - pAuthorTag.insert(0,emTag) + pAuthorTag.insert(0,aTag) divTag.insert(dtc,pAuthorTag) dtc += 1 + ''' # Insert an
between non-series and series if not current_series and non_series_books and book['series']: # Insert an
@@ -3745,6 +4061,7 @@ class EPUB_MOBI(CatalogPlugin): hrTag['class'] = "series_divider" divTag.insert(dtc,hrTag) dtc += 1 + ''' # Check for series if book['series'] and book['series'] != current_series: @@ -3752,7 +4069,14 @@ class EPUB_MOBI(CatalogPlugin): current_series = book['series'] pSeriesTag = Tag(soup,'p') pSeriesTag['class'] = "series" - pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + book['series'])) + if self.opts.generate_series: + aTag = Tag(soup,'a') + aTag['href'] = "%s.html#%s_series" % ('BySeries', + re.sub('\W','',book['series']).lower()) + aTag.insert(0, book['series']) + pSeriesTag.insert(0, aTag) + else: + pSeriesTag.insert(0,NavigableString('%s' % book['series'])) divTag.insert(dtc,pSeriesTag) dtc += 1 @@ -3809,7 +4133,7 @@ class EPUB_MOBI(CatalogPlugin): def generateHTMLDescriptionHeader(self, title): title_border = '' if self.opts.fmt == 'epub' else \ - '

' + '
' header = ''' @@ -3855,7 +4179,7 @@ class EPUB_MOBI(CatalogPlugin):   -

+
@@ -3897,7 +4221,7 @@ class EPUB_MOBI(CatalogPlugin):

-

+
@@ -4049,6 +4373,12 @@ class EPUB_MOBI(CatalogPlugin): except: self.opts.log.error("generateThumbnail(): Error with %s" % title['title']) + def getFriendlyGenreTag(self, genre): + # Find the first instance of friendly_tag matching genre + for friendly_tag in self.genre_tags_dict: + if self.genre_tags_dict[friendly_tag] == genre: + return friendly_tag + def getMarkerTags(self): ''' Return a list of special marker tags to be excluded from genre list ''' markerTags = [] @@ -4063,12 +4393,6 @@ class EPUB_MOBI(CatalogPlugin): else: return char - def getFriendlyGenreTag(self, genre): - # Find the first instance of friendly_tag matching genre - for friendly_tag in self.genre_tags_dict: - if self.genre_tags_dict[friendly_tag] == genre: - return friendly_tag - def markdownComments(self, comments): ''' Convert random comment text to normalized, xml-legal block of

s @@ -4224,13 +4548,15 @@ class EPUB_MOBI(CatalogPlugin): opts.fmt = self.fmt = path_to_output.rpartition('.')[2] # Add local options - opts.creator = "calibre" + opts.creator = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y')) + opts.creator_sort_as = '%s %s' % ('calibre', strftime('%Y-%m-%d')) opts.connected_kindle = False # Finalize output_profile op = opts.output_profile if op is None: op = 'default' + if opts.connected_device['name'] and 'kindle' in opts.connected_device['name'].lower(): opts.connected_kindle = True if opts.connected_device['serial'] and opts.connected_device['serial'][:4] in ['B004','B005']: @@ -4256,7 +4582,8 @@ class EPUB_MOBI(CatalogPlugin): opts.exclude_genre = '\[^.\]' build_log.append(" converting empty exclude_genre to '\[^.\]'") - if opts.connected_device['name']: + if opts.connected_device['is_device_connected'] and \ + opts.connected_device['kind'] == 'device': if opts.connected_device['serial']: build_log.append(u" connected_device: '%s' #%s%s " % \ (opts.connected_device['name'], @@ -4267,9 +4594,14 @@ class EPUB_MOBI(CatalogPlugin): build_log.append(u" mount point: %s" % storage) else: build_log.append(u" connected_device: '%s'" % opts.connected_device['name']) - for storage in opts.connected_device['storage']: - if storage: - build_log.append(u" mount point: %s" % storage) + try: + for storage in opts.connected_device['storage']: + if storage: + build_log.append(u" mount point: %s" % storage) + except: + build_log.append(u" (no mount points)") + else: + build_log.append(u" connected_device: '%s'" % opts.connected_device['name']) opts_dict = vars(opts) if opts_dict['ids']: diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index ea42fe5998..9a2d0b0a62 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -324,6 +324,7 @@ def do_remove(db, ids): db.delete_book(y) send_message() + db.clean() def remove_option_parser(): return get_parser(_( @@ -449,6 +450,7 @@ def command_show_metadata(args, dbpath): def do_set_metadata(db, id, stream): mi = OPF(stream) db.set_metadata(id, mi) + db.clean() do_show_metadata(db, id, False) send_message() @@ -574,6 +576,9 @@ def command_add_custom_column(args, dbpath): return 1 do_add_custom_column(get_db(dbpath, opts), args[0], args[1], args[2], opts.is_multiple, json.loads(opts.display)) + # Re-open the DB so that field_metadata is reflects the column changes + db = get_db(dbpath, opts) + db.prefs['field_metadata'] = db.field_metadata.all_metadata() return 0 def catalog_option_parser(args): @@ -672,7 +677,14 @@ def command_catalog(args, dbpath): # No support for connected device in CLI environment # Parallel initialization in calibre.gui2.tools:generate_catalog() - opts.connected_device = { 'storage':None,'serial':None,'save_template':None,'name':None} + opts.connected_device = { + 'is_device_connected': False, + 'kind': None, + 'name': None, + 'save_template': None, + 'serial': None, + 'storage': None, + } with plugin: plugin.run(args[1], opts, get_db(dbpath, opts)) @@ -797,6 +809,9 @@ def command_remove_custom_column(args, dbpath): return 1 do_remove_custom_column(get_db(dbpath, opts), args[0], opts.force) + # Re-open the DB so that field_metadata is reflects the column changes + db = get_db(dbpath, opts) + db.prefs['field_metadata'] = db.field_metadata.all_metadata() return 0 def saved_searches_option_parser(): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 4b6fb2f7f2..4106f8c965 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -144,6 +144,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.initialize_dynamic() def initialize_dynamic(self): + self.field_metadata = FieldMetadata() #Ensure we start with a clean copy self.prefs = DBPrefs(self) defs = self.prefs.defaults defs['gui_restriction'] = defs['cs_restriction'] = '' @@ -289,10 +290,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # Reconstruct the user categories, putting them into field_metadata # Assumption is that someone else will fix them if they change. + self.field_metadata.remove_dynamic_categories() tb_cats = self.field_metadata - for k in tb_cats.keys(): - if tb_cats[k]['kind'] in ['user', 'search']: - del tb_cats[k] for user_cat in sorted(self.prefs.get('user_categories', {}).keys()): cat_name = user_cat+':' # add the ':' to avoid name collision tb_cats.add_user_category(label=cat_name, name=user_cat) @@ -331,7 +330,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn', 'publisher', 'rating', 'series', 'series_index', 'tags', - 'title', 'timestamp', 'uuid', 'pubdate'): + 'title', 'timestamp', 'uuid', 'pubdate', 'ondevice'): setattr(self, prop, functools.partial(get_property, loc=self.FIELD_MAP['comments' if prop == 'comment' else prop])) setattr(self, 'title_sort', functools.partial(get_property, @@ -639,16 +638,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def book_on_device_string(self, id): loc = [] + count = 0 on = self.book_on_device(id) if on is not None: - m, a, b = on + m, a, b, count = on[:4] if m is not None: loc.append(_('Main')) if a is not None: loc.append(_('Card A')) if b is not None: loc.append(_('Card B')) - return ', '.join(loc) + return ', '.join(loc) + ((' (%s books)'%count) if count > 1 else '') def set_book_on_device_func(self, func): self.book_on_device_func = func @@ -1125,7 +1125,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if not authors: authors = [_('Unknown')] self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,)) - self.conn.execute('DELETE FROM authors WHERE (SELECT COUNT(id) FROM books_authors_link WHERE author=authors.id) < 1') for a in authors: if not a: continue @@ -2151,8 +2150,6 @@ books_series_link feeds os.remove(self.dbpath) shutil.copyfile(dest, self.dbpath) self.connect() - self.field_metadata.remove_dynamic_categories() - self.field_metadata.remove_custom_fields() self.initialize_dynamic() self.refresh() if os.path.exists(dest): diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index e28b6d422a..66cdee51f0 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -306,7 +306,7 @@ class FieldMetadata(dict): self._tb_cats[k]['label'] = k self._tb_cats[k]['display'] = {} self._tb_cats[k]['is_editable'] = True - self._add_search_terms_to_map(k, self._tb_cats[k]['search_terms']) + self._add_search_terms_to_map(k, v['search_terms']) self.custom_field_prefix = '#' self.get = self._tb_cats.get @@ -371,6 +371,12 @@ class FieldMetadata(dict): def get_custom_fields(self): return [l for l in self._tb_cats if self._tb_cats[l]['is_custom']] + def all_metadata(self): + l = {} + for k in self._tb_cats: + l[k] = self._tb_cats[k] + return l + def get_custom_field_metadata(self): l = {} for k in self._tb_cats: @@ -408,10 +414,6 @@ class FieldMetadata(dict): self._add_search_terms_to_map(key, [key]) self.custom_label_to_key_map[label+'_index'] = key - def remove_custom_fields(self): - for key in self.get_custom_fields(): - del self._tb_cats[key] - def remove_dynamic_categories(self): for key in list(self._tb_cats.keys()): val = self._tb_cats[key] diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 15020855f7..f5c4063789 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -61,7 +61,7 @@ def config(defaults=None): 'actual e-book file(s).')) x('formats', default='all', help=_('Comma separated list of formats to save for each book.' - ' By default all available books are saved.')) + ' By default all available formats are saved.')) x('template', default=DEFAULT_TEMPLATE, help=_('The template to control the filename and directory structure of the saved files. ' 'Default is "%s" which will save books into a per-author ' diff --git a/src/calibre/manual/custom.py b/src/calibre/manual/custom.py index eb0a65ac33..80eeb59e0b 100644 --- a/src/calibre/manual/custom.py +++ b/src/calibre/manual/custom.py @@ -28,7 +28,7 @@ Command Line Interface .. image:: ../images/cli.png -On OS X you have to go to Preferences->Advanced and click install command line +On OS X you have to go to Preferences->Advanced->Miscellaneous and click install command line tools to make the command line tools available. On other platforms, just start a terminal and type the command. diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index 0dd2e349f3..c35defc0b0 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -11,7 +11,7 @@ Customizing |app| *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 specify your own static resources like icons and templates to override the defaults and finally how to -use *plugins* to add funtionality to |app|. +use *plugins* to add functionality to |app|. .. contents:: :depth: 2 @@ -45,7 +45,7 @@ All static resources are stored in the resources sub-folder of the calibre insta from the calibre website it will be :file:`/opt/calibre/resources`. These paths can change depending on where you choose to install |app|. You should not change the files in this resources folder, as your changes will get overwritten the next time you update |app|. Instead, go to -:guilabel:`Preferences->Advanced` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. Place the files in the appropriate sub folders, for example place images in :file:`resources/images`, etc. +:guilabel:`Preferences->Advanced->Miscellaneous` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. Place the files in the appropriate sub folders, for example place images in :file:`resources/images`, etc. |app| will automatically use your custom file in preference to the builtin one the next time it is started. For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is diff --git a/src/calibre/manual/develop.rst b/src/calibre/manual/develop.rst index ca067e45bc..f95d51bfca 100644 --- a/src/calibre/manual/develop.rst +++ b/src/calibre/manual/develop.rst @@ -123,7 +123,7 @@ the previously checked out calibre code directory, for example:: cd /Users/kovid/work/calibre -calibre is the directory that contains the src and resources sub directories. Ensure you have installed the |app| commandline tools via Preferences->Advanced in the |app| GUI. +calibre is the directory that contains the src and resources sub directories. Ensure you have installed the |app| commandline tools via :guilabel:Preferences->Advanced->Miscellaneous in the |app| GUI. The next step is to set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path to the src directory. So, following the example above, it would be ``/Users/kovid/work/calibre/src``. Apple diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 0a32f442c0..b93444f4c3 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -62,7 +62,7 @@ How do I convert my file containing non-English characters, or smart quotes? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are two aspects to this problem: 1. Knowing the encoding of the source file: |app| tries to guess what character encoding your source files use, but often, this is impossible, so you need to tell it what encoding to use. This can be done in the GUI via the :guilabel:`Input character encoding` field in the :guilabel:`Look & Feel` section. The command-line tools all have an :option:`--input-encoding` option. - 2. When adding HTML files to |app|, you may need to tell |app| what encoding the files are in. To do this go to Preferences->Plugins->File Type plugins and customize the HTML2Zip plugin, telling it what encoding your HTML files are in. Now when you add HTML files to |app| they will be correctly processed. HTML files from different sources often have different encodings, so you may have to change this setting repeatedly. A common encoding for many files from the web is ``cp1252`` and I would suggest you try that first. Note that when converting HTML files, leave the input encoding setting mentioned above blank. This is because the HTML2ZIP plugin automatically converts the HTML files to a standard encoding (utf-8). + 2. When adding HTML files to |app|, you may need to tell |app| what encoding the files are in. To do this go to :guilabel:`Preferences->Advanced->Plugins->File Type plugins` and customize the HTML2Zip plugin, telling it what encoding your HTML files are in. Now when you add HTML files to |app| they will be correctly processed. HTML files from different sources often have different encodings, so you may have to change this setting repeatedly. A common encoding for many files from the web is ``cp1252`` and I would suggest you try that first. Note that when converting HTML files, leave the input encoding setting mentioned above blank. This is because the HTML2ZIP plugin automatically converts the HTML files to a standard encoding (utf-8). 3. Embedding fonts: If you are generating an LRF file to read on your SONY Reader, you are limited by the fact that the Reader only supports a few non-English characters in the fonts it comes pre-loaded with. You can work around this problem by embedding a unicode-aware font that supports the character set your file uses into the LRF file. You should embed atleast a serif and a sans-serif font. Be aware that embedding fonts significantly slows down page-turn speed on the reader. @@ -92,7 +92,7 @@ We just need some information from you: * What e-book formats does your device support? * Is there a special directory on the device in which all e-book files should be placed? * We also need information about your device that |app| will collect automatically. First, if your - device supports SD cards, insert them. Then connect your device. In calibre go to Preferences->Advanced + device supports SD cards, insert them. Then connect your device. In calibre go to :guilabel:`Preferences->Advanced->Miscellaneous` and click the "Debug device detection" button. This will create some debug output. Copy it to a file and repeat the process, this time with your device disconnected. * Send both the above outputs to us with the other information and we will write a device driver for your @@ -109,11 +109,11 @@ of which books are members are shown on the device view. When you send a book to the reader, |app| will add the book to collections based on the metadata for that book. By default, collections are created from tags and series. You can control what metadata is used by going to -Preferences->Plugins->Device Interface plugins and customizing the SONY device interface plugin. If you remove all +:guilabel:`Preferences->Advanced->Plugins->Device Interface plugins` and customizing the SONY device interface plugin. If you remove all values, |app| will not add the book to any collection. Collection management is largely controlled by the 'Metadata management' option found at -Preferences->Add/Save->Sending to device. If set to 'Manual' (the default), managing collections is left to +:guilabel:`Preferences->Import/Export->Sending books to devices`. If set to 'Manual' (the default), managing collections is left to the user; |app| will not delete already existing collections for a book on your reader when you resend the book to the reader, but |app| will add the book to collections if necessary. To ensure that the collections for a book are based only on current |app| metadata, first delete the books from the reader, then resend the @@ -185,8 +185,8 @@ The easiest way to browse your |app| collection on your Apple device (iPad/iPhon 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 + * Set the Preferred Output Format in |app| to EPUB (The output format can be set under :guilabel:`Preferences->Interface->Behavior`) + * Set the output profile to iPad (this will work for iPhone/iPods as well), under :guilabel:`Preferences->Conversion->Common Options->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. @@ -217,7 +217,7 @@ Can I access my |app| books using the web browser in my Kindle or other reading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |app| has a *Content Server* that exports the books in |app| as a web page. You can turn it on under -Preferences->Content Server. Then just point the web browser on your device to the computer running +:guilabel:`Preferences->Network->Sharing over the net`. Then just point the web browser on your device to the computer running the Content Server and you will be able to browse your book collection. For example, if the computer running the server has IP address 63.45.128.5, in the browser, you would type:: @@ -277,14 +277,14 @@ In |app|, you would instead use tags to mark genre and read status and then just Why doesn't |app| have a column for foo? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -|app| is designed to have columns for the most frequently and widely used fields. In addition, you can add any columns you like. Columns can be added via Preferences->Interface. +|app| is designed to have columns for the most frequently and widely used fields. In addition, you can add any columns you like. Columns can be added via :guilabel:`Preferences->Interface->Add your own columns`. Watch the tutorial `UI Power tips `_ to learn how to create your own columns. How do I move my |app| library from one computer to another? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder. -Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to Preferences->Advanced and click the Check database integrity button. It will warn you about missing files, if any, which you should then transfer by hand. +Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to :guilabel:`Preferences->Advanced->Miscellaneous` and click the "Check database integrity button". It will warn you about missing files, if any, which you should then transfer by hand. Content From The Web diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index e9573e91be..aa49c51b76 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -345,6 +345,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes - Show book details * - :kbd:`M` - Merge selected records + * - :kbd:`Alt+M` + - Merge selected records, keeping originals * - :kbd:`O` - Open containing folder * - :kbd:`S` diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst index 8b6919db90..1b9b47ed3d 100644 --- a/src/calibre/manual/plugins.rst +++ b/src/calibre/manual/plugins.rst @@ -165,3 +165,19 @@ User Interface Actions :members: :member-order: bysource +Preferences Plugins +-------------------------- + +.. autoclass:: calibre.customize.PreferencesPlugin + :show-inheritance: + :members: + :member-order: bysource + +.. autoclass:: calibre.gui2.preferences.ConfigWidgetInterface + :members: + :member-order: bysource + +.. autoclass:: calibre.gui2.preferences.ConfigWidgetBase + :members: + :member-order: bysource + diff --git a/src/calibre/manual/portable.rst b/src/calibre/manual/portable.rst index 2a88107842..a2c8e323d8 100644 --- a/src/calibre/manual/portable.rst +++ b/src/calibre/manual/portable.rst @@ -46,8 +46,8 @@ The steps required to prepare the USB stick are as follows: * Deselect the options for creating Menu shortcuts; creating a calibre shortcut on the desktop; and adding Calibre to the path * Create the CalibreLibrary folder inside the Calibre_Root_Folder. If you have an existing Calibre library copy it and all its contents to the CalibreLibrary folder. If you do not already have a library do not worry as a new one will be created at this location when Calibre is started. - * Create the CalibreConfig folder inside the Calibre_Root_Folder. This will hold your personal Calibre configuration settings. If you have an existing Calibre installation and want to copy the current settings then copy the contents of your current configuration folder to the CalibreConfig folder. You can find the location of your current configuration folder by going to Preferences->Advanced and clicking the “Open calibre configuration Directory” button. - * When you have started Calibre, go into Preferences->General and check that you have set the Job Priority to ‘Low’. This setting keeps single-processor Windows systems responsive without affecting Calibre performance to any noticeable degree. On multi-processor or multi-core systems this setting does not matter as much, but setting it will do no harm. + * Create the CalibreConfig folder inside the Calibre_Root_Folder. This will hold your personal Calibre configuration settings. If you have an existing Calibre installation and want to copy the current settings then copy the contents of your current configuration folder to the CalibreConfig folder. You can find the location of your current configuration folder by going to :guilabel:`Preferences->Advanced->Miscellaneous` and clicking the “Open calibre configuration Directory” button. + * When you have started Calibre, go into :guilabel:`Preferences->Interface->Behavior` and check that you have set the Job Priority to ‘Low’. This setting keeps single-processor Windows systems responsive without affecting Calibre performance to any noticeable degree. On multi-processor or multi-core systems this setting does not matter as much, but setting it will do no harm. Using calibre-portable.bat --------------------------- diff --git a/src/calibre/manual/viewer.rst b/src/calibre/manual/viewer.rst index dd70674b99..8fdbb4b021 100644 --- a/src/calibre/manual/viewer.rst +++ b/src/calibre/manual/viewer.rst @@ -18,7 +18,7 @@ Starting the viewer You can view any of the books in your |app| library by selecting the book and pressing the View button. This will open up the book in the e-book viewer. You can also launch the viewer by itself, from the Start menu in windows or using the command :command:`ebook-viewer` in Linux and OS X (you have to install the command line tools on OS X -first by going to Preferences->Advanced). +first by going to :guilabel:`Preferences->Advanced->Miscellaneous`). Navigating around an e-book ----------------------------- diff --git a/src/calibre/translations/ar.po b/src/calibre/translations/ar.po index 8ef4a782d7..2538d0a304 100644 --- a/src/calibre/translations/ar.po +++ b/src/calibre/translations/ar.po @@ -7,14 +7,14 @@ msgid "" msgstr "" "Project-Id-Version: calibre\n" "Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2010-08-27 18:12+0000\n" -"PO-Revision-Date: 2010-08-27 20:34+0000\n" -"Last-Translator: Kovid Goyal \n" +"POT-Creation-Date: 2010-09-05 23:43+0000\n" +"PO-Revision-Date: 2010-09-06 05:21+0000\n" +"Last-Translator: Hsn \n" "Language-Team: Arabic \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-08-28 03:35+0000\n" +"X-Launchpad-Export-Date: 2010-09-07 04:46+0000\n" "X-Generator: Launchpad (build Unknown)\n" #: /home/kovid/work/calibre/src/calibre/customize/__init__.py:43 @@ -22,8 +22,9 @@ msgid "Does absolutely nothing" msgstr "لا يفعل شيءً" #: /home/kovid/work/calibre/src/calibre/customize/__init__.py:46 -#: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:72 +#: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:74 #: /home/kovid/work/calibre/src/calibre/devices/kindle/driver.py:76 +#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:395 #: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:70 #: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:71 #: /home/kovid/work/calibre/src/calibre/devices/prs500/books.py:267 @@ -106,16 +107,16 @@ msgstr "لا يفعل شيءً" #: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:330 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:289 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:292 -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:172 -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:179 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:41 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:137 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:144 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:42 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:111 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:136 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:138 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:869 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:878 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1163 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1166 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:862 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:871 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1155 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1158 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:47 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:120 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:155 @@ -130,17 +131,17 @@ msgstr "لا يفعل شيءً" #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:186 #: /home/kovid/work/calibre/src/calibre/library/cli.py:213 #: /home/kovid/work/calibre/src/calibre/library/database.py:913 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:362 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:374 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1007 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1076 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1760 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1762 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1889 -#: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:201 -#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:136 -#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:139 -#: /home/kovid/work/calibre/src/calibre/library/server/xml.py:70 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:375 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:387 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1057 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1126 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1825 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1827 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1954 +#: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:211 +#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:137 +#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:140 +#: /home/kovid/work/calibre/src/calibre/library/server/xml.py:71 #: /home/kovid/work/calibre/src/calibre/utils/localization.py:117 #: /home/kovid/work/calibre/src/calibre/utils/podofo/__init__.py:46 #: /home/kovid/work/calibre/src/calibre/utils/podofo/__init__.py:64 @@ -172,13 +173,15 @@ msgstr "مولد الكاتالوج" #: /home/kovid/work/calibre/src/calibre/customize/__init__.py:366 msgid "User Interface Action" -msgstr "" +msgstr "واجهة المستخدم" #: /home/kovid/work/calibre/src/calibre/customize/__init__.py:376 #: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:17 #: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:22 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:605 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:201 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/main.py:150 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/main.py:213 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/main.py:234 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:206 msgid "Preferences" msgstr "التفضيلات" @@ -255,15 +258,164 @@ msgstr "ضبط دليل المعلومات في الملفات %s" msgid "Set metadata from %s files" msgstr "ضبط دليل المعلومات من ملفات %s" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:681 -msgid "Look and Feel" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/customize/builtins.py:682 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:199 +msgid "Look and Feel" +msgstr "المظهر" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:684 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:696 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:707 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:718 msgid "Interface" msgstr "الواجهة" +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:688 +msgid "Adjust the look and feel of the calibre interface to suit your tastes" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:694 +msgid "Behavior" +msgstr "سلوك" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:700 +msgid "Change the way calibre behaves" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:705 +msgid "Add your own columns" +msgstr "اضف عامودك الخاص" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:711 +msgid "Add/remove your own columns to the calibre book list" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:716 +msgid "Customize the toolbar" +msgstr "خصِّص شريط الأدوات" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:722 +msgid "" +"Customize the toolbars and context menus, changing which actions are " +"available in each" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:728 +msgid "Input Options" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:730 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:741 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:752 +msgid "Conversion" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:734 +msgid "Set conversion options specific to each input format" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:739 +msgid "Common Options" +msgstr "خيارات متداولة" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:745 +msgid "Set conversion options common to all formats" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:750 +msgid "Output Options" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:756 +msgid "Set conversion options specific to each output format" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:761 +msgid "Adding books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:763 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:775 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:787 +msgid "Import/Export" +msgstr "إستيراد/تصدير" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:767 +msgid "Control how calibre reads metadata from files when adding books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:773 +msgid "Saving books to disk" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:779 +msgid "" +"Control how calibre exports files from its database to disk when using Save " +"to disk" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:785 +msgid "Sending books to devices" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:791 +msgid "Control how calibre transfers files to your ebook reader" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:797 +msgid "Sharing books by email" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:799 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:811 +msgid "Sharing" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:803 +msgid "" +"Setup sharing of books via email. Can be used for automatic sending of " +"downloaded news to your devices" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:809 +msgid "Sharing over the net" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:815 +msgid "" +"Setup the calibre Content Server which will give you access to your calibre " +"library from anywhere, on any device, over the internet" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:822 +msgid "Plugins" +msgstr "الملحقات" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:824 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:836 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:847 +msgid "Advanced" +msgstr "متقدّم" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:828 +msgid "Add/remove/customize various bits of calibre functionality" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:834 +msgid "Tweaks" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:840 +msgid "Fine tune how calibre behaves in various contexts" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:845 +msgid "Miscellaneous" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:851 +msgid "Miscellaneous advanced configuration" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/customize/conversion.py:102 msgid "Conversion Input" msgstr "دخل التحويل" @@ -517,13 +669,13 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2822 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2861 msgid "%d of %d" -msgstr "" +msgstr "%d من %d" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:369 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:962 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2867 msgid "finished" -msgstr "" +msgstr "تم" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:544 msgid "Use Series as Category in iTunes/iBooks" @@ -551,16 +703,16 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:823 #: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:851 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:244 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:190 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:203 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1644 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:192 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:205 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1694 #: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:132 msgid "News" msgstr "الأخبار" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2729 msgid "Communicate with iTunes." -msgstr "" +msgstr "تواصل معا iTunes" #: /home/kovid/work/calibre/src/calibre/devices/binatone/driver.py:17 msgid "Communicate with the Binatone Readme eBook reader." @@ -568,7 +720,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/blackberry/driver.py:13 msgid "Communicate with the Blackberry smart phone." -msgstr "" +msgstr "تواصل معا جهاز بلاك برري" #: /home/kovid/work/calibre/src/calibre/devices/blackberry/driver.py:14 #: /home/kovid/work/calibre/src/calibre/devices/nuut2/driver.py:18 @@ -682,7 +834,7 @@ msgstr "التواصل مع القارئ الكتاب الاليكترونى Iri msgid "Communicate with the JetBook eBook reader." msgstr "التواصل مع القارئ الكتاب الاليكترونى JetBook ." -#: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:86 +#: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:88 msgid "Communicate with the MiBuk Wolder reader." msgstr "" @@ -739,7 +891,7 @@ msgstr "إضافة كتب لقائمة البيانات الوصفية للجه #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:375 #: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:251 msgid "Not Implemented" -msgstr "" +msgstr "غير مطبق" #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:376 msgid "" @@ -777,7 +929,7 @@ msgstr "التواصل مع نوكيا 810" #: /home/kovid/work/calibre/src/calibre/devices/nokia/driver.py:74 msgid "Communicate with the Nokia E52" -msgstr "" +msgstr "تواصل معا جهاز نوكيا E52" #: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:20 msgid "The Nook" @@ -810,7 +962,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:145 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/structure.py:68 msgid "Unnamed" -msgstr "" +msgstr "بلا اسم" #: /home/kovid/work/calibre/src/calibre/devices/sne/driver.py:17 msgid "Communicate with the Samsung SNE eBook reader." @@ -874,7 +1026,7 @@ msgstr "لا توجد مساحة كافية في بطاقة التخزين" #: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:12 msgid "Configure Device" -msgstr "" +msgstr "ضنط الجهاز" #: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:37 msgid "settings for device drivers" @@ -889,7 +1041,7 @@ msgid "Place files in sub directories if the device supports them" msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:43 -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:81 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:86 msgid "Read metadata from files on device" msgstr "" @@ -902,7 +1054,7 @@ msgid "Template to control how books are saved" msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:50 -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:84 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:89 msgid "Extra customization" msgstr "" @@ -1923,7 +2075,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:65 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:359 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:887 -#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:569 +#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:589 msgid "Title" msgstr "العنوان" @@ -1950,9 +2102,9 @@ msgstr "المنتج" #: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:35 #: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:210 #: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:211 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:189 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:99 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:67 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:72 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:318 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1079 msgid "Comments" @@ -2270,7 +2422,7 @@ msgstr "صقحة العنوان" #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1401 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/htmltoc.py:15 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:53 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:199 msgid "Table of Contents" msgstr "المحتويات" @@ -2957,7 +3109,7 @@ msgid "Add books to your calibre library from the connected device" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:20 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:499 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:498 msgid "Fetch annotations (experimental)" msgstr "" @@ -2974,7 +3126,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:63 #: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:31 #: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:86 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:99 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:115 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:74 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:140 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:176 @@ -3025,7 +3177,7 @@ msgid "Generating %s catalog..." msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:54 -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:264 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:229 msgid "No books found" msgstr "" @@ -3047,46 +3199,85 @@ msgstr "" msgid "Select destination for %s.%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:73 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:50 -#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:110 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:81 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar.py:51 +#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:111 msgid "%d books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:74 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:82 msgid "Choose calibre library to work with" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:83 -msgid "Switch to library..." +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:91 +msgid "Switch/create library..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:91 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:99 msgid "Quick switch" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:149 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:101 +msgid "Rename library" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:103 +msgid "Delete library" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:168 +msgid "Rename" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:169 +msgid "Choose a new name for the library %s. " +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:170 +msgid "Note that the actual library folder will be renamed." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:177 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:185 +msgid "Already exists" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:178 +msgid "The folder %s already exists. Delete it first." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:186 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:53 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns.py:100 +msgid "Are you sure?" +msgstr "هل أنت متأكّد؟" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:187 +msgid "All files from %s will be permanently deleted. Are you sure?" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:207 msgid "No library found" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:150 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:208 msgid "" "No existing calibre library was found at %s. It will be removed from the " "list of known libraries." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:182 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:187 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:240 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:245 #: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:100 -#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:540 +#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:542 msgid "Not allowed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:183 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:241 msgid "You cannot change libraries when a device is connected." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:188 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:246 msgid "You cannot change libraries while jobs are running." msgstr "" @@ -3114,42 +3305,42 @@ msgstr "لا يمكن تحويله" msgid "Starting conversion of %d book(s)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:63 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:79 msgid "Copy to library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:64 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:80 msgid "Copy selected books to the specified library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:98 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:114 msgid "Cannot copy" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:103 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:119 msgid "No library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:104 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:120 msgid "No library found at %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:107 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:123 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:127 msgid "Copying" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:121 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:137 msgid "Could not copy books: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:121 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:678 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:137 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:671 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:234 msgid "Failed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:124 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:140 msgid "Copied %d books to %s" msgstr "" @@ -3206,14 +3397,14 @@ msgid "Main memory" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:115 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:436 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:445 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:435 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:444 msgid "Storage Card A" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:116 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:438 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:447 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:437 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:446 msgid "Storage Card B" msgstr "" @@ -3374,9 +3565,9 @@ msgid "Failed to download metadata:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:129 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:608 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:569 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:1001 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:607 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc.py:65 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc.py:112 #: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:54 msgid "Error" msgstr "خطأ" @@ -3571,27 +3762,27 @@ msgstr "" msgid "Books by same author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24 -msgid "Alt+S" -msgstr "Alt+S" - #: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24 msgid "Books in this series" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:25 +msgid "Alt+Shift+S" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26 msgid "Alt+P" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:25 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26 msgid "Books by this publisher" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:27 msgid "Alt+T" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:27 msgid "Books with the same tags" msgstr "كتب بنفس الوسوم" @@ -3642,60 +3833,60 @@ msgstr "" msgid "Searching in" msgstr "يتم البحث في" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:232 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:197 msgid "Adding..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:245 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:210 msgid "Searching in all sub-directories..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:258 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:223 msgid "Path error" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:259 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:224 msgid "The specified directory could not be processed." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:263 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:811 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:228 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:804 msgid "No books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:328 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:293 msgid "Added" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:341 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:306 msgid "Adding failed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:342 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:307 msgid "" "The add books process seems to have hung. Try restarting calibre and adding " "the books in smaller increments, until you find the problem book." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:357 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:322 msgid "Duplicates found!" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:358 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:323 msgid "" "Books with the same title as the following already exist in the database. " "Add them anyway?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:361 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:326 msgid "Adding duplicates..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:428 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:393 msgid "Saving..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add.py:481 +#: /home/kovid/work/calibre/src/calibre/gui2/add.py:446 msgid "Saved" msgstr "تم الحفظ" @@ -3712,7 +3903,7 @@ msgid "Looking for duplicates based on file hash" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/__init__.py:109 -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:65 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:70 msgid "Choose root folder" msgstr "" @@ -3729,7 +3920,7 @@ msgid "Add books to calibre" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/scan_ui.py:21 -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:57 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:62 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/finish_ui.py:41 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/kindle_ui.py:41 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/library_ui.py:49 @@ -3745,15 +3936,15 @@ msgstr "" msgid "This may take a few minutes" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:58 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:63 msgid "Choose the location to add books from" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:59 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:64 msgid "Select a folder on your hard disk" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:60 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:65 msgid "" "

calibre can scan your computer for existing books automatically. These " "books will then be copied into the calibre library. This wizard will " @@ -3765,83 +3956,81 @@ msgid "" "not under the root folder you choose.

" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:63 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:68 msgid "&Root folder:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:64 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:69 msgid "" "This folder and its sub-folders will be scanned for books to import into " "calibre's library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:66 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:52 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:53 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:125 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:171 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:53 -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:79 -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:80 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:72 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:620 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:621 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:633 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:635 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:637 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:639 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:640 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:693 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:101 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:103 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:106 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:108 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:364 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:71 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:57 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:58 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:130 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:176 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:58 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:84 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:77 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:369 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:383 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:394 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:396 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:398 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:404 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:87 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:90 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:156 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:159 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:163 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:166 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:126 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:128 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:374 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:388 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:399 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:401 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:403 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:409 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:92 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:95 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:161 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:164 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:168 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:171 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:131 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:135 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:75 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:77 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:267 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:269 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:270 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:143 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:144 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:78 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:80 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:82 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:84 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:133 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:140 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:80 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:82 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:272 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:274 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:275 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:156 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:157 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:87 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:89 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:90 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:83 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:85 -#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:75 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:87 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:89 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:90 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins_ui.py:88 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:103 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:105 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:108 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:110 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:80 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:186 +#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:191 msgid "..." msgstr "..." -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:67 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:72 msgid "Handle multiple files per book" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:68 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:73 msgid "" "&One book per folder, assumes every ebook file in a folder is the same book " "in a different format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:69 +#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:74 msgid "" "&Multiple books per folder, assumes every ebook file is a different book" msgstr "" @@ -3866,8 +4055,8 @@ msgstr "المسار" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:118 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:119 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:122 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:229 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:312 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:24 #: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:100 msgid "Formats" msgstr "التهيئات" @@ -3890,9 +4079,9 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1074 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1078 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:47 -#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:73 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:78 -#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:282 +#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:293 msgid "None" msgstr "بدون" @@ -3942,13 +4131,13 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:68 #: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_tab_template_ui.py:27 #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:88 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:49 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:54 #: /home/kovid/work/calibre/src/calibre/gui2/convert/epub_output_ui.py:48 #: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_input_ui.py:28 #: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_output_ui.py:31 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:119 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:124 #: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:115 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:166 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:171 #: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output_ui.py:66 #: /home/kovid/work/calibre/src/calibre/gui2/convert/page_setup_ui.py:115 #: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_input_ui.py:31 @@ -3960,16 +4149,26 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/toc_ui.py:62 #: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_input_ui.py:46 #: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_output_ui.py:45 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:50 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:55 #: /home/kovid/work/calibre/src/calibre/gui2/convert/xpath_wizard_ui.py:67 -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:77 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/save_template_ui.py:41 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:96 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:82 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_item_ui.py:35 #: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:106 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:125 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:76 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/adding_ui.py:48 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:81 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/conversion_ui.py:54 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:81 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/email_ui.py:65 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:97 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc_ui.py:63 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins_ui.py:81 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template_ui.py:46 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/saving_ui.py:67 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/sending_ui.py:61 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:123 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:98 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/tweaks_ui.py:49 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:107 msgid "Form" msgstr "استمارة" @@ -4052,8 +4251,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi.py:20 #: /home/kovid/work/calibre/src/calibre/library/catalog.py:550 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1607 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1625 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1657 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1675 msgid "Catalog" msgstr "" @@ -4127,52 +4326,52 @@ msgid "input" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:89 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:94 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:99 msgid "&Number of Colors:" msgstr "&عدد الألوان:" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:90 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:96 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:101 msgid "Disable &normalize" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:91 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:97 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:102 msgid "Keep &aspect ratio" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:92 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:98 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:103 msgid "Disable &Sharpening" msgstr "تعطيل تشحيذ&" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:93 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:104 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:109 msgid "Disable &Trimming" msgstr "تعطيل اقتصاص&" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:94 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:103 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:108 msgid "&Wide" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:95 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:99 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:104 msgid "&Landscape" msgstr "&عرضي" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:96 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:101 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:106 msgid "&Right to left" msgstr "&يمين إلى يسار" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:97 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:100 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:105 msgid "Don't so&rt" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:98 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:102 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:107 msgid "De&speckle" msgstr "" @@ -4181,7 +4380,7 @@ msgid "&Disable comic processing" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:100 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:115 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:120 msgid "&Output format:" msgstr "" @@ -4198,7 +4397,7 @@ msgid "Debug the conversion process." msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:39 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:51 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:56 msgid "Choose debug folder" msgstr "" @@ -4210,7 +4409,7 @@ msgstr "" msgid "Failed to create debug directory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:50 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:55 msgid "" "Choose a folder to put the debug output into. If you specify a folder, " "calibre will place a lot of debug output into it. This will be useful in " @@ -4218,7 +4417,7 @@ msgid "" "conversion parameters like Table of Contents and Chapter Detection." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:54 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:59 msgid "" "The debug process outputs the intermediate HTML generated at various stages " "of the conversion process. This HTML can sometimes serve as a good starting " @@ -4276,11 +4475,11 @@ msgstr "" msgid "Sectionize Chapters (Use with care!)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:99 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:104 msgid "Font rescaling wizard" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:100 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:105 msgid "" "

This wizard will help you choose an appropriate font size key for your " "needs. Just enter the base font size of the input document and then enter an " @@ -4296,25 +4495,25 @@ msgid "" "for a discussion of how font size rescaling works.

" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:103 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:108 msgid "&Output document" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:104 #: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:109 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:114 msgid "&Base font size:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:105 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:123 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:110 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:128 msgid "Font size &key:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:106 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:110 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:112 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:122 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:115 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:117 #: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:127 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:132 #: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:118 #: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:120 #: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:125 @@ -4325,23 +4524,23 @@ msgstr "" msgid " pt" msgstr " pt" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:107 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:112 msgid "Use &default values" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:108 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:113 msgid "&Input document" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:116 msgid "&Font size: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:113 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:118 msgid " will map to size: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:114 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:119 msgid "0.0 pt" msgstr "" @@ -4365,66 +4564,66 @@ msgstr "" msgid "Justify text" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:120 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:125 msgid "&Disable font size rescaling" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:121 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:126 msgid "Base &font size:" msgstr "حجم الخط& الأساسي:" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:124 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:129 msgid "Wizard to help you choose an appropriate font size key" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:126 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:131 msgid "Line &height:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:128 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:133 msgid "Input character &encoding:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:129 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:134 msgid "Remove &spacing between paragraphs" msgstr "حذف الفراغات& بين الفقرات" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:130 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:135 msgid "Indent size:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:131 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:136 msgid "" "

When calibre removes inter paragraph spacing, it automatically sets a " "paragraph indent, to ensure that paragraphs can be easily distinguished. " "This option controls the width of that indent." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:132 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:137 msgid " em" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:133 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:138 msgid "Text justification:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:134 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:139 msgid "&Linearize tables" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:135 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:140 msgid "Extra &CSS" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:141 msgid "&Transliterate unicode characters to ASCII" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:137 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:142 msgid "Insert &blank line" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:138 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:143 msgid "Keep &ligatures" msgstr "" @@ -4482,7 +4681,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:41 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:114 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:200 msgid "Metadata" msgstr "ميتاداتا" @@ -4523,64 +4722,64 @@ msgstr "" msgid " is not a valid picture" msgstr " ليست صورة صالحة" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:167 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:400 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:172 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:405 msgid "Book Cover" msgstr "غلاف الكتاب" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:168 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:173 msgid "Use cover from &source file" msgstr "استخدم غلاف من المصدر&" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:169 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:401 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:174 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:406 msgid "Change &cover image:" msgstr "تغيير صورة الغلاف&:" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:170 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:175 msgid "Browse for an image to use as the cover of this book." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:172 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:361 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:177 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:366 msgid "&Title: " msgstr ":ال&عنوان " -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:173 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:362 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:178 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:367 msgid "Change the title of this book" msgstr "تغيير عنوان هذا الكتاب" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:174 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:161 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:365 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:179 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:166 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:370 msgid "&Author(s): " msgstr "ال&مؤلف: " -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:175 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:180 msgid "Author So&rt:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:176 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:181 msgid "" "Change the author(s) of this book. Multiple authors should be separated by a " "comma" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:177 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:170 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:374 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:182 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:175 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:379 msgid "&Publisher: " msgstr "&الناشر: " -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:178 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:375 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:183 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:380 msgid "Ta&gs: " msgstr "الو&سوم: " -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:179 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:172 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:376 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:177 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:381 msgid "" "Tags categorize the book. This is particularly useful while searching. " "

They can be any words or phrases, separated by commas." @@ -4588,23 +4787,23 @@ msgstr "" "الوسوم تصنّف الكتاب. هذا يفيد كثيراً في البحث.

ممكن تكون أي كلمة أو " "مجموعة كلمات، مفرقة بفاصلة." -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:180 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:179 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:379 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:185 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:384 msgid "&Series:" msgstr "&سلسلات:" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:181 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:182 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:180 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:181 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:380 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:381 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:186 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:187 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:185 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:186 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:385 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:386 msgid "List of known series. You can add new series." msgstr "قائمة السلسلات المعروفة. بإمكانك إضافة سلسلات جديدة." -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:183 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:386 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:188 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:391 msgid "Book " msgstr "الكتاب " @@ -4612,7 +4811,7 @@ msgstr "الكتاب " msgid "MOBI Output" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output.py:42 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output.py:43 msgid "Default" msgstr "" @@ -4775,18 +4974,18 @@ msgstr "" msgid "Options specific to the input format." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:112 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:64 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:91 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:48 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:117 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:69 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:96 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:53 msgid "Dialog" msgstr "حوار" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:113 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:118 msgid "&Input format:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:114 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/single_ui.py:119 msgid "Use &saved conversion settings for individual books" msgstr "" @@ -4818,12 +5017,12 @@ msgid "Footer regular expression:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:57 -#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:76 +#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:87 msgid "Invalid regular expression" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:58 -#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:77 +#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:88 msgid "Invalid regular expression: %s" msgstr "" @@ -4942,18 +5141,18 @@ msgstr "" msgid "Force maximum line length" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:51 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:65 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:66 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_ui.py:41 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:49 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:57 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:49 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:50 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:56 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:70 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:71 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_ui.py:46 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:54 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:62 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:54 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:55 msgid "TextLabel" msgstr "تسمية النصّ" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:52 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:57 msgid "Use a wizard to help construct the XPath expression" msgstr "" @@ -5054,14 +5253,14 @@ msgid "Undefined" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:59 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:131 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:133 +#: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:241 +#: /home/kovid/work/calibre/src/calibre/library/server/xml.py:119 msgid "Yes" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:59 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:132 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:134 +#: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:243 +#: /home/kovid/work/calibre/src/calibre/library/server/xml.py:121 msgid "No" msgstr "" @@ -5082,7 +5281,7 @@ msgid " index:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:451 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:188 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:193 msgid "Automatically number books in this series" msgstr "" @@ -5098,253 +5297,253 @@ msgstr "" msgid "tags to remove" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:49 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:48 #: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:136 msgid "No details available." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:166 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:165 msgid "Device no longer connected." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:284 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:283 msgid "Get device information" msgstr "احصل على معلومات الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:295 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:294 msgid "Get list of books on device" msgstr "احصل على قائمة الكتب على الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:305 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:304 msgid "Get annotations from device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:314 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:313 msgid "Send metadata to device" msgstr "ارسل الميتاداتا إلى الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:319 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:318 msgid "Send collections to device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:343 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:342 msgid "Upload %d books to device" msgstr "رفع %d كتاب إلى الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:358 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:357 msgid "Delete books from device" msgstr "حذف كتب من الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:375 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:374 msgid "Download books from device" msgstr "تنزيل الكتب من الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:385 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:384 msgid "View book on device" msgstr "عرض كتاب على الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:419 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:418 msgid "Set default send to device action" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:425 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:424 msgid "Send to main memory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:427 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:426 msgid "Send to storage card A" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:429 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:428 msgid "Send to storage card B" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:434 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:443 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:433 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:442 msgid "Main Memory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:454 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:453 msgid "Send and delete from library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:455 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:454 msgid "Send specific format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:491 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:490 msgid "Eject device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:609 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:608 msgid "Error communicating with device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:636 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:629 msgid "Select folder to open as device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:684 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:677 msgid "Error talking to device" msgstr "خطأ في الاتصال بالجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:685 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:678 msgid "" "There was a temporary error talking to the device. Please unplug and " "reconnect the device and or reboot." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:724 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:717 msgid "Device: " msgstr "الجهاز: " -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:726 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:719 msgid " detected." msgstr " تم كشفه." -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:812 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:805 msgid "selected to send" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:817 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:810 msgid "Choose format to send to device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:826 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:819 msgid "No device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:827 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:820 msgid "Cannot send: No device is connected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:830 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:834 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:823 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:827 msgid "No card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:831 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:835 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:824 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:828 msgid "Cannot send: Device has no storage card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:876 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:869 msgid "E-book:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:879 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:872 msgid "Attached, you will find the e-book" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:880 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:180 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:873 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:107 msgid "by" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:881 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:874 msgid "in the %s format." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:894 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:887 msgid "Sending email to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:924 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:932 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1026 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1088 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:917 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:925 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1018 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1080 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1199 #: /home/kovid/work/calibre/src/calibre/gui2/device.py:1207 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1215 msgid "No suitable formats" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:925 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:918 msgid "Auto convert the following books before sending via email?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:933 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:926 msgid "" "Could not email the following books as no suitable formats were found:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:951 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:944 msgid "Failed to email books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:952 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:945 msgid "Failed to email the following books:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:956 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:949 msgid "Sent by email:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:985 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:977 msgid "News:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:986 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:978 msgid "Attached is the" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:997 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:989 msgid "Sent news to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1027 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1089 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1208 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1019 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1081 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1200 msgid "Auto convert the following books before uploading to the device?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1057 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1049 msgid "Sending catalogs to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1121 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1113 msgid "Sending news to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1174 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1166 msgid "Sending books to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1216 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1208 msgid "" "Could not upload the following books to the device, as no suitable formats " "were found. Convert the book(s) to a format supported by your device first." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1278 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1270 msgid "No space on device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1279 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1271 msgid "" "

Cannot upload books to device there is no more free space available " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:78 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:83 msgid "Select available formats and their order for this device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:82 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:87 msgid "Use sub directories" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:88 msgid "Use author sort for author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:90 msgid "Save &template:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:43 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:48 msgid "Add books by ISBN" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:44 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:49 msgid "" "

Enter a list of ISBNs in the box to the left, one per line. calibre will " "automatically create entries for books based on the ISBN and download " @@ -5352,19 +5551,19 @@ msgid "" "ignored." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:45 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:50 msgid "&Paste from clipboard" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:68 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:73 msgid "Fit &cover within view" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:69 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:74 msgid "&Previous" msgstr "ال&سابق" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:70 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:75 msgid "&Next" msgstr "ال&تالي" @@ -5398,7 +5597,7 @@ msgstr "" msgid "Catalog options" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_ui.py:40 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_ui.py:45 msgid "Choose Format" msgstr "إختيار التهيئة" @@ -5446,27 +5645,27 @@ msgstr "" msgid "%s is not an existing folder" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:66 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:71 msgid "Choose your calibre library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:67 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:72 msgid "Your calibre library is currently located at {0}" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:68 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:73 msgid "New &Location:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:69 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:74 msgid "Use &existing library at the new location" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:70 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:75 msgid "&Create an empty library at the new location" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:71 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:76 msgid "&Move current library to new location" msgstr "" @@ -5478,15 +5677,15 @@ msgstr "ضبط الإفتراضي في تحويل الرسومات (ملفات C msgid "Set options for converting %s" msgstr "ضبط الخيارات لتحويل %s" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:92 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:97 msgid "&Title:" msgstr ":ال&عنوان" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:93 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:98 msgid "&Author(s):" msgstr "ال&مؤلف:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:95 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:100 msgid "&Profile:" msgstr "&طور:" @@ -5494,1094 +5693,11 @@ msgstr "&طور:" msgid "Edit Comments" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:173 -msgid "%(plugin_type)s %(plugins)s" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:174 -msgid "plugins" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:183 -msgid "" -"\n" -"Customization: " -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:198 -msgid "General" -msgstr "عام" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:200 -msgid "Conversion" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:201 -msgid "" -"Email\n" -"Delivery" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:202 -msgid "Add/Save" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:203 -msgid "Advanced" -msgstr "متقدّم" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:204 -msgid "" -"Content\n" -"Server" -msgstr "" -"محتوى\n" -"خادم" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:205 -msgid "Plugins" -msgstr "الملحقات" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:229 -msgid "Auto send" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:229 -msgid "Email" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:234 -msgid "Formats to email. The first matching format will be sent." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:235 -msgid "" -"If checked, downloaded news will be automatically mailed
to this email " -"address (provided it is in one of the listed formats)." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:309 -msgid "new email address" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:491 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:24 -msgid "Wide" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:492 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:24 -msgid "Narrow" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:508 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:50 -msgid "Medium" -msgstr "متوسط" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:508 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:50 -msgid "Small" -msgstr "صغير" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:509 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:51 -msgid "Large" -msgstr "كبير" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:515 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:54 -msgid "Always" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:515 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:54 -msgid "Automatic" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:516 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:55 -msgid "Never" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:529 -msgid "Toolbars/Context menus" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:543 -msgid "Done" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:544 -msgid "Confirmation dialogs have all been reset" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:549 -msgid "System port selected" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:550 -msgid "" -"The value %d you have chosen for the content server port is a system " -"port. Your operating system may not allow the server to run on this " -"port. To be safe choose a port number larger than 1024." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:570 -msgid "Failed to install command line tools." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:573 -msgid "Command line tools installed" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:574 -msgid "Command line tools installed in" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:575 -msgid "" -"If you move calibre.app, you have to re-install the command line tools." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:626 -msgid "No valid plugin path" -msgstr "مسار الملحق غير صالح" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:627 -msgid "%s is not a valid plugin path" -msgstr "%s ليس مسار لملحق صالح" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:630 -msgid "Choose plugin" -msgstr "إختيار الملحق" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:642 -msgid "Plugin cannot be disabled" -msgstr "لا يمكن تعطيل الملحق" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:643 -msgid "The plugin: %s cannot be disabled" -msgstr "الملحق: %s لا يمكن تعطيله" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:652 -msgid "Plugin not customizable" -msgstr "لا يمكن تخصيص الملحق" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:653 -msgid "Plugin: %s does not need customization" -msgstr "الملحق: %s لا يحتاج التخصيص" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:661 -msgid "Customize" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:699 -msgid "Cannot remove builtin plugin" -msgstr "لم يمكن حذف الملحق المضمن" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:700 -msgid " cannot be removed. It is a builtin plugin. Try disabling it instead." -msgstr " لا يمكن حذفه. هذا ملحق مضمن في البرنامج. حاول تعطيله بدلاً من حذفه." - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:715 -msgid "Invalid tweaks" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:716 -msgid "" -"The tweaks you entered are invalid, try resetting the tweaks to default and " -"changing them one by one until you find the invalid setting." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:746 -msgid "You must select a column to delete it" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:751 -msgid "The selected column is not a custom column" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:752 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:48 -msgid "Are you sure?" -msgstr "هل أنت متأكّد؟" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:753 -msgid "Do you really want to delete column %s and all its data?" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:820 -msgid "Error log:" -msgstr "سجل الأخطاء:" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:827 -msgid "Access log:" -msgstr "سجل النفاذ:" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:855 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:319 -msgid "Failed to start content server" -msgstr "فشل في تشغيل خادم المحتوى" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:880 -msgid "Invalid size" -msgstr "حجم غير صالح" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:881 -msgid "The size %s is invalid. must be of the form widthxheight" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:947 -msgid "Must restart" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:948 -msgid "" -"The changes you made require that Calibre be restarted. Please restart as " -"soon as practical." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:982 -msgid "Checking database integrity" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:1002 -msgid "Failed to check database integrity" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:1007 -msgid "Some inconsistencies found" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:1008 -msgid "" -"The following books had formats listed in the database that are not actually " -"available. The entries for the formats have been removed. You should check " -"them manually. This can happen if you manipulate the files in the library " -"folder directly." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:133 -msgid "TabWidget" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:134 -msgid "" -"Here you can control how calibre will read metadata from the files you add " -"to it. calibre can either read metadata from the contents of the file, or " -"from the filename." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:135 -msgid "Read metadata only from &file name" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:136 -msgid "" -"Swap the firstname and lastname of the author. This affects only metadata " -"read from file names." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:137 -msgid "&Swap author firstname and lastname" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:138 -msgid "" -"If an existing book with a similar title and author is found that does not " -"have the format being added, the format is added\n" -"to the existing book, instead of creating a new entry. If the existing book " -"already has the format, then it is silently ignored.\n" -"\n" -"Title match ignores leading indefinite articles (\"the\", \"a\", \"an\"), " -"punctuation, case, etc. Author match is exact." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:142 -msgid "" -"If books with similar titles and authors found, &merge the new files " -"automatically" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:143 -msgid "&Configure metadata from file name" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:144 -msgid "&Adding books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:145 -msgid "" -"Here you can control how calibre will save your books when you click the " -"Save to Disk button:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:146 -msgid "Save &cover separately" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:147 -msgid "Update &metadata in saved copies" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:148 -msgid "Save metadata in &OPF file" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:149 -msgid "Convert non-English characters to &English equivalents" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:150 -msgid "Format &dates as:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:151 -msgid "File &formats to save:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:152 -msgid "Replace space with &underscores" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:153 -msgid "Change paths to &lowercase" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:154 -msgid "&Saving books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:155 -msgid "Metadata &management:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:156 -msgid "Manual management" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:157 -msgid "Only on send" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:158 -msgid "Automatic management" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:159 -msgid "" -"

  • Manual Management: Calibre updates the metadata and adds " -"collections only when a book is sent. With this option, calibre will never " -"remove a collection.
  • \n" -"
  • Only on send: Calibre updates metadata and adds/removes " -"collections for a book only when it is sent to the device.
  • \n" -"
  • Automatic management: Calibre automatically keeps metadata on the " -"device in sync with the calibre library, on every connect
  • " -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:162 -msgid "" -"Here you can control how calibre will save your books when you click the " -"Send to Device button. This setting can be overriden for individual devices " -"by customizing the device interface plugins in Preferences->Plugins" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:163 -msgid "Sending to &device" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:606 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:128 -msgid "Show notification when &new version is available" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:607 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:127 -msgid "Download &social metadata (tags/ratings/etc.) by default" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:608 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:126 -msgid "&Overwrite author and title by default when fetching metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:609 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:131 -msgid "Default network &timeout:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:610 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:132 -msgid "" -"Set the default timeout for network fetches (i.e. anytime we go out to the " -"internet to get information)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:611 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:133 -msgid " seconds" -msgstr " ثانية" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:612 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:100 -msgid "Choose &language (requires restart):" -msgstr "إختر ال&لغة (يحتاج إعادة تشغيل):" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:613 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:134 -msgid "Normal" -msgstr "عادي" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:614 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:135 -msgid "High" -msgstr "مرتفع" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:615 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:136 -msgid "Low" -msgstr "منخفض" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:616 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:137 -msgid "Job &priority:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:617 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:138 -msgid "Preferred &output format:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:618 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:141 -msgid "Reset all disabled &confirmation dialogs" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:619 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:142 -msgid "Preferred &input format order:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:622 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:98 -msgid "User Interface &layout (needs restart):" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:623 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:99 -msgid "&Number of covers to show in browse mode (needs restart):" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:624 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:139 -msgid "Restriction to apply when the current library is opened:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:625 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:140 -msgid "" -"Apply this restriction on calibre startup if the current library is being " -"used. Also applied when switching to this library. Note that this setting is " -"per library. " -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:626 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:102 -msgid "Disable all animations. Useful if you have a slow/old computer." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:627 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:103 -msgid "Disable &animations" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:628 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:110 -msgid "&Toolbar" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:629 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:111 -msgid "&Icon size:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:630 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:112 -msgid "Show &text under icons:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:631 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:130 -msgid "&Delete news from library when it is automatically sent to reader" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:632 -msgid "Select visible &columns in library view" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:634 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:79 -msgid "Remove a user-defined column" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:636 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:81 -msgid "Add a user-defined column" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:638 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:83 -msgid "Edit settings of a user-defined column" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:641 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:145 -msgid "Use internal &viewer for:" -msgstr "استخدم المستعرض& الداخلي في:" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:642 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:109 -msgid "Search as you type" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:643 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:107 -msgid "Use &Roman numerals for series" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:644 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:104 -msgid "Enable system &tray icon (needs restart)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:645 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:101 -msgid "Show &average ratings in the tags browser" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:646 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:129 -msgid "Automatically send downloaded &news to ebook reader" -msgstr "إرسال الأخبار& التي تم تنزيلها آلياً إلى قارئ الكتب الإلكترونية" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:647 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:105 -msgid "Show &splash screen at startup" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:648 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:108 -msgid "Show cover &browser in a separate window (needs restart)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:649 -msgid "Show ¬ifications in system tray" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:650 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:662 -msgid "&Miscellaneous" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:651 -msgid "Add an email address to which to send books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:652 -msgid "&Add email" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:653 -msgid "Make &default" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:654 -msgid "&Remove email" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:655 -msgid "" -"calibre can send your books to you (or your reader) by email. Emails will be " -"automatically sent for downloaded news to all email addresses that have Auto-" -"send checked." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:656 -msgid "&Maximum number of waiting worker processes (needs restart):" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:657 -msgid "Limit the max. simultaneous jobs to the available CPU &cores" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:658 -msgid "Debug &device detection" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:659 -msgid "&Check database integrity" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:660 -msgid "Open calibre &configuration directory" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:661 -msgid "&Install command line tools" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:663 -msgid "" -"Values for the tweaks are shown below. Edit them to change the behavior of " -"calibre" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:664 -msgid "All available tweaks" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:665 -msgid "&Current tweaks" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:666 -msgid "&Restore to defaults" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:667 -msgid "&Tweaks" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:668 -msgid "" -"calibre contains a network server that allows you to access your book " -"collection using a browser from anywhere in the world. Any changes to the " -"settings will only take effect after a server restart." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:669 -msgid "Server &port:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:670 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:58 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:212 -#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:117 -msgid "&Username:" -msgstr "&اسم المستخدم:" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:671 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:59 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:213 -#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:119 -msgid "&Password:" -msgstr "&كلمة السرّ" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:672 -msgid "" -"If you leave the password blank, anyone will be able to access your book " -"collection using the web interface." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:673 -msgid "" -"The maximum size (widthxheight) for displayed covers. Larger covers are " -"resized. " -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:674 -msgid "Max. &cover size:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:675 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:60 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:214 -msgid "&Show password" -msgstr "إظهار& كلمة السرّ" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:676 -msgid "Max. &OPDS items per query:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:677 -msgid "Max. OPDS &ungrouped items:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:678 -msgid "Restriction (saved search) to apply:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:679 -msgid "" -"This restriction (based on a saved search) will restrict the books the " -"content server makes available to those matching the search. This setting is " -"per library (i.e. you can have a different restriction per library)." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:680 -msgid "&Start Server" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:681 -msgid "St&op Server" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:682 -msgid "&Test Server" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:683 -msgid "Run server &automatically on startup" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:684 -msgid "View &server logs" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:685 -#: /home/kovid/work/calibre/src/calibre/gui2/wizard/stanza_ui.py:46 -msgid "" -"

    Remember to leave calibre running as the server only runs as long as " -"calibre is running.\n" -"

    Stanza should see your calibre collection automatically. If not, try " -"adding the URL http://myhostname:8080 as a new catalog in the Stanza reader " -"on your iPhone. Here myhostname should be the fully qualified hostname or " -"the IP address of the computer calibre is running on." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:687 -msgid "" -"Here you can customize the behavior of Calibre by controlling what plugins " -"it uses." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:688 -msgid "Enable/&Disable plugin" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:689 -msgid "&Customize plugin" -msgstr "ت&خصيص الملحق" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:690 -msgid "&Remove plugin" -msgstr "&حذف الملحق" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:691 -msgid "Add new plugin" -msgstr "إضافة ملحق جديد" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:692 -msgid "Plugin &file:" -msgstr "&ملف الملحق:" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:694 -msgid "&Add" -msgstr "&إضافة" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:125 -msgid "Create Tag-based Column" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:126 -msgid "Lookup name" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:127 -msgid "Column heading" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:128 -msgid "Column type" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:129 -msgid "Use brackets" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:130 -msgid "Values can be edited" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:135 -msgid "Text" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:136 -msgid "Number" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:137 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:31 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 -#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:69 -#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:889 -#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:568 -msgid "Date" -msgstr "تاريخ" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:138 -msgid "Tag on book" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:139 -msgid "Explanation text added in create_ct_column.py" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_ct_column_ui.py:140 -msgid "Create and edit tag-based columns" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:19 -msgid "Text, column shown in the tag browser" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:22 -msgid "Comma separated text, like tags, shown in the tag browser" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:25 -msgid "Long text, like comments, not shown in the tag browser" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:28 -msgid "Text column for keeping series-like information" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:33 -msgid "Floating point numbers" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:35 -msgid "Integers" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:37 -msgid "Ratings, shown with stars" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:40 -msgid "Yes/No" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:69 -msgid "No column selected" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:70 -msgid "No column has been selected" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:74 -msgid "Selected column is not a user-defined column" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:105 -msgid "No lookup name was provided" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:107 -msgid "" -"The lookup name must contain only lower case letters, digits and " -"underscores, and start with a letter" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:116 -msgid "No column heading was provided" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:122 -msgid "The lookup name %s is already used" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column.py:132 -msgid "The heading %s is already used" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:101 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:117 -msgid "Create or edit custom columns" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:102 -msgid "&Lookup name" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:103 -msgid "Column &heading" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:104 -msgid "" -"Used for searching the column. Must contain only digits and lower case " -"letters." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:105 -msgid "" -"Column heading in the library view and category name in the tag browser" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:106 -msgid "Column &type" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:107 -msgid "What kind of information will be kept in the column." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:108 -msgid "" -"

    Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's " -"for year.

    \n" -"

    For example:\n" -"

      \n" -"
    • ddd, d MMM yyyy gives Mon, 5 Jan 2010
    • \n" -"
    • dd MMMM yy gives 05 January 10
    • \n" -"
    " -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:114 -msgid "Use MMM yyyy for month + year, yyyy for year only" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:115 -msgid "Default: dd MMM yyyy." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/create_custom_column_ui.py:116 -msgid "Format for &dates" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/device_debug.py:21 -msgid "Getting debug information" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/device_debug.py:22 -msgid "Copy to &clipboard" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/device_debug.py:24 -msgid "Debug device detection" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/save_template.py:44 -msgid "Invalid template" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/save_template.py:45 -msgid "The template %s is invalid:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/save_template_ui.py:42 -msgid "Save &template" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/save_template_ui.py:43 -msgid "" -"By adjusting the template below, you can control what folders the files are " -"saved in and what filenames they are given. You can use the / character to " -"indicate sub-folders. Available metadata variables are described below. If a " -"particular book does not have some metadata, the variable will be replaced " -"by the empty string." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/save_template_ui.py:44 -msgid "Available variables:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/social.py:34 -msgid "Downloading social metadata, please wait..." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:35 -msgid "Switch between library and device views" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:38 -msgid "Separator" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:51 -msgid "Choose library" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:201 -msgid "The main toolbar" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:202 -msgid "The main toolbar when a device is connected" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:203 -msgid "The context menu for the books in the calibre library" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:205 -msgid "The context menu for the books on the device" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:243 -msgid "Cannot add" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:244 -msgid "Cannot add the actions %s to this location" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:261 -msgid "Cannot remove" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar.py:262 -msgid "Cannot remove the actions %s from this location" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:97 -msgid "Customize the actions in:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:98 -msgid "A&vailable actions" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:99 -msgid "&Current actions" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:100 -msgid "Move selected action up" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:102 -msgid "Move selected action down" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:104 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:78 -msgid "Ctrl+S" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:105 -msgid "Add selected actions to toolbar" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:107 -msgid "Remove selected actions from toolbar" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/toolbar_ui.py:109 -msgid "Restore to &default" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:50 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:55 msgid "&Show this warning again" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/conversion_error_ui.py:42 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/conversion_error_ui.py:47 msgid "ERROR" msgstr "خطأ" @@ -6595,6 +5711,14 @@ msgstr "" msgid "Location" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 +#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:69 +#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:889 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:31 +#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:588 +msgid "Date" +msgstr "تاريخ" + #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1064 msgid "Format" @@ -6668,11 +5792,11 @@ msgid "" "No metadata found, try adjusting the title and author or the ISBN key." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:90 msgid "Fetch metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:86 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:91 msgid "" "

    calibre can find metadata for your books from two locations: Google " "Books and isbndb.com.

    To use isbndb.com you must sign up for a " @@ -6680,48 +5804,48 @@ msgid "" "below." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:87 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:92 msgid "&Access Key:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:88 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:93 msgid "Fetch" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:89 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:94 msgid "Matches" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:90 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:95 msgid "" "Select the book that most closely matches your copy from the list below" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:91 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:96 msgid "Download &social metadata (tags/rating/etc.) for the selected book" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:92 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata_ui.py:97 msgid "Overwrite author and title with author and title of selected book" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/job_view_ui.py:37 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/job_view_ui.py:42 msgid "Details of job" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:44 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:49 msgid "Active Jobs" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:45 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:50 msgid "&Stop selected job" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:46 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:51 msgid "Show job &details" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:47 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:52 msgid "Stop &all non device jobs" msgstr "" @@ -6733,82 +5857,82 @@ msgstr "" msgid "Applying changes to %d books. This may take a while." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:160 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:165 msgid "Edit Meta information" msgstr "تحرير معلومات الميتا" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:162 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:167 msgid "A&utomatically set author sort" msgstr "ضبط& ترتيب المؤلف آلياً" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:163 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:168 msgid "Author s&ort: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:164 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:367 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:169 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:372 msgid "" "Specify how the author(s) of this book should be sorted. For example Charles " "Dickens should be sorted as Dickens, Charles." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:165 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:370 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:170 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:375 msgid "&Rating:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:166 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:167 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:371 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:372 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:171 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:172 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:376 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:377 msgid "Rating of this book. 0-5 stars" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:168 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:173 msgid "No change" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:169 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:373 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:174 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:378 msgid " stars" msgstr " نجمة" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:171 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:176 msgid "Add ta&gs: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:173 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:174 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:377 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:378 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:178 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:179 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:382 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:383 msgid "Open Tag Editor" msgstr "فتح محرر الوسوم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:175 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:180 msgid "&Remove tags:" msgstr "حذف& الوسوم:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:176 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:181 msgid "Comma separated list of tags to remove from the books. " msgstr "قائمة من الوسوم مفرقة بالفاصلة لحذفها من الكتب. " -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:177 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:182 msgid "Remove all" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:178 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:183 msgid "Check this box to remove all tags from the books." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:182 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:187 msgid "Remove &format:" msgstr "حذف الت&هيئة:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:183 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:188 msgid "&Swap title and author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:189 msgid "" "Selected books will be automatically numbered,\n" "in the order you selected them.\n" @@ -6816,24 +5940,24 @@ msgid "" "Book A will have series number 1 and Book B series number 2." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:189 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:194 msgid "" "Remove stored conversion settings for the selected books.\n" "\n" "Future conversion of these books will use the default settings." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:192 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:197 msgid "Remove &stored conversion settings for the selected books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:193 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:408 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:198 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413 msgid "&Basic metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:194 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:409 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:199 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:414 msgid "&Custom metadata" msgstr "" @@ -6974,99 +6098,119 @@ msgstr "" msgid "Could not open %s. Is it being used by another program?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:359 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:364 msgid "Edit Meta Information" msgstr "تحرير معلومات الميتا" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:360 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:365 msgid "Meta information" msgstr "معلومات الميتا" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:363 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:368 msgid "Swap the author and title" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:366 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:371 msgid "Author S&ort: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:368 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:373 msgid "" "Automatically create the author sort entry based on the current author entry" msgstr "ينشئ مدخل ترتيب المؤلف حسب مدخل المؤلف الحالي" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:382 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:387 msgid "Remove unused series (Series that have no books)" msgstr "حذف سلسلات غير مستخدمة (سلسلات التي لا تحتوي على كتب)" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:384 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:389 msgid "IS&BN:" msgstr "IS&BN:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:385 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:390 msgid "Publishe&d:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:388 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:393 msgid "dd MMM yyyy" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:389 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:394 msgid "&Date:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:390 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:395 msgid "&Comments" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:391 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:396 msgid "&Fetch metadata from server" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:392 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:397 msgid "Available Formats" msgstr "التهيئات المتوفرة" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:393 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:398 msgid "Add a new format for this book to the database" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:395 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:400 msgid "Remove the selected formats for this book from the database." msgstr "حذف التهيئات المختارة لهذا الكتاب من قاعدة البيانات." -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:397 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:402 msgid "Set the cover for the book from the selected format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:399 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:404 msgid "Update metadata from the metadata in the selected format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:402 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:407 msgid "&Browse" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:403 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:408 msgid "Reset cover to default" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:405 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:410 msgid "Download &cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:406 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:411 msgid "Generate a default cover based on the title and author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:407 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:412 msgid "&Generate cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:56 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:61 msgid "Password needed" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:63 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:217 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:125 +#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:117 +msgid "&Username:" +msgstr "&اسم المستخدم:" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:64 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:218 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:126 +#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:119 +msgid "&Password:" +msgstr "&كلمة السرّ" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:65 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:219 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:130 +msgid "&Show password" +msgstr "إظهار& كلمة السرّ" + #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress.py:59 msgid "Aborting..." msgstr "" @@ -7080,31 +6224,31 @@ msgid "" "The current saved search will be permanently deleted. Are you sure?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:88 msgid "Saved Search Editor" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:84 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:89 msgid "Saved Search: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:90 msgid "Select a saved search to edit" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:86 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:91 msgid "Delete this selected saved search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:88 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:93 msgid "Enter a new saved search name." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:89 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:94 msgid "Add the new saved search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:91 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:96 msgid "Change the contents of the saved search" msgstr "" @@ -7133,7 +6277,7 @@ msgid "Last downloaded" msgstr "آخر تنزيل" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:220 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:192 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:197 msgid "Schedule news download" msgstr "جدولة تنزيل الأخبار" @@ -7153,116 +6297,116 @@ msgstr "" msgid "Cannot download news as no internet connection is active" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:193 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:198 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:278 msgid "Recipes" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:199 msgid "Download all scheduled recipes at once" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:200 msgid "Download &all scheduled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:196 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:201 msgid "blurb" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:197 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:202 msgid "&Schedule for download:" msgstr "ج&دولة للتنزيل:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:198 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:208 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:203 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:213 msgid "Every " msgstr "كل " -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:199 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:204 msgid "day" msgstr "اليوم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:200 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:205 msgid "Monday" msgstr "الأثنين" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:201 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:206 msgid "Tuesday" msgstr "الثلاثاء" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:202 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:207 msgid "Wednesday" msgstr "الأربعاء" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:203 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:208 msgid "Thursday" msgstr "الخميس" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:204 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:209 msgid "Friday" msgstr "الجمعة" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:205 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:210 msgid "Saturday" msgstr "السبت" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:206 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:211 msgid "Sunday" msgstr "الأحد" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:207 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:212 msgid "at" msgstr "في" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:209 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:214 msgid "" "Interval at which to download this recipe. A value of zero means that the " "recipe will be downloaded every hour." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:210 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:222 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:263 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:215 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:227 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:268 msgid " days" msgstr " يوم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:211 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:216 msgid "&Account" msgstr "&حساب" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:215 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:220 msgid "For the scheduling to work, you must leave calibre running." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:216 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:221 msgid "&Schedule" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:217 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:222 msgid "Add &title as tag" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:218 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:223 msgid "&Extra tags:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:219 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:224 msgid "&Advanced" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:220 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:225 msgid "&Download now" msgstr "&تنزيل الآن" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:221 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:226 msgid "" "Delete downloaded news older than the specified number of days. Set to zero " "to disable." msgstr "" "حذف الأخبار المنزّلة التي أقدم من العدد المخصص من الأيام. اضبطه بـ0 لتعطيله." -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:223 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:228 msgid "Delete downloaded news older than " msgstr "حذف أخبار أقدم من " @@ -7284,56 +6428,56 @@ msgstr "" msgid "Negate" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:113 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:118 msgid "Advanced Search" msgstr "بحث متقدم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:114 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:119 msgid "Find entries that have..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:115 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:120 msgid "&All these words:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:116 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:121 msgid "This exact &phrase:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:117 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:122 msgid "&One or more of these words:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:118 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:123 msgid "But dont show entries that have..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:119 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:124 msgid "Any of these &unwanted words:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:120 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:125 msgid "What kind of match to use:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:121 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:126 msgid "Contains: the word or phrase matches anywhere in the metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:122 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:127 msgid "Equals: the word or phrase must match an entire metadata field" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:123 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:128 msgid "" "Regular expression: the expression must match anywhere in the metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:124 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:129 msgid " " msgstr " " -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:125 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:130 msgid "" "See the User Manual for more help" @@ -7362,51 +6506,51 @@ msgid "" "The current tag category will be permanently deleted. Are you sure?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:153 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:158 msgid "User Categories Editor" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:154 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:159 msgid "A&vailable items" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:155 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:160 msgid "Apply tags to current tag category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:157 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:162 msgid "A&pplied items" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:158 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:163 msgid "Unapply (remove) tag from current tag category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:160 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:165 msgid "Category name: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:161 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:166 msgid "Select a category to edit" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:162 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:167 msgid "Delete this selected tag category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:164 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:169 msgid "Enter a new category name. Select the kind before adding it." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:165 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:170 msgid "Add the new category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:167 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:172 msgid "Category filter: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:168 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:173 msgid "Select the content kind of the new category" msgstr "" @@ -7421,15 +6565,15 @@ msgid "" "to delete them?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:123 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:128 msgid "Tag Editor" msgstr "محرر الوسوم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:124 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:129 msgid "A&vailable tags" msgstr "الوسوم المت&وفرة" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:125 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:130 msgid "" "Delete tag from database. This will unapply the tag from all books and then " "remove it from the database." @@ -7437,23 +6581,23 @@ msgstr "" "حذف الوسم من قائمة البيانات. هذا سيحذف الوسم من الكتب وثم سيحذف الوسم تماماً " "من القاعدة." -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:127 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:132 msgid "Apply tag to current book" msgstr "علّم الكتاب الحالي بالوسم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:129 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:134 msgid "A&pplied tags" msgstr "الوسوم الم&علّمة" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:130 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:135 msgid "Unapply (remove) tag from current book" msgstr "حذف الوسم من الكتاب الحالي" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:132 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:137 msgid "&Add tag:" msgstr "إض&افة وسم:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:133 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:138 msgid "" "If the tag you want is not in the available list, you can add it here. " "Accepts a comma separated list of tags." @@ -7461,7 +6605,7 @@ msgstr "" "إذا الوسم الذي تريده ليس متوفر، بإمكانك إضافته هنا. يقبل قائمة من الوسوم " "مفرّقة بفاصلات." -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:134 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_editor_ui.py:139 msgid "Add tag to available tags and apply it to current book" msgstr "إضافة وسم إلى قائمة الوسوم المتوفرة وعلّم الكتاب الحالي به" @@ -7499,33 +6643,38 @@ msgstr "" msgid "Are you certain you want to delete the following items?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:72 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:77 msgid "Category Editor" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:73 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:78 msgid "Items in use" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:74 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:79 msgid "" "Delete item from database. This will unapply the item from all books and " "then remove it from the database." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:76 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:81 msgid "Rename the item in every book where it is used." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:51 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:106 +msgid "Ctrl+S" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:56 msgid "Test email settings" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:52 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:57 msgid "Send test mail from %s to:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:53 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:58 #: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:115 msgid "&Test" msgstr "&تجربة" @@ -7544,7 +6693,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:150 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:161 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:255 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:260 msgid "Switch to Advanced mode" msgstr "" @@ -7569,10 +6718,6 @@ msgstr "" msgid "The feed %s must have a URL" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:185 -msgid "Already exists" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:186 msgid "This feed has already been added to the recipe" msgstr "" @@ -7613,35 +6758,35 @@ msgstr "" msgid "Choose a recipe file" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:248 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:253 msgid "Add custom news source" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:249 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:254 msgid "Available user recipes" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:250 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:255 msgid "Add/Update &recipe" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:251 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:256 msgid "&Remove recipe" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:252 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:257 msgid "&Share recipe" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:253 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:258 msgid "Customize &builtin recipe" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:254 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:259 msgid "&Load recipe from file" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:256 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:261 msgid "" "