Merge from trunk

This commit is contained in:
Charles Haley 2011-06-17 16:34:55 +01:00
commit 0876a3efd1
49 changed files with 1669 additions and 287 deletions

View File

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

View File

@ -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'<div class="materia">.*<div class="materia_cabecalho">',re.DOTALL|re.IGNORECASE),
lambda match: '<div class="materia"><div class="materia_cabecalho">')
]

View File

@ -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,7 +1727,13 @@ Monocle.Book = function (dataSource) {
function setPageAt(pageDiv, locus) {
locus = pageNumberAt(pageDiv, locus);
if (!locus.load) {
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);
@ -1673,6 +1746,7 @@ Monocle.Book = function (dataSource) {
}
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,12 +2854,18 @@ 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) {
} else {
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;
}
}
}
p.length = Math.ceil(p.measurements.scrollWidth / p.measurements.width);
p.dirty = false;
@ -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'];
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';
}
}
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);
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;
Monocle.defer(function () {
setX(pageDiv, 0, { duration: dur }, callback);
});
}
function jumpOut(pageDiv, callback) {
var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0;
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'
};
Monocle.defer(function () {
setX(upperPage(), 0, slideOpts, callback);
});
}
@ -3391,7 +3565,9 @@ Monocle.Flippers.Slider = function (reader) {
duration: k.durations.SLIDE,
timing: 'ease-in'
};
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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,6 +949,7 @@ class Device(DeviceConfig, DevicePlugin):
extra_components = []
tag = special_tag
if tag.startswith(_('News')):
if self.NEWS_IN_FOLDER:
extra_components.append('News')
else:
for c in tag.split('/'):

View File

@ -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 <td> outside a
# <table>
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

View File

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

View File

@ -85,6 +85,10 @@ class ISBNMerge(object):
isbns, min_year = xisbn.get_isbn_pool(isbn)
if not isbns:
isbns = frozenset([isbn])
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):

View File

@ -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,8 +62,7 @@ class xISBN(object):
data = []
id_ = len(self._data)
self._data.append(data)
for rec in data:
for i in rec.get('isbn', []):
for i in self.isbns_in_data(data):
self._map[i] = id_
self._map[isbn] = id_
return self._data[self._map[isbn]]

View File

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

View File

@ -241,6 +241,7 @@ class Serializer(object):
if self.write_page_breaks_after_item:
buffer.write('<mbp:pagebreak/>')
buffer.write('</div>')
self.anchor_offset = None
def serialize_elem(self, elem, item, nsrmap=NSRMAP):
buffer = self.buffer

View File

@ -11,7 +11,6 @@ __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
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):

View File

@ -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 <grant.drake@gmail.com>'
__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_()

View File

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

View File

@ -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 <grant.drake@gmail.com>'
__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:
'''
<li><a href="http://www.mobileread.com/forums/showthread.php?t=121787">Book Sync</a><br />
<i>Add books to a list to be automatically sent to your device the next time it is connected.<br />
<span class="resize_1">Version: 1.1; Released: 02-22-2011; Calibre: 0.7.42; Author: kiwidude; <br />
Platforms: Windows, OSX, Linux; History: Yes;</span></i></li>
'''
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('<a href="http://www.foo.com/">Plugin Forum Thread</a>', 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?'), '<p>'+
_('Are you sure you want to uninstall the <b>%s</b> 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, '<p>' + \
_('Installing plugins is a <b>security risk</b>. '
'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 <b>%s</b>') % 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 <b>{0}</b> successfully installed under <b>'
' {1} plugins</b>. 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 <b>%s</b> 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('<div\s.*?>', '<div>', 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

View File

@ -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'<span style="color:red; font-weight: bold">%s: <a'
' href="update:%s">%s<a></span>') % (
_('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)

View File

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

View File

@ -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('''
<div style="margin-bottom:2ex">Found <b>%d</b> results</div>
<div>To see <b>details</b>, click on any result</div>''' %
len(self.worker.results))
self.results_found.emit()

View File

@ -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'),
@ -64,7 +60,14 @@ class ConditionEditor(QWidget): # {{{
}
for x in ('float', 'rating', 'datetime'):
self.action_map[x] = self.action_map['int']
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 (
_('<li>If the <b>%s</b> column <b>%s</b> value: <b>%s</b>') %
(c, a, prepare_string_for_xml(v)))
(c, action_name, prepare_string_for_xml(v)))
# }}}

View File

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

View File

@ -113,16 +113,49 @@
</layout>
</item>
<item>
<widget class="QPushButton" name="button_plugin_add">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QPushButton" name="button_plugin_new">
<property name="text">
<string>&amp;Add a new plugin</string>
<string>Get &amp;new plugins</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/plugins.png</normaloff>:/images/plugins.png</iconset>
<normaloff>:/images/plugins/plugin_new.png</normaloff>:/images/plugins/plugin_new.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_plugin_updates">
<property name="text">
<string>Check for &amp;updated plugins</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/plugins/plugin_updater.png</normaloff>:/images/plugins/plugin_updater.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_plugin_add">
<property name="text">
<string>&amp;Load plugin from file</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>

View File

@ -6,7 +6,7 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__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')))

View File

@ -7,7 +7,7 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__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')))

View File

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

View File

@ -3,16 +3,30 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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 <a href="http://calibre-ebook.com/whats-new'
'">new features</a>.') + '<p>'+_('Update <b>only</b> 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)
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'<span style="color:red; font-weight: bold">%s: '
u'<a href="update:%s">%s%s</a></span>') % (
_('Update found'), version, calibre_version, plt)
else:
msg = (u'<a href="update:%s">%d %s</a>')%(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)

View File

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

View File

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

View File

@ -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 <http://www.wisecleaner.com>`_.
* 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 <http://www.mobileread.com/forums/forumdisplay.php?f=166>`_.
If you still cannot get the installer to work and you are on windows, you can use the `calibre portable install <http://calibre-ebook.com/download_portable>`_, which does not need an installer (it is just a zip file).
My antivirus program claims |app| is a virus/trojan?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

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