diff --git a/resources/content_server/browse.css b/resources/content_server/browse.css deleted file mode 100644 index f400f13365..0000000000 --- a/resources/content_server/browse.css +++ /dev/null @@ -1,91 +0,0 @@ -body { - background-color: #fffcf2; - font-family: sans-serif; -} - -#container { - max-width: 1000px; - min-width: 400px; - min-height: 230px; - background-color: #F6F3E9; - margin-left: auto; - margin-right: auto; -} - -#header { - height: 130px; - background: url('/static/logo.png') no-repeat scroll left top #39322b; -} - -#content { - max-width: 1000px; - min-width: 400px; - min-height: 100px; - padding-bottom: 30px; -} - -/* Header {{{ */ -#header .area { - width: 300px; - height: 130px; - position: relative; - margin-left: 230px; - color: white; - font-size: xx-large; - font-family: monospace; - overflow: hidden; -} - -#header .bubble { - position: absolute; - left: 93px; - top: 21px; - width: 135px; - height: 84px; - display: table; -} - -#header .bubble p { - display: table-cell; - vertical-align: middle; - text-align: center; -} - -/* }}} */ - -h2.library_name { - font-family: monospace; - font-size: 50px; - font-weight: normal; - color: white; - margin-left: 250px; - padding-top: 40px; -} - -/* Combobox {{{ */ - -.ui-button { margin-left: -1px; } -.ui-button-icon-only .ui-button-text { padding: 0.35em; } -.ui-autocomplete-input { margin: 0; padding: 0.48em 0 0.47em 0.45em; } - -/* }}} */ - -/* Sort select {{{ */ - -.sort_select { float: left; margin-left: 1em; margin-top: 2ex; font-size: small; } - -/* }}} */ - -/* Search bar {{{ */ - -#search_box { - float: right; - margin-right: 1em; - margin-top: 2ex; - font-size: small; -} - -#search_box .search_input { padding: 0.35em } - -/* }}} */ - diff --git a/resources/content_server/browse.html b/resources/content_server/browse.html deleted file mode 100644 index a71fff9e22..0000000000 --- a/resources/content_server/browse.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - ..:: calibre library ::.. {title} - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - diff --git a/resources/content_server/browse.js b/resources/content_server/browse.js deleted file mode 100644 index b917a1646d..0000000000 --- a/resources/content_server/browse.js +++ /dev/null @@ -1,121 +0,0 @@ - -// Widgets {{{ - -// Combobox {{{ - -(function( $ ) { - $.widget( "ui.combobox", { - _create: function() { - var self = this, - select = this.element.hide(), - selected = select.children( ":selected" ), - value = selected.val() ? selected.text() : ""; - var input = $( "" ) - .insertAfter( select ) - .val( value ) - .autocomplete({ - delay: 0, - minLength: 0, - source: function( request, response ) { - var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" ); - response( select.children( "option" ).map(function() { - var text = $( this ).text(); - if ( this.value && ( !request.term || matcher.test(text) ) ) - return { - label: text.replace( - new RegExp( - "(?![^&;]+;)(?!<[^<>]*)(" + - $.ui.autocomplete.escapeRegex(request.term) + - ")(?![^<>]*>)(?![^&;]+;)", "gi" - ), "$1" ), - value: text, - option: this - }; - }) ); - }, - select: function( event, ui ) { - ui.item.option.selected = true; - self._trigger( "selected", event, { - item: ui.item.option - }); - }, - change: function( event, ui ) { - if ( !ui.item ) { - var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $(this).val() ) + "$", "i" ), - valid = false; - select.children( "option" ).each(function() { - if ( this.value.match( matcher ) ) { - this.selected = valid = true; - return false; - } - }); - if ( !valid ) { - // remove invalid value, as it didn't match anything - $( this ).val( "" ); - select.val( "" ); - return false; - } - } - } - }) - .addClass( "ui-widget ui-widget-content ui-corner-left" ); - - input.data( "autocomplete" )._renderItem = function( ul, item ) { - return $( "
  • " ) - .data( "item.autocomplete", item ) - .append( "" + item.label + "" ) - .appendTo( ul ); - }; - - $( "" ) - .attr( "tabIndex", -1 ) - .attr( "title", "Show All Items" ) - .insertAfter( input ) - .button({ - icons: { - primary: "ui-icon-triangle-1-s" - }, - text: false - }) - .removeClass( "ui-corner-all" ) - .addClass( "ui-corner-right ui-button-icon" ) - .click(function() { - // close if already visible - if ( input.autocomplete( "widget" ).is( ":visible" ) ) { - input.autocomplete( "close" ); - return; - } - - // pass empty string as value to search for, displaying all results - input.autocomplete( "search", "" ); - input.focus(); - }); - } - }); -})( jQuery ); -// }}} - -// }}} - -// Sort {{{ - -function init_sort_combobox() { - $("#sort_combobox").combobox(); -} - -// }}} - - -function init() { - $("#container").corner("30px"); - $("#header").corner("30px"); - - init_sort_combobox(); - - $("#search_box input:submit").button(); -} - -// Top-level feed {{{ -function toplevel() { -} -// }}} diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css new file mode 100644 index 0000000000..92ed4c3ce6 --- /dev/null +++ b/resources/content_server/browse/browse.css @@ -0,0 +1,440 @@ +body { + background-color: #fffcf2; + font-family: sans-serif; + margin: 0 0 0 0; + padding: 0 0 0 0; +} + +#container { + max-width: 1000px; + min-width: 400px; + min-height: 230px; + background-color: #F6F3E9; + margin-left: auto; + margin-right: auto; + -moz-box-shadow: 5px 5px 5px #ccc; + -webkit-box-shadow: 5px 5px 5px #ccc; + box-shadow: 5px 5px 5px #ccc; +} + +#header { + height: 130px; + background: url('/static/logo.png') no-repeat scroll left top #39322b; +} + +#content { + max-width: 1000px; + min-width: 400px; + min-height: 100px; +} + +#main { + padding-left: 0.5em; + padding-right: 0.5em; + +} + +#footer { + font-size: small; + color: #a6a399; + text-align: right; + margin-right: 30px; +} + +/* Header {{{ */ +#header .area { + width: 300px; + height: 130px; + position: relative; + margin-left: 230px; + color: white; + font-size: xx-large; + font-family: monospace; + overflow: hidden; + z-index: 2; +} + +#header .bubble { + position: absolute; + left: 93px; + top: 21px; + width: 135px; + height: 84px; + display: table; +} + +#header .bubble p { + display: table-cell; + vertical-align: middle; + text-align: center; +} + +#header a { + text-decoration: none; + color: white; + cursor: pointer; + white-space: nowrap; + text-shadow: #27211b 2px 2px 2px; +} + +#header a:hover { + background-color: #39a9cf; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + text-shadow: #27211b 1px 1px 1px; + -moz-box-shadow: 5px 5px 5px #222; + -webkit-box-shadow: 5px 5px 5px #222; + box-shadow: 5px 5px 5px #222; + +} + +#nav-container { + position: relative; + height: 130px; + top: -130px; + left: 0%; +} + +ul#primary-nav { + display: block; + margin-right: 60px; + text-align: right; + margin-top: 90px; + line-height: 20px; + cursor: default; + position: relative; + top: -2ex; +} + +ul#primary-nav li { + display: inline; + padding: 0 4px; +} + +ul#primary-nav li a { + padding: 6px; + font-variant: small-caps; + font-weight: bold; + white-space: nowrap; +} + + +#donate { + display: block; + width: 200px; + height: 38px; + overflow: hidden; + position: relative; + top: -260px; + left: 65%; +} + +#calibre-home-link { + position: relative; + top: -298px; + left: 0%; + z-index: 2; + height: 130px; + width: 230px; + cursor: pointer; +} + +h2.library_name { + font-family: monospace; + font-size: 50px; + font-weight: normal; + color: white; + margin-left: 250px; + padding-top: 40px; +} + +/* }}} */ + +/* Sort select {{{ */ + +.sort_select { + float: left; + margin-left: 1em; + margin-top: 2ex; + max-height: 2.75em; + overflow: hidden; +} + +.sort_select label { + font-size: medium; +} + +/* }}} */ + +/* Search bar {{{ */ + +#search_box { + float: right; + margin-right: 1em; + margin-top: 2ex; + max-height: 2.75em; + overflow: hidden; +} + +#search_box .ui-button { + padding: 0.25em; +} + +/* }}} */ + +/* Top level {{{ */ +.toplevel ul { + list-style-type: none; + margin: 0; + padding: 0; + margin-left: auto; + margin-right: auto; + display: block; +} + +.toplevel li { + margin: 0.75em; + padding: 0.75em; + 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; + -moz-box-shadow: 5px 5px 5px #ccc; + -webkit-box-shadow: 5px 5px 5px #ccc; + box-shadow: 5px 5px 5px #ccc; + +} + +.toplevel li span.url { display: none } +.toplevel li span.label { +} + + +/* }}} */ + +/* Category {{{ */ +.category > div.category-container { + width: 100%; + margin-top: 1ex; + margin-bottom: 1ex; + display: table; +} + +.category div.category-item { + display: table-row; +} + +.category div.category-item > div { + padding: 0.75em; + text-align: center; + cursor: pointer; + display: table-cell; +} + +.category div.category-name { font-weight: bold } + +.category div.category-item:hover > div { + background-color: #d6d3c9; + -moz-box-shadow: 5px 5px 5px #ccc; + -webkit-box-shadow: 5px 5px 5px #ccc; + box-shadow: 5px 5px 5px #ccc; + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + +} + +.category div.category-item span.href { display: none } + +#groups span.load_href { display: none } + +#groups h3 { + font-weight: bold; + margin-top: 1ex; + padding-left: 30px; + padding-top: 0.5ex; + padding-bottom: 0.5ex; +} + +#groups h3 span { + font-weight: normal +} + +/* }}} */ + +/* Booklist {{{ */ + +#booklist .page { + 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%; +} + +/* }}} */ + diff --git a/resources/content_server/browse/browse.html b/resources/content_server/browse/browse.html new file mode 100644 index 0000000000..4acc15f3ea --- /dev/null +++ b/resources/content_server/browse/browse.html @@ -0,0 +1,99 @@ + + + + + + ..:: calibre library ::.. {title} + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    +
    + + +
    + +
     
    +
     
    +
     
    + +
    + {main} +
    + +
    +
    +
    + + diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js new file mode 100644 index 0000000000..29b84ac2d7 --- /dev/null +++ b/resources/content_server/browse/browse.js @@ -0,0 +1,289 @@ + +// 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 + options = options || {}; + if (value === null) { + value = ''; + options.expires = -1; + } + var expires = ''; + if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { + var date; + if (typeof options.expires == 'number') { + date = new Date(); + date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); + } else { + date = options.expires; + } + expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE + } + // CAUTION: Needed to parenthesize options.path and options.domain + // in the following expressions, otherwise they evaluate to undefined + // in the packed version for some reason... + var path = options.path ? '; path=' + (options.path) : ''; + var domain = options.domain ? '; domain=' + (options.domain) : ''; + var secure = options.secure ? '; secure' : ''; + document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); + } else { // only name given, get cookie + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +}; + +// }}} + +// Sort {{{ + +function init_sort_combobox() { + $("#sort_combobox").multiselect({ + multiple: false, + header: sort_select_label, + noneSelectedText: sort_select_label, + selectedList: 1, + click: function(event, ui){ + $(this).multiselect("close"); + cookie(sort_cookie_name, ui.value); + window.location.reload(); + } + }); +} + +// }}} + +function init() { + $("#container").corner("30px"); + $("#header").corner("30px"); + $("#calibre-home-link").click(function() { window.location = "http://calibre-ebook.com"; }); + + init_sort_combobox(); + + $("#search_box input:submit").button(); +} + +// 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.url").text(); + window.location = href; + }); + + toplevel_layout(); + $(window).resize(toplevel_layout); + +} +// }}} + +function render_error(msg) { + return '

     Error: '+msg+"

    " +} + +// Category feed {{{ + +function category_clicked() { + var href = $(this).find("span.href").html(); + window.location = href; +} + +function category() { + $(".category .category-item").click(category_clicked); + + $(".category a.navlink").button(); + + $("#groups").accordion({ + collapsible: true, + active: false, + autoHeight: false, + clearStyle: true, + header: "h3", + + change: function(event, ui) { + if (ui.newContent) { + var href = ui.newContent.children("span.load_href").html(); + ui.newContent.children(".loading").show(); + 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", + timeout: 600000, //milliseconds (10 minutes) + error: function(xhr, stat, err) { + this.children(".loaded").html(render_error(stat)); + this.children(".loaded").show(); + this.children(".loading").hide(); + } + }); + } + } + } + }); +} +// }}} + +// 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('LoadingLoading, please wait…'); + 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); + }); +} diff --git a/resources/content_server/browse/details.html b/resources/content_server/browse/details.html new file mode 100644 index 0000000000..59af5c535e --- /dev/null +++ b/resources/content_server/browse/details.html @@ -0,0 +1,10 @@ +
    +
    + Cover of {title} +
    +
    +
    {formats}
    + {fields} + {comments} +
    +
    diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html new file mode 100644 index 0000000000..de175d3b53 --- /dev/null +++ b/resources/content_server/browse/summary.html @@ -0,0 +1,20 @@ +
    +
    + Cover of {title} + {get_button} +
    +
    +
    + {stars} + {series} + {details} + {permalink} +
    +
    {title}
    +
    {authors}
    +
    {comments}
    +
    {tags}
    +
    {other_formats}
    +
    + +
    diff --git a/resources/content_server/index.html b/resources/content_server/index.html index f9f0aff491..cf9c6e6356 100644 --- a/resources/content_server/index.html +++ b/resources/content_server/index.html @@ -15,7 +15,7 @@ ').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")})},stop:function(){d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options; -if(a.css("opacity"))b._opacity=a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!=document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!= -"HTML"){if(!c.axis||c.axis!="x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY=0;h--){var i=c.snapElements[h].left,k=i+c.snapElements[h].width,j=c.snapElements[h].top,l=j+c.snapElements[h].height;if(i-e=j&&f<=l||h>=j&&h<=l||fl)&&(e>= -i&&e<=k||g>=i&&g<=k||ek);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.s 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 Selectable 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/Selectables - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"), -selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("
    ")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX, -c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting", -c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d= -this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.righti||a.bottomb&&a.rightg&&a.bottom *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){this.containerCache={};this.element.addClass("ui-sortable"); -this.refresh();this.floating=this.items.length?/left|right/.test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a==="disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this, -arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&&!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem= -c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset, -{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment(); -if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start", -a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a);return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute"); -if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0],e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a, -c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset();c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]== -document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp();this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate", -null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem): -d(this.domPosition.parent).prepend(this.currentItem);return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")},toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute|| -"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")}, -_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith();if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!= -this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a= -this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable"); -if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h= -0;b--){var c=this.items[b],e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b=this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width= -this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f=d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f}, -update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b= -null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out",a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this)); -this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h-f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])? -g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive", -g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this,this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over= -0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop",a,this._uiHash());for(e=0;e").addClass("ui-autocomplete").appendTo(e(this.options.appendTo||"body",b)[0]).mousedown(function(c){var d=a.menu.element[0]; -c.target===d&&setTimeout(function(){e(document).one("mousedown",function(f){f.target!==a.element[0]&&f.target!==d&&!e.ui.contains(d,f.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,d){d=d.item.data("item.autocomplete");false!==a._trigger("focus",null,{item:d})&&/^key/.test(c.originalEvent.type)&&a.element.val(d.value)},selected:function(c,d){d=d.item.data("item.autocomplete");var f=a.previous;if(a.element[0]!==b.activeElement){a.element.focus(); -a.previous=f}if(false!==a._trigger("select",c,{item:d})){a.term=d.value;a.element.val(d.value)}a.close(c);a.selectedItem=d},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");e.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"); -this.menu.element.remove();e.Widget.prototype.destroy.call(this)},_setOption:function(a,b){e.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(e(b||"body",this.element[0].ownerDocument)[0])},_initSource:function(){var a=this,b,c;if(e.isArray(this.options.source)){b=this.options.source;this.source=function(d,f){f(e.ui.autocomplete.filter(b,d.term))}}else if(typeof this.options.source==="string"){c=this.options.source;this.source= -function(d,f){a.xhr&&a.xhr.abort();a.xhr=e.getJSON(c,d,function(g,i,h){h===a.xhr&&f(g);a.xhr=null})}}else this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(e("").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});e.extend(e.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")}, -filter:function(a,b){var c=new RegExp(e.ui.autocomplete.escapeRegex(b),"i");return e.grep(a,function(d){return c.test(d.label||d.value||d)})}})})(jQuery); -(function(e){e.widget("ui.menu",{_create:function(){var a=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(b){if(e(b.target).closest(".ui-menu-item a").length){b.preventDefault();a.select(b)}});this.refresh()},refresh:function(){var a=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", --1).mouseenter(function(b){a.activate(b,e(this).parent())}).mouseleave(function(){a.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.attr("scrollTop"),f=this.element.height();if(c<0)this.element.attr("scrollTop",d+c);else c>=f&&this.element.attr("scrollTop",d+c-f+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",a,{item:b})}, -deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");this._trigger("blur");this.active=null}},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(this.active){a=this.active[a+"All"](".ui-menu-item").eq(0); -a.length?this.activate(c,a):this.activate(c,this.element.children(b))}else this.activate(c,this.element.children(b))},nextPage:function(a){if(this.hasScroll())if(!this.active||this.last())this.activate(a,this.element.children(":first"));else{var b=this.active.offset().top,c=this.element.height(),d=this.element.children("li").filter(function(){var f=e(this).offset().top-b-c+e(this).height();return f<10&&f>-10});d.length||(d=this.element.children(":last"));this.activate(a,d)}else this.activate(a,this.element.children(!this.active|| -this.last()?":first":":last"))},previousPage:function(a){if(this.hasScroll())if(!this.active||this.first())this.activate(a,this.element.children(":last"));else{var b=this.active.offset().top,c=this.element.height();result=this.element.children("li").filter(function(){var d=e(this).offset().top-b+c-e(this).height();return d<10&&d>-10});result.length||(result=this.element.children(":first"));this.activate(a,result)}else this.activate(a,this.element.children(!this.active||this.first()?":last":":first"))}, -hasScroll:function(){return this.element.height()").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");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("
    ");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("").appendTo(this.element).addClass("ui-slider-handle"); -if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur(); -else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e= -false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h=== -a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); -this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a, -g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= -this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= -this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); -c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;fthis._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= -this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f- -g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"}, -b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.5"})})(jQuery); -;/* - * jQuery UI Tabs 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/Tabs - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
    ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
  • #{label}
  • "},_create:function(){this._tabify(true)},_setOption:function(a,e){if(a=="selected")this.options.collapsible&& -e==this.options.selected||this.select(e);else{this.options[a]=e;this._tabify()}},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var a=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[a].concat(d.makeArray(arguments)))},_ui:function(a,e){return{tab:a,panel:e,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var a= -d(this);a.html(a.data("label.tabs")).removeData("label.tabs")})},_tabify:function(a){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var b=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| -(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))b.panels=b.panels.add(b._sanitizeSelector(i));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=b._tabId(f);f.href="#"+i;f=d("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(b.panels[g-1]||b.list);f.data("destroy.tabs",true)}b.panels=b.panels.add(f)}else c.disabled.push(g)});if(a){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); -this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(b._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= -this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return b.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); -if(c.selected>=0&&this.anchors.length){this.panels.eq(c.selected).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");b.element.queue("tabs",function(){b._trigger("show",null,b._ui(b.anchors[c.selected],b.panels[c.selected]))});this.load(c.selected)}d(window).bind("unload",function(){b.lis.add(b.anchors).unbind(".tabs");b.lis=b.anchors=b.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"));this.element[c.collapsible?"addClass": -"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);a=0;for(var j;j=this.lis[a];a++)d(j)[d.inArray(a,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+g)};this.lis.bind("mouseover.tabs", -function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal",function(){e(f,o);b._trigger("show", -null,b._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");b._trigger("show",null,b._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);b.element.dequeue("tabs")})}:function(g,f){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");b.element.dequeue("tabs")};this.anchors.bind(c.event+".tabs", -function(){var g=this,f=d(g).closest("li"),i=b.panels.filter(":not(.ui-tabs-hide)"),l=d(b._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||b.panels.filter(":animated").length||b._trigger("select",null,b._ui(this,l[0]))===false){this.blur();return false}c.selected=b.anchors.index(this);b.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected=-1;c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs", -function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this));this.blur();return false}c.cookie&&b._cookie(c.selected,c.cookie);if(l.length){i.length&&b.element.queue("tabs",function(){s(g,i)});b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier.";d.browser.msie&&this.blur()});this.anchors.bind("click.tabs", -function(){return false})},_getIndex:function(a){if(typeof a=="string")a=this.anchors.index(this.anchors.filter("[href$="+a+"]"));return a},destroy:function(){var a=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e=d.data(this,"href.tabs");if(e)this.href= -e;var b=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){b.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});a.cookie&&this._cookie(null,a.cookie);return this},add:function(a,e,b){if(b===p)b=this.anchors.length; -var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,a).replace(/#\{label\}/g,e));a=!a.indexOf("#")?a.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=d("#"+a);j.length||(j=d(h.panelTemplate).attr("id",a).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(b>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[b]); -j.insertBefore(this.panels[b])}h.disabled=d.map(h.disabled,function(k){return k>=b?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[b],this.panels[b]));return this},remove:function(a){a=this._getIndex(a);var e=this.options,b=this.lis.eq(a).remove(),c=this.panels.eq(a).remove(); -if(b.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(a+(a+1=a?--h:h});this._tabify();this._trigger("remove",null,this._ui(b.find("a")[0],c[0]));return this},enable:function(a){a=this._getIndex(a);var e=this.options;if(d.inArray(a,e.disabled)!=-1){this.lis.eq(a).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(b){return b!=a});this._trigger("enable",null, -this._ui(this.anchors[a],this.panels[a]));return this}},disable:function(a){a=this._getIndex(a);var e=this.options;if(a!=e.selected){this.lis.eq(a).addClass("ui-state-disabled");e.disabled.push(a);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))}return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this}, -load:function(a){a=this._getIndex(a);var e=this,b=this.options,c=this.anchors.eq(a)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(a).addClass("ui-state-processing");if(b.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(b.spinner)}this.xhr=d.ajax(d.extend({},b.ajaxOptions,{url:h,success:function(k,n){d(e._sanitizeSelector(c.hash)).html(k);e._cleanup();b.cache&&d.data(c,"cache.tabs", -true);e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.error(k,n,a,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this},url:function(a, -e){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.5"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(a,e){var b=this,c=this.options,h=b._rotate||(b._rotate=function(j){clearTimeout(b.rotation);b.rotation=setTimeout(function(){var k=c.selected;b.select(++k')}function E(a,b){d.extend(a, -b);for(var c in b)if(b[c]==null||b[c]==G)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.5"}});var y=(new Date).getTime();d.extend(L.prototype,{markerClassName:"hasDatepicker",log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){E(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]= -f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_])/g,"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:d('
    ')}}, -_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&& -b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c=="focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f== -""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker():d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a, -c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b), -true);this._updateDatepicker(b);this._updateAlternate(b)}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+=1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}E(a.settings,e||{});b=b&&b.constructor== -Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]); -d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}}, -_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().removeClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b= -d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().addClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false; -for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target|| -a;if(a.nodeName.toLowerCase()!="input")a=d("input",a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);d.datepicker._curInst&&d.datepicker._curInst!=b&&d.datepicker._curInst.dpDiv.stop(true,true);var c=d.datepicker._get(b,"beforeShow");E(b.settings,c?c.apply(a,[a,b]):{});b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value="";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a); -d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b);c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&& -d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){d.datepicker._datepickerShowing=true;var i=d.datepicker._getBorders(b.dpDiv);b.dpDiv.find("iframe.ui-datepicker-cover").css({left:-i[0],top:-i[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})};b.dpDiv.zIndex(d(a).zIndex()+1);d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f, -h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}},_updateDatepicker:function(a){var b=this,c=d.datepicker._getBorders(a.dpDiv);a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function(){d(this).removeClass("ui-state-hover"); -this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).removeClass("ui-datepicker-prev-hover");this.className.indexOf("ui-datepicker-next")!=-1&&d(this).removeClass("ui-datepicker-next-hover")}).bind("mouseover",function(){if(!b._isDisabledDatepicker(a.inline?a.dpDiv.parent()[0]:a.input[0])){d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");d(this).addClass("ui-state-hover");this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).addClass("ui-datepicker-prev-hover"); -this.className.indexOf("ui-datepicker-next")!=-1&&d(this).addClass("ui-datepicker-next-hover")}}).end().find("."+this._dayOverClass+" a").trigger("mouseover").end();c=this._getNumberOfMonths(a);var e=c[1];e>1?a.dpDiv.addClass("ui-datepicker-multi-"+e).css("width",17*e+"em"):a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");a.dpDiv[(c[0]!=1||c[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"); -a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input.focus()},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(),h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(), -k=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>k&&k>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b=this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1);)a=a[b?"previousSibling":"nextSibling"]; -a=d(a).offset();return[a.left,a.top]},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b);this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();if(a=this._get(b,"onClose"))a.apply(b.input?b.input[0]:null,[b.input?b.input.val(): -"",b]);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&& -!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth; -b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e._selectingMonthYear=false;e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_clickMonthYear:function(a){var b= -this._getInst(d(a)[0]);b.input&&b._selectingMonthYear&&setTimeout(function(){b.input.focus()},0);b._selectingMonthYear=!b._selectingMonthYear},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a= -d(a);this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a, -"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b== -"object"?b.toString():b+"";if(b=="")return null;for(var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff,f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,k=c=-1,l=-1,u=-1,j=false,o=function(p){(p=z+1 --1){k=1;l=u;do{e=this._getDaysInMonth(c,k-1);if(l<=e)break;k++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,k-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=k||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24* -60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames:null)||this._defaults.monthNames;var i=function(o){(o=j+112?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e? -"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),k= -this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay?new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),j=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=j&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a, -"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-k,1)),this._getFormatConfig(a));n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+ -n+"";var r=this._get(a,"nextText");r=!h?r:this.formatDate(r,this._daylightSavingAdjust(new Date(m,g+k,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+r+"":f?"":''+r+"";k=this._get(a,"currentText");r=this._get(a,"gotoCurrent")&&a.currentDay?u:b;k=!h?k:this.formatDate(k,r,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
    '+(c?h:"")+(this._isInRange(a,r)?'":"")+(c?"":h)+"
    ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;k=this._get(a,"showWeek");r=this._get(a,"dayNames");this._get(a,"dayNamesShort");var s=this._get(a,"dayNamesMin"),z=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),w=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var M=this._getDefaultDate(a),I="",C=0;C1)switch(D){case 0:x+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:x+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:x+=" ui-datepicker-group-middle";t="";break}x+='">'}x+='
    '+(/all|left/.test(t)&&C==0?c? -f:n:"")+(/all|right/.test(t)&&C==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,j,o,C>0||D>0,z,v)+'
    ';var A=k?'":"";for(t=0;t<7;t++){var q=(t+h)%7;A+="=5?' class="ui-datepicker-week-end"':"")+'>'+s[q]+""}x+=A+"";A=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, -A);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;A=l?6:Math.ceil((t+A)/7);q=this._daylightSavingAdjust(new Date(m,g,1-t));for(var O=0;O";var P=!k?"":'";for(t=0;t<7;t++){var F=p?p.apply(a.input?a.input[0]:null,[q]):[true,""],B=q.getMonth()!=g,K=B&&!H||!F[0]||j&&qo;P+='";q.setDate(q.getDate()+1);q=this._daylightSavingAdjust(q)}x+=P+""}g++;if(g>11){g=0;m++}x+="
    '+this._get(a,"weekHeader")+"
    '+this._get(a,"calculateWeek")(q)+""+(B&&!w?" ":K?''+q.getDate()+ -"":''+q.getDate()+"")+"
    "+(l?""+(i[0]>0&&D==i[1]-1?'
    ':""):"");N+=x}I+=N}I+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': -"");a._keyEvent=false;return I},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var k=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),j='
    ',o="";if(h||!k)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(j+=o+(h||!(k&&l)?" ":""));if(h||!l)j+=''+c+"";else{g=this._get(a,"yearRange").split(":");var r=(new Date).getFullYear();i=function(s){s=s.match(/c[+-].*/)?c+parseInt(s.substring(1),10):s.match(/[+-].*/)?r+parseInt(s,10):parseInt(s,10);return isNaN(s)?r:s};b=i(g[0]);g=Math.max(b, -i(g[1]||""));b=e?Math.max(b,e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(j+='"}j+=this._get(a,"yearSuffix");if(u)j+=(h||!(k&&l)?" ":"")+o;j+="
    ";return j},_adjustInstDate:function(a,b,c){var e= -a.drawYear+(c=="Y"?b:0),f=a.drawMonth+(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a, -"onChangeMonthYear");if(b)b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a); -c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a, -"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker= -function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b)); -return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.5";window["DP_jQuery_"+y]=d})(jQuery); -;/* - * jQuery UI Progressbar 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/Progressbar - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function(b,c){b.widget("ui.progressbar",{options:{value:0},min:0,max:100,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.max,"aria-valuenow":this._value()});this.valueDiv=b("
    ").appendTo(this.element);this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); -this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===c)return this._value();this._setOption("value",a);return this},_setOption:function(a,d){if(a==="value"){this.options.value=d;this._refreshValue();this._trigger("change")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.max,Math.max(this.min,a))},_refreshValue:function(){var a=this.value();this.valueDiv.toggleClass("ui-corner-right", -a===this.max).width(a+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.5"})})(jQuery); ;/* * jQuery UI Effects 1.8.5 * diff --git a/resources/content_server/star-half.png b/resources/content_server/star-half.png new file mode 100644 index 0000000000..3c19e90a8a Binary files /dev/null and b/resources/content_server/star-half.png differ diff --git a/resources/content_server/star-off.png b/resources/content_server/star-off.png new file mode 100644 index 0000000000..956fa7c637 Binary files /dev/null and b/resources/content_server/star-off.png differ diff --git a/resources/content_server/star-on.png b/resources/content_server/star-on.png new file mode 100644 index 0000000000..975fe7f323 Binary files /dev/null and b/resources/content_server/star-on.png differ diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 48845da920..86921886ad 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -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' diff --git a/resources/images/mimetypes/snb.png b/resources/images/mimetypes/snb.png new file mode 100644 index 0000000000..41b55f4343 Binary files /dev/null and b/resources/images/mimetypes/snb.png differ diff --git a/resources/images/news/frazpc.png b/resources/images/news/frazpc.png new file mode 100644 index 0000000000..525a13faa9 Binary files /dev/null and b/resources/images/news/frazpc.png differ diff --git a/resources/images/news/orsai.png b/resources/images/news/orsai.png new file mode 100644 index 0000000000..e65f02206c Binary files /dev/null and b/resources/images/news/orsai.png differ diff --git a/resources/images/news/rstones.png b/resources/images/news/rstones.png new file mode 100644 index 0000000000..7f6159c13c Binary files /dev/null and b/resources/images/news/rstones.png differ diff --git a/resources/recipes/business_standard.recipe b/resources/recipes/business_standard.recipe index 75b2e0af8f..badca48733 100644 --- a/resources/recipes/business_standard.recipe +++ b/resources/recipes/business_standard.recipe @@ -1,7 +1,5 @@ -#!/usr/bin/env python - __license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' +__copyright__ = '2009-2010, Darko Miletic ' ''' www.business-standard.com ''' @@ -28,30 +26,22 @@ class BusinessStandard(BasicNewsRecipe): ,'publisher' : publisher ,'linearize_tables': True } - - remove_attributes=['style'] - remove_tags = [dict(name=['object','link','script','iframe'])] + keep_only_tags=[dict(attrs={'class':'TableClas'})] + remove_tags = [ + dict(name=['object','link','script','iframe','base','meta']) + ,dict(attrs={'class':'rightDiv2'}) + ,dict(name='table',attrs={'width':'450px'}) + ] + remove_attributes=['width','height'] feeds = [ - (u'News Now' , u'http://feeds.business-standard.com/News-Now.xml' ) - ,(u'Banking & finance' , u'http://feeds.business-standard.com/Banking-Finance-All.xml' ) - ,(u'Companies & Industry', u'http://feeds.business-standard.com/Companies-Industry-All.xml') - ,(u'Economy & Policy' , u'http://feeds.business-standard.com/Economy-Policy-All.xml' ) - ,(u'Tech World' , u'http://feeds.business-standard.com/Tech-World-All.xml' ) - ,(u'Life & Leisure' , u'http://feeds.business-standard.com/Life-Leisure-All.xml' ) - ,(u'Markets & Investing' , u'http://feeds.business-standard.com/Markets-Investing-All.xml' ) - ,(u'Management & Mktg' , u'http://feeds.business-standard.com/Management-Mktg-All.xml' ) - ,(u'Automobiles' , u'http://feeds.business-standard.com/Automobiles.xml' ) - ,(u'Aviation' , u'http://feeds.business-standard.com/Aviation.xml' ) + (u'News Now' , u'http://feeds.business-standard.com/rss/online.xml') + ,(u'Banking & finance' , u'http://feeds.business-standard.com/rss/3_0.xml' ) + ,(u'Companies & Industry', u'http://feeds.business-standard.com/rss/2_0.xml' ) + ,(u'Economy & Policy' , u'http://feeds.business-standard.com/rss/4_0.xml' ) + ,(u'Tech World' , u'http://feeds.business-standard.com/rss/8_0.xml' ) + ,(u'Life & Leisure' , u'http://feeds.business-standard.com/rss/6_0.xml' ) + ,(u'Markets & Investing' , u'http://feeds.business-standard.com/rss/1_0.xml' ) + ,(u'Management & Mktg' , u'http://feeds.business-standard.com/rss/7_0.xml' ) + ,(u'Opinion' , u'http://feeds.business-standard.com/rss/5_0.xml' ) ] - - def print_version(self, url): - autono = url.rpartition('autono=')[2] - tp = 'on' - hk = url.rpartition('bKeyFlag=')[1] - if hk == '': - tp = '' - return 'http://www.business-standard.com/india/printpage.php?autono=' + autono + '&tp=' + tp - - def get_article_url(self, article): - return article.get('guid', None) diff --git a/resources/recipes/el_cultural.recipe b/resources/recipes/el_cultural.recipe new file mode 100644 index 0000000000..124343398b --- /dev/null +++ b/resources/recipes/el_cultural.recipe @@ -0,0 +1,86 @@ +from calibre.web.feeds.recipes import BasicNewsRecipe + +class RevistaElCultural(BasicNewsRecipe): + + title = 'Revista El Cultural' + __author__ = 'Jefferson Frantz' + description = 'Revista de cultura' + timefmt = ' [%d %b, %Y]' + language = 'es' + + no_stylesheets = True + remove_javascript = True + + extra_css = 'h1{ font-family: sans-serif; font-size: large; font-weight: bolder; text-align: justify } h2{ font-family: sans-serif; font-size: small; font-weight: 500; text-align: justify } h3{ font-family: sans-serif; font-size: small; font-weight: 500; text-align: justify } h4{ font-family: sans-serif; font-weight: lighter; font-size: medium; font-style: italic; text-align: justify } .rtsArticuloFirma{ font-family: sans-serif; font-size: small; text-align: justify } .column span-13 last{ font-family: sans-serif; font-size: medium; text-align: justify } .rtsImgArticulo{font-family: serif; font-size: small; color: #000000; text-align: justify}' + + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + + return soup + + keep_only_tags = [dict(name='div', attrs={'class':['column span-13 last']}),dict(name='div', attrs={'class':['rtsImgArticulo']})] + + remove_tags = [ + dict(name=['object','link','script','ul']) + ,dict(name='div', attrs={'class':['rtsRating']}) + + ] + + + #TO GET ARTICLES IN SECTION + def ec_parse_section(self, url, titleSection): + print 'Section: '+ titleSection + soup = self.index_to_soup(url) + div = soup.find(attrs={'id':'gallery'}) + current_articles = [] + + for a in div.findAllNext('a', href=True): + if a is None: + continue + title = self.tag_to_string(a) + + url = a.get('href', False) + if not url or not title: + continue + + if not url.startswith('/version_papel/'+titleSection+'/'): + if len(current_articles) > 0 and not url.startswith('/secciones/'): + break + continue + + if url.startswith('/version_papel/'+titleSection+'/'): + url = 'http://www.elcultural.es'+url + + self.log('\t\tFound article:', title[0:title.find("|")-1]) + self.log('\t\t\t', url) + current_articles.append({'title': title[0:title.find("|")-1], 'url':url, + 'description':'', 'date':''}) + + return current_articles + + + # To GET SECTIONS + def parse_index(self): + feeds = [] + for title, url in [ + ('LETRAS', + 'http://www.elcultural.es/pdf_sumario/cultural/Sumario_El_Cultural_en_PDF'), + ('ARTE', + 'http://www.elcultural.es/pdf_sumario/cultural/Sumario_El_Cultural_en_PDF'), + ('CINE', + 'http://www.elcultural.es/pdf_sumario/cultural/Sumario_El_Cultural_en_PDF'), + ('CIENCIA', + 'http://www.elcultural.es/pdf_sumario/cultural/Sumario_El_Cultural_en_PDF'), +## ('OPINION', +## 'http://www.elcultural.es/pdf_sumario/cultural/Sumario_El_Cultural_en_PDF'), + ('ESCENARIOS', + 'http://www.elcultural.es/pdf_sumario/cultural/Sumario_El_Cultural_en_PDF'), + ]: + articles = self.ec_parse_section(url,title) + if articles: + feeds.append((title, articles)) + + + return feeds diff --git a/resources/recipes/el_mercurio_chile.recipe b/resources/recipes/el_mercurio_chile.recipe index e08a21d33a..a8371f5af9 100644 --- a/resources/recipes/el_mercurio_chile.recipe +++ b/resources/recipes/el_mercurio_chile.recipe @@ -1,7 +1,5 @@ -#!/usr/bin/env python - __license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' +__copyright__ = '2009-2010, Darko Miletic ' ''' emol.com ''' @@ -19,43 +17,34 @@ class ElMercurio(BasicNewsRecipe): no_stylesheets = True use_embedded_content = False encoding = 'cp1252' - cover_url = 'http://www.emol.com/especiales/logo_emol/logo_emol.gif' + masthead_url = 'http://www.emol.com/especiales/logo_emol/logo_emol.gif' remove_javascript = True use_embedded_content = False + language = 'es' + - html2lrf_options = [ - '--comment', description - , '--category', category - , '--publisher', publisher - ] - - html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' - - keep_only_tags = [ - dict(name='div', attrs={'class':'despliegue-txt_750px'}) - ,dict(name='div', attrs={'id':'div_cuerpo_participa'}) - ] - - remove_tags = [ - dict(name='div', attrs={'class':'contenedor_despliegue-col-left300'}) - ,dict(name='div', attrs={'id':['div_centro_dn_opc','div_cabezera','div_secciones','div_contenidos','div_pie','nav']}) - ] + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + keep_only_tags = [dict(name='div', attrs={'id':['cont_iz_titulobajada','cont_iz_creditos_1_a','cont_iz_cuerpo']})] + remove_tags = [dict(name='div', attrs={'id':'cont_iz_cuerpo_relacionados'})] + remove_attributes = ['height','width'] + feeds = [ - (u'Noticias de ultima hora', u'http://www.emol.com/rss20/rss.asp?canal=0') - ,(u'Nacional', u'http://www.emol.com/rss20/rss.asp?canal=1') - ,(u'Mundo', u'http://www.emol.com/rss20/rss.asp?canal=2') - ,(u'Deportes', u'http://www.emol.com/rss20/rss.asp?canal=4') - ,(u'Magazine', u'http://www.emol.com/rss20/rss.asp?canal=6') - ,(u'Tecnologia', u'http://www.emol.com/rss20/rss.asp?canal=5') - ,(u'La Musica', u'http://www.emol.com/rss20/rss.asp?canal=7') + (u'Noticias de ultima hora', u'http://rss.emol.com/rss.asp?canal=0') + ,(u'Nacional', u'http://rss.emol.com/rss.asp?canal=1') + ,(u'Mundo', u'http://rss.emol.com/rss.asp?canal=2') + ,(u'Deportes', u'http://rss.emol.com/rss.asp?canal=4') + ,(u'Magazine', u'http://rss.emol.com/rss.asp?canal=6') + ,(u'Tecnologia', u'http://rss.emol.com/rss.asp?canal=5') ] def preprocess_html(self, soup): - mtag = '' - soup.head.insert(0,mtag) for item in soup.findAll(style=True): del item['style'] return soup - language = 'es' diff --git a/resources/recipes/financial_times_uk.recipe b/resources/recipes/financial_times_uk.recipe new file mode 100644 index 0000000000..152e6a9f59 --- /dev/null +++ b/resources/recipes/financial_times_uk.recipe @@ -0,0 +1,74 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +ft.com +''' +from calibre import strftime +from calibre.web.feeds.news import BasicNewsRecipe + +class FinancialTimes(BasicNewsRecipe): + title = u'Financial Times - UK printed edition' + __author__ = 'Darko Miletic' + description = 'Financial world news' + oldest_article = 2 + language = 'en_GB' + max_articles_per_feed = 250 + no_stylesheets = True + use_embedded_content = False + needs_subscription = True + encoding = 'utf8' + simultaneous_downloads= 1 + delay = 1 + LOGIN = 'https://registration.ft.com/registration/barrier/login' + INDEX = 'http://www.ft.com/uk-edition' + PREFIX = 'http://www.ft.com' + + def get_browser(self): + br = BasicNewsRecipe.get_browser() + if self.username is not None and self.password is not None: + br.open(self.LOGIN) + br.select_form(name='loginForm') + br['username'] = self.username + br['password'] = self.password + br.submit() + return br + + keep_only_tags = [ dict(name='div', attrs={'id':'cont'}) ] + remove_tags_after = dict(name='p', attrs={'class':'copyright'}) + remove_tags = [ + dict(name='div', attrs={'id':'floating-con'}) + ,dict(name=['meta','iframe','base','object','embed','link']) + ] + remove_attributes = ['width','height','lang'] + + extra_css = """ + body{font-family:Arial,Helvetica,sans-serif;} + h2{font-size:large;} + .ft-story-header{font-size:xx-small;} + .ft-story-body{font-size:small;} + a{color:#003399;} + .container{font-size:x-small;} + h3{font-size:x-small;color:#003399;} + .copyright{font-size: x-small} + """ + + def parse_index(self): + articles = [] + soup = self.index_to_soup(self.INDEX) + wide = soup.find('div',attrs={'class':'wide'}) + if wide: + for item in wide.findAll('a',href=True): + url = self.PREFIX + item['href'] + title = self.tag_to_string(item) + date = strftime(self.timefmt) + articles.append({ + 'title' :title + ,'date' :date + ,'url' :url + ,'description':'' + }) + return [('FT UK edition',articles)] + + def preprocess_html(self, soup): + return self.adeify_images(soup) + diff --git a/resources/recipes/frazpc.recipe b/resources/recipes/frazpc.recipe new file mode 100644 index 0000000000..56e45076ac --- /dev/null +++ b/resources/recipes/frazpc.recipe @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2010, Tomasz Dlugosz ' +''' +frazpc.pl +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re +class FrazPC(BasicNewsRecipe): + title = u'frazpc.pl' + publisher = u'frazpc.pl' + description = u'Tw\xf3j Vortal Technologiczny' + language = 'pl' + __author__ = u'Tomasz D\u0142ugosz' + oldest_article = 7 + max_articles_per_feed = 100 + use_embedded_content = False + no_stylesheets = True + + feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ] + + keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})] + + remove_tags = [dict(name='p', attrs={'class':'gray tagsP fs11'})] + + preprocess_regexps = [ + (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in + [(r'
    (Skomentuj|Komentarz(e)?\([0-9]*\))  \|', lambda match: '')] + ] + + remove_attributes = [ 'width', 'height' ] diff --git a/resources/recipes/globe_and_mail.recipe b/resources/recipes/globe_and_mail.recipe index b2a9915250..b6e6b5c25b 100644 --- a/resources/recipes/globe_and_mail.recipe +++ b/resources/recipes/globe_and_mail.recipe @@ -26,31 +26,12 @@ class GlobeAndMail(BasicNewsRecipe): #credit {margin-top:0px;} .tag {font-size: 22pt;}''' description = 'Canada\'s national newspaper' - remove_tags_before = dict(id="article-top") - remove_tags = [ - {'id':['util', 'article-tabs', 'comments', 'article-relations', - 'gallery-controls', 'video', 'galleryLoading','deck','header', - 'toolsBottom'] }, - {'class':['credit','inline-img-caption','tab-pointer'] }, - dict(name='div', attrs={'id':['lead-photo', 'most-popular-story']}), - dict(name='div', attrs={'class':'right'}), - dict(name='div', attrs={'id':'footer'}), - dict(name='div', attrs={'id':'beta-msg'}), - dict(name='img', attrs={'class':'headshot'}), - dict(name='div', attrs={'class':'brand'}), - dict(name='div', attrs={'id':'nav-wrap'}), - dict(name='div', attrs={'id':'featureTopics'}), - dict(name='div', attrs={'id':'videoNav'}), - dict(name='div', attrs={'id':'blog-header'}), - dict(name='div', attrs={'id':'right-rail'}), - dict(name='div', attrs={'id':'group-footer-container'}), - dict(name=['iframe', 'style']) - ] - remove_attributes = ['style'] - remove_tags_after = [{'id':['article-content']}, - {'class':['pull','inline-img'] }, - dict(name='img', attrs={'class':'inline-media-embed'}), - ] + keep_only_tags = [dict(name='article')] + remove_tags = [dict(name='aside'), + dict(name='footer'), + dict(name='div', attrs={'class':(lambda x: isinstance(x, (str,unicode)) and 'articlecommentcountholder' in x.split(' '))}), + dict(name='ul', attrs={'class':(lambda x: isinstance(x, (str,unicode)) and 'articletoolbar' in x.split(' '))}), + ] feeds = [ (u'Latest headlines', u'http://www.theglobeandmail.com/?service=rss'), (u'Top stories', u'http://www.theglobeandmail.com/?service=rss&feed=topstories'), diff --git a/resources/recipes/malaysian_mirror.recipe b/resources/recipes/malaysian_mirror.recipe new file mode 100644 index 0000000000..e61538431a --- /dev/null +++ b/resources/recipes/malaysian_mirror.recipe @@ -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 + diff --git a/resources/recipes/miami_herald.recipe b/resources/recipes/miami_herald.recipe index 4500b02097..bdff88b375 100644 --- a/resources/recipes/miami_herald.recipe +++ b/resources/recipes/miami_herald.recipe @@ -1,6 +1,6 @@ __license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' +__copyright__ = '2009-2010, Darko Miletic ' ''' miamiherald.com ''' @@ -15,13 +15,11 @@ class TheMiamiHerald(BasicNewsRecipe): max_articles_per_feed = 100 publisher = u'The Miami Herald' category = u'miami herald, weather, dolphins, news, miami news, local news, miamiherald, miami newspaper, miamiherald.com, miami, the miami herald, broward, miami-dade' - language = 'en' - + language = 'en' no_stylesheets = True use_embedded_content = False encoding = 'cp1252' remove_javascript = True - extra_css = ''' h1{font-family:Arial,Helvetica,sans-serif; font-size:large; color:#1A272F; } .subheadline{font-family:Arial,Helvetica,sans-serif; font-size:30%; color: #666666;} @@ -33,50 +31,35 @@ class TheMiamiHerald(BasicNewsRecipe): .imageCaption{font-family:Arial,Helvetica,sans-serif; font-size:30%; color:#666666; } ''' - keep_only_tags = [dict(name='div', attrs={'id':['storyBody','storyPhotoContentArea']}), - ] + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + keep_only_tags = [dict(name='div', attrs={'id':'wide'}),] - remove_tags = [dict(name=['object','link','embed']), - dict(name='div', attrs={'class':["imageBuyButton","shareLinksArea","storyTools","spill_navigation pagination","circPromoArea","storyTools_footer","storyYahooContentMatch"]}) , - dict(name='div', attrs={'id':["pluck","mlt","storyAssets"]}) ] + remove_tags = [dict(name=['object','link','embed','iframe','meta'])] feeds = [ - (u'Breaking News' , u'http://www.miamiherald.com/416/index.xml' ) - ,(u'Miami-Dade' , u'http://www.miamiherald.com/460/index.xml' ) - ,(u'Broward' , u'http://www.miamiherald.com/467/index.xml' ) - ,(u'Florida Keys' , u'http://www.miamiherald.com/505/index.xml' ) - ,(u'Florida' , u'http://www.miamiherald.com/569/index.xml' ) - ,(u'Nation' , u'http://www.miamiherald.com/509/index.xml' ) - ,(u'World' , u'http://www.miamiherald.com/578/index.xml' ) - ,(u'Americas' , u'http://www.miamiherald.com/579/index.xml' ) - ,(u'Cuba' , u'http://www.miamiherald.com/581/index.xml' ) - ,(u'Haiti' , u'http://www.miamiherald.com/582/index.xml' ) - ,(u'Politics' , u'http://www.miamiherald.com/515/index.xml' ) - ,(u'Education' , u'http://www.miamiherald.com/295/index.xml' ) - ,(u'Environment' , u'http://www.miamiherald.com/573/index.xml' ) + (u'Breaking News' , u'http://www.miamiherald.com/news/breaking-news/index.xml' ) + ,(u'Miami-Dade' , u'http://www.miamiherald.com/news/miami-dade/index.xml' ) + ,(u'Broward' , u'http://www.miamiherald.com/news/broward/index.xml' ) + ,(u'Florida Keys' , u'http://www.miamiherald.com/news/florida-keys/index.xml' ) + ,(u'Florida' , u'http://www.miamiherald.com/news/florida/index.xml' ) + ,(u'Nation' , u'http://www.miamiherald.com/news/nation/index.xml' ) + ,(u'World' , u'http://www.miamiherald.com/news/world/index.xml' ) + ,(u'Americas' , u'http://www.miamiherald.com/news/americas/index.xml' ) + ,(u'Cuba' , u'http://www.miamiherald.com/news/americas/cuba/index.xml' ) + ,(u'Haiti' , u'http://www.miamiherald.com/news/americas/haiti/index.xml' ) + ,(u'Politics' , u'http://www.miamiherald.com/news/politics/index.xml' ) + ,(u'Education' , u'http://www.miamiherald.com/news/education/index.xml' ) + ,(u'Environment' , u'http://www.miamiherald.com/news/environment/index.xml' ) ] - - - - - def get_article_url(self, article): - ans = article.get('guid', None) - print ans - try: - self.log('Looking for full story link in', ans) - soup = self.index_to_soup(ans) - x = soup.find(text="Full Story") - - if x is not None: - a = x.parent - if a and a.has_key('href'): - ans = 'http://www.miamiherald.com'+a['href'] - self.log('Found full story link', ans) - except: - pass - return ans - - - + def print_version(self, url): + art, sep, rest = url.rpartition('/') + art2, sep2, rest2 = art.rpartition('/') + return art2 + '/v-print/' + rest2 + '/' + rest diff --git a/resources/recipes/novaya_gazeta.recipe b/resources/recipes/novaya_gazeta.recipe new file mode 100644 index 0000000000..50ce83b130 --- /dev/null +++ b/resources/recipes/novaya_gazeta.recipe @@ -0,0 +1,18 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1286819935(BasicNewsRecipe): + title = u'Novaya Gazeta' + __author__ = 'muwa' + oldest_article = 7 + max_articles_per_feed = 100 + no_stylesheets = True + conversion_options = {'linearize_tables' : True} + remove_attributes = ['style'] + language = 'ru' + + feeds = [(u'Articles', u'http://www.novayagazeta.ru/rss_number.xml')] + + + def print_version(self, url): + return url + '?print=true' + diff --git a/resources/recipes/orsai.recipe b/resources/recipes/orsai.recipe new file mode 100644 index 0000000000..2d9659b89b --- /dev/null +++ b/resources/recipes/orsai.recipe @@ -0,0 +1,37 @@ + +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +orsai.bitacoras.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Orsai(BasicNewsRecipe): + title = 'Orsai' + __author__ = 'Darko Miletic' + language = 'es' + oldest_article = 35 + max_articles_per_feed = 100 + encoding = 'utf-8' + no_stylesheets = True + use_embedded_content = False + publication_type = 'blog' + masthead_url = 'http://orsai.bitacoras.com/wp-content/themes/orsai/images/logo_orsai.png' + + conversion_options = { + 'comment' : 'Blog literario de Hernán Casciari' + , 'tags' : 'blog, Argentina, España, literatura, Casciari' + , 'publisher': 'Editorial Orsai S.L.' + , 'language' : 'es' + } + + keep_only_tags=[dict(attrs={'class':['entry-title','entry-meta','entry-content','commentlist']})] + remove_tags=[dict(name='img',attrs={'class':'avatar avatar-40 photo'})] + feeds = [(u'Articulos', u'http://orsai.bitacoras.com/feed')] + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + return self.adeify_images(soup) + diff --git a/resources/recipes/rstones.recipe b/resources/recipes/rstones.recipe new file mode 100644 index 0000000000..fa09701e15 --- /dev/null +++ b/resources/recipes/rstones.recipe @@ -0,0 +1,82 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__author__ = 'Tony Stegall' +__copyright__ = '2010, Tony Stegall or Tonythebookworm on mobileread.com' +__version__ = 'v1.01' +__date__ = '07, October 2010' +__description__ = 'Rolling Stones Mag' + +''' +http://www.rollingstone.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class RollingStones(BasicNewsRecipe): + __author__ = 'Tony Stegall' + description = 'Rolling Stones Mag' + cover_url = 'http://gallery.celebritypro.com/data/media/648/kid-rock-rolling-stone-cover.jpg' + masthead_url = 'http://origin.myfonts.com/s/ec/cc-200804/Rolling_Stone-logo.gif' + + + title = 'Rolling Stones Mag' + category = 'Music Reviews, Movie Reviews, entertainment news' + + language = 'en' + timefmt = '[%a, %d %b, %Y]' + + oldest_article = 15 + max_articles_per_feed = 25 + use_embedded_content = False + no_stylesheets = True + + remove_javascript = True + ##################################################################################### + # cleanup section # + ##################################################################################### + keep_only_tags = [ + dict(name='div', attrs={'class':['c65l']}), + dict(name='div', attrs={'id':['col1']}), + + + ] + remove_tags = [ + dict(name='div', attrs={'class': ['storyActions upper','storyActions lowerArticleNav']}), + dict(name='div', attrs={'id': ['comments','related']}), + ] + + + feeds = [ + (u'News', u'http://www.rollingstone.com/siteServices/rss/allNews'), + (u'Blogs', u'http://www.rollingstone.com/siteServices/rss/allBlogs'), + (u'Movie Reviews', u'http://www.rollingstone.com/siteServices/rss/movieReviews'), + (u'Album Reviews', u'http://www.rollingstone.com/siteServices/rss/albumReviews'), + (u'Song Reviews', u'http://www.rollingstone.com/siteServices/rss/songReviews'), + + + ] + + + + def get_article_url(self, article): + return article.get('guid', None) + + + def append_page(self, soup, appendtag, position): + ''' + Some are the articles are multipage so the below function + will get the articles that have + ''' + pager = soup.find('li',attrs={'class':'next'}) + if pager: + nexturl = pager.a['href'] + soup2 = self.index_to_soup(nexturl) + texttag = soup2.find('div', attrs={'id':'storyTextContainer'}) + for it in texttag.findAll(style=True): + del it['style'] + newpos = len(texttag.contents) + self.append_page(soup2,texttag,newpos) + texttag.extract() + appendtag.insert(position,texttag) + + diff --git a/resources/recipes/volksrant.recipe b/resources/recipes/volksrant.recipe index 0229bb0376..6f3ec4ce0d 100644 --- a/resources/recipes/volksrant.recipe +++ b/resources/recipes/volksrant.recipe @@ -6,7 +6,19 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' +''' + Modified by Tony Stegall + on 10/10/10 to include function to grab print version of articles +''' + +from datetime import date from calibre.web.feeds.news import BasicNewsRecipe +''' +added by Tony Stegall +''' +####################################################### +from calibre.ptempfile import PersistentTemporaryFile +####################################################### class AdvancedUserRecipe1249039563(BasicNewsRecipe): title = u'De Volkskrant' @@ -16,20 +28,58 @@ class AdvancedUserRecipe1249039563(BasicNewsRecipe): no_stylesheets = True language = 'nl' - keep_only_tags = [dict(name='div', attrs={'id':'leftColumnArticle'}) ] - remove_tags = [ - dict(name='div',attrs={'class':'article_tools'}), - dict(name='div',attrs={'id':'article_tools'}), - dict(name='div',attrs={'class':'articletools'}), - dict(name='div',attrs={'id':'articletools'}), - dict(name='div',attrs={'id':'myOverlay'}), - dict(name='div',attrs={'id':'trackback'}), - dict(name='div',attrs={'id':'googleBanner'}), - dict(name='div',attrs={'id':'article_headlines'}), - ] extra_css = ''' body{font-family:Arial,Helvetica,sans-serif; font-size:small;} h1{font-size:large;} ''' + ''' + Change Log: + Date: 10/10/10 - Modified code to include obfuscated to get the print version + Author: Tony Stegall + ''' + ####################################################################################################### + temp_files = [] + articles_are_obfuscated = True - feeds = [(u'Laatste Nieuws', u'http://volkskrant.nl/rss/laatstenieuws.rss'), (u'Binnenlands nieuws', u'http://volkskrant.nl/rss/nederland.rss'), (u'Buitenlands nieuws', u'http://volkskrant.nl/rss/internationaal.rss'), (u'Economisch nieuws', u'http://volkskrant.nl/rss/economie.rss'), (u'Sportnieuws', u'http://volkskrant.nl/rss/sport.rss'), (u'Kunstnieuws', u'http://volkskrant.nl/rss/kunst.rss'), (u'Wetenschapsnieuws', u'http://feeds.feedburner.com/DeVolkskrantWetenschap'), (u'Technologienieuws', u'http://feeds.feedburner.com/vkmedia')] + def get_obfuscated_article(self, url): + br = self.get_browser() + print 'THE CURRENT URL IS: ', url + br.open(url) + year = date.today().year + + try: + response = br.follow_link(url_regex='.*?(%d)(\\/)(article)(\\/)(print)(\\/)'%year, nr = 0) + html = response.read() + except: + response = br.open(url) + html = response.read() + + self.temp_files.append(PersistentTemporaryFile('_fa.html')) + self.temp_files[-1].write(html) + self.temp_files[-1].close() + return self.temp_files[-1].name + + ############################################################################################################### + + ''' + Change Log: + Date: 10/15/2010 + Feeds updated by Martin Tarenskeen + ''' + + feeds = [ + (u'Laatste Nieuws', u'http://www.volkskrant.nl/rss/laatstenieuws.rss'), + (u'Binnenland', u'http://www.volkskrant.nl/rss/nederland.rss'), + (u'Buitenland', u'http://www.volkskrant.nl/rss/internationaal.rss'), + (u'Economie', u'http://www.volkskrant.nl/rss/economie.rss'), + (u'Sport', u'http://www.volkskrant.nl/rss/sport.rss'), + (u'Cultuur', u'http://www.volkskrant.nl/rss/kunst.rss'), + (u'Gezondheid & Wetenschap', u'http://www.volkskrant.nl/rss/wetenschap.rss'), + (u'Internet & Media', u'http://www.volkskrant.nl/rss/media.rss') ] + + +''' +example for formating +''' +# original url: http://www.volkskrant.nl/vk/nl/2668/Buitenland/article/detail/1031493/2010/10/10/Noord-Korea-ziet-nieuwe-leider.dhtml +# print url : http://www.volkskrant.nl/vk/nl/2668/2010/article/print/detail/1031493/Noord-Korea-ziet-nieuwe-leider.dhtml diff --git a/setup/installer/__init__.py b/setup/installer/__init__.py index f38d175b4c..b976c4d448 100644 --- a/setup/installer/__init__.py +++ b/setup/installer/__init__.py @@ -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() diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 6fda73f785..fe187a1400 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -292,6 +292,17 @@ class RTFMetadataReader(MetadataReaderPlugin): def get_metadata(self, stream, ftype): 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): @@ -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 @@ -434,6 +446,7 @@ from calibre.ebooks.rb.output import RBOutput from calibre.ebooks.rtf.output import RTFOutput from calibre.ebooks.tcr.output import TCROutput from calibre.ebooks.txt.output import TXTOutput +from calibre.ebooks.snb.output import SNBOutput from calibre.customize.profiles import input_profiles, output_profiles @@ -495,6 +508,7 @@ plugins += [ TXTInput, LRFInput, CHMInput, + SNBInput, ] plugins += [ EPUBOutput, @@ -510,6 +524,7 @@ plugins += [ RTFOutput, TCROutput, TXTOutput, + SNBOutput, ] # Order here matters. The first matched device is the one used. plugins += [ diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 1d879f0c5d..4fa53b1cdb 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -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())) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 9465b789ae..9ad3cf3e08 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -13,7 +13,8 @@ from calibre.devices.errors import UserFeedback from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.interface import DevicePlugin from calibre.ebooks.BeautifulSoup import BeautifulSoup -from calibre.ebooks.metadata import authors_to_string, MetaInformation +from calibre.ebooks.metadata import authors_to_string, MetaInformation, \ + title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.epub import set_metadata from calibre.library.server.utils import strftime @@ -96,6 +97,9 @@ class ITUNES(DriverBase): OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') + BACKLOADING_ERROR_MESSAGE = _( + "Cannot copy books directly from iDevice. " + "Drag from iTunes Library to desktop, then add to calibre's Library window.") # Product IDs: # 0x1291 iPod Touch @@ -3128,6 +3132,9 @@ class Book(Metadata): See ebooks.metadata.book.base ''' def __init__(self,title,author): - Metadata.__init__(self, title, authors=[author]) + @property + def title_sorter(self): + return title_sort(self.title) + diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index aee35649d2..75453c74b9 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -39,6 +39,10 @@ class DevicePlugin(Plugin): #: Whether the metadata on books can be set via the GUI. CAN_SET_METADATA = ['title', 'authors', 'collections'] + # Set this to None if the books on the device are files that the GUI can + # access in order to add the books from the device to the library + BACKLOADING_ERROR_MESSAGE = _('Cannot get files from this device') + #: Path separator for paths to books on device path_sep = os.sep diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py new file mode 100644 index 0000000000..ba26c2b56c --- /dev/null +++ b/src/calibre/devices/udisks.py @@ -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 ' +__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) + + diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index a267d18584..462d78b233 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' import os, re, time, sys +from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.devices.mime import mime_type_ext from calibre.devices.interface import BookList as _BookList @@ -54,7 +55,7 @@ class Book(Metadata): def title_sorter(self): doc = '''String to sort the title. If absent, title is returned''' def fget(self): - return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip() + return title_sort(self.title) return property(doc=doc, fget=fget) @dynamic_property @@ -124,7 +125,6 @@ class CollectionsBookList(BookList): collections = {} # This map of sets is used to avoid linear searches when testing for # book equality - collections_lpaths = {} for book in self: # Make sure we can identify this book via the lpath lpath = getattr(book, 'lpath', None) @@ -198,20 +198,22 @@ class CollectionsBookList(BookList): cat_name = category if cat_name not in collections: - collections[cat_name] = [] - collections_lpaths[cat_name] = set() - if lpath in collections_lpaths[cat_name]: - continue - collections_lpaths[cat_name].add(lpath) + collections[cat_name] = {} if is_series: - collections[cat_name].append( - (book, book.get(attr+'_index', sys.maxint))) + if doing_dc: + collections[cat_name][lpath] = \ + (book, book.get('series_index', sys.maxint)) + else: + collections[cat_name][lpath] = \ + (book, book.get(attr+'_index', sys.maxint)) else: - collections[cat_name].append( - (book, book.get('title_sort', 'zzzz'))) + if lpath not in collections[cat_name]: + collections[cat_name][lpath] = \ + (book, book.get('title_sort', 'zzzz')) # Sort collections result = {} - for category, books in collections.items(): + for category, lpaths in collections.items(): + books = lpaths.values() books.sort(cmp=lambda x,y:cmp(x[1], y[1])) result[category] = [x[0] for x in books] return result diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 78d4606e85..6f938cbcbd 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -94,6 +94,9 @@ class Device(DeviceConfig, DevicePlugin): EBOOK_DIR_CARD_B = '' DELETE_EXTS = [] + # USB disk-based devices can see the book files on the device, so can + # copy these back to the library + BACKLOADING_ERROR_MESSAGE = None def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): @@ -527,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] @@ -582,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) @@ -614,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: @@ -729,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: diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 624b277e61..9bdf937dd1 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -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): diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py index 73587edfa4..025e252005 100644 --- a/src/calibre/ebooks/chm/reader.py +++ b/src/calibre/ebooks/chm/reader.py @@ -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 ";" 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 diff --git a/src/calibre/ebooks/comic/input.py b/src/calibre/ebooks/comic/input.py index 23f5906a53..04d097ac67 100755 --- a/src/calibre/ebooks/comic/input.py +++ b/src/calibre/ebooks/comic/input.py @@ -94,7 +94,7 @@ class PageProcessor(list): from calibre.utils.magick import PixelWand for i, wand in enumerate(self.pages): pw = PixelWand() - pw.color = 'white' + pw.color = '#ffffff' wand.set_border_color(pw) if self.rotate: diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py index 07a054eeaa..83cf6ee0ed 100644 --- a/src/calibre/ebooks/metadata/isbndb.py +++ b/src/calibre/ebooks/metadata/isbndb.py @@ -45,7 +45,7 @@ def fetch_metadata(url, max=100, timeout=5.): class ISBNDBMetadata(Metadata): def __init__(self, book): - Metadata.__init__(self, None, []) + Metadata.__init__(self, None) def tostring(e): if not hasattr(e, 'string'): @@ -58,21 +58,21 @@ class ISBNDBMetadata(Metadata): return ans self.isbn = unicode(book.get('isbn13', book.get('isbn'))) - self.title = tostring(book.find('titlelong')) - if not self.title: - self.title = tostring(book.find('title')) - if not self.title: - self.title = _('Unknown') + title = tostring(book.find('titlelong')) + if not title: + title = tostring(book.find('title')) + self.title = title self.title = unicode(self.title).strip() - self.authors = [] + authors = [] au = tostring(book.find('authorstext')) if au: au = au.strip() temp = au.split(',') for au in temp: if not au: continue - self.authors.extend([a.strip() for a in au.split('&')]) - + authors.extend([a.strip() for a in au.split('&')]) + if authors: + self.authors = authors try: self.author_sort = tostring(book.find('authors').find('person')) if self.authors and self.author_sort == self.authors[0]: diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index 87b8d3b535..cbd9db3f04 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -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 diff --git a/src/calibre/ebooks/metadata/snb.py b/src/calibre/ebooks/metadata/snb.py new file mode 100755 index 0000000000..2a330b19e6 --- /dev/null +++ b/src/calibre/ebooks/metadata/snb.py @@ -0,0 +1,47 @@ +'''Read meta information from SNB files''' + +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2010, Li Fanxi ' + +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 diff --git a/src/calibre/ebooks/metadata/toc.py b/src/calibre/ebooks/metadata/toc.py index 8c6f3f6baf..0ed527d26a 100644 --- a/src/calibre/ebooks/metadata/toc.py +++ b/src/calibre/ebooks/metadata/toc.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' -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 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) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index e85098e293..cf80e4abe2 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -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}) diff --git a/src/calibre/ebooks/oeb/transforms/rasterize.py b/src/calibre/ebooks/oeb/transforms/rasterize.py index 1026b625bf..b09037498a 100644 --- a/src/calibre/ebooks/oeb/transforms/rasterize.py +++ b/src/calibre/ebooks/oeb/transforms/rasterize.py @@ -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)) diff --git a/src/calibre/ebooks/snb/__init__.py b/src/calibre/ebooks/snb/__init__.py new file mode 100644 index 0000000000..d83022b362 --- /dev/null +++ b/src/calibre/ebooks/snb/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2010, Li Fanxi ' +__docformat__ = 'restructuredtext en' + +''' +Used for snb output +''' + diff --git a/src/calibre/ebooks/snb/input.py b/src/calibre/ebooks/snb/input.py new file mode 100755 index 0000000000..052db6d059 --- /dev/null +++ b/src/calibre/ebooks/snb/input.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__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'%s\n%s\n' + +def html_encode(s): + return s.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>').replace(u'"', u'"').replace(u"'", u''').replace(u'\n', u'
    ').replace(u' ', u' ') + +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'

    %s

    ' % html_encode(line.text)) + elif line.tag == 'img': + lines.append(u'

    ' % 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 + diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py new file mode 100644 index 0000000000..549ee51446 --- /dev/null +++ b/src/calibre/ebooks/snb/output.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__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); diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py new file mode 100644 index 0000000000..ed5aa45c08 --- /dev/null +++ b/src/calibre/ebooks/snb/snbfile.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__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 " + 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()) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py new file mode 100644 index 0000000000..e3eed5a476 --- /dev/null +++ b/src/calibre/ebooks/snb/snbml.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__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 diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index be1f8f4eaf..d2e7016e6f 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -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']), ] @@ -235,6 +236,10 @@ class AddAction(InterfaceAction): self.gui.refresh_ondevice() def add_books_from_device(self, view, paths=None): + backloading_err = self.gui.device_manager.device.BACKLOADING_ERROR_MESSAGE + if backloading_err is not None: + return error_dialog(self.gui, _('Add to library'), backloading_err, + show=True) if paths is None: rows = view.selectionModel().selectedRows() if not rows or len(rows) == 0: diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 2a81a1500d..c46d77cd06 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -169,7 +169,7 @@ class EditMetadataAction(InterfaceAction): self.gui.tags_view.blockSignals(False) if changed: m = self.gui.library_view.model() - m.resort(reset=False) + m.refresh(reset=False) m.research() self.gui.tags_view.recount() if self.gui.cover_flow: diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index b206bf68a6..7a02cf4429 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -172,7 +172,7 @@ class MetadataWidget(Widget, Ui_Form): if _file: _file = os.path.abspath(_file) if not os.access(_file, os.R_OK): - d = error_dialog(self.window, _('Cannot read'), + d = error_dialog(self.parent(), _('Cannot read'), _('You do not have permission to read the file: ') + _file) d.exec_() return @@ -181,14 +181,14 @@ class MetadataWidget(Widget, Ui_Form): cf = open(_file, "rb") cover = cf.read() except IOError, e: - d = error_dialog(self.window, _('Error reading file'), + d = error_dialog(self.parent(), _('Error reading file'), _("

    There was an error reading from file:
    ") + _file + "


    "+str(e)) d.exec_() if cover: pix = QPixmap() pix.loadFromData(cover) if pix.isNull(): - d = error_dialog(self.window, _('Error reading file'), + d = error_dialog(self.parent(), _('Error reading file'), _file + _(" is not a valid picture")) d.exec_() else: diff --git a/src/calibre/gui2/convert/snb_output.py b/src/calibre/gui2/convert/snb_output.py new file mode 100644 index 0000000000..b3ebfc747f --- /dev/null +++ b/src/calibre/gui2/convert/snb_output.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2009, John Schember ' +__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) diff --git a/src/calibre/gui2/convert/snb_output.ui b/src/calibre/gui2/convert/snb_output.ui new file mode 100644 index 0000000000..a5ff8ce7ef --- /dev/null +++ b/src/calibre/gui2/convert/snb_output.ui @@ -0,0 +1,74 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index bc9f5cf671..e662c6a5cc 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -793,11 +793,17 @@ class DeviceMixin(object): # {{{ self.set_books_in_library(job.result, reset=True) mainlist, cardalist, cardblist = job.result self.memory_view.set_database(mainlist) - self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA, + self.device_manager.device.BACKLOADING_ERROR_MESSAGE + is None) self.card_a_view.set_database(cardalist) - self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA, + self.device_manager.device.BACKLOADING_ERROR_MESSAGE + is None) self.card_b_view.set_database(cardblist) - self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA, + self.device_manager.device.BACKLOADING_ERROR_MESSAGE + is None) self.sync_news() self.sync_catalogs() self.refresh_ondevice() diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 60e24dbceb..0fe537b598 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -7,7 +7,7 @@ 0 0 752 - 715 + 633 @@ -263,7 +263,7 @@ 20 - 00 + 0 @@ -357,13 +357,13 @@ from the value in the box - - Change title to title case - Force the title to be in title case. If both this and swap authors are checked, title and author are swapped before the title case is set + + Change title to title case + @@ -486,15 +486,15 @@ Future conversion of these books will use the default settings. - - Enter the what you are looking for, either plain text or a regular expression, depending on the mode - 100 0 + + Enter the what you are looking for, either plain text or a regular expression, depending on the mode + @@ -656,6 +656,14 @@ nothing should be put between the original text and the inserted text true + + + 0 + 0 + 726 + 334 + + @@ -674,19 +682,6 @@ nothing should be put between the original text and the inserted text - - - - Qt::Vertical - - - - 20 - 0 - - - - @@ -733,14 +728,33 @@ nothing should be put between the original text and the inserted text author_sort rating publisher - tag_editor_button tags + tag_editor_button remove_tags + remove_all_tags series + clear_series autonumber_series + series_numbering_restarts + series_start_number remove_format + remove_conversion_settings swap_title_and_author + change_title_to_title_case button_box + central_widget + search_field + search_mode + search_for + case_sensitive + replace_with + replace_func + destination_field + replace_mode + comma_separated + scrollArea11 + test_text + test_result diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index b39b752ac6..ef1bddca0c 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -434,9 +434,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.pubdate.setDate(QDate(pubdate.year, pubdate.month, pubdate.day)) timestamp = db.timestamp(self.id, index_is_id=True) - self.orig_timestamp = timestamp self.date.setDate(QDate(timestamp.year, timestamp.month, timestamp.day)) + self.orig_date = qt_to_dt(self.date.date()) exts = self.db.formats(row) if exts: @@ -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) @@ -802,7 +805,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.db.set_pubdate(self.id, d, notify=False, commit=False) d = self.date.date() d = qt_to_dt(d) - if d.date() != self.orig_timestamp.date(): + if d != self.orig_date: self.db.set_timestamp(self.id, d, notify=False, commit=False) self.db.commit() diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 30f4a2d8a2..071c5778a8 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -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] diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 57ea04fb75..8f86bf43b8 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -30,6 +30,7 @@ class BooksView(QTableView): # {{{ def __init__(self, parent, modelcls=BooksModel): QTableView.__init__(self, parent) + self.drag_allowed = True self.setDragEnabled(True) self.setDragDropOverwriteMode(False) self.setDragDropMode(self.DragDrop) @@ -505,6 +506,8 @@ class BooksView(QTableView): # {{{ return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): + if not self.drag_allowed: + return if self.drag_start_pos is None: return QTableView.mouseMoveEvent(self, event) @@ -613,7 +616,7 @@ class BooksView(QTableView): # {{{ def close(self): self._model.close() - def set_editable(self, editable): + def set_editable(self, editable, supports_backloading): self._model.set_editable(editable) def connect_to_search_box(self, sb, search_done): @@ -700,5 +703,9 @@ class DeviceBooksView(BooksView): # {{{ error_dialog(self, _('Not allowed'), _('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_() + def set_editable(self, editable, supports_backloading): + self._model.set_editable(editable) + self.drag_allowed = supports_backloading + # }}} diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index d8f59b8db7..6e510cbfa5 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -73,6 +73,8 @@ class SearchBox2(QComboBox): self.normal_background = 'rgb(255, 255, 255, 0%)' self.line_edit = SearchLineEdit(self) self.setLineEdit(self.line_edit) + c = self.line_edit.completer() + c.setCompletionMode(c.PopupCompletion) self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.DirectConnection) self.line_edit.mouse_released.connect(self.mouse_released, diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 88a9220024..6e0aef1b99 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -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 @@ -84,12 +84,14 @@ class TagsView(QTreeView): # {{{ self.setAcceptDrops(True) self.setDragDropMode(self.DropOnly) self.setDropIndicatorShown(True) + self.setAutoExpandDelay(500) def set_database(self, db, tag_match, sort_by): self.hidden_categories = config['tag_browser_hidden_categories'] self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories, - search_restriction=None) + search_restriction=None, + drag_drop_finished=self.drag_drop_finished) self.sort_by = sort_by self.tag_match = tag_match self.db = db @@ -109,103 +111,6 @@ class TagsView(QTreeView): # {{{ def database_changed(self, event, ids): self.refresh_required.emit() - def dragEnterEvent(self, event): - md = event.mimeData() - if md.hasFormat("application/calibre+from_library"): - event.setDropAction(Qt.CopyAction) - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - allowed = False - idx = self.indexAt(event.pos()) - m = self.model() - p = m.parent(idx) - if idx.isValid() and p.isValid(): - item = m.data(p, Qt.UserRole) - fm = self.db.metadata_for_field(item.category_key) - if item.category_key in \ - ('tags', 'series', 'authors', 'rating', 'publisher') or\ - (fm['is_custom'] and \ - fm['datatype'] in ['text', 'rating', 'series']): - allowed = True - if allowed: - event.acceptProposedAction() - else: - event.ignore() - - def dropEvent(self, event): - idx = self.indexAt(event.pos()) - m = self.model() - p = m.parent(idx) - if idx.isValid() and p.isValid(): - item = m.data(p, Qt.UserRole) - if item.type == TagTreeItem.CATEGORY: - fm = self.db.metadata_for_field(item.category_key) - if item.category_key in \ - ('tags', 'series', 'authors', 'rating', 'publisher') or\ - (fm['is_custom'] and \ - fm['datatype'] in ['text', 'rating', 'series']): - child = m.data(idx, Qt.UserRole) - md = event.mimeData() - mime = 'application/calibre+from_library' - ids = list(map(int, str(md.data(mime)).split())) - self.handle_drop(item, child, ids) - event.accept() - return - event.ignore() - - def handle_drop(self, parent, child, ids): - # print 'Dropped ids:', ids, parent.category_key, child.tag.name - key = parent.category_key - if (key == 'authors' and len(ids) >= 5): - if not confirm('

    '+_('Changing the authors for several books can ' - 'take a while. Are you sure?') - +'

    ', 'tag_browser_drop_authors', self): - return - elif len(ids) > 15: - if not confirm('

    '+_('Changing the metadata for that many books ' - 'can take a while. Are you sure?') - +'

    ', 'tag_browser_many_changes', self): - return - - fm = self.db.metadata_for_field(key) - is_multiple = fm['is_multiple'] - val = child.tag.name - for id in ids: - mi = self.db.get_metadata(id, index_is_id=True) - - # Prepare to ignore the author, unless it is changed. Title is - # always ignored -- see the call to set_metadata - set_authors = False - - # Author_sort cannot change explicitly. Changing the author might - # change it. - mi.author_sort = None # Never will change by itself. - - if key == 'authors': - mi.authors = [val] - set_authors=True - elif fm['datatype'] == 'rating': - mi.set(key, len(val) * 2) - elif fm['is_custom'] and fm['datatype'] == 'series': - mi.set(key, val, extra=1.0) - elif is_multiple: - new_val = mi.get(key, []) - if val in new_val: - # Fortunately, only one field can change, so the continue - # won't break anything - continue - new_val.append(val) - mi.set(key, new_val) - else: - mi.set(key, val) - self.db.set_metadata(id, mi, set_title=False, - set_authors=set_authors, commit=False) - self.db.commit() - self.drag_drop_finished.emit(ids) - @property def match_all(self): return self.tag_match and self.tag_match.currentIndex() > 0 @@ -374,7 +279,8 @@ class TagsView(QTreeView): # {{{ try: self._model = TagsModel(self.db, parent=self, hidden_categories=self.hidden_categories, - search_restriction=self.search_restriction) + search_restriction=self.search_restriction, + drag_drop_finished=self.drag_drop_finished) self.setModel(self._model) except: # The DB must be gone. Set the model to None and hope that someone @@ -469,24 +375,20 @@ class TagTreeItem(object): # {{{ class TagsModel(QAbstractItemModel): # {{{ - def __init__(self, db, parent, hidden_categories=None, search_restriction=None): + def __init__(self, db, parent, hidden_categories=None, + search_restriction=None, drag_drop_finished=None): QAbstractItemModel.__init__(self, parent) # 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 self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('minus.png'))] self.db = db @@ -519,6 +421,79 @@ class TagsModel(QAbstractItemModel): # {{{ tag.avg_rating = None TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) + def mimeTypes(self): + return ["application/calibre+from_library"] + + def dropMimeData(self, md, action, row, column, parent): + if not md.hasFormat("application/calibre+from_library") or \ + action != Qt.CopyAction: + return False + idx = parent + if idx.isValid(): + node = self.data(idx, Qt.UserRole) + if node.type == TagTreeItem.TAG: + fm = self.db.metadata_for_field(node.tag.category) + if node.tag.category in \ + ('tags', 'series', 'authors', 'rating', 'publisher') or \ + (fm['is_custom'] and \ + fm['datatype'] in ['text', 'rating', 'series']): + mime = 'application/calibre+from_library' + ids = list(map(int, str(md.data(mime)).split())) + self.handle_drop(node, ids) + return True + return False + + + def handle_drop(self, on_node, ids): + #print 'Dropped ids:', ids, on_node.tag + key = on_node.tag.category + if (key == 'authors' and len(ids) >= 5): + if not confirm('

    '+_('Changing the authors for several books can ' + 'take a while. Are you sure?') + +'

    ', 'tag_browser_drop_authors', self.parent()): + return + elif len(ids) > 15: + if not confirm('

    '+_('Changing the metadata for that many books ' + 'can take a while. Are you sure?') + +'

    ', 'tag_browser_many_changes', self.parent()): + return + + fm = self.db.metadata_for_field(key) + is_multiple = fm['is_multiple'] + val = on_node.tag.name + for id in ids: + mi = self.db.get_metadata(id, index_is_id=True) + + # Prepare to ignore the author, unless it is changed. Title is + # always ignored -- see the call to set_metadata + set_authors = False + + # Author_sort cannot change explicitly. Changing the author might + # change it. + mi.author_sort = None # Never will change by itself. + + if key == 'authors': + mi.authors = [val] + set_authors=True + elif fm['datatype'] == 'rating': + mi.set(key, len(val) * 2) + elif fm['is_custom'] and fm['datatype'] == 'series': + mi.set(key, val, extra=1.0) + elif is_multiple: + new_val = mi.get(key, []) + if val in new_val: + # Fortunately, only one field can change, so the continue + # won't break anything + continue + new_val.append(val) + mi.set(key, new_val) + else: + mi.set(key, val) + self.db.set_metadata(id, mi, set_title=False, + set_authors=set_authors, commit=False) + self.db.commit() + self.drag_drop_finished.emit(ids) + def set_search_restriction(self, s): self.search_restriction = s @@ -650,12 +625,19 @@ class TagsModel(QAbstractItemModel): # {{{ def flags(self, index, *args): ans = Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable - if index.isValid() and self.parent(index).isValid(): - ans |= Qt.ItemIsDropEnabled + if index.isValid(): + node = self.data(index, Qt.UserRole) + if node.type == TagTreeItem.TAG: + fm = self.db.metadata_for_field(node.tag.category) + if node.tag.category in \ + ('tags', 'series', 'authors', 'rating', 'publisher') or \ + (fm['is_custom'] and \ + fm['datatype'] in ['text', 'rating', 'series']): + ans |= Qt.ItemIsDropEnabled return ans def supportedDropActions(self): - return Qt.CopyAction|Qt.MoveAction + return Qt.CopyAction def path_for_index(self, index): ans = [] diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 26fd2cadc9..f0f29a67e6 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -695,6 +695,9 @@ def config(defaults=None): c.add_opt('raise_window', ['--raise-window'], default=False, help=_('If specified, viewer window will try to come to the ' 'front when started.')) + c.add_opt('full_screen', ['--full-screen', '--fullscreen', '-f'], default=False, + help=_('If specified, viewer window will try to open ' + 'full screen when started.')) c.add_opt('remember_window_size', default=False, help=_('Remember last used window size')) c.add_opt('debug_javascript', ['--debug-javascript'], default=False, @@ -726,8 +729,10 @@ def main(args=sys.argv): main.show() if opts.raise_window: main.raise_() - with main: - return app.exec_() + if opts.full_screen: + main.action_full_screen.trigger() + with main: + return app.exec_() return 0 if __name__ == '__main__': diff --git a/src/calibre/library/comments.py b/src/calibre/library/comments.py index 32ae65b31e..670d9f2564 100644 --- a/src/calibre/library/comments.py +++ b/src/calibre/library/comments.py @@ -42,6 +42,8 @@ def comments_to_html(comments): Deprecated HTML returns as HTML via BeautifulSoup() ''' + if not comments: + return u'

    ' if not isinstance(comments, unicode): comments = comments.decode(preferred_encoding, 'replace') diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5bec43ab28..bbfef47977 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -10,6 +10,7 @@ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re from itertools import repeat from math import floor from Queue import Queue +from operator import itemgetter from PyQt4.QtGui import QImage @@ -68,7 +69,7 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile class Tag(object): def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None, - tooltip=None, icon=None): + tooltip=None, icon=None, category=None): self.name = name self.id = id self.count = count @@ -81,9 +82,11 @@ class Tag(object): tooltip = _('%sAverage rating is %3.1f')%(tooltip, self.avg_rating) self.tooltip = tooltip self.icon = icon + self.category = category def __unicode__(self): - return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip) + return u'%s:%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, + self.category, self.tooltip) def __str__(self): return unicode(self).encode('utf-8') @@ -1102,21 +1105,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tooltip = self.custom_column_label_map[label]['name'] datatype = cat['datatype'] + avgr = itemgetter(3) + item_not_zero_func = lambda x: x[2] > 0 if datatype == 'rating': # eliminate the zero ratings line as well as count == 0 item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0) formatter = (lambda x:u'\u2605'*int(x/2)) + avgr = itemgetter(1) elif category == 'authors': - item_not_zero_func = (lambda x: x[2] > 0) # Clean up the authors strings to human-readable form formatter = (lambda x: x.replace('|', ',')) else: - item_not_zero_func = (lambda x: x[2] > 0) formatter = (lambda x:unicode(x)) categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], - avg=r[3], sort=r[4], - icon=icon, tooltip=tooltip) + avg=avgr(r), sort=r[4], icon=icon, + tooltip=tooltip, category=category) for r in data if item_not_zero_func(r)] # Needed for legacy databases that have multiple ratings that @@ -1148,7 +1152,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): WHERE format="%s"'''%fmt, all=False) if count > 0: - categories['formats'].append(Tag(fmt, count=count, icon=icon)) + categories['formats'].append(Tag(fmt, count=count, icon=icon, + category='formats')) if sort == 'popularity': categories['formats'].sort(key=lambda x: x.count, reverse=True) @@ -1194,7 +1199,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if icon_map and 'search' in icon_map: icon = icon_map['search'] for srch in saved_searches().names(): - items.append(Tag(srch, tooltip=saved_searches().lookup(srch), icon=icon)) + items.append(Tag(srch, tooltip=saved_searches().lookup(srch), + icon=icon, category='search')) if len(items): if icon_map is not None: icon_map['search'] = icon_map['search'] diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index b43a6620d0..dbc871026e 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -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, diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 42d3d76dfb..247e6945e6 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -5,12 +5,177 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import operator, os +import operator, os, json +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, suffix=''): # {{{ + pages = [] + num = len(ids) + pos = 0 + delta = 25 + while ids: + page = list(ids[:delta]) + pages.append((page, pos)) + ids = ids[delta:] + pos += len(page) + page_template = u'''\ +
    +
    + + + +
    +
    {2}
    +
    +
    + ''' + rpages = [] + 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')) + '…', + start=pos+1, end=pos+len(pg))) + rpages = u'\n\n'.join(rpages) + + templ = u'''\ +

    {0} {suffix}

    +
    +
    + {navbar} +
    + {pages} +
    + {navbar} +
    +
    + ''' + + navbar = u'''\ + + + + '''.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): + x = x.encode('utf-8') + return x +# }}} + +def render_rating(rating, container='span', prefix=None): # {{{ + if rating < 0.1: + return '', '' + added = 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): + n = rating - added + x = 'half' + if n <= 0.1: + x = 'off' + elif n >= 0.9: + x = 'on' + ans.append( + u'{0}'.format( + rstring, x)) + added += 1 + ans.append(''%container) + return u''.join(ans), rstring + +# }}} + +def get_category_items(category, items, db, datatype): # {{{ + + def item(i): + templ = (u'
    ' + '
    {0}
    {1}
    ' + '
    {2}' + '{3}
    ') + rating, rstring = render_rating(i.avg_rating) + name = xml(i.name) + if datatype == 'rating': + name = xml(_('%d stars')%int(i.avg_rating)) + id_ = i.id + if id_ is None: + id_ = hexlify(force_unicode(name).encode('utf-8')) + id_ = xml(str(id_)) + desc = '' + if i.count > 0: + 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(href), rstring) + + items = list(map(item, items)) + return '\n'.join(['
    '] + items + ['
    ']) + +# }}} + +class Endpoint(object): # {{{ + 'Manage encoding, mime-type, last modified, cookies, etc.' + + def __init__(self, mimetype='text/html; charset=utf-8', sort_type='category'): + self.mimetype = mimetype + self.sort_type = sort_type + self.sort_kwarg = sort_type + '_sort' + self.sort_cookie_name = 'calibre_browse_server_sort_'+self.sort_type + + def __call__(eself, func): + + def do(self, *args, **kwargs): + 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 + updated = self.db.last_modified() + cherrypy.response.headers['Last-Modified'] = \ + self.last_modified(max(updated, self.build_time)) + ans = utf8(ans) + return ans + + do.__name__ = func.__name__ + + return do +# }}} class BrowseServer(object): @@ -19,76 +184,511 @@ class BrowseServer(object): connect('browse', base_href, self.browse_catalog) connect('browse_catalog', base_href+'/category/{category}', self.browse_catalog) - connect('browse_list', base_href+'/list/{query}', self.browse_list) - connect('browse_search', base_href+'/search/{query}', + connect('browse_category_group', + base_href+'/category_group/{category}/{group}', + self.browse_category_group) + connect('browse_matches', + base_href+'/matches/{category}/{cid}', + self.browse_matches) + connect('browse_booklist_page', + base_href+'/booklist_page', + self.browse_booklist_page) + connect('browse_search', base_href+'/search', self.browse_search) - connect('browse_book', base_href+'/book/{uuid}', self.browse_book) - connect('browse_json', base_href+'/json/{query}', self.browse_json) + 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, category=True): - def generate(): - if category: - sort_opts = [('rating', _('Average rating')), ('name', - _('Name')), ('popularity', _('Popularity'))] - else: - fm = self.db.field_metadata - sort_opts = [(x, fm[x]['name']) for x in fm.sortable_field_keys() - if fm[x]['name']] - prefix = 'category' if category else 'book' - ans = P('content_server/browse.html', data=True) - ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':')) - opts = ['' % (prefix, xml(k), - xml(n)) for k, n in - sorted(sort_opts, key=operator.itemgetter(1))] - opts = [''] + opts - ans = ans.replace('{sort_select_options}', '\n\t\t\t'.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 + # Templates {{{ + def browse_template(self, sort, category=True, initial_search=''): + + if not hasattr(self, '__browse_template__') or \ + self.opts.develop: + self.__browse_template__ = \ + P('content_server/browse/browse.html', data=True).decode('utf-8') + + ans = self.__browse_template__ + scn = 'calibre_browse_server_sort_' + + 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 = ['' % ( + '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_catalog(self, category=None): - if category == None: - ans = self.browse_template().format(title='', - script='toplevel();') - else: - raise cherrypy.HTTPError(404, 'Not found') - cherrypy.response.headers['Content-Type'] = 'text/html' + 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', '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))): + if len(categories[category]) == 0: + continue + if category == 'formats': + continue + meta = category_meta.get(category, None) + if meta is None: + continue + 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 = [('
  • {0}' + '{0}' + '/browse/category/{1}
  • ') + .format(xml(x, True), xml(y), xml(_('Browse books by')), + src='/browse/icon/'+z) + for x, y, z in cats] + + main = '

    {0}

      {1}
    '\ + .format(_('Choose a category to browse by:'), '\n\n'.join(cats)) + return self.browse_template('name').format(title='', + script='toplevel();', main=main) + + def browse_sort_categories(self, items, sort): + if sort not in ('rating', 'name', 'popularity'): + sort = 'name' + def sorter(x): + ans = getattr(x, 'sort', x.name) + if hasattr(ans, 'upper'): + ans = ans.upper() + return ans + items.sort(key=sorter) + if sort == 'popularity': + items.sort(key=operator.attrgetter('count'), reverse=True) + elif sort == 'rating': + items.sort(key=operator.attrgetter('avg_rating'), reverse=True) + return sort + + def browse_category(self, category, sort): + categories = self.categories_cache() + if category not in categories: + raise cherrypy.HTTPError(404, 'category not found') + category_meta = self.db.field_metadata + category_name = category_meta[category]['name'] + datatype = category_meta[category]['datatype'] + + + items = categories[category] + sort = self.browse_sort_categories(items, sort) + + script = 'true' + + if len(items) <= self.opts.max_opds_ungrouped_items: + script = 'false' + items = get_category_items(category, items, self.db, datatype) + else: + getter = lambda x: unicode(getattr(x, 'sort', x.name)) + starts = set([]) + for x in items: + val = getter(x) + if not val: + val = u'A' + starts.add(val[0].upper()) + category_groups = OrderedDict() + for x in sorted(starts): + category_groups[x] = len([y for y in items if + getter(y).upper().startswith(x)]) + items = [(u'

    {0} [{2}]

    ' + u'' + u'
    {1}{1}
    ' + u'{3}
    ').format( + xml(s, True), + xml(_('Loading, please wait'))+'…', + unicode(c), + xml(u'/browse/category_group/%s/%s'%(category, s))) + for s, c in category_groups.items()] + items = '\n\n'.join(items) + items = u'
    \n{0}
    '.format(items) + + + + script = 'category(%s);'%script + + main = u''' +
    +

    {0}

    + {2} ↑ + {1} +
    + '''.format( + xml(_('Browsing by')+': ' + category_name), items, + xml(_('Up'), True)) + + return self.browse_template(sort).format(title=category_name, + script=script, main=main) + + @Endpoint(mimetype='application/json; charset=utf-8') + 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() + if category not in categories: + raise cherrypy.HTTPError(404, 'category not found') + + category_meta = self.db.field_metadata + datatype = category_meta[category]['datatype'] + + if not group: + raise cherrypy.HTTPError(404, 'invalid group') + + items = categories[category] + entries = [] + getter = lambda x: unicode(getattr(x, 'sort', x.name)) + for x in items: + val = getter(x) + if not val: + val = u'A' + if val.upper().startswith(group): + entries.append(x) + + sort = self.browse_sort_categories(entries, sort) + entries = get_category_items(category, entries, self.db, datatype) + 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' + if category == None: + ans = self.browse_toplevel() + elif category == 'newest': + raise cherrypy.InternalRedirect('/browse/matches/newest/dummy') + else: + ans = self.browse_category(category, category_sort) + return ans # }}} # Book Lists {{{ - def browse_list(self, query=None): - raise NotImplementedError() + + def browse_sort_book_list(self, items, sort): + fm = self.db.field_metadata + keys = frozenset(fm.sortable_field_keys()) + if sort not in keys: + sort = 'title' + self.sort(items, 'title', True) + if sort != 'title': + ascending = fm[sort]['datatype'] not in ('rating', 'datetime', + 'series') + self.sort(items, sort, ascending) + return sort + + @Endpoint(sort_type='list') + def browse_matches(self, category=None, cid=None, list_sort=None): + if not cid: + raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid) + categories = self.categories_cache() + + if category not in categories and category != 'newest': + raise cherrypy.HTTPError(404, 'category not found') + fm = self.db.field_metadata + try: + 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: + ids = self.search_cache('search:"%s"'%which) + except: + raise cherrypy.HTTPError(404, 'Search: %r not understood'%which) + elif category == 'newest': + ids = list(self.db.data.iterallids()) + hide_sort = 'true' + else: + 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, suffix=_('in') + ' ' + category_name) + + 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'{3}'\ + .format(f, fname, id_, f.upper()) for f in + other_fmts] + ofmts = ', '.join(ofmts) + args['other_formats'] = u'%s: ' % \ + _('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'] = \ + '%s' % \ + (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'%s: '%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'{3}'\ + .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'%s: '%xml(m['name']) + \ + render_rating(mi.rating/2.0, prefix=m['name'])[0] + else: + r = u'%s: '%xml(m['name']) + \ + args[field] + fields.append((m['name'], r)) + + fields.sort(key=lambda x: x[0].lower()) + fields = [u'
    {0}
    '.format(f[1]) for f in + fields] + fields = u'
    %s
    '%('\n\n'.join(fields)) + + comments.sort(key=lambda x: x[0].lower()) + comments = [(u'
    %s: ' + u'
    %s
    ') % (xml(c[0]), + c[1]) for c in comments] + comments = u'
    %s
    '%('\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): - 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() - # }}} - - # JSON {{{ - def browse_json(self, query=None): - raise NotImplementedError() - # }}} diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py index 94e4a1c041..29602a114c 100644 --- a/src/calibre/library/server/cache.py +++ b/src/calibre/library/server/cache.py @@ -29,6 +29,11 @@ class Cache(object): def categories_cache(self, restrict_to=frozenset([])): + base_restriction = self.search_cache('') + if restrict_to: + restrict_to = frozenset(restrict_to).intersection(base_restriction) + else: + restrict_to = base_restriction old = self._category_cache.pop(frozenset(restrict_to), None) if old is None or old[0] <= self.db.last_modified(): categories = self.db.get_categories(ids=restrict_to) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 7139b12d08..d95cd1818c 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -5,18 +5,15 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, os, cStringIO +import re, os import cherrypy -try: - from PIL import Image as PILImage - PILImage -except ImportError: - import Image as PILImage from calibre import fit_image, guess_type from calibre.utils.date import fromtimestamp from calibre.library.caches import SortKeyGenerator +from calibre.utils.magick.draw import save_cover_data_to, Image, \ + thumbnail as generate_thumbnail class CSSortKeyGenerator(SortKeyGenerator): @@ -35,6 +32,7 @@ class ContentServer(object): def add_routes(self, connect): connect('root', '/', self.index) + connect('old', '/old', self.old) connect('get', '/get/{what}/{id}', self.get, conditions=dict(method=["GET", "HEAD"])) connect('static', '/static/{name:.*?}', self.static, @@ -76,8 +74,13 @@ class ContentServer(object): id = int(match.group()) if not self.db.has_id(id): raise cherrypy.HTTPError(400, 'id:%d does not exist in database'%id) - if what == 'thumb': - return self.get_cover(id, thumbnail=True) + if what == 'thumb' or what.startswith('thumb_'): + try: + width, height = map(int, what.split('_')[1:]) + except: + width, height = 60, 80 + return self.get_cover(id, thumbnail=True, thumb_width=width, + thumb_height=height) if what == 'cover': return self.get_cover(id) return self.get_format(id, what) @@ -123,38 +126,43 @@ class ContentServer(object): return self.static('index.html') + def old(self, **kwargs): + return self.static('index.html') + # Actually get content from the database {{{ - def get_cover(self, id, thumbnail=False): - cover = self.db.cover(id, index_is_id=True, as_file=False) - if cover is None: - cover = self.default_cover - cherrypy.response.headers['Content-Type'] = 'image/jpeg' - cherrypy.response.timeout = 3600 - path = getattr(cover, 'name', False) - updated = fromtimestamp(os.stat(path).st_mtime) if path and \ - os.access(path, os.R_OK) else self.build_time - cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) + def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80): try: - f = cStringIO.StringIO(cover) - try: - im = PILImage.open(f) - except IOError: - raise cherrypy.HTTPError(404, 'No valid cover found') - width, height = im.size + cherrypy.response.headers['Content-Type'] = 'image/jpeg' + cherrypy.response.timeout = 3600 + cover = self.db.cover(id, index_is_id=True, as_file=True) + if cover is None: + cover = self.default_cover + updated = self.build_time + else: + with cover as f: + updated = fromtimestamp(os.fstat(f.fileno()).st_mtime) + cover = f.read() + cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) + + if thumbnail: + return generate_thumbnail(cover, + width=thumb_width, height=thumb_height)[-1] + + img = Image() + img.load(cover) + width, height = img.size scaled, width, height = fit_image(width, height, - 60 if thumbnail else self.max_cover_width, - 80 if thumbnail else self.max_cover_height) + thumb_width if thumbnail else self.max_cover_width, + thumb_height if thumbnail else self.max_cover_height) if not scaled: return cover - im = im.resize((int(width), int(height)), PILImage.ANTIALIAS) - of = cStringIO.StringIO() - im.convert('RGB').save(of, 'JPEG') - return of.getvalue() + return save_cover_data_to(img, 'img.jpg', return_data=True, + resize_to=(width, height)) except Exception, err: import traceback cherrypy.log.error('Failed to generate cover:') cherrypy.log.error(traceback.print_exc()) - raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err) + raise cherrypy.HTTPError(404, 'Failed to generate cover: %r'%err) def get_format(self, id, format): format = format.upper() diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py index 856363d7db..b9ca24a823 100644 --- a/src/calibre/library/server/mobile.py +++ b/src/calibre/library/server/mobile.py @@ -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] diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index f1aeb583db..16e7d34cbf 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -18,7 +18,7 @@ from calibre.constants import __appname__ from calibre.ebooks.metadata import fmt_sidx from calibre.library.comments import comments_to_html from calibre.library.server import custom_fields_to_display -from calibre.library.server.utils import format_tag_string +from calibre.library.server.utils import format_tag_string, Offsets from calibre import guess_type from calibre.utils.ordered_dict import OrderedDict @@ -321,26 +321,6 @@ class CategoryGroupFeed(NavFeed): self.root.append(CATALOG_GROUP_ENTRY(item, which, base_href, version, updated)) -class OPDSOffsets(object): - - def __init__(self, offset, delta, total): - if offset < 0: - offset = 0 - if offset >= total: - raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset) - last_allowed_index = total - 1 - last_current_index = offset + delta - 1 - self.offset = offset - self.next_offset = last_current_index + 1 - if self.next_offset > last_allowed_index: - self.next_offset = -1 - self.previous_offset = self.offset - delta - if self.previous_offset < 0: - self.previous_offset = 0 - self.last_offset = last_allowed_index - delta - if self.last_offset < 0: - self.last_offset = 0 - class OPDSServer(object): @@ -374,7 +354,7 @@ class OPDSServer(object): items = [x for x in self.db.data.iterall() if x[idx] in ids] self.sort(items, sort_by, ascending) max_items = self.opts.max_opds_items - offsets = OPDSOffsets(offset, max_items, len(items)) + offsets = Offsets(offset, max_items, len(items)) items = items[offsets.offset:offsets.offset+max_items] updated = self.db.last_modified() cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) @@ -448,7 +428,7 @@ class OPDSServer(object): id_ = 'calibre-category-group-feed:'+category+':'+which max_items = self.opts.max_opds_items - offsets = OPDSOffsets(offset, max_items, len(items)) + offsets = Offsets(offset, max_items, len(items)) items = list(items)[offsets.offset:offsets.offset+max_items] cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) @@ -495,7 +475,7 @@ class OPDSServer(object): if len(items) <= MAX_ITEMS: max_items = self.opts.max_opds_items - offsets = OPDSOffsets(offset, max_items, len(items)) + offsets = Offsets(offset, max_items, len(items)) items = list(items)[offsets.offset:offsets.offset+max_items] ans = CategoryFeed(items, which, id_, updated, version, offsets, page_url, up_url, self.db) @@ -516,7 +496,7 @@ class OPDSServer(object): getattr(y, 'sort', y.name).startswith(x)]) items = [Group(x, y) for x, y in category_groups.items()] max_items = self.opts.max_opds_items - offsets = OPDSOffsets(offset, max_items, len(items)) + offsets = Offsets(offset, max_items, len(items)) items = items[offsets.offset:offsets.offset+max_items] ans = CategoryGroupFeed(items, which, id_, updated, version, offsets, page_url, up_url) diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py index 9a64948a3d..35c92f7ae2 100644 --- a/src/calibre/library/server/utils.py +++ b/src/calibre/library/server/utils.py @@ -13,6 +13,28 @@ from calibre import strftime as _strftime, prints from calibre.utils.date import now as nowf from calibre.utils.config import tweaks +class Offsets(object): + 'Calculate offsets for a paginated view' + + def __init__(self, offset, delta, total): + if offset < 0: + offset = 0 + if offset >= total: + raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset) + last_allowed_index = total - 1 + last_current_index = offset + delta - 1 + self.slice_upper_bound = offset+delta + self.offset = offset + self.next_offset = last_current_index + 1 + if self.next_offset > last_allowed_index: + self.next_offset = -1 + self.previous_offset = self.offset - delta + if self.previous_offset < 0: + self.previous_offset = 0 + self.last_offset = last_allowed_index - delta + if self.last_offset < 0: + self.last_offset = 0 + def expose(func): diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py index 469d2457e7..e99fc2839c 100644 --- a/src/calibre/library/server/xml.py +++ b/src/calibre/library/server/xml.py @@ -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) diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index c35defc0b0..e0f799f572 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -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 diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 3cf171bc1b..220e7ff9e4 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -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 @@ -387,6 +387,12 @@ solve it, look for a corrupted font file on your system, in ~/Library/Fonts or t check for corrupted fonts in OS X is to start the "Font Book" application, select all fonts and then in the File menu, choose "Validate fonts". + +I downloaded the installer, but it is not working? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Best place to ask for more help is in the `forums `_. + My antivirus program claims |app| is a virus/trojan? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -418,3 +424,14 @@ How do I run calibre from my USB stick? A portable version of calibre is available at: `portableapps.com `_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions `. +Why are there so many calibre-parallel processes on my system? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +|app| maintains two separate worker process pools. One is used for adding books/saving to disk and the other for conversions. You can control the number of worker processes via :guilabel:`Preferences->Advanced->Miscellaneous`. So if you set it to 6 that means a maximum of 3 conversions will run simultaneously. And that is why you will see the number of worker processes changes by two when you use the up and down arrows. On windows, you can set the priority that these processes run with. This can be useful on older, single CPU machines, if you find them slowing down to a crawl when conversions are running. + +In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously. + +And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run the GUI thread of the main process or in a separate process. + +Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes. + diff --git a/src/calibre/manual/index.rst b/src/calibre/manual/index.rst index d63b0b71a9..bc8e8a97c2 100644 --- a/src/calibre/manual/index.rst +++ b/src/calibre/manual/index.rst @@ -17,10 +17,10 @@ To get started with more advanced usage, you should read about the :ref:`Graphic You will find the list of :ref:`Frequently Asked Questions ` useful as well. -.. only:: html and online +.. only:: online + + An e-book version of this User Manual is available in `EPUB format `_. - An e-book version of this User Manual is available in `EPUB format `_. Because the User Manual uses advanced formatting, it is only suitable for use with the |app| e-book viewer. - Sections ------------ diff --git a/src/calibre/manual/news.rst b/src/calibre/manual/news.rst index de50fd1c19..88b6dd47bc 100644 --- a/src/calibre/manual/news.rst +++ b/src/calibre/manual/news.rst @@ -295,6 +295,9 @@ To learn more about writing advanced recipes using some of the facilities, avail `Built-in recipes `_ The source code for the built-in recipes that come with |app| + `The calibre recipes forum `_ + Lots of knowledgeable |app| recipe writers hang out here. + API documentation -------------------- diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index b731dfe26e..e1eb876cb7 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -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 ------------------------ diff --git a/src/calibre/startup.py b/src/calibre/startup.py index 1046cd93b3..e384153993 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -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] == '+' diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 5e2cb6535a..76c086cc58 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -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: diff --git a/src/calibre/utils/ipc/launch.py b/src/calibre/utils/ipc/launch.py index 8d3628d69a..a179f356be 100644 --- a/src/calibre/utils/ipc/launch.py +++ b/src/calibre/utils/ipc/launch.py @@ -14,7 +14,10 @@ from calibre.ptempfile import PersistentTemporaryFile, base_dir if iswindows: import win32process - _windows_null_file = open(os.devnull, 'wb') + try: + _windows_null_file = open(os.devnull, 'wb') + except: + raise RuntimeError('NUL %r file missing in windows'%os.devnull) class Worker(object): ''' diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py index 3a4fca09c0..bf0f48db7d 100644 --- a/src/calibre/utils/magick/__init__.py +++ b/src/calibre/utils/magick/__init__.py @@ -109,6 +109,13 @@ class Image(_magick.Image): # {{{ return _magick.Image.load(self, bytes(data)) def open(self, path_or_file): + if not hasattr(path_or_file, 'read') and \ + path_or_file.lower().endswith('.wmf'): + # Special handling for WMF files as ImageMagick seems + # to hand while reading them from a blob on linux + if isinstance(path_or_file, unicode): + path_or_file = path_or_file.encode(filesystem_encoding) + return _magick.Image.read(self, path_or_file) data = path_or_file if hasattr(data, 'read'): data = data.read() diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index 6808215554..5c978a27e0 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -25,6 +25,7 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, resize and the input and output image formats are the same, no changes are made. + :param data: Image data as bytestring or Image object :param compression_quality: The quality of the image after compression. Number between 1 and 100. 1 means highest compression, 100 means no compression (lossless). @@ -33,8 +34,11 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, ''' changed = False - img = Image() - img.load(data) + if isinstance(data, Image): + img = data + else: + img = Image() + img.load(data) orig_fmt = normalize_format_name(img.format) fmt = os.path.splitext(path)[1] fmt = normalize_format_name(fmt[1:]) diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index b1436a830b..0aab5f1fd7 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -414,6 +414,24 @@ magick_Image_load(magick_Image *self, PyObject *args, PyObject *kwargs) { // }}} +// Image.load {{{ +static PyObject * +magick_Image_read(magick_Image *self, PyObject *args, PyObject *kwargs) { + const char *data; + MagickBooleanType res; + + if (!PyArg_ParseTuple(args, "s", &data)) return NULL; + + res = MagickReadImage(self->wand, data); + + if (!res) + return magick_set_exception(self->wand); + + Py_RETURN_NONE; +} + +// }}} + // Image.create_canvas {{{ static PyObject * magick_Image_create_canvas(magick_Image *self, PyObject *args, PyObject *kwargs) @@ -873,6 +891,10 @@ static PyMethodDef magick_Image_methods[] = { "Load an image from a byte buffer (string)" }, + {"read", (PyCFunction)magick_Image_read, METH_VARARGS, + "Read image from path. Path must be a bytestring in the filesystem encoding" + }, + {"export", (PyCFunction)magick_Image_export, METH_VARARGS, "export(format) -> bytestring\n\n Export the image as the specified format" }, diff --git a/src/calibre/utils/smartypants.py b/src/calibre/utils/smartypants.py index 44aac4de8c..62845b8d7a 100644 --- a/src/calibre/utils/smartypants.py +++ b/src/calibre/utils/smartypants.py @@ -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): diff --git a/src/calibre/utils/smtp.py b/src/calibre/utils/smtp.py index 230a983b74..b8b46a96cb 100644 --- a/src/calibre/utils/smtp.py +++ b/src/calibre/utils/smtp.py @@ -11,6 +11,7 @@ This module implements a simple commandline SMTP client that supports: import sys, traceback, os from email import encoders +from calibre import isbytestring def create_mail(from_, to, subject, text=None, attachment_data=None, attachment_type=None, attachment_name=None): @@ -26,7 +27,10 @@ def create_mail(from_, to, subject, text=None, attachment_data=None, if text is not None: from email.mime.text import MIMEText - msg = MIMEText(text) + if isbytestring(text): + msg = MIMEText(text) + else: + msg = MIMEText(text, 'plain', 'utf-8') outer.attach(msg) if attachment_data is not None: diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index d1e7866198..f3d77061c3 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -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 diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py index 1dd19dc524..012e24a799 100644 --- a/src/calibre/web/feeds/recipes/collection.py +++ b/src/calibre/web/feeds/recipes/collection.py @@ -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', '') })