[Sync] Sync from trunk. Revision 7339.

This commit is contained in:
Li Fanxi 2010-12-23 23:51:03 +08:00
commit 3d951f90cc
185 changed files with 32171 additions and 9747 deletions

View File

@ -4,6 +4,126 @@
# for important features/bug fixes. # for important features/bug fixes.
# Also, each release can have new and improved recipes. # Also, each release can have new and improved recipes.
- version: 0.7.34
date: 2010-12-17
new features:
- title: "Page turn animations in the e-book viewer"
type: major
description: >
"Now when you use the Page Down/Page Up keys or the next/previous page buttons in the viewer, page turning will be animated. The duration of the animation can be controlled in the viewer preferences. Setting it to 0 disables the animation completely."
- title: "Conversion pipeline: Add an option to set the minimum line height of all elemnts as a percentage of the computed font size. By default, calibre now sets the line height to 120% of the computed font size."
- title: "Large speedup in startup times and post metadata edit wait for large libraries"
- title: "Allow changing the font used in the calibre interface via Preferences->Look and feel"
- title: "Allow editing of the title sort value for a book via the edit metadata dialog"
- title: "Disable the cover cache. This means that if you are running calibre on an underpowered machine, you might notice some slow down in the cover browser. On the other hand, calibre's memory consumption is reduced."
- title: "You can now restart calibre in debug mode by clicking the arrow next to the Preferences button. In debug mode, after you quit calibre, a diagnostic log will popup"
tickets: [7359]
- title: "When creating a new calibre library add an option to copy the custom column, saved searches, etc from the current library."
tickets: [7643]
- title: "Add more tweaks to control how the next available series number is calculated."
tickets: [7892]
- title: "Add a tweak to control layout of the custom metadata tab in the edit metadata dialog"
- title: "Apple driver: Set series number as track number on windows when sending books to iTunes"
- title: "Drivers for PocketBook 701 and Samsung E65"
- title: "E-book viewer: Add option to have the mouse wheel flip pages"
- title: "Add a load_resources method to the InterfaceAction and Plugin classes to facilitate loading of resources from plugin ZIP files"
- title: "E-book viewer: Add option to not remember position in book when quitting."
tickets: [7699]
- title: "When sorting the book list, keep the current book visible after the sort completes."
tickets: [7504]
- title: "EPUB Output: Add an option to flatten the EPUB file structure, specially for FBReaderJ."
tickets: [7788]
- title: "EPUB Output: Ensure all files inside the generated EPUB have unique filenames, to support broken EPUB readers like Stanza, Aldiko, FBReader and Sigil"
- title: "FB2 Output: Add support for some 2.1 style tags."
- title: "Bulk metadata edit: Add options to delete cover/generate default cover."
tickets: [7885]
- title: "Fix a regression in 0.7.33 that broke updating covers in ebook files when saving to disk."
tickets: [7886]
- title: "Don't refresh the Tag browser if it is hidden. Speeds up metadata editing with large libraries, if you hide teh Tag Browser."
- title: "MOBI Output: Add option to ignore margins in input document"
tickets: [7877]
- title: "Kobo driver: Add support for 1.8.x firmware"
bug fixes:
- title: "Fix various memory leaks introduced in the last couple of releases"
- title: "EPUB metadata: When rendering first page as the cover, handle embedded svg correctly."
tickets: [7909]
- title: "Disable multiple library support when the CALIBRE_OVERRIDE_DATABASE_PATH env var is set"
- title: "Content server: Fix bug that could cause saved search based restrictions to not exclude all books"
tickets: [7876]
- title: "Topaz metadata: Read metadata correctly from Topaz files that have MOBI file extensions"
- title: "MOBI Input: Handle the (rare) MOBI files that do not specify per paragraph text indents correctly."
tickets: [7869]
- title: "MOBI metadata reader: Handle invalid PRC files with spurious image_offset headers"
- title: "Fix drag/drop of new cover to book detail panel does not update cover browser"
tickets: [7890]
- title: "Do not open the book details dialog when double click on the scrollbars in the book details panel"
tickets: [7826]
- title: "Templates: Fix {tags} not working when no tags are present"
tickets: [7888]
- title: "HTML metadata: Fix regression that broke parsing of some meta tags"
tickets: [7851]
- title: "Preferences: Add tooltips to buddy labels as well."
tickets: [7873]
- title: "Content server: Fix handling of root URL when using --url-prefix"
- title: "Ensure that the default encoding used by python is never ASCII (needed when running a non frozen version of calibre on linux)"
improved recipes:
- Astronomy Picture of the day
- New Scientist
- Radikal
- Times of India
- Economic Times
- Zeit Online
- Dilbert
new recipes:
- title: "Various Japanes news sources, National Geographic and paper.li"
author: "Hiroshi Miura"
- title: "Science based medicine"
author: "BuzzKill"
- title: "Kompiutierra"
author: "Vadim Dyadkin"
- version: 0.7.33 - version: 0.7.33
date: 2010-12-10 date: 2010-12-10

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

831
imgsrc/edit-cut.svg Normal file
View File

@ -0,0 +1,831 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docbase="/home/david/Oxygen/trunk/scalable/actions"
sodipodi:docname="edit-cut.svgz"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
inkscape:export-filename="edit-cut.png"
inkscape:export-xdpi="22.5"
inkscape:export-ydpi="22.5">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4792">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4794" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop4796" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4758">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4760" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4762" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4722">
<stop
style="stop-color:#dfdfdf;stop-opacity:1"
offset="0"
id="stop4724" />
<stop
style="stop-color:#606060;stop-opacity:1"
offset="1"
id="stop4726" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4635">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4637" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4639" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4618">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4620" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4622" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4488">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4490" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4492" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4445">
<stop
style="stop-color:#606060;stop-opacity:1"
offset="0"
id="stop4447" />
<stop
style="stop-color:#343434;stop-opacity:1"
offset="1"
id="stop4449" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4253">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4255" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop4257" />
</linearGradient>
<linearGradient
id="linearGradient3792"
inkscape:collect="always">
<stop
id="stop3794"
offset="0"
style="stop-color:#ffffff;stop-opacity:1" />
<stop
id="stop3796"
offset="1"
style="stop-color:#e7e7e7;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3631">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3633" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3635" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3475">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop3477" />
<stop
style="stop-color:#cbcbcb;stop-opacity:1"
offset="1"
id="stop3479" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3467">
<stop
style="stop-color:#e8e8e8;stop-opacity:1"
offset="0"
id="stop3469" />
<stop
style="stop-color:#888888;stop-opacity:0.53714287"
offset="1"
id="stop3471" />
</linearGradient>
<linearGradient
id="linearGradient3176"
inkscape:collect="always">
<stop
id="stop3178"
offset="0"
style="stop-color:#323232;stop-opacity:1;" />
<stop
id="stop3180"
offset="1"
style="stop-color:#000000;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3516"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315637,39.880442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3518"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315636,39.008928)"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3520"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-0.5000017,0.8660252)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3522"
gradientUnits="userSpaceOnUse"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3475"
id="linearGradient3524"
gradientUnits="userSpaceOnUse"
x1="62.646275"
y1="53.750923"
x2="52.066586"
y2="53.750923" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3526"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-0.5000017,0.8660252)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3467"
id="linearGradient3530"
gradientUnits="userSpaceOnUse"
x1="63.553711"
y1="16.056862"
x2="63.553711"
y2="63.136379" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3467"
id="linearGradient3609"
gradientUnits="userSpaceOnUse"
x1="63.553711"
y1="16.056862"
x2="63.553711"
y2="63.136379"
gradientTransform="matrix(0.8715135,3.655296e-8,-3.655296e-8,0.8715135,8.2476912,8.4795979)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3614"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315637,39.880442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3792"
id="linearGradient3617"
gradientUnits="userSpaceOnUse"
x1="62.646275"
y1="53.750923"
x2="52.066586"
y2="53.750923"
gradientTransform="matrix(0.8513047,-0.4911732,0.4915009,0.850737,-22.138279,32.363934)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3625"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315637,39.880442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3627"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315636,39.008928)"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3631"
id="linearGradient3637"
x1="61.911907"
y1="72.456772"
x2="59.719414"
y2="73.709625"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8541426,0,0,1.8529064,-52.138638,-62.025773)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3631"
id="linearGradient3641"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-5.0049887,0.680543,-0.6885831,-4.9465506,418.08891,395.15615)"
x1="61.911907"
y1="72.456772"
x2="59.719414"
y2="73.709625" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3467"
id="linearGradient3644"
gradientUnits="userSpaceOnUse"
x1="63.553711"
y1="16.056862"
x2="63.553711"
y2="63.136379"
gradientTransform="matrix(-0.8715135,3.655296e-8,3.655296e-8,0.8715135,120.39367,8.4795979)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3649"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7547529,-0.4357568,-0.4357568,0.7547529,140.957,39.880442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3475"
id="linearGradient3652"
gradientUnits="userSpaceOnUse"
x1="62.646275"
y1="53.750923"
x2="52.066586"
y2="53.750923"
gradientTransform="matrix(-0.7547529,-0.4357568,-0.4357568,0.7547529,140.957,39.008928)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3655"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7547529,-0.4357568,-0.4357568,0.7547529,140.957,38.380442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3657"
gradientUnits="userSpaceOnUse"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955"
gradientTransform="matrix(-0.7547529,-0.4357568,-0.4357568,0.7547529,140.957,37.508928)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3669"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315637,39.880442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3671"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.315636,39.008928)"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3792"
id="linearGradient3802"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513047,-0.4911732,-0.4915009,0.850737,150.74175,32.363934)"
x1="62.646275"
y1="53.750923"
x2="52.066586"
y2="53.750923" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3838"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7547529,-0.4357568,-0.4357568,0.7547529,140.957,40.880442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3847"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.013047,38.380442)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3849"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.013047,37.508928)"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3881"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7547529,-0.4357568,-0.4357568,0.7547529,140.957,37.508928)"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient3887"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7547529,-0.4357568,0.4357568,0.7547529,-12.013047,37.508928)"
x1="69.501228"
y1="109.56824"
x2="56.484062"
y2="117.84955" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient4006"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513047,-0.4911732,-0.4915009,0.850737,150.74175,33.346282)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient4085"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513047,-0.4911732,-0.4915009,0.850737,149.24175,36.346282)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3176"
id="linearGradient4087"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513047,-0.4911732,-0.4915009,0.850737,150.74175,33.346282)"
x1="63.245899"
y1="107.23933"
x2="58.32019"
y2="107.5107" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3792"
id="linearGradient4250"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513047,-0.4911732,-0.4915009,0.850737,150.74175,32.363934)"
x1="62.646275"
y1="53.750923"
x2="52.066586"
y2="53.750923" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4253"
id="linearGradient4259"
x1="65.414917"
y1="63.2187"
x2="71.566734"
y2="58.624897"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4445"
id="linearGradient4443"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8513046,-0.4911732,0.4915009,0.850737,-21.796979,30.673174)"
x1="66.410789"
y1="111.09748"
x2="56.771309"
y2="111.40427" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4455">
<path
style="fill:#343434;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 75.1875,70.0625 C 74.330819,70.112586 73.601545,70.703549 73.375,71.53125 L 71.21875,79.59375 C 71.09204,80.103085 71.170605,80.641816 71.4375,81.09375 L 89.625,112.5625 C 91.972816,116.62633 95.167933,119.72417 98.625,121.46875 C 102.08207,123.21333 105.97364,123.61751 109.15625,121.78125 C 112.33881,119.94502 113.935,116.36699 114.15625,112.5 C 114.3775,108.63301 113.28602,104.31505 110.9375,100.25 C 108.5891,96.185153 105.39577,93.080216 101.9375,91.34375 C 98.479225,89.607284 94.586162,89.2278 91.40625,91.0625 C 90.654039,91.496499 90.578013,91.480641 90,91.1875 C 89.421987,90.894359 88.429852,89.981353 87.25,88.34375 C 84.890296,85.068543 81.724299,79.185679 77.03125,71.0625 C 76.654739,70.40782 75.941586,70.021025 75.1875,70.0625 z M 97.9375,99.03125 C 99.847583,98.899901 103.29363,100.88292 105.4375,104.59375 C 106.75724,106.8781 107.33023,109.26773 107.25,111.125 C 107.16977,112.98227 106.50165,114.13243 105.59375,114.65625 C 104.68591,115.18004 103.39934,115.17251 101.75,114.3125 C 100.10066,113.45249 98.288767,111.75356 96.96875,109.46875 C 95.649001,107.18439 95.076021,104.76352 95.15625,102.90625 C 95.236479,101.04898 95.873353,99.898826 96.78125,99.375 C 97.154526,99.159631 97.520851,99.0599 97.9375,99.03125 z "
id="path4457"
sodipodi:nodetypes="cccccsssssssscccsssssssc" />
</clipPath>
<filter
inkscape:collect="always"
id="filter4475">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.47498194"
id="feGaussianBlur4477" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4488"
id="linearGradient4494"
x1="100.23751"
y1="84.952927"
x2="115.33315"
y2="111.09933"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1304268,0,0,1.1304268,-12.155804,-11.996273)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4445"
id="linearGradient4572"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513046,-0.4911732,-0.4915009,0.850737,150.64081,30.673174)"
x1="66.410789"
y1="111.09748"
x2="56.771309"
y2="111.40427" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4488"
id="linearGradient4574"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1304268,0,0,1.1304268,140.99964,-11.996273)"
x1="100.23751"
y1="84.952927"
x2="115.33315"
y2="111.09933" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4618"
id="linearGradient4624"
x1="39.66201"
y1="99.394554"
x2="32.5625"
y2="108.58216"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7874752,0.1143529,-0.1388531,-0.6485276,62.315135,175.57221)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4445"
id="linearGradient4630"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513046,-0.4911732,-0.4915009,0.850737,150.14081,34.173174)"
x1="66.410789"
y1="111.09748"
x2="56.771309"
y2="111.40427" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4445"
id="linearGradient4632"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.8513046,-0.4911732,-0.4915009,0.850737,150.64081,30.673174)"
x1="66.410789"
y1="111.09748"
x2="56.771309"
y2="111.40427" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4635"
id="linearGradient4641"
x1="24.636236"
y1="118.53715"
x2="24.636236"
y2="110.80067"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:collect="always"
id="filter4691">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.41898454"
id="feGaussianBlur4693" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4635"
id="linearGradient4701"
gradientUnits="userSpaceOnUse"
x1="24.636236"
y1="118.53715"
x2="24.636236"
y2="110.80067" />
<filter
inkscape:collect="always"
id="filter4711">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.71627592"
id="feGaussianBlur4713" />
</filter>
<filter
inkscape:collect="always"
id="filter4715">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.71627592"
id="feGaussianBlur4717" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4722"
id="radialGradient4728"
cx="66"
cy="54"
fx="66.495979"
fy="53.140942"
r="2"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0315502,-0.5959631,0.5955656,1.0308618,-34.242853,37.667027)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4758"
id="linearGradient4764"
x1="51.619904"
y1="81.644371"
x2="46.564438"
y2="99.975533"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(3.132137,-2.5057096)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4792"
id="linearGradient4798"
x1="64.46875"
y1="67.044975"
x2="65.410522"
y2="80.970673"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:collect="always"
id="filter4821">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.98653907"
id="feGaussianBlur4823" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.7890625"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="g3690"
width="128px"
height="128px"
showgrid="false"
gridspacingx="2px"
gridspacingy="2px"
gridempspacing="4"
inkscape:grid-points="true"
showborder="false"
borderlayer="false"
inkscape:showpageshadow="false"
inkscape:window-width="794"
inkscape:window-height="730"
inkscape:window-x="0"
inkscape:window-y="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3690">
<path
style="opacity:0.5;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient4798);stroke-width:3.54751818;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4821)"
d="M 35.28125,14.46875 C 34.326418,15.020173 34.451101,16.184543 34.6875,16.78125 C 41.824923,35.39161 48.223262,52.347787 58.15625,70.75 L 56.625,73.21875 C 56.244936,72.547795 55.487111,72.144016 54.6875,72.21875 C 54.034234,72.275803 53.450423,72.649442 53.125,73.21875 C 48.962268,80.428813 46.167367,85.651204 44.09375,88.53125 C 43.056941,89.971273 42.203917,90.769641 41.75,91 C 41.296083,91.230359 41.306883,91.263851 40.6875,90.90625 C 37.787275,89.231804 34.269689,89.591888 31.15625,91.15625 C 28.042811,92.720612 25.164593,95.484072 23.0625,99.125 C 20.960291,102.76613 19.988354,106.64212 20.1875,110.125 C 20.386646,113.60788 21.847132,116.85528 24.75,118.53125 C 27.652918,120.20725 31.200269,119.82161 34.3125,118.25 C 37.424731,116.67839 40.273497,113.92116 42.375,110.28125 L 58.5,82.34375 C 58.674659,82.047999 58.772646,81.71253 58.78125,81.375 C 60.494006,80.173524 62.436255,79.043092 64.3125,78.25 C 66.301575,79.090643 68.394793,80.309785 70.1875,81.59375 C 70.216655,81.858831 70.304417,82.106865 70.4375,82.34375 L 86.5625,110.28125 C 88.664002,113.92116 91.544019,116.67839 94.65625,118.25 C 97.768481,119.82161 101.28458,120.20725 104.1875,118.53125 C 107.09037,116.85528 108.55085,113.60788 108.75,110.125 C 108.94915,106.64212 107.97721,102.76613 105.875,99.125 C 103.77291,95.484072 100.89469,92.720612 97.78125,91.15625 C 94.667811,89.591888 91.150225,89.231804 88.25,90.90625 C 87.630617,91.263851 87.641417,91.230359 87.1875,91 C 86.733583,90.769641 85.880559,89.971273 84.84375,88.53125 C 82.770133,85.651204 79.975232,80.428813 75.8125,73.21875 C 75.435989,72.56407 74.722836,72.177275 73.96875,72.21875 C 73.178739,72.268279 72.494718,72.772672 72.21875,73.5 L 70.5,70.75 C 80.43299,52.347791 86.83133,35.391608 93.96875,16.78125 C 94.205146,16.184543 94.329835,15.020174 93.375,14.46875 L 64.3125,60.875 L 35.28125,14.46875 z M 34.78125,98.375 C 35.1163,98.398055 35.384215,98.481147 35.6875,98.65625 C 36.408735,99.072656 36.962348,100.02888 37.03125,101.625 C 37.100152,103.22112 36.619402,105.28826 35.46875,107.28125 C 34.317829,109.27471 32.759661,110.76121 31.34375,111.5 C 29.927839,112.23879 28.846185,112.22887 28.125,111.8125 C 27.40377,111.3961 26.850153,110.47112 26.78125,108.875 C 26.712347,107.27888 27.193102,105.18048 28.34375,103.1875 C 30.21285,99.950123 33.224696,98.267893 34.78125,98.375 z M 94.15625,98.375 C 95.712802,98.267893 98.724654,99.950125 100.59375,103.1875 C 101.7444,105.18048 102.22515,107.27888 102.15625,108.875 C 102.08735,110.47112 101.56498,111.3961 100.84375,111.8125 C 100.12257,112.22887 99.04091,112.23879 97.625,111.5 C 96.20909,110.76121 94.650922,109.27471 93.5,107.28125 C 92.349349,105.28826 91.837348,103.22112 91.90625,101.625 C 91.975152,100.02888 92.528765,99.072656 93.25,98.65625 C 93.553281,98.48115 93.8212,98.398055 94.15625,98.375 z "
id="path3483"
transform="matrix(1.1279249,0,0,1.1271729,-8.2471649,-11.605871)" />
<path
sodipodi:nodetypes="cccccsssssssscccsssssssc"
id="path4566"
d="M 53.65633,70.0625 C 54.51301,70.112586 55.24229,70.703549 55.46883,71.53125 L 57.62508,79.59375 C 57.75179,80.103085 57.67323,80.641816 57.40633,81.09375 L 39.218832,112.5625 C 36.871016,116.62633 33.675899,119.72417 30.218832,121.46875 C 26.761762,123.21333 22.870192,123.61751 19.687582,121.78125 C 16.505022,119.94502 14.908832,116.36699 14.687582,112.5 C 14.466332,108.63301 15.557812,104.31505 17.906332,100.25 C 20.254732,96.185153 23.448062,93.080216 26.906332,91.34375 C 30.364607,89.607284 34.25767,89.2278 37.437582,91.0625 C 38.189793,91.496499 38.265819,91.480641 38.843832,91.1875 C 39.421845,90.894359 40.41398,89.981353 41.593832,88.34375 C 43.95354,85.068543 47.11953,79.185679 51.81258,71.0625 C 52.18909,70.40782 52.90225,70.021025 53.65633,70.0625 z M 30.906332,99.03125 C 28.996249,98.899901 25.550202,100.88292 23.406332,104.59375 C 22.086592,106.8781 21.513602,109.26773 21.593832,111.125 C 21.674062,112.98227 22.342182,114.13243 23.250082,114.65625 C 24.157922,115.18004 25.444492,115.17251 27.093832,114.3125 C 28.743172,113.45249 30.555065,111.75356 31.875082,109.46875 C 33.194831,107.18439 33.767811,104.76352 33.687582,102.90625 C 33.607353,101.04898 32.970479,99.898826 32.062582,99.375 C 31.689306,99.159631 31.322981,99.0599 30.906332,99.03125 z "
style="fill:url(#linearGradient4572);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccscssssccsssscc"
id="path4568"
d="M 55.65241,70.312928 C 56.57508,70.366873 57.35134,71.011135 57.59533,71.902591 L 57.66599,72.185197 C 57.29393,71.760421 56.80725,71.477828 56.21763,71.443355 C 55.40545,71.398686 54.60957,71.798027 54.20405,72.50313 C 49.14952,81.252005 45.75918,87.592357 43.217718,91.119846 C 41.946986,92.883587 40.872884,93.877473 40.250348,94.193194 C 39.627813,94.508912 39.541489,94.519318 38.731337,94.051891 C 35.306492,92.075871 31.1164,92.499608 27.391742,94.369823 C 23.667082,96.240042 20.241732,99.5652 17.712462,103.94313 L 17.147252,102.8127 C 19.676522,98.434769 23.101862,95.109615 26.826532,93.239397 C 30.551186,91.369181 34.741278,90.945444 38.166123,92.921464 C 38.976275,93.388891 39.062599,93.378485 39.685134,93.062767 C 40.307671,92.747046 41.381773,91.75316 42.652505,89.989419 C 45.19396,86.461931 48.58431,80.121578 53.63884,71.372703 C 54.04436,70.667601 54.84024,70.268259 55.65241,70.312928 z "
style="opacity:0.77227723;fill:url(#linearGradient4574);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
clip-path="url(#clipPath4455)"
style="opacity:0.70297032;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4475)"
d="M 75.1875,70.0625 C 74.330819,70.112586 73.601545,70.703549 73.375,71.53125 L 71.21875,79.59375 C 71.09204,80.103085 71.170605,80.641816 71.4375,81.09375 L 89.625,112.5625 C 91.972816,116.62633 95.167933,119.72417 98.625,121.46875 C 102.08207,123.21333 105.97364,123.61751 109.15625,121.78125 C 112.33881,119.94502 113.935,116.36699 114.15625,112.5 C 114.3775,108.63301 113.28602,104.31505 110.9375,100.25 C 108.5891,96.185153 105.39577,93.080216 101.9375,91.34375 C 98.479225,89.607284 94.586162,89.2278 91.40625,91.0625 C 90.654039,91.496499 90.578013,91.480641 90,91.1875 C 89.421987,90.894359 88.429852,89.981353 87.25,88.34375 C 84.890296,85.068543 81.724299,79.185679 77.03125,71.0625 C 76.654739,70.40782 75.941586,70.021025 75.1875,70.0625 z M 97.9375,99.03125 C 99.847583,98.899901 103.29363,100.88292 105.4375,104.59375 C 106.75724,106.8781 107.33023,109.26773 107.25,111.125 C 107.16977,112.98227 106.50165,114.13243 105.59375,114.65625 C 104.68591,115.18004 103.39934,115.17251 101.75,114.3125 C 100.10066,113.45249 98.288767,111.75356 96.96875,109.46875 C 95.649001,107.18439 95.076021,104.76352 95.15625,102.90625 C 95.236479,101.04898 95.873353,99.898826 96.78125,99.375 C 97.154526,99.159631 97.520851,99.0599 97.9375,99.03125 z "
id="path4570"
sodipodi:nodetypes="cccccsssssssscccsssssssc"
transform="matrix(-1,0,0,1,128.84383,0)" />
<path
style="fill:url(#linearGradient4443);fill-opacity:1.0;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 75.1875,70.0625 C 74.330819,70.112586 73.601545,70.703549 73.375,71.53125 L 71.21875,79.59375 C 71.09204,80.103085 71.170605,80.641816 71.4375,81.09375 L 89.625,112.5625 C 91.972816,116.62633 95.167933,119.72417 98.625,121.46875 C 102.08207,123.21333 105.97364,123.61751 109.15625,121.78125 C 112.33881,119.94502 113.935,116.36699 114.15625,112.5 C 114.3775,108.63301 113.28602,104.31505 110.9375,100.25 C 108.5891,96.185153 105.39577,93.080216 101.9375,91.34375 C 98.479225,89.607284 94.586162,89.2278 91.40625,91.0625 C 90.654039,91.496499 90.578013,91.480641 90,91.1875 C 89.421987,90.894359 88.429852,89.981353 87.25,88.34375 C 84.890296,85.068543 81.724299,79.185679 77.03125,71.0625 C 76.654739,70.40782 75.941586,70.021025 75.1875,70.0625 z M 97.9375,99.03125 C 99.847583,98.899901 103.29363,100.88292 105.4375,104.59375 C 106.75724,106.8781 107.33023,109.26773 107.25,111.125 C 107.16977,112.98227 106.50165,114.13243 105.59375,114.65625 C 104.68591,115.18004 103.39934,115.17251 101.75,114.3125 C 100.10066,113.45249 98.288767,111.75356 96.96875,109.46875 C 95.649001,107.18439 95.076021,104.76352 95.15625,102.90625 C 95.236479,101.04898 95.873353,99.898826 96.78125,99.375 C 97.154526,99.159631 97.520851,99.0599 97.9375,99.03125 z "
id="path3845"
sodipodi:nodetypes="cccccsssssssscccsssssssc" />
<path
style="opacity:0.77227723;fill:url(#linearGradient4494);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 73.191419,70.312928 C 72.268749,70.366873 71.492489,71.011135 71.248498,71.902591 L 71.177846,72.185197 C 71.549903,71.760421 72.036582,71.477828 72.626206,71.443355 C 73.438379,71.398686 74.234264,71.798027 74.639778,72.50313 C 79.694311,81.252005 83.084654,87.592357 85.626114,91.119846 C 86.896846,92.883587 87.970948,93.877473 88.593484,94.193194 C 89.216019,94.508912 89.302343,94.519318 90.112495,94.051891 C 93.53734,92.075871 97.727432,92.499608 101.45209,94.369823 C 105.17675,96.240042 108.6021,99.5652 111.13137,103.94313 L 111.69658,102.8127 C 109.16731,98.434769 105.74197,95.109615 102.0173,93.239397 C 98.292646,91.369181 94.102554,90.945444 90.677709,92.921464 C 89.867557,93.388891 89.781233,93.378485 89.158698,93.062767 C 88.536161,92.747046 87.462059,91.75316 86.191327,89.989419 C 83.649868,86.461931 80.259524,80.121578 75.204992,71.372703 C 74.799477,70.667601 74.003592,70.268259 73.191419,70.312928 z "
id="path4479"
sodipodi:nodetypes="cccscssssccsssscc" />
<path
sodipodi:nodetypes="cccccsssssssscccsssssssc"
id="path4451"
d="M 75.1875,70.0625 C 74.330819,70.112586 73.601545,70.703549 73.375,71.53125 L 71.21875,79.59375 C 71.09204,80.103085 71.170605,80.641816 71.4375,81.09375 L 89.625,112.5625 C 91.972816,116.62633 95.167933,119.72417 98.625,121.46875 C 102.08207,123.21333 105.97364,123.61751 109.15625,121.78125 C 112.33881,119.94502 113.935,116.36699 114.15625,112.5 C 114.3775,108.63301 113.28602,104.31505 110.9375,100.25 C 108.5891,96.185153 105.39577,93.080216 101.9375,91.34375 C 98.479225,89.607284 94.586162,89.2278 91.40625,91.0625 C 90.654039,91.496499 90.578013,91.480641 90,91.1875 C 89.421987,90.894359 88.429852,89.981353 87.25,88.34375 C 84.890296,85.068543 81.724299,79.185679 77.03125,71.0625 C 76.654739,70.40782 75.941586,70.021025 75.1875,70.0625 z M 97.9375,99.03125 C 99.847583,98.899901 103.29363,100.88292 105.4375,104.59375 C 106.75724,106.8781 107.33023,109.26773 107.25,111.125 C 107.16977,112.98227 106.50165,114.13243 105.59375,114.65625 C 104.68591,115.18004 103.39934,115.17251 101.75,114.3125 C 100.10066,113.45249 98.288767,111.75356 96.96875,109.46875 C 95.649001,107.18439 95.076021,104.76352 95.15625,102.90625 C 95.236479,101.04898 95.873353,99.898826 96.78125,99.375 C 97.154526,99.159631 97.520851,99.0599 97.9375,99.03125 z "
style="fill:none;fill-opacity:1.0;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4475);opacity:0.7029703"
clip-path="url(#clipPath4455)" />
<path
style="fill:url(#linearGradient3802);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 97.96087,1.9111929 L 53.629372,72.750318 L 52.479384,84.564291 C 54.445388,81.161344 61.782615,76.320141 67.153838,74.881885 C 81.238527,50.502756 89.411871,28.739567 98.680477,4.5950145 C 98.954672,3.9035524 99.068368,2.5501817 97.96087,1.9111929 z "
id="path3506"
sodipodi:nodetypes="cccccc" />
<path
sodipodi:nodetypes="cc"
id="path4269"
d="M 97.692496,2.3849918 L 61.738989,59.428222"
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:url(#linearGradient3637);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 59.75 65.65625 L 57.71875 66.21875 L 56.28125 68.5 L 60.59375 77.71875 C 62.419602 76.674902 64.33553 75.770565 66.09375 75.1875 L 59.75 65.65625 z "
id="path3629" />
<path
style="fill:url(#linearGradient4259);fill-opacity:1.0;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 67.1875 51.0625 L 59 64.15625 L 66.1875 75.15625 C 66.513817 75.049746 66.842006 74.959145 67.15625 74.875 C 69.715966 70.444398 72.069962 66.100297 74.28125 61.8125 L 67.1875 51.0625 z "
id="path4248" />
<path
sodipodi:nodetypes="cccccc"
id="path2160"
d="M 30.642611,1.9111929 L 74.974109,72.750318 L 76.124096,84.564291 C 74.158093,81.161344 66.820866,76.320141 61.449644,74.881885 C 47.364955,50.502756 39.191609,28.739567 29.923004,4.5950145 C 29.648808,3.9035524 29.535112,2.5501817 30.642611,1.9111929 z "
style="fill:url(#linearGradient3617);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 30.738989,2.3849918 L 64.192496,55.928222"
id="path4267"
sodipodi:nodetypes="cc" />
<path
transform="matrix(1.4403715,-0.8310572,0.8315989,1.4394331,-75.999063,45.513227)"
d="M 68 54 A 2 2 0 1 1 64,54 A 2 2 0 1 1 68 54 z"
sodipodi:ry="2"
sodipodi:rx="2"
sodipodi:cy="54"
sodipodi:cx="66"
id="path3146"
style="opacity:0.96000001;fill:url(#radialGradient4728);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<image
y="0"
x="160"
id="image4278"
height="128"
width="128"
sodipodi:absref="/home/david/Oxygen/trunk/32x32/actions/edit-copy.png"
xlink:href="/home/david/Oxygen/trunk/32x32/actions/edit-copy.png" />
<image
y="0"
x="288"
id="image4288"
height="128"
width="128"
sodipodi:absref="/home/david/Oxygen/trunk/32x32/actions/edit-paste.png"
xlink:href="/home/david/Oxygen/trunk/32x32/actions/edit-paste.png" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;opacity:0.70792079;filter:url(#filter4715)"
d="M 57.53125 79.25 L 39.71875 110.0625 C 37.370936 114.12633 34.175817 117.22417 30.71875 118.96875 C 27.26168 120.71333 23.37011 121.11751 20.1875 119.28125 C 17.00494 117.44502 15.40875 113.86699 15.1875 110 C 15.119229 108.80676 15.194921 107.57094 15.375 106.3125 C 14.812237 108.42607 14.574653 110.52767 14.6875 112.5 C 14.90875 116.36699 16.50494 119.94502 19.6875 121.78125 C 22.87011 123.61751 26.76168 123.21333 30.21875 121.46875 C 33.675817 119.72417 36.870936 116.62633 39.21875 112.5625 L 57.40625 81.09375 C 57.67315 80.641816 57.75171 80.103085 57.625 79.59375 L 57.53125 79.25 z M 31.40625 96.53125 C 29.496167 96.399901 26.05012 98.38292 23.90625 102.09375 C 22.840787 103.93797 22.266248 105.85407 22.125 107.5 C 22.422918 106.54782 22.850187 105.55624 23.40625 104.59375 C 25.55012 100.88292 28.996167 98.899901 30.90625 99.03125 C 31.322899 99.0599 31.689226 99.159631 32.0625 99.375 C 32.970397 99.898826 33.607273 101.04898 33.6875 102.90625 C 33.703141 103.26833 33.690955 103.66277 33.65625 104.0625 C 34.065399 102.74599 34.233925 101.48097 34.1875 100.40625 C 34.107273 98.54898 33.470397 97.398826 32.5625 96.875 C 32.189226 96.659631 31.822899 96.5599 31.40625 96.53125 z "
id="path4576" />
<path
id="path4609"
d="M 57.53125,79.25 L 39.71875,110.0625 C 37.370936,114.12633 34.175817,117.22417 30.71875,118.96875 C 27.26168,120.71333 23.37011,121.11751 20.1875,119.28125 C 17.00494,117.44502 15.40875,113.86699 15.1875,110 C 15.119229,108.80676 15.194921,107.57094 15.375,106.3125 C 14.812237,108.42607 14.574653,110.52767 14.6875,112.5 C 14.90875,116.36699 16.50494,119.94502 19.6875,121.78125 C 22.87011,123.61751 26.76168,123.21333 30.21875,121.46875 C 33.675817,119.72417 36.870936,116.62633 39.21875,112.5625 L 57.40625,81.09375 C 57.67315,80.641816 57.75171,80.103085 57.625,79.59375 L 57.53125,79.25 z M 31.40625,96.53125 C 29.496167,96.399901 26.05012,98.38292 23.90625,102.09375 C 22.840787,103.93797 22.266248,105.85407 22.125,107.5 C 22.422918,106.54782 22.850187,105.55624 23.40625,104.59375 C 25.55012,100.88292 28.996167,98.899901 30.90625,99.03125 C 31.322899,99.0599 31.689226,99.159631 32.0625,99.375 C 32.970397,99.898826 33.607273,101.04898 33.6875,102.90625 C 33.703141,103.26833 33.690955,103.66277 33.65625,104.0625 C 34.065399,102.74599 34.233925,101.48097 34.1875,100.40625 C 34.107273,98.54898 33.470397,97.398826 32.5625,96.875 C 32.189226,96.659631 31.822899,96.5599 31.40625,96.53125 z "
style="opacity:0.70792081;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4711)"
transform="matrix(-1,0,0,1,128.84431,0)" />
<path
style="opacity:0.48514851;fill:url(#linearGradient4641);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4691)"
d="M 33.1875,106.4375 C 32.888012,107.43505 32.455199,108.46448 31.875,109.46875 C 30.554983,111.75356 28.74309,113.45249 27.09375,114.3125 C 25.44441,115.17251 24.15784,115.18004 23.25,114.65625 C 22.3421,114.13243 21.67398,112.98227 21.59375,111.125 C 21.212006,112.3879 21.048791,113.58424 21.09375,114.625 C 21.17398,116.48227 21.8421,117.63243 22.75,118.15625 C 23.65784,118.68004 24.94441,118.67251 26.59375,117.8125 C 28.24309,116.95249 30.054983,115.25356 31.375,112.96875 C 32.687576,110.6968 33.260229,108.29075 33.1875,106.4375 z "
id="path4626"
sodipodi:nodetypes="cssscssssc"
transform="matrix(1.6395402,0,0,1.5188129,-15.9999,-58.979717)" />
<path
transform="matrix(-1.6395402,0,0,1.5188129,144.99452,-58.979717)"
sodipodi:nodetypes="cssscssssc"
id="path4699"
d="M 33.1875,106.4375 C 32.888012,107.43505 32.455199,108.46448 31.875,109.46875 C 30.554983,111.75356 28.74309,113.45249 27.09375,114.3125 C 25.44441,115.17251 24.15784,115.18004 23.25,114.65625 C 22.3421,114.13243 21.67398,112.98227 21.59375,111.125 C 21.212006,112.3879 21.048791,113.58424 21.09375,114.625 C 21.17398,116.48227 21.8421,117.63243 22.75,118.15625 C 23.65784,118.68004 24.94441,118.67251 26.59375,117.8125 C 28.24309,116.95249 30.054983,115.25356 31.375,112.96875 C 32.687576,110.6968 33.260229,108.29075 33.1875,106.4375 z "
style="opacity:0.48514851;fill:url(#linearGradient4701);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4691)" />
<path
style="opacity:0.77722772;fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 65.770092,66.282577 L 62.183456,70.557029"
id="path4730" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 43 KiB

3302
imgsrc/edit-paste.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -1,722 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg2606"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docname="edit-undo.svgz"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
sodipodi:docbase="/home/david/oxygen/trunk/scalable/actions"
inkscape:export-filename="edit-undo.png"
inkscape:export-xdpi="11.25"
inkscape:export-ydpi="11.25">
<defs
id="defs2608">
<linearGradient
inkscape:collect="always"
id="linearGradient3342">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3344" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3347" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3326">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3328" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3330" />
</linearGradient>
<linearGradient
id="linearGradient3825">
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="0"
id="stop3827" />
<stop
id="stop3833"
offset="0.5"
style="stop-color:#ffffff;stop-opacity:0.18705036;" />
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="1"
id="stop3829" />
</linearGradient>
<linearGradient
id="linearGradient3751">
<stop
style="stop-color:#beba2c;stop-opacity:1;"
offset="0"
id="stop3753" />
<stop
style="stop-color:#b6be2c;stop-opacity:0;"
offset="1"
id="stop3755" />
</linearGradient>
<linearGradient
id="linearGradient3528">
<stop
style="stop-color:#eaf209;stop-opacity:1;"
offset="0"
id="stop3530" />
<stop
style="stop-color:#c7c634;stop-opacity:0;"
offset="1"
id="stop3532" />
</linearGradient>
<linearGradient
id="linearGradient3295">
<stop
style="stop-color:#fffe63;stop-opacity:1;"
offset="0"
id="stop3297" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3299" />
</linearGradient>
<linearGradient
id="linearGradient3202">
<stop
style="stop-color:#fcff9c;stop-opacity:1;"
offset="0"
id="stop3204" />
<stop
style="stop-color:#c1a965;stop-opacity:0;"
offset="1"
id="stop3206" />
</linearGradient>
<radialGradient
id="XMLID_4_"
cx="48"
cy="-0.2148"
r="55.148"
gradientTransform="matrix(0.9792,0,0,0.9725,133.0002,20.8762)"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
style="stop-color:#cfd13d;stop-opacity:1;"
id="stop3082" />
<stop
offset="1"
style="stop-color:#db8900;stop-opacity:1;"
id="stop3090" />
</radialGradient>
<linearGradient
id="linearGradient3260"
inkscape:collect="always">
<stop
id="stop3262"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3264"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient3344">
<stop
id="stop3346"
offset="0"
style="stop-color:#fdff63;stop-opacity:1;" />
<stop
id="stop3348"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient3449">
<stop
id="stop3451"
offset="0"
style="stop-color:#000000;stop-opacity:1;" />
<stop
id="stop3453"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#XMLID_4_"
id="linearGradient3516"
x1="147.09375"
y1="33.40625"
x2="8.083992"
y2="123.90625"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#XMLID_4_"
id="radialGradient3524"
cx="67.09375"
cy="116.90625"
fx="67.09375"
fy="116.90625"
r="56"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3528"
id="radialGradient3535"
cx="99.726295"
cy="27.418272"
fx="64.689766"
fy="68.231934"
r="56"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.693735,5.8671246e-2,-3.6242796e-2,0.4285387,33.939389,26.8809)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3295"
id="linearGradient3548"
x1="75.09375"
y1="4.5317035"
x2="75.09375"
y2="80.172485"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="linearGradient3581"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4.2161108e-3,1.9705695e-3)"
x1="75.09375"
y1="4.5317035"
x2="75.09375"
y2="80.172485" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3295"
id="linearGradient3613"
x1="208.59375"
y1="130.40625"
x2="208.59375"
y2="63.426777"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-137,0)" />
<filter
inkscape:collect="always"
id="filter3639">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.0580524"
id="feGaussianBlur3641" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3449"
id="radialGradient3654"
cx="-10.165252"
cy="66.906013"
fx="-10.165252"
fy="66.906013"
r="59.995121"
gradientTransform="matrix(0.4582893,-2.1035589e-8,4.5903973e-8,1.0000813,20.447953,-5.1974351e-3)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3449"
id="radialGradient3658"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.2676699,0.521376,-0.7037472,0.3612977,108.99386,-36.062981)"
cx="167.67001"
cy="80.404922"
fx="167.67001"
fy="80.404922"
r="59.995121" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="linearGradient3676"
x1="120.0625"
y1="12.569496"
x2="125.30366"
y2="14.444496"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect" />
<filter
inkscape:collect="always"
x="-0.15096202"
width="1.301924"
y="-0.13732364"
height="1.2746473"
id="filter3738">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.39257441"
id="feGaussianBlur3740" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3449"
id="radialGradient3744"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.982366,1.671718e-2,-3.5801148e-3,0.2103843,-18.56344,30.477792)"
cx="72.684891"
cy="48.228905"
fx="74.871155"
fy="26.862719"
r="59.995121" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3751"
id="radialGradient3757"
cx="66.01458"
cy="126.69183"
fx="66.01458"
fy="126.69183"
r="59.99512"
gradientTransform="matrix(0.675025,0,0,0.3583625,19.527377,41.004647)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="radialGradient3767"
cx="64.088867"
cy="7.4108429"
fx="64.088867"
fy="7.4108429"
r="59.995121"
gradientTransform="matrix(0.3093869,0,0,0.4779247,44.260611,3.8644223)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3344"
id="linearGradient3771"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
x1="120.0625"
y1="12.569496"
x2="125.30366"
y2="14.444496" />
<filter
inkscape:collect="always"
id="filter3438">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.3342697"
id="feGaussianBlur3440" />
</filter>
<filter
inkscape:collect="always"
id="filter3630">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.89883985"
id="feGaussianBlur3632" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3825"
id="radialGradient2361"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.6484284,0.1017206,-3.1257154e-2,0.1992521,-4.56257,53.15916)"
cx="-112.17241"
cy="118.60459"
fx="-113.14772"
fy="59.708473"
r="59.99512" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient2363"
gradientUnits="userSpaceOnUse"
x1="-151.43935"
y1="37.68198"
x2="-152.26776"
y2="57.25" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient2365"
gradientUnits="userSpaceOnUse"
x1="-132.51041"
y1="39.803303"
x2="-158.92462"
y2="72.881729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient2367"
gradientUnits="userSpaceOnUse"
x1="-83.012932"
y1="44.753052"
x2="-158.92462"
y2="72.881729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3349"
x1="-73"
y1="105.625"
x2="-163"
y2="86.125"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:collect="always"
x="-0.087741371"
width="1.1754827"
y="-0.10211017"
height="1.2042203"
id="filter3363">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="3.0526685"
id="feGaussianBlur3365" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3372"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-40.5,-1.5)"
x1="-83.593941"
y1="137.13324"
x2="-138.0043"
y2="92.603989" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3376"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-40.5,-1.5)"
x1="-61.802711"
y1="99.979607"
x2="-136.51074"
y2="112.70422" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3825"
id="radialGradient3388"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.6484284,0.1017206,-3.1257154e-2,0.1992521,-4.56257,53.15916)"
cx="-112.17241"
cy="118.60459"
fx="-113.14772"
fy="59.708473"
r="59.99512" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient3390"
gradientUnits="userSpaceOnUse"
x1="-151.43935"
y1="37.68198"
x2="-152.26776"
y2="57.25" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient3392"
gradientUnits="userSpaceOnUse"
x1="-132.51041"
y1="39.803303"
x2="-158.92462"
y2="72.881729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient3394"
gradientUnits="userSpaceOnUse"
x1="-83.012932"
y1="44.753052"
x2="-158.92462"
y2="72.881729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3396"
gradientUnits="userSpaceOnUse"
x1="-73"
y1="105.625"
x2="-163"
y2="86.125" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3398"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-40.5,-1.5)"
x1="-83.593941"
y1="137.13324"
x2="-138.0043"
y2="92.603989" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3400"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-40.5,-1.5)"
x1="-61.802711"
y1="99.979607"
x2="-136.51074"
y2="112.70422" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3825"
id="radialGradient3422"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.6484284,0.1017206,-3.1257154e-2,0.1992521,-4.56257,53.15916)"
cx="-112.17241"
cy="118.60459"
fx="-113.14772"
fy="59.708473"
r="59.99512" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient3424"
gradientUnits="userSpaceOnUse"
x1="-151.43935"
y1="37.68198"
x2="-152.26776"
y2="57.25" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient3426"
gradientUnits="userSpaceOnUse"
x1="-132.51041"
y1="39.803303"
x2="-158.92462"
y2="72.881729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3326"
id="linearGradient3428"
gradientUnits="userSpaceOnUse"
x1="-83.012932"
y1="44.753052"
x2="-158.92462"
y2="72.881729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3430"
gradientUnits="userSpaceOnUse"
x1="-73"
y1="105.625"
x2="-163"
y2="86.125" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3432"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-40.5,-1.5)"
x1="-83.593941"
y1="137.13324"
x2="-138.0043"
y2="92.603989" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3342"
id="linearGradient3434"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-40.5,-1.5)"
x1="-61.802711"
y1="99.979607"
x2="-136.51074"
y2="112.70422" />
<mask
maskUnits="userSpaceOnUse"
id="mask3402">
<g
id="g3404"
transform="translate(167.50257,-3.755156e-3)">
<g
id="g3406"
transform="translate(80.51637,30.885255)">
<path
style="opacity:1;fill:url(#radialGradient3422);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M -184.42232,-32.47243 C -217.54751,-32.47243 -248.42232,-0.097625 -248.42232,33.02757 C -248.42232,66.15276 -217.04751,97.02757 -183.92232,97.02757 C -153.6332,97.02757 -128.58571,70.81131 -124.51607,41.68382 L -159.54732,36.65257 C -161.172,48.6137 -171.47739,57.62132 -183.92232,57.62132 C -197.49395,57.62132 -206.01607,46.0992 -206.01607,32.52757 C -206.01607,18.955936 -199.99395,12.43382 -186.42232,12.43382 C -179.6365,12.433819 -176.50103,10.198864 -172.04732,14.65257 L -176.8745,25.979749 C -178.93037,28.035619 -179.11822,29.285529 -178.55411,30.595278 C -178.04554,31.776057 -177.03338,33.12132 -174.34438,33.12132 L -130.39107,33.12132 C -126.54518,33.12132 -123.93208,30.466941 -123.93208,26.871189 L -124.00095,-17.206829 C -124.00095,-19.687584 -124.90346,-21.050058 -126.18242,-21.556444 C -127.49674,-22.076829 -129.21563,-21.679122 -131.28951,-19.605244 L -141.48482,-19.40993 C -152.34417,-30.269281 -167.85972,-32.47243 -184.42232,-32.47243 z "
id="path3408"
sodipodi:nodetypes="cssccsssccsccccsccc" />
<path
sodipodi:nodetypes="ccccc"
style="fill:url(#linearGradient3424);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3438)"
d="M -161.5,34.5 C -162,37 -180,54 -180,54 L -132.5,80 L -112,38.5 L -161.5,34.5 z "
id="path3410" />
<path
sodipodi:nodetypes="ccccc"
id="path3412"
d="M -161.5,34.5 C -162,37 -181.27817,54.389087 -181.27817,54.389087 L -151.62742,97.591883 L -112,38.5 L -161.5,34.5 z "
style="fill:url(#linearGradient3426);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3438)" />
<path
style="fill:url(#linearGradient3428);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3438)"
d="M -161.5,34.5 C -162,37 -181.27817,54.389087 -181.27817,54.389087 L -151.62742,97.591883 L -112,38.5 L -161.5,34.5 z "
id="path3414"
sodipodi:nodetypes="ccccc" />
</g>
<rect
y="69.75"
x="-119"
height="71.75"
width="83.5"
id="rect3416"
style="opacity:1;fill:url(#linearGradient3430);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter3363)" />
<path
sodipodi:nodetypes="ccccc"
id="path3418"
d="M -159.5,68.25 L -39.138259,55.983708 L -93.453327,162.55286 L -197.79465,128.96507 L -159.5,68.25 z "
style="fill:url(#linearGradient3432);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter3363)"
transform="matrix(0.6393762,0.7688941,-0.7688941,0.6393762,37.597642,128.08723)" />
<path
transform="matrix(0.6393762,0.7688941,-0.7688941,0.6393762,37.597642,128.08723)"
style="fill:url(#linearGradient3434);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter3363)"
d="M -159.5,68.25 L -39.138259,55.983708 L -93.453327,162.55286 L -197.79465,128.96507 L -159.5,68.25 z "
id="path3420"
sodipodi:nodetypes="ccccc" />
</g>
</mask>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3751"
id="linearGradient3565"
x1="-267.47665"
y1="18.103027"
x2="-33.476654"
y2="18.103027"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3295"
id="linearGradient3567"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-137,0)"
x1="208.59375"
y1="130.40625"
x2="208.59375"
y2="63.426777" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.6484375"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="128px"
height="128px"
gridspacingx="4px"
gridspacingy="4px"
gridempspacing="2"
showgrid="false"
inkscape:grid-points="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="748"
inkscape:window-height="681"
inkscape:window-x="526"
inkscape:window-y="51" />
<metadata
id="metadata2611">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3835"
mask="url(#mask3402)"
transform="matrix(-1,0,0,1,128.17774,0)">
<path
sodipodi:nodetypes="cssccsssccsccccsccc"
id="rect3204"
d="M 64.09375,3.90625 C 30.968558,3.9062499 4.0937499,30.781055 4.09375,63.90625 C 4.0937501,97.031442 30.96856,123.90625 64.09375,123.90625 C 94.382866,123.90625 119.43036,101.68999 123.5,72.5625 L 88.46875,67.53125 C 86.844066,79.492379 76.538676,88.5 64.09375,88.5 C 50.522122,88.499999 39.5,77.477881 39.5,63.90625 C 39.500001,50.334616 50.522119,39.3125 64.09375,39.3125 C 70.879568,39.312499 77.015044,42.077544 81.46875,46.53125 L 71.141571,56.858429 C 69.085701,58.914299 68.897846,60.164209 69.461963,61.473958 C 69.970531,62.654737 70.982695,64 73.671688,64 L 117.625,64 C 121.47089,64 124.08399,61.345621 124.08399,57.749869 L 124.01512,13.671851 C 124.01512,11.191096 123.11261,9.8286218 121.83365,9.3222363 C 120.51933,8.8018514 118.80044,9.1995576 116.72656,11.273436 L 106.53125,21.46875 C 95.671902,10.609399 80.656349,3.90625 64.09375,3.90625 z "
style="opacity:1;fill:url(#linearGradient3516);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1" />
<path
style="opacity:0.79775277;fill:url(#radialGradient3757);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 64.09375,3.90625 C 30.968558,3.9062499 4.0937499,30.781055 4.09375,63.90625 C 4.0937501,97.031442 30.96856,123.90625 64.09375,123.90625 C 94.382866,123.90625 119.43036,101.68999 123.5,72.5625 L 88.46875,67.53125 C 86.844066,79.492379 76.538676,88.5 64.09375,88.5 C 50.522122,88.499999 39.5,77.477881 39.5,63.90625 C 39.500001,50.334616 50.522119,39.3125 64.09375,39.3125 C 70.879568,39.312499 77.015044,42.077544 81.46875,46.53125 L 71.141571,56.858429 C 69.085701,58.914299 68.897846,60.164209 69.461963,61.473958 C 69.970531,62.654737 70.982695,64 73.671688,64 L 117.625,64 C 121.47089,64 124.08399,61.345621 124.08399,57.749869 L 124.01512,13.671851 C 124.01512,11.191096 123.11261,9.8286218 121.83365,9.3222363 C 120.51933,8.8018514 118.80044,9.1995576 116.72656,11.273436 L 106.53125,21.46875 C 95.671902,10.609399 80.656349,3.90625 64.09375,3.90625 z "
id="path3749"
sodipodi:nodetypes="cssccsssccsccccsccc" />
<path
sodipodi:nodetypes="cssccsssccsccccsccc"
id="path3656"
d="M 64.09375,3.90625 C 30.968558,3.9062499 4.0937499,30.781055 4.09375,63.90625 C 4.0937501,97.031442 30.96856,123.90625 64.09375,123.90625 C 94.382866,123.90625 119.43036,101.68999 123.5,72.5625 L 88.46875,67.53125 C 86.844066,79.492379 76.538676,88.5 64.09375,88.5 C 50.522122,88.499999 39.5,77.477881 39.5,63.90625 C 39.500001,50.334616 50.522119,39.3125 64.09375,39.3125 C 70.879568,39.312499 77.015044,42.077544 81.46875,46.53125 L 71.141571,56.858429 C 69.085701,58.914299 68.897846,60.164209 69.461963,61.473958 C 69.970531,62.654737 70.982695,64 73.671688,64 L 117.625,64 C 121.47089,64 124.08399,61.345621 124.08399,57.749869 L 124.01512,13.671851 C 124.01512,11.191096 123.11261,9.8286218 121.83365,9.3222363 C 120.51933,8.8018514 118.80044,9.1995576 116.72656,11.273436 L 106.53125,21.46875 C 95.671902,10.609399 80.656349,3.90625 64.09375,3.90625 z "
style="opacity:0.68913861;fill:url(#radialGradient3658);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1" />
<path
style="opacity:1;fill:url(#radialGradient3654);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 64.09375,3.90625 C 30.968558,3.9062499 4.0937499,30.781055 4.09375,63.90625 C 4.0937501,97.031442 30.96856,123.90625 64.09375,123.90625 C 94.382866,123.90625 119.43036,101.68999 123.5,72.5625 L 88.46875,67.53125 C 86.844066,79.492379 76.538676,88.5 64.09375,88.5 C 50.522122,88.499999 39.5,77.477881 39.5,63.90625 C 39.500001,50.334616 50.522119,39.3125 64.09375,39.3125 C 70.879568,39.312499 77.015044,42.077544 81.46875,46.53125 L 71.141571,56.858429 C 69.085701,58.914299 68.897846,60.164209 69.461963,61.473958 C 69.970531,62.654737 70.982695,64 73.671688,64 L 117.625,64 C 121.47089,64 124.08399,61.345621 124.08399,57.749869 L 124.01512,13.671851 C 124.01512,11.191096 123.11261,9.8286218 121.83365,9.3222363 C 120.51933,8.8018514 118.80044,9.1995576 116.72656,11.273436 L 106.53125,21.46875 C 95.671902,10.609399 80.656349,3.90625 64.09375,3.90625 z "
id="path3643"
sodipodi:nodetypes="cssccsssccsccccsccc" />
<path
sodipodi:nodetypes="cssccsssccsccccsccc"
id="path3742"
d="M 64.09375,3.90625 C 30.968558,3.9062499 4.0937499,30.781055 4.09375,63.90625 C 4.0937501,97.031442 30.96856,123.90625 64.09375,123.90625 C 94.382866,123.90625 119.43036,101.68999 123.5,72.5625 L 88.46875,67.53125 C 86.844066,79.492379 76.538676,88.5 64.09375,88.5 C 50.522122,88.499999 39.5,77.477881 39.5,63.90625 C 39.500001,50.334616 50.522119,39.3125 64.09375,39.3125 C 70.879568,39.312499 77.015044,42.077544 81.46875,46.53125 L 71.141571,56.858429 C 69.085701,58.914299 68.897846,60.164209 69.461963,61.473958 C 69.970531,62.654737 70.982695,64 73.671688,64 L 117.625,64 C 121.47089,64 124.08399,61.345621 124.08399,57.749869 L 124.01512,13.671851 C 124.01512,11.191096 123.11261,9.8286218 121.83365,9.3222363 C 120.51933,8.8018514 118.80044,9.1995576 116.72656,11.273436 L 106.53125,21.46875 C 95.671902,10.609399 80.656349,3.90625 64.09375,3.90625 z "
style="opacity:0.79775277;fill:url(#radialGradient3744);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1" />
<path
style="opacity:0.74531836;fill:url(#radialGradient3767);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter3630)"
d="M 64.09375,4.20625 C 30.968558,4.2062499 4.0937499,30.781055 4.09375,63.90625 C 4.0937501,97.031442 30.96856,123.90625 64.09375,123.90625 C 94.382866,123.90625 119.43036,101.68999 123.5,72.5625 L 88.46875,67.53125 C 86.844066,79.492379 76.538676,88.5 64.09375,88.5 C 50.522122,88.499999 39.5,77.477881 39.5,63.90625 C 39.500001,50.334616 50.522119,39.3125 64.09375,39.3125 C 70.879568,39.312499 77.015044,42.077544 81.46875,46.53125 L 71.141571,56.858429 C 69.085701,58.914299 68.897846,60.164209 69.461963,61.473958 C 69.970531,62.654737 70.982695,64 73.671688,64 L 117.625,64 C 121.47089,64 124.08399,61.345621 124.08399,57.749869 L 124.01512,13.671851 C 124.01512,11.191096 123.11261,9.8286218 121.83365,9.3222363 C 120.51933,8.8018514 118.80044,9.1995576 116.72656,11.273436 L 106.53125,21.46875 C 95.671902,10.609399 80.656349,4.20625 64.09375,4.20625 z "
id="path3759"
sodipodi:nodetypes="cssccsssccsccccsccc" />
<path
sodipodi:nodetypes="cccczc"
id="path3660"
d="M 117.6875,10.75 L 118.625,15.125 L 119.875,16 L 123.875,13.375 C 124.12188,11.651249 123.52383,10.027571 121.9375,9.3749999 C 120.35116,8.7224285 118.77622,9.5017032 117.6875,10.75 z "
style="opacity:0.82022472;fill:url(#linearGradient3676);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3738)" />
<path
transform="matrix(0,1,1,0,60.363582,-60.363586)"
style="opacity:0.82022472;fill:url(#linearGradient3771);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3738)"
d="M 117.6875,10.75 L 119.875,13.875 L 120.375,13.75 L 123.875,13.375 C 124.12188,11.651249 123.52383,10.027571 121.9375,9.3749999 C 120.35116,8.7224285 118.77622,9.5017032 117.6875,10.75 z "
id="path3769"
sodipodi:nodetypes="cccczc" />
<path
id="path3494"
d="M 64.09375,7.90625 C 33.132052,7.9062499 8.0937499,32.944549 8.09375,63.90625 C 8.0937501,94.867948 33.132054,119.90625 64.09375,119.90625 C 91.026646,119.90625 113.21548,101.0995 118.625,75.90625 L 91.5,72.03125 C 88.061436,83.928551 77.059621,92.5 64.09375,92.5 C 48.356404,92.499999 35.5,79.643599 35.5,63.90625 C 35.500001,48.168899 48.356402,35.3125 64.09375,35.3125 C 71.966166,35.312499 79.145304,38.520304 84.3125,43.6875 C 85.071964,44.438909 85.499997,45.462886 85.5,46.53125 C 85.5,47.599614 85.071964,48.623591 84.3125,49.375 L 73.6875,60 L 117.625,60 C 119.63039,60 120.09375,59.407836 120.09375,57.75 L 120,13.65625 L 109.375,24.3125 C 108.62359,25.071964 107.59961,25.5 106.53125,25.5 C 105.46289,25.5 104.43891,25.071964 103.6875,24.3125 C 93.549835,14.174833 79.577106,7.90625 64.09375,7.90625 z "
style="opacity:1;fill:url(#radialGradient3524);fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1" />
<path
style="opacity:1;fill:url(#radialGradient3535);fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 64.09375,7.90625 C 33.132052,7.9062499 8.0937499,32.944549 8.09375,63.90625 C 8.0937501,94.867948 33.132054,119.90625 64.09375,119.90625 C 91.026646,119.90625 113.21548,101.0995 118.625,75.90625 L 91.5,72.03125 C 88.061436,83.928551 77.059621,92.5 64.09375,92.5 C 48.356404,92.499999 35.5,79.643599 35.5,63.90625 C 35.500001,48.168899 48.356402,35.3125 64.09375,35.3125 C 71.966166,35.312499 79.145304,38.520304 84.3125,43.6875 C 85.071964,44.438909 85.499997,45.462886 85.5,46.53125 C 85.5,47.599614 85.071964,48.623591 84.3125,49.375 L 73.6875,60 L 117.625,60 C 119.63039,60 120.09375,59.407836 120.09375,57.75 L 120,13.65625 L 109.375,24.3125 C 108.62359,25.071964 107.59961,25.5 106.53125,25.5 C 105.46289,25.5 104.43891,25.071964 103.6875,24.3125 C 93.549835,14.174833 79.577106,7.90625 64.09375,7.90625 z "
id="path3526" />
<path
sodipodi:nodetypes="csccssccccccscc"
id="path3537"
d="M 64.09375,7.90625 C 33.132052,7.9062499 8.0937499,32.944549 8.09375,63.90625 C 8.09375,64.474122 8.1082724,65.029981 8.125,65.59375 C 14.11447,66.271402 20.266218,66.74388 26.53125,67 C 26.260548,56.540958 30.202859,46.025084 38.34375,38.21875 C 53.683067,23.509813 78.072313,24.004431 92.78125,39.34375 C 95.545099,42.226046 97.537852,45.032117 99.34375,48.59375 L 78.84375,59 L 98,59 C 105.9282,56.973373 113.18621,55.563033 120.09375,52.8125 L 120,13.65625 L 109.375,24.3125 C 108.62359,25.071964 107.59961,25.5 106.53125,25.5 C 105.46289,25.5 104.43891,25.071964 103.6875,24.3125 C 93.549835,14.174833 79.577106,7.90625 64.09375,7.90625 z "
style="opacity:1;fill:url(#linearGradient3548);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1" />
<path
sodipodi:nodetypes="cscscscccccccccccssssssccscscc"
id="path3553"
d="M 64.099866,7.9087646 C 33.138176,7.9087644 8.0998661,32.947063 8.0998661,63.908764 C 8.0998761,64.087476 8.0973761,64.263059 8.0998661,64.440014 C 8.3531061,33.696509 33.295846,8.9087645 64.099866,8.9087646 C 79.583236,8.9087645 93.555946,15.177347 103.69361,25.315014 C 104.44503,26.074479 105.469,26.502514 106.53736,26.502514 C 107.60573,26.502515 108.6297,26.074478 109.38111,25.315014 L 119.50611,15.158764 L 119.99986,52.708764 C 113.09232,55.459294 105.43431,56.569624 97.506116,58.596264 L 78.849866,59.002514 L 98.006116,59.002514 C 105.93431,56.975884 113.19232,55.565544 120.09986,52.815014 L 120.00611,14.658764 L 120.00611,13.658764 L 119.50611,14.158764 L 109.38111,24.315014 C 108.62971,25.074479 107.60572,25.502514 106.53736,25.502514 C 105.46901,25.502515 104.44502,25.074478 103.69361,24.315014 C 103.68314,24.304548 103.67283,24.294222 103.66236,24.283764 C 103.60999,24.231473 103.55869,24.179598 103.50611,24.127514 C 102.93231,23.559643 102.35524,23.012364 101.75611,22.471264 C 101.67459,22.397145 101.58807,22.326157 101.50611,22.252514 C 91.590066,13.342335 78.496526,7.9087646 64.099866,7.9087646 z M 63.443616,27.127514 C 54.205446,27.378034 45.040176,30.920194 37.849866,37.815014 C 30.217786,45.133448 26.722316,55.187931 26.537366,65.033764 C 26.777246,55.231884 30.717786,45.539698 38.349866,38.221264 C 51.665996,25.452364 71.803196,24.123207 86.506116,34.033764 C 79.627056,29.22869 71.518656,26.908534 63.443616,27.127514 z "
style="opacity:1;fill:url(#linearGradient3581);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1" />
<path
id="path3603"
d="M 63.59375,7.90625 C 32.63205,7.9062499 7.59375,32.944549 7.59375,63.90625 C 7.59375,94.867948 32.63205,119.90625 63.59375,119.90625 C 90.52665,119.90625 112.71548,101.0995 118.125,75.90625 L 91,72.03125 C 87.56144,83.928551 76.55962,92.5 63.59375,92.5 C 47.8564,92.499999 35,79.643599 35,63.90625 C 35,48.168899 47.8564,35.3125 63.59375,35.3125 C 71.46617,35.312499 78.6453,38.520304 83.8125,43.6875 C 84.57196,44.438909 85,45.462886 85,46.53125 C 85,47.599614 84.57196,48.623591 83.8125,49.375 L 73.1875,60 L 117.125,60 C 119.13039,60 119.59375,59.407836 119.59375,57.75 L 119.5,13.65625 L 108.875,24.3125 C 108.12359,25.071964 107.09961,25.5 106.03125,25.5 C 104.96289,25.5 103.93891,25.071964 103.1875,24.3125 C 93.04984,14.174833 79.07711,7.90625 63.59375,7.90625 z "
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient3567);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter3639)" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -12,13 +12,24 @@ defaults.
# The algorithm used to assign a new book in an existing series a series number. # The algorithm used to assign a new book in an existing series a series number.
# New series numbers assigned using this tweak are always integer values, except
# if a constant non-integer is specified.
# Possible values are: # Possible values are:
# next - Next available number # next - First available integer larger than the largest existing number
# first_free - First available integer larger than 0
# next_free - First available integer larger than the smallest existing number
# last_free - First available integer smaller than the largest existing number
# Return largest existing + 1 if no free number is found
# const - Assign the number 1 always # const - Assign the number 1 always
# a number - Assign that number always. The number is not in quotes. Note that
# 0.0 can be used here.
# Examples:
# series_index_auto_increment = 'next'
# series_index_auto_increment = 'next_free'
# series_index_auto_increment = 16.5
series_index_auto_increment = 'next' series_index_auto_increment = 'next'
# The algorithm used to copy author to author_sort # The algorithm used to copy author to author_sort
# Possible values are: # Possible values are:
# invert: use "fn ln" -> "ln, fn" (the original algorithm) # invert: use "fn ln" -> "ln, fn" (the original algorithm)
@ -30,6 +41,20 @@ series_index_auto_increment = 'next'
# selecting 'manage authors', and pressing 'Recalculate all author sort values'. # selecting 'manage authors', and pressing 'Recalculate all author sort values'.
author_sort_copy_method = 'invert' author_sort_copy_method = 'invert'
# Set which author field to display in the tags pane (the list of authors,
# series, publishers etc on the left hand side). The choices are author and
# author_sort. This tweak affects only what is displayed under the authors
# category in the tags pane and content server. Please note that if you set this
# to author_sort, it is very possible to see duplicate names in the list because
# although it is guaranteed that author names are unique, there is no such
# guarantee for author_sort values. Showing duplicates won't break anything, but
# it could lead to some confusion. When using 'author_sort', the tooltip will
# show the author's name.
# Examples:
# categories_use_field_for_author_name = 'author'
# categories_use_field_for_author_name = 'author_sort'
categories_use_field_for_author_name = 'author'
# Set whether boolean custom columns are two- or three-valued. # Set whether boolean custom columns are two- or three-valued.
# Two-values for true booleans # Two-values for true booleans
@ -235,3 +260,9 @@ doubleclick_on_library_view = 'open_viewer'
# Example: locale_for_sorting = 'fr' -- sort using French rules. # Example: locale_for_sorting = 'fr' -- sort using French rules.
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules. # Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
locale_for_sorting = '' locale_for_sorting = ''
# Set whether to use one or two columns for custom metadata when editing
# metadata one book at a time. If True, then the fields are laid out using two
# columns. If False, one column is used.
metadata_single_use_2_cols_for_custom_fields = True

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -11,6 +11,7 @@ class APOD(BasicNewsRecipe):
remove_javascript = True remove_javascript = True
recursions = 0 recursions = 0
oldest_article = 14 oldest_article = 14
remove_attributes = ['onmouseover', 'onmouseout']
feeds = [ feeds = [
(u'Astronomy Picture of the Day', u'http://apod.nasa.gov/apod.rss') (u'Astronomy Picture of the Day', u'http://apod.nasa.gov/apod.rss')

View File

@ -1,64 +1,102 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008 Kovid Goyal kovid@kovidgoyal.net, 2010 Darko Miletic <darko.miletic at gmail.com>'
''' '''
http://www.businessweek.com/magazine/news/articles/business_news.htm www.businessweek.com
''' '''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class BWmagazine(BasicNewsRecipe): class BusinessWeek(BasicNewsRecipe):
title = 'BusinessWeek Magazine' title = 'Business Week'
__author__ = 'Darko Miletic' __author__ = 'Kovid Goyal and Darko Miletic'
description = 'Stay up to date with BusinessWeek magazine articles. Read news on international business, personal finances & the economy in the BusinessWeek online magazine.' description = 'Read the latest international business news & stock market news. Get updated company profiles, financial advice, global economy and technology news.'
publisher = 'Bloomberg L.P.' publisher = 'Bloomberg L.P.'
category = 'news, International Business News, current news in international business,international business articles, personal business, business week magazine, business week magazine articles, business week magazine online, business week online magazine' category = 'Business, business news, stock market, stock market news, financial advice, company profiles, financial advice, global economy, technology news'
oldest_article = 10 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 200
no_stylesheets = True no_stylesheets = True
encoding = 'utf-8' encoding = 'utf8'
use_embedded_content = False use_embedded_content = False
language = 'en' language = 'en'
INDEX = 'http://www.businessweek.com/magazine/news/articles/business_news.htm' remove_empty_feeds = True
publication_type = 'magazine'
cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg' cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg'
masthead_url = 'http://assets.businessweek.com/images/bw-logo.png'
extra_css = """
body{font-family: Helvetica,Arial,sans-serif }
img{margin-bottom: 0.4em; display:block}
.tagline{color: gray; font-style: italic}
.photoCredit{font-size: small; color: gray}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
, 'publisher' : publisher , 'publisher' : publisher
, 'language' : language , 'language' : language
} }
remove_tags = [
dict(attrs={'class':'inStory'})
,dict(name=['meta','link','iframe','base','embed','object','table','th','tr','td'])
,dict(attrs={'id':['inset','videoDisplay']})
]
keep_only_tags = [dict(name='div', attrs={'id':['story-body','storyBody']})]
remove_attributes = ['lang']
match_regexps = [r'http://www.businessweek.com/.*_page_[1-9].*']
def parse_index(self):
articles = []
soup = self.index_to_soup(self.INDEX)
ditem = soup.find('div',attrs={'id':'column2'})
if ditem:
for item in ditem.findAll('h3'):
title_prefix = ''
description = ''
feed_link = item.find('a')
if feed_link and feed_link.has_key('href'):
url = 'http://www.businessweek.com/magazine/' + feed_link['href'].partition('../../')[2]
title = title_prefix + self.tag_to_string(feed_link)
date = strftime(self.timefmt)
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
return [(soup.head.title.string, articles)]
keep_only_tags = dict(name='div', attrs={'id':'storyBody'}) feeds = [
(u'Top Stories', u'http://www.businessweek.com/topStories/rss/topStories.rss'),
(u'Top News' , u'http://www.businessweek.com/rss/bwdaily.rss' ),
(u'Asia', u'http://www.businessweek.com/rss/asia.rss'),
(u'Autos', u'http://www.businessweek.com/rss/autos/index.rss'),
(u'Classic Cars', u'http://rss.businessweek.com/bw_rss/classiccars'),
(u'Hybrids', u'http://rss.businessweek.com/bw_rss/hybrids'),
(u'Europe', u'http://www.businessweek.com/rss/europe.rss'),
(u'Auto Reviews', u'http://rss.businessweek.com/bw_rss/autoreviews'),
(u'Innovation & Design', u'http://www.businessweek.com/rss/innovate.rss'),
(u'Architecture', u'http://www.businessweek.com/rss/architecture.rss'),
(u'Brand Equity', u'http://www.businessweek.com/rss/brandequity.rss'),
(u'Auto Design', u'http://www.businessweek.com/rss/carbuff.rss'),
(u'Game Room', u'http://rss.businessweek.com/bw_rss/gameroom'),
(u'Technology', u'http://www.businessweek.com/rss/technology.rss'),
(u'Investing', u'http://rss.businessweek.com/bw_rss/investor'),
(u'Small Business', u'http://www.businessweek.com/rss/smallbiz.rss'),
(u'Careers', u'http://rss.businessweek.com/bw_rss/careers'),
(u'B-Schools', u'http://www.businessweek.com/rss/bschools.rss'),
(u'Magazine Selections', u'http://www.businessweek.com/rss/magazine.rss'),
(u'CEO Guide to Tech', u'http://www.businessweek.com/rss/ceo_guide_tech.rss'),
]
def get_article_url(self, article):
url = article.get('guid', None)
if 'podcasts' in url:
return None
if 'surveys' in url:
return None
if 'images' in url:
return None
if 'feedroom' in url:
return None
if '/magazine/toc/' in url:
return None
rurl, sep, rest = url.rpartition('?')
if rurl:
return rurl
return rest
def print_version(self, url): def print_version(self, url):
rurl = url.rpartition('?')[0] if '/news/' in url or '/blog/ in url':
if rurl == '': return url
rurl = url rurl = url.replace('http://www.businessweek.com/','http://www.businessweek.com/print/')
return rurl.replace('.com/magazine/','.com/print/magazine/') return rurl.replace('/investing/','/investor/')
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Derek Liang <Derek.liang.ca @@@at@@@ gmail.com>'
'''
cnd.org
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class TheCND(BasicNewsRecipe):
title = 'CND'
__author__ = 'Derek Liang'
description = ''
INDEX = 'http://cnd.org'
language = 'zh'
conversion_options = {'linearize_tables':True}
remove_tags_before = dict(name='div', id='articleHead')
remove_tags_after = dict(id='copyright')
remove_tags = [dict(name='table', attrs={'align':'right'}), dict(name='img', attrs={'src':'http://my.cnd.org/images/logo.gif'}), dict(name='hr', attrs={}), dict(name='small', attrs={})]
no_stylesheets = True
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
def print_version(self, url):
if url.find('news/article.php') >= 0:
return re.sub("^[^=]*", "http://my.cnd.org/modules/news/print.php?storyid", url)
else:
return re.sub("^[^=]*", "http://my.cnd.org/modules/wfsection/print.php?articleid", url)
def parse_index(self):
soup = self.index_to_soup(self.INDEX)
feeds = []
articles = {}
for a in soup.findAll('a', attrs={'target':'_cnd'}):
url = a['href']
if url.find('article.php') < 0 :
continue
if url.startswith('/'):
url = 'http://cnd.org'+url
title = self.tag_to_string(a)
self.log('\tFound article: ', title, 'at', url)
date = a.nextSibling
if (date is not None) and len(date)>2:
if not articles.has_key(date):
articles[date] = []
articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
self.log('\t\tAppend to : ', date)
self.log('log articles', articles)
mostCurrent = sorted(articles).pop()
self.title = 'CND ' + mostCurrent
feeds.append((self.title, articles[mostCurrent]))
return feeds
def populate_article_metadata(self, article, soup, first):
header = soup.find('h3')
self.log('header: ' + self.tag_to_string(header))
pass

View File

@ -0,0 +1,42 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
globaleconomicanalysis.blogspot.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class GlobalEconomicAnalysis(BasicNewsRecipe):
title = "Mish's Global Economic Trend Analysis"
__author__ = 'Darko Miletic'
description = 'Thoughts on the global economy, housing, gold, silver, interest rates, oil, energy, China, commodities, the dollar, Euro, Renminbi, Yen, inflation, deflation, stagflation, precious metals, emerging markets, and policy decisions that affect the global markets.'
publisher = 'Mike Shedlock'
category = 'news, politics, economy, banking'
oldest_article = 7
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = True
language = 'en'
remove_empty_feeds = True
publication_type = 'blog'
masthead_url = 'http://www.pagina12.com.ar/commons/imgs/logo-home.gif'
extra_css = """
body{font-family: Arial,Helvetica,sans-serif }
img{margin-bottom: 0.4em; display:block}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
dict(name=['meta','link','iframe','object','embed'])
,dict(attrs={'class':'blogger-post-footer'})
]
remove_attributes=['border']
feeds = [(u'Articles', u'http://feeds2.feedburner.com/MishsGlobalEconomicTrendAnalysis')]

View File

@ -40,13 +40,12 @@ class GazetvanAntwerpen(BasicNewsRecipe):
remove_tags_after = dict(name='span', attrs={'class':'author'}) remove_tags_after = dict(name='span', attrs={'class':'author'})
feeds = [ feeds = [
(u'Overzicht & Blikvanger', u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/overview/overzicht' ) (u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' )
,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' )
,(u'Stad & Regio' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/stadenregio' ) ,(u'Stad & Regio' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/stadenregio' )
,(u'Economie' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/economie' ) ,(u'Economie' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/economie' )
,(u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' )
,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' )
,(u'Media & Cultur' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur') ,(u'Media & Cultur' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur')
,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur') ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/wetenschap' )
,(u'Sport' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/sport' ) ,(u'Sport' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/sport' )
] ]

View File

@ -1,88 +1,72 @@
# -*- coding: utf-8 -*- import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
class JournalofHospitalMedicine(BasicNewsRecipe): class JournalofHospitalMedicine(BasicNewsRecipe):
title = 'Journal of Hospital Medicine' title = 'Journal of Hospital Medicine'
__author__ = 'Krittika Goyal' __author__ = 'Kovid Goyal'
description = 'Medical news' description = 'Medical news'
timefmt = ' [%d %b, %Y]' timefmt = ' [%d %b, %Y]'
needs_subscription = True needs_subscription = True
language = 'en' language = 'en'
no_stylesheets = True no_stylesheets = True
#remove_tags_before = dict(name='div', attrs={'align':'center'}) keep_only_tags = [dict(id=['articleTitle', 'articleMeta', 'fulltext'])]
#remove_tags_after = dict(name='ol', attrs={'compact':'COMPACT'}) remove_tags = [dict(attrs={'class':'licensedContent'})]
remove_tags = [
dict(name='iframe'),
dict(name='div', attrs={'class':'subContent'}),
dict(name='div', attrs={'id':['contentFrame']}),
#dict(name='form', attrs={'onsubmit':"return verifySearch(this.w,'Keyword, citation, or author')"}),
#dict(name='table', attrs={'align':'RIGHT'}),
]
# TO LOGIN # TO LOGIN
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open('http://www3.interscience.wiley.com/cgi-bin/home') br.open('http://www3.interscience.wiley.com/cgi-bin/home')
br.select_form(name='siteLogin') br.select_form(nr=0)
br['LoginName'] = self.username br['j_username'] = self.username
br['Password'] = self.password br['j_password'] = self.password
response = br.submit() response = br.submit()
raw = response.read() raw = response.read()
if 'userName = ""' in raw: if '<h2>LOGGED IN</h2>' not in raw:
raise Exception('Login failed. Check your username and password') raise Exception('Login failed. Check your username and password')
return br return br
#TO GET ARTICLE TOC #TO GET ARTICLE TOC
def johm_get_index(self): def johm_get_index(self):
return self.index_to_soup('http://www3.interscience.wiley.com/journal/111081937/home') return self.index_to_soup('http://onlinelibrary.wiley.com/journal/10.1002/(ISSN)1553-5606/currentissue')
# To parse artice toc # To parse artice toc
def parse_index(self): def parse_index(self):
parse_soup = self.johm_get_index() soup = self.johm_get_index()
toc = soup.find(id='issueTocGroups')
feeds = []
for group in toc.findAll('li', id=re.compile(r'group\d+')):
gtitle = group.find(attrs={'class':'subSectionHeading'})
if gtitle is None:
continue
gtitle = self.tag_to_string(gtitle)
arts = group.find(attrs={'class':'articles'})
if arts is None:
continue
self.log('Found section:', gtitle)
articles = []
for art in arts.findAll(attrs={'class':lambda x: x and 'tocArticle'
in x}):
a = art.find('a', href=True)
if a is None:
continue
url = a.get('href')
if url.startswith('/'):
url = 'http://onlinelibrary.wiley.com' + url
url = url.replace('/abstract', '/full')
title = self.tag_to_string(a)
a.extract()
pm = art.find(attrs={'class':'productMenu'})
if pm is not None:
pm.extract()
desc = self.tag_to_string(art)
self.log('\tFound article:', title, 'at', url)
articles.append({'title':title, 'url':url, 'description':desc,
'date':''})
if articles:
feeds.append((gtitle, articles))
div = parse_soup.find(id='contentCell') return feeds
current_section = None
current_articles = []
feeds = []
for x in div.findAll(True):
if x.name == 'h4':
# Section heading found
if current_articles and current_section:
feeds.append((current_section, current_articles))
current_section = self.tag_to_string(x)
current_articles = []
self.log('\tFound section:', current_section)
if current_section is not None and x.name == 'strong':
title = self.tag_to_string(x)
p = x.parent.parent.find('a', href=lambda x: x and '/HTMLSTART' in x)
if p is None:
continue
url = p.get('href', False)
if not url or not title:
continue
if url.startswith('/'):
url = 'http://www3.interscience.wiley.com'+url
url = url.replace('/HTMLSTART', '/main.html,ftx_abs')
self.log('\t\tFound article:', title)
self.log('\t\t\t', url)
#if url.startswith('/'):
#url = 'http://online.wsj.com'+url
current_articles.append({'title': title, 'url':url,
'description':'', 'date':''})
if current_articles and current_section:
feeds.append((current_section, current_articles))
return feeds
def preprocess_html(self, soup):
for img in soup.findAll('img', src=True):
img['src'] = img['src'].replace('tfig', 'nfig')
return soup

View File

@ -78,4 +78,6 @@ class Lanacion(BasicNewsRecipe):
] ]
def preprocess_html(self, soup): def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup) return self.adeify_images(soup)

View File

@ -4,7 +4,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class LeMonde(BasicNewsRecipe): class LeMonde(BasicNewsRecipe):
title = 'Le Monde' title = 'Le Monde'
__author__ = 'veezh' __author__ = 'veezh'
description = 'Actualités' description = u'Actualit\xe9s'
oldest_article = 1 oldest_article = 1
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True

View File

@ -4,23 +4,14 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class NYTimes(BasicNewsRecipe): class NYTimes(BasicNewsRecipe):
title = 'New England Journal of Medicine' title = 'New England Journal of Medicine'
__author__ = 'Krittika Goyal' __author__ = 'Kovid Goyal'
description = 'Medical news' description = 'Medical news'
timefmt = ' [%d %b, %Y]' timefmt = ' [%d %b, %Y]'
needs_subscription = True needs_subscription = True
language = 'en' language = 'en'
no_stylesheets = True no_stylesheets = True
remove_tags_before = dict(name='div', attrs={'align':'center'}) keep_only_tags = dict(id='content')
remove_tags_after = dict(name='ol', attrs={'compact':'COMPACT'})
remove_tags = [
dict(name='iframe'),
#dict(name='div', attrs={'class':'related-articles'}),
dict(name='div', attrs={'id':['sidebar']}),
#dict(name='form', attrs={'onsubmit':"return verifySearch(this.w,'Keyword, citation, or author')"}),
dict(name='table', attrs={'align':'RIGHT'}),
]
#TO LOGIN #TO LOGIN
@ -38,61 +29,50 @@ class NYTimes(BasicNewsRecipe):
#TO GET ARTICLE TOC #TO GET ARTICLE TOC
def nejm_get_index(self): def nejm_get_index(self):
return self.index_to_soup('http://content.nejm.org/current.dtl') return self.index_to_soup('http://content.nejm.org/current.dtl')
# To parse artice toc # To parse artice toc
def parse_index(self): def parse_index(self):
parse_soup = self.nejm_get_index() parse_soup = self.nejm_get_index()
div = parse_soup.find(id='centerTOC') feeds = []
current_section = None div = parse_soup.find(attrs={'class':'tocContent'})
current_articles = [] for group in div.findAll(attrs={'class':'articleGrouping'}):
feeds = [] feed_title = group.find(attrs={'class':'articleType'})
for x in div.findAll(True): if feed_title is None:
if x.name == 'img' and '/toc/' in x.get('src', '') and 'uarrow.gif' not in x.get('src', ''):
# Section heading found
if current_articles and current_section and 'Week in the' not in current_section:
feeds.append((current_section, current_articles))
current_section = x.get('alt')
current_articles = []
self.log('\tFound section:', current_section)
if current_section is not None and x.name == 'strong':
title = self.tag_to_string(x)
a = x.parent.find('a', href=lambda x: x and '/full/' in x)
if a is None:
continue
url = a.get('href', False)
if not url or not title:
continue
if url.startswith('/'):
url = 'http://content.nejm.org'+url
self.log('\t\tFound article:', title)
self.log('\t\t\t', url)
if url.startswith('/'):
url = 'http://online.wsj.com'+url
current_articles.append({'title': title, 'url':url,
'description':'', 'date':''})
if current_articles and current_section:
feeds.append((current_section, current_articles))
return feeds
def preprocess_html(self, soup):
for a in soup.findAll(text=lambda x: x and '[in this window]' in x):
a = a.findParent('a')
url = a.get('href', None)
if not url:
continue continue
if url.startswith('/'): feed_title = self.tag_to_string(feed_title)
url = 'http://content.nejm.org'+url articles = []
isoup = self.index_to_soup(url) self.log('Found section:', feed_title)
img = isoup.find('img', src=lambda x: x and for art in group.findAll(attrs={'class':lambda x: x and 'articleEntry'
x.startswith('/content/')) in x}):
if img is not None: link = art.find(attrs={'class':lambda x:x and 'articleLink' in
img.extract() x})
table = a.findParent('table') if link is None:
table.replaceWith(img) continue
return soup a = link.find('a', href=True)
if a is None:
continue
url = a.get('href')
if url.startswith('/'):
url = 'http://www.nejm.org'+url
title = self.tag_to_string(a)
self.log.info('\tFound article:', title, 'at', url)
article = {'title':title, 'url':url, 'date':''}
au = art.find(attrs={'class':'articleAuthors'})
if au is not None:
article['author'] = self.tag_to_string(au)
desc = art.find(attrs={'class':'hover_text'})
if desc is not None:
desc = self.tag_to_string(desc)
if 'author' in article:
desc = ' by ' + article['author'] + ' ' +desc
article['description'] = desc
articles.append(article)
if articles:
feeds.append((feed_title, articles))
return feeds

View File

@ -5,6 +5,7 @@ newscientist.com
''' '''
import re import re
import urllib
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class NewScientist(BasicNewsRecipe): class NewScientist(BasicNewsRecipe):
@ -24,7 +25,7 @@ class NewScientist(BasicNewsRecipe):
needs_subscription = 'optional' needs_subscription = 'optional'
extra_css = """ extra_css = """
body{font-family: Arial,sans-serif} body{font-family: Arial,sans-serif}
img{margin-bottom: 0.8em} img{margin-bottom: 0.8em; display: block}
.quotebx{font-size: x-large; font-weight: bold; margin-right: 2em; margin-left: 2em} .quotebx{font-size: x-large; font-weight: bold; margin-right: 2em; margin-left: 2em}
""" """
@ -41,12 +42,14 @@ class NewScientist(BasicNewsRecipe):
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open('http://www.newscientist.com/') br.open('http://www.newscientist.com/')
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
br.open('https://www.newscientist.com/user/login?redirectURL=') br.open('https://www.newscientist.com/user/login')
br.select_form(nr=2) data = urllib.urlencode({ 'source':'form'
br['loginId' ] = self.username ,'redirectURL':''
br['password'] = self.password ,'loginId':self.username
br.submit() ,'password':self.password
})
br.open('https://www.newscientist.com/user/login',data)
return br return br
remove_tags = [ remove_tags = [
@ -55,21 +58,22 @@ class NewScientist(BasicNewsRecipe):
,dict(name='p' , attrs={'class':['marker','infotext' ]}) ,dict(name='p' , attrs={'class':['marker','infotext' ]})
,dict(name='meta' , attrs={'name' :'description' }) ,dict(name='meta' , attrs={'name' :'description' })
,dict(name='a' , attrs={'rel' :'tag' }) ,dict(name='a' , attrs={'rel' :'tag' })
,dict(name='ul' , attrs={'class':'markerlist' })
,dict(name=['link','base','meta','iframe','object','embed']) ,dict(name=['link','base','meta','iframe','object','embed'])
] ]
remove_tags_after = dict(attrs={'class':['nbpcopy','comments']}) remove_tags_after = dict(attrs={'class':['nbpcopy','comments']})
remove_attributes = ['height','width','lang'] remove_attributes = ['height','width','lang','onclick']
feeds = [ feeds = [
(u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' ) (u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' )
,(u'Magazine' , u'http://www.newscientist.com/feed/magazine' ) ,(u'Magazine' , u'http://feeds.newscientist.com/magazine' )
,(u'Health' , u'http://www.newscientist.com/feed/view?id=2&type=channel' ) ,(u'Health' , u'http://feeds.newscientist.com/health' )
,(u'Life' , u'http://www.newscientist.com/feed/view?id=3&type=channel' ) ,(u'Life' , u'http://feeds.newscientist.com/life' )
,(u'Space' , u'http://www.newscientist.com/feed/view?id=6&type=channel' ) ,(u'Space' , u'http://feeds.newscientist.com/space' )
,(u'Physics and Mathematics' , u'http://www.newscientist.com/feed/view?id=4&type=channel' ) ,(u'Physics and Mathematics' , u'http://feeds.newscientist.com/physics-math' )
,(u'Environment' , u'http://www.newscientist.com/feed/view?id=1&type=channel' ) ,(u'Environment' , u'http://feeds.newscientist.com/environment' )
,(u'Science in Society' , u'http://www.newscientist.com/feed/view?id=5&type=channel' ) ,(u'Science in Society' , u'http://feeds.newscientist.com/science-in-society' )
,(u'Tech' , u'http://www.newscientist.com/feed/view?id=7&type=channel' ) ,(u'Tech' , u'http://feeds.newscientist.com/tech' )
] ]
def get_article_url(self, article): def get_article_url(self, article):
@ -79,11 +83,21 @@ class NewScientist(BasicNewsRecipe):
return url + '?full=true&print=true' return url + '?full=true&print=true'
def preprocess_html(self, soup): def preprocess_html(self, soup):
if soup.html.has_key('id'):
del soup.html['id']
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll(['quote','quotetext']): for item in soup.findAll(['quote','quotetext']):
item.name='p' item.name='p'
for item in soup.findAll(['xref','figref']):
tstr = item.string
item.replaceWith(tstr)
for tg in soup.findAll('a'): for tg in soup.findAll('a'):
if tg.string == 'Home': if tg.string == 'Home':
tg.parent.extract() tg.parent.extract()
return self.adeify_images(soup) else:
return self.adeify_images(soup) if tg.string is not None:
tstr = tg.string
tg.replaceWith(tstr)
return soup

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#Based on Lars Jacob's Taz Digiabo recipe
__license__ = 'GPL v3'
__copyright__ = '2010, veezh'
'''
www.nrc.nl
'''
import os, urllib2, zipfile
import time
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ptempfile import PersistentTemporaryFile
class NRCHandelsblad(BasicNewsRecipe):
title = u'NRC Handelsblad'
description = u'De EPUB-versie van NRC'
language = 'nl'
lang = 'nl-NL'
__author__ = 'veezh'
conversion_options = {
'no_default_epub_cover' : True
}
def build_index(self):
today = time.strftime("%Y%m%d")
domain = "http://digitaleeditie.nrc.nl"
url = domain + "/digitaleeditie/helekrant/epub/nrc_" + today + ".epub"
# print url
try:
f = urllib2.urlopen(url)
except urllib2.HTTPError:
self.report_progress(0,_('Kan niet inloggen om editie te downloaden'))
raise ValueError('Krant van vandaag nog niet beschikbaar')
tmp = PersistentTemporaryFile(suffix='.epub')
self.report_progress(0,_('downloading epub'))
tmp.write(f.read())
tmp.close()
zfile = zipfile.ZipFile(tmp.name, 'r')
self.report_progress(0,_('extracting epub'))
zfile.extractall(self.output_dir)
tmp.close()
index = os.path.join(self.output_dir, 'content.opf')
self.report_progress(1,_('epub downloaded and extracted'))
return index

View File

@ -13,14 +13,16 @@ class Radikal_tr(BasicNewsRecipe):
description = 'News from Turkey' description = 'News from Turkey'
publisher = 'radikal' publisher = 'radikal'
category = 'news, politics, Turkey' category = 'news, politics, Turkey'
oldest_article = 2 oldest_article = 7
max_articles_per_feed = 150 max_articles_per_feed = 150
no_stylesheets = True no_stylesheets = True
encoding = 'cp1254' encoding = 'cp1254'
use_embedded_content = False use_embedded_content = False
masthead_url = 'http://www.radikal.com.tr/D/i/1/V2/radikal_logo.jpg' masthead_url = 'http://www.radikal.com.tr/D/i/1/V2/radikal_logo.jpg'
language = 'tr' language = 'tr'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif } ' extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
.article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
@ -34,7 +36,13 @@ class Radikal_tr(BasicNewsRecipe):
remove_tags_after = dict(attrs={'id':'haberDetayYazi'}) remove_tags_after = dict(attrs={'id':'haberDetayYazi'})
feeds = [(u'Yazarlar', u'http://www.radikal.com.tr/d/rss/RssYazarlar.xml')] feeds = [
(u'Yazarlar' , u'http://www.radikal.com.tr/d/rss/RssYazarlar.xml')
,(u'Turkiye' , u'http://www.radikal.com.tr/d/rss/Rss_97.xml' )
,(u'Politika' , u'http://www.radikal.com.tr/d/rss/Rss_98.xml' )
,(u'Dis Haberler', u'http://www.radikal.com.tr/d/rss/Rss_100.xml' )
,(u'Ekonomi' , u'http://www.radikal.com.tr/d/rss/Rss_101.xml' )
]
def print_version(self, url): def print_version(self, url):
articleid = url.rpartition('ArticleID=')[2] articleid = url.rpartition('ArticleID=')[2]

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Derek Liang <Derek.liang.ca @@@at@@@ gmail.com>'
'''
wenxuecity.com
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class TheCND(BasicNewsRecipe):
title = 'wenxuecity - znjy'
__author__ = 'Derek Liang'
description = ''
INDEX = 'http://bbs.wenxuecity.com/znjy/?elite=1'
language = 'zh'
conversion_options = {'linearize_tables':True}
remove_tags_before = dict(name='div', id='message')
remove_tags_after = dict(name='div', id='message')
remove_tags = [dict(name='div', id='postmeta'), dict(name='div', id='footer')]
no_stylesheets = True
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
def print_version(self, url):
return url + '?print'
def parse_index(self):
soup = self.index_to_soup(self.INDEX)
feeds = []
articles = {}
for a in soup.findAll('a', attrs={'class':'post'}):
url = a['href']
if url.startswith('/'):
url = 'http://bbs.wenxuecity.com'+url
title = self.tag_to_string(a)
self.log('\tFound article: ', title, ' at:', url)
dateReg = re.search( '(\d\d?)/(\d\d?)/(\d\d)', self.tag_to_string(a.parent) )
date = '%(y)s/%(m)02d/%(d)02d' % {'y' : dateReg.group(3), 'm' : int(dateReg.group(1)), 'd' : int(dateReg.group(2)) }
if not articles.has_key(date):
articles[date] = []
articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
self.log('\t\tAppend to : ', date)
self.log('log articles', articles)
mostCurrent = sorted(articles).pop()
self.title = '文学城 - 子女教育 - ' + mostCurrent
feeds.append((self.title, articles[mostCurrent]))
return feeds
def populate_article_metadata(self, article, soup, first):
header = soup.find('h3')
self.log('header: ' + self.tag_to_string(header))
pass

View File

@ -46,7 +46,7 @@ class WallStreetJournal(BasicNewsRecipe):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
br.open('http://commerce.wsj.com/auth/login') br.open('http://commerce.wsj.com/auth/login')
br.select_form(nr=0) br.select_form(nr=1)
br['user'] = self.username br['user'] = self.username
br['password'] = self.password br['password'] = self.password
res = br.submit() res = br.submit()

View File

@ -318,7 +318,11 @@ class LinuxFreeze(Command):
import codecs import codecs
def set_default_encoding(): def set_default_encoding():
locale.setlocale(locale.LC_ALL, '') try:
locale.setlocale(locale.LC_ALL, '')
except:
print 'WARNING: Failed to set default libc locale, using en_US.UTF-8'
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
enc = locale.getdefaultlocale()[1] enc = locale.getdefaultlocale()[1]
if not enc: if not enc:
enc = locale.nl_langinfo(locale.CODESET) enc = locale.nl_langinfo(locale.CODESET)

View File

@ -36,6 +36,16 @@ Install BeautifulSoup 3.0.x manually into site-packages (3.1.x parses broken HTM
Install pywin32 and edit win32com\__init__.py setting _frozen = True and Install pywin32 and edit win32com\__init__.py setting _frozen = True and
__gen_path__ to a temp dir (otherwise it tries to set it to a dir in the install tree which leads to permission errors) __gen_path__ to a temp dir (otherwise it tries to set it to a dir in the install tree which leads to permission errors)
Note that you should use::
import tempfile
__gen_path__ = os.path.join(
tempfile.gettempdir(), "gen_py",
"%d.%d" % (sys.version_info[0], sys.version_info[1]))
Use gettempdir instead of the win32 api method as gettempdir returns a temp dir that is guaranteed to actually work.
Also edit win32com\client\gencache.py and change the except IOError on line 57 to catch all exceptions. Also edit win32com\client\gencache.py and change the except IOError on line 57 to catch all exceptions.
SQLite SQLite

View File

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

View File

@ -457,7 +457,8 @@ from calibre.devices.blackberry.driver import BLACKBERRY
from calibre.devices.cybook.driver import CYBOOK, ORIZON from calibre.devices.cybook.driver import CYBOOK, ORIZON
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \ from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \ POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602 BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602, \
POCKETBOOK701
from calibre.devices.iliad.driver import ILIAD from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800 from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
@ -473,10 +474,10 @@ from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
SOVOS, PICO SOVOS, PICO, SUNSTECH_EB700
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
@ -546,9 +547,7 @@ plugins += [
JETBOOK_MINI, JETBOOK_MINI,
MIBUK, MIBUK,
SHINEBOOK, SHINEBOOK,
POCKETBOOK360, POCKETBOOK360, POCKETBOOK301, POCKETBOOK602, POCKETBOOK701,
POCKETBOOK301,
POCKETBOOK602,
KINDLE, KINDLE,
KINDLE2, KINDLE2,
KINDLE_DX, KINDLE_DX,
@ -581,7 +580,7 @@ plugins += [
ELONEX, ELONEX,
TECLAST_K3, TECLAST_K3,
NEWSMY, NEWSMY,
PICO, PICO, SUNSTECH_EB700,
IPAPYRUS, IPAPYRUS,
SOVOS, SOVOS,
EDGE, EDGE,
@ -602,8 +601,9 @@ plugins += [
VELOCITYMICRO, VELOCITYMICRO,
PDNOVEL_KOBO, PDNOVEL_KOBO,
LUMIREAD, LUMIREAD,
ITUNES, ALURATEK_COLOR,
BAMBOOK, BAMBOOK,
ITUNES,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')] x.__name__.endswith('MetadataReader')]

View File

@ -23,6 +23,12 @@ Run an embedded python interpreter.
help='Debug the specified device driver.') help='Debug the specified device driver.')
parser.add_option('-g', '--gui', default=False, action='store_true', parser.add_option('-g', '--gui', default=False, action='store_true',
help='Run the GUI',) help='Run the GUI',)
parser.add_option('--gui-debug', default=None,
help='Run the GUI with a debug console, logging to the'
' specified path',)
parser.add_option('--show-gui-debug', default=None,
help='Display the specified log file.',)
parser.add_option('-w', '--viewer', default=False, action='store_true', parser.add_option('-w', '--viewer', default=False, action='store_true',
help='Run the ebook viewer',) help='Run the ebook viewer',)
parser.add_option('--paths', default=False, action='store_true', parser.add_option('--paths', default=False, action='store_true',
@ -135,7 +141,28 @@ def add_simple_plugin(path_to_plugin):
os.chdir(odir) os.chdir(odir)
shutil.rmtree(tdir) shutil.rmtree(tdir)
def run_debug_gui(logpath):
import time, platform
time.sleep(3) # Give previous GUI time to shutdown fully and release locks
from calibre.constants import __appname__, __version__, isosx
print __appname__, _('Debug log')
print __appname__, __version__
print platform.platform()
print platform.system()
print platform.system_alias(platform.system(), platform.release(),
platform.version())
print 'Python', platform.python_version()
try:
if iswindows:
print 'Windows:', platform.win32_ver()
elif isosx:
print 'OSX:', platform.mac_ver()
else:
print 'Linux:', platform.linux_distribution()
except:
pass
from calibre.gui2.main import main
main(['__CALIBRE_GUI_DEBUG__', logpath])
def main(args=sys.argv): def main(args=sys.argv):
from calibre.constants import debug from calibre.constants import debug
@ -154,6 +181,20 @@ def main(args=sys.argv):
if opts.gui: if opts.gui:
from calibre.gui2.main import main from calibre.gui2.main import main
main(['calibre']) main(['calibre'])
elif opts.gui_debug is not None:
run_debug_gui(opts.gui_debug)
elif opts.show_gui_debug:
import time, re
time.sleep(1)
from calibre.gui2 import open_local_file
if iswindows:
with open(opts.show_gui_debug, 'r+b') as f:
raw = f.read()
raw = re.sub('(?<!\r)\n', '\r\n', raw)
f.seek(0)
f.truncate()
f.write(raw)
open_local_file(opts.show_gui_debug)
elif opts.viewer: elif opts.viewer:
from calibre.gui2.viewer.main import main from calibre.gui2.viewer.main import main
vargs = ['ebook-viewer', '--debug-javascript'] vargs = ['ebook-viewer', '--debug-javascript']

View File

@ -24,11 +24,11 @@ class ANDROID(USBMS):
0xc92 : [0x100], 0xc97: [0x226]}, 0xc92 : [0x100], 0xc97: [0x226]},
# Eken # Eken
0x040d : { 0x8510 : [0x0001] }, 0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216], 0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],
0x4285 : [0x216]}, 0x4285 : [0x216], 0x42a3 : [0x216] },
# Sony Ericsson # Sony Ericsson
0xfce : { 0xd12e : [0x0100]}, 0xfce : { 0xd12e : [0x0100]},
@ -49,8 +49,9 @@ class ANDROID(USBMS):
# Dell # Dell
0x413c : { 0xb007 : [0x0100, 0x0224]}, 0x413c : { 0xb007 : [0x0100, 0x0224]},
# Eken? # LG
0x040d : { 0x0851 : [0x0001]}, 0x1004 : { 0x61cc : [0x100] },
} }
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books'] EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
@ -59,13 +60,13 @@ class ANDROID(USBMS):
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX'] 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810'] 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID'] 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD']
OSX_MAIN_MEM = 'HTC Android Phone Media' OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -688,7 +688,7 @@ class ITUNES(DriverBase):
if DEBUG: if DEBUG:
self.log.info("ITUNES:get_device_information()") self.log.info("ITUNES:get_device_information()")
return ('iDevice','hw v1.0','sw v1.0', 'mime type normally goes here') return (self.sources['iPod'],'hw v1.0','sw v1.0', 'mime type normally goes here')
def get_file(self, path, outfile, end_session=True): def get_file(self, path, outfile, end_session=True):
''' '''
@ -2775,10 +2775,19 @@ class ITUNES(DriverBase):
if lb_added: if lb_added:
lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
lb_added.EpisodeID = metadata_x.series lb_added.EpisodeID = metadata_x.series
try:
lb_added.TrackNumber = metadata_x.series_index
except:
if DEBUG:
self.log.warning(" iTunes automation interface reported an error"
" setting TrackNumber in iTunes")
try: try:
lb_added.EpisodeNumber = metadata_x.series_index lb_added.EpisodeNumber = metadata_x.series_index
except: except:
pass if DEBUG:
self.log.warning(" iTunes automation interface reported an error"
" setting EpisodeNumber in iTunes")
# If no plugboard transform applied to tags, change the Genre/Category to Series # If no plugboard transform applied to tags, change the Genre/Category to Series
if metadata.tags == metadata_x.tags: if metadata.tags == metadata_x.tags:
@ -2792,6 +2801,13 @@ class ITUNES(DriverBase):
if db_added: if db_added:
db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
db_added.EpisodeID = metadata_x.series db_added.EpisodeID = metadata_x.series
try:
db_added.TrackNumber = metadata_x.series_index
except:
if DEBUG:
self.log.warning(" iTunes automation interface reported an error"
" setting TrackNumber on iDevice")
try: try:
db_added.EpisodeNumber = metadata_x.series_index db_added.EpisodeNumber = metadata_x.series_index
except: except:

View File

@ -43,15 +43,15 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
'Unable to add book to library directly from Bambook. ' 'Unable to add book to library directly from Bambook. '
'Please save the book to disk and add the file to library from disk.') 'Please save the book to disk and add the file to library from disk.')
METADATA_CACHE = '.calibre.bambook' METADATA_CACHE = '.calibre.bambook'
METADATA_FILE_GUID = 'calibremetadata.snb' METADATA_FILE_GUID = 'calibremetadata.snb'
bambook = None bambook = None
def reset(self, key='-1', log_packets=False, report_progress=None, def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None) : detected_device=None) :
self.open() self.open()
def open(self): def open(self):
# Make sure the Bambook library is ready # Make sure the Bambook library is ready
if not is_bambook_lib_ready(): if not is_bambook_lib_ready():
@ -93,7 +93,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
if self.bambook: if self.bambook:
deviceInfo = self.bambook.GetDeviceInfo() deviceInfo = self.bambook.GetDeviceInfo()
return (_("Bambook"), "SD928", deviceInfo.firmwareVersion, "MimeType") return (_("Bambook"), "SD928", deviceInfo.firmwareVersion, "MimeType")
def card_prefix(self, end_session=True): def card_prefix(self, end_session=True):
''' '''
Return a 2 element list of the prefix to paths on the cards. Return a 2 element list of the prefix to paths on the cards.
@ -164,7 +164,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
continue continue
b = self.book_class('', book.bookGuid) b = self.book_class('', book.bookGuid)
b.title = book.bookName.decode(text_encoding) b.title = book.bookName.decode(text_encoding)
b.authors = [ book.bookAuthor.decode(text_encoding) ] b.authors = [ book.bookAuthor.decode(text_encoding) ]
b.size = 0 b.size = 0
b.datatime = time.gmtime() b.datatime = time.gmtime()
b.lpath = book.bookGuid b.lpath = book.bookGuid
@ -187,7 +187,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
if self.update_metadata_item(book, booklist[idx]): if self.update_metadata_item(book, booklist[idx]):
changed = True changed = True
else: else:
if booklist.add_book(book, if booklist.add_book(book,
replace_metadata=False): replace_metadata=False):
changed = True changed = True
except: # Probably a filename encoding error except: # Probably a filename encoding error
@ -322,17 +322,17 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
''' '''
if not self.bambook: if not self.bambook:
return return
json_codec = JsonCodec() json_codec = JsonCodec()
# Create stub virtual book for sync info # Create stub virtual book for sync info
with TemporaryDirectory() as tdir: with TemporaryDirectory() as tdir:
snbcdir = os.path.join(tdir, 'snbc') snbcdir = os.path.join(tdir, 'snbc')
snbfdir = os.path.join(tdir, 'snbf') snbfdir = os.path.join(tdir, 'snbf')
os.mkdir(snbcdir) os.mkdir(snbcdir)
os.mkdir(snbfdir) os.mkdir(snbfdir)
f = open(os.path.join(snbfdir, 'book.snbf'), 'wb') f = open(os.path.join(snbfdir, 'book.snbf'), 'wb')
f.write('''<book-snbf version="1.0"> f.write('''<book-snbf version="1.0">
<head> <head>
@ -362,7 +362,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
cache_name = os.path.join(snbcdir, self.METADATA_CACHE) cache_name = os.path.join(snbcdir, self.METADATA_CACHE)
with open(cache_name, 'wb') as f: with open(cache_name, 'wb') as f:
json_codec.encode_to_file(f, booklists[0]) json_codec.encode_to_file(f, booklists[0])
with TemporaryFile('.snb') as f: with TemporaryFile('.snb') as f:
if self.bambook.PackageSNB(f, tdir): if self.bambook.PackageSNB(f, tdir):
if not self.bambook.SendFile(f, self.METADATA_FILE_GUID): if not self.bambook.SendFile(f, self.METADATA_FILE_GUID):
@ -441,8 +441,8 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
with TemporaryDirectory() as tdir: with TemporaryDirectory() as tdir:
if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir): if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir):
cache_name = os.path.join(tdir, self.METADATA_CACHE) cache_name = os.path.join(tdir, self.METADATA_CACHE)
if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID), if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID),
'snbc/' + self.METADATA_CACHE, 'snbc/' + self.METADATA_CACHE,
cache_name): cache_name):
json_codec = JsonCodec() json_codec = JsonCodec()
if os.access(cache_name, os.R_OK): if os.access(cache_name, os.R_OK):
@ -460,7 +460,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
def update_metadata_item(cls, book, blb): def update_metadata_item(cls, book, blb):
# Currently, we do not have enough information # Currently, we do not have enough information
# from Bambook SDK to judge whether a book has # from Bambook SDK to judge whether a book has
# been changed, we assume all books has been # been changed, we assume all books has been
# changed. # changed.
changed = True changed = True
# if book.bookName.decode(text_encoding) != blb.title: # if book.bookName.decode(text_encoding) != blb.title:

View File

@ -5,14 +5,14 @@ __copyright__ = '2010, Li Fanxi <lifanxi at freemindworld.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
''' '''
Sanda library wrapper Sanda library wrapper
''' '''
import ctypes, uuid, hashlib, os, sys import ctypes, uuid, hashlib, os, sys
from threading import Event, Lock from threading import Event, Lock
from calibre.constants import iswindows, islinux, isosx from calibre.constants import iswindows, islinux, isosx
from calibre import load_library from calibre import load_library
try: try:
_lib_name = 'libBambookCore' _lib_name = 'libBambookCore'
cdll = ctypes.cdll cdll = ctypes.cdll
@ -71,7 +71,7 @@ TRANS_STATUS_ERR = 2 #传输出错
BBKeyNum0 = 0 BBKeyNum0 = 0
BBKeyNum1 = 1 BBKeyNum1 = 1
BBKeyNum2 = 2 BBKeyNum2 = 2
BBKeyNum3 = 3 BBKeyNum3 = 3
BBKeyNum4 = 4 BBKeyNum4 = 4
BBKeyNum5 = 5 BBKeyNum5 = 5
BBKeyNum6 = 6 BBKeyNum6 = 6
@ -85,7 +85,7 @@ BBKeyDown = 13
BBKeyLeft = 14 BBKeyLeft = 14
BBKeyRight = 15 BBKeyRight = 15
BBKeyPageUp = 16 BBKeyPageUp = 16
BBKeyPageDown = 17 BBKeyPageDown = 17
BBKeyOK = 18 BBKeyOK = 18
BBKeyESC = 19 BBKeyESC = 19
BBKeyBookshelf = 20 BBKeyBookshelf = 20
@ -158,7 +158,7 @@ def BambookGetErrorString(code):
func = lib_handle.BambookGetErrorString func = lib_handle.BambookGetErrorString
func.restype = ctypes.c_char_p func.restype = ctypes.c_char_p
return func(code) return func(code)
# extern "C" BB_RESULT BambookGetSDKVersion(uint32_t * version); # extern "C" BB_RESULT BambookGetSDKVersion(uint32_t * version);
def BambookGetSDKVersion(): def BambookGetSDKVersion():
@ -275,7 +275,7 @@ def BambookReplacePrivBook(handle, filename, bookID, callback, userData):
return True return True
else: else:
return False return False
# extern "C" BB_RESULT BambookFetchPrivBook(BB_HANDLE hConn, const char * # extern "C" BB_RESULT BambookFetchPrivBook(BB_HANDLE hConn, const char *
# lpszBookID, const char * lpszFilePath, TransCallback pCallbackFunc, intptr_t userData); # lpszBookID, const char * lpszFilePath, TransCallback pCallbackFunc, intptr_t userData);
def BambookFetchPrivBook(handle, bookID, filename, callback, userData): def BambookFetchPrivBook(handle, bookID, filename, callback, userData):
@ -339,7 +339,7 @@ class Bambook:
if self.handle: if self.handle:
return BambookDisconnect(self.handle) return BambookDisconnect(self.handle)
return False return False
def GetState(self): def GetState(self):
if self.handle: if self.handle:
return BambookGetConnectStatus(self.handle) return BambookGetConnectStatus(self.handle)
@ -354,7 +354,7 @@ class Bambook:
if self.handle: if self.handle:
taskID = job.NewJob() taskID = job.NewJob()
if guid: if guid:
if BambookReplacePrivBook(self.handle, fileName, guid, if BambookReplacePrivBook(self.handle, fileName, guid,
bambookTransferCallback, taskID): bambookTransferCallback, taskID):
if(job.WaitJob(taskID)): if(job.WaitJob(taskID)):
job.DeleteJob(taskID) job.DeleteJob(taskID)
@ -391,7 +391,7 @@ class Bambook:
else: else:
job.DeleteJob(taskID) job.DeleteJob(taskID)
return False return False
return False return False
def DeleteFile(self, guid): def DeleteFile(self, guid):
if self.handle: if self.handle:
@ -404,13 +404,13 @@ class Bambook:
books = [] books = []
bookInfo = PrivBookInfo() bookInfo = PrivBookInfo()
bi = ctypes.pointer(bookInfo) bi = ctypes.pointer(bookInfo)
ret = BambookGetFirstPrivBookInfo(self.handle, bi) ret = BambookGetFirstPrivBookInfo(self.handle, bi)
while ret: while ret:
books.append(bi.contents.Clone()) books.append(bi.contents.Clone())
ret = BambookGetNextPrivBookInfo(self.handle, bi) ret = BambookGetNextPrivBookInfo(self.handle, bi)
return books return books
@staticmethod @staticmethod
def GetSDKVersion(): def GetSDKVersion():
return BambookGetSDKVersion() return BambookGetSDKVersion()
@ -431,13 +431,13 @@ class Bambook:
ret = BambookUnpackFileFromSnb(fileName, 'snbf/toc.snbf', path + '/snbf/toc.snbf') ret = BambookUnpackFileFromSnb(fileName, 'snbf/toc.snbf', path + '/snbf/toc.snbf')
if not ret: if not ret:
return False return False
return True return True
@staticmethod @staticmethod
def PackageSNB(fileName, path): def PackageSNB(fileName, path):
return BambookPackSnbFromDir(fileName, path) return BambookPackSnbFromDir(fileName, path)
def passed(): def passed():
print "> Pass" print "> Pass"
@ -460,7 +460,7 @@ if __name__ == "__main__":
passed() passed()
else: else:
failed() failed()
print "Verify good SNB File" print "Verify good SNB File"
if bb.VerifySNB(u'/tmp/f8268e6c1f4e78c.snb'): if bb.VerifySNB(u'/tmp/f8268e6c1f4e78c.snb'):
passed() passed()
@ -472,13 +472,13 @@ if __name__ == "__main__":
passed() passed()
else: else:
failed() failed()
print "Extract SNB File" print "Extract SNB File"
if bb.ExtractSNB('./test.snb', '/tmp/test'): if bb.ExtractSNB('./test.snb', '/tmp/test'):
passed() passed()
else: else:
failed() failed()
print "Packet SNB File" print "Packet SNB File"
if bb.PackageSNB('/tmp/tmp.snb', '/tmp/test') and bb.VerifySNB('/tmp/tmp.snb'): if bb.PackageSNB('/tmp/tmp.snb', '/tmp/test') and bb.VerifySNB('/tmp/tmp.snb'):
passed() passed()
@ -509,7 +509,7 @@ if __name__ == "__main__":
passed() passed()
else: else:
failed() failed()
print "Get book list" print "Get book list"
books = bb.GetBookList() books = bb.GetBookList()
if len(books) > 10: if len(books) > 10:

View File

@ -246,3 +246,32 @@ class POCKETBOOK602(USBMS):
VENDOR_NAME = '' VENDOR_NAME = ''
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB902'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB902']
class POCKETBOOK701(USBMS):
name = 'PocketBook 701 Device Interface'
description = _('Communicate with the PocketBook 701')
author = _('Kovid Goyal')
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm',
'doc', 'tcr', 'txt']
EBOOK_DIR_MAIN = 'books'
SUPPORTS_SUB_DIRS = True
VENDOR_ID = [0x18d1]
PRODUCT_ID = [0xa004]
BCD = [0x0224]
VENDOR_NAME = 'ANDROID'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
def windows_sort_drives(self, drives):
if len(drives) < 2: return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda:
drives['main'] = carda
drives['carda'] = main
return drives

View File

@ -96,7 +96,7 @@ class KOBO(USBMS):
for idx,b in enumerate(bl): for idx,b in enumerate(bl):
bl_cache[b.lpath] = idx bl_cache[b.lpath] = idx
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus): def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType):
changed = False changed = False
# if path_to_ext(path) in self.FORMATS: # if path_to_ext(path) in self.FORMATS:
try: try:
@ -124,7 +124,7 @@ class KOBO(USBMS):
#print "Image name Normalized: " + imagename #print "Image name Normalized: " + imagename
if imagename is not None: if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename) bl[idx].thumbnail = ImageWrapper(imagename)
if (ContentType != '6'and self.has_kepubs == False) or (self.has_kepubs == True): if (ContentType != '6' and MimeType != 'Shortcover'):
if self.update_metadata_item(bl[idx]): if self.update_metadata_item(bl[idx]):
# print 'update_metadata_item returned true' # print 'update_metadata_item returned true'
changed = True changed = True
@ -132,7 +132,7 @@ class KOBO(USBMS):
playlist_map[lpath] not in bl[idx].device_collections: playlist_map[lpath] not in bl[idx].device_collections:
bl[idx].device_collections.append(playlist_map[lpath]) bl[idx].device_collections.append(playlist_map[lpath])
else: else:
if ContentType == '6' and self.has_kepubs == False: if ContentType == '6' and MimeType == 'Shortcover':
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576) book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
else: else:
try: try:
@ -177,15 +177,15 @@ class KOBO(USBMS):
for i, row in enumerate(cursor): for i, row in enumerate(cursor):
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...')) # self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
path = self.path_from_contentid(row[3], row[5], oncard) path = self.path_from_contentid(row[3], row[5], row[4], oncard)
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip' mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
# debug_print("mime:", mime) # debug_print("mime:", mime)
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"): if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7]) changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4])
# print "shortbook: " + path # print "shortbook: " + path
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"): elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7]) changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4])
if changed: if changed:
need_sync = True need_sync = True
@ -363,7 +363,8 @@ class KOBO(USBMS):
def contentid_from_path(self, path, ContentType): def contentid_from_path(self, path, ContentType):
if ContentType == 6: if ContentType == 6:
if self.has_kepubs == False: extension = os.path.splitext(path)[1]
if extension == '.kobo':
ContentID = os.path.splitext(path)[0] ContentID = os.path.splitext(path)[0]
# Remove the prefix on the file. it could be either # Remove the prefix on the file. it could be either
ContentID = ContentID.replace(self._main_prefix, '') ContentID = ContentID.replace(self._main_prefix, '')
@ -411,7 +412,7 @@ class KOBO(USBMS):
ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored
return ContentType return ContentType
def path_from_contentid(self, ContentID, ContentType, oncard): def path_from_contentid(self, ContentID, ContentType, MimeType, oncard):
path = ContentID path = ContentID
if oncard == 'cardb': if oncard == 'cardb':
@ -420,13 +421,13 @@ class KOBO(USBMS):
path = path.replace("file:///mnt/sd/", self._card_a_prefix) path = path.replace("file:///mnt/sd/", self._card_a_prefix)
# print "SD Card: " + path # print "SD Card: " + path
else: else:
if ContentType == "6" and self.has_kepubs == False: if ContentType == "6" and MimeType == 'Shortcover':
# This is a hack as the kobo files do not exist # This is a hack as the kobo files do not exist
# but the path is required to make a unique id # but the path is required to make a unique id
# for calibre's reference # for calibre's reference
path = self._main_prefix + path + '.kobo' path = self._main_prefix + path + '.kobo'
# print "Path: " + path # print "Path: " + path
elif (ContentType == "6" or ContentType == "10") and self.has_kepubs == True: elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
path = self._main_prefix + '.kobo/kepub/' + path path = self._main_prefix + '.kobo/kepub/' + path
# print "Internal: " + path # print "Internal: " + path
else: else:

View File

@ -62,9 +62,9 @@ class SWEEX(USBMS):
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ['epub', 'prc', 'fb2', 'html', 'rtf', 'chm', 'pdf', 'txt'] FORMATS = ['epub', 'prc', 'fb2', 'html', 'rtf', 'chm', 'pdf', 'txt']
VENDOR_ID = [0x0525] VENDOR_ID = [0x0525, 0x177f]
PRODUCT_ID = [0xa4a5] PRODUCT_ID = [0xa4a5, 0x300]
BCD = [0x0319] BCD = [0x0319, 0x110]
VENDOR_NAME = 'SWEEX' VENDOR_NAME = 'SWEEX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOKREADER' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOKREADER'
@ -104,7 +104,7 @@ class PDNOVEL(USBMS):
VENDOR_NAME = 'ANDROID' VENDOR_NAME = 'ANDROID'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
THUMBNAIL_HEIGHT = 144 THUMBNAIL_HEIGHT = 130
EBOOK_DIR_MAIN = 'eBooks' EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False
@ -204,3 +204,23 @@ class LUMIREAD(USBMS):
with open(cfilepath+'.jpg', 'wb') as f: with open(cfilepath+'.jpg', 'wb') as f:
f.write(metadata.thumbnail[-1]) f.write(metadata.thumbnail[-1])
class ALURATEK_COLOR(USBMS):
name = 'Aluratek Color Device Interface'
gui_name = 'Aluratek Color'
description = _('Communicate with the Aluratek Color')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'fb2', 'txt', 'pdf']
VENDOR_ID = [0x1f3a]
PRODUCT_ID = [0x1000]
BCD = [0x0002]
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'books'
VENDOR_NAME = 'USB_2.0'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB_FLASH_DRIVER'

View File

@ -58,9 +58,16 @@ class PRS505(USBMS):
SUPPORTS_USE_AUTHOR_SORT = True SUPPORTS_USE_AUTHOR_SORT = True
EBOOK_DIR_MAIN = 'database/media/books' EBOOK_DIR_MAIN = 'database/media/books'
ALL_BY_TITLE = _('All by title')
ALL_BY_AUTHOR = _('All by author')
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields ' EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
'to turn into collections on the device. Possibilities include: ')+\ 'to turn into collections on the device. Possibilities include: ')+\
'series, tags, authors' 'series, tags, authors' +\
_('. Two special collections are available: %s:%s and %s:%s. Add '
'these values to the list to enable them. The collections will be '
'given the name provided after the ":" character.')%(
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR)
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags']) EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
plugboard = None plugboard = None
@ -151,7 +158,7 @@ class PRS505(USBMS):
blists[i] = booklists[i] blists[i] = booklists[i]
opts = self.settings() opts = self.settings()
if opts.extra_customization: if opts.extra_customization:
collections = [x.lower().strip() for x in collections = [x.strip() for x in
opts.extra_customization.split(',')] opts.extra_customization.split(',')]
else: else:
collections = [] collections = []
@ -179,6 +186,8 @@ class PRS505(USBMS):
self.plugboard_func = pb_func self.plugboard_func = pb_func
def upload_cover(self, path, filename, metadata, filepath): def upload_cover(self, path, filename, metadata, filepath):
return # Disabled as the SONY's don't need this thumbnail anyway and
# older models don't auto delete it
if metadata.thumbnail and metadata.thumbnail[-1]: if metadata.thumbnail and metadata.thumbnail[-1]:
path = path.replace('/', os.sep) path = path.replace('/', os.sep)
is_main = path.startswith(self._main_prefix) is_main = path.startswith(self._main_prefix)

View File

@ -410,6 +410,9 @@ class XMLCache(object):
newmi = book.deepcopy_metadata() newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, plugboard) newmi.template_to_attribute(book, plugboard)
newmi.set('_new_book', getattr(book, '_new_book', False)) newmi.set('_new_book', getattr(book, '_new_book', False))
book.set('_pb_title_sort',
newmi.get('title_sort', newmi.get('title', None)))
book.set('_pb_author_sort', newmi.get('author_sort', ''))
else: else:
newmi = book newmi = book
(gtz_count, ltz_count, use_tz_var) = \ (gtz_count, ltz_count, use_tz_var) = \

View File

@ -23,16 +23,16 @@ class SNE(USBMS):
FORMATS = ['epub', 'pdf', 'txt'] FORMATS = ['epub', 'pdf', 'txt']
VENDOR_ID = [0x04e8] VENDOR_ID = [0x04e8]
PRODUCT_ID = [0x2051, 0x2053] PRODUCT_ID = [0x2051, 0x2053, 0x2054]
BCD = [0x0323] BCD = [0x0323]
VENDOR_NAME = 'SAMSUNG' VENDOR_NAME = 'SAMSUNG'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'SNE-60' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['SNE-60', 'E65']
MAIN_MEMORY_VOLUME_LABEL = 'SNE Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'SNE Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card' STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card'
EBOOK_DIR_MAIN = 'Books' EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Books'
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True

View File

@ -72,3 +72,13 @@ class SOVOS(TECLAST_K3):
VENDOR_NAME = 'RK28XX' VENDOR_NAME = 'RK28XX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC'
class SUNSTECH_EB700(TECLAST_K3):
name = 'Sunstech EB700 device interface'
gui_name = 'EB700'
description = _('Communicate with the Sunstech EB700 reader.')
FORMATS = ['epub', 'fb2', 'pdf', 'pdb', 'txt']
VENDOR_NAME = 'SUNEB700'
WINDOWS_MAIN_MEM = 'USB-MSC'

View File

@ -132,9 +132,24 @@ class CollectionsBookList(BookList):
use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect' use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect'
collections = {} collections = {}
# This map of sets is used to avoid linear searches when testing for
# book equality # get the special collection names
all_by_author = ''
all_by_title = ''
ca = []
for c in collection_attributes:
if c.startswith('aba:') and c[4:]:
all_by_author = c[4:].strip()
elif c.startswith('abt:') and c[4:]:
all_by_title = c[4:].strip()
else:
ca.append(c.lower())
collection_attributes = ca
for book in self: for book in self:
tsval = book.get('_pb_title_sort',
book.get('title_sort', book.get('title', 'zzzz')))
asval = book.get('_pb_author_sort', book.get('author_sort', ''))
# Make sure we can identify this book via the lpath # Make sure we can identify this book via the lpath
lpath = getattr(book, 'lpath', None) lpath = getattr(book, 'lpath', None)
if lpath is None: if lpath is None:
@ -211,22 +226,29 @@ class CollectionsBookList(BookList):
collections[cat_name] = {} collections[cat_name] = {}
if use_renaming_rules and sort_attr: if use_renaming_rules and sort_attr:
sort_val = book.get(sort_attr, None) sort_val = book.get(sort_attr, None)
collections[cat_name][lpath] = \ collections[cat_name][lpath] = (book, sort_val, tsval)
(book, sort_val, book.get('title_sort', 'zzzz'))
elif is_series: elif is_series:
if doing_dc: if doing_dc:
collections[cat_name][lpath] = \ collections[cat_name][lpath] = \
(book, book.get('series_index', sys.maxint), (book, book.get('series_index', sys.maxint), tsval)
book.get('title_sort', 'zzzz'))
else: else:
collections[cat_name][lpath] = \ collections[cat_name][lpath] = \
(book, book.get(attr+'_index', sys.maxint), (book, book.get(attr+'_index', sys.maxint), tsval)
book.get('title_sort', 'zzzz'))
else: else:
if lpath not in collections[cat_name]: if lpath not in collections[cat_name]:
collections[cat_name][lpath] = \ collections[cat_name][lpath] = (book, tsval, tsval)
(book, book.get('title_sort', 'zzzz'),
book.get('title_sort', 'zzzz')) # All books by author
if all_by_author:
if all_by_author not in collections:
collections[all_by_author] = {}
collections[all_by_author][lpath] = (book, asval, tsval)
# All books by title
if all_by_title:
if all_by_title not in collections:
collections[all_by_title] = {}
collections[all_by_title][lpath] = (book, tsval, asval)
# Sort collections # Sort collections
result = {} result = {}

View File

@ -605,8 +605,9 @@ class Device(DeviceConfig, DevicePlugin):
main, carda, cardb = self.find_device_nodes() main, carda, cardb = self.find_device_nodes()
if main is None: if main is None:
raise DeviceError(_('Unable to detect the %s disk drive. Your ' raise DeviceError(_('Unable to detect the %s disk drive. Either '
' kernel is probably exporting a deprecated version of SYSFS.') 'the device has already been ejected, or your '
'kernel is exporting a deprecated version of SYSFS.')
%self.__class__.__name__) %self.__class__.__name__)
self._linux_mount_map = {} self._linux_mount_map = {}

View File

@ -22,6 +22,9 @@ class UnknownFormatError(Exception):
class DRMError(ValueError): class DRMError(ValueError):
pass pass
class ParserError(ValueError):
pass
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm', BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc', 'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
@ -39,6 +42,10 @@ class HTMLRenderer(object):
try: try:
if not ok: if not ok:
raise RuntimeError('Rendering of HTML failed.') raise RuntimeError('Rendering of HTML failed.')
de = self.page.mainFrame().documentElement()
pe = de.findFirst('parsererror')
if not pe.isNull():
raise ParserError(pe.toPlainText())
image = QImage(self.page.viewportSize(), QImage.Format_ARGB32) image = QImage(self.page.viewportSize(), QImage.Format_ARGB32)
image.setDotsPerMeterX(96*(100/2.54)) image.setDotsPerMeterX(96*(100/2.54))
image.setDotsPerMeterY(96*(100/2.54)) image.setDotsPerMeterY(96*(100/2.54))
@ -104,7 +111,7 @@ def render_html_svg_workaround(path_to_html, log, width=590, height=750):
return data return data
def render_html(path_to_html, width=590, height=750): def render_html(path_to_html, width=590, height=750, as_xhtml=True):
from PyQt4.QtWebKit import QWebPage from PyQt4.QtWebKit import QWebPage
from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize
from calibre.gui2 import is_ok_to_use_qt from calibre.gui2 import is_ok_to_use_qt
@ -122,11 +129,18 @@ def render_html(path_to_html, width=590, height=750):
renderer = HTMLRenderer(page, loop) renderer = HTMLRenderer(page, loop)
page.connect(page, SIGNAL('loadFinished(bool)'), renderer, page.connect(page, SIGNAL('loadFinished(bool)'), renderer,
Qt.QueuedConnection) Qt.QueuedConnection)
page.mainFrame().load(QUrl.fromLocalFile(path_to_html)) if as_xhtml:
page.mainFrame().setContent(open(path_to_html, 'rb').read(),
'application/xhtml+xml', QUrl.fromLocalFile(path_to_html))
else:
page.mainFrame().load(QUrl.fromLocalFile(path_to_html))
loop.exec_() loop.exec_()
renderer.loop = renderer.page = None renderer.loop = renderer.page = None
del page del page
del loop del loop
if isinstance(renderer.exception, ParserError) and as_xhtml:
return render_html(path_to_html, width=width, height=height,
as_xhtml=False)
return renderer return renderer
def check_ebook_format(stream, current_guess): def check_ebook_format(stream, current_guess):

View File

@ -27,13 +27,10 @@ class FB2MLizer(object):
''' '''
Todo: * Include more FB2 specific tags in the conversion. Todo: * Include more FB2 specific tags in the conversion.
* Handle a tags. * Handle a tags.
* Figure out some way to turn oeb_book.toc items into <section><title>
<p> to allow for readers to generate toc from the document.
''' '''
def __init__(self, log): def __init__(self, log):
self.log = log self.log = log
self.image_hrefs = {}
self.reset_state() self.reset_state()
def reset_state(self): def reset_state(self):
@ -43,17 +40,25 @@ class FB2MLizer(object):
# in different directories. FB2 images are all in a flat layout so we rename all images # in different directories. FB2 images are all in a flat layout so we rename all images
# into a sequential numbering system to ensure there are no collisions between image names. # into a sequential numbering system to ensure there are no collisions between image names.
self.image_hrefs = {} self.image_hrefs = {}
# Mapping of toc items and their
self.toc = {}
# Used to see whether a new <section> needs to be opened
self.section_level = 0
def extract_content(self, oeb_book, opts): def extract_content(self, oeb_book, opts):
self.log.info('Converting XHTML to FB2 markup...') self.log.info('Converting XHTML to FB2 markup...')
self.oeb_book = oeb_book self.oeb_book = oeb_book
self.opts = opts self.opts = opts
self.reset_state()
# Used for adding <section>s and <title>s to allow readers
# to generate toc from the document.
if self.opts.sectionize == 'toc':
self.create_flat_toc(self.oeb_book.toc, 1)
return self.fb2mlize_spine() return self.fb2mlize_spine()
def fb2mlize_spine(self): def fb2mlize_spine(self):
self.reset_state()
output = [self.fb2_header()] output = [self.fb2_header()]
output.append(self.get_text()) output.append(self.get_text())
output.append(self.fb2mlize_images()) output.append(self.fb2mlize_images())
@ -66,13 +71,19 @@ class FB2MLizer(object):
return u'<?xml version="1.0" encoding="UTF-8"?>' + output return u'<?xml version="1.0" encoding="UTF-8"?>' + output
def clean_text(self, text): def clean_text(self, text):
text = re.sub(r'(?miu)<section>\s*</section>', '', text)
text = re.sub(r'(?miu)\s+</section>', '</section>', text)
text = re.sub(r'(?miu)</section><section>', '</section>\n\n<section>', text)
text = re.sub(r'(?miu)<p>\s*</p>', '', text) text = re.sub(r'(?miu)<p>\s*</p>', '', text)
text = re.sub(r'(?miu)\s+</p>', '</p>', text) text = re.sub(r'(?miu)\s*</p>', '</p>', text)
text = re.sub(r'(?miu)</p><p>', '</p>\n\n<p>', text) text = re.sub(r'(?miu)</p>\s*<p>', '</p>\n\n<p>', text)
text = re.sub(r'(?miu)<title>\s*</title>', '', text)
text = re.sub(r'(?miu)\s+</title>', '</title>', text)
text = re.sub(r'(?miu)<section>\s*</section>', '', text)
text = re.sub(r'(?miu)\s*</section>', '\n</section>', text)
text = re.sub(r'(?miu)</section>\s*', '</section>\n\n', text)
text = re.sub(r'(?miu)\s*<section>', '\n<section>', text)
text = re.sub(r'(?miu)<section>\s*', '<section>\n', text)
text = re.sub(r'(?miu)</section><section>', '</section>\n\n<section>', text)
if self.opts.insert_blank_line: if self.opts.insert_blank_line:
text = re.sub(r'(?miu)</p>', '</p><empty-line />', text) text = re.sub(r'(?miu)</p>', '</p><empty-line />', text)
@ -144,12 +155,34 @@ class FB2MLizer(object):
def get_text(self): def get_text(self):
text = ['<body>'] text = ['<body>']
# Create main section if there are no others to create
if self.opts.sectionize == 'nothing':
text.append('<section>')
self.section_level += 1
for item in self.oeb_book.spine: for item in self.oeb_book.spine:
self.log.debug('Converting %s to FictionBook2 XML' % item.href) self.log.debug('Converting %s to FictionBook2 XML' % item.href)
stylizer = Stylizer(item.data, item.href, self.oeb_book, self.opts, self.opts.output_profile) stylizer = Stylizer(item.data, item.href, self.oeb_book, self.opts, self.opts.output_profile)
text.append('<section>')
# Start a <section> if we must sectionize each file or if the TOC references this page
page_section_open = False
if self.opts.sectionize == 'files' or self.toc.get(item.href) == 'page':
text.append('<section>')
page_section_open = True
self.section_level += 1
text += self.dump_text(item.data.find(XHTML('body')), stylizer, item) text += self.dump_text(item.data.find(XHTML('body')), stylizer, item)
if page_section_open:
text.append('</section>')
self.section_level -= 1
# Close any open sections
while self.section_level > 0:
text.append('</section>') text.append('</section>')
self.section_level -= 1
return ''.join(text) + '</body>' return ''.join(text) + '</body>'
def fb2mlize_images(self): def fb2mlize_images(self):
@ -184,6 +217,17 @@ class FB2MLizer(object):
'%s.' % (item.href, e)) '%s.' % (item.href, e))
return ''.join(images) return ''.join(images)
def create_flat_toc(self, nodes, level):
for item in nodes:
href, mid, id = item.href.partition('#')
if not id:
self.toc[href] = 'page'
else:
if not self.toc.get(href, None):
self.toc[href] = {}
self.toc[href][id] = level
self.create_flat_toc(item.nodes, level + 1)
def ensure_p(self): def ensure_p(self):
if self.in_p: if self.in_p:
return [], [] return [], []
@ -254,10 +298,38 @@ class FB2MLizer(object):
# First tag in tree # First tag in tree
tag = barename(elem_tree.tag) tag = barename(elem_tree.tag)
# Convert TOC entries to <title>s and add <section>s
if self.opts.sectionize == 'toc':
# A section cannot be a child of any other element than another section,
# so leave the tag alone if there are parents
if not tag_stack:
# There are two reasons to start a new section here: the TOC pointed to
# this page (then we use the first non-<body> on the page as a <title>), or
# the TOC pointed to a specific element
newlevel = 0
toc_entry = self.toc.get(page.href, None)
if toc_entry == 'page':
if tag != 'body' and hasattr(elem_tree, 'text') and elem_tree.text:
newlevel = 1
self.toc[page.href] = None
elif toc_entry and elem_tree.attrib.get('id', None):
newlevel = toc_entry.get(elem_tree.attrib.get('id', None), None)
# Start a new section if necessary
if newlevel:
if not (newlevel > self.section_level):
fb2_out.append('</section>')
self.section_level -= 1
fb2_out.append('<section>')
self.section_level += 1
fb2_out.append('<title>')
tags.append('title')
if self.section_level == 0:
# If none of the prior processing made a section, make one now to be FB2 spec compliant
fb2_out.append('<section>')
self.section_level += 1
# Process the XHTML tag if it needs to be converted to an FB2 tag. # Process the XHTML tag if it needs to be converted to an FB2 tag.
if tag == 'h1' and self.opts.h1_to_title or tag == 'h2' and self.opts.h2_to_title or tag == 'h3' and self.opts.h3_to_title:
fb2_out.append('<title>')
tags.append('title')
if tag == 'img': if tag == 'img':
if elem_tree.attrib.get('src', None): if elem_tree.attrib.get('src', None):
# Only write the image tag if it is in the manifest. # Only write the image tag if it is in the manifest.

View File

@ -16,15 +16,15 @@ class FB2Output(OutputFormatPlugin):
file_type = 'fb2' file_type = 'fb2'
options = set([ options = set([
OptionRecommendation(name='h1_to_title', OptionRecommendation(name='sectionize',
recommended_value=False, level=OptionRecommendation.LOW, recommended_value='files', level=OptionRecommendation.LOW,
help=_('Wrap all h1 tags with fb2 title elements.')), choices=['toc', 'files', 'nothing'],
OptionRecommendation(name='h2_to_title', help=_('Specify the sectionization of elements. '
recommended_value=False, level=OptionRecommendation.LOW, 'A value of "nothing" turns the book into a single section. '
help=_('Wrap all h2 tags with fb2 title elements.')), 'A value of "files" turns each file into a separate section; use this if your device is having trouble. '
OptionRecommendation(name='h3_to_title', 'A value of "Table of Contents" turns the entries in the Table of Contents into titles and creates sections; '
recommended_value=False, level=OptionRecommendation.LOW, 'if it fails, adjust the "Structure Detection" and/or "Table of Contents" settings '
help=_('Wrap all h3 tags with fb2 title elements.')), '(turn on "Force use of auto-generated Table of Contents).')),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):

View File

@ -55,8 +55,12 @@ except:
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033]) _ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
def title_sort(title): def title_sort(title, order=None):
if order is None:
order = tweaks['title_series_sorting']
title = title.strip() title = title.strip()
if order == 'strictly_alphabetic':
return title
if title and title[0] in _ignore_starts: if title and title[0] in _ignore_starts:
title = title[1:] title = title[1:]
match = _title_pat.search(title) match = _title_pat.search(title)

View File

@ -463,6 +463,8 @@ class Metadata(object):
other_lang = getattr(other, 'language', None) other_lang = getattr(other, 'language', None)
if other_lang and other_lang.lower() != 'und': if other_lang and other_lang.lower() != 'und':
self.language = other_lang self.language = other_lang
if not getattr(self, 'series', None):
self.series_index = None
def format_series_index(self, val=None): def format_series_index(self, val=None):
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx

View File

@ -17,6 +17,7 @@ pdfreflow, pdfreflow_error = plugins['pdfreflow']
def get_metadata(stream, cover=True): def get_metadata(stream, cover=True):
if pdfreflow is None: if pdfreflow is None:
raise RuntimeError(pdfreflow_error) raise RuntimeError(pdfreflow_error)
stream.seek(0)
raw = stream.read() raw = stream.read()
#isbn = _isbn_pat.search(raw) #isbn = _isbn_pat.search(raw)
#if isbn is not None: #if isbn is not None:

View File

@ -11,12 +11,11 @@ import os, re, uuid, logging
from mimetypes import types_map from mimetypes import types_map
from collections import defaultdict from collections import defaultdict
from itertools import count from itertools import count
from urlparse import urldefrag, urlparse, urlunparse from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote from urllib import unquote as urlunquote
from urlparse import urljoin
from lxml import etree, html from lxml import etree, html
from cssutils import CSSParser from cssutils import CSSParser, parseString, parseStyle, replaceUrls
from cssutils.css import CSSRule from cssutils.css import CSSRule
import calibre import calibre
@ -88,11 +87,11 @@ def XLINK(name):
def CALIBRE(name): def CALIBRE(name):
return '{%s}%s' % (CALIBRE_NS, name) return '{%s}%s' % (CALIBRE_NS, name)
_css_url_re = re.compile(r'url\((.*?)\)', re.I) _css_url_re = re.compile(r'url\s*\((.*?)\)', re.I)
_css_import_re = re.compile(r'@import "(.*?)"') _css_import_re = re.compile(r'@import "(.*?)"')
_archive_re = re.compile(r'[^ ]+') _archive_re = re.compile(r'[^ ]+')
def iterlinks(root): def iterlinks(root, find_links_in_css=True):
''' '''
Iterate over all links in a OEB Document. Iterate over all links in a OEB Document.
@ -134,6 +133,8 @@ def iterlinks(root):
yield (el, attr, attribs[attr], 0) yield (el, attr, attribs[attr], 0)
if not find_links_in_css:
continue
if tag == XHTML('style') and el.text: if tag == XHTML('style') and el.text:
for match in _css_url_re.finditer(el.text): for match in _css_url_re.finditer(el.text):
yield (el, None, match.group(1), match.start(1)) yield (el, None, match.group(1), match.start(1))
@ -180,7 +181,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
''' '''
if resolve_base_href: if resolve_base_href:
resolve_base_href(root) resolve_base_href(root)
for el, attrib, link, pos in iterlinks(root): for el, attrib, link, pos in iterlinks(root, find_links_in_css=False):
new_link = link_repl_func(link.strip()) new_link = link_repl_func(link.strip())
if new_link == link: if new_link == link:
continue continue
@ -203,6 +204,40 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
new = cur[:pos] + new_link + cur[pos+len(link):] new = cur[:pos] + new_link + cur[pos+len(link):]
el.attrib[attrib] = new el.attrib[attrib] = new
def set_property(v):
if v.CSS_PRIMITIVE_VALUE == v.cssValueType and \
v.CSS_URI == v.primitiveType:
v.setStringValue(v.CSS_URI,
link_repl_func(v.getStringValue()))
for el in root.iter():
try:
tag = el.tag
except UnicodeDecodeError:
continue
if tag == XHTML('style') and el.text and \
(_css_url_re.search(el.text) is not None or '@import' in
el.text):
stylesheet = parseString(el.text)
replaceUrls(stylesheet, link_repl_func)
el.text = '\n'+stylesheet.cssText + '\n'
if 'style' in el.attrib:
text = el.attrib['style']
if _css_url_re.search(text) is not None:
stext = parseStyle(text)
for p in stext.getProperties(all=True):
v = p.cssValue
if v.CSS_VALUE_LIST == v.cssValueType:
for item in v:
set_property(item)
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
set_property(v)
el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
' ')
EPUB_MIME = types_map['.epub'] EPUB_MIME = types_map['.epub']
XHTML_MIME = types_map['.xhtml'] XHTML_MIME = types_map['.xhtml']
@ -622,7 +657,10 @@ class Metadata(object):
attrib[key] = prefixname(value, nsrmap) attrib[key] = prefixname(value, nsrmap)
if namespace(self.term) == DC11_NS: if namespace(self.term) == DC11_NS:
elem = element(parent, self.term, attrib=attrib) elem = element(parent, self.term, attrib=attrib)
elem.text = self.value try:
elem.text = self.value
except:
elem.text = repr(self.value)
else: else:
elem = element(parent, OPF('meta'), attrib=attrib) elem = element(parent, OPF('meta'), attrib=attrib)
elem.attrib['name'] = prefixname(self.term, nsrmap) elem.attrib['name'] = prefixname(self.term, nsrmap)

View File

@ -257,7 +257,6 @@ class EbookIterator(object):
s.max_page = s.start_page + s.pages - 1 s.max_page = s.start_page + s.pages - 1
self.toc = self.opf.toc self.toc = self.opf.toc
self.find_embedded_fonts()
self.read_bookmarks() self.read_bookmarks()
return self return self

View File

@ -205,7 +205,10 @@ class Stylizer(object):
NameError, # thrown on OS X instead of SelectorSyntaxError NameError, # thrown on OS X instead of SelectorSyntaxError
SelectorSyntaxError): SelectorSyntaxError):
continue continue
matches = selector(tree) try:
matches = selector(tree)
except etree.XPathEvalError:
continue
if not matches: if not matches:
ntext = capital_sel_pat.sub(lambda m: m.group().lower(), text) ntext = capital_sel_pat.sub(lambda m: m.group().lower(), text)

View File

@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import posixpath import posixpath
from urlparse import urldefrag from urlparse import urldefrag, urlparse
from lxml import etree from lxml import etree
import cssutils import cssutils
@ -67,6 +67,10 @@ class RenameFiles(object): # {{{
def url_replacer(self, orig_url): def url_replacer(self, orig_url):
url = urlnormalize(orig_url) url = urlnormalize(orig_url)
parts = urlparse(url)
if parts.scheme:
# Only rewrite local URLs
return orig_url
path, frag = urldefrag(url) path, frag = urldefrag(url)
if self.renamed_items_map: if self.renamed_items_map:
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item) orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)

View File

@ -72,8 +72,8 @@ class PML_HTMLizer(object):
'ra': ('<span id="r%s"></span><a href="#%s">', '</a>'), 'ra': ('<span id="r%s"></span><a href="#%s">', '</a>'),
'c': ('<div style="text-align: center; margin: auto;">', '</div>'), 'c': ('<div style="text-align: center; margin: auto;">', '</div>'),
'r': ('<div style="text-align: right;">', '</div>'), 'r': ('<div style="text-align: right;">', '</div>'),
't': ('<div style="text-indent: 5%;">', '</div>'), 't': ('<div style="margin-left: 5%;">', '</div>'),
'T': ('<div style="text-indent: %s;">', '</div>'), 'T': ('<div style="margin-left: %s;">', '</div>'),
'i': ('<span style="font-style: italic;">', '</span>'), 'i': ('<span style="font-style: italic;">', '</span>'),
'u': ('<span style="text-decoration: underline;">', '</span>'), 'u': ('<span style="text-decoration: underline;">', '</span>'),
'd': ('<span style="text-decoration: line-through;">', '</span>'), 'd': ('<span style="text-decoration: line-through;">', '</span>'),

View File

@ -245,7 +245,7 @@ class RTFInput(InputFormatPlugin):
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException
self.options = options self.opts = options
self.log = log self.log = log
self.log('Converting RTF to XML...') self.log('Converting RTF to XML...')
#Name of the preprocesssed RTF file #Name of the preprocesssed RTF file
@ -290,12 +290,12 @@ class RTFInput(InputFormatPlugin):
res = transform.tostring(result) res = transform.tostring(result)
res = res[:100].replace('xmlns:html', 'xmlns') + res[100:] res = res[:100].replace('xmlns:html', 'xmlns') + res[100:]
# Replace newlines inserted by the 'empty_paragraphs' option in rtf2xml with html blank lines # Replace newlines inserted by the 'empty_paragraphs' option in rtf2xml with html blank lines
if not getattr(self.options, 'remove_paragraph_spacing', False): if not getattr(self.opts, 'remove_paragraph_spacing', False):
res = re.sub('\s*<body>', '<body>', res) res = re.sub('\s*<body>', '<body>', res)
res = re.sub('(?<=\n)\n{2}', res = re.sub('(?<=\n)\n{2}',
u'<p>\u00a0</p>\n'.encode('utf-8'), res) u'<p>\u00a0</p>\n'.encode('utf-8'), res)
if self.options.preprocess_html: if self.opts.preprocess_html:
preprocessor = PreProcessor(self.options, log=getattr(self, 'log', None)) preprocessor = PreProcessor(self.opts, log=getattr(self, 'log', None))
res = preprocessor(res) res = preprocessor(res)
f.write(res) f.write(res)
self.write_inline_css(inline_class, border_styles) self.write_inline_css(inline_class, border_styles)

View File

@ -540,6 +540,7 @@ def choose_dir(window, name, title, default_dir='~'):
parent=window, name=name, mode=QFileDialog.Directory, parent=window, name=name, mode=QFileDialog.Directory,
default_dir=default_dir) default_dir=default_dir)
dir = fd.get_files() dir = fd.get_files()
fd.setParent(None)
if dir: if dir:
return dir[0] return dir[0]
@ -560,6 +561,7 @@ def choose_files(window, name, title,
fd = FileDialog(title=title, name=name, filters=filters, fd = FileDialog(title=title, name=name, filters=filters,
parent=window, add_all_files_filter=all_files, mode=mode, parent=window, add_all_files_filter=all_files, mode=mode,
) )
fd.setParent(None)
if fd.accepted: if fd.accepted:
return fd.get_files() return fd.get_files()
return None return None
@ -570,6 +572,7 @@ def choose_images(window, name, title, select_only_single_file=True):
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])], filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
parent=window, add_all_files_filter=False, mode=mode, parent=window, add_all_files_filter=False, mode=mode,
) )
fd.setParent(None)
if fd.accepted: if fd.accepted:
return fd.get_files() return fd.get_files()
return None return None

View File

@ -243,7 +243,9 @@ class AddAction(InterfaceAction):
if hasattr(self._adder, 'cleanup'): if hasattr(self._adder, 'cleanup'):
self._adder.cleanup() self._adder.cleanup()
self._adder = None self._adder.setParent(None)
del self._adder
self._adder = None
def _add_from_device_adder(self, paths=[], names=[], infos=[], def _add_from_device_adder(self, paths=[], names=[], infos=[],
on_card=None, model=None): on_card=None, model=None):

View File

@ -138,6 +138,10 @@ class CheckIntegrity(QProgressDialog):
'You should check them manually. This can ' 'You should check them manually. This can '
'happen if you manipulate the files in the ' 'happen if you manipulate the files in the '
'library folder directly.'), det_msg=det_msg, show=True) 'library folder directly.'), det_msg=det_msg, show=True)
else:
info_dialog(self, _('No errors found'),
_('The integrity check completed with no uncorrectable errors found.'),
show=True)
self.reset() self.reset()
# }}} # }}}
@ -162,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction):
self.choose_menu = QMenu(self.gui) self.choose_menu = QMenu(self.gui)
self.qaction.setMenu(self.choose_menu) self.qaction.setMenu(self.choose_menu)
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
self.choose_menu.addAction(self.action_choose) self.choose_menu.addAction(self.action_choose)
@ -172,6 +177,11 @@ class ChooseLibraryAction(InterfaceAction):
self.delete_menu = QMenu(_('Delete library')) self.delete_menu = QMenu(_('Delete library'))
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu) self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
None, None), attr='action_pick_random')
ac.triggered.connect(self.pick_random)
self.choose_menu.addAction(ac)
self.rename_separator = self.choose_menu.addSeparator() self.rename_separator = self.choose_menu.addSeparator()
self.switch_actions = [] self.switch_actions = []
@ -209,6 +219,12 @@ class ChooseLibraryAction(InterfaceAction):
self.maintenance_menu.addAction(ac) self.maintenance_menu.addAction(ac)
self.choose_menu.addMenu(self.maintenance_menu) self.choose_menu.addMenu(self.maintenance_menu)
def pick_random(self, *args):
import random
pick = random.randint(0, self.gui.library_view.model().rowCount(None))
self.gui.library_view.set_current_row(pick)
self.gui.library_view.scroll_to_row(pick)
def library_name(self): def library_name(self):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
path = db.library_path path = db.library_path

View File

@ -12,6 +12,7 @@ from PyQt4.Qt import QMenu, QObject, QTimer
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.confirm_delete_location import confirm_location
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
single_shot = partial(QTimer.singleShot, 10) single_shot = partial(QTimer.singleShot, 10)
@ -96,10 +97,15 @@ class DeleteAction(InterfaceAction):
for action in list(self.delete_menu.actions())[1:]: for action in list(self.delete_menu.actions())[1:]:
action.setEnabled(enabled) action.setEnabled(enabled)
def _get_selected_formats(self, msg): def _get_selected_formats(self, msg, ids):
from calibre.gui2.dialogs.select_formats import SelectFormats from calibre.gui2.dialogs.select_formats import SelectFormats
fmts = self.gui.library_view.model().db.all_formats() fmts = set([])
d = SelectFormats([x.lower() for x in fmts], msg, parent=self.gui) db = self.gui.library_view.model().db
for x in ids:
fmts_ = db.formats(x, index_is_id=True, verify_formats=False)
if fmts_:
fmts.update(frozenset([x.lower() for x in fmts_.split(',')]))
d = SelectFormats(list(sorted(fmts)), msg, parent=self.gui)
if d.exec_() != d.Accepted: if d.exec_() != d.Accepted:
return None return None
return d.selected_formats return d.selected_formats
@ -117,7 +123,7 @@ class DeleteAction(InterfaceAction):
if not ids: if not ids:
return return
fmts = self._get_selected_formats( fmts = self._get_selected_formats(
_('Choose formats to be deleted')) _('Choose formats to be deleted'), ids)
if not fmts: if not fmts:
return return
for id in ids: for id in ids:
@ -135,7 +141,7 @@ class DeleteAction(InterfaceAction):
if not ids: if not ids:
return return
fmts = self._get_selected_formats( fmts = self._get_selected_formats(
'<p>'+_('Choose formats <b>not</b> to be deleted')) '<p>'+_('Choose formats <b>not</b> to be deleted'), ids)
if fmts is None: if fmts is None:
return return
for id in ids: for id in ids:
@ -223,7 +229,31 @@ class DeleteAction(InterfaceAction):
rows = view.selectionModel().selectedRows() rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
return return
# Library view is visible.
if self.gui.stack.currentIndex() == 0: if self.gui.stack.currentIndex() == 0:
# Ask the user if they want to delete the book from the library or device if it is in both.
if self.gui.device_manager.is_device_connected:
on_device = False
on_device_ids = self._get_selected_ids()
for id in on_device_ids:
res = self.gui.book_on_device(id)
if res[0] or res[1] or res[2]:
on_device = True
if on_device:
break
if on_device:
loc = confirm_location('<p>' + _('Some of the selected books are on the attached device. '
'<b>Where</b> do you want the selected files deleted from?'),
self.gui)
if not loc:
return
elif loc == 'dev':
self.remove_matching_books_from_device()
return
elif loc == 'both':
self.remove_matching_books_from_device()
# The following will run if the selected books are not on a connected device.
# The user has selected to delete from the library or the device and library.
if not confirm('<p>'+_('The selected books will be ' if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> and the files ' '<b>permanently deleted</b> and the files '
'removed from your calibre library. Are you sure?') 'removed from your calibre library. Are you sure?')
@ -239,7 +269,7 @@ class DeleteAction(InterfaceAction):
else: else:
self.__md = MultiDeleter(self.gui, rows, self.__md = MultiDeleter(self.gui, rows,
partial(self.library_ids_deleted, current_row=row)) partial(self.library_ids_deleted, current_row=row))
# Device view is visible.
else: else:
if not confirm('<p>'+_('The selected books will be ' if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> ' '<b>permanently deleted</b> '

View File

@ -10,6 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2.preferences.main import Preferences from calibre.gui2.preferences.main import Preferences
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.constants import DEBUG
class PreferencesAction(InterfaceAction): class PreferencesAction(InterfaceAction):
@ -22,6 +23,10 @@ class PreferencesAction(InterfaceAction):
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config) pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'), pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
self.gui.run_wizard) self.gui.run_wizard)
if not DEBUG:
pm.addSeparator()
pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
self.debug_restart)
self.qaction.setMenu(pm) self.qaction.setMenu(pm)
self.preferences_menu = pm self.preferences_menu = pm
for x in (self.gui.preferences_action, self.qaction): for x in (self.gui.preferences_action, self.qaction):
@ -44,4 +49,6 @@ class PreferencesAction(InterfaceAction):
d.run_wizard_requested.connect(self.gui.run_wizard, d.run_wizard_requested.connect(self.gui.run_wizard,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
def debug_restart(self, *args):
self.gui.quit(restart=True, debug_on_restart=True)

View File

@ -368,6 +368,15 @@ class Adder(QObject): # {{{
shutil.rmtree(self.worker.tdir) shutil.rmtree(self.worker.tdir)
except: except:
pass pass
self._parent = None
self.pd.setParent(None)
del self.pd
self.pd = None
if hasattr(self, 'db_adder'):
self.db_adder.setParent(None)
del self.db_adder
self.db_adder = None
@property @property
def number_of_books_added(self): def number_of_books_added(self):

View File

@ -5,40 +5,207 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re, os
from lxml import html from lxml import html
from lxml.html import soupparser from lxml.html import soupparser
from PyQt4.Qt import QApplication, QFontInfo, QPalette, QSize, QWidget, \ from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
QToolBar, QVBoxLayout, QAction, QIcon QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
from PyQt4.QtWebKit import QWebView QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
from PyQt4.QtWebKit import QWebView, QWebPage
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre import xml_replace_entities from calibre import xml_replace_entities
from calibre.gui2 import open_url
class EditorWidget(QWebView): class PageAction(QAction): # {{{
def __init__(self, wac, icon, text, checkable, view):
QAction.__init__(self, QIcon(I(icon+'.png')), text, view)
self._page_action = getattr(QWebPage, wac)
self.setCheckable(checkable)
self.triggered.connect(self.trigger_page_action)
view.selectionChanged.connect(self.update_state,
type=Qt.QueuedConnection)
self.page_action.changed.connect(self.update_state,
type=Qt.QueuedConnection)
@property
def page_action(self):
return self.parent().pageAction(self._page_action)
def trigger_page_action(self, *args):
self.page_action.trigger()
def update_state(self, *args):
if self.isCheckable():
self.setChecked(self.page_action.isChecked())
self.setEnabled(self.page_action.isEnabled())
# }}}
class BlockStyleAction(QAction): # {{{
def __init__(self, text, name, view):
QAction.__init__(self, text, view)
self._name = name
self.triggered.connect(self.apply_style)
def apply_style(self, *args):
self.parent().exec_command('formatBlock', self._name)
# }}}
class EditorWidget(QWebView): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
for name, icon, text, checkable in [ for wac, name, icon, text, checkable in [
('bold', 'format-text-bold', _('Bold'), True), ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True),
('italic', 'format-text-italic', _('Italic'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'),
('underline', 'format-text-underline', _('Underline'), True), True),
('strikethrough', 'format-text-underline', _('Underline'), True), ('ToggleUnderline', 'underline', 'format-text-underline',
_('Underline'), True),
('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough',
_('Strikethrough'), True),
('ToggleSuperscript', 'superscript', 'format-text-superscript',
_('Superscript'), True),
('ToggleSubscript', 'subscript', 'format-text-subscript',
_('Subscript'), True),
('InsertOrderedList', 'ordered_list', 'format-list-ordered',
_('Ordered list'), True),
('InsertUnorderedList', 'unordered_list', 'format-list-unordered',
_('Unordered list'), True),
('AlignLeft', 'align_left', 'format-justify-left',
_('Align left'), False),
('AlignCenter', 'align_center', 'format-justify-center',
_('Align center'), False),
('AlignRight', 'align_right', 'format-justify-right',
_('Align right'), False),
('AlignJustified', 'align_justified', 'format-justify-fill',
_('Align justified'), False),
('Undo', 'undo', 'edit-undo', _('Undo'), False),
('Redo', 'redo', 'edit-redo', _('Redo'), False),
('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False),
('Copy', 'copy', 'edit-copy', _('Copy'), False),
('Paste', 'paste', 'edit-paste', _('Paste'), False),
('Cut', 'cut', 'edit-cut', _('Cut'), False),
('Indent', 'indent', 'format-indent-more',
_('Increase Indentation'), False),
('Outdent', 'outdent', 'format-indent-less',
_('Decrease Indentation'), False),
('SelectAll', 'select_all', 'edit-select-all',
_('Select all'), False),
]: ]:
ac = QAction(QIcon(I(icon+'.png')), text, self) ac = PageAction(wac, icon, text, checkable, self)
ac.setCheckable(checkable)
setattr(self, 'action_'+name, ac) setattr(self, 'action_'+name, ac)
self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'),
self)
self.action_color.triggered.connect(self.foreground_color)
self.action_background = QAction(QIcon(I('format-fill-color')),
_('Background color'), self)
self.action_background.triggered.connect(self.background_color)
self.action_block_style = QAction(QIcon(I('format-text-heading')),
_('Style text block'), self)
self.action_block_style.setToolTip(
_('Style the selected text block'))
self.block_style_menu = QMenu(self)
self.action_block_style.setMenu(self.block_style_menu)
self.block_style_actions = []
for text, name in [
(_('Normal'), 'p'),
(_('Heading') +' 1', 'h1'),
(_('Heading') +' 2', 'h2'),
(_('Heading') +' 3', 'h3'),
(_('Heading') +' 4', 'h4'),
(_('Heading') +' 5', 'h5'),
(_('Heading') +' 6', 'h6'),
(_('Pre-formatted'), 'pre'),
(_('Blockquote'), 'blockquote'),
(_('Address'), 'address'),
]:
ac = BlockStyleAction(text, name, self)
self.block_style_menu.addAction(ac)
self.block_style_actions.append(ac)
self.action_insert_link = QAction(QIcon(I('insert-link.png')),
_('Insert link'), self)
self.action_insert_link.triggered.connect(self.insert_link)
self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page().linkClicked.connect(self.link_clicked)
def link_clicked(self, url):
open_url(url)
def foreground_color(self):
col = QColorDialog.getColor(Qt.black, self,
_('Choose foreground color'), QColorDialog.ShowAlphaChannel)
if col.isValid():
self.exec_command('foreColor', unicode(col.name()))
def background_color(self):
col = QColorDialog.getColor(Qt.white, self,
_('Choose background color'), QColorDialog.ShowAlphaChannel)
if col.isValid():
self.exec_command('hiliteColor', unicode(col.name()))
def insert_link(self, *args):
link, ok = QInputDialog.getText(self, _('Create link'),
_('Enter URL'))
if not ok:
return
url = self.parse_link(unicode(link))
if url.isValid():
url = unicode(url.toString())
self.exec_command('createLink', url)
def parse_link(self, link):
link = link.strip()
has_schema = re.match(r'^[a-zA-Z]+:', link)
if has_schema is not None:
url = QUrl(link, QUrl.TolerantMode)
if url.isValid():
return url
if os.path.exists(link):
return QUrl.fromLocalFile(link)
if has_schema is None:
first, _, rest = link.partition('.')
prefix = 'http'
if first == 'ftp':
prefix = 'ftp'
url = QUrl(prefix +'://'+link, QUrl.TolerantMode)
if url.isValid():
return url
return QUrl(link, QUrl.TolerantMode)
def sizeHint(self): def sizeHint(self):
return QSize(150, 150) return QSize(150, 150)
def exec_command(self, cmd, arg=None):
frame = self.page().mainFrame()
if arg is not None:
js = 'document.execCommand("%s", false, "%s");' % (cmd, arg)
else:
js = 'document.execCommand("%s", false, null);' % cmd
frame.evaluateJavaScript(js)
@dynamic_property @dynamic_property
def html(self): def html(self):
def fget(self): def fget(self):
ans = u'' ans = u''
check = unicode(self.page().mainFrame().toPlainText()).strip()
if not check:
return ans
try: try:
raw = unicode(self.page().mainFrame().toHtml()) raw = unicode(self.page().mainFrame().toHtml())
raw = xml_to_unicode(raw, strip_encoding_pats=True, raw = xml_to_unicode(raw, strip_encoding_pats=True,
@ -67,12 +234,7 @@ class EditorWidget(QWebView):
def fset(self, val): def fset(self, val):
self.setHtml(val) self.setHtml(val)
f = QFontInfo(QApplication.font(self)).pixelSize() f = QFontInfo(QApplication.font(self)).pixelSize()
b = unicode(QApplication.palette().color(QPalette.Normal, style = 'font-size: %dpx;' % (f,)
QPalette.Base).name())
c = unicode(QApplication.palette().color(QPalette.Normal,
QPalette.Text).name())
style = 'font-size: %dpx; background-color: %s; color: %s' % (f, b,
c)
for body in self.page().mainFrame().documentElement().findAll('body'): for body in self.page().mainFrame().documentElement().findAll('body'):
body.setAttribute('style', style) body.setAttribute('style', style)
@ -80,26 +242,316 @@ class EditorWidget(QWebView):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
# }}}
class Editor(QWidget): # Highlighter {{{
State_Text = -1
State_DocType = 0
State_Comment = 1
State_TagStart = 2
State_TagName = 3
State_InsideTag = 4
State_AttributeName = 5
State_SingleQuote = 6
State_DoubleQuote = 7
State_AttributeValue = 8
class Highlighter(QSyntaxHighlighter):
def __init__(self, doc):
QSyntaxHighlighter.__init__(self, doc)
self.colors = {}
self.colors['doctype'] = QColor(192, 192, 192)
self.colors['entity'] = QColor(128, 128, 128)
self.colors['tag'] = QColor(136, 18, 128)
self.colors['comment'] = QColor( 35, 110, 37)
self.colors['attrname'] = QColor(153, 69, 0)
self.colors['attrval'] = QColor( 36, 36, 170)
def highlightBlock(self, text):
state = self.previousBlockState()
len_ = text.length()
start = 0
pos = 0
while pos < len_:
if state == State_Comment:
start = pos
while pos < len_:
if text.mid(pos, 3) == "-->":
pos += 3;
state = State_Text;
break
else:
pos += 1
self.setFormat(start, pos - start, self.colors['comment'])
elif state == State_DocType:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('>'):
state = State_Text
break
self.setFormat(start, pos - start, self.colors['doctype'])
# at '<' in e.g. "<span>foo</span>"
elif state == State_TagStart:
start = pos + 1
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('>'):
state = State_Text
break
if not ch.isSpace():
pos -= 1
state = State_TagName
break
# at 'b' in e.g "<blockquote>foo</blockquote>"
elif state == State_TagName:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch.isSpace():
pos -= 1
state = State_InsideTag
break
if ch == QChar('>'):
state = State_Text
break
self.setFormat(start, pos - start, self.colors['tag']);
# anywhere after tag name and before tag closing ('>')
elif state == State_InsideTag:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('/'):
continue
if ch == QChar('>'):
state = State_Text
break
if not ch.isSpace():
pos -= 1
state = State_AttributeName
break
# at 's' in e.g. <img src=bla.png/>
elif state == State_AttributeName:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('='):
state = State_AttributeValue
break
if ch in (QChar('>'), QChar('/')):
state = State_InsideTag
break
self.setFormat(start, pos - start, self.colors['attrname'])
# after '=' in e.g. <img src=bla.png/>
elif state == State_AttributeValue:
start = pos
# find first non-space character
while pos < len_:
ch = text.at(pos)
pos += 1
# handle opening single quote
if ch == QChar("'"):
state = State_SingleQuote
break
# handle opening double quote
if ch == QChar('"'):
state = State_DoubleQuote
break
if not ch.isSpace():
break
if state == State_AttributeValue:
# attribute value without quote
# just stop at non-space or tag delimiter
start = pos
while pos < len_:
ch = text.at(pos);
if ch.isSpace():
break
if ch in (QChar('>'), QChar('/')):
break
pos += 1
state = State_InsideTag
self.setFormat(start, pos - start, self.colors['attrval'])
# after the opening single quote in an attribute value
elif state == State_SingleQuote:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar("'"):
break
state = State_InsideTag
self.setFormat(start, pos - start, self.colors['attrval'])
# after the opening double quote in an attribute value
elif state == State_DoubleQuote:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('"'):
break
state = State_InsideTag
self.setFormat(start, pos - start, self.colors['attrval'])
else:
# State_Text and default
while pos < len_:
ch = text.at(pos)
if ch == QChar('<'):
if text.mid(pos, 4) == "<!--":
state = State_Comment
else:
if text.mid(pos, 9).toUpper() == "<!DOCTYPE":
state = State_DocType
else:
state = State_TagStart
break;
elif ch == QChar('&'):
start = pos
while pos < len_ and text.at(pos) != QChar(';'):
self.setFormat(start, pos - start,
self.colors['entity'])
pos += 1
else:
pos += 1
self.setCurrentBlockState(state)
# }}}
class Editor(QWidget): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.toolbar = QToolBar(self) self.toolbar1 = QToolBar(self)
self.toolbar2 = QToolBar(self)
self.editor = EditorWidget(self) self.editor = EditorWidget(self)
self.tabs = QTabWidget(self)
self.tabs.setTabPosition(self.tabs.South)
self.wyswyg = QWidget(self.tabs)
self.code_edit = QPlainTextEdit(self.tabs)
self.source_dirty = False
self.wyswyg_dirty = True
self._layout = QVBoxLayout(self) self._layout = QVBoxLayout(self)
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
self.setLayout(self._layout) self.setLayout(self._layout)
self._layout.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
self._layout.addWidget(self.toolbar) l.addWidget(self.toolbar1)
self._layout.addWidget(self.editor) l.addWidget(self.toolbar2)
l.addWidget(self.editor)
self._layout.addWidget(self.tabs)
self.tabs.addTab(self.wyswyg, _('Normal view'))
self.tabs.addTab(self.code_edit, _('HTML Source'))
self.tabs.currentChanged[int].connect(self.change_tab)
self.highlighter = Highlighter(self.code_edit.document())
for x in ('bold', 'italic', 'underline', 'strikethrough',
'superscript', 'subscript', 'indent', 'outdent'):
ac = getattr(self.editor, 'action_'+x)
if x in ('superscript', 'indent'):
self.toolbar2.addSeparator()
self.toolbar2.addAction(ac)
self.toolbar2.addSeparator()
for x in ('left', 'center', 'right', 'justified'):
ac = getattr(self.editor, 'action_align_'+x)
self.toolbar2.addAction(ac)
self.toolbar2.addSeparator()
self.toolbar1.addAction(self.editor.action_undo)
self.toolbar1.addAction(self.editor.action_redo)
self.toolbar1.addAction(self.editor.action_select_all)
self.toolbar1.addAction(self.editor.action_remove_format)
self.toolbar1.addSeparator()
for x in ('copy', 'cut', 'paste'):
ac = getattr(self.editor, 'action_'+x)
self.toolbar1.addAction(ac)
self.toolbar1.addSeparator()
for x in ('', 'un'):
ac = getattr(self.editor, 'action_%sordered_list'%x)
self.toolbar1.addAction(ac)
self.toolbar1.addSeparator()
self.toolbar1.addAction(self.editor.action_color)
self.toolbar1.addAction(self.editor.action_background)
self.toolbar1.addSeparator()
self.toolbar1.addAction(self.editor.action_block_style)
w = self.toolbar1.widgetForAction(self.editor.action_block_style)
w.setPopupMode(w.InstantPopup)
self.toolbar1.addAction(self.editor.action_insert_link)
self.code_edit.textChanged.connect(self.code_dirtied)
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)
@dynamic_property @dynamic_property
def html(self): def html(self):
def fset(self, v): def fset(self, v):
self.editor.html = v self.editor.html = v
return property(fget=lambda self:self.editor.html, fset=fset) def fget(self):
self.tabs.setCurrentIndex(0)
return self.editor.html
return property(fget=fget, fset=fset)
def change_tab(self, index):
#print 'reloading:', (index and self.wyswyg_dirty) or (not index and
# self.source_dirty)
if index == 1: # changing to code view
if self.wyswyg_dirty:
self.code_edit.setPlainText(self.editor.html)
self.wyswyg_dirty = False
elif index == 0: #changing to wyswyg
if self.source_dirty:
self.editor.html = unicode(self.code_edit.toPlainText())
self.source_dirty = False
def wyswyg_dirtied(self, *args):
self.wyswyg_dirty = True
def code_dirtied(self, *args):
self.source_dirty = True
# }}}
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])

View File

@ -17,6 +17,8 @@ class PluginWidget(Widget, Ui_Form):
ICON = I('mimetypes/fb2.png') ICON = I('mimetypes/fb2.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, ['h1_to_title', 'h2_to_title', 'h3_to_title']) Widget.__init__(self, parent, ['sectionize'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
for x in ('toc', 'files', 'nothing'):
self.opt_sectionize.addItem(x)
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)

View File

@ -14,7 +14,7 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="3" column="0"> <item row="1" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -28,23 +28,19 @@
</spacer> </spacer>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="opt_h1_to_title"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Wrap h1 tags with &lt;title&gt; elements</string> <string>Sectionize:</string>
</property> </property>
</widget> <property name="buddy">
</item> <cstring>opt_sectionize</cstring>
<item row="1" column="0"> </property>
<widget class="QCheckBox" name="opt_h2_to_title"> </widget>
<property name="text"> </item>
<string>Wrap h2 tags with &lt;title&gt; elements</string> <item row="0" column="1">
</property> <widget class="QComboBox" name="opt_sectionize">
</widget> <property name="minimumContentsLength">
</item> <number>20</number>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_h3_to_title">
<property name="text">
<string>Wrap h3 tags with &lt;title&gt; elements</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -11,7 +11,6 @@ from PyQt4.Qt import Qt
from calibre.gui2.convert.mobi_output_ui import Ui_Form from calibre.gui2.convert.mobi_output_ui import Ui_Form
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
from calibre.gui2.widgets import FontFamilyModel from calibre.gui2.widgets import FontFamilyModel
from calibre.utils.fonts import fontconfig
font_family_model = None font_family_model = None
@ -28,6 +27,7 @@ class PluginWidget(Widget, Ui_Form):
'mobi_ignore_margins', 'mobi_ignore_margins',
'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc'] 'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc']
) )
from calibre.utils.fonts import fontconfig
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
global font_family_model global font_family_model

View File

@ -303,7 +303,7 @@ class Series(Base):
if val == '': if val == '':
val = s_index = None val = s_index = None
elif s_index == 0.0: elif s_index == 0.0:
if tweaks['series_index_auto_increment'] == 'next': if tweaks['series_index_auto_increment'] != 'const':
s_index = self.db.get_next_cc_series_num_for(val, s_index = self.db.get_next_cc_series_num_for(val,
num=self.col_id) num=self.col_id)
else: else:
@ -572,7 +572,6 @@ class BulkSeries(BulkBase):
val = None if clear else self.normalize_ui_val(val) val = None if clear else self.normalize_ui_val(val)
if clear or val != '': if clear or val != '':
extras = [] extras = []
next_index = self.db.get_next_cc_series_num_for(val, num=self.col_id)
for book_id in book_ids: for book_id in book_ids:
if clear: if clear:
extras.append(None) extras.append(None)
@ -581,16 +580,13 @@ class BulkSeries(BulkBase):
if force_start: if force_start:
s_index = at_value s_index = at_value
at_value += 1 at_value += 1
elif tweaks['series_index_auto_increment'] == 'next': elif tweaks['series_index_auto_increment'] != 'const':
s_index = next_index s_index = self.db.get_next_cc_series_num_for(val, num=self.col_id)
next_index += 1
else: else:
s_index = 1.0 s_index = 1.0
else: else:
s_index = self.db.get_custom_extra(book_id, num=self.col_id, s_index = self.db.get_custom_extra(book_id, num=self.col_id,
index_is_id=True) index_is_id=True)
if s_index is None:
s_index = 1.0
extras.append(s_index) extras.append(s_index)
self.db.set_custom_bulk(book_ids, val, extras=extras, self.db.set_custom_bulk(book_ids, val, extras=extras,
num=self.col_id, notify=notify) num=self.col_id, notify=notify)

View File

@ -593,7 +593,6 @@ class DeviceMenu(QMenu): # {{{
# }}} # }}}
class DeviceMixin(object): # {{{ class DeviceMixin(object): # {{{
def __init__(self): def __init__(self):

View File

@ -32,6 +32,11 @@ class ChooseLibrary(QDialog, Ui_Dialog):
loc = unicode(self.old_location.text()).format(lp) loc = unicode(self.old_location.text()).format(lp)
self.old_location.setText(loc) self.old_location.setText(loc)
self.browse_button.clicked.connect(self.choose_loc) self.browse_button.clicked.connect(self.choose_loc)
self.empty_library.toggled.connect(self.empty_library_toggled)
self.copy_structure.setEnabled(False)
def empty_library_toggled(self, to_what):
self.copy_structure.setEnabled(to_what)
def choose_loc(self, *args): def choose_loc(self, *args):
loc = choose_dir(self, 'choose library location', loc = choose_dir(self, 'choose library location',
@ -64,7 +69,7 @@ class ChooseLibrary(QDialog, Ui_Dialog):
def perform_action(self, ac, loc): def perform_action(self, ac, loc):
if ac in ('new', 'existing'): if ac in ('new', 'existing'):
prefs['library_path'] = loc prefs['library_path'] = loc
self.callback(loc) self.callback(loc, copy_structure=self.copy_structure.isChecked())
else: else:
move_library(self.db.library_path, loc, self.parent(), move_library(self.db.library_path, loc, self.parent(),
self.callback) self.callback)

View File

@ -49,11 +49,26 @@
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="3"> <item row="5" column="0" colspan="3">
<widget class="QRadioButton" name="empty_library"> <layout class="QHBoxLayout" name="hbox1">
<property name="text"> <item>
<string>&amp;Create an empty library at the new location</string> <widget class="QRadioButton" name="empty_library">
</property> <property name="text">
</widget> <string>&amp;Create an empty library at the new location</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="copy_structure">
<property name="text">
<string>&amp;Copy structure from the current library</string>
</property>
<property name="toolTip">
<string>Copy the custom columns, saved searches, column widths, plugboards,
user categories, and other information from the old to the new library</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item row="6" column="0" colspan="3"> <item row="6" column="0" colspan="3">
<widget class="QRadioButton" name="move_library"> <widget class="QRadioButton" name="move_library">

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' \
'2010, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from functools import partial
from calibre.gui2.dialogs.confirm_delete_location_ui import Ui_Dialog
from PyQt4.Qt import QDialog, Qt, QPixmap, QIcon
class Dialog(QDialog, Ui_Dialog):
def __init__(self, msg, name, parent):
QDialog.__init__(self, parent)
self.setupUi(self)
self.loc = None
self.msg.setText(msg)
self.name = name
self.buttonBox.setFocus(Qt.OtherFocusReason)
self.button_lib.clicked.connect(partial(self.set_loc, 'lib'))
self.button_device.clicked.connect(partial(self.set_loc, 'dev'))
self.button_both.clicked.connect(partial(self.set_loc, 'both'))
def set_loc(self, loc):
self.loc = loc
self.accept()
def choice(self):
return self.loc
def break_cycles(self):
for x in ('lib', 'device', 'both'):
b = getattr(self, 'button_'+x)
try:
b.clicked.disconnect()
except:
pass
def confirm_location(msg, name, parent=None, pixmap='dialog_warning.png'):
d = Dialog(msg, name, parent)
d.label.setPixmap(QPixmap(I(pixmap)))
d.setWindowIcon(QIcon(I(pixmap)))
d.resize(d.sizeHint())
ret = d.exec_()
d.break_cycles()
if ret == d.Accepted:
return d.choice()
return None

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>459</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Where do you want to delete from?</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/dialog_warning.png</normaloff>:/images/dialog_warning.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="pixmap">
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="msg">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="button_lib">
<property name="text">
<string>Library</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_device">
<property name="text">
<string>Device</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_both">
<property name="text">
<string>Library and Device</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -16,6 +16,7 @@ from calibre.gui2 import error_dialog, NONE, info_dialog, config
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.widgets import ProgressIndicator
from calibre import strftime, force_unicode from calibre import strftime, force_unicode
from calibre.customize.ui import get_isbndb_key, set_isbndb_key from calibre.customize.ui import get_isbndb_key, set_isbndb_key
from calibre.utils.icu import sort_key
_hung_fetchers = set([]) _hung_fetchers = set([])
@ -72,27 +73,33 @@ class Matches(QAbstractTableModel):
def summary(self, row): def summary(self, row):
return self.matches[row].comments return self.matches[row].comments
def data_as_text(self, book, col):
if col == 0 and book.title is not None:
return book.title
elif col == 1:
return ', '.join(book.authors)
elif col == 2 and book.author_sort is not None:
return book.author_sort
elif col == 3 and book.publisher is not None:
return book.publisher
elif col == 4 and book.isbn is not None:
return book.isbn
elif col == 5 and hasattr(book.pubdate, 'timetuple'):
return strftime('%b %Y', book.pubdate.timetuple())
elif col == 6 and book.has_cover:
return 'y'
elif col == 7 and book.comments:
return 'y'
return ''
def data(self, index, role): def data(self, index, role):
row, col = index.row(), index.column() row, col = index.row(), index.column()
book = self.matches[row] book = self.matches[row]
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
res = None res = self.data_as_text(book, col)
if col == 0: if col <= 5 and res:
res = book.title return QVariant(res)
elif col == 1: return NONE
res = ', '.join(book.authors)
elif col == 2:
res = book.author_sort
elif col == 3:
res = book.publisher
elif col == 4:
res = book.isbn
elif col == 5:
if hasattr(book.pubdate, 'timetuple'):
res = strftime('%b %Y', book.pubdate.timetuple())
if not res:
return NONE
return QVariant(res)
elif role == Qt.DecorationRole: elif role == Qt.DecorationRole:
if col == 6 and book.has_cover: if col == 6 and book.has_cover:
return self.yes_icon return self.yes_icon
@ -100,6 +107,16 @@ class Matches(QAbstractTableModel):
return self.yes_icon return self.yes_icon
return NONE return NONE
def sort(self, col, order, reset=True):
if not self.matches:
return
descending = order == Qt.DescendingOrder
self.matches.sort(None,
lambda x: sort_key(unicode(force_unicode(self.data_as_text(x, col)))),
descending)
if reset:
self.reset()
class FetchMetadata(QDialog, Ui_FetchMetadata): class FetchMetadata(QDialog, Ui_FetchMetadata):
HANG_TIME = 75 #seconds HANG_TIME = 75 #seconds
@ -136,6 +153,11 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.connect(self.matches, SIGNAL('entered(QModelIndex)'), self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
self.show_summary) self.show_summary)
self.matches.setMouseTracking(True) self.matches.setMouseTracking(True)
# Enabling sorting and setting a sort column will not change the initial
# order of the results, as they are filled in later
self.matches.setSortingEnabled(True)
self.matches.horizontalHeader().sectionClicked.connect(self.show_sort_indicator)
self.matches.horizontalHeader().setSortIndicatorShown(False)
self.fetch_metadata() self.fetch_metadata()
self.opt_get_social_metadata.setChecked(config['get_social_metadata']) self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata']) self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata'])
@ -243,3 +265,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
def chosen(self, index): def chosen(self, index):
self.matches.setCurrentIndex(index) self.matches.setCurrentIndex(index)
self.accept() self.accept()
def show_sort_indicator(self, *args):
self.matches.horizontalHeader().setSortIndicatorShown(True)

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to edit metadata in bulk''' '''Dialog to edit metadata in bulk'''
import re import re, os
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \ from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
pyqtSignal, QDialogButtonBox pyqtSignal, QDialogButtonBox
@ -12,12 +12,42 @@ from PyQt4 import QtGui
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils.config import dynamic from calibre.utils.config import dynamic
from calibre.utils.titlecase import titlecase from calibre.utils.titlecase import titlecase
from calibre.utils.icu import sort_key, capitalize from calibre.utils.icu import sort_key, capitalize
from calibre.utils.config import prefs
from calibre.utils.magick.draw import identify_data
def get_cover_data(path):
old = prefs['read_file_metadata']
if not old:
prefs['read_file_metadata'] = True
cdata = area = None
try:
mi = get_metadata(open(path, 'rb'),
os.path.splitext(path)[1][1:].lower())
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata:
width, height, fmt = identify_data(cdata)
area = width*height
except:
cdata = area = None
if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old
return cdata, area
class MyBlockingBusy(QDialog): class MyBlockingBusy(QDialog):
@ -146,6 +176,20 @@ class MyBlockingBusy(QDialog):
cdata = calibre_cover(mi.title, mi.format_field('authors')[-1], cdata = calibre_cover(mi.title, mi.format_field('authors')[-1],
series_string=series_string) series_string=series_string)
self.db.set_cover(id, cdata) self.db.set_cover(id, cdata)
elif cover_action == 'fromfmt':
fmts = self.db.formats(id, index_is_id=True, verify_formats=False)
if fmts:
covers = []
for fmt in fmts.split(','):
fmt = self.db.format_abspath(id, fmt, index_is_id=True)
if not fmt: continue
cdata, area = get_cover_data(fmt)
if cdata:
covers.append((cdata, area))
covers.sort(key=lambda x: x[1])
if covers:
self.db.set_cover(id, covers[-1][0])
covers = []
elif self.current_phase == 2: elif self.current_phase == 2:
# All of these just affect the DB, so we can tolerate a total rollback # All of these just affect the DB, so we can tolerate a total rollback
if do_auto_author: if do_auto_author:
@ -268,6 +312,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
def prepare_search_and_replace(self): def prepare_search_and_replace(self):
self.search_for.initialize('bulk_edit_search_for') self.search_for.initialize('bulk_edit_search_for')
self.replace_with.initialize('bulk_edit_replace_with') self.replace_with.initialize('bulk_edit_replace_with')
self.s_r_template.initialize('bulk_edit_template')
self.test_text.initialize('bulk_edit_test_test') self.test_text.initialize('bulk_edit_test_test')
self.all_fields = [''] self.all_fields = ['']
self.writable_fields = [''] self.writable_fields = ['']
@ -282,9 +327,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
if f in ['sort'] or fm[f]['datatype'] == 'composite': if f in ['sort'] or fm[f]['datatype'] == 'composite':
self.all_fields.append(f) self.all_fields.append(f)
self.all_fields.sort() self.all_fields.sort()
self.all_fields.insert(1, '{template}')
self.writable_fields.sort() self.writable_fields.sort()
self.search_field.setMaxVisibleItems(20) self.search_field.setMaxVisibleItems(25)
self.destination_field.setMaxVisibleItems(20) self.destination_field.setMaxVisibleItems(25)
offset = 10 offset = 10
self.s_r_number_of_books = min(10, len(self.ids)) self.s_r_number_of_books = min(10, len(self.ids))
for i in range(1,self.s_r_number_of_books+1): for i in range(1,self.s_r_number_of_books+1):
@ -360,22 +406,28 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.test_text.editTextChanged[str].connect(self.s_r_paint_results) self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
self.comma_separated.stateChanged.connect(self.s_r_paint_results) self.comma_separated.stateChanged.connect(self.s_r_paint_results)
self.case_sensitive.stateChanged.connect(self.s_r_paint_results) self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
self.s_r_template.lost_focus.connect(self.s_r_template_changed)
self.central_widget.setCurrentIndex(0) self.central_widget.setCurrentIndex(0)
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive) self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive) self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive)
self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive)
self.s_r_search_mode_changed(self.search_mode.currentIndex()) self.s_r_search_mode_changed(self.search_mode.currentIndex())
def s_r_get_field(self, mi, field): def s_r_get_field(self, mi, field):
if field: if field:
if field == '{template}':
v = composite_formatter.safe_format\
(unicode(self.s_r_template.text()), mi, _('S/R TEMPLATE ERROR'), mi)
return [v]
fm = self.db.metadata_for_field(field) fm = self.db.metadata_for_field(field)
if field == 'sort': if field == 'sort':
val = mi.get('title_sort', None) val = mi.get('title_sort', None)
else: else:
val = mi.get(field, None) val = mi.get(field, None)
if val is None: if val is None:
val = [] val = [] if fm['is_multiple'] else ['']
elif not fm['is_multiple']: elif not fm['is_multiple']:
val = [val] val = [val]
elif field == 'authors': elif field == 'authors':
@ -384,7 +436,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
val = [] val = []
return val return val
def s_r_template_changed(self):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_search_field_changed(self, idx): def s_r_search_field_changed(self, idx):
if self.search_mode.currentIndex() != 0 and idx == 1: # Template
self.s_r_template.setVisible(True)
self.template_label.setVisible(True)
else:
self.s_r_template.setVisible(False)
self.template_label.setVisible(False)
for i in range(0, self.s_r_number_of_books): for i in range(0, self.s_r_number_of_books):
w = getattr(self, 'book_%d_text'%(i+1)) w = getattr(self, 'book_%d_text'%(i+1))
mi = self.db.get_metadata(self.ids[i], index_is_id=True) mi = self.db.get_metadata(self.ids[i], index_is_id=True)
@ -547,11 +608,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
if not dest: if not dest:
dest = source dest = source
dfm = self.db.field_metadata[dest] dfm = self.db.field_metadata[dest]
mi = self.db.get_metadata(id, index_is_id=True,) mi = self.db.get_metadata(id, index_is_id=True,)
val = mi.get(source)
if val is None:
return
val = self.s_r_do_regexp(mi) val = self.s_r_do_regexp(mi)
val = self.s_r_do_destination(mi, val) val = self.s_r_do_destination(mi, val)
if dfm['is_multiple']: if dfm['is_multiple']:
@ -700,6 +757,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
cover_action = 'remove' cover_action = 'remove'
elif self.cover_generate.isChecked(): elif self.cover_generate.isChecked():
cover_action = 'generate' cover_action = 'generate'
elif self.cover_from_fmt.isChecked():
cover_action = 'fromfmt'
args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series,
do_autonumber, do_remove_format, remove_format, do_swap_ta, do_autonumber, do_remove_format, remove_format, do_swap_ta,

View File

@ -414,6 +414,13 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QRadioButton" name="cover_from_fmt">
<property name="text">
<string>Set from &amp;ebook file(s)</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -501,6 +508,29 @@ Future conversion of these books will use the default settings.</string>
</layout> </layout>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="template_label">
<property name="text">
<string>Te&amp;mplate:</string>
</property>
<property name="buddy">
<cstring>s_r_template</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="HistoryLineEdit" name="s_r_template">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a template to be used as the source for the search/replace</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="xlabel_2"> <widget class="QLabel" name="xlabel_2">
<property name="text"> <property name="text">
<string>&amp;Search for:</string> <string>&amp;Search for:</string>
@ -510,7 +540,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="5" column="1">
<widget class="HistoryLineEdit" name="search_for"> <widget class="HistoryLineEdit" name="search_for">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -523,7 +553,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="2"> <item row="5" column="2">
<widget class="QCheckBox" name="case_sensitive"> <widget class="QCheckBox" name="case_sensitive">
<property name="toolTip"> <property name="toolTip">
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string> <string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
@ -536,7 +566,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QLabel" name="xlabel_4"> <widget class="QLabel" name="xlabel_4">
<property name="text"> <property name="text">
<string>&amp;Replace with:</string> <string>&amp;Replace with:</string>
@ -546,14 +576,14 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="6" column="1">
<widget class="HistoryLineEdit" name="replace_with"> <widget class="HistoryLineEdit" name="replace_with">
<property name="toolTip"> <property name="toolTip">
<string>The replacement text. The matched search text will be replaced with this string</string> <string>The replacement text. The matched search text will be replaced with this string</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="2"> <item row="6" column="2">
<layout class="QHBoxLayout" name="verticalLayout"> <layout class="QHBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="label_41"> <widget class="QLabel" name="label_41">
@ -588,7 +618,7 @@ field is processed. In regular expression mode, only the matched text is process
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="destination_field_label"> <widget class="QLabel" name="destination_field_label">
<property name="text"> <property name="text">
<string>&amp;Destination field:</string> <string>&amp;Destination field:</string>
@ -598,14 +628,15 @@ field is processed. In regular expression mode, only the matched text is process
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="7" column="1">
<widget class="QComboBox" name="destination_field"> <widget class="QComboBox" name="destination_field">
<property name="toolTip"> <property name="toolTip">
<string>The field that the text will be put into after all replacements. If blank, the source field is used.</string> <string>The field that the text will be put into after all replacements.
If blank, the source field is used if the field is modifiable</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="2"> <item row="7" column="2">
<layout class="QHBoxLayout" name="verticalLayout"> <layout class="QHBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="replace_mode_label"> <widget class="QLabel" name="replace_mode_label">
@ -653,7 +684,7 @@ nothing should be put between the original text and the inserted text</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="7" column="1"> <item row="8" column="1">
<widget class="QLabel" name="xlabel_3"> <widget class="QLabel" name="xlabel_3">
<property name="text"> <property name="text">
<string>Test &amp;text</string> <string>Test &amp;text</string>
@ -663,7 +694,7 @@ nothing should be put between the original text and the inserted text</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="2"> <item row="8" column="2">
<widget class="QLabel" name="label_51"> <widget class="QLabel" name="label_51">
<property name="text"> <property name="text">
<string>Test re&amp;sult</string> <string>Test re&amp;sult</string>
@ -686,8 +717,8 @@ nothing should be put between the original text and the inserted text</string>
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>122</width> <width>726</width>
<height>38</height> <height>334</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="testgrid"> <layout class="QGridLayout" name="testgrid">
@ -784,6 +815,7 @@ nothing should be put between the original text and the inserted text</string>
<tabstop>central_widget</tabstop> <tabstop>central_widget</tabstop>
<tabstop>search_field</tabstop> <tabstop>search_field</tabstop>
<tabstop>search_mode</tabstop> <tabstop>search_mode</tabstop>
<tabstop>s_r_template</tabstop>
<tabstop>search_for</tabstop> <tabstop>search_for</tabstop>
<tabstop>case_sensitive</tabstop> <tabstop>case_sensitive</tabstop>
<tabstop>replace_with</tabstop> <tabstop>replace_with</tabstop>

View File

@ -23,7 +23,7 @@ from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import string_to_authors, \ from calibre.ebooks.metadata import string_to_authors, \
authors_to_string, check_isbn authors_to_string, check_isbn, title_sort
from calibre.ebooks.metadata.covers import download_cover from calibre.ebooks.metadata.covers import download_cover
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
@ -34,6 +34,7 @@ from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.preferences.social import SocialMetadata from calibre.gui2.preferences.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre import strftime from calibre import strftime
from calibre.library.comments import comments_to_html
class CoverFetcher(Thread): # {{{ class CoverFetcher(Thread): # {{{
@ -195,7 +196,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
_file + _(" is not a valid picture")) _file + _(" is not a valid picture"))
d.exec_() d.exec_()
else: else:
self.cover_path.setText(_file)
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.update_cover_tooltip() self.update_cover_tooltip()
self.cover_changed = True self.cover_changed = True
@ -409,7 +409,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if mi.series_index is not None: if mi.series_index is not None:
self.series_index.setValue(float(mi.series_index)) self.series_index.setValue(float(mi.series_index))
if mi.comments and mi.comments.strip(): if mi.comments and mi.comments.strip():
self.comments.setPlainText(mi.comments) comments = comments_to_html(mi.comments)
self.comments.html = comments
def sync_formats(self): def sync_formats(self):
@ -444,13 +445,24 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover_fetcher = None self.cover_fetcher = None
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter) self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
base = unicode(self.author_sort.toolTip()) base = unicode(self.author_sort.toolTip())
self.ok_aus_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+ ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
_(' The green color indicates that the current ' _(' The green color indicates that the current '
'author sort matches the current author')) 'author sort matches the current author'))
self.bad_aus_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+ bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
_(' The red color indicates that the current ' _(' The red color indicates that the current '
'author sort does not match the current author')) 'author sort does not match the current author. '
'No action is required if this is what you want.'))
self.aus_tooltips = (ok_tooltip, bad_tooltip)
base = unicode(self.title_sort.toolTip())
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
_(' The green color indicates that the current '
'title sort matches the current title'))
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
_(' The red color warns that the current '
'title sort does not match the current title. '
'No action is required if this is what you want.'))
self.ts_tooltips = (ok_tooltip, bad_tooltip)
self.row_delta = 0 self.row_delta = 0
if prev: if prev:
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
@ -506,7 +518,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.remove_unused_series) self.remove_unused_series)
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'), QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
self.deduce_author_sort) self.deduce_author_sort)
QObject.connect(self.auto_title_sort, SIGNAL('clicked()'),
self.deduce_title_sort)
self.trim_cover_button.clicked.connect(self.trim_cover) self.trim_cover_button.clicked.connect(self.trim_cover)
self.connect(self.title_sort, SIGNAL('textChanged(const QString&)'),
self.title_sort_box_changed)
self.connect(self.title, SIGNAL('textChanged(const QString&)'),
self.title_box_changed)
self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'), self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'),
self.author_sort_box_changed) self.author_sort_box_changed)
self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'), self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'),
@ -523,6 +541,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.title.setText(db.title(row)) self.title.setText(db.title(row))
self.title_sort.setText(db.title_sort(row))
isbn = db.isbn(self.id, index_is_id=True) isbn = db.isbn(self.id, index_is_id=True)
if not isbn: if not isbn:
isbn = '' isbn = ''
@ -538,7 +557,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if rating > 0: if rating > 0:
self.rating.setValue(int(rating/2.)) self.rating.setValue(int(rating/2.))
comments = self.db.comments(row) comments = self.db.comments(row)
self.comments.setPlainText(comments if comments else '') if comments and comments.strip():
comments = comments_to_html(comments)
self.comments.html = comments
cover = self.db.cover(row) cover = self.db.cover(row)
pubdate = db.pubdate(self.id, index_is_id=True) pubdate = db.pubdate(self.id, index_is_id=True)
self.pubdate.setDate(QDate(pubdate.year, pubdate.month, self.pubdate.setDate(QDate(pubdate.year, pubdate.month,
@ -598,8 +619,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
w = self.central_widget.widget(1) w = self.central_widget.widget(1)
layout = w.layout() layout = w.layout()
self.custom_column_widgets, self.__cc_spacers = \ self.custom_column_widgets, self.__cc_spacers = \
populate_metadata_page(layout, self.db, self.id, populate_metadata_page(layout, self.db, self.id, parent=w, bulk=False,
parent=w, bulk=False, two_column=True) two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
self.__custom_col_layouts = [layout] self.__custom_col_layouts = [layout]
ans = self.custom_column_widgets ans = self.custom_column_widgets
for i in range(len(ans)-1): for i in range(len(ans)-1):
@ -610,27 +631,40 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
for c in range(2, len(ans[i].widgets), 2): for c in range(2, len(ans[i].widgets), 2):
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1]) w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
def title_box_changed(self, txt):
ts = unicode(txt)
ts = title_sort(ts)
self.mark_box_as_ok(control = self.title_sort, tt=self.ts_tooltips,
normal=(unicode(self.title_sort.text()) == ts))
def title_sort_box_changed(self, txt):
ts = unicode(txt)
self.mark_box_as_ok(control = self.title_sort, tt=self.ts_tooltips,
normal=(title_sort(unicode(self.title.text())) == ts))
def authors_box_changed(self, txt): def authors_box_changed(self, txt):
aus = unicode(txt) aus = unicode(txt)
aus = re.sub(r'\s+et al\.$', '', aus) aus = re.sub(r'\s+et al\.$', '', aus)
aus = self.db.author_sort_from_authors(string_to_authors(aus)) aus = self.db.author_sort_from_authors(string_to_authors(aus))
self.mark_author_sort(normal=(unicode(self.author_sort.text()) == aus)) self.mark_box_as_ok(control = self.author_sort, tt=self.aus_tooltips,
normal=(unicode(self.author_sort.text()) == aus))
def author_sort_box_changed(self, txt): def author_sort_box_changed(self, txt):
au = unicode(self.authors.text()) au = unicode(self.authors.text())
au = re.sub(r'\s+et al\.$', '', au) au = re.sub(r'\s+et al\.$', '', au)
au = self.db.author_sort_from_authors(string_to_authors(au)) au = self.db.author_sort_from_authors(string_to_authors(au))
self.mark_author_sort(normal=(au == txt)) self.mark_box_as_ok(control = self.author_sort, tt=self.aus_tooltips,
normal=(au == txt))
def mark_author_sort(self, normal=True): def mark_box_as_ok(self, control, tt, normal=True):
if normal: if normal:
col = 'rgb(0, 255, 0, 20%)' col = 'rgb(0, 255, 0, 20%)'
else: else:
col = 'rgb(255, 0, 0, 20%)' col = 'rgb(255, 0, 0, 20%)'
self.author_sort.setStyleSheet('QLineEdit { color: black; ' control.setStyleSheet('QLineEdit { color: black; '
'background-color: %s; }'%col) 'background-color: %s; }'%col)
tt = self.ok_aus_tooltip if normal else self.bad_aus_tooltip tt = tt[0] if normal else tt[1]
self.author_sort.setToolTip(tt) control.setToolTip(tt)
def validate_isbn(self, isbn): def validate_isbn(self, isbn):
isbn = unicode(isbn).strip() isbn = unicode(isbn).strip()
@ -652,12 +686,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
authors = string_to_authors(au) authors = string_to_authors(au)
self.author_sort.setText(self.db.author_sort_from_authors(authors)) self.author_sort.setText(self.db.author_sort_from_authors(authors))
def deduce_title_sort(self):
ts = unicode(self.title.text())
self.title_sort.setText(title_sort(ts))
def swap_title_author(self): def swap_title_author(self):
title = self.title.text() title = self.title.text()
self.title.setText(self.authors.text()) self.title.setText(self.authors.text())
self.authors.setText(title) self.authors.setText(title)
self.author_sort.setText('') self.deduce_author_sort()
self.deduce_title_sort()
def initialize_combos(self): def initialize_combos(self):
self.initalize_authors() self.initalize_authors()
@ -771,10 +809,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.pubdate.setDate(QDate(dt.year, dt.month, dt.day)) self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
summ = book.comments summ = book.comments
if summ: if summ:
prefix = unicode(self.comments.toPlainText()) prefix = self.comments.html
if prefix: if prefix:
prefix += '\n' prefix += '\n'
self.comments.setPlainText(prefix + summ) self.comments.html = prefix + comments_to_html(summ)
if book.rating is not None: if book.rating is not None:
self.rating.setValue(int(book.rating)) self.rating.setValue(int(book.rating))
if book.tags: if book.tags:
@ -804,7 +842,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
series = unicode(self.series.text()).strip() series = unicode(self.series.text()).strip()
if series and series != self.original_series_name: if series and series != self.original_series_name:
ns = 1 ns = 1
if tweaks['series_index_auto_increment'] == 'next': if tweaks['series_index_auto_increment'] != 'const':
ns = self.db.get_next_series_num_for(series) ns = self.db.get_next_series_num_for(series)
self.series_index.setValue(ns) self.series_index.setValue(ns)
self.original_series_name = series self.original_series_name = series
@ -838,6 +876,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
title = unicode(self.title.text()).strip() title = unicode(self.title.text()).strip()
if title != self.original_title: if title != self.original_title:
self.db.set_title(self.id, title, notify=False) self.db.set_title(self.id, title, notify=False)
# This must be after setting the title because of the DB update trigger
ts = unicode(self.title_sort.text()).strip()
if ts:
self.db.set_title_sort(self.id, ts, notify=False, commit=False)
au = unicode(self.authors.text()).strip() au = unicode(self.authors.text()).strip()
if au and au != self.original_author: if au and au != self.original_author:
self.db.set_authors(self.id, string_to_authors(au), notify=False) self.db.set_authors(self.id, string_to_authors(au), notify=False)
@ -860,7 +902,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.db.set_series_index(self.id, self.series_index.value(), self.db.set_series_index(self.id, self.series_index.value(),
notify=False, commit=False) notify=False, commit=False)
self.db.set_comment(self.id, self.db.set_comment(self.id,
unicode(self.comments.toPlainText()).strip(), self.comments.html,
notify=False, commit=False) notify=False, commit=False)
d = self.pubdate.date() d = self.pubdate.date()
d = qt_to_dt(d) d = qt_to_dt(d)
@ -897,16 +939,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
QDialog.reject(self, *args) QDialog.reject(self, *args)
def read_state(self): def read_state(self):
wg = dynamic.get('metasingle_window_geometry', None) wg = dynamic.get('metasingle_window_geometry2', None)
ss = dynamic.get('metasingle_splitter_state', None) ss = dynamic.get('metasingle_splitter_state2', None)
if wg is not None: if wg is not None:
self.restoreGeometry(wg) self.restoreGeometry(wg)
if ss is not None: if ss is not None:
self.splitter.restoreState(ss) self.splitter.restoreState(ss)
def save_state(self): def save_state(self):
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry())) dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry()))
dynamic.set('metasingle_splitter_state', dynamic.set('metasingle_splitter_state2',
bytes(self.splitter.saveState())) bytes(self.splitter.saveState()))
def break_cycles(self): def break_cycles(self):

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>887</width> <width>994</width>
<height>750</height> <height>716</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -43,8 +43,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>879</width> <width>986</width>
<height>711</height> <height>677</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_5"> <layout class="QVBoxLayout" name="verticalLayout_5">
@ -66,8 +66,8 @@
<attribute name="title"> <attribute name="title">
<string>&amp;Basic metadata</string> <string>&amp;Basic metadata</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QGridLayout" name="gridLayout_5">
<item> <item row="0" column="0">
<widget class="QSplitter" name="splitter"> <widget class="QSplitter" name="splitter">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -100,27 +100,27 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2" rowspan="2"> <item row="1" column="0">
<widget class="QToolButton" name="swap_button"> <widget class="QLabel" name="label">
<property name="toolTip">
<string>Swap the author and title</string>
</property>
<property name="text"> <property name="text">
<string>...</string> <string>Title &amp;sort: </string>
</property> </property>
<property name="icon"> <property name="alignment">
<iconset resource="../../../../resources/images.qrc"> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<normaloff>:/images/swap.png</normaloff>:/images/swap.png</iconset>
</property> </property>
<property name="iconSize"> <property name="buddy">
<size> <cstring>title_sort</cstring>
<width>16</width>
<height>16</height>
</size>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="1">
<widget class="EnLineEdit" name="title_sort">
<property name="toolTip">
<string>Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>&amp;Author(s): </string> <string>&amp;Author(s): </string>
@ -133,7 +133,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="1">
<widget class="EnComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <property name="text">
<string>Author S&amp;ort: </string> <string>Author S&amp;ort: </string>
@ -146,34 +153,15 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1" colspan="2"> <item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout"> <widget class="EnLineEdit" name="author_sort">
<item> <property name="toolTip">
<widget class="EnLineEdit" name="author_sort"> <string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
<property name="toolTip">
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.</string> If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.</string>
</property> </property>
</widget> </widget>
</item>
<item>
<widget class="QToolButton" name="auto_author_sort">
<property name="toolTip">
<string>Automatically create the author sort entry based on the current author entry.
Using this button to create author sort will change author sort from red to green.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/auto_author_sort.png</normaloff>:/images/auto_author_sort.png</iconset>
</property>
</widget>
</item>
</layout>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>&amp;Rating:</string> <string>&amp;Rating:</string>
@ -186,7 +174,7 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" colspan="2"> <item row="4" column="1" colspan="2">
<widget class="QSpinBox" name="rating"> <widget class="QSpinBox" name="rating">
<property name="toolTip"> <property name="toolTip">
<string>Rating of this book. 0-5 stars</string> <string>Rating of this book. 0-5 stars</string>
@ -205,7 +193,7 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>&amp;Publisher: </string> <string>&amp;Publisher: </string>
@ -218,7 +206,14 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="5" column="1" colspan="2">
<widget class="EnComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>Ta&amp;gs: </string> <string>Ta&amp;gs: </string>
@ -231,32 +226,7 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1" colspan="2"> <item row="7" column="0">
<layout class="QHBoxLayout" name="_2">
<item>
<widget class="TagsLineEdit" name="tags">
<property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="tag_editor_button">
<property name="toolTip">
<string>Open Tag Editor</string>
</property>
<property name="text">
<string>Open Tag Editor</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>&amp;Series:</string> <string>&amp;Series:</string>
@ -272,7 +242,7 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1" colspan="2"> <item row="7" column="1">
<layout class="QHBoxLayout" name="_3"> <layout class="QHBoxLayout" name="_3">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
@ -293,59 +263,9 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QToolButton" name="remove_series_button">
<property name="toolTip">
<string>Remove unused series (Series that have no books)</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item row="8" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>IS&amp;BN:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>isbn</cstring>
</property>
</widget>
</item>
<item row="8" column="1" colspan="2"> <item row="8" column="1" colspan="2">
<widget class="QLineEdit" name="isbn"/>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Publishe&amp;d:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>pubdate</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="EnComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="series_index"> <widget class="QDoubleSpinBox" name="series_index">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
@ -358,34 +278,23 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1" colspan="2"> <item row="9" column="0">
<widget class="QDateEdit" name="pubdate"> <widget class="QLabel" name="label_9">
<property name="displayFormat"> <property name="text">
<string>MMM yyyy</string> <string>IS&amp;BN:</string>
</property> </property>
<property name="calendarPopup"> <property name="alignment">
<bool>true</bool> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
</widget> <property name="buddy">
</item> <cstring>isbn</cstring>
<item row="1" column="1">
<widget class="EnComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1" colspan="2"> <item row="9" column="1" colspan="2">
<widget class="QDateEdit" name="date"> <widget class="QLineEdit" name="isbn"/>
<property name="displayFormat">
<string>dd MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item> </item>
<item row="9" column="0"> <item row="10" column="0">
<widget class="QLabel" name="label_11"> <widget class="QLabel" name="label_11">
<property name="text"> <property name="text">
<string>&amp;Date:</string> <string>&amp;Date:</string>
@ -398,22 +307,181 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
</layout> <item row="10" column="1" colspan="2">
</widget> <widget class="QDateEdit" name="date">
</item> <property name="displayFormat">
<item> <string>dd MMM yyyy</string>
<widget class="QGroupBox" name="groupBox_2"> </property>
<property name="title"> <property name="calendarPopup">
<string>&amp;Comments</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QTextEdit" name="comments">
<property name="tabChangesFocus">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="acceptRichText"> </widget>
<bool>false</bool> </item>
<item row="11" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Publishe&amp;d:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>pubdate</cstring>
</property>
</widget>
</item>
<item row="11" column="1" colspan="2">
<widget class="QDateEdit" name="pubdate">
<property name="displayFormat">
<string>MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2" rowspan="4">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="auto_title_sort">
<property name="toolTip">
<string>Automatically create the title sort entry based on the current title entry.
Using this button to create title sort will change title sort from red to green.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/auto_author_sort.png</normaloff>:/images/auto_author_sort.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="swap_button">
<property name="toolTip">
<string>Swap the author and title</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/swap.png</normaloff>:/images/swap.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="auto_author_sort">
<property name="toolTip">
<string>Automatically create the author sort entry based on the current author entry.
Using this button to create author sort will change author sort from red to green.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/auto_author_sort.png</normaloff>:/images/auto_author_sort.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="_2">
<item>
<widget class="TagsLineEdit" name="tags">
<property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="2">
<widget class="QToolButton" name="tag_editor_button">
<property name="toolTip">
<string>Open Tag Editor</string>
</property>
<property name="text">
<string>Open Tag Editor</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QToolButton" name="remove_series_button">
<property name="toolTip">
<string>Remove unused series (Series that have no books)</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property> </property>
</widget> </widget>
</item> </item>
@ -427,10 +495,148 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="layoutWidget_2"> <widget class="QWidget" name="layoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="bc_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Book Cover</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Change &amp;cover image:</string>
</property>
<property name="buddy">
<cstring>cover_button</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="cover_button">
<property name="text">
<string>&amp;Browse</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="trim_cover_button">
<property name="toolTip">
<string>Remove border (if any) from cover</string>
</property>
<property name="text">
<string>T&amp;rim</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reset_cover">
<property name="toolTip">
<string>Reset cover to default</string>
</property>
<property name="text">
<string>&amp;Remove</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="_6">
<item>
<widget class="QPushButton" name="fetch_cover_button">
<property name="text">
<string>Download co&amp;ver</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item> <item>
<widget class="QGroupBox" name="af_group_box"> <widget class="QGroupBox" name="af_group_box">
<property name="sizePolicy"> <property name="sizePolicy">
@ -459,6 +665,12 @@ Using this button to create author sort will change author sort from red to gree
<height>140</height> <height>140</height>
</size> </size>
</property> </property>
<property name="baseSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="dragDropMode"> <property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum> <enum>QAbstractItemView::DropOnly</enum>
</property> </property>
@ -557,129 +769,22 @@ Using this button to create author sort will change author sort from red to gree
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="bc_box"> <widget class="QGroupBox" name="groupBox">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>10</verstretch> <verstretch>10</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="title"> <property name="title">
<string>Book Cover</string> <string>&amp;Comments</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QVBoxLayout" name="verticalLayout_8">
<property name="margin">
<number>0</number>
</property>
<item> <item>
<widget class="ImageView" name="cover" native="true"> <widget class="Editor" name="comments" native="true"/>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Change &amp;cover image:</string>
</property>
<property name="buddy">
<cstring>cover_path</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="cover_path">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cover_button">
<property name="text">
<string>&amp;Browse</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="trim_cover_button">
<property name="toolTip">
<string>Remove border (if any) from cover</string>
</property>
<property name="text">
<string>T&amp;rim</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="reset_cover">
<property name="toolTip">
<string>Reset cover to default</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="_6">
<item>
<widget class="QPushButton" name="fetch_cover_button">
<property name="text">
<string>Download co&amp;ver</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -741,13 +846,21 @@ Using this button to create author sort will change author sort from red to gree
<header>calibre/gui2/widgets.h</header> <header>calibre/gui2/widgets.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>Editor</class>
<extends>QWidget</extends>
<header location="global">calibre/gui2/comments_editor.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>title</tabstop> <tabstop>title</tabstop>
<tabstop>auto_title_sort</tabstop>
<tabstop>title_sort</tabstop>
<tabstop>swap_button</tabstop> <tabstop>swap_button</tabstop>
<tabstop>authors</tabstop> <tabstop>authors</tabstop>
<tabstop>author_sort</tabstop>
<tabstop>auto_author_sort</tabstop> <tabstop>auto_author_sort</tabstop>
<tabstop>author_sort</tabstop>
<tabstop>rating</tabstop> <tabstop>rating</tabstop>
<tabstop>publisher</tabstop> <tabstop>publisher</tabstop>
<tabstop>tags</tabstop> <tabstop>tags</tabstop>
@ -758,20 +871,20 @@ Using this button to create author sort will change author sort from red to gree
<tabstop>isbn</tabstop> <tabstop>isbn</tabstop>
<tabstop>date</tabstop> <tabstop>date</tabstop>
<tabstop>pubdate</tabstop> <tabstop>pubdate</tabstop>
<tabstop>comments</tabstop>
<tabstop>fetch_metadata_button</tabstop> <tabstop>fetch_metadata_button</tabstop>
<tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop>
<tabstop>button_set_cover</tabstop> <tabstop>button_set_cover</tabstop>
<tabstop>button_set_metadata</tabstop> <tabstop>button_set_metadata</tabstop>
<tabstop>formats</tabstop> <tabstop>formats</tabstop>
<tabstop>cover_path</tabstop> <tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop>
<tabstop>cover_button</tabstop>
<tabstop>trim_cover_button</tabstop>
<tabstop>reset_cover</tabstop> <tabstop>reset_cover</tabstop>
<tabstop>fetch_cover_button</tabstop> <tabstop>fetch_cover_button</tabstop>
<tabstop>generate_cover_button</tabstop> <tabstop>generate_cover_button</tabstop>
<tabstop>button_box</tabstop>
<tabstop>scrollArea</tabstop> <tabstop>scrollArea</tabstop>
<tabstop>central_widget</tabstop> <tabstop>central_widget</tabstop>
<tabstop>button_box</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>

View File

@ -12,7 +12,7 @@ from operator import attrgetter
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
QModelIndex, QVariant, QDate QModelIndex, QVariant, QDate
from calibre.gui2 import NONE, config, UNDEFINED_QDATE, FunctionDispatcher from calibre.gui2 import NONE, config, UNDEFINED_QDATE
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
@ -22,8 +22,7 @@ from calibre.utils.icu import sort_key, strcmp as icu_strcmp
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH, CoverCache, MetadataBackup REGEXP_MATCH, MetadataBackup
from calibre.library.cli import parse_series_string
from calibre import strftime, isbytestring, prepare_string_for_xml from calibre import strftime, isbytestring, prepare_string_for_xml
from calibre.constants import filesystem_encoding, DEBUG from calibre.constants import filesystem_encoding, DEBUG
from calibre.gui2.library import DEFAULT_SORT from calibre.gui2.library import DEFAULT_SORT
@ -89,7 +88,6 @@ class BooksModel(QAbstractTableModel): # {{{
self.headers = {} self.headers = {}
self.alignment_map = {} self.alignment_map = {}
self.buffer_size = buffer self.buffer_size = buffer
self.cover_cache = None
self.metadata_backup = None self.metadata_backup = None
self.bool_yes_icon = QIcon(I('ok.png')) self.bool_yes_icon = QIcon(I('ok.png'))
self.bool_no_icon = QIcon(I('list_remove.png')) self.bool_no_icon = QIcon(I('list_remove.png'))
@ -113,10 +111,6 @@ class BooksModel(QAbstractTableModel): # {{{
def is_custom_column(self, cc_label): def is_custom_column(self, cc_label):
return cc_label in self.custom_columns return cc_label in self.custom_columns
def clear_caches(self):
if self.cover_cache:
self.cover_cache.clear_cache()
def read_config(self): def read_config(self):
self.use_roman_numbers = config['use_roman_numerals_for_series_number'] self.use_roman_numbers = config['use_roman_numerals_for_series_number']
@ -154,18 +148,8 @@ class BooksModel(QAbstractTableModel): # {{{
self.build_data_convertors() self.build_data_convertors()
self.reset() self.reset()
self.database_changed.emit(db) self.database_changed.emit(db)
if self.cover_cache is not None:
self.cover_cache.stop()
# Would like to to a join here, but the thread might be waiting to
# do something on the GUI thread. Deadlock.
self.cover_cache = CoverCache(db, FunctionDispatcher(self.db.cover))
self.cover_cache.start()
self.stop_metadata_backup() self.stop_metadata_backup()
self.start_metadata_backup() self.start_metadata_backup()
def refresh_cover(event, ids):
if event == 'cover' and self.cover_cache is not None:
self.cover_cache.refresh(ids)
db.add_listener(refresh_cover)
def start_metadata_backup(self): def start_metadata_backup(self):
self.metadata_backup = MetadataBackup(self.db) self.metadata_backup = MetadataBackup(self.db)
@ -225,7 +209,6 @@ class BooksModel(QAbstractTableModel): # {{{
def books_deleted(self): def books_deleted(self):
self.count_changed() self.count_changed()
self.clear_caches()
self.reset() self.reset()
def delete_books(self, indices): def delete_books(self, indices):
@ -254,7 +237,6 @@ class BooksModel(QAbstractTableModel): # {{{
return return
self.last_search = text self.last_search = text
if reset: if reset:
self.clear_caches()
self.reset() self.reset()
if self.last_search: if self.last_search:
# Do not issue search done for the null search. It is used to clear # Do not issue search done for the null search. It is used to clear
@ -269,7 +251,6 @@ class BooksModel(QAbstractTableModel): # {{{
label = self.column_map[col] label = self.column_map[col]
self.db.sort(label, ascending) self.db.sort(label, ascending)
if reset: if reset:
self.clear_caches()
self.reset() self.reset()
self.sorted_on = (label, order) self.sorted_on = (label, order)
self.sort_history.insert(0, self.sorted_on) self.sort_history.insert(0, self.sorted_on)
@ -357,26 +338,10 @@ class BooksModel(QAbstractTableModel): # {{{
data[name] = val data[name] = val
return data return data
def set_cache(self, idx):
l, r = 0, self.count()-1
if self.cover_cache is not None:
l = max(l, idx-self.buffer_size)
r = min(r, idx+self.buffer_size)
k = min(r-idx, idx-l)
ids = [idx]
for i in range(1, k):
ids.extend([idx-i, idx+i])
ids = ids + [i for i in range(l, r, 1) if i not in ids]
try:
ids = [self.db.id(i) for i in ids]
except IndexError:
return
self.cover_cache.set_cache(ids)
def current_changed(self, current, previous, emit_signal=True): def current_changed(self, current, previous, emit_signal=True):
if current.isValid(): if current.isValid():
idx = current.row() idx = current.row()
self.set_cache(idx)
data = self.get_book_display_info(idx) data = self.get_book_display_info(idx)
if emit_signal: if emit_signal:
self.new_bookdisplay_data.emit(data) self.new_bookdisplay_data.emit(data)
@ -533,13 +498,7 @@ class BooksModel(QAbstractTableModel): # {{{
def cover(self, row_number): def cover(self, row_number):
data = None data = None
try: try:
id = self.db.id(row_number) data = self.db.cover(row_number)
if self.cover_cache is not None:
img = self.cover_cache.cover(id)
if not img.isNull():
return img
if not data:
data = self.db.cover(row_number)
except IndexError: # Happens if database has not yet been refreshed except IndexError: # Happens if database has not yet been refreshed
pass pass
@ -765,9 +724,7 @@ class BooksModel(QAbstractTableModel): # {{{
return False return False
val = qt_to_dt(val, as_utc=False) val = qt_to_dt(val, as_utc=False)
elif typ == 'series': elif typ == 'series':
val, s_index = parse_series_string(self.db, label, value.toString()) val = unicode(value.toString()).strip()
if not val:
val = s_index = None
elif typ == 'composite': elif typ == 'composite':
tmpl = unicode(value.toString()).strip() tmpl = unicode(value.toString()).strip()
disp = cc['display'] disp = cc['display']
@ -812,7 +769,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.db.set_series_index(id, float(match.group(1))) self.db.set_series_index(id, float(match.group(1)))
val = pat.sub('', val).strip() val = pat.sub('', val).strip()
elif val: elif val:
if tweaks['series_index_auto_increment'] == 'next': if tweaks['series_index_auto_increment'] != 'const':
ni = self.db.get_next_series_num_for(val) ni = self.db.get_next_series_num_for(val)
if ni != 1: if ni != 1:
self.db.set_series_index(id, ni) self.db.set_series_index(id, ni)

View File

@ -123,8 +123,8 @@ class BooksView(QTableView): # {{{
elif action == 'show': elif action == 'show':
h.setSectionHidden(idx, False) h.setSectionHidden(idx, False)
if h.sectionSize(idx) < 3: if h.sectionSize(idx) < 3:
sz = h.sectionSizeHint(idx) sz = h.sectionSizeHint(idx)
h.resizeSection(idx, sz) h.resizeSection(idx, sz)
elif action == 'ascending': elif action == 'ascending':
self.sortByColumn(idx, Qt.AscendingOrder) self.sortByColumn(idx, Qt.AscendingOrder)
elif action == 'descending': elif action == 'descending':

View File

@ -135,9 +135,10 @@ class GuiRunner(QObject):
'''Make sure an event loop is running before starting the main work of '''Make sure an event loop is running before starting the main work of
initialization''' initialization'''
def __init__(self, opts, args, actions, listener, app): def __init__(self, opts, args, actions, listener, app, gui_debug=None):
self.startup_time = time.time() self.startup_time = time.time()
self.opts, self.args, self.listener, self.app = opts, args, listener, app self.opts, self.args, self.listener, self.app = opts, args, listener, app
self.gui_debug = gui_debug
self.actions = actions self.actions = actions
self.main = None self.main = None
QObject.__init__(self) QObject.__init__(self)
@ -148,7 +149,7 @@ class GuiRunner(QObject):
def start_gui(self): def start_gui(self):
from calibre.gui2.ui import Main from calibre.gui2.ui import Main
main = Main(self.opts) main = Main(self.opts, gui_debug=self.gui_debug)
if self.splash_screen is not None: if self.splash_screen is not None:
self.splash_screen.showMessage(_('Initializing user interface...')) self.splash_screen.showMessage(_('Initializing user interface...'))
self.splash_screen.finish(main) self.splash_screen.finish(main)
@ -249,34 +250,69 @@ class GuiRunner(QObject):
self.initialize_db() self.initialize_db()
def run_in_debug_mode(logpath=None):
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
import tempfile, subprocess
fd, logpath = tempfile.mkstemp('.txt')
os.close(fd)
if hasattr(sys, 'frameworks_dir'):
base = os.path.dirname(sys.frameworks_dir)
if 'console.app' not in base:
base = os.path.join(base, 'console.app', 'Contents')
exe = os.path.basename(e)
exe = os.path.join(base, 'MacOS', exe+'-debug')
else:
base, ext = os.path.splitext(e)
exe = base + '-debug' + ext
print 'Starting debug executable:', exe
creationflags = 0
if iswindows:
import win32process
creationflags = win32process.CREATE_NO_WINDOW
subprocess.Popen([exe, '--gui-debug', logpath], stdout=open(logpath, 'w'),
stderr=subprocess.STDOUT, stdin=open(os.devnull, 'r'),
creationflags=creationflags)
def run_gui(opts, args, actions, listener, app): def run_gui(opts, args, actions, listener, app, gui_debug=None):
initialize_file_icon_provider() initialize_file_icon_provider()
if not dynamic.get('welcome_wizard_was_run', False): if not dynamic.get('welcome_wizard_was_run', False):
from calibre.gui2.wizard import wizard from calibre.gui2.wizard import wizard
wizard().exec_() wizard().exec_()
dynamic.set('welcome_wizard_was_run', True) dynamic.set('welcome_wizard_was_run', True)
runner = GuiRunner(opts, args, actions, listener, app) runner = GuiRunner(opts, args, actions, listener, app, gui_debug=gui_debug)
ret = app.exec_() ret = app.exec_()
if getattr(runner.main, 'run_wizard_b4_shutdown', False): if getattr(runner.main, 'run_wizard_b4_shutdown', False):
from calibre.gui2.wizard import wizard from calibre.gui2.wizard import wizard
wizard().exec_() wizard().exec_()
if getattr(runner.main, 'restart_after_quit', False): if getattr(runner.main, 'restart_after_quit', False):
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0] e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
print 'Restarting with:', e, sys.argv if getattr(runner.main, 'debug_on_restart', False):
if hasattr(sys, 'frameworks_dir'): run_in_debug_mode()
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
import subprocess
subprocess.Popen('sleep 3s; open '+app, shell=True)
else: else:
os.execvp(e, sys.argv) print 'Restarting with:', e, sys.argv
if hasattr(sys, 'frameworks_dir'):
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
import subprocess
subprocess.Popen('sleep 3s; open '+app, shell=True)
else:
os.execvp(e, sys.argv)
else: else:
if iswindows: if iswindows:
try: try:
runner.main.system_tray_icon.hide() runner.main.system_tray_icon.hide()
except: except:
pass pass
if runner.main.gui_debug is not None:
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
import subprocess
creationflags = 0
if iswindows:
import win32process
creationflags = win32process.CREATE_NO_WINDOW
subprocess.Popen([e, '--show-gui-debug', runner.main.gui_debug],
creationflags=creationflags, stdout=open(os.devnull, 'w'),
stderr=subprocess.PIPE, stdin=open(os.devnull, 'r'))
return ret return ret
def cant_start(msg=_('If you are sure it is not running')+', ', def cant_start(msg=_('If you are sure it is not running')+', ',
@ -317,6 +353,11 @@ def communicate(args):
def main(args=sys.argv): def main(args=sys.argv):
gui_debug = None
if args[0] == '__CALIBRE_GUI_DEBUG__':
gui_debug = args[1]
args = ['calibre']
app, opts, args, actions = init_qt(args) app, opts, args, actions = init_qt(args)
from calibre.utils.lock import singleinstance from calibre.utils.lock import singleinstance
from multiprocessing.connection import Listener from multiprocessing.connection import Listener
@ -333,9 +374,11 @@ def main(args=sys.argv):
except socket.error: except socket.error:
cant_start() cant_start()
else: else:
return run_gui(opts, args, actions, listener, app) return run_gui(opts, args, actions, listener, app,
gui_debug=gui_debug)
else: else:
return run_gui(opts, args, actions, listener, app) return run_gui(opts, args, actions, listener, app,
gui_debug=gui_debug)
otherinstance = False otherinstance = False
try: try:
listener = Listener(address=ADDRESS) listener = Listener(address=ADDRESS)
@ -345,8 +388,7 @@ def main(args=sys.argv):
# On windows only singleinstance can be trusted # On windows only singleinstance can be trusted
otherinstance = True if iswindows else False otherinstance = True if iswindows else False
if not otherinstance: if not otherinstance:
sys.setcheckinterval(50) # Make GUI more responsive return run_gui(opts, args, actions, listener, app, gui_debug=gui_debug)
return run_gui(opts, args, actions, listener, app)
communicate(args) communicate(args)

View File

@ -206,17 +206,23 @@ class SearchBox2(QComboBox): # {{{
self.line_edit.blockSignals(yes) self.line_edit.blockSignals(yes)
def set_search_string(self, txt, store_in_history=False, emit_changed=True): def set_search_string(self, txt, store_in_history=False, emit_changed=True):
self.setFocus(Qt.OtherFocusReason) if not store_in_history:
if not txt: self.activated.disconnect()
self.clear() try:
else: self.setFocus(Qt.OtherFocusReason)
self.normalize_state() if not txt:
self.setEditText(txt) self.clear()
self.line_edit.end(False) else:
if emit_changed: self.normalize_state()
self.changed.emit() self.setEditText(txt)
self._do_search(store_in_history=store_in_history) self.line_edit.end(False)
self.focus_to_library.emit() if emit_changed:
self.changed.emit()
self._do_search(store_in_history=store_in_history)
self.focus_to_library.emit()
finally:
if not store_in_history:
self.activated.connect(self.history_selected)
def search_as_you_type(self, enabled): def search_as_you_type(self, enabled):
self.as_you_type = enabled self.as_you_type = enabled

View File

@ -18,6 +18,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
@ -409,17 +410,31 @@ class TagTreeItem(object): # {{{
return NONE return NONE
def tag_data(self, role): def tag_data(self, role):
tag = self.tag
if tag.category == 'authors' and \
tweaks['categories_use_field_for_author_name'] == 'author_sort':
name = tag.sort
tt_author = True
else:
name = tag.name
tt_author = False
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
if self.tag.count == 0: if tag.count == 0:
return QVariant('%s'%(self.tag.name)) return QVariant('%s'%(name))
else: else:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) return QVariant('[%d] %s'%(tag.count, name))
if role == Qt.EditRole: if role == Qt.EditRole:
return QVariant(self.tag.name) return QVariant(tag.name)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icon_state_map[self.tag.state] return self.icon_state_map[tag.state]
if role == Qt.ToolTipRole and self.tag.tooltip is not None: if role == Qt.ToolTipRole:
return QVariant(self.tag.tooltip) if tt_author:
if tag.tooltip is not None:
return QVariant('(%s) %s'%(tag.name, tag.tooltip))
else:
return QVariant(tag.name)
if tag.tooltip is not None:
return QVariant(tag.tooltip)
return NONE return NONE
def toggle(self): def toggle(self):
@ -680,8 +695,10 @@ class TagsModel(QAbstractItemModel): # {{{
def setData(self, index, value, role=Qt.EditRole): def setData(self, index, value, role=Qt.EditRole):
if not index.isValid(): if not index.isValid():
return NONE return NONE
# set up to position at the category label # set up to reposition at the same item. We can do this except if
path = self.path_for_index(self.parent(index)) # working with the last item and that item is deleted, in which case
# we position at the parent label
path = index.model().path_for_index(index)
val = unicode(value.toString()) val = unicode(value.toString())
if not val: if not val:
error_dialog(self.tags_view, _('Item is blank'), error_dialog(self.tags_view, _('Item is blank'),
@ -932,18 +949,22 @@ class TagBrowserMixin(object): # {{{
for old_id in to_rename[text]: for old_id in to_rename[text]:
rename_func(old_id, new_name=unicode(text)) rename_func(old_id, new_name=unicode(text))
# Clean up everything, as information could have changed for many books. # Clean up the library view
self.library_view.model().refresh() self.do_tag_item_renamed()
self.tags_view.set_new_model() self.tags_view.set_new_model() # does a refresh for free
self.tags_view.recount()
self.saved_search.clear()
self.search.clear()
def do_tag_item_renamed(self): def do_tag_item_renamed(self):
# Clean up library view and search # Clean up library view and search
self.library_view.model().refresh() # get information to redo the selection
self.saved_search.clear() rows = [r.row() for r in \
self.search.clear() self.library_view.selectionModel().selectedRows()]
m = self.library_view.model()
ids = [m.id(r) for r in rows]
m.refresh(reset=False)
m.research()
self.library_view.select_rows(ids)
# refreshing the tags view happens at the emit()/call() site
def do_author_sort_edit(self, parent, id): def do_author_sort_edit(self, parent, id):
db = self.library_view.model().db db = self.library_view.model().db

View File

@ -96,10 +96,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
'The main GUI' 'The main GUI'
def __init__(self, opts, parent=None): def __init__(self, opts, parent=None, gui_debug=None):
MainWindow.__init__(self, opts, parent) MainWindow.__init__(self, opts, parent)
self.opts = opts self.opts = opts
self.device_connected = None self.device_connected = None
self.gui_debug = gui_debug
acmap = OrderedDict() acmap = OrderedDict()
for action in interface_actions(): for action in interface_actions():
ac = action.load_actual_plugin(self) ac = action.load_actual_plugin(self)
@ -261,6 +262,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
for ac in self.iactions.values(): for ac in self.iactions.values():
ac.initialization_complete() ac.initialization_complete()
if show_gui and self.gui_debug is not None:
info_dialog(self, _('Debug mode'), '<p>' +
_('You have started calibre in debug mode. After you '
'quit calibre, the debug log will be available in '
'the file: %s<p>The '
'log will be displayed automatically.')%self.gui_debug, show=True)
def start_content_server(self): def start_content_server(self):
from calibre.library.server.main import start_threaded_server from calibre.library.server.main import start_threaded_server
from calibre.library.server import server_config from calibre.library.server import server_config
@ -369,13 +378,16 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def booklists(self): def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def library_moved(self, newloc): def library_moved(self, newloc, copy_structure=False):
if newloc is None: return if newloc is None: return
default_prefs = None
try: try:
olddb = self.library_view.model().db olddb = self.library_view.model().db
if copy_structure:
default_prefs = olddb.prefs
except: except:
olddb = None olddb = None
db = LibraryDatabase2(newloc) db = LibraryDatabase2(newloc, default_prefs=default_prefs)
if self.content_server is not None: if self.content_server is not None:
self.content_server.set_database(db) self.content_server.set_database(db)
self.library_path = newloc self.library_path = newloc
@ -495,7 +507,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
dynamic.set('sort_history', self.library_view.model().sort_history) dynamic.set('sort_history', self.library_view.model().sort_history)
self.save_layout_state() self.save_layout_state()
def quit(self, checked=True, restart=False): def quit(self, checked=True, restart=False, debug_on_restart=False):
if not self.confirm_quit(): if not self.confirm_quit():
return return
try: try:
@ -503,6 +515,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except: except:
pass pass
self.restart_after_quit = restart self.restart_after_quit = restart
self.debug_on_restart = debug_on_restart
QApplication.instance().quit() QApplication.instance().quit()
def donate(self, *args): def donate(self, *args):
@ -583,9 +596,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
while self.spare_servers: while self.spare_servers:
self.spare_servers.pop().close() self.spare_servers.pop().close()
self.device_manager.keep_going = False self.device_manager.keep_going = False
cc = self.library_view.model().cover_cache
if cc is not None:
cc.stop()
mb = self.library_view.model().metadata_backup mb = self.library_view.model().metadata_backup
if mb is not None: if mb is not None:
mb.stop() mb.stop()

View File

@ -7,14 +7,14 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>479</width> <width>479</width>
<height>606</height> <height>591</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Configure Ebook viewer</string> <string>Configure Ebook viewer</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/config.png</normaloff>:/images/config.png</iconset> <normaloff>:/images/config.png</normaloff>:/images/config.png</iconset>
</property> </property>
<layout class="QGridLayout" name="gridLayout_4"> <layout class="QGridLayout" name="gridLayout_4">
@ -85,11 +85,7 @@
<item row="2" column="1"> <item row="2" column="1">
<widget class="QFontComboBox" name="mono_family"/> <widget class="QFontComboBox" name="mono_family"/>
</item> </item>
</layout> <item row="3" column="0">
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>&amp;Default font size:</string> <string>&amp;Default font size:</string>
@ -99,7 +95,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="3" column="1">
<widget class="QSpinBox" name="default_font_size"> <widget class="QSpinBox" name="default_font_size">
<property name="suffix"> <property name="suffix">
<string> px</string> <string> px</string>
@ -112,7 +108,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Monospace &amp;font size:</string> <string>Monospace &amp;font size:</string>
@ -122,7 +118,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="4" column="1">
<widget class="QSpinBox" name="mono_font_size"> <widget class="QSpinBox" name="mono_font_size">
<property name="suffix"> <property name="suffix">
<string> px</string> <string> px</string>
@ -135,7 +131,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>S&amp;tandard font:</string> <string>S&amp;tandard font:</string>
@ -145,7 +141,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="5" column="1">
<widget class="QComboBox" name="standard_font"> <widget class="QComboBox" name="standard_font">
<item> <item>
<property name="text"> <property name="text">
@ -164,91 +160,125 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_window_size">
<property name="text">
<string>Remember last used &amp;window size</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_current_page">
<property name="text">
<string>Remember the &amp;current page when quitting</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="max_view_width">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Maximum &amp;view width:</string>
</property>
<property name="buddy">
<cstring>max_view_width</cstring>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="hyphenate">
<property name="text">
<string>H&amp;yphenate (break line in the middle of large words)</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="hyphenate_default_lang">
<property name="toolTip">
<string>The default language to use for hyphenation rules. If the book does not specify a language, this will be used.</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Default &amp;language for hyphenation:</string>
</property>
<property name="buddy">
<cstring>hyphenate_default_lang</cstring>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_fit_images">
<property name="text">
<string>&amp;Resize images larger than the viewer window (needs restart)</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item row="3" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;User stylesheet</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="1" column="0">
<widget class="QPlainTextEdit" name="css"/>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_window_size">
<property name="text">
<string>Remember last used &amp;window size</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_current_page">
<property name="text">
<string>Remember the &amp;current page when quitting</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="hyphenate">
<property name="text">
<string>H&amp;yphenate (break line in the middle of large words)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="hyphenate_default_lang">
<property name="toolTip">
<string>The default language to use for hyphenation rules. If the book does not specify a language, this will be used.</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Default &amp;language for hyphenation:</string>
</property>
<property name="buddy">
<cstring>hyphenate_default_lang</cstring>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="opt_fit_images">
<property name="text">
<string>&amp;Resize images larger than the viewer window (needs restart)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Page flip &amp;duration:</string>
</property>
<property name="buddy">
<cstring>opt_page_flip_duration</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="opt_page_flip_duration">
<property name="specialValueText">
<string>disabled</string>
</property>
<property name="suffix">
<string> secs</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.100000000000000</double>
</property>
<property name="maximum">
<double>3.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>0.500000000000000</double>
</property>
</widget>
</item>
<item row="10" column="0" colspan="2">
<widget class="QCheckBox" name="opt_wheel_flips_pages">
<property name="text">
<string>Mouse &amp;wheel flips pages</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="max_view_width">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Maximum &amp;view width:</string>
</property>
<property name="buddy">
<cstring>max_view_width</cstring>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab_2"> <widget class="QWidget" name="tab_2">
@ -268,6 +298,29 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>User &amp;Stylesheet</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>&lt;p&gt;A CSS stylesheet that can be used to control the look and feel of books. For examples, click &lt;a href=&quot;http://www.mobileread.com/forums/showthread.php?t=51500&quot;&gt;here&lt;/a&gt;.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="css"/>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
</layout> </layout>
@ -276,12 +329,8 @@
<tabstop>serif_family</tabstop> <tabstop>serif_family</tabstop>
<tabstop>sans_family</tabstop> <tabstop>sans_family</tabstop>
<tabstop>mono_family</tabstop> <tabstop>mono_family</tabstop>
<tabstop>default_font_size</tabstop>
<tabstop>mono_font_size</tabstop>
<tabstop>standard_font</tabstop>
<tabstop>max_view_width</tabstop> <tabstop>max_view_width</tabstop>
<tabstop>opt_remember_window_size</tabstop> <tabstop>opt_remember_window_size</tabstop>
<tabstop>css</tabstop>
<tabstop>buttonBox</tabstop> <tabstop>buttonBox</tabstop>
</tabstops> </tabstops>
<resources> <resources>

View File

@ -3,8 +3,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
''' # Imports {{{
'''
import os, math, re, glob, sys import os, math, re, glob, sys
from base64 import b64encode from base64 import b64encode
from functools import partial from functools import partial
@ -18,11 +17,15 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from calibre.utils.config import Config, StringConfig from calibre.utils.config import Config, StringConfig
from calibre.utils.localization import get_language from calibre.utils.localization import get_language
from calibre.gui2.viewer.config_ui import Ui_Dialog from calibre.gui2.viewer.config_ui import Ui_Dialog
from calibre.gui2.viewer.flip import SlideFlip
from calibre.gui2.viewer.gestures import Gestures
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre import prints, guess_type from calibre import prints, guess_type
from calibre.gui2.viewer.keys import SHORTCUTS from calibre.gui2.viewer.keys import SHORTCUTS
# }}}
bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \ bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \
hyphenator = images = hyphen_pats = None hyphenator = images = hyphen_pats = None
@ -32,6 +35,7 @@ def load_builtin_fonts():
QFontDatabase.addApplicationFont(f) QFontDatabase.addApplicationFont(f)
return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono' return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono'
# Config {{{
def config(defaults=None): def config(defaults=None):
desc = _('Options to customize the ebook viewer') desc = _('Options to customize the ebook viewer')
if defaults is None: if defaults is None:
@ -52,6 +56,11 @@ def config(defaults=None):
help=_('Default language for hyphenation rules')) help=_('Default language for hyphenation rules'))
c.add_opt('remember_current_page', default=True, c.add_opt('remember_current_page', default=True,
help=_('Save the current position in the document, when quitting')) help=_('Save the current position in the document, when quitting'))
c.add_opt('wheel_flips_pages', default=False,
help=_('Have the mouse wheel turn pages'))
c.add_opt('page_flip_duration', default=0.5,
help=_('The time, in seconds, for the page flip animation. Default'
' is half a second.'))
fonts = c.add_group('FONTS', _('Font options')) fonts = c.add_group('FONTS', _('Font options'))
fonts('serif_family', default='Times New Roman' if iswindows else 'Liberation Serif', fonts('serif_family', default='Times New Roman' if iswindows else 'Liberation Serif',
@ -75,6 +84,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
opts = config().parse() opts = config().parse()
self.opt_remember_window_size.setChecked(opts.remember_window_size) self.opt_remember_window_size.setChecked(opts.remember_window_size)
self.opt_remember_current_page.setChecked(opts.remember_current_page) self.opt_remember_current_page.setChecked(opts.remember_current_page)
self.opt_wheel_flips_pages.setChecked(opts.wheel_flips_pages)
self.opt_page_flip_duration.setValue(opts.page_flip_duration)
self.serif_family.setCurrentFont(QFont(opts.serif_family)) self.serif_family.setCurrentFont(QFont(opts.serif_family))
self.sans_family.setCurrentFont(QFont(opts.sans_family)) self.sans_family.setCurrentFont(QFont(opts.sans_family))
self.mono_family.setCurrentFont(QFont(opts.mono_family)) self.mono_family.setCurrentFont(QFont(opts.mono_family))
@ -122,13 +133,16 @@ class ConfigDialog(QDialog, Ui_Dialog):
c.set('max_view_width', int(self.max_view_width.value())) c.set('max_view_width', int(self.max_view_width.value()))
c.set('hyphenate', self.hyphenate.isChecked()) c.set('hyphenate', self.hyphenate.isChecked())
c.set('remember_current_page', self.opt_remember_current_page.isChecked()) c.set('remember_current_page', self.opt_remember_current_page.isChecked())
c.set('wheel_flips_pages', self.opt_wheel_flips_pages.isChecked())
c.set('page_flip_duration', self.opt_page_flip_duration.value())
idx = self.hyphenate_default_lang.currentIndex() idx = self.hyphenate_default_lang.currentIndex()
c.set('hyphenate_default_lang', c.set('hyphenate_default_lang',
str(self.hyphenate_default_lang.itemData(idx).toString())) str(self.hyphenate_default_lang.itemData(idx).toString()))
return QDialog.accept(self, *args) return QDialog.accept(self, *args)
# }}}
class Document(QWebPage): class Document(QWebPage): # {{{
def set_font_settings(self): def set_font_settings(self):
opts = config().parse() opts = config().parse()
@ -178,6 +192,7 @@ class Document(QWebPage):
# Miscellaneous # Miscellaneous
settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True)
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
self.set_user_stylesheet() self.set_user_stylesheet()
self.misc_config() self.misc_config()
@ -197,6 +212,9 @@ class Document(QWebPage):
self.hyphenate = opts.hyphenate self.hyphenate = opts.hyphenate
self.hyphenate_default_lang = opts.hyphenate_default_lang self.hyphenate_default_lang = opts.hyphenate_default_lang
self.do_fit_images = opts.fit_images self.do_fit_images = opts.fit_images
self.page_flip_duration = opts.page_flip_duration
self.enable_page_flip = self.page_flip_duration > 0.1
self.wheel_flips_pages = opts.wheel_flips_pages
def fit_images(self): def fit_images(self):
if self.do_fit_images: if self.do_fit_images:
@ -435,7 +453,9 @@ class Document(QWebPage):
self.height+amount) self.height+amount)
self.setPreferredContentsSize(s) self.setPreferredContentsSize(s)
class EntityDeclarationProcessor(object): # }}}
class EntityDeclarationProcessor(object): # {{{
def __init__(self, html): def __init__(self, html):
self.declared_entities = {} self.declared_entities = {}
@ -446,13 +466,17 @@ class EntityDeclarationProcessor(object):
self.processed_html = html self.processed_html = html
for key, val in self.declared_entities.iteritems(): for key, val in self.declared_entities.iteritems():
self.processed_html = self.processed_html.replace('&%s;'%key, val) self.processed_html = self.processed_html.replace('&%s;'%key, val)
# }}}
class DocumentView(QWebView): class DocumentView(QWebView): # {{{
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern) DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
def __init__(self, *args): def __init__(self, *args):
QWebView.__init__(self, *args) QWebView.__init__(self, *args)
self.flipper = SlideFlip(self)
self.gestures = Gestures()
self.is_auto_repeat_event = False
self.debug_javascript = False self.debug_javascript = False
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
self.self_closing_pat = re.compile(r'<([a-z1-6]+)\s+([^>]+)/>', self.self_closing_pat = re.compile(r'<([a-z1-6]+)\s+([^>]+)/>',
@ -693,6 +717,13 @@ class DocumentView(QWebView):
self.manager.scrolled(self.document.scroll_fraction) self.manager.scrolled(self.document.scroll_fraction)
self.turn_off_internal_scrollbars() self.turn_off_internal_scrollbars()
if self.flipper.isVisible():
if self.flipper.running:
self.flipper.setVisible(False)
else:
self.flipper(self.current_page_image(),
duration=self.document.page_flip_duration)
def turn_off_internal_scrollbars(self): def turn_off_internal_scrollbars(self):
self.document.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.document.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
@ -708,12 +739,17 @@ class DocumentView(QWebView):
return False return False
return True return True
def find_next_blank_line(self, overlap): def current_page_image(self, overlap=-1):
if overlap < 0:
overlap = self.height()
img = QImage(self.width(), overlap, QImage.Format_ARGB32) img = QImage(self.width(), overlap, QImage.Format_ARGB32)
painter = QPainter(img) painter = QPainter(img)
# Render a region of width x overlap pixels atthe bottom of the current viewport
self.document.mainFrame().render(painter, QRegion(0, 0, self.width(), overlap)) self.document.mainFrame().render(painter, QRegion(0, 0, self.width(), overlap))
painter.end() painter.end()
return img
def find_next_blank_line(self, overlap):
img = self.current_page_image(overlap)
for i in range(overlap-1, -1, -1): for i in range(overlap-1, -1, -1):
if self.test_line(img, i): if self.test_line(img, i):
self.scroll_by(y=i, notify=False) self.scroll_by(y=i, notify=False)
@ -721,22 +757,42 @@ class DocumentView(QWebView):
self.scroll_by(y=overlap) self.scroll_by(y=overlap)
def previous_page(self): def previous_page(self):
if self.flipper.running and not self.is_auto_repeat_event:
return
if self.loading_url is not None:
return
epf = self.document.enable_page_flip and not self.is_auto_repeat_event
delta_y = self.document.window_height - 25 delta_y = self.document.window_height - 25
if self.document.at_top: if self.document.at_top:
if self.manager is not None: if self.manager is not None:
self.to_bottom = True self.to_bottom = True
self.manager.previous_document() if epf:
self.flipper.initialize(self.current_page_image(), False)
self.manager.previous_document()
else: else:
opos = self.document.ypos opos = self.document.ypos
upper_limit = opos - delta_y upper_limit = opos - delta_y
if upper_limit < 0: if upper_limit < 0:
upper_limit = 0 upper_limit = 0
if upper_limit < opos: if upper_limit < opos:
if epf:
self.flipper.initialize(self.current_page_image(),
forwards=False)
self.document.scroll_to(self.document.xpos, upper_limit) self.document.scroll_to(self.document.xpos, upper_limit)
if epf:
self.flipper(self.current_page_image(),
duration=self.document.page_flip_duration)
if self.manager is not None: if self.manager is not None:
self.manager.scrolled(self.scroll_fraction) self.manager.scrolled(self.scroll_fraction)
def next_page(self): def next_page(self):
if self.flipper.running and not self.is_auto_repeat_event:
return
if self.loading_url is not None:
return
epf = self.document.enable_page_flip and not self.is_auto_repeat_event
window_height = self.document.window_height window_height = self.document.window_height
document_height = self.document.height document_height = self.document.height
ddelta = document_height - window_height ddelta = document_height - window_height
@ -746,6 +802,8 @@ class DocumentView(QWebView):
delta_y = window_height - 25 delta_y = window_height - 25
if self.document.at_bottom or ddelta <= 0: if self.document.at_bottom or ddelta <= 0:
if self.manager is not None: if self.manager is not None:
if epf:
self.flipper.initialize(self.current_page_image())
self.manager.next_document() self.manager.next_document()
elif ddelta < 25: elif ddelta < 25:
self.scroll_by(y=ddelta) self.scroll_by(y=ddelta)
@ -758,6 +816,8 @@ class DocumentView(QWebView):
#print 'After set padding=0:', self.document.ypos #print 'After set padding=0:', self.document.ypos
if opos < oopos: if opos < oopos:
if self.manager is not None: if self.manager is not None:
if epf:
self.flipper.initialize(self.current_page_image())
self.manager.next_document() self.manager.next_document()
return return
lower_limit = opos + delta_y # Max value of top y co-ord after scrolling lower_limit = opos + delta_y # Max value of top y co-ord after scrolling
@ -766,10 +826,14 @@ class DocumentView(QWebView):
padding = lower_limit - max_y padding = lower_limit - max_y
if padding == window_height: if padding == window_height:
if self.manager is not None: if self.manager is not None:
if epf:
self.flipper.initialize(self.current_page_image())
self.manager.next_document() self.manager.next_document()
return return
#print 'Setting padding to:', lower_limit - max_y #print 'Setting padding to:', lower_limit - max_y
self.document.set_bottom_padding(lower_limit - max_y) self.document.set_bottom_padding(lower_limit - max_y)
if epf:
self.flipper.initialize(self.current_page_image())
#print 'Document height:', self.document.height #print 'Document height:', self.document.height
max_y = self.document.height - window_height max_y = self.document.height - window_height
lower_limit = min(max_y, lower_limit) lower_limit = min(max_y, lower_limit)
@ -780,6 +844,9 @@ class DocumentView(QWebView):
#print 'After scroll pos:', self.document.ypos #print 'After scroll pos:', self.document.ypos
self.find_next_blank_line(window_height - actually_scrolled) self.find_next_blank_line(window_height - actually_scrolled)
#print 'After blank line pos:', self.document.ypos #print 'After blank line pos:', self.document.ypos
if epf:
self.flipper(self.current_page_image(),
duration=self.document.page_flip_duration)
if self.manager is not None: if self.manager is not None:
self.manager.scrolled(self.scroll_fraction) self.manager.scrolled(self.scroll_fraction)
#print 'After all:', self.document.ypos #print 'After all:', self.document.ypos
@ -833,6 +900,10 @@ class DocumentView(QWebView):
def wheelEvent(self, event): def wheelEvent(self, event):
if event.delta() < -14: if event.delta() < -14:
if self.document.wheel_flips_pages:
self.next_page()
event.accept()
return
if self.document.at_bottom: if self.document.at_bottom:
self.scroll_by(y=15) # at_bottom can lie on windows self.scroll_by(y=15) # at_bottom can lie on windows
if self.manager is not None: if self.manager is not None:
@ -840,6 +911,11 @@ class DocumentView(QWebView):
event.accept() event.accept()
return return
elif event.delta() > 14: elif event.delta() > 14:
if self.document.wheel_flips_pages:
self.previous_page()
event.accept()
return
if self.document.at_top: if self.document.at_top:
if self.manager is not None: if self.manager is not None:
self.manager.previous_document() self.manager.previous_document()
@ -862,7 +938,11 @@ class DocumentView(QWebView):
key = self.shortcuts.get_match(event) key = self.shortcuts.get_match(event)
func = self.goto_location_actions.get(key, None) func = self.goto_location_actions.get(key, None)
if func is not None: if func is not None:
func() self.is_auto_repeat_event = event.isAutoRepeat()
try:
func()
finally:
self.is_auto_repeat_event = False
elif key == 'Down': elif key == 'Down':
self.scroll_by(y=15) self.scroll_by(y=15)
elif key == 'Up': elif key == 'Up':
@ -881,6 +961,29 @@ class DocumentView(QWebView):
self.manager.viewport_resized(self.scroll_fraction) self.manager.viewport_resized(self.scroll_fraction)
return ret return ret
def event(self, ev):
typ = ev.type()
if typ == ev.TouchBegin:
try:
self.gestures.start_gesture('touch', ev)
except:
import traceback
traceback.print_exc()
elif typ == ev.TouchEnd:
try:
gesture = self.gestures.end_gesture('touch', ev, self.rect())
except:
import traceback
traceback.print_exc()
if gesture is not None:
ev.accept()
if gesture == 'lineleft':
self.next_page()
elif gesture == 'lineright':
self.previous_page()
return True
return QWebView.event(self, ev)
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
opos = self.document.ypos opos = self.document.ypos
ret = QWebView.mouseReleaseEvent(self, ev) ret = QWebView.mouseReleaseEvent(self, ev)
@ -889,4 +992,5 @@ class DocumentView(QWebView):
self.manager.scrolled(self.scroll_fraction) self.manager.scrolled(self.scroll_fraction)
return ret return ret
# }}}

Some files were not shown because too many files have changed in this diff Show More