Merge from trunk
@ -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/')]
|
||||
|
||||
|
||||
|
40
recipes/noticias_r7.recipe
Normal 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">')
|
||||
]
|
@ -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;
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
BIN
resources/images/plugins/mobileread.png
Normal file
After Width: | Height: | Size: 641 B |
BIN
resources/images/plugins/plugin_deprecated.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
resources/images/plugins/plugin_disabled_invalid.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/plugins/plugin_disabled_ok.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
resources/images/plugins/plugin_disabled_valid.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/images/plugins/plugin_new.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
resources/images/plugins/plugin_new_invalid.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
resources/images/plugins/plugin_new_valid.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
resources/images/plugins/plugin_updater.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
resources/images/plugins/plugin_updater_updates.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
resources/images/plugins/plugin_upgrade_invalid.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
resources/images/plugins/plugin_upgrade_ok.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/plugins/plugin_upgrade_valid.png
Normal file
After Width: | Height: | Size: 14 KiB |
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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]
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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('/'):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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]]
|
||||
|
@ -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':
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
33
src/calibre/gui2/actions/plugin_updates.py
Normal 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_()
|
@ -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):
|
||||
|
869
src/calibre/gui2/dialogs/plugin_updater.py
Normal 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
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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)))
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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'):
|
||||
|
@ -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>&Add a new plugin</string>
|
||||
<string>Get &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 &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>&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>
|
||||
|
@ -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')))
|
||||
|
||||
|
@ -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')))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
||||
# }}}
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -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
|
||||
|