diff --git a/recipes/metro_uk.recipe b/recipes/metro_uk.recipe index deced5976b..2d5155ef29 100644 --- a/recipes/metro_uk.recipe +++ b/recipes/metro_uk.recipe @@ -1,29 +1,32 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1306097511(BasicNewsRecipe): title = u'Metro UK' - - no_stylesheets = True - oldest_article = 1 - max_articles_per_feed = 200 + description = 'News as provide by The Metro -UK' __author__ = 'Dave Asbury' + no_stylesheets = True + oldest_article = 1 + max_articles_per_feed = 25 + remove_empty_feeds = True + remove_javascript = True + + language = 'en_GB' - simultaneous_downloads= 3 + masthead_url = 'http://e-edition.metro.co.uk/images/metro_logo.gif' + extra_css = 'h2 {font: sans-serif medium;}' keep_only_tags = [ + dict(name='h1'),dict(name='h2', attrs={'class':'h2'}), dict(attrs={'class':['img-cnt figure']}), - dict(attrs={'class':['art-img']}), - dict(name='h1'), - dict(name='h2', attrs={'class':'h2'}), + dict(attrs={'class':['art-img']}), + dict(name='div', attrs={'class':'art-lft'}) ] - remove_tags = [dict(name='div', attrs={'class':[ 'metroCommentFormWrap', - 'commentForm', 'metroCommentInnerWrap', - 'art-rgt','pluck-app pluck-comm','news m12 clrd clr-l p5t', 'flt-r' ]})] - + remove_tags = [dict(name='div', attrs={'class':[ 'news m12 clrd clr-b p5t shareBtm', 'commentForm', 'metroCommentInnerWrap', + 'art-rgt','pluck-app pluck-comm','news m12 clrd clr-l p5t', 'flt-r' ]}), + dict(attrs={'class':[ 'metroCommentFormWrap','commentText','commentsNav','avatar','submDateAndTime']}) + ] feeds = [ (u'News', u'http://www.metro.co.uk/rss/news/'), (u'Money', u'http://www.metro.co.uk/rss/money/'), (u'Sport', u'http://www.metro.co.uk/rss/sport/'), (u'Film', u'http://www.metro.co.uk/rss/metrolife/film/'), (u'Music', u'http://www.metro.co.uk/rss/metrolife/music/'), (u'TV', u'http://www.metro.co.uk/rss/tv/'), (u'Showbiz', u'http://www.metro.co.uk/rss/showbiz/'), (u'Weird News', u'http://www.metro.co.uk/rss/weird/'), (u'Travel', u'http://www.metro.co.uk/rss/travel/'), (u'Lifestyle', u'http://www.metro.co.uk/rss/lifestyle/'), (u'Books', u'http://www.metro.co.uk/rss/lifestyle/books/'), (u'Food', u'http://www.metro.co.uk/rss/lifestyle/restaurants/')] - - diff --git a/recipes/noticias_r7.recipe b/recipes/noticias_r7.recipe new file mode 100644 index 0000000000..b7495bb77e --- /dev/null +++ b/recipes/noticias_r7.recipe @@ -0,0 +1,40 @@ +import re +from calibre.web.feeds.news import BasicNewsRecipe + +class PortalR7(BasicNewsRecipe): + title = 'Noticias R7' + __author__ = 'Diniz Bortolotto' + description = 'Noticias Portal R7' + oldest_article = 2 + max_articles_per_feed = 20 + encoding = 'utf8' + publisher = 'Rede Record' + category = 'news, Brazil' + language = 'pt_BR' + publication_type = 'newsportal' + use_embedded_content = False + no_stylesheets = True + remove_javascript = True + remove_attributes = ['style'] + + feeds = [ + (u'Brasil', u'http://www.r7.com/data/rss/brasil.xml'), + (u'Economia', u'http://www.r7.com/data/rss/economia.xml'), + (u'Internacional', u'http://www.r7.com/data/rss/internacional.xml'), + (u'Tecnologia e Ci\xeancia', u'http://www.r7.com/data/rss/tecnologiaCiencia.xml') + ] + reverse_article_order = True + + keep_only_tags = [dict(name='div', attrs={'class':'materia'})] + remove_tags = [ + dict(id=['espalhe', 'report-erro']), + dict(name='ul', attrs={'class':'controles'}), + dict(name='ul', attrs={'class':'relacionados'}), + dict(name='div', attrs={'class':'materia_banner'}), + dict(name='div', attrs={'class':'materia_controles'}) + ] + + preprocess_regexps = [ + (re.compile(r'
.*
',re.DOTALL|re.IGNORECASE), + lambda match: '
') + ] diff --git a/resources/content_server/read/monocle.js b/resources/content_server/read/monocle.js index 8d58ba8a06..560e4af641 100644 --- a/resources/content_server/read/monocle.js +++ b/resources/content_server/read/monocle.js @@ -1,5 +1,5 @@ Monocle = { - VERSION: "1.0.0" + VERSION: "2.0.0" }; @@ -170,7 +170,8 @@ Monocle.Browser.has.iframeTouchBug = Monocle.Browser.iOSVersionBelow("4.2"); Monocle.Browser.has.selectThruBug = Monocle.Browser.iOSVersionBelow("4.2"); Monocle.Browser.has.mustScrollSheaf = Monocle.Browser.is.MobileSafari; -Monocle.Browser.has.iframeDoubleWidthBug = Monocle.Browser.has.mustScrollSheaf; +Monocle.Browser.has.iframeDoubleWidthBug = + Monocle.Browser.has.mustScrollSheaf || Monocle.Browser.on.Kindle3; Monocle.Browser.has.floatColumnBug = Monocle.Browser.is.WebKit; @@ -181,6 +182,11 @@ Monocle.Browser.has.jumpFlickerBug = Monocle.Browser.on.MacOSX && Monocle.Browser.is.WebKit; +Monocle.Browser.has.columnOverflowPaintBug = Monocle.Browser.is.WebKit && + !Monocle.Browser.is.MobileSafari && + navigator.userAgent.indexOf("AppleWebKit/534") > 0; + + if (typeof window.console == "undefined") { window.console = { messages: [], @@ -241,6 +247,7 @@ Monocle.Factory = function (element, label, index, reader) { function initialize() { + if (!p.label) { return; } var node = p.reader.properties.graph; node[p.label] = node[p.label] || []; if (typeof p.index == 'undefined' && node[p.label][p.index]) { @@ -274,7 +281,11 @@ Monocle.Factory = function (element, label, index, reader) { function make(tagName, oLabel, index_or_options, or_options) { var oIndex, options; - if (arguments.length == 2) { + if (arguments.length == 1) { + oLabel = null, + oIndex = 0; + options = {}; + } else if (arguments.length == 2) { oIndex = 0; options = {}; } else if (arguments.length == 4) { @@ -376,6 +387,22 @@ Monocle.pieceLoaded('factory'); Monocle.Events = {} +Monocle.Events.dispatch = function (elem, evtType, data, cancelable) { + if (!document.createEvent) { + return true; + } + var evt = document.createEvent("Events"); + evt.initEvent(evtType, false, cancelable || false); + evt.m = data; + try { + return elem.dispatchEvent(evt); + } catch(e) { + console.warn("Failed to dispatch event: "+evtType); + return false; + } +} + + Monocle.Events.listen = function (elem, evtType, fn, useCapture) { if (elem.addEventListener) { return elem.addEventListener(evtType, fn, useCapture || false); @@ -405,7 +432,7 @@ Monocle.Events.listenForContact = function (elem, fns, options) { pageY: ci.pageY }; - var target = evt.target || window.srcElement; + var target = evt.target || evt.srcElement; while (target.nodeType != 1 && target.parentNode) { target = target.parentNode; } @@ -527,13 +554,18 @@ Monocle.Events.deafenForContact = function (elem, listeners) { } -Monocle.Events.listenForTap = function (elem, fn) { +Monocle.Events.listenForTap = function (elem, fn, activeClass) { var startPos; if (Monocle.Browser.on.Kindle3) { Monocle.Events.listen(elem, 'click', function () {}); } + var annul = function () { + startPos = null; + if (activeClass && elem.dom) { elem.dom.removeClass(activeClass); } + } + var annulIfOutOfBounds = function (evt) { if (evt.type.match(/^mouse/)) { return; @@ -545,7 +577,7 @@ Monocle.Events.listenForTap = function (elem, fn) { evt.m.registrantX < 0 || evt.m.registrantX > elem.offsetWidth || evt.m.registrantY < 0 || evt.m.registrantY > elem.offsetHeight ) { - startPos = null; + annul(); } else { evt.preventDefault(); } @@ -557,6 +589,7 @@ Monocle.Events.listenForTap = function (elem, fn) { start: function (evt) { startPos = [evt.m.pageX, evt.m.pageY]; evt.preventDefault(); + if (activeClass && elem.dom) { elem.dom.addClass(activeClass); } }, move: annulIfOutOfBounds, end: function (evt) { @@ -565,10 +598,9 @@ Monocle.Events.listenForTap = function (elem, fn) { evt.m.startOffset = startPos; fn(evt); } + annul(); }, - cancel: function (evt) { - startPos = null; - } + cancel: annul }, { useCapture: false @@ -997,6 +1029,9 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { createReaderElements(); p.defaultStyles = addPageStyles(k.DEFAULT_STYLE_RULES, false); + if (options.stylesheet) { + p.initialStyles = addPageStyles(options.stylesheet, false); + } primeFrames(options.primeURL, function () { applyStyles(); @@ -1077,6 +1112,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { if (Monocle.Browser.is.WebKit) { frame.contentDocument.documentElement.style.overflow = "hidden"; } + dispatchEvent('monocle:frameprimed', { frame: frame, pageIndex: pageCount }); if ((pageCount += 1) == pageMax) { Monocle.defer(callback); } @@ -1131,6 +1167,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { var pageCount = 0; if (typeof callback == 'function') { var watcher = function (evt) { + dispatchEvent('monocle:firstcomponentchange', evt.m); if ((pageCount += 1) == p.flipper.pageCount) { deafen('monocle:componentchange', watcher); callback(); @@ -1239,7 +1276,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { page.appendChild(runner); ctrlData.elements.push(runner); } - } else if (cType == "modal" || cType == "popover") { + } else if (cType == "modal" || cType == "popover" || cType == "hud") { ctrlElem = ctrl.createControlElements(overlay); overlay.appendChild(ctrlElem); ctrlData.elements.push(ctrlElem); @@ -1312,24 +1349,33 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { var controlData = dataForControl(ctrl); if (!controlData) { console.warn("No data for control: " + ctrl); - return; + return false; } - if (controlData.hidden == false) { - return; + + if (showingControl(ctrl)) { + return false; } + + var overlay = dom.find('overlay'); + if (controlData.usesOverlay && controlData.controlType != "hud") { + for (var i = 0, ii = p.controls.length; i < ii; ++i) { + if (p.controls[i].usesOverlay && !p.controls[i].hidden) { + return false; + } + } + overlay.style.display = "block"; + } + for (var i = 0; i < controlData.elements.length; ++i) { controlData.elements[i].style.display = "block"; } - var overlay = dom.find('overlay'); - if (controlData.usesOverlay) { - overlay.style.display = "block"; - } + if (controlData.controlType == "popover") { overlay.listeners = Monocle.Events.listenForContact( overlay, { start: function (evt) { - obj = evt.target || window.event.srcElement; + var obj = evt.target || window.event.srcElement; do { if (obj == controlData.elements[0]) { return true; } } while (obj && (obj = obj.parentNode)); @@ -1346,22 +1392,18 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { ctrl.properties.hidden = false; } dispatchEvent('controlshow', ctrl, false); + return true; + } + + + function showingControl(ctrl) { + var controlData = dataForControl(ctrl); + return controlData.hidden == false; } function dispatchEvent(evtType, data, cancelable) { - if (!document.createEvent) { - return true; - } - var evt = document.createEvent("Events"); - evt.initEvent(evtType, false, cancelable || false); - evt.m = data; - try { - return dom.find('box').dispatchEvent(evt); - } catch(e) { - console.warn("Failed to dispatch event: " + evtType); - return false; - } + return Monocle.Events.dispatch(dom.find('box'), evtType, data, cancelable); } @@ -1502,6 +1544,7 @@ Monocle.Reader = function (node, bookData, options, onLoadCallback) { API.addControl = addControl; API.hideControl = hideControl; API.showControl = showControl; + API.showingControl = showingControl; API.dispatchEvent = dispatchEvent; API.listen = listen; API.deafen = deafen; @@ -1527,22 +1570,32 @@ Monocle.Reader.DEFAULT_CLASS_PREFIX = 'monelem_' Monocle.Reader.FLIPPER_DEFAULT_CLASS = "Slider"; Monocle.Reader.FLIPPER_LEGACY_CLASS = "Legacy"; Monocle.Reader.DEFAULT_STYLE_RULES = [ - "html * {" + + "html#RS\\:monocle * {" + + "-webkit-font-smoothing: subpixel-antialiased;" + "text-rendering: auto !important;" + "word-wrap: break-word !important;" + + "overflow: visible !important;" + (Monocle.Browser.has.floatColumnBug ? "float: none !important;" : "") + - "}" + - "body {" + + "}", + "html#RS\\:monocle body {" + "margin: 0 !important;" + "padding: 0 !important;" + "-webkit-text-size-adjust: none;" + - "}" + - "table, img {" + + "}", + "html#RS\\:monocle body * {" + "max-width: 100% !important;" + - "max-height: 90% !important;" + + "}", + "html#RS\\:monocle img, html#RS\\:monocle video, html#RS\\:monocle object {" + + "max-height: 95% !important;" + "}" ] +if (Monocle.Browser.has.columnOverflowPaintBug) { + Monocle.Reader.DEFAULT_STYLE_RULES.push( + "::-webkit-scrollbar { width: 0; height: 0; }" + ) +} + Monocle.pieceLoaded('reader'); /* BOOK */ @@ -1586,6 +1639,16 @@ Monocle.Book = function (dataSource) { locus.load = true; locus.componentId = p.componentIds[0]; return locus; + } else if ( + cIndex < 0 && + locus.componentId && + currComponent.properties.id != locus.componentId + ) { + pageDiv.m.reader.dispatchEvent( + "monocle:notfound", + { href: locus.componentId } + ); + return null; } else if (cIndex < 0) { component = currComponent; locus.componentId = pageDiv.m.activeFrame.m.component.properties.id; @@ -1619,6 +1682,8 @@ Monocle.Book = function (dataSource) { result.page += locus.direction; } else if (typeof(locus.anchor) == "string") { result.page = component.pageForChapter(locus.anchor, pageDiv); + } else if (typeof(locus.xpath) == "string") { + result.page = component.pageForXPath(locus.xpath, pageDiv); } else if (typeof(locus.position) == "string") { if (locus.position == "start") { result.page = 1; @@ -1638,6 +1703,7 @@ Monocle.Book = function (dataSource) { if (result.page < 1) { if (cIndex == 0) { result.page = 1; + result.boundarystart = true; } else { result.load = true; result.componentId = p.componentIds[cIndex - 1]; @@ -1647,6 +1713,7 @@ Monocle.Book = function (dataSource) { } else if (result.page > lastPageNum['new']) { if (cIndex == p.lastCIndex) { result.page = lastPageNum['new']; + result.boundaryend = true; } else { result.load = true; result.componentId = p.componentIds[cIndex + 1]; @@ -1660,18 +1727,25 @@ Monocle.Book = function (dataSource) { function setPageAt(pageDiv, locus) { locus = pageNumberAt(pageDiv, locus); - if (!locus.load) { - var component = p.components[p.componentIds.indexOf(locus.componentId)]; - pageDiv.m.place = pageDiv.m.place || new Monocle.Place(); - pageDiv.m.place.setPlace(component, locus.page); + if (locus && !locus.load) { + var evtData = { locus: locus, page: pageDiv } + if (locus.boundarystart) { + pageDiv.m.reader.dispatchEvent('monocle:boundarystart', evtData); + } else if (locus.boundaryend) { + pageDiv.m.reader.dispatchEvent('monocle:boundaryend', evtData); + } else { + var component = p.components[p.componentIds.indexOf(locus.componentId)]; + pageDiv.m.place = pageDiv.m.place || new Monocle.Place(); + pageDiv.m.place.setPlace(component, locus.page); - var evtData = { - page: pageDiv, - locus: locus, - pageNumber: pageDiv.m.place.pageNumber(), - componentId: locus.componentId + var evtData = { + page: pageDiv, + locus: locus, + pageNumber: pageDiv.m.place.pageNumber(), + componentId: locus.componentId + } + pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData); } - pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData); } return locus; } @@ -1683,6 +1757,10 @@ Monocle.Book = function (dataSource) { locus = pageNumberAt(pageDiv, locus); } + if (!locus) { + return; + } + if (!locus.load) { callback(locus); return; @@ -1690,7 +1768,9 @@ Monocle.Book = function (dataSource) { var findPageNumber = function () { locus = setPageAt(pageDiv, locus); - if (locus.load) { + if (!locus) { + return; + } else if (locus.load) { loadPageAt(pageDiv, locus, callback, progressCallback) } else { callback(locus); @@ -1715,10 +1795,12 @@ Monocle.Book = function (dataSource) { } - function setOrLoadPageAt(pageDiv, locus, callback, progressCallback) { + function setOrLoadPageAt(pageDiv, locus, callback, onProgress, onFail) { locus = setPageAt(pageDiv, locus); - if (locus.load) { - loadPageAt(pageDiv, locus, callback, progressCallback); + if (!locus) { + if (onFail) { onFail(); } + } else if (locus.load) { + loadPageAt(pageDiv, locus, callback, onProgress); } else { callback(locus); } @@ -1864,13 +1946,18 @@ Monocle.Place = function () { } - function percentageThrough() { + function percentAtTopOfPage() { + return p.percent - 1.0 / p.component.lastPageNumber(); + } + + + function percentAtBottomOfPage() { return p.percent; } - function pageAtPercentageThrough(pc) { - return Math.max(Math.round(p.component.lastPageNumber() * pc), 1); + function pageAtPercentageThrough(percent) { + return Math.max(Math.round(p.component.lastPageNumber() * percent), 1); } @@ -1911,6 +1998,8 @@ Monocle.Place = function () { } if (options.direction) { locus.page += options.direction; + } else { + locus.percent = percentAtBottomOfPage(); } return locus; } @@ -1942,7 +2031,9 @@ Monocle.Place = function () { API.setPlace = setPlace; API.setPercentageThrough = setPercentageThrough; API.componentId = componentId; - API.percentageThrough = percentageThrough; + API.percentAtTopOfPage = percentAtTopOfPage; + API.percentAtBottomOfPage = percentAtBottomOfPage; + API.percentageThrough = percentAtBottomOfPage; API.pageAtPercentageThrough = pageAtPercentageThrough; API.pageNumber = pageNumber; API.chapterInfo = chapterInfo; @@ -2158,11 +2249,13 @@ Monocle.Component = function (book, id, index, chapters, source) { if (p.chapters[0] && typeof p.chapters[0].percent == "number") { return; } + var doc = pageDiv.m.activeFrame.contentDocument; for (var i = 0; i < p.chapters.length; ++i) { var chp = p.chapters[i]; chp.percent = 0; if (chp.fragment) { - chp.percent = pageDiv.m.dimensions.percentageThroughOfId(chp.fragment); + var node = doc.getElementById(chp.fragment); + chp.percent = pageDiv.m.dimensions.percentageThroughOfNode(node); } } return p.chapters; @@ -2187,14 +2280,37 @@ Monocle.Component = function (book, id, index, chapters, source) { if (!fragment) { return 1; } - var pc2pn = function (pc) { return Math.floor(pc * p.pageLength) + 1 } for (var i = 0; i < p.chapters.length; ++i) { if (p.chapters[i].fragment == fragment) { - return pc2pn(p.chapters[i].percent); + return percentToPageNumber(p.chapters[i].percent); } } - var percent = pageDiv.m.dimensions.percentageThroughOfId(fragment); - return pc2pn(percent); + var doc = pageDiv.m.activeFrame.contentDocument; + var node = doc.getElementById(fragment); + var percent = pageDiv.m.dimensions.percentageThroughOfNode(node); + return percentToPageNumber(percent); + } + + + function pageForXPath(xpath, pageDiv) { + var doc = pageDiv.m.activeFrame.contentDocument; + var percent = 0; + if (typeof doc.evaluate == "function") { + var node = doc.evaluate( + xpath, + doc, + null, + 9, + null + ).singleNodeValue; + var percent = pageDiv.m.dimensions.percentageThroughOfNode(node); + } + return percentToPageNumber(percent); + } + + + function percentToPageNumber(pc) { + return Math.floor(pc * p.pageLength) + 1; } @@ -2207,6 +2323,7 @@ Monocle.Component = function (book, id, index, chapters, source) { API.updateDimensions = updateDimensions; API.chapterForPage = chapterForPage; API.pageForChapter = pageForChapter; + API.pageForXPath = pageForXPath; API.lastPageNumber = lastPageNumber; return API; @@ -2415,9 +2532,11 @@ Monocle.Dimensions.Vert = function (pageDiv) { } - function percentageThroughOfId(id) { + function percentageThroughOfNode(target) { + if (!target) { + return 0; + } var doc = p.page.m.activeFrame.contentDocument; - var target = doc.getElementById(id); var offset = 0; if (target.getBoundingClientRect) { offset = target.getBoundingClientRect().top; @@ -2456,7 +2575,7 @@ Monocle.Dimensions.Vert = function (pageDiv) { API.hasChanged = hasChanged; API.measure = measure; API.pages = pages; - API.percentageThroughOfId = percentageThroughOfId; + API.percentageThroughOfNode = percentageThroughOfNode; API.locusToOffset = locusToOffset; initialize(); @@ -2713,8 +2832,7 @@ Monocle.Dimensions.Columns = function (pageDiv) { (!p.measurements) || (p.measurements.width != newMeasurements.width) || (p.measurements.height != newMeasurements.height) || - (p.measurements.scrollWidth != newMeasurements.scrollWidth) || - (p.measurements.fontSize != newMeasurements.fontSize) + (p.measurements.scrollWidth != newMeasurements.scrollWidth) ); } @@ -2736,10 +2854,16 @@ Monocle.Dimensions.Columns = function (pageDiv) { if (!lc || !lc.getBoundingClientRect) { console.warn('Empty document for page['+p.page.m.pageIndex+']'); p.measurements.scrollWidth = p.measurements.width; - } else if (lc.getBoundingClientRect().bottom > p.measurements.height) { - p.measurements.scrollWidth = p.measurements.width * 2; } else { - p.measurements.scrollWidth = p.measurements.width; + var bcr = lc.getBoundingClientRect(); + if ( + bcr.right > p.measurements.width || + bcr.bottom > p.measurements.height + ) { + p.measurements.scrollWidth = p.measurements.width * 2; + } else { + p.measurements.scrollWidth = p.measurements.width; + } } } @@ -2758,12 +2882,11 @@ Monocle.Dimensions.Columns = function (pageDiv) { } - function percentageThroughOfId(id) { - var doc = p.page.m.activeFrame.contentDocument; - var target = doc.getElementById(id); + function percentageThroughOfNode(target) { if (!target) { return 0; } + var doc = p.page.m.activeFrame.contentDocument; var offset = 0; if (target.getBoundingClientRect) { offset = target.getBoundingClientRect().left; @@ -2785,20 +2908,30 @@ Monocle.Dimensions.Columns = function (pageDiv) { function componentChanged(evt) { if (evt.m['page'] != p.page) { return; } var doc = evt.m['document']; - Monocle.Styles.applyRules(doc.body, k.BODY_STYLES); + if (Monocle.Browser.has.columnOverflowPaintBug) { + var div = doc.createElement('div'); + Monocle.Styles.applyRules(div, k.BODY_STYLES); + div.style.cssText += "overflow: scroll !important;"; + while (doc.body.childNodes.length) { + div.appendChild(doc.body.firstChild); + } + doc.body.appendChild(div); + } else { + Monocle.Styles.applyRules(doc.body, k.BODY_STYLES); - if (Monocle.Browser.is.WebKit) { - doc.documentElement.style.overflow = 'hidden'; + if (Monocle.Browser.is.WebKit) { + doc.documentElement.style.overflow = 'hidden'; + } } + p.dirty = true; } function setColumnWidth() { var cw = p.page.m.sheafDiv.clientWidth; - var doc = p.page.m.activeFrame.contentDocument; if (currBodyStyleValue('column-width') != cw+"px") { - Monocle.Styles.affix(doc.body, 'column-width', cw+"px"); + Monocle.Styles.affix(columnedElement(), 'column-width', cw+"px"); p.dirty = true; } } @@ -2809,8 +2942,7 @@ Monocle.Dimensions.Columns = function (pageDiv) { return { width: sheaf.clientWidth, height: sheaf.clientHeight, - scrollWidth: scrollerWidth(), - fontSize: currBodyStyleValue('font-size') + scrollWidth: scrollerWidth() } } @@ -2819,16 +2951,24 @@ Monocle.Dimensions.Columns = function (pageDiv) { if (Monocle.Browser.has.mustScrollSheaf) { return p.page.m.sheafDiv; } else { - return p.page.m.activeFrame.contentDocument.body; + return columnedElement(); } } + function columnedElement() { + var elem = p.page.m.activeFrame.contentDocument.body; + return Monocle.Browser.has.columnOverflowPaintBug ? elem.firstChild : elem; + } + + function scrollerWidth() { var bdy = p.page.m.activeFrame.contentDocument.body; if (Monocle.Browser.has.iframeDoubleWidthBug) { - if (Monocle.Browser.on.Android) { - return bdy.scrollWidth * 1.5; // I actually have no idea why 1.5. + if (Monocle.Browser.on.Kindle3) { + return scrollerElement().scrollWidth; + } else if (Monocle.Browser.on.Android) { + return bdy.scrollWidth; } else if (Monocle.Browser.iOSVersion < "4.1") { var hbw = bdy.scrollWidth / 2; var sew = scrollerElement().scrollWidth; @@ -2838,15 +2978,18 @@ Monocle.Dimensions.Columns = function (pageDiv) { var hbw = bdy.scrollWidth / 2; return hbw; } - } else if (Monocle.Browser.is.Gecko) { - var lc = bdy.lastChild; - while (lc && lc.nodeType != 1) { - lc = lc.previousSibling; - } - if (lc && lc.getBoundingClientRect) { - return lc.getBoundingClientRect().right; + } else if (bdy.getBoundingClientRect) { + var elems = bdy.getElementsByTagName('*'); + var bdyRect = bdy.getBoundingClientRect(); + var l = bdyRect.left, r = bdyRect.right; + for (var i = elems.length - 1; i >= 0; --i) { + var rect = elems[i].getBoundingClientRect(); + l = Math.min(l, rect.left); + r = Math.max(r, rect.right); } + return Math.abs(l) + Math.abs(r); } + return scrollerElement().scrollWidth; } @@ -2867,8 +3010,14 @@ Monocle.Dimensions.Columns = function (pageDiv) { function translateToLocus(locus) { var offset = locusToOffset(locus); - var bdy = p.page.m.activeFrame.contentDocument.body; - Monocle.Styles.affix(bdy, "transform", "translateX("+offset+"px)"); + p.page.m.offset = 0 - offset; + if (k.SETX && !Monocle.Browser.has.columnOverflowPaintBug) { + var bdy = p.page.m.activeFrame.contentDocument.body; + Monocle.Styles.affix(bdy, "transform", "translateX("+offset+"px)"); + } else { + var scrElem = scrollerElement(); + scrElem.scrollLeft = 0 - offset; + } return offset; } @@ -2876,7 +3025,7 @@ Monocle.Dimensions.Columns = function (pageDiv) { API.hasChanged = hasChanged; API.measure = measure; API.pages = pages; - API.percentageThroughOfId = percentageThroughOfId; + API.percentageThroughOfNode = percentageThroughOfNode; API.locusToOffset = locusToOffset; API.translateToLocus = translateToLocus; @@ -2898,6 +3047,8 @@ Monocle.Dimensions.Columns.BODY_STYLES = { "column-fill": "auto" } +Monocle.Dimensions.Columns.SETX = true; // Set to false for scrollLeft. + if (Monocle.Browser.has.iframeDoubleWidthBug) { Monocle.Dimensions.Columns.BODY_STYLES["min-width"] = "200%"; } else { @@ -2924,6 +3075,8 @@ Monocle.Flippers.Slider = function (reader) { function addPage(pageDiv) { pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv); + + Monocle.Styles.setX(pageDiv, "0px"); } @@ -2963,6 +3116,7 @@ Monocle.Flippers.Slider = function (reader) { function interactiveMode(bState) { + p.reader.dispatchEvent('monocle:interactive:'+(bState ? 'on' : 'off')); if (!Monocle.Browser.has.selectThruBug) { return; } @@ -2994,10 +3148,10 @@ Monocle.Flippers.Slider = function (reader) { function moveTo(locus, callback) { var fn = function () { - prepareNextPage(announceTurn); - if (typeof callback == "function") { - callback(); - } + prepareNextPage(function () { + if (typeof callback == "function") { callback(); } + announceTurn(); + }); } setPage(upperPage(), locus, fn); } @@ -3045,12 +3199,26 @@ Monocle.Flippers.Slider = function (reader) { if (dir == k.FORWARDS) { if (getPlace().onLastPageOfBook()) { + p.reader.dispatchEvent( + 'monocle:boundaryend', + { + locus: getPlace().getLocus({ direction : dir }), + page: upperPage() + } + ); resetTurnData(); return; } onGoingForward(boxPointX); } else if (dir == k.BACKWARDS) { if (getPlace().onFirstPageOfBook()) { + p.reader.dispatchEvent( + 'monocle:boundarystart', + { + locus: getPlace().getLocus({ direction : dir }), + page: upperPage() + } + ); resetTurnData(); return; } @@ -3215,14 +3383,14 @@ Monocle.Flippers.Slider = function (reader) { function announceTurn() { - hideWaitControl(upperPage()); - hideWaitControl(lowerPage()); p.reader.dispatchEvent('monocle:turn'); resetTurnData(); } function resetTurnData() { + hideWaitControl(upperPage()); + hideWaitControl(lowerPage()); p.turnData = {}; } @@ -3268,7 +3436,7 @@ Monocle.Flippers.Slider = function (reader) { (new Date()).getTime() - stamp > duration || Math.abs(currX - finalX) <= Math.abs((currX + step) - finalX) ) { - clearTimeout(elem.setXTransitionInterval) + clearTimeout(elem.setXTransitionInterval); Monocle.Styles.setX(elem, finalX); if (elem.setXTCB) { elem.setXTCB(); @@ -3366,13 +3534,17 @@ Monocle.Flippers.Slider = function (reader) { function jumpIn(pageDiv, callback) { var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0; - setX(pageDiv, 0, { duration: dur }, callback); + Monocle.defer(function () { + setX(pageDiv, 0, { duration: dur }, callback); + }); } function jumpOut(pageDiv, callback) { var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0; - setX(pageDiv, 0 - pageDiv.offsetWidth, { duration: dur }, callback); + Monocle.defer(function () { + setX(pageDiv, 0 - pageDiv.offsetWidth, { duration: dur }, callback); + }); } @@ -3382,7 +3554,9 @@ Monocle.Flippers.Slider = function (reader) { duration: k.durations.SLIDE, timing: 'ease-in' }; - setX(upperPage(), 0, slideOpts, callback); + Monocle.defer(function () { + setX(upperPage(), 0, slideOpts, callback); + }); } @@ -3391,7 +3565,9 @@ Monocle.Flippers.Slider = function (reader) { duration: k.durations.SLIDE, timing: 'ease-in' }; - setX(upperPage(), 0 - upperPage().offsetWidth, slideOpts, callback); + Monocle.defer(function () { + setX(upperPage(), 0 - upperPage().offsetWidth, slideOpts, callback); + }); } @@ -3418,13 +3594,13 @@ Monocle.Flippers.Slider = function (reader) { function showWaitControl(page) { var ctrl = p.reader.dom.find('flippers_slider_wait', page.m.pageIndex); - ctrl.style.opacity = 0.5; + ctrl.style.visibility = "visible"; } function hideWaitControl(page) { var ctrl = p.reader.dom.find('flippers_slider_wait', page.m.pageIndex); - ctrl.style.opacity = 0; + ctrl.style.visibility = "hidden"; } API.pageCount = p.pageCount; diff --git a/resources/images/devices/itunes.png b/resources/images/devices/itunes.png index cd8579d492..cc0493d9eb 100644 Binary files a/resources/images/devices/itunes.png and b/resources/images/devices/itunes.png differ diff --git a/resources/images/plugins/mobileread.png b/resources/images/plugins/mobileread.png new file mode 100644 index 0000000000..ec04625351 Binary files /dev/null and b/resources/images/plugins/mobileread.png differ diff --git a/resources/images/plugins/plugin_deprecated.png b/resources/images/plugins/plugin_deprecated.png new file mode 100644 index 0000000000..bf5e3e2d21 Binary files /dev/null and b/resources/images/plugins/plugin_deprecated.png differ diff --git a/resources/images/plugins/plugin_disabled_invalid.png b/resources/images/plugins/plugin_disabled_invalid.png new file mode 100644 index 0000000000..f905703564 Binary files /dev/null and b/resources/images/plugins/plugin_disabled_invalid.png differ diff --git a/resources/images/plugins/plugin_disabled_ok.png b/resources/images/plugins/plugin_disabled_ok.png new file mode 100644 index 0000000000..0ae52697cd Binary files /dev/null and b/resources/images/plugins/plugin_disabled_ok.png differ diff --git a/resources/images/plugins/plugin_disabled_valid.png b/resources/images/plugins/plugin_disabled_valid.png new file mode 100644 index 0000000000..f3c7ea5fb9 Binary files /dev/null and b/resources/images/plugins/plugin_disabled_valid.png differ diff --git a/resources/images/plugins/plugin_new.png b/resources/images/plugins/plugin_new.png new file mode 100644 index 0000000000..cf76fd0f67 Binary files /dev/null and b/resources/images/plugins/plugin_new.png differ diff --git a/resources/images/plugins/plugin_new_invalid.png b/resources/images/plugins/plugin_new_invalid.png new file mode 100644 index 0000000000..ac9143b2b2 Binary files /dev/null and b/resources/images/plugins/plugin_new_invalid.png differ diff --git a/resources/images/plugins/plugin_new_valid.png b/resources/images/plugins/plugin_new_valid.png new file mode 100644 index 0000000000..0dfea2852e Binary files /dev/null and b/resources/images/plugins/plugin_new_valid.png differ diff --git a/resources/images/plugins/plugin_updater.png b/resources/images/plugins/plugin_updater.png new file mode 100644 index 0000000000..bbd541dc03 Binary files /dev/null and b/resources/images/plugins/plugin_updater.png differ diff --git a/resources/images/plugins/plugin_updater_updates.png b/resources/images/plugins/plugin_updater_updates.png new file mode 100644 index 0000000000..f9cb8456ce Binary files /dev/null and b/resources/images/plugins/plugin_updater_updates.png differ diff --git a/resources/images/plugins/plugin_upgrade_invalid.png b/resources/images/plugins/plugin_upgrade_invalid.png new file mode 100644 index 0000000000..e19963ce62 Binary files /dev/null and b/resources/images/plugins/plugin_upgrade_invalid.png differ diff --git a/resources/images/plugins/plugin_upgrade_ok.png b/resources/images/plugins/plugin_upgrade_ok.png new file mode 100644 index 0000000000..7cd2e89127 Binary files /dev/null and b/resources/images/plugins/plugin_upgrade_ok.png differ diff --git a/resources/images/plugins/plugin_upgrade_valid.png b/resources/images/plugins/plugin_upgrade_valid.png new file mode 100644 index 0000000000..5a0b79b11a Binary files /dev/null and b/resources/images/plugins/plugin_upgrade_valid.png differ diff --git a/setup/installer/windows/portable.c b/setup/installer/windows/portable.c index 2a557e9174..05763bc303 100644 --- a/setup/installer/windows/portable.c +++ b/setup/installer/windows/portable.c @@ -95,6 +95,11 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) { ExitProcess(1); } + if (! SetEnvironmentVariable(TEXT("CALIBRE_PORTABLE_BUILD"), exe)) { + show_last_error(TEXT("Failed to set environment variables")); + ExitProcess(1); + } + dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP; _sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, TEXT(" \"--with-library=%s\""), library_dir); diff --git a/src/calibre/constants.py b/src/calibre/constants.py index f6b0a8d802..627f1b751a 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -32,6 +32,7 @@ isbsd = isfreebsd or isnetbsd islinux = not(iswindows or isosx or isbsd) isfrozen = hasattr(sys, 'frozen') isunix = isosx or islinux +isportable = os.environ.get('CALIBRE_PORTABLE_BUILD', None) is not None try: preferred_encoding = locale.getpreferredencoding() diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index ec0f28273f..d1c5b6ccd5 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -594,7 +594,7 @@ from calibre.devices.iliad.driver import ILIAD from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800 from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX -from calibre.devices.nook.driver import NOOK, NOOK_COLOR, NOOK_TSR +from calibre.devices.nook.driver import NOOK, NOOK_COLOR from calibre.devices.prs505.driver import PRS505 from calibre.devices.user_defined.driver import USER_DEFINED from calibre.devices.android.driver import ANDROID, S60 @@ -603,10 +603,11 @@ from calibre.devices.eslick.driver import ESLICK, EBK52 from calibre.devices.nuut2.driver import NUUT2 from calibre.devices.iriver.driver import IRIVER_STORY from calibre.devices.binatone.driver import README -from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK +from calibre.devices.hanvon.driver import (N516, EB511, ALEX, AZBOOKA, THEBOOK, + LIBREAIR) from calibre.devices.edge.driver import EDGE -from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ - SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER +from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS, + SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER) from calibre.devices.sne.driver import SNE from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL, GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR, @@ -693,7 +694,7 @@ plugins += [ KINDLE, KINDLE2, KINDLE_DX, - NOOK, NOOK_COLOR, NOOK_TSR, + NOOK, NOOK_COLOR, PRS505, ANDROID, S60, @@ -716,7 +717,7 @@ plugins += [ EB600, README, N516, - THEBOOK, + THEBOOK, LIBREAIR, EB511, ELONEX, TECLAST_K3, @@ -866,13 +867,20 @@ class ActionStore(InterfaceActionBase): from calibre.gui2.store.config.store import save_settings as save save(config_widget) +class ActionPluginUpdater(InterfaceActionBase): + name = 'Plugin Updater' + author = 'Grant Drake' + description = 'Queries the MobileRead forums for updates to plugins to install' + actual_plugin = 'calibre.gui2.actions.plugin_updates:PluginUpdaterAction' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, - ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore] + ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore, + ActionPluginUpdater] # }}} diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 24b720ec3e..e259400ec9 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -493,6 +493,8 @@ def initialize_plugin(plugin, path_to_zip_file): raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:') %tb) + '\n'+tb) +def has_external_plugins(): + return bool(config['plugins']) def initialize_plugins(): global _initialized_plugins diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index e1133ab929..dea5844028 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -135,7 +135,8 @@ class ITUNES(DriverBase): ''' Calling sequences: Initialization: - can_handle() or can_handle_windows() + can_handle() | can_handle_windows() + _launch_iTunes() reset() open() card_prefix() diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py index f9dec178c6..3798257c2d 100644 --- a/src/calibre/devices/hanvon/driver.py +++ b/src/calibre/devices/hanvon/driver.py @@ -52,6 +52,18 @@ class THEBOOK(N516): EBOOK_DIR_MAIN = 'My books' WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE' +class LIBREAIR(N516): + name = 'Libre Air Driver' + gui_name = 'Libre Air' + description = _('Communicate with the Libre Air reader.') + author = 'Kovid Goyal' + FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'rtf', 'txt', 'pdf'] + + BCD = [0x399] + VENDOR_NAME = 'ALURATEK' + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGET' + EBOOK_DIR_MAIN = 'Books' + class ALEX(N516): name = 'Alex driver' diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py index e09fb7eaf9..9c8f882f3d 100644 --- a/src/calibre/devices/nook/driver.py +++ b/src/calibre/devices/nook/driver.py @@ -81,55 +81,28 @@ class NOOK(USBMS): return [x.replace('#', '_') for x in components] class NOOK_COLOR(NOOK): - gui_name = _('Nook Color') - description = _('Communicate with the Nook Color eBook reader.') + description = _('Communicate with the Nook Color and TSR eBook readers.') - PRODUCT_ID = [0x002] + PRODUCT_ID = [0x002, 0x003] BCD = [0x216] - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK' + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK' EBOOK_DIR_MAIN = 'My Files' + NEWS_IN_FOLDER = False + + def upload_cover(self, path, filename, metadata, filepath): + pass + + def get_carda_ebook_dir(self, for_upload=False): + if for_upload: + return self.EBOOK_DIR_MAIN + return '' def create_upload_path(self, path, mdata, fname, create_dirs=True): - filepath = NOOK.create_upload_path(self, path, mdata, fname, - create_dirs=False) - edm = self.EBOOK_DIR_MAIN - subdir = 'Books' - if mdata.tags: - if _('News') in mdata.tags: - subdir = 'Magazines' - filepath = filepath.replace(os.sep+edm+os.sep, - os.sep+edm+os.sep+subdir+os.sep) - filedir = os.path.dirname(filepath) - if create_dirs and not os.path.exists(filedir): - os.makedirs(filedir) - - return filepath - - def upload_cover(self, path, filename, metadata, filepath): - pass - - def get_carda_ebook_dir(self, for_upload=False): - if for_upload: - return 'My Files/Books' - return '' - -class NOOK_TSR(NOOK): - gui_name = _('Nook Simple') - description = _('Communicate with the Nook TSR eBook reader.') - - PRODUCT_ID = [0x003] - BCD = [0x216] - - EBOOK_DIR_MAIN = 'My Files/Books' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK' - - def upload_cover(self, path, filename, metadata, filepath): - pass - - def get_carda_ebook_dir(self, for_upload=False): - if for_upload: - return 'My Files/Books' - return '' + is_news = mdata.tags and _('News') in mdata.tags + subdir = 'Magazines' if is_news else 'Books' + path = os.path.join(path, subdir) + return USBMS.create_upload_path(self, path, mdata, fname, + create_dirs=create_dirs) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index f0090989d9..442f3701c4 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -101,6 +101,9 @@ class Device(DeviceConfig, DevicePlugin): #: The maximum length of paths created on the device MAX_PATH_LEN = 250 + #: Put news in its own folder + NEWS_IN_FOLDER = True + def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): self._main_prefix = self._card_a_prefix = self._card_b_prefix = None @@ -946,7 +949,8 @@ class Device(DeviceConfig, DevicePlugin): extra_components = [] tag = special_tag if tag.startswith(_('News')): - extra_components.append('News') + if self.NEWS_IN_FOLDER: + extra_components.append('News') else: for c in tag.split('/'): c = sanitize(c) diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py index bea90eeba8..bb515f95a4 100644 --- a/src/calibre/ebooks/epub/output.py +++ b/src/calibre/ebooks/epub/output.py @@ -394,6 +394,13 @@ class EPUBOutput(OutputFormatPlugin): for tag in XPath('//h:img[@src]')(root): tag.set('src', tag.get('src', '').replace('&', '')) + # ADE whimpers in fright when it encounters a outside a + # + in_table = XPath('ancestor::h:table') + for tag in XPath('//h:td|//h:tr|//h:th')(root): + if not in_table(tag): + tag.tag = XHTML('div') + special_chars = re.compile(u'[\u200b\u00ad]') for elem in root.iterdescendants(): if getattr(elem, 'text', False): @@ -413,7 +420,7 @@ class EPUBOutput(OutputFormatPlugin): rule.style.removeProperty('margin-left') # padding-left breaks rendering in webkit and gecko rule.style.removeProperty('padding-left') - # Change whitespace:pre to pre-line to accommodate readers that + # Change whitespace:pre to pre-wrap to accommodate readers that # cannot scroll horizontally for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE): style = rule.style diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py index 3d5f6c00ef..ce6c46c6cf 100644 --- a/src/calibre/ebooks/html/input.py +++ b/src/calibre/ebooks/html/input.py @@ -455,13 +455,16 @@ class HTMLInput(InputFormatPlugin): bhref = os.path.basename(link) id, href = self.oeb.manifest.generate(id='added', href=bhref) + guessed = self.guess_type(href)[0] + media_type = guessed or self.BINARY_MIME + if 'text' in media_type: + self.log.warn('Ignoring link to text file %r'%link_) + return None + self.oeb.log.debug('Added', link) self.oeb.container = self.DirContainer(os.path.dirname(link), self.oeb.log, ignore_opf=True) # Load into memory - guessed = self.guess_type(href)[0] - media_type = guessed or self.BINARY_MIME - item = self.oeb.manifest.add(id, href, media_type) item.html_input_href = bhref if guessed in self.OEB_STYLES: diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 303bb2db6e..849dbd1555 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -85,7 +85,11 @@ class ISBNMerge(object): isbns, min_year = xisbn.get_isbn_pool(isbn) if not isbns: isbns = frozenset([isbn]) - self.pools[isbns] = pool = (min_year, []) + if isbns in self.pools: + # xISBN had a brain fart + pool = self.pools[isbns] + else: + self.pools[isbns] = pool = (min_year, []) if not self.pool_has_result_from_same_source(pool, result): pool[1].append(result) diff --git a/src/calibre/ebooks/metadata/xisbn.py b/src/calibre/ebooks/metadata/xisbn.py index 56156c034e..ff21dfd89d 100644 --- a/src/calibre/ebooks/metadata/xisbn.py +++ b/src/calibre/ebooks/metadata/xisbn.py @@ -45,6 +45,11 @@ class xISBN(object): ans.append(rec) return ans + def isbns_in_data(self, data): + for rec in data: + for i in rec.get('isbn', []): + yield i + def get_data(self, isbn): isbn = self.purify(isbn) with self.lock: @@ -57,9 +62,8 @@ class xISBN(object): data = [] id_ = len(self._data) self._data.append(data) - for rec in data: - for i in rec.get('isbn', []): - self._map[i] = id_ + for i in self.isbns_in_data(data): + self._map[i] = id_ self._map[isbn] = id_ return self._data[self._map[isbn]] diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 2275552c15..d108742f3c 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -442,9 +442,16 @@ class MobiMLizer(object): if tag in TABLE_TAGS and self.ignore_tables: tag = 'span' if tag == 'td' else 'div' - # GR: Added 'width', 'border' and 'scope' + if tag == 'table': + col = style.backgroundColor + if col: + elem.set('bgcolor', col) + css = style.cssdict() + if 'border' in css or 'border-width' in css: + elem.set('border', '1') if tag in TABLE_TAGS: - for attr in ('rowspan', 'colspan','width','border','scope'): + for attr in ('rowspan', 'colspan', 'width', 'border', 'scope', + 'bgcolor'): if attr in elem.attrib: istate.attrib[attr] = elem.attrib[attr] if tag == 'q': diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 2ca62f0dea..bf9de37513 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -241,6 +241,7 @@ class Serializer(object): if self.write_page_breaks_after_item: buffer.write('') buffer.write('') + self.anchor_offset = None def serialize_elem(self, elem, item, nsrmap=NSRMAP): buffer = self.buffer diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index fc7a27b5cd..f6ff594701 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -11,7 +11,6 @@ __copyright__ = '2008, Marshall T. Vandegrift ' import os, itertools, re, logging, copy, unicodedata from weakref import WeakKeyDictionary from xml.dom import SyntaxErr as CSSSyntaxError -import cssutils from cssutils.css import (CSSStyleRule, CSSPageRule, CSSStyleDeclaration, CSSFontFaceRule, cssproperties) try: @@ -20,7 +19,8 @@ try: except ImportError: # cssutils >= 0.9.8 from cssutils.css import PropertyValue as CSSValueList -from cssutils import profile as cssprofiles +from cssutils import (profile as cssprofiles, parseString, parseStyle, log as + cssutils_log, CSSParser, profiles) from lxml import etree from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError from calibre import force_unicode @@ -28,7 +28,7 @@ from calibre.ebooks import unit_convert from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize -cssutils.log.setLevel(logging.WARN) +cssutils_log.setLevel(logging.WARN) _html_css_stylesheet = None @@ -36,7 +36,7 @@ def html_css_stylesheet(): global _html_css_stylesheet if _html_css_stylesheet is None: html_css = open(P('templates/html.css'), 'rb').read() - _html_css_stylesheet = cssutils.parseString(html_css) + _html_css_stylesheet = parseString(html_css) _html_css_stylesheet.namespaces['h'] = XHTML_NS return _html_css_stylesheet @@ -157,11 +157,11 @@ class Stylizer(object): # Add cssutils parsing profiles from output_profile for profile in self.opts.output_profile.extra_css_modules: - cssutils.profile.addProfile(profile['name'], + cssprofiles.addProfile(profile['name'], profile['props'], profile['macros']) - parser = cssutils.CSSParser(fetcher=self._fetch_css_file, + parser = CSSParser(fetcher=self._fetch_css_file, log=logging.getLogger('calibre.css')) self.font_face_rules = [] for elem in head: @@ -473,6 +473,7 @@ class Style(object): self._width = None self._height = None self._lineHeight = None + self._bgcolor = None stylizer._styles[element] = self def set(self, prop, val): @@ -533,6 +534,48 @@ class Style(object): def pt_to_px(self, value): return (self._profile.dpi / 72.0) * value + @property + def backgroundColor(self): + ''' + Return the background color by parsing both the background-color and + background shortcut properties. Note that inheritance/default values + are not used. None is returned if no background color is set. + ''' + + def validate_color(col): + return cssprofiles.validateWithProfile('color', + col, + profiles=[profiles.Profiles.CSS_LEVEL_2])[1] + + if self._bgcolor is None: + col = None + val = self._style.get('background-color', None) + if val and validate_color(val): + col = val + else: + val = self._style.get('background', None) + if val is not None: + try: + style = parseStyle('background: '+val) + val = style.getProperty('background').cssValue + try: + val = list(val) + except: + # val is CSSPrimitiveValue + val = [val] + for c in val: + c = c.cssText + if validate_color(c): + col = c + break + except: + pass + if col is None: + self._bgcolor = False + else: + self._bgcolor = col + return self._bgcolor if self._bgcolor else None + @property def fontSize(self): def normalize_fontsize(value, base): diff --git a/src/calibre/gui2/actions/plugin_updates.py b/src/calibre/gui2/actions/plugin_updates.py new file mode 100644 index 0000000000..f8adbbe98f --- /dev/null +++ b/src/calibre/gui2/actions/plugin_updates.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Grant Drake ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QApplication, Qt, QIcon +from calibre.gui2.actions import InterfaceAction +from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, + FILTER_ALL, FILTER_UPDATE_AVAILABLE) + +class PluginUpdaterAction(InterfaceAction): + + name = 'Plugin Updater' + action_spec = (_('Plugin Updater'), None, None, None) + action_type = 'current' + + def genesis(self): + self.qaction.setIcon(QIcon(I('plugins/plugin_updater.png'))) + self.qaction.triggered.connect(self.check_for_plugin_updates) + + def check_for_plugin_updates(self): + # Get the user to choose a plugin to install + initial_filter = FILTER_UPDATE_AVAILABLE + mods = QApplication.keyboardModifiers() + if mods & Qt.ControlModifier or mods & Qt.ShiftModifier: + initial_filter = FILTER_ALL + + d = PluginUpdaterDialog(self.gui, initial_filter=initial_filter) + d.exec_() diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py index 1ebd4ea6ba..c3bf9bbe8b 100644 --- a/src/calibre/gui2/actions/preferences.py +++ b/src/calibre/gui2/actions/preferences.py @@ -24,6 +24,8 @@ class PreferencesAction(InterfaceAction): pm.addAction(QIcon(I('config.png')), _('Change calibre behavior'), self.do_config) pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'), self.gui.run_wizard) + pm.addAction(QIcon(I('plugins/plugin_updater.png')), + _('Get plugins to enhance calibre'), self.get_plugins) if not DEBUG: pm.addSeparator() ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'), @@ -36,6 +38,12 @@ class PreferencesAction(InterfaceAction): for x in (self.gui.preferences_action, self.qaction): x.triggered.connect(self.do_config) + def get_plugins(self): + from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, + FILTER_NOT_INSTALLED) + d = PluginUpdaterDialog(self.gui, + initial_filter=FILTER_NOT_INSTALLED) + d.exec_() def do_config(self, checked=False, initial_plugin=None, close_after_initial=False): diff --git a/src/calibre/gui2/dialogs/plugin_updater.py b/src/calibre/gui2/dialogs/plugin_updater.py new file mode 100644 index 0000000000..e7d641b43e --- /dev/null +++ b/src/calibre/gui2/dialogs/plugin_updater.py @@ -0,0 +1,869 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Grant Drake ' +__docformat__ = 'restructuredtext en' + +import re, datetime, traceback +from lxml import html +from PyQt4.Qt import (Qt, QUrl, QFrame, QVBoxLayout, QLabel, QBrush, QTextEdit, + QComboBox, QAbstractItemView, QHBoxLayout, QDialogButtonBox, + QAbstractTableModel, QVariant, QTableView, QModelIndex, + QSortFilterProxyModel, QAction, QIcon, QDialog, + QFont, QPixmap, QSize) +from calibre import browser, prints +from calibre.constants import numeric_version, iswindows, isosx, DEBUG +from calibre.customize.ui import (initialized_plugins, is_disabled, remove_plugin, + add_plugin, enable_plugin, disable_plugin, + NameConflict, has_external_plugins) +from calibre.gui2 import error_dialog, question_dialog, info_dialog, NONE, open_url, gprefs +from calibre.gui2.preferences.plugins import ConfigWidget +from calibre.utils.date import UNDEFINED_DATE, format_date + + +MR_URL = 'http://www.mobileread.com/forums/' +MR_INDEX_URL = MR_URL + 'showpost.php?p=1362767&postcount=1' + +FILTER_ALL = 0 +FILTER_INSTALLED = 1 +FILTER_UPDATE_AVAILABLE = 2 +FILTER_NOT_INSTALLED = 3 + +def get_plugin_updates_available(): + ''' + API exposed to read whether there are updates available for any + of the installed user plugins. + Returns None if no updates found + Returns list(DisplayPlugin) of plugins installed that have a new version + ''' + if not has_external_plugins(): + return None + display_plugins = read_available_plugins() + if display_plugins: + update_plugins = filter(filter_upgradeable_plugins, display_plugins) + if len(update_plugins) > 0: + return update_plugins + return None + +def filter_upgradeable_plugins(display_plugin): + return display_plugin.is_upgrade_available() + +def filter_not_installed_plugins(display_plugin): + return not display_plugin.is_installed() + +def read_available_plugins(): + display_plugins = [] + br = browser() + br.set_handle_gzip(True) + try: + raw = br.open_novisit(MR_INDEX_URL).read() + if not raw: + return + except: + traceback.print_exc() + return + raw = raw.decode('utf-8', errors='replace') + root = html.fromstring(raw) + list_nodes = root.xpath('//div[@id="post_message_1362767"]/ul/li') + # Add our deprecated plugins which are nested in a grey span + list_nodes.extend(root.xpath('//div[@id="post_message_1362767"]/span/ul/li')) + for list_node in list_nodes: + try: + display_plugin = DisplayPlugin(list_node) + get_installed_plugin_status(display_plugin) + display_plugins.append(display_plugin) + except: + if DEBUG: + prints('======= MobileRead Parse Error =======') + traceback.print_exc() + prints(html.tostring(list_node)) + display_plugins = sorted(display_plugins, key=lambda k: k.name) + return display_plugins + +def get_installed_plugin_status(display_plugin): + display_plugin.installed_version = None + display_plugin.plugin = None + for plugin in initialized_plugins(): + if plugin.name == display_plugin.name: + display_plugin.plugin = plugin + display_plugin.installed_version = plugin.version + break + if display_plugin.uninstall_plugins: + # Plugin requires a specific plugin name to be uninstalled first + # This could occur when a plugin is renamed (Kindle Collections) + # or multiple plugins deprecated into a newly named one. + # Check whether user has the previous version(s) installed + plugins_to_remove = list(display_plugin.uninstall_plugins) + for plugin_to_uninstall in plugins_to_remove: + found = False + for plugin in initialized_plugins(): + if plugin.name == plugin_to_uninstall: + found = True + break + if not found: + display_plugin.uninstall_plugins.remove(plugin_to_uninstall) + + +class ImageTitleLayout(QHBoxLayout): + ''' + A reusable layout widget displaying an image followed by a title + ''' + def __init__(self, parent, icon_name, title): + QHBoxLayout.__init__(self) + title_font = QFont() + title_font.setPointSize(16) + title_image_label = QLabel(parent) + pixmap = QPixmap() + pixmap.load(I(icon_name)) + if pixmap is None: + error_dialog(parent, _('Restart required'), + _('You must restart Calibre before using this plugin!'), show=True) + else: + title_image_label.setPixmap(pixmap) + title_image_label.setMaximumSize(32, 32) + title_image_label.setScaledContents(True) + self.addWidget(title_image_label) + shelf_label = QLabel(title, parent) + shelf_label.setFont(title_font) + self.addWidget(shelf_label) + self.insertStretch(-1) + + +class SizePersistedDialog(QDialog): + ''' + This dialog is a base class for any dialogs that want their size/position + restored when they are next opened. + ''' + + initial_extra_size = QSize(0, 0) + + def __init__(self, parent, unique_pref_name): + QDialog.__init__(self, parent) + self.unique_pref_name = unique_pref_name + self.geom = gprefs.get(unique_pref_name, None) + self.finished.connect(self.dialog_closing) + + def resize_dialog(self): + if self.geom is None: + self.resize(self.sizeHint()+self.initial_extra_size) + else: + self.restoreGeometry(self.geom) + + def dialog_closing(self, result): + geom = bytearray(self.saveGeometry()) + gprefs[self.unique_pref_name] = geom + + +class VersionHistoryDialog(SizePersistedDialog): + + def __init__(self, parent, plugin_name, html): + SizePersistedDialog.__init__(self, parent, 'Plugin Updater plugin:version history dialog') + self.setWindowTitle(_('Version History for %s')%plugin_name) + + layout = QVBoxLayout(self) + self.setLayout(layout) + + self.notes = QTextEdit(html, self) + self.notes.setReadOnly(True) + layout.addWidget(self.notes) + + self.button_box = QDialogButtonBox(QDialogButtonBox.Close) + self.button_box.rejected.connect(self.reject) + layout.addWidget(self.button_box) + + # Cause our dialog size to be restored from prefs or created on first usage + self.resize_dialog() + + +class PluginFilterComboBox(QComboBox): + def __init__(self, parent): + QComboBox.__init__(self, parent) + items = [_('All'), _('Installed'), _('Update available'), _('Not installed')] + self.addItems(items) + + +class DisplayPlugin(object): + + def __init__(self, list_node): + # The html from the index web page looks like this: + ''' +
  • Book Sync
    +Add books to a list to be automatically sent to your device the next time it is connected.
    +Version: 1.1; Released: 02-22-2011; Calibre: 0.7.42; Author: kiwidude;
    +Platforms: Windows, OSX, Linux; History: Yes;
  • + ''' + self.name = list_node.xpath('a')[0].text_content().strip() + self.forum_link = list_node.xpath('a/@href')[0].strip() + self.installed_version = None + + description_text = list_node.xpath('i')[0].text_content() + description_parts = description_text.partition('Version:') + self.description = description_parts[0].strip() + + details_text = description_parts[1] + description_parts[2].replace('\r\n','') + details_pairs = details_text.split(';') + details = {} + for details_pair in details_pairs: + pair = details_pair.split(':') + if len(pair) == 2: + key = pair[0].strip().lower() + value = pair[1].strip() + details[key] = value + + donation_node = list_node.xpath('i/span/a/@href') + self.donation_link = donation_node[0] if donation_node else None + + self.available_version = self._version_text_to_tuple(details.get('version', None)) + + release_date = details.get('released', '01-01-0101').split('-') + date_parts = [int(re.search(r'(\d+)', x).group(1)) for x in release_date] + self.release_date = datetime.date(date_parts[2], date_parts[0], date_parts[1]) + + self.calibre_required_version = self._version_text_to_tuple(details.get('calibre', None)) + self.author = details.get('author', '') + self.platforms = [p.strip().lower() for p in details.get('platforms', '').split(',')] + # Optional pairing just for plugins which require checking for uninstall first + self.uninstall_plugins = [] + uninstall = details.get('uninstall', None) + if uninstall: + self.uninstall_plugins = [i.strip() for i in uninstall.split(',')] + self.has_changelog = details.get('history', 'No').lower() in ['yes', 'true'] + self.is_deprecated = details.get('deprecated', 'No').lower() in ['yes', 'true'] + + def _version_text_to_tuple(self, version_text): + if version_text: + ver = version_text.split('.') + while len(ver) < 3: + ver.append('0') + ver = [int(re.search(r'(\d+)', x).group(1)) for x in ver] + return tuple(ver) + else: + return None + + def is_disabled(self): + if self.plugin is None: + return False + return is_disabled(self.plugin) + + def is_installed(self): + return self.installed_version is not None + + def is_upgrade_available(self): + return self.is_installed() and (self.installed_version < self.available_version \ + or self.is_deprecated) + + def is_valid_platform(self): + if iswindows: + return 'windows' in self.platforms + if isosx: + return 'osx' in self.platforms + return 'linux' in self.platforms + + def is_valid_calibre(self): + return numeric_version >= self.calibre_required_version + + def is_valid_to_install(self): + return self.is_valid_platform() and self.is_valid_calibre() and not self.is_deprecated + + +class DisplayPluginSortFilterModel(QSortFilterProxyModel): + + def __init__(self, parent): + QSortFilterProxyModel.__init__(self, parent) + self.setSortRole(Qt.UserRole) + self.filter_criteria = FILTER_ALL + + def filterAcceptsRow(self, sourceRow, sourceParent): + index = self.sourceModel().index(sourceRow, 0, sourceParent) + display_plugin = self.sourceModel().display_plugins[index.row()] + if self.filter_criteria == FILTER_ALL: + return not (display_plugin.is_deprecated and not display_plugin.is_installed()) + if self.filter_criteria == FILTER_INSTALLED: + return display_plugin.is_installed() + if self.filter_criteria == FILTER_UPDATE_AVAILABLE: + return display_plugin.is_upgrade_available() + if self.filter_criteria == FILTER_NOT_INSTALLED: + return not display_plugin.is_installed() and not display_plugin.is_deprecated + return False + + def set_filter_criteria(self, filter_value): + self.filter_criteria = filter_value + self.invalidateFilter() + + +class DisplayPluginModel(QAbstractTableModel): + + def __init__(self, display_plugins): + QAbstractTableModel.__init__(self) + self.display_plugins = display_plugins + self.headers = map(QVariant, [_('Plugin Name'), _('Donate'), _('Status'), _('Installed'), + _('Available'), _('Released'), _('Calibre'), _('Author')]) + + def rowCount(self, *args): + return len(self.display_plugins) + + def columnCount(self, *args): + return len(self.headers) + + 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): + if not index.isValid(): + return NONE; + row, col = index.row(), index.column() + if row < 0 or row >= self.rowCount(): + return NONE + display_plugin = self.display_plugins[row] + if role in [Qt.DisplayRole, Qt.UserRole]: + if col == 0: + return QVariant(display_plugin.name) + if col == 1: + if display_plugin.donation_link: + return QVariant(_('PayPal')) + if col == 2: + return self._get_status(display_plugin) + if col == 3: + return QVariant(self._get_display_version(display_plugin.installed_version)) + if col == 4: + return QVariant(self._get_display_version(display_plugin.available_version)) + if col == 5: + if role == Qt.UserRole: + return self._get_display_release_date(display_plugin.release_date, 'yyyyMMdd') + else: + return self._get_display_release_date(display_plugin.release_date) + if col == 6: + return QVariant(self._get_display_version(display_plugin.calibre_required_version)) + if col == 7: + return QVariant(display_plugin.author) + elif role == Qt.DecorationRole: + if col == 0: + return self._get_status_icon(display_plugin) + if col == 1: + if display_plugin.donation_link: + return QIcon(I('donate.png')) + elif role == Qt.ToolTipRole: + if col == 1 and display_plugin.donation_link: + return QVariant(_('This plugin is FREE but you can reward the developer for their effort\n' + 'by donating to them via PayPal.\n\n' + 'Right-click and choose Donate to reward: ')+display_plugin.author) + else: + return self._get_status_tooltip(display_plugin) + elif role == Qt.ForegroundRole: + if col != 1: # Never change colour of the donation column + if display_plugin.is_deprecated: + return QVariant(QBrush(Qt.blue)) + if display_plugin.is_disabled(): + return QVariant(QBrush(Qt.gray)) + return NONE + + def plugin_to_index(self, display_plugin): + for i, p in enumerate(self.display_plugins): + if display_plugin == p: + return self.index(i, 0, QModelIndex()) + return QModelIndex() + + def refresh_plugin(self, display_plugin): + idx = self.plugin_to_index(display_plugin) + self.dataChanged.emit(idx, idx) + + def _get_display_release_date(self, date_value, format='dd MMM yyyy'): + if date_value and date_value != UNDEFINED_DATE: + return QVariant(format_date(date_value, format)) + return NONE + + def _get_display_version(self, version): + if version is None: + return '' + return '.'.join([str(v) for v in list(version)]) + + def _get_status(self, display_plugin): + if not display_plugin.is_valid_platform(): + return _('Platform unavailable') + if not display_plugin.is_valid_calibre(): + return _('Calibre upgrade required') + if display_plugin.is_installed(): + if display_plugin.is_deprecated: + return _('Plugin deprecated') + elif display_plugin.is_upgrade_available(): + return _('New version available') + else: + return _('Latest version installed') + return _('Not installed') + + def _get_status_icon(self, display_plugin): + if display_plugin.is_deprecated: + icon_name = 'plugin_deprecated.png' + elif display_plugin.is_disabled(): + if display_plugin.is_upgrade_available(): + if display_plugin.is_valid_to_install(): + icon_name = 'plugin_disabled_valid.png' + else: + icon_name = 'plugin_disabled_invalid.png' + else: + icon_name = 'plugin_disabled_ok.png' + elif display_plugin.is_installed(): + if display_plugin.is_upgrade_available(): + if display_plugin.is_valid_to_install(): + icon_name = 'plugin_upgrade_valid.png' + else: + icon_name = 'plugin_upgrade_invalid.png' + else: + icon_name = 'plugin_upgrade_ok.png' + else: # A plugin available not currently installed + if display_plugin.is_valid_to_install(): + icon_name = 'plugin_new_valid.png' + else: + icon_name = 'plugin_new_invalid.png' + return QIcon(I('plugins/' + icon_name)) + + def _get_status_tooltip(self, display_plugin): + if display_plugin.is_deprecated: + return QVariant(_('This plugin has been deprecated and should be uninstalled')+'\n\n'+ + _('Right-click to see more options')) + if not display_plugin.is_valid_platform(): + return QVariant(_('This plugin can only be installed on: %s') % \ + ', '.join(display_plugin.platforms)+'\n\n'+ + _('Right-click to see more options')) + if numeric_version < display_plugin.calibre_required_version: + return QVariant(_('You must upgrade to at least Calibre %s before installing this plugin') % \ + self._get_display_version(display_plugin.calibre_required_version)+'\n\n'+ + _('Right-click to see more options')) + if display_plugin.installed_version < display_plugin.available_version: + if display_plugin.installed_version is None: + return QVariant(_('You can install this plugin')+'\n\n'+ + _('Right-click to see more options')) + else: + return QVariant(_('A new version of this plugin is available')+'\n\n'+ + _('Right-click to see more options')) + return QVariant(_('This plugin is installed and up-to-date')+'\n\n'+ + _('Right-click to see more options')) + + +class PluginUpdaterDialog(SizePersistedDialog): + + initial_extra_size = QSize(350, 100) + + def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE): + SizePersistedDialog.__init__(self, gui, 'Plugin Updater plugin:plugin updater dialog') + self.gui = gui + self.forum_link = None + self.model = None + self._initialize_controls() + self._create_context_menu() + + display_plugins = read_available_plugins() + + if display_plugins: + self.model = DisplayPluginModel(display_plugins) + self.proxy_model = DisplayPluginSortFilterModel(self) + self.proxy_model.setSourceModel(self.model) + self.plugin_view.setModel(self.proxy_model) + self.plugin_view.resizeColumnsToContents() + self.plugin_view.selectionModel().currentRowChanged.connect(self._plugin_current_changed) + self.plugin_view.doubleClicked.connect(self.install_button.click) + self.filter_combo.setCurrentIndex(initial_filter) + self._select_and_focus_view() + else: + error_dialog(self.gui, _('Update Check Failed'), + _('Unable to reach the MobileRead plugins forum index page.'), + det_msg=MR_INDEX_URL, show=True) + self.filter_combo.setEnabled(False) + # Cause our dialog size to be restored from prefs or created on first usage + self.resize_dialog() + + def _initialize_controls(self): + self.setWindowTitle(_('User plugins')) + self.setWindowIcon(QIcon(I('plugins/plugin_updater.png'))) + layout = QVBoxLayout(self) + self.setLayout(layout) + title_layout = ImageTitleLayout(self, 'plugins/plugin_updater.png', + _('User Plugins')) + layout.addLayout(title_layout) + + header_layout = QHBoxLayout() + layout.addLayout(header_layout) + self.filter_combo = PluginFilterComboBox(self) + self.filter_combo.setMinimumContentsLength(20) + self.filter_combo.currentIndexChanged[int].connect(self._filter_combo_changed) + header_layout.addWidget(QLabel(_('Filter list of plugins')+':', self)) + header_layout.addWidget(self.filter_combo) + header_layout.addStretch(10) + + self.plugin_view = QTableView(self) + self.plugin_view.horizontalHeader().setStretchLastSection(True) + self.plugin_view.setSelectionBehavior(QAbstractItemView.SelectRows) + self.plugin_view.setSelectionMode(QAbstractItemView.SingleSelection) + self.plugin_view.setAlternatingRowColors(True) + self.plugin_view.setSortingEnabled(True) + self.plugin_view.setIconSize(QSize(28, 28)) + layout.addWidget(self.plugin_view) + + details_layout = QHBoxLayout() + layout.addLayout(details_layout) + forum_label = QLabel('Plugin Forum Thread', self) + forum_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) + forum_label.linkActivated.connect(self._forum_label_activated) + details_layout.addWidget(QLabel(_('Description')+':', self), 0, Qt.AlignLeft) + details_layout.addWidget(forum_label, 1, Qt.AlignRight) + + self.description = QLabel(self) + self.description.setFrameStyle(QFrame.Panel | QFrame.Sunken) + self.description.setAlignment(Qt.AlignTop | Qt.AlignLeft) + self.description.setMinimumHeight(40) + layout.addWidget(self.description) + + self.button_box = QDialogButtonBox(QDialogButtonBox.Close) + self.button_box.rejected.connect(self._close_clicked) + self.install_button = self.button_box.addButton(_('&Install'), QDialogButtonBox.AcceptRole) + self.install_button.setToolTip(_('Install the selected plugin')) + self.install_button.clicked.connect(self._install_clicked) + self.install_button.setEnabled(False) + self.configure_button = self.button_box.addButton(' '+_('&Customize plugin ')+' ', QDialogButtonBox.ResetRole) + self.configure_button.setToolTip(_('Customize the options for this plugin')) + self.configure_button.clicked.connect(self._configure_clicked) + self.configure_button.setEnabled(False) + layout.addWidget(self.button_box) + + def _create_context_menu(self): + self.plugin_view.setContextMenuPolicy(Qt.ActionsContextMenu) + self.install_action = QAction(QIcon(I('plugins/plugin_upgrade_ok.png')), _('&Install'), self) + self.install_action.setToolTip(_('Install the selected plugin')) + self.install_action.triggered.connect(self._install_clicked) + self.install_action.setEnabled(False) + self.plugin_view.addAction(self.install_action) + self.history_action = QAction(QIcon(I('chapters.png')), _('Version &History'), self) + self.history_action.setToolTip(_('Show history of changes to this plugin')) + self.history_action.triggered.connect(self._history_clicked) + self.history_action.setEnabled(False) + self.plugin_view.addAction(self.history_action) + self.forum_action = QAction(QIcon(I('plugins/mobileread.png')), _('Plugin &Forum Thread'), self) + self.forum_action.triggered.connect(self._forum_label_activated) + self.forum_action.setEnabled(False) + self.plugin_view.addAction(self.forum_action) + + sep1 = QAction(self) + sep1.setSeparator(True) + self.plugin_view.addAction(sep1) + + self.toggle_enabled_action = QAction(_('Enable/&Disable plugin'), self) + self.toggle_enabled_action.setToolTip(_('Enable or disable this plugin')) + self.toggle_enabled_action.triggered.connect(self._toggle_enabled_clicked) + self.toggle_enabled_action.setEnabled(False) + self.plugin_view.addAction(self.toggle_enabled_action) + self.uninstall_action = QAction(_('&Remove plugin'), self) + self.uninstall_action.setToolTip(_('Uninstall the selected plugin')) + self.uninstall_action.triggered.connect(self._uninstall_clicked) + self.uninstall_action.setEnabled(False) + self.plugin_view.addAction(self.uninstall_action) + + sep2 = QAction(self) + sep2.setSeparator(True) + self.plugin_view.addAction(sep2) + + self.donate_enabled_action = QAction(QIcon(I('donate.png')), _('Donate to developer'), self) + self.donate_enabled_action.setToolTip(_('Donate to the developer of this plugin')) + self.donate_enabled_action.triggered.connect(self._donate_clicked) + self.donate_enabled_action.setEnabled(False) + self.plugin_view.addAction(self.donate_enabled_action) + + sep3 = QAction(self) + sep3.setSeparator(True) + self.plugin_view.addAction(sep3) + + self.configure_action = QAction(QIcon(I('config.png')), _('&Customize plugin'), self) + self.configure_action.setToolTip(_('Customize the options for this plugin')) + self.configure_action.triggered.connect(self._configure_clicked) + self.configure_action.setEnabled(False) + self.plugin_view.addAction(self.configure_action) + + def _close_clicked(self): + # Force our toolbar/action to be updated based on uninstalled updates + if self.model: + update_plugins = filter(filter_upgradeable_plugins, self.model.display_plugins) + self.gui.recalc_update_label(len(update_plugins)) + self.reject() + + def _plugin_current_changed(self, current, previous): + if current.isValid(): + actual_idx = self.proxy_model.mapToSource(current) + display_plugin = self.model.display_plugins[actual_idx.row()] + self.description.setText(display_plugin.description) + self.forum_link = display_plugin.forum_link + self.forum_action.setEnabled(bool(self.forum_link)) + self.install_button.setEnabled(display_plugin.is_valid_to_install()) + self.install_action.setEnabled(self.install_button.isEnabled()) + self.uninstall_action.setEnabled(display_plugin.is_installed()) + self.history_action.setEnabled(display_plugin.has_changelog) + self.configure_button.setEnabled(display_plugin.is_installed()) + self.configure_action.setEnabled(self.configure_button.isEnabled()) + self.toggle_enabled_action.setEnabled(display_plugin.is_installed()) + self.donate_enabled_action.setEnabled(bool(display_plugin.donation_link)) + else: + self.description.setText('') + self.forum_link = None + self.forum_action.setEnabled(False) + self.install_button.setEnabled(False) + self.install_action.setEnabled(False) + self.uninstall_action.setEnabled(False) + self.history_action.setEnabled(False) + self.configure_button.setEnabled(False) + self.configure_action.setEnabled(False) + self.toggle_enabled_action.setEnabled(False) + self.donate_enabled_action.setEnabled(False) + + def _donate_clicked(self): + plugin = self._selected_display_plugin() + if plugin and plugin.donation_link: + open_url(QUrl(plugin.donation_link)) + + def _select_and_focus_view(self, change_selection=True): + if change_selection and self.plugin_view.model().rowCount() > 0: + self.plugin_view.selectRow(0) + else: + idx = self.plugin_view.selectionModel().currentIndex() + self._plugin_current_changed(idx, 0) + self.plugin_view.setFocus() + + def _filter_combo_changed(self, idx): + self.proxy_model.set_filter_criteria(idx) + if idx == FILTER_NOT_INSTALLED: + self.plugin_view.sortByColumn(5, Qt.DescendingOrder) + else: + self.plugin_view.sortByColumn(0, Qt.AscendingOrder) + self._select_and_focus_view() + + def _forum_label_activated(self): + if self.forum_link: + open_url(QUrl(self.forum_link)) + + def _selected_display_plugin(self): + idx = self.plugin_view.selectionModel().currentIndex() + actual_idx = self.proxy_model.mapToSource(idx) + return self.model.display_plugins[actual_idx.row()] + + def _uninstall_plugin(self, name_to_remove): + if DEBUG: + prints('Removing plugin: ', name_to_remove) + remove_plugin(name_to_remove) + # Make sure that any other plugins that required this plugin + # to be uninstalled first have the requirement removed + for display_plugin in self.model.display_plugins: + # Make sure we update the status and display of the + # plugin we just uninstalled + if name_to_remove in display_plugin.uninstall_plugins: + if DEBUG: + prints('Removing uninstall dependency for: ', display_plugin.name) + display_plugin.uninstall_plugins.remove(name_to_remove) + if display_plugin.name == name_to_remove: + if DEBUG: + prints('Resetting plugin to uninstalled status: ', display_plugin.name) + display_plugin.installed_version = None + display_plugin.plugin = None + display_plugin.uninstall_plugins = [] + if self.proxy_model.filter_criteria not in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]: + self.model.refresh_plugin(display_plugin) + + def _uninstall_clicked(self): + display_plugin = self._selected_display_plugin() + if not question_dialog(self, _('Are you sure?'), '

    '+ + _('Are you sure you want to uninstall the %s plugin?')%display_plugin.name, + show_copy_button=False): + return + self._uninstall_plugin(display_plugin.name) + if self.proxy_model.filter_criteria in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]: + self.model.reset() + self._select_and_focus_view() + else: + self._select_and_focus_view(change_selection=False) + + def _install_clicked(self): + display_plugin = self._selected_display_plugin() + if not question_dialog(self, _('Install %s')%display_plugin.name, '

    ' + \ + _('Installing plugins is a security risk. ' + 'Plugins can contain a virus/malware. ' + 'Only install it if you got it from a trusted source.' + ' Are you sure you want to proceed?'), + show_copy_button=False): + return + + if display_plugin.uninstall_plugins: + uninstall_names = list(display_plugin.uninstall_plugins) + if DEBUG: + prints('Uninstalling plugin: ', ', '.join(uninstall_names)) + for name_to_remove in uninstall_names: + self._uninstall_plugin(name_to_remove) + + if DEBUG: + prints('Locating zip file for %s: %s'% (display_plugin.name, display_plugin.forum_link)) + self.gui.status_bar.showMessage(_('Locating zip file for %s: %s') % (display_plugin.name, display_plugin.forum_link)) + plugin_zip_url = self._read_zip_attachment_url(display_plugin.forum_link) + if not plugin_zip_url: + return error_dialog(self.gui, _('Install Plugin Failed'), + _('Unable to locate a plugin zip file for %s') % display_plugin.name, + det_msg=display_plugin.forum_link, show=True) + + if DEBUG: + prints('Downloading plugin zip attachment: ', plugin_zip_url) + self.gui.status_bar.showMessage(_('Downloading plugin zip attachment: %s') % plugin_zip_url) + zip_path = self._download_zip(plugin_zip_url) + + if DEBUG: + prints('Installing plugin: ', zip_path) + self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path) + + try: + try: + plugin = add_plugin(zip_path) + except NameConflict as e: + return error_dialog(self.gui, _('Already exists'), + unicode(e), show=True) + # Check for any toolbars to add to. + widget = ConfigWidget(self.gui) + widget.gui = self.gui + widget.check_for_add_to_toolbars(plugin) + self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name) + info_dialog(self.gui, _('Success'), + _('Plugin {0} successfully installed under ' + ' {1} plugins. You may have to restart calibre ' + 'for the plugin to take effect.').format(plugin.name, plugin.type), + show=True, show_copy_button=False) + + display_plugin.plugin = plugin + # We cannot read the 'actual' version information as the plugin will not be loaded yet + display_plugin.installed_version = display_plugin.available_version + except: + if DEBUG: + prints('ERROR occurred while installing plugin: %s'%display_plugin.name) + traceback.print_exc() + error_dialog(self.gui, _('Install Plugin Failed'), + _('A problem occurred while installing this plugin.' + ' This plugin will now be uninstalled.' + ' Please post the error message in details below into' + ' the forum thread for this plugin and restart Calibre.'), + det_msg=traceback.format_exc(), show=True) + if DEBUG: + prints('Due to error now uninstalling plugin: %s'%display_plugin.name) + remove_plugin(display_plugin.name) + display_plugin.plugin = None + + display_plugin.uninstall_plugins = [] + if self.proxy_model.filter_criteria in [FILTER_NOT_INSTALLED, FILTER_UPDATE_AVAILABLE]: + self.model.reset() + self._select_and_focus_view() + else: + self.model.refresh_plugin(display_plugin) + self._select_and_focus_view(change_selection=False) + + def _history_clicked(self): + display_plugin = self._selected_display_plugin() + text = self._read_version_history_html(display_plugin.forum_link) + if text: + dlg = VersionHistoryDialog(self, display_plugin.name, text) + dlg.exec_() + else: + return error_dialog(self, _('Version history missing'), + _('Unable to find the version history for %s')%display_plugin.name, + show=True) + + def _configure_clicked(self): + display_plugin = self._selected_display_plugin() + plugin = display_plugin.plugin + if not plugin.is_customizable(): + return info_dialog(self, _('Plugin not customizable'), + _('Plugin: %s does not need customization')%plugin.name, show=True) + from calibre.customize import InterfaceActionBase + if isinstance(plugin, InterfaceActionBase) and not getattr(plugin, + 'actual_iaction_plugin_loaded', False): + return error_dialog(self, _('Must restart'), + _('You must restart calibre before you can' + ' configure the %s plugin')%plugin.name, show=True) + plugin.do_user_config(self.parent()) + + def _toggle_enabled_clicked(self): + display_plugin = self._selected_display_plugin() + plugin = display_plugin.plugin + if not plugin.can_be_disabled: + return error_dialog(self,_('Plugin cannot be disabled'), + _('The plugin: %s cannot be disabled')%plugin.name, show=True) + if is_disabled(plugin): + enable_plugin(plugin) + else: + disable_plugin(plugin) + self.model.refresh_plugin(display_plugin) + + def _read_version_history_html(self, forum_link): + br = browser() + br.set_handle_gzip(True) + try: + raw = br.open_novisit(forum_link).read() + if not raw: + return None + except: + traceback.print_exc() + return None + raw = raw.decode('utf-8', errors='replace') + root = html.fromstring(raw) + spoiler_nodes = root.xpath('//div[@class="smallfont" and strong="Spoiler"]') + for spoiler_node in spoiler_nodes: + try: + if spoiler_node.getprevious() is None: + # This is a spoiler node that has been indented using [INDENT] + # Need to go up to parent div, then previous node to get header + heading_node = spoiler_node.getparent().getprevious() + else: + # This is a spoiler node after a BR tag from the heading + heading_node = spoiler_node.getprevious().getprevious() + if heading_node is None: + continue + if heading_node.text_content().lower().find('version history') != -1: + div_node = spoiler_node.xpath('div')[0] + text = html.tostring(div_node, method='html', encoding=unicode) + return re.sub('', '

    ', text) + except: + if DEBUG: + prints('======= MobileRead Parse Error =======') + traceback.print_exc() + prints(html.tostring(spoiler_node)) + return None + + def _read_zip_attachment_url(self, forum_link): + br = browser() + br.set_handle_gzip(True) + try: + raw = br.open_novisit(forum_link).read() + if not raw: + return None + except: + traceback.print_exc() + return None + raw = raw.decode('utf-8', errors='replace') + root = html.fromstring(raw) + attachment_nodes = root.xpath('//fieldset/table/tr/td/a') + for attachment_node in attachment_nodes: + try: + filename = attachment_node.text_content().lower() + if filename.find('.zip') != -1: + full_url = MR_URL + attachment_node.attrib['href'] + return full_url + except: + if DEBUG: + prints('======= MobileRead Parse Error =======') + traceback.print_exc() + prints(html.tostring(attachment_node)) + return None + + def _download_zip(self, plugin_zip_url): + from calibre.ptempfile import PersistentTemporaryFile + br = browser() + br.set_handle_gzip(True) + raw = br.open_novisit(plugin_zip_url).read() + pt = PersistentTemporaryFile('.zip') + pt.write(raw) + pt.close() + return pt.name diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index a75ff01b21..874be6832f 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -27,7 +27,6 @@ def partial(*args, **kwargs): _keep_refs.append(ans) return ans - class LibraryViewMixin(object): # {{{ def __init__(self, db): @@ -145,6 +144,7 @@ class UpdateLabel(QLabel): # {{{ def __init__(self, *args, **kwargs): QLabel.__init__(self, *args, **kwargs) + self.setCursor(Qt.PointingHandCursor) def contextMenuEvent(self, e): pass @@ -182,14 +182,6 @@ class StatusBar(QStatusBar): # {{{ self.defmsg.setText(self.default_message) self.clearMessage() - def new_version_available(self, ver, url): - msg = (u'%s: %s') % ( - _('Update found'), ver, ver) - self.update_label.setText(msg) - self.update_label.setCursor(Qt.PointingHandCursor) - self.update_label.setVisible(True) - def get_version(self): dv = os.environ.get('CALIBRE_DEVELOP_FROM', None) v = __version__ @@ -257,12 +249,6 @@ class LayoutMixin(object): # {{{ self.setStatusBar(self.status_bar) self.status_bar.update_label.linkActivated.connect(self.update_link_clicked) - def update_link_clicked(self, url): - url = unicode(url) - if url.startswith('update:'): - version = url.partition(':')[-1] - self.update_found(version, force=True) - def finalize_layout(self): self.status_bar.initialize(self.system_tray_icon) self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b62aa28a68..d818f2db2a 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -388,6 +388,10 @@ class MetadataSingleDialogBase(ResizableDialog): def apply_changes(self): self.changed.add(self.book_id) + if self.db is None: + # break_cycles has already been called, don't know why this should + # happen but a user reported it + return True for widget in self.basic_metadata_widgets: try: if not widget.commit(self.db, self.book_id): diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 013ab42684..ff4b9feac8 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -236,6 +236,11 @@ class ResultsView(QTableView): # {{{ self.resizeRowsToContents() self.resizeColumnsToContents() self.setFocus(Qt.OtherFocusReason) + idx = self.model().index(0, 0) + if idx.isValid() and self.model().rowCount() > 0: + self.show_details(idx) + sm = self.selectionModel() + sm.select(idx, sm.ClearAndSelect|sm.Rows) def currentChanged(self, current, previous): ret = QTableView.currentChanged(self, current, previous) @@ -480,12 +485,6 @@ class IdentifyWidget(QWidget): # {{{ return self.results_view.show_results(self.worker.results) - - self.comments_view.show_data(''' -
    Found %d results
    -
    To see details, click on any result
    ''' % - len(self.worker.results)) - self.results_found.emit() diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index b3c7873b45..3b581be701 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -22,11 +22,7 @@ from calibre.library.coloring import (Rule, conditionable_columns, class ConditionEditor(QWidget): # {{{ - def __init__(self, fm, parent=None): - QWidget.__init__(self, parent) - self.fm = fm - - self.action_map = { + ACTION_MAP = { 'bool' : ( (_('is true'), 'is true',), (_('is false'), 'is false'), @@ -61,10 +57,17 @@ class ConditionEditor(QWidget): # {{{ (_('is set'), 'is set'), (_('is not set'), 'is not set'), ), - } + } - for x in ('float', 'rating', 'datetime'): - self.action_map[x] = self.action_map['int'] + for x in ('float', 'rating', 'datetime'): + ACTION_MAP[x] = ACTION_MAP['int'] + + + def __init__(self, fm, parent=None): + QWidget.__init__(self, parent) + self.fm = fm + + self.action_map = self.ACTION_MAP self.l = l = QGridLayout(self) self.setLayout(l) @@ -446,9 +449,15 @@ class RulesModel(QAbstractListModel): # {{{ def condition_to_html(self, condition): c, a, v = condition + action_name = a + for actions in ConditionEditor.ACTION_MAP.itervalues(): + for trans, ac in actions: + if ac == a: + action_name = trans + return ( _('
  • If the %s column %s value: %s') % - (c, a, prepare_string_for_xml(v))) + (c, action_name, prepare_string_for_xml(v))) # }}} diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index 4f88e5aa1d..246df79d8f 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -8,16 +8,16 @@ __docformat__ = 'restructuredtext en' import textwrap, os from collections import OrderedDict -from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \ - QBrush +from PyQt4.Qt import (Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, + QBrush) 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, plugin_customization, add_plugin, remove_plugin, NameConflict) -from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \ - question_dialog, gprefs +from calibre.gui2 import (NONE, error_dialog, info_dialog, choose_files, + question_dialog, gprefs) from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.icu import lower @@ -217,6 +217,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.customize_plugin_button.clicked.connect(self.customize_plugin) self.remove_plugin_button.clicked.connect(self.remove_plugin) self.button_plugin_add.clicked.connect(self.add_plugin) + self.button_plugin_updates.clicked.connect(self.update_plugins) + self.button_plugin_new.clicked.connect(self.get_plugins) self.search.initialize('plugin_search_history', help_text=_('Search for plugin')) self.search.search.connect(self.find) @@ -353,6 +355,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): plugin.name + _(' cannot be removed. It is a ' 'builtin plugin. Try disabling it instead.')).exec_() + def get_plugins(self): + self.update_plugins(not_installed=True) + + def update_plugins(self, not_installed=False): + from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, + FILTER_UPDATE_AVAILABLE, FILTER_NOT_INSTALLED) + mode = FILTER_NOT_INSTALLED if not_installed else FILTER_UPDATE_AVAILABLE + d = PluginUpdaterDialog(self.gui, initial_filter=mode) + d.exec_() + self._plugin_model.populate() + self._plugin_model.reset() + self.changed_signal.emit() + def reload_store_plugins(self): self.gui.load_store_plugins() if self.gui.iactions.has_key('Store'): diff --git a/src/calibre/gui2/preferences/plugins.ui b/src/calibre/gui2/preferences/plugins.ui index ebf422dfe3..b4dfd4426e 100644 --- a/src/calibre/gui2/preferences/plugins.ui +++ b/src/calibre/gui2/preferences/plugins.ui @@ -113,16 +113,49 @@ - - - &Add a new plugin - - - - :/images/plugins.png:/images/plugins.png + + + QFrame::HLine + + + + + + Get &new plugins + + + + :/images/plugins/plugin_new.png:/images/plugins/plugin_new.png + + + + + + + Check for &updated plugins + + + + :/images/plugins/plugin_updater.png:/images/plugins/plugin_updater.png + + + + + + + &Load plugin from file + + + + :/images/document_open.png:/images/document_open.png + + + + + diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.py b/src/calibre/gui2/store/config/chooser/chooser_widget.py index a9399028f8..5e7eca8c66 100644 --- a/src/calibre/gui2/store/config/chooser/chooser_widget.py +++ b/src/calibre/gui2/store/config/chooser/chooser_widget.py @@ -6,7 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QWidget, QIcon, QDialog) +from PyQt4.Qt import (QWidget, QIcon, QDialog, QComboBox) from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form @@ -18,6 +18,8 @@ class StoreChooserWidget(QWidget, Ui_Form): self.setupUi(self) self.query.initialize('store_config_chooser_query') + self.query.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) + self.query.setMinimumContentsLength(25) self.adv_search_builder.setIcon(QIcon(I('search.png'))) diff --git a/src/calibre/gui2/store/mobileread/store_dialog.py b/src/calibre/gui2/store/mobileread/store_dialog.py index 8908c9bb68..749f96a614 100644 --- a/src/calibre/gui2/store/mobileread/store_dialog.py +++ b/src/calibre/gui2/store/mobileread/store_dialog.py @@ -7,7 +7,7 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (Qt, QDialog, QIcon) +from PyQt4.Qt import (Qt, QDialog, QIcon, QComboBox) from calibre.gui2.store.mobileread.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.mobileread.models import BooksModel @@ -21,6 +21,8 @@ class MobileReadStoreDialog(QDialog, Ui_Dialog): self.plugin = plugin self.search_query.initialize('store_mobileread_search') + self.search_query.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) + self.search_query.setMinimumContentsLength(25) self.adv_search_button.setIcon(QIcon(I('search.png'))) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index f406b4923e..7db4d1bbaf 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -10,7 +10,8 @@ import re from random import shuffle from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel, - QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout) + QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout, + QComboBox) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator @@ -57,6 +58,8 @@ class SearchDialog(QDialog, Ui_Dialog): # Set the search query self.search_edit.setText(query) + self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) + self.search_edit.setMinimumContentsLength(25) # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index 9aae245d98..aacf30fe10 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -3,16 +3,30 @@ __copyright__ = '2008, Kovid Goyal ' import traceback -from PyQt4.Qt import QThread, pyqtSignal, Qt, QUrl, QDialog, QGridLayout, \ - QLabel, QCheckBox, QDialogButtonBox, QIcon, QPixmap +from PyQt4.Qt import (QThread, pyqtSignal, Qt, QUrl, QDialog, QGridLayout, + QLabel, QCheckBox, QDialogButtonBox, QIcon, QPixmap) import mechanize -from calibre.constants import __appname__, __version__, iswindows, isosx +from calibre.constants import (__appname__, __version__, iswindows, isosx, + isportable) from calibre import browser from calibre.utils.config import prefs from calibre.gui2 import config, dynamic, open_url +from calibre.gui2.dialogs.plugin_updater import get_plugin_updates_available URL = 'http://status.calibre-ebook.com/latest' +NO_CALIBRE_UPDATE = '-0.0.0' +VSEP = '|' + +def get_newest_version(): + br = browser() + req = mechanize.Request(URL) + req.add_header('CALIBRE_VERSION', __version__) + req.add_header('CALIBRE_OS', + 'win' if iswindows else 'osx' if isosx else 'oth') + req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid']) + version = br.open(req).read().strip() + return version class CheckForUpdates(QThread): @@ -24,23 +38,29 @@ class CheckForUpdates(QThread): def run(self): while True: + calibre_update_version = NO_CALIBRE_UPDATE + plugins_update_found = 0 try: - br = browser() - req = mechanize.Request(URL) - req.add_header('CALIBRE_VERSION', __version__) - req.add_header('CALIBRE_OS', - 'win' if iswindows else 'osx' if isosx else 'oth') - req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid']) - version = br.open(req).read().strip() + version = get_newest_version() if version and version != __version__ and len(version) < 10: - self.update_found.emit(version) + calibre_update_version = version except: traceback.print_exc() + try: + update_plugins = get_plugin_updates_available() + if update_plugins is not None: + plugins_update_found = len(update_plugins) + except: + traceback.print_exc() + if (calibre_update_version != NO_CALIBRE_UPDATE or + plugins_update_found > 0): + self.update_found.emit('%s%s%d'%(calibre_update_version, + VSEP, plugins_update_found)) self.sleep(self.INTERVAL) class UpdateNotification(QDialog): - def __init__(self, version, parent=None): + def __init__(self, calibre_version, plugin_updates, parent=None): QDialog.__init__(self, parent) self.resize(400, 250) self.l = QGridLayout() @@ -54,7 +74,8 @@ class UpdateNotification(QDialog): 'See the new features.') + '

    '+_('Update only if one of the ' 'new features or bug fixes is important to you. ' - 'If the current version works well for you, do not update.'))%(__appname__, version)) + 'If the current version works well for you, do not update.'))%( + __appname__, calibre_version)) self.label.setOpenExternalLinks(True) self.label.setWordWrap(True) self.setWindowTitle(_('Update available!')) @@ -70,18 +91,30 @@ class UpdateNotification(QDialog): b = self.bb.addButton(_('&Get update'), self.bb.AcceptRole) b.setDefault(True) b.setIcon(QIcon(I('arrow-down.png'))) + if plugin_updates > 0: + b = self.bb.addButton(_('Update &plugins'), self.bb.ActionRole) + b.setIcon(QIcon(I('plugins/plugin_updater.png'))) + b.clicked.connect(self.get_plugins, type=Qt.QueuedConnection) self.bb.addButton(self.bb.Cancel) self.l.addWidget(self.bb, 2, 0, 1, -1) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) - dynamic.set('update to version %s'%version, False) + dynamic.set('update to version %s'%calibre_version, False) + + def get_plugins(self): + from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, + FILTER_UPDATE_AVAILABLE) + d = PluginUpdaterDialog(self.parent(), + initial_filter=FILTER_UPDATE_AVAILABLE) + d.exec_() def show_future(self, *args): config.set('new_version_notification', bool(self.cb.isChecked())) def accept(self): - url = 'http://calibre-ebook.com/download_'+\ - ('windows' if iswindows else 'osx' if isosx else 'linux') + url = ('http://calibre-ebook.com/download_' + + ('portable' if isportable else 'windows' if iswindows + else 'osx' if isosx else 'linux')) open_url(QUrl(url)) QDialog.accept(self) @@ -89,21 +122,79 @@ class UpdateNotification(QDialog): class UpdateMixin(object): def __init__(self, opts): + self.last_newest_calibre_version = NO_CALIBRE_UPDATE if not opts.no_update_check: self.update_checker = CheckForUpdates(self) self.update_checker.update_found.connect(self.update_found, type=Qt.QueuedConnection) self.update_checker.start() - def update_found(self, version, force=False): - os = 'windows' if iswindows else 'osx' if isosx else 'linux' - url = 'http://calibre-ebook.com/download_%s'%os - self.status_bar.new_version_available(version, url) + def recalc_update_label(self, number_of_plugin_updates): + self.update_found('%s%s%d'%(self.last_newest_calibre_version, VSEP, + number_of_plugin_updates), no_show_popup=True) - if force or (config.get('new_version_notification') and \ - dynamic.get('update to version %s'%version, True)): - self._update_notification__ = UpdateNotification(version, - parent=self) - self._update_notification__.show() + def update_found(self, version, force=False, no_show_popup=False): + try: + calibre_version, plugin_updates = version.split(VSEP) + plugin_updates = int(plugin_updates) + except: + traceback.print_exc() + return + self.last_newest_calibre_version = calibre_version + has_calibre_update = calibre_version and calibre_version != NO_CALIBRE_UPDATE + has_plugin_updates = plugin_updates > 0 + self.plugin_update_found(plugin_updates) + + if not has_calibre_update and not has_plugin_updates: + self.status_bar.update_label.setVisible(False) + return + if has_calibre_update: + plt = u'' + if has_plugin_updates: + plt = _(' (%d plugin updates)')%plugin_updates + msg = (u'%s: ' + u'%s%s') % ( + _('Update found'), version, calibre_version, plt) + else: + msg = (u'%d %s')%(version, plugin_updates, + _('updated plugins')) + self.status_bar.update_label.setText(msg) + self.status_bar.update_label.setVisible(True) + if has_calibre_update: + if (force or (config.get('new_version_notification') and + dynamic.get('update to version %s'%calibre_version, True))): + if not no_show_popup: + self._update_notification__ = UpdateNotification(calibre_version, + plugin_updates, parent=self) + self._update_notification__.show() + elif has_plugin_updates: + if force: + from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, + FILTER_UPDATE_AVAILABLE) + d = PluginUpdaterDialog(self, + initial_filter=FILTER_UPDATE_AVAILABLE) + d.exec_() + + def plugin_update_found(self, number_of_updates): + # Change the plugin icon to indicate there are updates available + plugin = self.iactions.get('Plugin Updates', None) + if not plugin: + return + if number_of_updates: + plugin.qaction.setText(_('Plugin Updates')+'*') + plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater_updates.png'))) + plugin.qaction.setToolTip( + _('There are %d plugin updates available')%number_of_updates) + else: + plugin.qaction.setText(_('Plugin Updates')) + plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater.png'))) + plugin.qaction.setToolTip(_('Install and configure user plugins')) + + def update_link_clicked(self, url): + url = unicode(url) + if url.startswith('update:'): + version = url[len('update:'):] + self.update_found(version, force=True) + diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index dd8d876005..96fbafe4d5 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -5,15 +5,15 @@ Miscellaneous widgets used in the GUI ''' import re, traceback -from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \ - QListWidgetItem, QTextCharFormat, QApplication, \ - QSyntaxHighlighter, QCursor, QColor, QWidget, \ - QPixmap, QSplitterHandle, QToolButton, \ - QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \ - QRegExp, QSettings, QSize, QSplitter, \ - QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, \ - QMenu, QStringListModel, QCompleter, QStringList, \ - QTimer, QRect, QFontDatabase, QGraphicsView +from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction, + QListWidgetItem, QTextCharFormat, QApplication, + QSyntaxHighlighter, QCursor, QColor, QWidget, + QPixmap, QSplitterHandle, QToolButton, + QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, + QRegExp, QSettings, QSize, QSplitter, + QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, + QMenu, QStringListModel, QCompleter, QStringList, + QTimer, QRect, QFontDatabase, QGraphicsView) from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs from calibre.gui2.filename_pattern_ui import Ui_Form @@ -21,12 +21,12 @@ from calibre import fit_image from calibre.ebooks import BOOK_EXTENSIONS from calibre.utils.config import prefs, XMLConfig, tweaks from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator -from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ - IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog +from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files, + IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog) history = XMLConfig('history') -class ProgressIndicator(QWidget): +class ProgressIndicator(QWidget): # {{{ def __init__(self, *args): QWidget.__init__(self, *args) @@ -57,8 +57,9 @@ class ProgressIndicator(QWidget): def stop(self): self.pi.stopAnimation() self.setVisible(False) +# }}} -class FilenamePattern(QWidget, Ui_Form): +class FilenamePattern(QWidget, Ui_Form): # {{{ changed_signal = pyqtSignal() @@ -148,8 +149,9 @@ class FilenamePattern(QWidget, Ui_Form): return pat +# }}} -class FormatList(QListWidget): +class FormatList(QListWidget): # {{{ DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS formats_dropped = pyqtSignal(object, object) delete_format = pyqtSignal() @@ -188,6 +190,8 @@ class FormatList(QListWidget): else: return QListWidget.keyPressEvent(self, event) +# }}} + class ImageDropMixin(object): # {{{ ''' Adds support for dropping images onto widgets and a context menu for @@ -262,7 +266,7 @@ class ImageDropMixin(object): # {{{ pixmap_to_data(pmap)) # }}} -class ImageView(QWidget, ImageDropMixin): +class ImageView(QWidget, ImageDropMixin): # {{{ BORDER_WIDTH = 1 cover_changed = pyqtSignal(object) @@ -314,8 +318,9 @@ class ImageView(QWidget, ImageDropMixin): p.drawRect(target) #p.drawRect(self.rect()) p.end() +# }}} -class CoverView(QGraphicsView, ImageDropMixin): +class CoverView(QGraphicsView, ImageDropMixin): # {{{ cover_changed = pyqtSignal(object) @@ -333,7 +338,9 @@ class CoverView(QGraphicsView, ImageDropMixin): self.scene.addPixmap(pmap) self.setScene(self.scene) -class FontFamilyModel(QAbstractListModel): +# }}} + +class FontFamilyModel(QAbstractListModel): # {{{ def __init__(self, *args): QAbstractListModel.__init__(self, *args) @@ -371,7 +378,9 @@ class FontFamilyModel(QAbstractListModel): def index_of(self, family): return self.families.index(family.strip()) +# }}} +# BasicList {{{ class BasicListItem(QListWidgetItem): def __init__(self, text, user_data=None): @@ -404,9 +413,9 @@ class BasicList(QListWidget): def items(self): for i in range(self.count()): yield self.item(i) +# }}} - -class LineEditECM(object): +class LineEditECM(object): # {{{ ''' Extend the context menu of a QLineEdit to include more actions. @@ -449,8 +458,9 @@ class LineEditECM(object): from calibre.utils.icu import capitalize self.setText(capitalize(unicode(self.text()))) +# }}} -class EnLineEdit(LineEditECM, QLineEdit): +class EnLineEdit(LineEditECM, QLineEdit): # {{{ ''' Enhanced QLineEdit. @@ -459,9 +469,9 @@ class EnLineEdit(LineEditECM, QLineEdit): ''' pass +# }}} - -class ItemsCompleter(QCompleter): +class ItemsCompleter(QCompleter): # {{{ ''' A completer object that completes a list of tags. It is used in conjunction @@ -486,8 +496,9 @@ class ItemsCompleter(QCompleter): model = QStringListModel(items, self) self.setModel(model) +# }}} -class CompleteLineEdit(EnLineEdit): +class CompleteLineEdit(EnLineEdit): # {{{ ''' A QLineEdit that can complete parts of text separated by separator. @@ -550,8 +561,9 @@ class CompleteLineEdit(EnLineEdit): self.setText(complete_text_pat % (before_text[:cursor_pos - prefix_len], text, self.separator, after_text)) self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra) +# }}} -class EnComboBox(QComboBox): +class EnComboBox(QComboBox): # {{{ ''' Enhanced QComboBox. @@ -575,7 +587,9 @@ class EnComboBox(QComboBox): idx = 0 self.setCurrentIndex(idx) -class CompleteComboBox(EnComboBox): +# }}} + +class CompleteComboBox(EnComboBox): # {{{ def __init__(self, *args): EnComboBox.__init__(self, *args) @@ -590,8 +604,9 @@ class CompleteComboBox(EnComboBox): def set_space_before_sep(self, space_before): self.lineEdit().set_space_before_sep(space_before) +# }}} -class HistoryLineEdit(QComboBox): +class HistoryLineEdit(QComboBox): # {{{ lost_focus = pyqtSignal() @@ -638,7 +653,9 @@ class HistoryLineEdit(QComboBox): QComboBox.focusOutEvent(self, e) self.lost_focus.emit() -class ComboBoxWithHelp(QComboBox): +# }}} + +class ComboBoxWithHelp(QComboBox): # {{{ ''' A combobox where item 0 is help text. CurrentText will return '' for item 0. Be sure to always fetch the text with currentText. Don't use the signals @@ -685,8 +702,9 @@ class ComboBoxWithHelp(QComboBox): QComboBox.hidePopup(self) self.set_state() +# }}} -class EncodingComboBox(QComboBox): +class EncodingComboBox(QComboBox): # {{{ ''' A combobox that holds text encodings support by Python. This is only populated with the most @@ -709,8 +727,9 @@ class EncodingComboBox(QComboBox): for item in self.ENCODINGS: self.addItem(item) +# }}} -class PythonHighlighter(QSyntaxHighlighter): +class PythonHighlighter(QSyntaxHighlighter): # {{{ Rules = [] Formats = {} @@ -948,6 +967,9 @@ class PythonHighlighter(QSyntaxHighlighter): QSyntaxHighlighter.rehighlight(self) QApplication.restoreOverrideCursor() +# }}} + +# Splitter {{{ class SplitterHandle(QSplitterHandle): double_clicked = pyqtSignal(object) @@ -1179,3 +1201,5 @@ class Splitter(QSplitter): # }}} +# }}} + diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index a19534191b..97454c90e2 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -149,6 +149,15 @@ class CSV_XML(CatalogPlugin): # {{{ elif field == 'comments': item = item.replace(u'\r\n',u' ') item = item.replace(u'\n',u' ') + + # Convert HTML to markdown text + if type(item) is unicode: + opening_tag = re.search('<(\w+)(\x20|>)',item) + if opening_tag: + closing_tag = re.search('<\/%s>$' % opening_tag.group(1), item) + if closing_tag: + item = html2text(item) + outstr.append(u'"%s"' % unicode(item).replace('"','""')) outfile.write(u','.join(outstr) + u'\n') diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 274bf03eb3..4b527e169c 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -515,7 +515,7 @@ Downloading from the internet can sometimes result in a corrupted download. If t * Try rebooting your computer and running a registry cleaner like `Wise registry cleaner `_. * Try downloading the installer with an alternate browser. For example if you are using Internet Explorer, try using Firefox or Chrome instead. -Best place to ask for more help is in the `forums `_. +If you still cannot get the installer to work and you are on windows, you can use the `calibre portable install `_, which does not need an installer (it is just a zip file). My antivirus program claims |app| is a virus/trojan? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py index 1eb504d282..87a65dd9e6 100644 --- a/src/calibre/web/feeds/recipes/collection.py +++ b/src/calibre/web/feeds/recipes/collection.py @@ -21,8 +21,8 @@ NS = 'http://calibre-ebook.com/recipe_collection' E = ElementMaker(namespace=NS, nsmap={None:NS}) def iterate_over_builtin_recipe_files(): - exclude = ['craigslist', 'iht', 'outlook_india', 'toronto_sun', - 'indian_express', 'india_today', 'livemint'] + exclude = ['craigslist', 'iht', 'toronto_sun', + 'india_today', 'livemint'] d = os.path.dirname base = os.path.join(d(d(d(d(d(d(os.path.abspath(__file__))))))), 'recipes') for f in os.listdir(base): @@ -101,6 +101,7 @@ def get_custom_recipe_collection(*args): if recipe_class is not None: rmap['custom:%s'%id_] = recipe_class except: + print 'Failed to load recipe from: %r'%fname import traceback traceback.print_exc() continue