merged main branch

This commit is contained in:
Fabian Graßl 2010-10-18 11:18:43 +02:00
commit a58fe07aa5
88 changed files with 44436 additions and 19620 deletions

View File

@ -4,6 +4,111 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.24
date: 2010-10-17
new features:
- title: "Content server: New interface that allows browsing via categories, similar to the Tag Browser in the calibre interface."
description: >
"You can access the new interface by going to /browse. So if your calibre content server is available at http://192.168.1.2, use
http://192.168.1.2/browse. The new interface requires a fairly modern browser, so no Internet Explorer 6,7."
type: major
- title: "Support for the SNB e-book format, used by the Bambook e-book reader"
type: major
- title: "Driver for the Wifi Kobo"
- title: "Edit metadata dialog: If metadata is downloaded successfully, set focus to download cover button"
- title: "News download system: Allow recipes with optional subscriptions"
tickets: [7199]
- title: "Templates: Improve the smarten function"
- title: "Linux device mounting: Use udisks, if it is available, to mount devices, so that I no longer have to hear bug reports from users using distro packages that have crippled calibre-mount-helper. You can turn off udisks by setting the environment variable CALIBRE_DISABLE_UDISKS=1"
- title: "Implement Drag'n'drop to tags in user categories"
tickets: [7172]
- title: "Ebook viewer: Add command line option to start in full screen mode"
- title: "Set completion mode on search boxes to popup completion"
- title: "Update version of jQuery used in content server and viewer. Required a little hackery in the viewer, hopefully nothing broke"
bug fixes:
- title: "Linux device drivers: Ignore read only partition exported by the device"
- title: "E-book viewer: Fix scrolling down with mouse wheel not always reaching bottom in windows"
- title: "Smarten punctuation: Fix bug in handling of comments and <style> tags"
- title: "EPUB Input: Handle EPUB files with components encoded in an encoding other than UTF-8 correctly, though why anyone would do that is a mystery."
tickets: [7196]
- title: "OS X commandline tools: Decode non-ascii command line arguments correctly"
tickets: [6964]
- title: "MOBI Output: Fix bug that broke conversion of <svg> elements in the input document when the <svg> element was followed by non-whitespace text."
tickets: [7083]
- title: "CHM Input: Fix handling of relative file paths in <img> tags."
tickets: [7159]
- title: "EPUB Output: Fix incorrect format for xml:lang when specifying a sub language"
tickets: [7198]
- title: "EPUB Input: Make parsing of toc.ncx more robust."
tickets: [7170]
- title: "Content server: Fix searching with non-ascii characters on windows"
tickets: [5249]
- title: "Fix average rating calculation for rating datatype in Tag Browser incorrect"
- title: "Comic Input: Fix image borders becoming yellow on some windows installs"
- title: "Email sending: Fix sending of email with non ascii chars"
tickets: [7137]
- title: "SONY driver: Fix collections created from series not in order with manual metadata management, if all books in the series are not sent at once"
- title: "Content server: Apply the search restriction when generating category lists as well"
- title: "RTF Input: Fix regression in conversion of WMF images on linux at least, maybe on other platforms as wel"
- title: "Fix isbndb.com metadata downloading sometimes yield a title of Unknown"
tickets: [7114]
- title: "Fix edit metadata dialog causing the hour:minute:seconds of the date column being lost, even when date is not changed"
tickets: [7125]
new recipes:
- title: "Revista El Cultural"
author: "Jefferson Frantz"
- title: "Novaya Gazeta"
author: "muwa"
- title: "frazpc.pl"
author: "Tomasz Dlugosz"
- title: "Orsai and Financial Times UK"
author: "Darko Miletic"
- title: "Malayasian Mirror and Rolling Stones"
author: "Tony Stegall"
improved recipes:
- Globe and Mail
- Business Standard
- Miami Herald
- El Mercurio
- volkskrant.nl
- GoComics.com
- The New Yorker
- version: 0.7.23
date: 2010-10-08
@ -51,6 +156,7 @@
- title: "CHM input: handle another class of broken CHM files"
tickets: [7058]
- title: "Make calibre worker processes use the same temp directory as the calibre GUI"
new recipes:
- title: "Communications of the Association for Computing Machinery"

View File

@ -176,6 +176,10 @@ h2.library_name {
overflow: hidden;
}
#search_box .ui-button {
padding: 0.25em;
}
/* }}} */
/* Top level {{{ */
@ -183,19 +187,32 @@ h2.library_name {
list-style-type: none;
margin: 0;
padding: 0;
margin-left: auto;
margin-right: auto;
display: block;
}
.toplevel li {
margin: 0.75em;
padding: 0.75em;
text-align: center;
cursor: pointer;
font-size: larger;
float: left;
border-radius: 15px;
-moz-border-radius: 15px;
-webkit-border-radius: 15px;
display: inline;
width: 250px;
height: 48px;
overflow: hidden;
}
.toplevel li img {
vertical-align: middle;
margin-right: 1em;
}
.toplevel li:hover {
background-color: #d6d3c9;
font-weight: bold;
@ -205,7 +222,10 @@ h2.library_name {
}
.toplevel li span { display: none }
.toplevel li span.url { display: none }
.toplevel li span.label {
}
/* }}} */
@ -265,9 +285,156 @@ h2.library_name {
display: none;
}
#booklist .load_data { display: none }
.loading img {
vertical-align: middle;
}
#booklist .summary {
margin-bottom: 2ex;
border-bottom: solid 1px black;
}
#booklist div.left {
float: left;
height: 190px;
width: 100px;
vertical-align: middle;
text-align: center;
margin-left: 1em;
margin-right: 2em;
}
#booklist div.left img {
display: block;
margin-bottom: 1ex;
margin-left: auto;
margin-right: auto;
}
#booklist div.right {
height: 190px;
overflow: auto;
margin-left: 1em;
margin-right: 1em;
}
#booklist div.right .stars {
float:right;
margin-right: 1em;
}
#booklist div.right .stars .rating_container {
display: block;
}
#booklist div.right .stars .series {
display: block;
}
#booklist .title {
font-size: larger;
}
#booklist a {
text-decoration: none;
color: blue;
}
#booklist a:hover {
color: red;
}
#booklist .left .ui-button-text {
font-size: medium;
color: black;
padding-left: 0.25em;
padding-right: 0.25em;
padding-top: 0.25em;
padding-bottom: 0.25em;
}
#booklist .listnav {
display: table;
width: 100%;
}
#booklist .listnav a {
color: blue;
text-decoration: none;
}
#booklist .listnav a:hover {
color: red;
}
#booklist .topnav {
border-bottom: solid black 1px;
margin-bottom: 1ex;
}
#booklist .navleft {
display: table-cell;
text-align: left;
}
#booklist .navleft a {
margin-right: 1em;
}
#booklist .navmiddle {
display: table-cell;
text-align: center;
}
#booklist .navright {
display: table-cell;
text-align: right;
}
#booklist .navright a {
margin-left: 1em;
}
/* }}} */
/* Details {{{ */
.details .left {
float: left;
max-width: 50%;
margin-right: 2em;
overflow: auto;
}
.details .right {
overflow: auto;
}
.details .formats {
margin-bottom: 2ex;
}
.details .right .formats a {
color: blue;
text-decoration: none;
}
.details .right .formats a:hover {
color: red;
}
.details .field {
margin-bottom: 0.5em;
}
.details .comment {
margin-left: 1em;
overflow: auto;
max-height: 50%;
}
/* }}} */

View File

@ -76,8 +76,8 @@
</select>
</div>
<div id="search_box">
<form name="search_form" action="/browse/search" method="get">
<input value="" type="text" title="Search"
<form name="search_form" action="/browse/search" method="get" accept-charset="UTF-8">
<input value="{initial_search}" type="text" title="Search" name="query"
class="search_input" />&nbsp;
<input type="submit" value="Search" title="Search" alt="Search" />
</form>
@ -94,5 +94,6 @@
</div>
</div>
</div>
<div id="book_details_dialog"></div>
</body>
</html>

View File

@ -1,5 +1,35 @@
// Cookies {{{
/**
* Create a cookie with the given name and value and other optional parameters.
*
* @example $.cookie('the_cookie', 'the_value');
* @desc Set the value of a cookie.
* @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
* @desc Create a cookie with all available options.
* @example $.cookie('the_cookie', 'the_value');
* @desc Create a session cookie.
* @example $.cookie('the_cookie', null);
* @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
* used when the cookie was set.
*
* @param String name The name of the cookie.
* @param String value The value of the cookie.
* @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
* @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
* If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
* If set to null or omitted, the cookie will be a session cookie and will not be retained
* when the the browser exits.
* @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
* @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
* @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
* require a secure protocol (like HTTPS).
* @type undefined
*
* @name $.cookie
* @cat Plugins/Cookie
* @author Klaus Hartl/klaus.hartl@stilbuero.de
*/
function cookie(name, value, options) {
if (typeof value != 'undefined') { // name and value given, set cookie
@ -55,7 +85,7 @@ function init_sort_combobox() {
selectedList: 1,
click: function(event, ui){
$(this).multiselect("close");
cookie(sort_cookie_name, ui.value, {expires: 365});
cookie(sort_cookie_name, ui.value);
window.location.reload();
}
});
@ -74,13 +104,25 @@ function init() {
}
// Top-level feed {{{
function toplevel_layout() {
var last = $(".toplevel li").last();
var title = $('.toplevel h3').first();
var bottom = last.position().top + last.height() - title.position().top;
$("#main").height(Math.max(200, bottom));
}
function toplevel() {
$(".sort_select").hide();
$(".toplevel li").click(function() {
var href = $(this).children("span").html();
var href = $(this).children("span.url").text();
window.location = href;
});
toplevel_layout();
$(window).resize(toplevel_layout);
}
// }}}
@ -89,11 +131,14 @@ function render_error(msg) {
}
// Category feed {{{
function category_clicked() {
var href = $(this).find("span.href").html();
window.location = href;
}
function category() {
$(".category .category-item").click(function() {
var href = $(this).find("span.href").html();
window.location = href;
});
$(".category .category-item").click(category_clicked);
$(".category a.navlink").button();
@ -111,10 +156,12 @@ function category() {
if (href) {
$.ajax({
url:href,
data:{'sort':cookie(sort_cookie_name)},
success: function(data) {
this.children(".loaded").html(data);
this.children(".loaded").show();
this.children(".loading").hide();
this.find('.category-item').click(category_clicked);
},
context: ui.newContent,
dataType: "json",
@ -132,4 +179,111 @@ function category() {
}
// }}}
// Booklist {{{
function first_page() {
load_page($("#booklist #page0"));
}
function last_page() {
load_page($("#booklist .page").last());
}
function next_page() {
var elem = $("#booklist .page:visible").next('.page');
if (elem.length > 0) load_page(elem);
else first_page();
}
function previous_page() {
var elem = $("#booklist .page:visible").prev('.page');
if (elem.length > 0) load_page(elem);
else last_page();
}
function load_page(elem) {
if (elem.is(":visible")) return;
var ld = elem.find('.load_data');
var ids = ld.attr('title');
var href = ld.find(".url").attr('title');
elem.children(".loaded").hide();
$.ajax({
url: href,
context: elem,
dataType: "json",
type: 'POST',
timeout: 600000, //milliseconds (10 minutes)
data: {'ids': ids},
error: function(xhr, stat, err) {
this.children(".loaded").html(render_error(stat));
this.children(".loaded").show();
this.children(".loading").hide();
},
success: function(data) {
this.children(".loaded").html(data);
this.find(".left a.read").button();
this.children(".loading").hide();
this.parent().find('.navmiddle .start').html(this.find('.load_data .start').attr('title'));
this.parent().find('.navmiddle .end').html(this.find('.load_data .end').attr('title'));
this.children(".loaded").fadeIn(1000);
}
});
$("#booklist .page:visible").hide();
elem.children(".loaded").hide();
elem.children(".loading").show();
elem.show();
}
function hidesort() {$("#content > .sort_select").hide();}
function booklist(hide_sort) {
if (hide_sort) hidesort();
var test = $("#booklist #page0").html();
if (!test) {
$("#booklist").html(render_error("No books found"));
return;
}
$("#book_details_dialog").dialog({
autoOpen: false,
modal: true,
show: 'slide'
});
first_page();
}
function show_details(a_dom) {
var book = $(a_dom).closest('div.summary');
var bd = $('#book_details_dialog');
bd.html('<span class="loading"><img src="/static/loading.gif" alt="Loading" />Loading, please wait&hellip;</span>');
bd.dialog('option', 'width', $(window).width() - 100);
bd.dialog('option', 'height', $(window).height() - 100);
bd.dialog('option', 'title', book.find('.title').text());
$.ajax({
url: book.find('.details-href').attr('title'),
context: bd,
dataType: "json",
timeout: 600000, //milliseconds (10 minutes)
error: function(xhr, stat, err) {
this.html(render_error(stat));
},
success: function(data) {
this.html(data);
}
});
bd.dialog('open');
}
// }}}
function book() {
hidesort();
$('.details .left img').load(function() {
var img = $('.details .left img');
var height = $('#main').height();
height = Math.max(height, img.height() + 100);
$('#main').height(height);
});
}

View File

@ -0,0 +1,10 @@
<div id="details_{id}" class="details">
<div class="left">
<img alt="Cover of {title}" src="/get/cover/{id}" />
</div>
<div class="right">
<div class="field formats">{formats}</div>
{fields}
{comments}
</div>
</div>

View File

@ -0,0 +1,20 @@
<div id="summary_{id}" class="summary">
<div class="left">
<img alt="Cover of {title}" src="/get/thumb_90_120/{id}" />
{get_button}
</div>
<div class="right">
<div class="stars">
<span class="rating_container">{stars}</span>
<span class="series">{series}</span>
<a href="#" onclick="show_details(this); return false;" title="{details_tt}">{details}</a>
<a href="/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
</div>
<div class="title"><strong>{title}</strong></div>
<div class="authors">{authors}</div>
<div class="comments">{comments}</div>
<div class="tags">{tags}</div>
<div class="formats">{other_formats}</div>
</div>
<div class="details-href" title="{details_href}" style="display:none"></div>
</div>

View File

@ -15,7 +15,7 @@
</div>
<div id="search_box">
<form name="search_form" onsubmit="search();return false;" action="./" method="get">
<form name="search_form" onsubmit="search();return false;" action="./" method="get" accept-charset="UTF-8">
<input value="" id="s" type="text" />
<input type="image" src="/static/btn_search_box.png" width="27" height="24" id="go" alt="Search" title="Search" />
</form>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 B

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -50,13 +50,13 @@
*
* http://docs.jquery.com/UI/Theming/API
*
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1em&cornerRadius=6px&bgColorHeader=cb842e&bgTextureHeader=02_glass.png&bgImgOpacityHeader=25&borderColorHeader=d49768&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=f4f0ec&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=100&borderColorContent=e0cfc2&fcContent=1e1b1d&iconColorContent=c47a23&bgColorDefault=ede4d4&bgTextureDefault=02_glass.png&bgImgOpacityDefault=70&borderColorDefault=cdc3b7&fcDefault=3f3731&iconColorDefault=f08000&bgColorHover=f5f0e5&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=f5ad66&fcHover=a46313&iconColorHover=f08000&bgColorActive=f4f0ec&bgTextureActive=04_highlight_hard.png&bgImgOpacityActive=100&borderColorActive=e0cfc2&fcActive=b85700&iconColorActive=f35f07&bgColorHighlight=f5f5b5&bgTextureHighlight=04_highlight_hard.png&bgImgOpacityHighlight=75&borderColorHighlight=d9bb73&fcHighlight=060200&iconColorHighlight=cb672b&bgColorError=fee4bd&bgTextureError=04_highlight_hard.png&bgImgOpacityError=65&borderColorError=f8893f&fcError=592003&iconColorError=ff7519&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=75&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=75&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* To view and modify this theme, visit http://jqueryui.com/themeroller/?tr=ffDefault=Helvetica,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=cb842e&bgTextureHeader=02_glass.png&bgImgOpacityHeader=25&borderColorHeader=d49768&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=f4f0ec&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=100&borderColorContent=e0cfc2&fcContent=1e1b1d&iconColorContent=c47a23&bgColorDefault=ede4d4&bgTextureDefault=02_glass.png&bgImgOpacityDefault=70&borderColorDefault=cdc3b7&fcDefault=3f3731&iconColorDefault=f08000&bgColorHover=f5f0e5&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=f5ad66&fcHover=a46313&iconColorHover=f08000&bgColorActive=f4f0ec&bgTextureActive=04_highlight_hard.png&bgImgOpacityActive=100&borderColorActive=e0cfc2&fcActive=b85700&iconColorActive=f35f07&bgColorHighlight=f5f5b5&bgTextureHighlight=04_highlight_hard.png&bgImgOpacityHighlight=75&borderColorHighlight=d9bb73&fcHighlight=060200&iconColorHighlight=cb672b&bgColorError=fee4bd&bgTextureError=04_highlight_hard.png&bgImgOpacityError=65&borderColorError=f8893f&fcError=592003&iconColorError=ff7519&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=75&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=75&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
*/
/* Component containers
----------------------------------*/
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
.ui-widget .ui-widget { font-size: 1em; }
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
.ui-widget-content { border: 1px solid #e0cfc2; background: #f4f0ec url(images/ui-bg_inset-soft_100_f4f0ec_1x100.png) 50% bottom repeat-x; color: #1e1b1d; }
@ -293,6 +293,25 @@
/* Overlays */
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_75_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_75_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*
* jQuery UI Resizable @VERSION
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Resizable#theming
*/
.ui-resizable { position: relative;}
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*
* jQuery UI Accordion @VERSION
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
@ -348,3 +367,24 @@ input.ui-button { padding: .4em 1em; }
/* workarounds */
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
/*
* jQuery UI Dialog @VERSION
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Dialog#theming
*/
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
.ui-dialog .ui-dialog-titlebar { padding: .5em 1em .3em; position: relative; }
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; }
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
.ui-draggable .ui-dialog-titlebar { cursor: move; }

View File

@ -63,6 +63,53 @@ a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_
b.left=d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];
b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=
c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery);
;/*
* jQuery UI Resizable 1.8.5
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Resizables
*
* Depends:
* jquery.ui.core.js
* jquery.ui.mouse.js
* jquery.ui.widget.js
*/
(function(e){e.widget("ui.resizable",e.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:false,animate:false,animateDuration:"slow",animateEasing:"swing",aspectRatio:false,autoHide:false,containment:false,ghost:false,grid:false,handles:"e,s,se",helper:false,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1E3},_create:function(){var b=this,a=this.options;this.element.addClass("ui-resizable");e.extend(this,{_aspectRatio:!!a.aspectRatio,aspectRatio:a.aspectRatio,originalElement:this.element,
_proportionallyResizeElements:[],_helper:a.helper||a.ghost||a.animate?a.helper||"ui-resizable-helper":null});if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)){/relative/.test(this.element.css("position"))&&e.browser.opera&&this.element.css({position:"relative",top:"auto",left:"auto"});this.element.wrap(e('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),
top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle=
this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",
nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d<c.length;d++){var f=e.trim(c[d]),g=e('<div class="ui-resizable-handle '+("ui-resizable-"+f)+'"></div>');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor==
String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),k=0;k=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,k);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection();
this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){e(this).removeClass("ui-resizable-autohide");b._handles.show()},function(){if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};
if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a=false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),
d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"});this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=
this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:
this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis];if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",
b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false},_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;
f={width:c.size.width-(f?0:c.sizeDiff.width),height:c.size.height-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f,{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",
b);this._helper&&this.helper.remove();return false},_updateCache:function(b){this.offset=this.helper.offset();if(l(b.left))this.position.left=b.left;if(l(b.top))this.position.top=b.top;if(l(b.height))this.size.height=b.height;if(l(b.width))this.size.width=b.width},_updateRatio:function(b){var a=this.position,c=this.size,d=this.axis;if(b.height)b.width=c.height*this.aspectRatio;else if(b.width)b.height=c.width/this.aspectRatio;if(d=="sw"){b.left=a.left+(c.width-b.width);b.top=null}if(d=="nw"){b.top=
a.top+(c.height-b.height);b.left=a.left+(c.width-b.width)}return b},_respectSize:function(b){var a=this.options,c=this.axis,d=l(b.width)&&a.maxWidth&&a.maxWidth<b.width,f=l(b.height)&&a.maxHeight&&a.maxHeight<b.height,g=l(b.width)&&a.minWidth&&a.minWidth>b.width,h=l(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,
k=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&k)b.left=i-a.minWidth;if(d&&k)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left=null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a<this._proportionallyResizeElements.length;a++){var c=this._proportionallyResizeElements[a];if(!this.borderDif){var d=[c.css("borderTopWidth"),
c.css("borderRightWidth"),c.css("borderBottomWidth"),c.css("borderLeftWidth")],f=[c.css("paddingTop"),c.css("paddingRight"),c.css("paddingBottom"),c.css("paddingLeft")];this.borderDif=e.map(d,function(g,h){g=parseInt(g,10)||0;h=parseInt(f[h],10)||0;return g+h})}e.browser.msie&&(e(b).is(":hidden")||e(b).parents(":hidden").length)||c.css({height:b.height()-this.borderDif[0]-this.borderDif[2]||0,width:b.width()-this.borderDif[1]-this.borderDif[3]||0})}},_renderProxy:function(){var b=this.options;this.elementOffset=
this.element.offset();if(this._helper){this.helper=this.helper||e('<div style="overflow:hidden;"></div>');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+
a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,
arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]);b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,
{version:"1.8.5"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(),10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,
function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top-f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var k=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:k.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=
(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(k.css("position"))){c._revertToRelativePosition=true;k.css({position:"absolute",top:"auto",left:"auto"})}k.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType?e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=
false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a=e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-
a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing,step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",
b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement=e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top",
"Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset;var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,
f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left:a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=
a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top-d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+
a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition,f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&
e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25,display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",
height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b=e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=
d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},l=function(b){return!isNaN(parseInt(b,10))}})(jQuery);
;/*
* jQuery UI Accordion 1.8.5
*
@ -118,6 +165,45 @@ true):a(this).button("widget").removeClass("ui-state-active").attr("aria-pressed
c=a("<span></span>").addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary;if(d.primary||d.secondary){b.addClass("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary"));d.primary&&b.prepend("<span class='ui-button-icon-primary ui-icon "+d.primary+"'></span>");d.secondary&&b.append("<span class='ui-button-icon-secondary ui-icon "+d.secondary+"'></span>");if(!this.options.text){b.addClass(e?"ui-button-icons-only":"ui-button-icon-only").removeClass("ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary");
this.hasTitle||b.attr("title",c)}}else b.addClass("ui-button-text-only")}}});a.widget("ui.buttonset",{_create:function(){this.element.addClass("ui-buttonset");this._init()},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c);a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){this.buttons=this.element.find(":button, :submit, :reset, :checkbox, :radio, a, :data(button)").filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":visible").filter(":first").addClass("ui-corner-left").end().filter(":last").addClass("ui-corner-right").end().end().end()},
destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy");a.Widget.prototype.destroy.call(this)}})})(jQuery);
;/*
* jQuery UI Dialog 1.8.5
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Dialog
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
* jquery.ui.button.js
* jquery.ui.draggable.js
* jquery.ui.mouse.js
* jquery.ui.position.js
* jquery.ui.resizable.js
*/
(function(c,j){c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:{my:"center",at:"center",of:window,collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");
if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||"&#160;",f=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("<div></div>")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog",
"aria-labelledby":f}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var e=(a.uiDialogTitlebar=c("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),h=c('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);
return false}).appendTo(e);(a.uiDialogTitlebarCloseText=c("<span></span>")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("<span></span>").addClass("ui-dialog-title").attr("id",f).html(d).prependTo(e);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;e.find("*").add(e).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&
g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");
b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==b.uiDialog[0])d=Math.max(d,c(this).css("z-index"))});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,f=d.options;if(f.modal&&!a||!f.stack&&!f.modal)return d._trigger("focus",b);if(f.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=
f.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.attr("scrollTop"),scrollLeft:d.element.attr("scrollLeft")};c.ui.dialog.maxZ+=1;d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;d.next().length&&d.appendTo("body");a._size();a._position(b.position);d.show(b.show);
a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(f){if(f.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),e=g.filter(":first");g=g.filter(":last");if(f.target===g[0]&&!f.shiftKey){e.focus(1);return false}else if(f.target===e[0]&&f.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,
f=c("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("<div></div>").addClass("ui-dialog-buttonset").appendTo(f);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,function(){return!(d=true)});if(d){c.each(a,function(e,h){h=c.isFunction(h)?{click:h,text:e}:h;e=c("<button></button>",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position,
offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);
b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(),
handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,
a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a);
f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b?
d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&&
d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||"&#160;"));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight-
b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),
function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()<c.ui.dialog.overlay.maxZ)return false})},1);c(document).bind("keydown.dialog-overlay",function(d){if(a.options.closeOnEscape&&d.keyCode&&d.keyCode===c.ui.keyCode.ESCAPE){a.close(d);d.preventDefault()}});c(window).bind("resize.dialog-overlay",c.ui.dialog.overlay.resize)}var b=
(this.oldInstances.pop()||c("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a,
b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a<b?c(window).height()+"px":a+"px"}else return c(document).height()+"px"},width:function(){var a,b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth);b=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth);return a<
b?c(window).width()+"px":a+"px"}else return c(document).width()+"px"},resize:function(){var a=c([]);c.each(c.ui.dialog.overlay.instances,function(){a=a.add(this)});a.css({width:0,height:0}).css({width:c.ui.dialog.overlay.width(),height:c.ui.dialog.overlay.height()})}});c.extend(c.ui.dialog.overlay.prototype,{destroy:function(){c.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);
;/*
* jQuery UI Effects 1.8.5
*

View File

@ -25,6 +25,9 @@ series_index_auto_increment = 'next'
# copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
# nocomma : "fn ln" -> "ln fn" (without the comma)
# When this tweak is changed, the author_sort values stored with each author
# must be recomputed by right-clicking on an author in the left-hand tags pane,
# selecting 'manage authors', and pressing 'Recalculate all author sort values'.
author_sort_copy_method = 'invert'

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Tony Stegall'
__copyright__ = '2010, Tony Stegall or Tonythebookworm on mobiread.com'
__version__ = '1'
__date__ = '16, October 2010'
__docformat__ = 'English'
from calibre.web.feeds.news import BasicNewsRecipe
class MalaysianMirror(BasicNewsRecipe):
title = 'MalaysianMirror'
__author__ = 'Tonythebookworm'
description = 'The Pulse of the Nation'
language = 'en'
no_stylesheets = True
publisher = 'Tonythebookworm'
category = 'news'
use_embedded_content= False
no_stylesheets = True
oldest_article = 24
remove_javascript = True
remove_empty_feeds = True
conversion_options = {'linearize_tables' : True}
extra_css = '''
#content_heading{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
td{text-align:right; font-size:small;margin-top:0px;margin-bottom: 0px;}
#content_body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [dict(name='table', attrs={'class':['contentpaneopen']})
]
remove_tags = [dict(name='table', attrs={'class':['buttonheading']})]
#######################################################################################################################
max_articles_per_feed = 10
'''
Make a variable that will hold the url for the main site because our links do not include the index
'''
INDEX = 'http://www.malaysianmirror.com'
def parse_index(self):
feeds = []
for title, url in [
(u"Media Buzz", u"http://www.malaysianmirror.com/media-buzz-front"),
(u"Life Style", u"http://www.malaysianmirror.com/lifestylefront"),
(u"Features", u"http://www.malaysianmirror.com/featurefront"),
]:
articles = self.make_links(url)
if articles:
feeds.append((title, articles))
return feeds
def make_links(self, url):
title = 'Temp'
current_articles = []
soup = self.index_to_soup(url)
# print 'The soup is: ', soup
for item in soup.findAll('div', attrs={'class':'contentheading'}):
#print 'item is: ', item
link = item.find('a')
#print 'the link is: ', link
if link:
url = self.INDEX + link['href']
title = self.tag_to_string(link)
#print 'the title is: ', title
#print 'the url is: ', url
#print 'the title is: ', title
current_articles.append({'title': title, 'url': url, 'description':'', 'date':''}) # append all this
return current_articles
def preprocess_html(self, soup):
for item in soup.findAll(attrs={'style':True}):
del item['style']
return soup

View File

@ -38,13 +38,19 @@ class Push(Command):
description = 'Push code to another host'
def run(self, opts):
from threading import Thread
threads = []
for host in (
r'Owner@winxp:/cygdrive/c/Documents\ and\ Settings/Owner/calibre',
'kovid@ox:calibre'
):
rcmd = BASE_RSYNC + EXCLUDES + ['.', host]
print '\n\nPushing to:', host, '\n'
threads.append(Thread(target=subprocess.check_call, args=(rcmd,)))
threads[-1].start()
subprocess.check_call(rcmd)
for thread in threads:
thread.join()

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.23'
__version__ = '0.7.24'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -293,6 +293,17 @@ class RTFMetadataReader(MetadataReaderPlugin):
from calibre.ebooks.metadata.rtf import get_metadata
return get_metadata(stream)
class SNBMetadataReader(MetadataReaderPlugin):
name = 'Read SNB metadata'
file_types = set(['snb'])
description = _('Read metadata from %s files') % 'SNB'
author = 'Li Fanxi'
def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.snb import get_metadata
return get_metadata(stream)
class TOPAZMetadataReader(MetadataReaderPlugin):
name = 'Read Topaz metadata'
@ -420,6 +431,7 @@ from calibre.ebooks.tcr.input import TCRInput
from calibre.ebooks.txt.input import TXTInput
from calibre.ebooks.lrf.input import LRFInput
from calibre.ebooks.chm.input import CHMInput
from calibre.ebooks.snb.input import SNBInput
from calibre.ebooks.epub.output import EPUBOutput
from calibre.ebooks.fb2.output import FB2Output
@ -435,6 +447,7 @@ from calibre.ebooks.rtf.output import RTFOutput
from calibre.ebooks.tcr.output import TCROutput
from calibre.ebooks.txt.output import TXTOutput
from calibre.ebooks.html.output import HTMLOutput
from calibre.ebooks.snb.output import SNBOutput
from calibre.customize.profiles import input_profiles, output_profiles
@ -496,6 +509,7 @@ plugins += [
TXTInput,
LRFInput,
CHMInput,
SNBInput,
]
plugins += [
EPUBOutput,
@ -512,6 +526,7 @@ plugins += [
TCROutput,
TXTOutput,
HTMLOutput,
SNBOutput,
]
# Order here matters. The first matched device is the one used.
plugins += [

View File

@ -120,6 +120,11 @@ class InputFormatPlugin(Plugin):
#: to make its output suitable for viewing
for_viewer = False
#: The encoding that this input plugin creates files in. A value of
#: None means that the encoding is undefined and must be
#: detected individually
output_encoding = 'utf-8'
#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.

View File

@ -647,11 +647,25 @@ class NookOutput(OutputProfile):
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class BambookOutput(OutputProfile):
name = 'Sanda Bambook'
short_name = 'bambook'
description = _('This profile is intended for the Sanda Bambook.')
# Screen size is a best guess
screen_size = (800, 600)
comic_screen_size = (700, 540)
dpi = 168.451
fbase = 12
fsizes = [10, 12, 14, 16]
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
iPadOutput, KoboReaderOutput,
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,]
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
BambookOutput, ]
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))

View File

@ -36,8 +36,8 @@ class KOBO(USBMS):
PRODUCT_ID = [0x4161]
BCD = [0x0110]
VENDOR_NAME = 'KOBO_INC'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '.KOBOEREADER'
VENDOR_NAME = ['KOBO_INC', 'KOBO']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER']
EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True

View File

@ -0,0 +1,88 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import dbus
import os
def node_mountpoint(node):
def de_mangle(raw):
return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012',
'\n').replace('\\0134', '\\')
for line in open('/proc/mounts').readlines():
line = line.split()
if line[0] == node:
return de_mangle(line[1])
return None
class UDisks(object):
def __init__(self):
if os.environ.get('CALIBRE_DISABLE_UDISKS', False):
raise Exception('User has aborted use of UDISKS')
self.bus = dbus.SystemBus()
self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks',
'/org/freedesktop/UDisks'), 'org.freedesktop.UDisks')
def device(self, device_node_path):
devpath = self.main.FindDeviceByDeviceFile(device_node_path)
return dbus.Interface(self.bus.get_object('org.freedesktop.UDisks',
devpath), 'org.freedesktop.UDisks.Device')
def mount(self, device_node_path):
d = self.device(device_node_path)
try:
return unicode(d.FilesystemMount('',
['auth_no_user_interaction', 'rw', 'noexec', 'nosuid',
'sync', 'nodev', 'uid=1000', 'gid=1000']))
except:
# May be already mounted, check
mp = node_mountpoint(str(device_node_path))
if mp is None:
raise
return mp
def unmount(self, device_node_path):
d = self.device(device_node_path)
d.FilesystemUnmount(['force'])
def eject(self, device_node_path):
parent = device_node_path
while parent[-1] in '0123456789':
parent = parent[:-1]
devices = [str(x) for x in self.main.EnumerateDeviceFiles()]
for d in devices:
if d.startswith(parent) and d != parent:
try:
self.unmount(d)
except:
import traceback
print 'Failed to unmount:', d
traceback.print_exc()
d = self.device(parent)
d.DriveEject([])
def mount(node_path):
u = UDisks()
u.mount(node_path)
def eject(node_path):
u = UDisks()
u.eject(node_path)
if __name__ == '__main__':
import sys
dev = sys.argv[1]
print 'Testing with node', dev
u = UDisks()
print 'Mounted at:', u.mount(dev)
print 'Ejecting'
u.eject(dev)

View File

@ -530,16 +530,8 @@ class Device(DeviceConfig, DevicePlugin):
return drives
def node_mountpoint(self, node):
def de_mangle(raw):
return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012',
'\n').replace('\\0134', '\\')
for line in open('/proc/mounts').readlines():
line = line.split()
if line[0] == node:
return de_mangle(line[1])
return None
from calibre.devices.udisks import node_mountpoint
return node_mountpoint(node)
def find_largest_partition(self, path):
node = path.split('/')[-1]
@ -585,6 +577,13 @@ class Device(DeviceConfig, DevicePlugin):
label += ' (%d)'%extra
def do_mount(node, label):
try:
from calibre.devices.udisks import mount
mount(node)
return 0
except:
pass
cmd = 'calibre-mount-helper'
if getattr(sys, 'frozen_path', False):
cmd = os.path.join(sys.frozen_path, cmd)
@ -617,6 +616,7 @@ class Device(DeviceConfig, DevicePlugin):
if not mp.endswith('/'): mp += '/'
self._linux_mount_map[main] = mp
self._main_prefix = mp
self._linux_main_device_node = main
cards = [(carda, '_card_a_prefix', 'carda'),
(cardb, '_card_b_prefix', 'cardb')]
for card, prefix, typ in cards:
@ -732,6 +732,11 @@ class Device(DeviceConfig, DevicePlugin):
pass
def eject_linux(self):
try:
from calibre.devices.udisks import eject
return eject(self._linux_main_device_node)
except:
pass
drives = self.find_device_nodes()
for drive in drives:
if drive:

View File

@ -25,7 +25,7 @@ class DRMError(ValueError):
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan']
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan', 'snb']
class HTMLRenderer(object):

View File

@ -93,6 +93,7 @@ class CHMReader(CHMFile):
return data
def ExtractFiles(self, output_dir=os.getcwdu()):
html_files = set([])
for path in self.Contents():
lpath = os.path.join(output_dir, path)
self._ensure_dir(lpath)
@ -106,14 +107,27 @@ class CHMReader(CHMFile):
lpath = lpath.split(';')[0]
try:
with open(lpath, 'wb') as f:
if guess_mimetype(path)[0] == ('text/html'):
data = self._reformat(data)
f.write(data)
try:
if 'html' in guess_mimetype(path)[0]:
html_files.add(lpath)
except:
pass
except:
if iswindows and len(lpath) > 250:
self.log.warn('%r filename too long, skipping'%path)
continue
raise
for lpath in html_files:
with open(lpath, 'r+b') as f:
data = f.read()
data = self._reformat(data, lpath)
if isinstance(data, unicode):
data = data.encode('utf-8')
f.seek(0)
f.truncate()
f.write(data)
self._extracted = True
files = [x for x in os.listdir(output_dir) if
os.path.isfile(os.path.join(output_dir, x))]
@ -125,7 +139,7 @@ class CHMReader(CHMFile):
if self.hhc_path not in files and files:
self.hhc_path = files[0]
def _reformat(self, data):
def _reformat(self, data, htmlpath):
try:
data = xml_to_unicode(data, strip_encoding_pats=True)[0]
soup = BeautifulSoup(data)
@ -169,15 +183,19 @@ class CHMReader(CHMFile):
br[0].extract()
# some images seem to be broken in some chm's :/
for img in soup('img'):
try:
# some are supposedly "relative"... lies.
while img['src'].startswith('../'): img['src'] = img['src'][3:]
# some have ";<junk>" at the end.
img['src'] = img['src'].split(';')[0]
except KeyError:
# and some don't even have a src= ?!
pass
base = os.path.dirname(htmlpath)
for img in soup('img', src=True):
src = img['src']
ipath = os.path.join(base, *src.split('/'))
if os.path.exists(ipath):
continue
src = src.split(';')[0]
if not src: continue
ipath = os.path.join(base, *src.split('/'))
if not os.path.exists(ipath):
while src.startswith('../'):
src = src[3:]
img['src'] = src
try:
# if there is only a single table with a single element
# in the body, replace it by the contents of this single element

View File

@ -838,7 +838,8 @@ OptionRecommendation(name='timestamp',
self.opts_to_mi(self.user_metadata)
if not hasattr(self.oeb, 'manifest'):
self.oeb = create_oebbook(self.log, self.oeb, self.opts,
self.input_plugin)
self.input_plugin,
encoding=self.input_plugin.output_encoding)
self.input_plugin.postprocess_book(self.oeb, self.opts, self.log)
self.opts.is_image_collection = self.input_plugin.is_image_collection
pr = CompositeProgressReporter(0.34, 0.67, self.ui_reporter)

View File

@ -543,6 +543,13 @@ class HTMLPreProcessor(object):
def smarten_punctuation(self, html):
from calibre.utils.smartypants import smartyPants
from calibre.ebooks.chardet import substitute_entites
from uuid import uuid4
start = 'calibre-smartypants-'+str(uuid4())
stop = 'calibre-smartypants-'+str(uuid4())
html = html.replace('<!--', start)
html = html.replace('-->', stop)
html = smartyPants(html)
html = html.replace(start, '<!--')
html = html.replace(stop, '-->')
return substitute_entites(html)

View File

@ -16,6 +16,7 @@ class EPUBInput(InputFormatPlugin):
author = 'Kovid Goyal'
description = 'Convert EPUB files (.epub) to HTML'
file_types = set(['epub'])
output_encoding = None
recommendations = set([('page_breaks_before', '/', OptionRecommendation.MED)])

View File

@ -15,7 +15,7 @@ _METADATA_PRIORITIES = [
'html', 'htm', 'xhtml', 'xhtm',
'rtf', 'fb2', 'pdf', 'prc', 'odt',
'epub', 'lit', 'lrx', 'lrf', 'mobi',
'rb', 'imp', 'azw'
'rb', 'imp', 'azw', 'snb'
]
# The priorities for loading metadata from different file types

View File

@ -0,0 +1,47 @@
'''Read meta information from SNB files'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
import os
from StringIO import StringIO
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.snb.snbfile import SNBFile
from lxml import etree
def get_metadata(stream, extract_cover=True):
""" Return metadata as a L{MetaInfo} object """
mi = MetaInformation(_('Unknown'), [_('Unknown')])
snbFile = SNBFile()
try:
if not hasattr(stream, 'write'):
snbFile.Parse(StringIO(stream), True)
else:
stream.seek(0)
snbFile.Parse(stream, True)
meta = snbFile.GetFileStream('snbf/book.snbf')
if meta != None:
meta = etree.fromstring(meta)
mi.title = meta.find('.//head/name').text
mi.authors = [meta.find('.//head/author').text]
mi.language = meta.find('.//head/language').text.lower().replace('_', '-')
mi.publisher = meta.find('.//head/publisher').text
if extract_cover:
cover = meta.find('.//head/cover')
if cover != None and cover.text != None:
root, ext = os.path.splitext(cover.text)
if ext == '.jpeg':
ext = '.jpg'
mi.cover_data = (ext[-3:], snbFile.GetFileStream('snbc/images/' + cover.text))
except Exception:
import traceback
traceback.print_exc()
return mi

View File

@ -2,7 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
import os, glob, re
import os, glob, re, functools
from urlparse import urlparse
from urllib import unquote
from uuid import uuid4
@ -11,7 +11,7 @@ from lxml import etree
from lxml.builder import ElementMaker
from calibre.constants import __appname__, __version__
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.chardet import xml_to_unicode
NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
@ -26,14 +26,6 @@ E = ElementMaker(namespace=NCX_NS, nsmap=NSMAP)
C = ElementMaker(namespace=CALIBRE_NS, nsmap=NSMAP)
class NCXSoup(BeautifulStoneSoup):
NESTABLE_TAGS = {'navpoint':[]}
def __init__(self, raw):
BeautifulStoneSoup.__init__(self, raw,
convertEntities=BeautifulSoup.HTML_ENTITIES,
selfClosingTags=['meta', 'content'])
class TOC(list):
@ -166,40 +158,60 @@ class TOC(list):
def read_ncx_toc(self, toc):
self.base_path = os.path.dirname(toc)
raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True)[0]
soup = NCXSoup(raw)
raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True,
strip_encoding_pats=True)[0]
root = etree.fromstring(raw, parser=etree.XMLParser(recover=True,
no_network=True))
xpn = {'re': 'http://exslt.org/regular-expressions'}
XPath = functools.partial(etree.XPath, namespaces=xpn)
def get_attr(node, default=None, attr='playorder'):
for name, val in node.attrib.items():
if name and val and name.lower().endswith(attr):
return val
return default
nl_path = XPath('./*[re:match(local-name(), "navlabel$", "i")]')
txt_path = XPath('./*[re:match(local-name(), "text$", "i")]')
content_path = XPath('./*[re:match(local-name(), "content$", "i")]')
np_path = XPath('./*[re:match(local-name(), "navpoint$", "i")]')
def process_navpoint(np, dest):
play_order = np.get('playOrder', None)
if play_order is None:
play_order = int(np.get('playorder', 1))
try:
play_order = int(get_attr(np, 1))
except:
play_order = 1
href = fragment = text = None
nl = np.find(re.compile('navlabel'))
if nl is not None:
nl = nl_path(np)
if nl:
nl = nl[0]
text = u''
for txt in nl.findAll(re.compile('text')):
text += u''.join([unicode(s) for s in txt.findAll(text=True)])
content = np.find(re.compile('content'))
if content is None or not content.has_key('src') or not txt:
for txt in txt_path(nl):
text += etree.tostring(txt, method='text',
encoding=unicode, with_tail=False)
content = content_path(np)
if not content or not text:
return
content = content[0]
src = get_attr(content, attr='src')
if src is None:
return
purl = urlparse(unquote(content['src']))
purl = urlparse(unquote(content.get('src')))
href, fragment = purl[2], purl[5]
nd = dest.add_item(href, fragment, text)
nd.play_order = play_order
for c in np:
if 'navpoint' in getattr(c, 'name', ''):
process_navpoint(c, nd)
for c in np_path(np):
process_navpoint(c, nd)
nm = soup.find(re.compile('navmap'))
if nm is None:
nm = XPath('//*[re:match(local-name(), "navmap$", "i")]')(root)
if not nm:
raise ValueError('NCX files must have a <navmap> element.')
nm = nm[0]
for elem in nm:
if 'navpoint' in getattr(elem, 'name', ''):
process_navpoint(elem, self)
for child in np_path(nm):
process_navpoint(child, self)
def read_html_toc(self, toc):
self.base_path = os.path.dirname(toc)

View File

@ -282,9 +282,9 @@ def XPath(expr):
def xpath(elem, expr):
return elem.xpath(expr, namespaces=XPNSMAP)
def xml2str(root, pretty_print=False, strip_comments=False):
def xml2str(root, pretty_print=False, strip_comments=False, with_tail=True):
ans = etree.tostring(root, encoding='utf-8', xml_declaration=True,
pretty_print=pretty_print)
pretty_print=pretty_print, with_tail=with_tail)
if strip_comments:
ans = re.compile(r'<!--.*?-->', re.DOTALL).sub('', ans)
@ -1908,6 +1908,7 @@ class OEBBook(object):
def _to_ncx(self):
lang = unicode(self.metadata.language[0])
lang = lang.replace('_', '-')
ncx = etree.Element(NCX('ncx'),
attrib={'version': '2005-1', XML('lang'): lang},
nsmap={None: NCX_NS})

View File

@ -55,7 +55,7 @@ class SVGRasterizer(object):
self.rasterize_cover()
def rasterize_svg(self, elem, width=0, height=0, format='PNG'):
data = QByteArray(xml2str(elem))
data = QByteArray(xml2str(elem, with_tail=False))
svg = QSvgRenderer(data)
size = svg.defaultSize()
view_box = elem.get('viewBox', elem.get('viewbox', None))

View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
'''
Used for snb output
'''

103
src/calibre/ebooks/snb/input.py Executable file
View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import os, uuid
from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.oeb.base import DirContainer
from calibre.ebooks.snb.snbfile import SNBFile
from calibre.ptempfile import TemporaryDirectory
from calibre.utils.filenames import ascii_filename
from lxml import etree
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
def html_encode(s):
return s.replace(u'&', u'&amp;').replace(u'<', u'&lt;').replace(u'>', u'&gt;').replace(u'"', u'&quot;').replace(u"'", u'&apos;').replace(u'\n', u'<br/>').replace(u' ', u'&nbsp;')
class SNBInput(InputFormatPlugin):
name = 'SNB Input'
author = 'Li Fanxi'
description = 'Convert SNB files to OEB'
file_types = set(['snb'])
options = set([
])
def convert(self, stream, options, file_ext, log,
accelerators):
log.debug("Parsing SNB file...")
snbFile = SNBFile()
try:
snbFile.Parse(stream)
except:
raise ValueError("Invalid SNB file")
if not snbFile.IsValid():
log.debug("Invaild SNB file")
raise ValueError("Invalid SNB file")
log.debug("Handle meta data ...")
from calibre.ebooks.conversion.plumber import create_oebbook
oeb = create_oebbook(log, None, options, self,
encoding=options.input_encoding, populate=False)
meta = snbFile.GetFileStream('snbf/book.snbf')
if meta != None:
meta = etree.fromstring(meta)
oeb.metadata.add('title', meta.find('.//head/name').text)
oeb.metadata.add('creator', meta.find('.//head/author').text, attrib={'role':'aut'})
oeb.metadata.add('language', meta.find('.//head/language').text.lower().replace('_', '-'))
oeb.metadata.add('creator', meta.find('.//head/generator').text)
oeb.metadata.add('publisher', meta.find('.//head/publisher').text)
cover = meta.find('.//head/cover')
if cover != None and cover.text != None:
oeb.guide.add('cover', 'Cover', cover.text)
bookid = str(uuid.uuid4())
oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')
for ident in oeb.metadata.identifier:
if 'id' in ident.attrib:
oeb.uid = oeb.metadata.identifier[0]
break
with TemporaryDirectory('_chm2oeb', keep=True) as tdir:
log.debug('Process TOC ...')
toc = snbFile.GetFileStream('snbf/toc.snbf')
oeb.container = DirContainer(tdir, log)
if toc != None:
toc = etree.fromstring(toc)
i = 1
for ch in toc.find('.//body'):
chapterName = ch.text
chapterSrc = ch.get('src')
fname = 'ch_%d.htm' % i
data = snbFile.GetFileStream('snbc/' + chapterSrc)
if data != None:
snbc = etree.fromstring(data)
outputFile = open(os.path.join(tdir, fname), 'wb')
lines = []
for line in snbc.find('.//body'):
if line.tag == 'text':
lines.append(u'<p>%s</p>' % html_encode(line.text))
elif line.tag == 'img':
lines.append(u'<p><img src="%s" /></p>' % html_encode(line.text))
outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace'))
outputFile.close()
oeb.toc.add(ch.text, fname)
id, href = oeb.manifest.generate(id='html',
href=ascii_filename(fname))
item = oeb.manifest.add(id, href, 'text/html')
item.html_input_href = fname
oeb.spine.add(item, True)
i = i + 1
imageFiles = snbFile.OutputImageFiles(tdir)
for f, m in imageFiles:
id, href = oeb.manifest.generate(id='image',
href=ascii_filename(f))
item = oeb.manifest.add(id, href, m)
item.html_input_href = f
return oeb

View File

@ -0,0 +1,264 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import os, string
from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation
from calibre.ptempfile import TemporaryDirectory
from calibre.constants import __appname__, __version__
from calibre.ebooks.snb.snbfile import SNBFile
from calibre.ebooks.snb.snbml import SNBMLizer, ProcessFileName
class SNBOutput(OutputFormatPlugin):
name = 'SNB Output'
author = 'Li Fanxi'
file_type = 'snb'
options = set([
# OptionRecommendation(name='newline', recommended_value='system',
# level=OptionRecommendation.LOW,
# short_switch='n', choices=TxtNewlines.NEWLINE_TYPES.keys(),
# help=_('Type of newline to use. Options are %s. Default is \'system\'. '
# 'Use \'old_mac\' for compatibility with Mac OS 9 and earlier. '
# 'For Mac OS X use \'unix\'. \'system\' will default to the newline '
# 'type used by this OS.') % sorted(TxtNewlines.NEWLINE_TYPES.keys())),
OptionRecommendation(name='snb_output_encoding', recommended_value='utf-8',
level=OptionRecommendation.LOW,
help=_('Specify the character encoding of the output document. ' \
'The default is utf-8.')),
# OptionRecommendation(name='inline_toc',
# recommended_value=False, level=OptionRecommendation.LOW,
# help=_('Add Table of Contents to beginning of the book.')),
OptionRecommendation(name='snb_max_line_length',
recommended_value=0, level=OptionRecommendation.LOW,
help=_('The maximum number of characters per line. This splits on '
'the first space before the specified value. If no space is found '
'the line will be broken at the space after and will exceed the '
'specified value. Also, there is a minimum of 25 characters. '
'Use 0 to disable line splitting.')),
# OptionRecommendation(name='force_max_line_length',
# recommended_value=False, level=OptionRecommendation.LOW,
# help=_('Force splitting on the max-line-length value when no space '
# 'is present. Also allows max-line-length to be below the minimum')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
self.opts = opts
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
try:
rasterizer = SVGRasterizer()
rasterizer(oeb_book, opts)
except Unavailable:
log.warn('SVG rasterizer unavailable, SVG will not be converted')
# Create temp dir
with TemporaryDirectory('_snb_output') as tdir:
# Create stub directories
snbfDir = os.path.join(tdir, 'snbf')
snbcDir = os.path.join(tdir, 'snbc')
snbiDir = os.path.join(tdir, 'snbc/images')
os.mkdir(snbfDir)
os.mkdir(snbcDir)
os.mkdir(snbiDir)
# Process Meta data
meta = oeb_book.metadata
if meta.title:
title = unicode(meta.title[0])
else:
title = ''
authors = [unicode(x) for x in meta.creator if x.role == 'aut']
if meta.publisher:
publishers = unicode(meta.publisher[0])
else:
publishers = ''
if meta.language:
lang = unicode(meta.language[0]).upper()
else:
lang = ''
if meta.description:
abstract = unicode(meta.description[0])
else:
abstract = ''
# Process Cover
g, m, s = oeb_book.guide, oeb_book.manifest, oeb_book.spine
href = None
if 'titlepage' not in g:
if 'cover' in g:
href = g['cover'].href
# Output book info file
bookInfoTree = etree.Element("book-snbf", version="1.0")
headTree = etree.SubElement(bookInfoTree, "head")
etree.SubElement(headTree, "name").text = title
etree.SubElement(headTree, "author").text = ' '.join(authors)
etree.SubElement(headTree, "language").text = lang
etree.SubElement(headTree, "rights")
etree.SubElement(headTree, "publisher").text = publishers
etree.SubElement(headTree, "generator").text = __appname__ + ' ' + __version__
etree.SubElement(headTree, "created")
etree.SubElement(headTree, "abstract").text = abstract
if href != None:
etree.SubElement(headTree, "cover").text = ProcessFileName(href)
else:
etree.SubElement(headTree, "cover")
bookInfoFile = open(os.path.join(snbfDir, 'book.snbf'), 'wb')
bookInfoFile.write(etree.tostring(bookInfoTree, pretty_print=True, encoding='utf-8'))
bookInfoFile.close()
# Output TOC
tocInfoTree = etree.Element("toc-snbf")
tocHead = etree.SubElement(tocInfoTree, "head")
tocBody = etree.SubElement(tocInfoTree, "body")
outputFiles = { }
if oeb_book.toc.count() == 0:
log.warn('This SNB file has no Table of Contents. '
'Creating a default TOC')
first = iter(oeb_book.spine).next()
oeb_book.toc.add(_('Start Page'), first.href)
else:
first = iter(oeb_book.spine).next()
if oeb_book.toc[0].href != first.href:
# The pages before the fist item in toc will be stored as
# "Cover Pages".
# oeb_book.toc does not support "insert", so we generate
# the tocInfoTree directly instead of modifying the toc
ch = etree.SubElement(tocBody, "chapter")
ch.set("src", ProcessFileName(first.href) + ".snbc")
ch.text = _('Cover Pages')
outputFiles[first.href] = []
outputFiles[first.href].append(("", _("Cover Pages")))
for tocitem in oeb_book.toc:
if tocitem.href.find('#') != -1:
item = string.split(tocitem.href, '#')
if len(item) != 2:
log.error('Error in TOC item: %s' % tocitem)
else:
if item[0] in outputFiles:
outputFiles[item[0]].append((item[1], tocitem.title))
else:
outputFiles[item[0]] = []
if not "" in outputFiles[item[0]]:
outputFiles[item[0]].append(("", tocitem.title + _(" (Preface)")))
ch = etree.SubElement(tocBody, "chapter")
ch.set("src", ProcessFileName(item[0]) + ".snbc")
ch.text = tocitem.title + _(" (Preface)")
outputFiles[item[0]].append((item[1], tocitem.title))
else:
if tocitem.href in outputFiles:
outputFiles[tocitem.href].append(("", tocitem.title))
else:
outputFiles[tocitem.href] = []
outputFiles[tocitem.href].append(("", tocitem.title))
ch = etree.SubElement(tocBody, "chapter")
ch.set("src", ProcessFileName(tocitem.href) + ".snbc")
ch.text = tocitem.title
etree.SubElement(tocHead, "chapters").text = '%d' % len(tocBody)
tocInfoFile = open(os.path.join(snbfDir, 'toc.snbf'), 'wb')
tocInfoFile.write(etree.tostring(tocInfoTree, pretty_print=True, encoding='utf-8'))
tocInfoFile.close()
# Output Files
oldTree = None
mergeLast = False
lastName = None
for item in s:
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES
if m.hrefs[item.href].media_type in OEB_DOCS:
if not item.href in outputFiles:
log.debug('File %s is unused in TOC. Continue in last chapter' % item.href)
mergeLast = True
else:
if oldTree != None and mergeLast:
log.debug('Output the modified chapter again: %s' % lastName)
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
outputFile.close()
mergeLast = False
log.debug('Converting %s to snbc...' % item.href)
snbwriter = SNBMLizer(log)
snbcTrees = None
if not mergeLast:
snbcTrees = snbwriter.extract_content(oeb_book, item, outputFiles[item.href], opts)
for subName in snbcTrees:
postfix = ''
if subName != '':
postfix = '_' + subName
lastName = ProcessFileName(item.href + postfix + ".snbc")
oldTree = snbcTrees[subName]
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
outputFile.close()
else:
log.debug('Merge %s with last TOC item...' % item.href)
snbwriter.merge_content(oldTree, oeb_book, item, [('', _("Start"))], opts)
# Output the last one if needed
log.debug('Output the last modified chapter again: %s' % lastName)
if oldTree != None and mergeLast:
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
outputFile.close()
mergeLast = False
for item in m:
if m.hrefs[item.href].media_type in OEB_IMAGES:
log.debug('Converting image: %s ...' % item.href)
content = m.hrefs[item.href].data
# Convert & Resize image
self.HandleImage(content, os.path.join(snbiDir, ProcessFileName(item.href)))
# Package as SNB File
snbFile = SNBFile()
snbFile.FromDir(tdir)
snbFile.Output(output_path)
def HandleImage(self, imageData, imagePath):
from calibre.utils.magick import Image
img = Image()
img.load(imageData)
(x,y) = img.size
if self.opts:
SCREEN_Y, SCREEN_X = self.opts.output_profile.comic_screen_size
else:
SCREEN_X = 540
SCREEN_Y = 700
# Handle big image only
if x > SCREEN_X or y > SCREEN_Y:
xScale = float(x) / SCREEN_X
yScale = float(y) / SCREEN_Y
scale = max(xScale, yScale)
# TODO : intelligent image rotation
# img = img.rotate(90)
# x,y = y,x
img.size = (x / scale, y / scale)
img.save(imagePath)
if __name__ == '__main__':
from calibre.ebooks.oeb.reader import OEBReader
from calibre.ebooks.oeb.base import OEBBook
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
from calibre.customize.profiles import HanlinV3Output
class OptionValues(object):
pass
opts = OptionValues()
opts.output_profile = HanlinV3Output(None)
html_preprocessor = HTMLPreProcessor(None, None, opts)
from calibre.utils.logging import default_log
oeb = OEBBook(default_log, html_preprocessor)
reader = OEBReader
reader()(oeb, '/tmp/bbb/processed/')
SNBOutput(None).convert(oeb, '/tmp/test.snb', None, None, default_log);

View File

@ -0,0 +1,319 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import sys, struct, zlib, bz2, os
from mimetypes import types_map
class FileStream:
def IsBinary(self):
return self.attr & 0x41000000 != 0x41000000
def compareFileStream(file1, file2):
return cmp(file1.fileName, file2.fileName)
class BlockData:
pass
class SNBFile:
MAGIC = 'SNBP000B'
REV80 = 0x00008000
REVA3 = 0x00A3A3A3
REVZ1 = 0x00000000
REVZ2 = 0x00000000
def __init__(self, inputFile = None):
self.files = []
self.blocks = []
if inputFile != None:
self.Open(inputFile)
def Open(self, inputFile):
self.fileName = inputFile
snbFile = open(self.fileName, "rb")
snbFile.seek(0)
self.Parse(snbFile)
snbFile.close()
def Parse(self, snbFile, metaOnly = False):
# Read header
vmbr = snbFile.read(44)
(self.magic, self.rev80, self.revA3, self.revZ1,
self.fileCount, self.vfatSize, self.vfatCompressed,
self.binStreamSize, self.plainStreamSizeUncompressed,
self.revZ2) = struct.unpack('>8siiiiiiiii', vmbr)
# Read FAT
self.vfat = zlib.decompress(snbFile.read(self.vfatCompressed))
self.ParseFile(self.vfat, self.fileCount)
# Read tail
snbFile.seek(-16, os.SEEK_END)
#plainStreamEnd = snbFile.tell()
tailblock = snbFile.read(16)
(self.tailSize, self.tailOffset, self.tailMagic) = struct.unpack('>ii8s', tailblock)
snbFile.seek(self.tailOffset)
self.vTailUncompressed = zlib.decompress(snbFile.read(self.tailSize))
self.tailSizeUncompressed = len(self.vTailUncompressed)
self.ParseTail(self.vTailUncompressed, self.fileCount)
# Uncompress file data
# Read files
binPos = 0
plainPos = 0
uncompressedData = None
for f in self.files:
if f.attr & 0x41000000 == 0x41000000:
# Compressed Files
if uncompressedData == None:
uncompressedData = ""
for i in range(self.plainBlock):
bzdc = bz2.BZ2Decompressor()
if (i < self.plainBlock - 1):
bSize = self.blocks[self.binBlock + i + 1].Offset - self.blocks[self.binBlock + i].Offset;
else:
bSize = self.tailOffset - self.blocks[self.binBlock + i].Offset;
snbFile.seek(self.blocks[self.binBlock + i].Offset);
try:
data = snbFile.read(bSize)
uncompressedData += bzdc.decompress(data)
except Exception, e:
print e
f.fileBody = uncompressedData[plainPos:plainPos+f.fileSize]
plainPos += f.fileSize
elif f.attr & 0x01000000 == 0x01000000:
# Binary Files
snbFile.seek(44 + self.vfatCompressed + binPos)
f.fileBody = snbFile.read(f.fileSize)
binPos += f.fileSize
else:
print f.attr, f.fileName
raise Exception("Invalid file")
def ParseFile(self, vfat, fileCount):
fileNames = vfat[fileCount*12:].split('\0');
for i in range(fileCount):
f = FileStream()
(f.attr, f.fileNameOffset, f.fileSize) = struct.unpack('>iii', vfat[i * 12 : (i+1)*12])
f.fileName = fileNames[i]
self.files.append(f)
def ParseTail(self, vtail, fileCount):
self.binBlock = (self.binStreamSize + 0x8000 - 1) / 0x8000;
self.plainBlock = (self.plainStreamSizeUncompressed + 0x8000 - 1) / 0x8000;
for i in range(self.binBlock + self.plainBlock):
block = BlockData()
(block.Offset,) = struct.unpack('>i', vtail[i * 4 : (i+1) * 4])
self.blocks.append(block)
for i in range(fileCount):
(self.files[i].blockIndex, self.files[i].contentOffset) = struct.unpack('>ii', vtail[(self.binBlock + self.plainBlock) * 4 + i * 8 : (self.binBlock + self.plainBlock) * 4 + (i+1) * 8])
def IsValid(self):
if self.magic != SNBFile.MAGIC:
return False
if self.rev80 != SNBFile.REV80:
return False
if self.revA3 != SNBFile.REVA3:
return False
if self.revZ1 != SNBFile.REVZ1:
return False
if self.revZ2 != SNBFile.REVZ2:
return False
if self.vfatSize != len(self.vfat):
return False
if self.fileCount != len(self.files):
return False
if (self.binBlock + self.plainBlock) * 4 + self.fileCount * 8 != self.tailSizeUncompressed:
return False
if self.tailMagic != SNBFile.MAGIC:
print self.tailMagic
return False
return True
def FromDir(self, tdir):
for root, dirs, files in os.walk(tdir):
for name in files:
p, ext = os.path.splitext(name)
if ext in [ ".snbf", ".snbc" ]:
self.AppendPlain(os.path.relpath(os.path.join(root, name), tdir), tdir)
else:
self.AppendBinary(os.path.relpath(os.path.join(root, name), tdir), tdir)
def AppendPlain(self, fileName, tdir):
f = FileStream()
f.attr = 0x41000000
f.fileSize = os.path.getsize(os.path.join(tdir,fileName))
f.fileBody = open(os.path.join(tdir,fileName), 'rb').read()
f.fileName = fileName.replace(os.sep, '/')
self.files.append(f)
def AppendBinary(self, fileName, tdir):
f = FileStream()
f.attr = 0x01000000
f.fileSize = os.path.getsize(os.path.join(tdir,fileName))
f.fileBody = open(os.path.join(tdir,fileName), 'rb').read()
f.fileName = fileName.replace(os.sep, '/')
self.files.append(f)
def GetFileStream(self, fileName):
for file in self.files:
if file.fileName == fileName:
return file.fileBody
return None
def OutputImageFiles(self, path):
fileNames = []
for f in self.files:
fname = os.path.basename(f.fileName)
root, ext = os.path.splitext(fname)
if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]:
file = open(os.path.join(path, fname), 'wb')
file.write(f.fileBody)
file.close()
fileNames.append((fname, types_map[ext]))
return fileNames
def Output(self, outputFile):
# Sort the files in file buffer,
# requried by the SNB file format
self.files.sort(compareFileStream)
outputFile = open(outputFile, 'wb')
# File header part 1
vmbrp1 = struct.pack('>8siiii', SNBFile.MAGIC, SNBFile.REV80, SNBFile.REVA3, SNBFile.REVZ1, len(self.files))
# Create VFAT & file stream
vfat = ''
fileNameTable = ''
plainStream = ''
binStream = ''
for f in self.files:
vfat += struct.pack('>iii', f.attr, len(fileNameTable), f.fileSize);
fileNameTable += (f.fileName + '\0')
if f.attr & 0x41000000 == 0x41000000:
# Plain Files
f.contentOffset = len(plainStream)
plainStream += f.fileBody
elif f.attr & 0x01000000 == 0x01000000:
# Binary Files
f.contentOffset = len(binStream)
binStream += f.fileBody
else:
print f.attr, f.fileName
raise Exception("Unknown file type")
vfatCompressed = zlib.compress(vfat+fileNameTable)
# File header part 2
vmbrp2 = struct.pack('>iiiii', len(vfat+fileNameTable), len(vfatCompressed), len(binStream), len(plainStream), SNBFile.REVZ2)
# Write header
outputFile.write(vmbrp1 + vmbrp2)
# Write vfat
outputFile.write(vfatCompressed)
# Generate block information
binBlockOffset = 0x2C + len(vfatCompressed)
plainBlockOffset = binBlockOffset + len(binStream)
binBlock = (len(binStream) + 0x8000 - 1) / 0x8000
#plainBlock = (len(plainStream) + 0x8000 - 1) / 0x8000
offset = 0
tailBlock = ''
for i in range(binBlock):
tailBlock += struct.pack('>i', binBlockOffset + offset)
offset += 0x8000;
tailRec = ''
for f in self.files:
t = 0
if f.IsBinary():
t = 0
else:
t = binBlock
tailRec += struct.pack('>ii', f.contentOffset / 0x8000 + t, f.contentOffset % 0x8000);
# Write binary stream
outputFile.write(binStream)
# Write plain stream
pos = 0
offset = 0
while pos < len(plainStream):
tailBlock += struct.pack('>i', plainBlockOffset + offset);
block = plainStream[pos:pos+0x8000];
compressed = bz2.compress(block)
outputFile.write(compressed)
offset += len(compressed)
pos += 0x8000
# Write tail block
compressedTail = zlib.compress(tailBlock + tailRec)
outputFile.write(compressedTail)
# Write tail pointer
veom = struct.pack('>ii', len(compressedTail), plainBlockOffset + offset)
outputFile.write(veom)
# Write file end mark
outputFile.write(SNBFile.MAGIC);
# Close
outputFile.close()
return
def Dump(self):
if self.fileName:
print "File Name:\t", self.fileName
print "File Count:\t", self.fileCount
print "VFAT Size(Compressed):\t%d(%d)" % (self.vfatSize, self.vfatCompressed)
print "Binary Stream Size:\t", self.binStreamSize
print "Plain Stream Uncompressed Size:\t", self.plainStreamSizeUncompressed
print "Binary Block Count:\t", self.binBlock
print "Plain Block Count:\t", self.plainBlock
for i in range(self.fileCount):
print "File ", i
f = self.files[i]
print "File Name: ", f.fileName
print "File Attr: ", f.attr
print "File Size: ", f.fileSize
print "Block Index: ", f.blockIndex
print "Content Offset: ", f.contentOffset
tempFile = open("/tmp/" + f.fileName, 'wb')
tempFile.write(f.fileBody)
tempFile.close()
def usage():
print "This unit test is for INTERNAL usage only!"
print "This unit test accept two parameters."
print "python snbfile.py <INPUTFILE> <DESTFILE>"
print "The input file will be extracted and write to dest file. "
print "Meta data of the file will be shown during this process."
def main():
if len(sys.argv) != 3:
usage()
sys.exit(0)
inputFile = sys.argv[1]
outputFile = sys.argv[2]
print "Input file: ", inputFile
print "Output file: ", outputFile
snbFile = SNBFile(inputFile)
if snbFile.IsValid():
snbFile.Dump()
snbFile.Output(outputFile)
else:
print "The input file is invalid."
return 1
return 0
if __name__ == "__main__":
"""SNB file unit test"""
sys.exit(main())

View File

@ -0,0 +1,263 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
'''
Transform OEB content into SNB format
'''
import os
import re
from lxml import etree
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace
from calibre.ebooks.oeb.stylizer import Stylizer
def ProcessFileName(fileName):
# Flat the path
fileName = fileName.replace("/", "_").replace(os.sep, "_")
# Handle bookmark for HTML file
fileName = fileName.replace("#", "_")
# Make it lower case
fileName = fileName.lower()
# Change all images to jpg
root, ext = os.path.splitext(fileName)
if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]:
fileName = root + '.jpg'
return fileName
BLOCK_TAGS = [
'div',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'tr',
]
BLOCK_STYLES = [
'block',
]
SPACE_TAGS = [
'td',
]
CALIBRE_SNB_IMG_TAG = "<$$calibre_snb_temp_img$$>"
CALIBRE_SNB_BM_TAG = "<$$calibre_snb_bm_tag$$>"
CALIBRE_SNB_PRE_TAG = "<$$calibre_snb_pre_tag$$>"
class SNBMLizer(object):
curSubItem = ""
# curText = [ ]
def __init__(self, log):
self.log = log
def extract_content(self, oeb_book, item, subitems, opts):
self.log.info('Converting XHTML to SNBC...')
self.oeb_book = oeb_book
self.opts = opts
self.item = item
self.subitems = subitems
return self.mlize();
def merge_content(self, old_tree, oeb_book, item, subitems, opts):
newTrees = self.extract_content(oeb_book, item, subitems, opts)
body = old_tree.find(".//body")
if body != None:
for subName in newTrees:
newbody = newTrees[subName].find(".//body")
for entity in newbody:
body.append(entity)
def mlize(self):
output = [ u'' ]
stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile)
content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode))
# content = self.remove_newlines(content)
trees = { }
for subitem, subtitle in self.subitems:
snbcTree = etree.Element("snbc")
etree.SubElement(etree.SubElement(snbcTree, "head"), "title").text = subtitle
etree.SubElement(snbcTree, "body")
trees[subitem] = snbcTree
output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, ""))
output += self.dump_text(self.subitems, etree.fromstring(content), stylizer)[0]
output = self.cleanup_text(u''.join(output))
subitem = ''
for line in output.splitlines():
if not line.find(CALIBRE_SNB_PRE_TAG) == 0:
line = line.strip(u' \t\n\r\u3000')
else:
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
etree.CDATA(line[len(CALIBRE_SNB_PRE_TAG):])
continue
if len(line) != 0:
if line.find(CALIBRE_SNB_IMG_TAG) == 0:
prefix = ProcessFileName(os.path.dirname(self.item.href))
if prefix != '':
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
prefix + '_' + line[len(CALIBRE_SNB_IMG_TAG):]
else:
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
line[len(CALIBRE_SNB_IMG_TAG):]
elif line.find(CALIBRE_SNB_BM_TAG) == 0:
subitem = line[len(CALIBRE_SNB_BM_TAG):]
else:
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
etree.CDATA(unicode(u'\u3000\u3000' + line))
return trees
def remove_newlines(self, text):
self.log.debug('\tRemove newlines for processing...')
text = text.replace('\r\n', ' ')
text = text.replace('\n', ' ')
text = text.replace('\r', ' ')
return text
def cleanup_text(self, text):
self.log.debug('\tClean up text...')
# Replace bad characters.
text = text.replace(u'\xc2', '')
text = text.replace(u'\xa0', ' ')
text = text.replace(u'\xa9', '(C)')
# Replace tabs, vertical tags and form feeds with single space.
text = text.replace('\t+', ' ')
text = text.replace('\v+', ' ')
text = text.replace('\f+', ' ')
# Single line paragraph.
text = re.sub('(?<=.)%s(?=.)' % os.linesep, ' ', text)
# Remove multiple spaces.
#text = re.sub('[ ]{2,}', ' ', text)
# Remove excessive newlines.
text = re.sub('\n[ ]+\n', '\n\n', text)
if self.opts.remove_paragraph_spacing:
text = re.sub('\n{2,}', '\n', text)
text = re.sub('(?imu)^(?=.)', '\t', text)
else:
text = re.sub('\n{3,}', '\n\n', text)
# Replace spaces at the beginning and end of lines
text = re.sub('(?imu)^[ ]+', '', text)
text = re.sub('(?imu)[ ]+$', '', text)
if self.opts.snb_max_line_length:
max_length = self.opts.snb_max_line_length
if self.opts.max_line_length < 25:# and not self.opts.force_max_line_length:
max_length = 25
short_lines = []
lines = text.splitlines()
for line in lines:
while len(line) > max_length:
space = line.rfind(' ', 0, max_length)
if space != -1:
# Space was found.
short_lines.append(line[:space])
line = line[space + 1:]
else:
# Space was not found.
if False and self.opts.force_max_line_length:
# Force breaking at max_lenght.
short_lines.append(line[:max_length])
line = line[max_length:]
else:
# Look for the first space after max_length.
space = line.find(' ', max_length, len(line))
if space != -1:
# Space was found.
short_lines.append(line[:space])
line = line[space + 1:]
else:
# No space was found cannot break line.
short_lines.append(line)
line = ''
# Add the text that was less than max_lengh to the list
short_lines.append(line)
text = '\n'.join(short_lines)
return text
def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''):
if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS:
return ['']
text = ['']
style = stylizer.style(elem)
if elem.attrib.get('id') != None and elem.attrib['id'] in [ href for href, title in subitems ]:
if self.curSubItem != None and self.curSubItem != elem.attrib['id']:
self.curSubItem = elem.attrib['id']
text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_BM_TAG, self.curSubItem))
if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \
or style['visibility'] == 'hidden':
return ['']
tag = barename(elem.tag)
in_block = False
# Are we in a paragraph block?
if tag in BLOCK_TAGS or style['display'] in BLOCK_STYLES:
in_block = True
if not end.endswith(u'\n\n') and hasattr(elem, 'text') and elem.text:
text.append(u'\n\n')
if tag in SPACE_TAGS:
if not end.endswith('u ') and hasattr(elem, 'text') and elem.text:
text.append(u' ')
if tag == 'img':
text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src'])))
if tag == 'br':
text.append(u'\n\n')
if tag == 'li':
li = '- '
pre = (tag == 'pre' or pre)
# Process tags that contain text.
if hasattr(elem, 'text') and elem.text:
if pre:
text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join((li + elem.text).splitlines()))
else:
text.append(li + elem.text)
li = ''
for item in elem:
en = u''
if len(text) >= 2:
en = text[-1][-2:]
t = self.dump_text(subitems, item, stylizer, en, pre, li)[0]
text += t
if in_block:
text.append(u'\n\n')
if hasattr(elem, 'tail') and elem.tail:
if pre:
text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.tail.splitlines()))
else:
text.append(li + elem.tail)
li = ''
return text, li

View File

@ -166,6 +166,7 @@ class AddAction(InterfaceAction):
(_('Topaz books'), ['tpz','azw1']),
(_('Text books'), ['txt', 'rtf']),
(_('PDF Books'), ['pdf']),
(_('SNB Books'), ['snb']),
(_('Comics'), ['cbz', 'cbr', 'cbc']),
(_('Archives'), ['zip', 'rar']),
]

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.convert.snb_output_ui import Ui_Form
from calibre.gui2.convert import Widget
newline_model = None
class PluginWidget(Widget, Ui_Form):
TITLE = _('SNB Output')
HELP = _('Options specific to')+' SNB '+_('output')
COMMIT_NAME = 'snb_output'
ICON = I('mimetypes/snb.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
[])
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)
# default = self.opt_newline.currentText()
# global newline_model
# if newline_model is None:
# newline_model = BasicComboModel(TxtNewlines.NEWLINE_TYPES.keys())
# self.newline_model = newline_model
# self.opt_newline.setModel(self.newline_model)
# default_index = self.opt_newline.findText(default)
# system_index = self.opt_newline.findText('system')
# self.opt_newline.setCurrentIndex(default_index if default_index != -1 else system_index if system_index != -1 else 0)

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<!-- <item row="0" column="0"> -->
<!-- <widget class="QLabel" name="label"> -->
<!-- <property name="text"> -->
<!-- <string>&amp;Line ending style:</string> -->
<!-- </property> -->
<!-- <property name="buddy"> -->
<!-- <cstring>opt_newline</cstring> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
<!-- <item row="0" column="1"> -->
<!-- <widget class="QComboBox" name="opt_newline"/> -->
<!-- </item> -->
<!-- <item row="4" column="0"> -->
<!-- <spacer name="verticalSpacer"> -->
<!-- <property name="orientation"> -->
<!-- <enum>Qt::Vertical</enum> -->
<!-- </property> -->
<!-- <property name="sizeHint" stdset="0"> -->
<!-- <size> -->
<!-- <width>20</width> -->
<!-- <height>246</height> -->
<!-- </size> -->
<!-- </property> -->
<!-- </spacer> -->
<!-- </item> -->
<!-- <item row="3" column="0" colspan="2"> -->
<!-- <widget class="QCheckBox" name="opt_inline_toc"> -->
<!-- <property name="text"> -->
<!-- <string>&amp;Inline TOC</string> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
<!-- <item row="1" column="1"> -->
<!-- <widget class="QSpinBox" name="opt_max_line_length"/> -->
<!-- </item> -->
<!-- <item row="1" column="0"> -->
<!-- <widget class="QLabel" name="label_2"> -->
<!-- <property name="text"> -->
<!-- <string>&amp;Maximum line length:</string> -->
<!-- </property> -->
<!-- <property name="buddy"> -->
<!-- <cstring>opt_max_line_length</cstring> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
<!-- <item row="2" column="0" colspan="2"> -->
<!-- <widget class="QCheckBox" name="opt_force_max_line_length"> -->
<!-- <property name="text"> -->
<!-- <string>Force maximum line length</string> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>752</width>
<height>715</height>
<height>633</height>
</rect>
</property>
<property name="windowTitle">
@ -660,8 +660,8 @@ nothing should be put between the original text and the inserted text</string>
<rect>
<x>0</x>
<y>0</y>
<width>122</width>
<height>34</height>
<width>726</width>
<height>334</height>
</rect>
</property>
<layout class="QGridLayout" name="testgrid">
@ -682,19 +682,6 @@ nothing should be put between the original text and the inserted text</string>
</widget>
</widget>
</item>
<item row="20" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>

View File

@ -25,7 +25,7 @@ from calibre.ebooks.metadata.covers import download_cover
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp, utc_tz
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.preferences.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page
@ -729,10 +729,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setText(book.series)
if book.series_index is not None:
self.series_index.setValue(book.series_index)
# Needed because of Qt focus bug on OS X
self.fetch_cover_button.setFocus(Qt.OtherFocusReason)
else:
error_dialog(self, _('Cannot fetch metadata'),
_('You must specify at least one of ISBN, Title, '
'Authors or Publisher'), show=True)
self.title.setFocus(Qt.OtherFocusReason)
def enable_series_index(self, *args):
self.series_index.setEnabled(True)

View File

@ -120,12 +120,15 @@ class SchedulerDialog(QDialog, Ui_Dialog):
if self.account.isVisible():
un, pw = map(unicode, (self.username.text(), self.password.text()))
un, pw = un.strip(), pw.strip()
if not un and not pw and self.schedule.isChecked():
error_dialog(self, _('Need username and password'),
_('You must provide a username and/or password to '
'use this news source.'), show=True)
return False
self.recipe_model.set_account_info(urn, un.strip(), pw.strip())
if not getattr(self, 'subscription_optional', False):
error_dialog(self, _('Need username and password'),
_('You must provide a username and/or password to '
'use this news source.'), show=True)
return False
if un or pw:
self.recipe_model.set_account_info(urn, un, pw)
if self.schedule.isChecked():
schedule_type = 'interval' if self.interval_button.isChecked() else 'day/time'
@ -157,7 +160,13 @@ class SchedulerDialog(QDialog, Ui_Dialog):
account_info = self.recipe_model.account_info_from_urn(urn)
customize_info = self.recipe_model.get_customize_info(urn)
self.account.setVisible(recipe.get('needs_subscription', '') == 'yes')
ns = recipe.get('needs_subscription', '')
self.account.setVisible(ns in ('yes', 'optional'))
self.subscription_optional = ns == 'optional'
act = _('Account')
act2 = _('(optional)') if self.subscription_optional else \
_('(required)')
self.account.setTitle(act+' '+act2)
un = pw = ''
if account_info is not None:
un, pw = account_info[:2]

View File

@ -17,7 +17,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE
from calibre.library.field_metadata import TagsIcons
from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
@ -382,17 +382,11 @@ class TagsModel(QAbstractItemModel): # {{{
# must do this here because 'QPixmap: Must construct a QApplication
# before a QPaintDevice'. The ':' in front avoids polluting either the
# user-defined categories (':' at end) or columns namespaces (no ':').
self.category_icon_map = TagsIcons({
'authors' : QIcon(I('user_profile.png')),
'series' : QIcon(I('series.png')),
'formats' : QIcon(I('book.png')),
'publisher' : QIcon(I('publisher.png')),
'rating' : QIcon(I('rating.png')),
'news' : QIcon(I('news.png')),
'tags' : QIcon(I('tags.png')),
':custom' : QIcon(I('column.png')),
':user' : QIcon(I('drawer.png')),
'search' : QIcon(I('search.png'))})
iconmap = {}
for key in category_icon_map:
iconmap[key] = QIcon(I(category_icon_map[key]))
self.category_icon_map = TagsIcons(iconmap)
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
self.drag_drop_finished = drag_drop_finished

View File

@ -353,6 +353,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.pending_bookmark = bm
if spine_index < 0 or spine_index >= len(self.iterator.spine):
spine_index = 0
self.pending_bookmark = None
self.load_path(self.iterator.spine[spine_index])
def toc_clicked(self, index):

View File

@ -42,6 +42,8 @@ def comments_to_html(comments):
Deprecated HTML returns as HTML via BeautifulSoup()
'''
if not comments:
return u'<p></p>'
if not isinstance(comments, unicode):
comments = comments.decode(preferred_encoding, 'replace')

View File

@ -22,6 +22,20 @@ class TagsIcons(dict):
raise ValueError('Missing category icon [%s]'%a)
self[a] = icon_dict[a]
category_icon_map = {
'authors' : 'user_profile.png',
'series' : 'series.png',
'formats' : 'book.png',
'publisher' : 'publisher.png',
'rating' : 'rating.png',
'news' : 'news.png',
'tags' : 'tags.png',
':custom' : 'column.png',
':user' : 'drawer.png',
'search' : 'search.png'
}
class FieldMetadata(dict):
'''
key: the key to the dictionary is:
@ -161,7 +175,7 @@ class FieldMetadata(dict):
'datatype':'text',
'is_multiple':None,
'kind':'field',
'name':None,
'name':_('Comments'),
'search_terms':['comments', 'comment'],
'is_custom':False, 'is_category':False}),
('cover', {'table':None,

View File

@ -6,42 +6,83 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import operator, os, json
from urllib import quote
from binascii import hexlify, unhexlify
import cherrypy
from calibre.constants import filesystem_encoding
from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml
from calibre import isbytestring, force_unicode, fit_image, \
prepare_string_for_xml as xml
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.filenames import ascii_filename
from calibre.utils.config import prefs
from calibre.utils.magick import Image
from calibre.library.comments import comments_to_html
from calibre.library.server import custom_fields_to_display
from calibre.library.field_metadata import category_icon_map
def render_book_list(ids):
def render_book_list(ids, suffix=''): # {{{
pages = []
num = len(ids)
pos = 0
delta = 25
while ids:
page = list(ids[:25])
pages.append(page)
ids = ids[25:]
page = list(ids[:delta])
pages.append((page, pos))
ids = ids[delta:]
pos += len(page)
page_template = u'''\
<div class="page" id="page{0}">
<div class="load_data" title="{1}"></div>
<div class="load_data" title="{1}">
<span class="url" title="/browse/booklist_page"></span>
<span class="start" title="{start}"></span>
<span class="end" title="{end}"></span>
</div>
<div class="loading"><img src="/static/loading.gif" /> {2}</div>
<div class="loaded"></div>
</div>
'''
rpages = []
for i, pg in enumerate(pages):
for i, x in enumerate(pages):
pg, pos = x
ld = xml(json.dumps(pg), True)
rpages.append(page_template.format(i, ld,
xml(_('Loading, please wait')) + '&hellip;'))
xml(_('Loading, please wait')) + '&hellip;',
start=pos+1, end=pos+len(pg)))
rpages = u'\n\n'.join(rpages)
templ = u'''\
<h3>{0}</h3>
<h3>{0} {suffix}</h3>
<div id="booklist">
<div class="listnav topnav">
{navbar}
</div>
{pages}
<div class="listnav bottomnav">
{navbar}
</div>
</div>
'''
return templ.format(_('Browsing %d books')%len(ids), pages=rpages)
navbar = u'''\
<div class="navleft">
<a href="#" onclick="first_page(); return false;">{first}</a>
<a href="#" onclick="previous_page(); return false;">{previous}</a>
</div>
<div class="navmiddle">
<span class="start">0</span> to <span class="end">0</span> of {num}
</div>
<div class="navright">
<a href="#" onclick="next_page(); return false;">{next}</a>
<a href="#" onclick="last_page(); return false;">{last}</a>
</div>
'''.format(first=_('First'), last=_('Last'), previous=_('Previous'),
next=_('Next'), num=num)
return templ.format(_('Browsing %d books')%num, suffix=suffix,
pages=rpages, navbar=navbar)
# }}}
def utf8(x): # {{{
if isinstance(x, unicode):
@ -49,11 +90,13 @@ def utf8(x): # {{{
return x
# }}}
def render_rating(rating, container='span'): # {{{
def render_rating(rating, container='span', prefix=None): # {{{
if rating < 0.1:
return '', ''
added = 0
rstring = xml(_('Average rating: %.1f stars')% (rating if rating else 0.0),
if prefix is None:
prefix = _('Average rating')
rstring = xml(_('%s: %.1f stars')% (prefix, rating if rating else 0.0),
True)
ans = ['<%s class="rating">' % (container)]
for i in range(5):
@ -89,10 +132,13 @@ def get_category_items(category, items, db, datatype): # {{{
id_ = xml(str(id_))
desc = ''
if i.count > 0:
desc += '[' + _('%d items')%i.count + ']'
href = '/browse/matches/%s/%s'%(category, id_)
desc += '[' + _('%d books')%i.count + ']'
q = i.category
if not q:
q = category
href = '/browse/matches/%s/%s'%(q, id_)
return templ.format(xml(name), rating,
xml(desc), xml(quote(href)), rstring)
xml(desc), xml(href), rstring)
items = list(map(item, items))
return '\n'.join(['<div class="category-container">'] + items + ['</div>'])
@ -111,11 +157,12 @@ class Endpoint(object): # {{{
def __call__(eself, func):
def do(self, *args, **kwargs):
sort_val = None
cookie = cherrypy.request.cookie
if cookie.has_key(eself.sort_cookie_name):
sort_val = cookie[eself.sort_cookie_name].value
kwargs[eself.sort_kwarg] = sort_val
if 'json' not in eself.mimetype:
sort_val = None
cookie = cherrypy.request.cookie
if cookie.has_key(eself.sort_cookie_name):
sort_val = cookie[eself.sort_cookie_name].value
kwargs[eself.sort_kwarg] = sort_val
ans = func(self, *args, **kwargs)
cherrypy.response.headers['Content-Type'] = eself.mimetype
@ -146,67 +193,119 @@ class BrowseServer(object):
connect('browse_booklist_page',
base_href+'/booklist_page',
self.browse_booklist_page)
connect('browse_search', base_href+'/search/{query}',
connect('browse_search', base_href+'/search',
self.browse_search)
connect('browse_details', base_href+'/details/{id}',
self.browse_details)
connect('browse_book', base_href+'/book/{id}',
self.browse_book)
connect('browse_category_icon', base_href+'/icon/{name}',
self.browse_icon)
def browse_template(self, sort, category=True):
# Templates {{{
def browse_template(self, sort, category=True, initial_search=''):
def generate():
scn = 'calibre_browse_server_sort_'
if not hasattr(self, '__browse_template__') or \
self.opts.develop:
self.__browse_template__ = \
P('content_server/browse/browse.html', data=True).decode('utf-8')
if category:
sort_opts = [('rating', _('Average rating')), ('name',
_('Name')), ('popularity', _('Popularity'))]
scn += 'category'
else:
scn += 'list'
fm = self.db.field_metadata
sort_opts, added = [], set([])
for x in fm.sortable_field_keys():
n = fm[x]['name']
if n not in added:
added.add(n)
sort_opts.append((x, n))
ans = self.__browse_template__
scn = 'calibre_browse_server_sort_'
ans = P('content_server/browse/browse.html',
data=True).decode('utf-8')
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
ans = ans.replace('{sort_cookie_name}', scn)
opts = ['<option %svalue="%s">%s</option>' % (
'selected="selected" ' if k==sort else '',
xml(k), xml(n), ) for k, n in
sorted(sort_opts, key=operator.itemgetter(1))]
ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts))
lp = self.db.library_path
if isbytestring(lp):
lp = force_unicode(lp, filesystem_encoding)
if isinstance(ans, unicode):
ans = ans.encode('utf-8')
ans = ans.replace('{library_name}', xml(os.path.basename(lp)))
ans = ans.replace('{library_path}', xml(lp, True))
return ans
if category:
sort_opts = [('rating', _('Average rating')), ('name',
_('Name')), ('popularity', _('Popularity'))]
scn += 'category'
else:
scn += 'list'
fm = self.db.field_metadata
sort_opts, added = [], set([])
displayed_custom_fields = custom_fields_to_display(self.db)
for x in fm.sortable_field_keys():
if x in ('ondevice', 'formats', 'sort'):
continue
if fm[x]['is_custom'] and x not in displayed_custom_fields:
continue
if x == 'comments' or fm[x]['datatype'] == 'comments':
continue
n = fm[x]['name']
if n not in added:
added.add(n)
sort_opts.append((x, n))
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
ans = ans.replace('{sort_cookie_name}', scn)
opts = ['<option %svalue="%s">%s</option>' % (
'selected="selected" ' if k==sort else '',
xml(k), xml(n), ) for k, n in
sorted(sort_opts, key=operator.itemgetter(1)) if k and n]
ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts))
lp = self.db.library_path
if isbytestring(lp):
lp = force_unicode(lp, filesystem_encoding)
if isinstance(ans, unicode):
ans = ans.encode('utf-8')
ans = ans.replace('{library_name}', xml(os.path.basename(lp)))
ans = ans.replace('{library_path}', xml(lp, True))
ans = ans.replace('{initial_search}', initial_search)
return ans
if self.opts.develop:
return generate()
if not hasattr(self, '__browse_template__'):
self.__browse_template__ = generate()
return self.__browse_template__
@property
def browse_summary_template(self):
if not hasattr(self, '__browse_summary_template__') or \
self.opts.develop:
self.__browse_summary_template__ = \
P('content_server/browse/summary.html', data=True).decode('utf-8')
return self.__browse_summary_template__
@property
def browse_details_template(self):
if not hasattr(self, '__browse_details_template__') or \
self.opts.develop:
self.__browse_details_template__ = \
P('content_server/browse/details.html', data=True).decode('utf-8')
return self.__browse_details_template__
# }}}
# Catalogs {{{
def browse_icon(self, name='blank.png'):
cherrypy.response.headers['Content-Type'] = 'image/png'
cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time)
if not hasattr(self, '__browse_icon_cache__'):
self.__browse_icon_cache__ = {}
if name not in self.__browse_icon_cache__:
try:
data = I(name, data=True)
except:
raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
img = Image()
img.load(data)
width, height = img.size
scaled, width, height = fit_image(width, height, 48, 48)
if scaled:
img.size = (width, height)
self.__browse_icon_cache__[name] = img.export('png')
return self.__browse_icon_cache__[name]
def browse_toplevel(self):
categories = self.categories_cache()
category_meta = self.db.field_metadata
cats = [
(_('Newest'), 'newest'),
(_('Newest'), 'newest', 'forward.png'),
]
def getter(x):
return category_meta[x]['name'].lower()
displayed_custom_fields = custom_fields_to_display(self.db)
for category in sorted(categories,
cmp=lambda x,y: cmp(getter(x), getter(y))):
cmp=lambda x,y: cmp(getter(x), getter(y))):
if len(categories[category]) == 0:
continue
if category == 'formats':
@ -214,10 +313,25 @@ class BrowseServer(object):
meta = category_meta.get(category, None)
if meta is None:
continue
cats.append((meta['name'], category))
cats = ['<li title="{2} {0}">{0}<span>/browse/category/{1}</span></li>'\
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')))
for x, y in cats]
if meta['is_custom'] and category not in displayed_custom_fields:
continue
# get the icon files
if category in category_icon_map:
icon = category_icon_map[category]
elif meta['is_custom']:
icon = category_icon_map[':custom']
elif meta['kind'] == 'user':
icon = category_icon_map[':user']
else:
icon = 'blank.png'
cats.append((meta['name'], category, icon))
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" />'
'<span class="label">{0}</span>'
'<span class="url">/browse/category/{1}</span></li>')
.format(xml(x, True), xml(y), xml(_('Browse books by')),
src='/browse/icon/'+z)
for x, y, z in cats]
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
.format(_('Choose a category to browse by:'), '\n\n'.join(cats))
@ -299,9 +413,9 @@ class BrowseServer(object):
script=script, main=main)
@Endpoint(mimetype='application/json; charset=utf-8')
def browse_category_group(self, category=None, group=None,
category_sort=None):
sort = category_sort
def browse_category_group(self, category=None, group=None, sort=None):
if sort == 'null':
sort = None
if sort not in ('rating', 'name', 'popularity'):
sort = 'name'
categories = self.categories_cache()
@ -329,7 +443,6 @@ class BrowseServer(object):
return json.dumps(entries, ensure_ascii=False)
@Endpoint()
def browse_catalog(self, category=None, category_sort=None):
'Entry point for top-level, categories and sub-categories'
@ -353,7 +466,8 @@ class BrowseServer(object):
sort = 'title'
self.sort(items, 'title', True)
if sort != 'title':
ascending = fm[sort]['datatype'] not in ('rating', 'datetime')
ascending = fm[sort]['datatype'] not in ('rating', 'datetime',
'series')
self.sort(items, sort, ascending)
return sort
@ -365,13 +479,17 @@ class BrowseServer(object):
if category not in categories and category != 'newest':
raise cherrypy.HTTPError(404, 'category not found')
fm = self.db.field_metadata
try:
category_name = self.db.field_metadata[category]['name']
category_name = fm[category]['name']
dt = fm[category]['datatype']
except:
if category != 'newest':
raise
category_name = _('Newest')
dt = None
hide_sort = 'true' if dt == 'series' else 'false'
if category == 'search':
which = unhexlify(cid)
try:
@ -380,38 +498,197 @@ class BrowseServer(object):
raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
elif category == 'newest':
ids = list(self.db.data.iterallids())
hide_sort = 'true'
else:
ids = self.db.get_books_for_category(category, cid)
q = category
if q == 'news':
q = 'tags'
ids = self.db.get_books_for_category(q, cid)
items = [self.db.data._data[x] for x in ids]
if category == 'newest':
list_sort = 'timestamp'
if dt == 'series':
list_sort = category
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids)
return self.browse_template(sort).format(
title=_('Books in') + " " +category_name,
script='booklist();', main=html)
html = render_book_list(ids, suffix=_('in') + ' ' + category_name)
@Endpoint(mimetype='application/json; charset=utf-8', sort_type='list')
def browse_booklist_page(self, ids=None, list_sort=None):
return self.browse_template(sort, category=False).format(
title=_('Books in') + " " +category_name,
script='booklist(%s);'%hide_sort, main=html)
def browse_get_book_args(self, mi, id_):
fmts = self.db.formats(id_, index_is_id=True)
if not fmts:
fmts = ''
fmts = [x.lower() for x in fmts.split(',') if x]
pf = prefs['output_format'].lower()
try:
fmt = pf if pf in fmts else fmts[0]
except:
fmt = None
args = {'id':id_, 'mi':mi,
}
for key in mi.all_field_keys():
val = mi.format_field(key)[1]
if not val:
val = ''
args[key] = xml(val, True)
fname = ascii_filename(args['title']) + ' - ' + ascii_filename(args['authors'])
return args, fmt, fmts, fname
@Endpoint(mimetype='application/json; charset=utf-8')
def browse_booklist_page(self, ids=None, sort=None):
if sort == 'null':
sort = None
if ids is None:
ids = json.dumps('[]')
try:
ids = json.loads(ids)
except:
raise cherrypy.HTTPError(404, 'invalid ids')
summs = []
for id_ in ids:
try:
id_ = int(id_)
mi = self.db.get_metadata(id_, index_is_id=True)
except:
continue
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
args['other_formats'] = ''
if fmts and fmt:
other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
if other_fmts:
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(f, fname, id_, f.upper()) for f in
other_fmts]
ofmts = ', '.join(ofmts)
args['other_formats'] = u'<strong>%s: </strong>' % \
_('Other formats') + ofmts
args['details_href'] = '/browse/details/'+str(id_)
if fmt:
href = '/get/%s/%s_%d.%s'%(
fmt, fname, id_, fmt)
rt = xml(_('Read %s in the %s format')%(args['title'],
fmt.upper()), True)
args['get_button'] = \
'<a href="%s" class="read" title="%s">%s</a>' % \
(xml(href, True), rt, xml(_('Get')))
else:
args['get_button'] = ''
args['comments'] = comments_to_html(mi.comments)
args['stars'] = ''
if mi.rating:
args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0]
if args['tags']:
args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
args['tags']
if args['series']:
args['series'] = args['series']
args['details'] = xml(_('Details'), True)
args['details_tt'] = xml(_('Show book details'), True)
args['permalink'] = xml(_('Permalink'), True)
args['permalink_tt'] = xml(_('A permanent link to this book'), True)
summs.append(self.browse_summary_template.format(**args))
return json.dumps('\n'.join(summs), ensure_ascii=False)
def browse_render_details(self, id_):
try:
mi = self.db.get_metadata(id_, index_is_id=True)
except:
return _('This book has been deleted')
else:
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
args['formats'] = ''
if fmts:
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(fmt, fname, id_, fmt.upper()) for fmt in
fmts]
ofmts = ', '.join(ofmts)
args['formats'] = ofmts
fields, comments = [], []
displayed_custom_fields = custom_fields_to_display(self.db)
for field, m in list(mi.get_all_standard_metadata(False).items()) + \
list(mi.get_all_user_metadata(False).items()):
if m['is_custom'] and field not in displayed_custom_fields:
continue
if m['datatype'] == 'comments' or field == 'comments':
comments.append((m['name'], comments_to_html(mi.get(field,
''))))
continue
if field in ('title', 'formats') or not args.get(field, False) \
or not m['name']:
continue
if m['datatype'] == 'rating':
r = u'<strong>%s: </strong>'%xml(m['name']) + \
render_rating(mi.rating/2.0, prefix=m['name'])[0]
else:
r = u'<strong>%s: </strong>'%xml(m['name']) + \
args[field]
fields.append((m['name'], r))
fields.sort(key=lambda x: x[0].lower())
fields = [u'<div class="field">{0}</div>'.format(f[1]) for f in
fields]
fields = u'<div class="fields">%s</div>'%('\n\n'.join(fields))
comments.sort(key=lambda x: x[0].lower())
comments = [(u'<div class="field"><strong>%s: </strong>'
u'<div class="comment">%s</div></div>') % (xml(c[0]),
c[1]) for c in comments]
comments = u'<div class="comments">%s</div>'%('\n\n'.join(comments))
return self.browse_details_template.format(id=id_,
title=xml(mi.title, True), fields=fields,
formats=args['formats'], comments=comments)
@Endpoint(mimetype='application/json; charset=utf-8')
def browse_details(self, id=None):
try:
id_ = int(id)
except:
raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
ans = self.browse_render_details(id_)
return json.dumps(ans, ensure_ascii=False)
@Endpoint()
def browse_book(self, id=None, category_sort=None):
try:
id_ = int(id)
except:
raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
ans = self.browse_render_details(id_)
return self.browse_template('').format(
title='', script='book();', main=ans)
# }}}
# Search {{{
def browse_search(self, query=None, offset=0, sort=None):
raise NotImplementedError()
# }}}
@Endpoint(sort_type='list')
def browse_search(self, query='', list_sort=None):
if isbytestring(query):
query = query.decode('UTF-8')
ids = self.db.search_getting_ids(query.strip(), self.search_restriction)
items = [self.db.data._data[x] for x in ids]
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids, suffix=_('in search')+': '+query)
return self.browse_template(sort, category=False, initial_search=query).format(
title=_('Matching books'),
script='booklist();', main=html)
# Book {{{
def browse_book(self, uuid=None):
raise NotImplementedError()
# }}}

View File

@ -140,7 +140,7 @@ class ContentServer(object):
updated = self.build_time
else:
with cover as f:
updated = fromtimestamp(os.stat(f.name).st_mtime)
updated = fromtimestamp(os.fstat(f.fileno()).st_mtime)
cover = f.read()
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)

View File

@ -17,7 +17,7 @@ from calibre.library.server import custom_fields_to_display
from calibre.library.server.utils import strftime, format_tag_string
from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import __appname__
from calibre import human_readable
from calibre import human_readable, isbytestring
from calibre.utils.date import utcfromtimestamp
from calibre.utils.filenames import ascii_filename
@ -29,6 +29,8 @@ def CLASS(*args, **kwargs): # class is a reserved word in Python
def build_search_box(num, search, sort, order): # {{{
div = DIV(id='search_box')
form = FORM('Show ', method='get', action='mobile')
form.set('accept-charset', 'UTF-8')
div.append(form)
num_select = SELECT(name='num')
@ -193,6 +195,8 @@ class MobileServer(object):
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
if not search:
search = ''
if isbytestring(search):
search = search.decode('UTF-8')
ids = self.db.search_getting_ids(search.strip(), self.search_restriction)
FM = self.db.FIELD_MAP
items = [r for r in iter(self.db) if r[FM['id']] in ids]

View File

@ -49,6 +49,8 @@ class XMLServer(object):
if not search:
search = ''
if isbytestring(search):
search = search.decode('UTF-8')
ids = self.db.search_getting_ids(search.strip(), self.search_restriction)

View File

@ -24,6 +24,7 @@ Environment variables
* ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking.
* ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`.
* ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code)
* ``CALIBRE_DISABLE_UDISKS`` - Used to disable the use of udisks for mounting/ejecting. Set it to 1 to use calibre-mount-helper instead.
* ``SYSFS_PATH`` - Use if sysfs is mounted somewhere other than /sys
* ``http_proxy`` - Used on linux to specify an HTTP proxy

View File

@ -20,9 +20,9 @@ What formats does |app| support conversion to/from?
|app| supports the conversion of many input formats to many output formats.
It can convert every input format in the following list, to every output format.
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, TCR, TXT
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, SNB, TCR, TXT
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PML, RB, PDF, TCR, TXT
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PML, RB, PDF, SNB, TCR, TXT
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers

View File

@ -122,7 +122,7 @@ The functions available are:
* ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
* ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed.
* ``lookup(field if not empty, field if empty)`` -- like test, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
* ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::
@ -151,7 +151,7 @@ The lookup function lets us do even fancier processing. For example, assume that
To accomplish this, we:
1. Create a composite field (call it AA) containing ``{series}/{series_index} - {title'}``. If the series is not empty, then this template will produce `series/series_index - title`.
2. Create a composite field (call it BB) containing ``{#genre:ifempty(Unknown)}/{author_sort}/{title}``. This template produces `genre/author_sort/title`, where an empty genre is replaced wuth `Unknown`.
3. Set the save template to ``{series:lookup(AA,BB)}``. This template chooses composite field AA if series is not empty, and composite field BB if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty.
3. Set the save template to ``{series:lookup(.,AA,BB)}``. This template chooses composite field AA if series is not empty, and composite field BB if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty.
Templates and Plugboards
------------------------

View File

@ -16,7 +16,7 @@ __builtin__.__dict__['_'] = lambda s: s
# immediately translated to the environment language
__builtin__.__dict__['__'] = lambda s: s
from calibre.constants import iswindows, preferred_encoding, plugins
from calibre.constants import iswindows, preferred_encoding, plugins, isosx
_run_once = False
winutil = winutilerror = None
@ -35,9 +35,17 @@ if not _run_once:
################################################################################
# Convert command line arguments to unicode
enc = preferred_encoding
if isosx:
# Newer versions of OS X seem to use UTF-8
try:
[x.decode('utf-8') for x in sys.argv[1:]]
enc = 'utf-8'
except:
pass
for i in range(1, len(sys.argv)):
if not isinstance(sys.argv[i], unicode):
sys.argv[i] = sys.argv[i].decode(preferred_encoding, 'replace')
sys.argv[i] = sys.argv[i].decode(enc, 'replace')
################################################################################
# Setup resources
@ -120,7 +128,8 @@ if not _run_once:
object.__setattr__(self, 'name', name)
def __getattribute__(self, attr):
if attr == 'name':
if attr in ('name', '__enter__', '__str__', '__unicode__',
'__repr__'):
return object.__getattribute__(self, attr)
fobject = object.__getattribute__(self, 'fobject')
return getattr(fobject, attr)
@ -141,6 +150,10 @@ if not _run_once:
def __unicode__(self):
return repr(self).decode('utf-8')
def __enter__(self):
fobject = object.__getattribute__(self, 'fobject')
fobject.__enter__()
return self
m = mode[0]
random = len(mode) > 1 and mode[1] == '+'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11559
src/calibre/translations/ur.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,11 +22,21 @@ class TemplateFormatter(string.Formatter):
self.book = None
self.kwargs = None
def _lookup(self, val, field_if_set, field_not_set):
if val:
return self.vformat('{'+field_if_set.strip()+'}', [], self.kwargs)
else:
return self.vformat('{'+field_not_set.strip()+'}', [], self.kwargs)
def _lookup(self, val, *args):
if len(args) == 2: # here for backwards compatibility
if val:
return self.vformat('{'+args[0].strip()+'}', [], self.kwargs)
else:
return self.vformat('{'+args[1].strip()+'}', [], self.kwargs)
if (len(args) % 2) != 1:
raise ValueError(_('lookup requires either 2 or an odd number of arguments'))
i = 0
while i < len(args):
if i + 1 >= len(args):
return self.vformat('{' + args[i].strip() + '}', [], self.kwargs)
if re.search(args[i], val):
return self.vformat('{'+args[i+1].strip() + '}', [], self.kwargs)
i += 2
def _test(self, val, value_if_set, value_not_set):
if val:
@ -41,6 +51,8 @@ class TemplateFormatter(string.Formatter):
return value_if_not
def _switch(self, val, *args):
if (len(args) % 2) != 1:
raise ValueError(_('switch requires an odd number of arguments'))
i = 0
while i < len(args):
if i + 1 >= len(args):
@ -73,7 +85,7 @@ class TemplateFormatter(string.Formatter):
'capitalize' : (0, lambda s,x: x.capitalize()),
'contains' : (3, _contains),
'ifempty' : (1, _ifempty),
'lookup' : (2, _lookup),
'lookup' : (-1, _lookup),
're' : (2, _re),
'shorten' : (3, _shorten),
'switch' : (-1, _switch),
@ -129,9 +141,9 @@ class TemplateFormatter(string.Formatter):
(func[0] > 0 and func[0] != len(args)):
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
if func[0] == 0:
val = func[1](self, val)
val = func[1](self, val).strip()
else:
val = func[1](self, val, *args)
val = func[1](self, val, *args).strip()
if val:
val = string.Formatter.format_field(self, val, dispfmt)
if not val:

View File

@ -376,7 +376,8 @@ default_smartypants_attr = "1"
import re
tags_to_skip_regex = re.compile(r"<(/)?(pre|code|kbd|script|math)[^>]*>", re.I)
# style added by Kovid
tags_to_skip_regex = re.compile(r"<(/)?(style|pre|code|kbd|script|math)[^>]*>", re.I)
def verify_installation(request):

View File

@ -110,9 +110,11 @@ class BasicNewsRecipe(Recipe):
#: If True the GUI will ask the user for a username and password
#: to use while downloading
#: @type: boolean
#: If set to "optional" the use of a username and password becomes optional
needs_subscription = False
#:
#: If True the navigation bar is center aligned, otherwise it is left aligned
center_navbar = True
@ -609,7 +611,8 @@ class BasicNewsRecipe(Recipe):
if self.needs_subscription and (\
self.username is None or self.password is None or \
(not self.username and not self.password)):
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)
if self.needs_subscription != 'optional':
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)
self.browser = self.get_browser()
self.image_map, self.image_counter = {}, 1

View File

@ -45,12 +45,17 @@ def serialize_recipe(urn, recipe_class):
return ans
default_author = _('You') if urn.startswith('custom:') else _('Unknown')
ns = attr('needs_subscription', False)
if not ns:
ns = 'no'
if ns is True:
ns = 'yes'
return E.recipe({
'id' : str(urn),
'title' : attr('title', _('Unknown')),
'author' : attr('__author__', default_author),
'language' : attr('language', 'und'),
'needs_subscription' : 'yes' if attr('needs_subscription', False) else 'no',
'needs_subscription' : ns,
'description' : attr('description', '')
})