mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from custcol trunk
This commit is contained in:
commit
c4e58f6d0a
553
resources/images/devices/folder.svg
Normal file
553
resources/images/devices/folder.svg
Normal file
@ -0,0 +1,553 @@
|
||||
<?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"
|
||||
version="1.0"
|
||||
x="0.0000000"
|
||||
y="0.0000000"
|
||||
width="48.000000px"
|
||||
height="48.000000px"
|
||||
id="svg1"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.44"
|
||||
sodipodi:docname="folder.svg"
|
||||
sodipodi:docbase="/home/lapo/Icone/Crux/crux-icon-theme/scalable/places"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-daritaliare.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<metadata
|
||||
id="metadata162">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Folder</dc:title>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Lapo Calamandrei</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:date>2006-06-26</dc:date>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
|
||||
<dc:identifier />
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>folder</rdf:li>
|
||||
<rdf:li>directory</rdf:li>
|
||||
<rdf:li>storage</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/GPL/2.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://web.resource.org/cc/Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://web.resource.org/cc/Distribution" />
|
||||
<cc:requires
|
||||
rdf:resource="http://web.resource.org/cc/Notice" />
|
||||
<cc:permits
|
||||
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
|
||||
<cc:requires
|
||||
rdf:resource="http://web.resource.org/cc/ShareAlike" />
|
||||
<cc:requires
|
||||
rdf:resource="http://web.resource.org/cc/SourceCode" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666"
|
||||
borderopacity="1"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1041"
|
||||
inkscape:window-height="655"
|
||||
inkscape:cy="24.626698"
|
||||
inkscape:cx="45.136759"
|
||||
inkscape:zoom="8"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:window-x="504"
|
||||
inkscape:window-y="101"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:showpageshadow="false"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:object-paths="false"
|
||||
gridspacingx="0.5px"
|
||||
gridspacingy="0.5px"
|
||||
gridempspacing="2"
|
||||
inkscape:grid-points="false"
|
||||
showborder="true"
|
||||
borderlayer="true">
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="36.062446"
|
||||
id="guide1934" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="15.003922"
|
||||
id="guide1941" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="4.5"
|
||||
id="guide1943" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="44.503533"
|
||||
id="guide1945" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="43.125"
|
||||
id="guide1947" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="39"
|
||||
id="guide1949" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="19.003495"
|
||||
id="guide2919" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="0.97227183"
|
||||
id="guide2212" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="47.994873"
|
||||
id="guide2214" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="111"
|
||||
id="guide4328" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="65.75"
|
||||
id="guide3135" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="129.75"
|
||||
id="guide3137" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="190.75"
|
||||
id="guide3139" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="212.48559"
|
||||
id="guide3316" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="178.01413"
|
||||
id="guide3318" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs3">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4232">
|
||||
<stop
|
||||
style="stop-color:#ad7fa8;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4234" />
|
||||
<stop
|
||||
style="stop-color:#75507b;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4236" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3311">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3313" />
|
||||
<stop
|
||||
style="stop-color:#555753;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3315" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3076">
|
||||
<stop
|
||||
id="stop3078"
|
||||
offset="0"
|
||||
style="stop-color:#5c3566;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop3080"
|
||||
offset="1"
|
||||
style="stop-color:#5c3566;stop-opacity:0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2994">
|
||||
<stop
|
||||
style="stop-color:#5c3566;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2996" />
|
||||
<stop
|
||||
style="stop-color:#5c3566;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop2998" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3923">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3925" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3927" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3908">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3910" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3912" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2894">
|
||||
<stop
|
||||
style="stop-color:#39213f;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2896" />
|
||||
<stop
|
||||
id="stop2900"
|
||||
offset="0.47619048"
|
||||
style="stop-color:#75507b;stop-opacity:1;" />
|
||||
<stop
|
||||
style="stop-color:#5c3566;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2898" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3100">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3102" />
|
||||
<stop
|
||||
id="stop2071"
|
||||
offset="1"
|
||||
style="stop-color:white;stop-opacity:0;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3018">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3020" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3022" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3100"
|
||||
id="linearGradient3685"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="17.02047"
|
||||
y1="-16.276186"
|
||||
x2="17.02047"
|
||||
y2="-29.344501" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2894"
|
||||
id="linearGradient3687"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1,0,0,0.996152,0,85.74795)"
|
||||
x1="9.4176369"
|
||||
y1="-44.922661"
|
||||
x2="9.4176369"
|
||||
y2="-59.636772" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3076"
|
||||
id="linearGradient3689"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="16.749592"
|
||||
y1="21.616077"
|
||||
x2="16.749592"
|
||||
y2="32.797989" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2994"
|
||||
id="linearGradient3691"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="16.749592"
|
||||
y1="40.51022"
|
||||
x2="16.749592"
|
||||
y2="36.268337" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3908"
|
||||
id="linearGradient3693"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="8.25"
|
||||
y1="-14.375"
|
||||
x2="8.25"
|
||||
y2="-30.879261" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3923"
|
||||
id="radialGradient3695"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.268741,0,-1.646661e-6,0.184077,-10.86781,18.45272)"
|
||||
cx="10.189716"
|
||||
cy="16.554359"
|
||||
fx="10.189716"
|
||||
fy="16.554359"
|
||||
r="22.5" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3311"
|
||||
id="linearGradient3938"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(0,1)"
|
||||
x1="20.625"
|
||||
y1="-90.064087"
|
||||
x2="20.625"
|
||||
y2="-84.029831" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3018"
|
||||
id="linearGradient3129"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="9"
|
||||
y1="-92.805496"
|
||||
x2="9"
|
||||
y2="-83.4375" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4232"
|
||||
id="linearGradient4238"
|
||||
x1="0.99999888"
|
||||
y1="30.499076"
|
||||
x2="53.999733"
|
||||
y2="37.624077"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="vectors"
|
||||
style="display:inline">
|
||||
<g
|
||||
id="g891"
|
||||
transform="matrix(0.186703,0,0,0.186703,-21.1073,57.62299)" />
|
||||
<g
|
||||
id="g3131"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="translate(-1,102)">
|
||||
<path
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-alt.png"
|
||||
sodipodi:nodetypes="czcccccccscc"
|
||||
id="path3108"
|
||||
d="M 8.5,-94.500002 C 7.5975,-94.500002 7.168127,-94.186392 7,-93.04412 C 5.118026,-83.070943 5.215756,-74.96574 5.5,-64.864899 C 5.5,-64.115602 6.116307,-63.50001 6.875,-63.50001 L 42.125,-63.50001 C 42.88369,-63.50001 43.44816,-64.117293 43.5,-64.864899 C 44.5,-82.91177 42.5,-88.135111 42.5,-88.135111 C 42.5,-88.884408 41.88369,-89.5 41.125,-89.5 L 23.5,-89.5 L 22.5,-92.897058 C 22.254867,-93.729789 21.9025,-94.500002 21,-94.500002 L 8.5,-94.500002 z "
|
||||
style="fill:url(#linearGradient3938);fill-opacity:1;stroke:#555753;stroke-width:0.99999934;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
id="path3110"
|
||||
d="M 8.5,-93.5 C 8.167757,-93.5 8.1531396,-93.465856 8.15625,-93.46875 C 8.1593604,-93.471644 8.0371607,-93.339789 7.96875,-92.875 C 7.9689149,-92.864584 7.9689149,-92.854166 7.96875,-92.84375 C 6.2697576,-83.840251 6.2178889,-74.709407 6.4375,-65.5 L 42.625,-65.5 C 43.308005,-81.554915 41.5625,-87.875 41.5625,-87.875 C 41.530831,-87.955244 41.509819,-88.039293 41.5,-88.125 C 41.5,-88.333118 41.341065,-88.5 41.125,-88.5 L 23.5,-88.5 C 23.062797,-88.50549 22.681309,-88.797964 22.5625,-89.21875 L 21.5625,-92.625 C 21.45311,-92.996605 21.310238,-93.289949 21.21875,-93.40625 C 21.127262,-93.522551 21.173845,-93.5 21,-93.5 L 8.5,-93.5 z "
|
||||
style="opacity:0.5;fill:none;fill-opacity:1;stroke:url(#linearGradient3129);stroke-width:0.99999934;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" />
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="matrix(0.216083,0,0,0.263095,-1.89323,-11.2424)"
|
||||
id="g3112"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<path
|
||||
d="M 32.706693,164.36026 C 22.319193,164.36026 13.956693,172.72276 13.956693,183.11026 C 13.956693,193.49776 22.319193,201.86026 32.706693,201.86026 L 205.20669,201.86026 C 215.59419,201.86026 223.95669,193.49776 223.95669,183.11026 C 223.95669,172.72276 215.59419,164.36026 205.20669,164.36026 L 32.706693,164.36026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3114" />
|
||||
<path
|
||||
d="M 32.706693,165.61026 C 23.011693,165.61026 15.206693,173.41526 15.206693,183.11026 C 15.206693,192.80526 23.011693,200.61026 32.706693,200.61026 L 205.20669,200.61026 C 214.90169,200.61026 222.70669,192.80526 222.70669,183.11026 C 222.70669,173.41526 214.90169,165.61026 205.20669,165.61026 L 32.706693,165.61026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3116" />
|
||||
<path
|
||||
d="M 32.706694,166.86026 C 23.704194,166.86026 16.456694,174.10776 16.456694,183.11026 C 16.456694,192.11276 23.704194,199.36026 32.706694,199.36026 L 205.20669,199.36026 C 214.20919,199.36026 221.45669,192.11276 221.45669,183.11026 C 221.45669,174.10776 214.20919,166.86026 205.20669,166.86026 L 32.706694,166.86026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3118" />
|
||||
<path
|
||||
d="M 32.706694,168.11026 C 24.396694,168.11026 17.706694,174.80026 17.706694,183.11026 C 17.706694,191.42026 24.396694,198.11026 32.706694,198.11026 L 205.20669,198.11026 C 213.51669,198.11026 220.20669,191.42026 220.20669,183.11026 C 220.20669,174.80026 213.51669,168.11026 205.20669,168.11026 L 32.706694,168.11026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3120" />
|
||||
<path
|
||||
d="M 32.707764,169.36026 C 25.090264,169.36026 18.957764,175.49276 18.957764,183.11026 C 18.957764,190.72776 25.090264,196.86026 32.707764,196.86026 L 205.20618,196.86026 C 212.82368,196.86026 218.95618,190.72776 218.95618,183.11026 C 218.95618,175.49276 212.82368,169.36026 205.20618,169.36026 L 32.707764,169.36026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3122" />
|
||||
<path
|
||||
d="M 32.706694,170.61026 C 25.781694,170.61026 20.206694,176.18526 20.206694,183.11026 C 20.206694,190.03526 25.781694,195.61026 32.706694,195.61026 L 205.20669,195.61026 C 212.13169,195.61026 217.70669,190.03526 217.70669,183.11026 C 217.70669,176.18526 212.13169,170.61026 205.20669,170.61026 L 32.706694,170.61026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3124" />
|
||||
<path
|
||||
d="M 32.706694,171.86026 C 26.474194,171.86026 21.456694,176.87776 21.456694,183.11026 C 21.456694,189.34276 26.474194,194.36026 32.706694,194.36026 L 205.20669,194.36026 C 211.43919,194.36026 216.45669,189.34276 216.45669,183.11026 C 216.45669,176.87776 211.43919,171.86026 205.20669,171.86026 L 32.706694,171.86026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3126" />
|
||||
<path
|
||||
d="M 32.706694,173.11026 C 27.166694,173.11026 22.706694,177.57026 22.706694,183.11026 C 22.706694,188.65026 27.166694,193.11026 32.706694,193.11026 L 205.20669,193.11026 C 210.74669,193.11026 215.20669,188.65026 215.20669,183.11026 C 215.20669,177.57026 210.74669,173.11026 205.20669,173.11026 L 32.706694,173.11026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3128" />
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
id="g3661"
|
||||
transform="translate(-1,-2)"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<g
|
||||
id="g3663">
|
||||
<path
|
||||
sodipodi:type="inkscape:offset"
|
||||
inkscape:radius="-0.99436891"
|
||||
inkscape:original="M 2.65625 -33.5 C 2.021877 -33.5 1.5 -33.140103 1.5 -32.6875 C 4.500001 -25.004483 4.5 -23.258435 4.5 -16.5 C 4.5 -15.5 5.5111718 -13.51291 6.65625 -13.4375 L 42.34375 -13.4375 C 43.989096 -13.51291 44.500268 -15.758435 44.5 -16.5 C 44.5 -21.258435 44.500267 -25.004482 47.5 -32.6875 C 47.5 -33.140104 46.978125 -33.5 46.34375 -33.5 L 2.65625 -33.5 z "
|
||||
style="opacity:0.32156863;fill:none;fill-opacity:1;stroke:url(#linearGradient3685);stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline"
|
||||
id="path3667"
|
||||
d="M 2.625,-32.5 C 5.3597846,-25.370787 5.5,-23.037216 5.5,-16.5 C 5.5,-16.335159 5.6904313,-15.649773 6,-15.15625 C 6.3095687,-14.662727 6.6985247,-14.438832 6.71875,-14.4375 L 42.3125,-14.4375 L 42.34375,-14.4375 C 42.706073,-14.463351 42.916739,-14.679637 43.15625,-15.15625 C 43.401014,-15.643317 43.500051,-16.358058 43.5,-16.5 C 43.5,-21.130327 43.573003,-25.119501 46.375,-32.5 C 46.360642,-32.501163 46.358943,-32.5 46.34375,-32.5 L 2.65625,-32.5 C 2.6410568,-32.5 2.6393578,-32.501163 2.625,-32.5 z "
|
||||
transform="translate(0,54)" />
|
||||
<path
|
||||
style="opacity:0.3254902;fill:none;fill-rule:evenodd;stroke:white;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 2.5,21.5 L 46.5,21.5"
|
||||
id="path3669"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccc"
|
||||
id="path3671"
|
||||
d="M 2.645078,20.5 L 46.354654,20.5 C 46.989027,20.5 47.499732,20.862969 47.499732,21.313831 C 44.5,28.967284 44.5,32.694452 44.5,37.434576 C 44.500268,38.173288 44,40.423032 42.354654,40.498152 L 6.645078,40.498152 C 5.5,40.423032 4.5,38.430728 4.5,37.434576 C 4.5,30.702148 4.5,28.967284 1.499999,21.313831 C 1.499999,20.862969 2.010705,20.5 2.645078,20.5 z "
|
||||
style="fill:url(#linearGradient4238);fill-opacity:1;stroke:url(#linearGradient3687);stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
id="path3673"
|
||||
d="M 2.65625,21 C 2.4314409,21 2.2397168,21.044736 2.125,21.125 C 2.0102832,21.205264 2,21.274638 2,21.3125 C 4.2218558,27.007434 4.7883072,29.513938 4.9375,33.3125 L 44.125,33.3125 C 44.328358,30.089308 44.943507,26.607344 47,21.3125 C 47,21.274637 46.989716,21.205264 46.875,21.125 C 46.760284,21.044736 46.56856,21 46.34375,21 L 2.65625,21 z "
|
||||
style="opacity:0.8;fill:url(#linearGradient3689);fill-opacity:1;stroke:none;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
id="path3675"
|
||||
d="M 4.625,30 C 4.9603306,32.150219 5,34.257337 5,37.4375 C 5,37.771087 5.2086862,38.497978 5.5625,39.0625 C 5.9077942,39.613428 6.3538112,39.964089 6.65625,40 L 6.6875,40 L 42.3125,40 L 42.34375,40 C 42.929781,39.95968 43.308738,39.567494 43.59375,39 C 43.883899,38.422277 44.000093,37.694547 44,37.4375 C 44,34.957143 44.019836,32.683092 44.46875,30 L 4.625,30 z "
|
||||
style="opacity:0.44313725;fill:url(#linearGradient3691);fill-opacity:1;stroke:none;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
transform="translate(0,54)"
|
||||
d="M 2.625,-32.5 C 5.3597846,-25.370787 5.5,-23.037216 5.5,-16.5 C 5.5,-16.335159 5.6904313,-15.649773 6,-15.15625 C 6.3095687,-14.662727 6.6985247,-14.438832 6.71875,-14.4375 L 42.3125,-14.4375 L 42.34375,-14.4375 C 42.706073,-14.463351 42.916739,-14.679637 43.15625,-15.15625 C 43.401014,-15.643317 43.500051,-16.358058 43.5,-16.5 C 43.5,-21.130327 43.573003,-25.119501 46.375,-32.5 C 46.360642,-32.501163 46.358943,-32.5 46.34375,-32.5 L 2.65625,-32.5 C 2.6410568,-32.5 2.6393578,-32.501163 2.625,-32.5 z "
|
||||
id="path3677"
|
||||
style="opacity:0.2;fill:none;fill-opacity:1;stroke:url(#linearGradient3693);stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline"
|
||||
inkscape:original="M 2.65625 -33.5 C 2.021877 -33.5 1.5 -33.140103 1.5 -32.6875 C 4.500001 -25.004483 4.5 -23.258435 4.5 -16.5 C 4.5 -15.5 5.5111718 -13.51291 6.65625 -13.4375 L 42.34375 -13.4375 C 43.989096 -13.51291 44.500268 -15.758435 44.5 -16.5 C 44.5 -21.258435 44.500267 -25.004482 47.5 -32.6875 C 47.5 -33.140104 46.978125 -33.5 46.34375 -33.5 L 2.65625 -33.5 z "
|
||||
inkscape:radius="-0.99436891"
|
||||
sodipodi:type="inkscape:offset" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path3679"
|
||||
d="M 2.5,21.5 L 46.5,21.5"
|
||||
style="opacity:0.4;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient3695);stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
id="g6102"
|
||||
inkscape:label="pixmap"
|
||||
style="display:inline"
|
||||
transform="translate(188.7605,-103.2651)" />
|
||||
<g
|
||||
transform="matrix(0.186703,0,0,0.186703,167.6532,-45.64211)"
|
||||
id="g6106" />
|
||||
<g
|
||||
inkscape:label="pattern"
|
||||
id="g30621"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
transform="translate(190.4218,35.092)" />
|
||||
<g
|
||||
inkscape:label="pattern"
|
||||
id="g35222"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
transform="translate(190.4218,81.092)" />
|
||||
<g
|
||||
id="g3198"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="translate(-1,102)">
|
||||
<rect
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-alt.png"
|
||||
ry="1.5"
|
||||
rx="1.5"
|
||||
y="-91.5"
|
||||
x="9.4999981"
|
||||
height="3"
|
||||
width="10.000002"
|
||||
id="rect2118"
|
||||
style="fill:#eeeeec;fill-opacity:1;stroke:#d3d7cf;stroke-width:0.99999988;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" />
|
||||
<g
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-alt.png"
|
||||
transform="translate(66,0)"
|
||||
id="g3371"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1"
|
||||
id="rect3373"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-55"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1;display:inline"
|
||||
id="rect3375"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-53"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1;display:inline"
|
||||
id="rect3377"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-51"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1;display:inline"
|
||||
id="rect3379"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-49"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 24 KiB |
26
resources/recipes/aprospect.recipe
Executable file
26
resources/recipes/aprospect.recipe
Executable file
@ -0,0 +1,26 @@
|
||||
import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AmericanProspect(BasicNewsRecipe):
|
||||
title = u'American Prospect'
|
||||
__author__ = u'Michael Heinz'
|
||||
oldest_article = 30
|
||||
language = 'en'
|
||||
max_articles_per_feed = 100
|
||||
recursions = 0
|
||||
no_stylesheets = True
|
||||
remove_javascript = True
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'<body.*?<div class="pad_10L10R">', re.DOTALL|re.IGNORECASE), lambda match: '<body><div>'),
|
||||
(re.compile(r'</div>.*</body>', re.DOTALL|re.IGNORECASE), lambda match: '</div></body>'),
|
||||
(re.compile('\r'),lambda match: ''),
|
||||
(re.compile(r'<!-- .+? -->', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
(re.compile(r'<link .+?>', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
(re.compile(r'<script.*?</script>', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
(re.compile(r'<noscript.*?</noscript>', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
(re.compile(r'<meta .*?/>', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'feed://www.prospect.org/articles_rss.jsp')]
|
||||
|
19
resources/recipes/factcheck.recipe
Normal file
19
resources/recipes/factcheck.recipe
Normal file
@ -0,0 +1,19 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class FactCheckOrg(BasicNewsRecipe):
|
||||
title = u'Factcheck'
|
||||
__author__ = u'Michael Heinz'
|
||||
language = 'en'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
recursion = 0
|
||||
|
||||
publication_type = 'magazine'
|
||||
masthead_url = 'http://factcheck.org/wp-content/themes/Streamline/images/headernew.jpg'
|
||||
cover_url = 'http://factcheck.org/wp-content/themes/Streamline/images/headernew.jpg'
|
||||
|
||||
remove_tags = [ dict({'id':['footer','footerabout','sidebar']}) ]
|
||||
|
||||
|
||||
feeds = [(u'Factcheck', u'feed://www.factcheck.org/feed/')]
|
||||
|
30
resources/recipes/politifact.recipe
Normal file
30
resources/recipes/politifact.recipe
Normal file
@ -0,0 +1,30 @@
|
||||
from calibre.wb.feeds.news import BasicNewsRecipe
|
||||
|
||||
class PolitiFactCom(BasicNewsRecipe):
|
||||
title = u'Politifact'
|
||||
__author__ = u'Michael Heinz'
|
||||
oldest_article = 21
|
||||
max_articles_per_feed = 100
|
||||
recursion = 0
|
||||
language = 'en'
|
||||
|
||||
no_stylesheets = True
|
||||
|
||||
publication_type = 'magazine'
|
||||
masthead_url = 'http://static.politifact.com.s3.amazonaws.com/images/politifactdotcom-flag-fff_01.png'
|
||||
cover_url = 'http://static.politifact.com.s3.amazonaws.com/images/politifactdotcom-flag-fff_01.png'
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':'pfstoryarchive'}),
|
||||
dict(name='div', attrs={'class':'pfhead'}),
|
||||
dict(name='div', attrs={'class':'boxmid'}),
|
||||
]
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'pfcontentleft'})]
|
||||
feeds = [
|
||||
(u'Articles', u'http://www.politifact.com/feeds/articles/truth-o-meter/'),
|
||||
(u'Obamameter', u'http://politifact.com/feeds/updates/'),
|
||||
(u'Statements', u'http://www.politifact.com/feeds/statements/truth-o-meter/')
|
||||
]
|
||||
|
||||
|
@ -42,7 +42,7 @@ class FOLDER_DEVICE(USBMS):
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
||||
#: Icon for this device
|
||||
icon = I('sd.svg')
|
||||
icon = I('devices/folder.svg')
|
||||
METADATA_CACHE = '.metadata.calibre'
|
||||
|
||||
_main_prefix = ''
|
||||
|
@ -1,370 +0,0 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''
|
||||
'''
|
||||
import re, time, functools
|
||||
from uuid import uuid4 as _uuid
|
||||
import xml.dom.minidom as dom
|
||||
from base64 import b64encode as encode
|
||||
|
||||
|
||||
from calibre.devices.usbms.books import BookList as _BookList
|
||||
from calibre.devices import strftime as _strftime
|
||||
from calibre.devices.prs505 import MEDIA_XML, CACHE_XML
|
||||
from calibre.devices.errors import PathError
|
||||
|
||||
strftime = functools.partial(_strftime, zone=time.gmtime)
|
||||
|
||||
MIME_MAP = {
|
||||
"lrf" : "application/x-sony-bbeb",
|
||||
'lrx' : 'application/x-sony-bbeb',
|
||||
"rtf" : "application/rtf",
|
||||
"pdf" : "application/pdf",
|
||||
"txt" : "text/plain" ,
|
||||
'epub': 'application/epub+zip',
|
||||
}
|
||||
|
||||
def uuid():
|
||||
return str(_uuid()).replace('-', '', 1).upper()
|
||||
|
||||
def sortable_title(title):
|
||||
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
|
||||
|
||||
class BookList(_BookList):
|
||||
|
||||
def __init__(self, oncard, prefix, settings):
|
||||
_BookList.__init__(self, oncard, prefix, settings)
|
||||
if prefix is None:
|
||||
return
|
||||
self.sony_id_cache = {}
|
||||
self.books_lpath_cache = {}
|
||||
opts = settings()
|
||||
self.collections = opts.extra_customization.split(',') if opts.extra_customization else []
|
||||
db = CACHE_XML if oncard else MEDIA_XML
|
||||
with open(prefix + db, 'rb') as xml_file:
|
||||
xml_file.seek(0)
|
||||
self.document = dom.parse(xml_file)
|
||||
self.root_element = self.document.documentElement
|
||||
self.mountpath = prefix
|
||||
records = self.root_element.getElementsByTagName('records')
|
||||
|
||||
if records:
|
||||
self.prefix = 'xs1:'
|
||||
self.root_element = records[0]
|
||||
else:
|
||||
self.prefix = ''
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
self.sony_id_cache[child.getAttribute('id')] = child.getAttribute('path')
|
||||
# set the key to none. Will be filled in later when booklist is built
|
||||
self.books_lpath_cache[child.getAttribute('path')] = None
|
||||
self.tag_order = {}
|
||||
|
||||
paths = self.purge_corrupted_files()
|
||||
for path in paths:
|
||||
try:
|
||||
self.del_file(path, end_session=False)
|
||||
except PathError: # Incase this is a refetch without a sync in between
|
||||
continue
|
||||
|
||||
|
||||
def max_id(self):
|
||||
max = 0
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
nid = int(child.getAttribute('id'))
|
||||
if nid > max:
|
||||
max = nid
|
||||
return max
|
||||
|
||||
def is_id_valid(self, id):
|
||||
'''Return True iff there is an element with C{id==id}.'''
|
||||
id = str(id)
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
if child.getAttribute('id') == id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def supports_tags(self):
|
||||
return True
|
||||
|
||||
def add_book(self, book, replace_metadata):
|
||||
# Add a node into the DOM tree, representing a book. Also add to booklist
|
||||
if book in self:
|
||||
# replacing metadata for book
|
||||
self.delete_node(book.lpath)
|
||||
else:
|
||||
self.append(book)
|
||||
if not replace_metadata:
|
||||
if self.books_lpath_cache.has_key(book.lpath):
|
||||
self.books_lpath_cache[book.lpath] = book
|
||||
return
|
||||
# Book not in metadata. Add it. Note that we don't need to worry about
|
||||
# extra books in the Sony metadata. The reader deletes them for us when
|
||||
# we disconnect. That said, if it becomes important one day, we can do
|
||||
# it by scanning the books_lpath_cache for None entries and removing the
|
||||
# corresponding nodes.
|
||||
self.books_lpath_cache[book.lpath] = book
|
||||
cid = self.max_id()+1
|
||||
node = self.document.createElement(self.prefix + "text")
|
||||
self.sony_id_cache[cid] = book.lpath
|
||||
mime = MIME_MAP.get(book.lpath.rpartition('.')[-1].lower(), MIME_MAP['epub'])
|
||||
try:
|
||||
sourceid = str(self[0].sourceid) if len(self) else '1'
|
||||
except:
|
||||
sourceid = '1'
|
||||
attrs = {
|
||||
"title" : book.title,
|
||||
'titleSorter' : sortable_title(book.title),
|
||||
"author" : book.format_authors() if book.format_authors() else _('Unknown'),
|
||||
"page":"0", "part":"0", "scale":"0", \
|
||||
"sourceid":sourceid, "id":str(cid), "date":"", \
|
||||
"mime":mime, "path":book.lpath, "size":str(book.size)
|
||||
}
|
||||
for attr in attrs.keys():
|
||||
node.setAttributeNode(self.document.createAttribute(attr))
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
try:
|
||||
w, h, data = book.thumbnail
|
||||
except:
|
||||
w, h, data = None, None, None
|
||||
|
||||
if data:
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th.setAttribute("width", str(w))
|
||||
th.setAttribute("height", str(h))
|
||||
jpeg = self.document.createElement(self.prefix + "jpeg")
|
||||
jpeg.appendChild(self.document.createTextNode(encode(data)))
|
||||
th.appendChild(jpeg)
|
||||
node.appendChild(th)
|
||||
self.root_element.appendChild(node)
|
||||
|
||||
tags = []
|
||||
for item in self.collections:
|
||||
item = item.strip()
|
||||
mitem = getattr(book, item, None)
|
||||
titems = []
|
||||
if mitem:
|
||||
if isinstance(mitem, list):
|
||||
titems = mitem
|
||||
else:
|
||||
titems = [mitem]
|
||||
if item == 'tags' and titems:
|
||||
litems = []
|
||||
for i in titems:
|
||||
if not i.strip().startswith('[') and not i.strip().endswith(']'):
|
||||
litems.append(i)
|
||||
titems = litems
|
||||
tags.extend(titems)
|
||||
if tags:
|
||||
tags = list(set(tags))
|
||||
if hasattr(book, 'tag_order'):
|
||||
self.tag_order.update(book.tag_order)
|
||||
self.set_playlists(cid, tags)
|
||||
return True # metadata cache has changed. Must sync at end
|
||||
|
||||
def _delete_node(self, node):
|
||||
nid = node.getAttribute('id')
|
||||
self.remove_from_playlists(nid)
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
|
||||
def delete_node(self, lpath):
|
||||
'''
|
||||
Remove DOM node corresponding to book with lpath.
|
||||
Also remove book from any collections it is part of.
|
||||
'''
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
if child.getAttribute('path') == lpath:
|
||||
self._delete_node(child)
|
||||
break
|
||||
|
||||
def remove_book(self, book):
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{path == path}.
|
||||
Also remove book from any collections it is part of, and remove
|
||||
from the booklist
|
||||
'''
|
||||
self.remove(book)
|
||||
self.delete_node(book.lpath)
|
||||
|
||||
def playlists(self):
|
||||
ans = []
|
||||
for c in self.root_element.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('playlist'):
|
||||
ans.append(c)
|
||||
return ans
|
||||
|
||||
def playlist_items(self):
|
||||
plitems = []
|
||||
for pl in self.playlists():
|
||||
for c in pl.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('item') and \
|
||||
hasattr(c, 'getAttribute'):
|
||||
try:
|
||||
c.getAttribute('id')
|
||||
except: # Unlinked node
|
||||
continue
|
||||
plitems.append(c)
|
||||
return plitems
|
||||
|
||||
def purge_corrupted_files(self):
|
||||
if not self.root_element:
|
||||
return []
|
||||
corrupted = self.root_element.getElementsByTagName(self.prefix+'corrupted')
|
||||
paths = []
|
||||
for c in corrupted:
|
||||
paths.append(c.getAttribute('path'))
|
||||
c.parentNode.removeChild(c)
|
||||
c.unlink()
|
||||
return paths
|
||||
|
||||
def purge_empty_playlists(self):
|
||||
''' Remove all playlists that have no children. Also removes any invalid playlist items.'''
|
||||
for pli in self.playlist_items():
|
||||
try:
|
||||
if not self.is_id_valid(pli.getAttribute('id')):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
except:
|
||||
continue
|
||||
for pl in self.playlists():
|
||||
empty = True
|
||||
for c in pl.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('item'):
|
||||
empty = False
|
||||
break
|
||||
if empty:
|
||||
pl.parentNode.removeChild(pl)
|
||||
pl.unlink()
|
||||
|
||||
def playlist_by_title(self, title):
|
||||
for pl in self.playlists():
|
||||
if pl.getAttribute('title').lower() == title.lower():
|
||||
return pl
|
||||
|
||||
def add_playlist(self, title):
|
||||
cid = self.max_id()+1
|
||||
pl = self.document.createElement(self.prefix+'playlist')
|
||||
pl.setAttribute('id', str(cid))
|
||||
pl.setAttribute('title', title)
|
||||
pl.setAttribute('uuid', uuid())
|
||||
self.root_element.insertBefore(pl, self.root_element.childNodes[-1])
|
||||
return pl
|
||||
|
||||
def remove_from_playlists(self, id):
|
||||
for pli in self.playlist_items():
|
||||
if pli.getAttribute('id') == str(id):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
tags = [t for t in tags if t]
|
||||
book.tags = tags
|
||||
self.set_playlists(book.id, tags)
|
||||
|
||||
def set_playlists(self, id, collections):
|
||||
self.remove_from_playlists(id)
|
||||
for collection in set(collections):
|
||||
coll = self.playlist_by_title(collection)
|
||||
if not coll:
|
||||
coll = self.add_playlist(collection)
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(id))
|
||||
coll.appendChild(item)
|
||||
|
||||
def next_id(self):
|
||||
return self.document.documentElement.getAttribute('nextID')
|
||||
|
||||
def set_next_id(self, id):
|
||||
self.document.documentElement.setAttribute('nextID', str(id))
|
||||
|
||||
def write(self, stream):
|
||||
""" Write XML representation of DOM tree to C{stream} """
|
||||
src = self.document.toxml('utf-8') + '\n'
|
||||
stream.write(src.replace("'", '''))
|
||||
|
||||
def reorder_playlists(self):
|
||||
for title in self.tag_order.keys():
|
||||
pl = self.playlist_by_title(title)
|
||||
if not pl:
|
||||
continue
|
||||
# make a list of the ids
|
||||
sony_ids = [id.getAttribute('id') \
|
||||
for id in pl.childNodes if hasattr(id, 'getAttribute')]
|
||||
# convert IDs in playlist to a list of lpaths
|
||||
sony_paths = [self.sony_id_cache[id] for id in sony_ids]
|
||||
# create list of books containing lpaths
|
||||
books = [self.books_lpath_cache.get(p, None) for p in sony_paths]
|
||||
# create dict of db_id -> sony_id
|
||||
imap = {}
|
||||
for book, sony_id in zip(books, sony_ids):
|
||||
if book is not None:
|
||||
db_id = book.application_id
|
||||
if db_id is None:
|
||||
db_id = book.db_id
|
||||
if db_id is not None:
|
||||
imap[book.application_id] = sony_id
|
||||
# filter the list, removing books not on device but on playlist
|
||||
books = [i for i in books if i is not None]
|
||||
# filter the order specification to the books we have
|
||||
ordered_ids = [db_id for db_id in self.tag_order[title] if db_id in imap]
|
||||
|
||||
# rewrite the playlist in the correct order
|
||||
if len(ordered_ids) < len(pl.childNodes):
|
||||
continue
|
||||
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
|
||||
for child in children:
|
||||
pl.removeChild(child)
|
||||
child.unlink()
|
||||
for id in ordered_ids:
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(imap[id]))
|
||||
pl.appendChild(item)
|
||||
|
||||
def fix_ids(main, carda, cardb):
|
||||
'''
|
||||
Adjust ids the XML databases.
|
||||
'''
|
||||
if hasattr(main, 'purge_empty_playlists'):
|
||||
main.purge_empty_playlists()
|
||||
if hasattr(carda, 'purge_empty_playlists'):
|
||||
carda.purge_empty_playlists()
|
||||
if hasattr(cardb, 'purge_empty_playlists'):
|
||||
cardb.purge_empty_playlists()
|
||||
|
||||
def regen_ids(db):
|
||||
if not hasattr(db, 'root_element'):
|
||||
return
|
||||
id_map = {}
|
||||
db.purge_empty_playlists()
|
||||
cid = 0 if db == main else 1
|
||||
for child in db.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute('id'):
|
||||
id_map[child.getAttribute('id')] = str(cid)
|
||||
child.setAttribute("sourceid",
|
||||
'0' if getattr(child, 'tagName', '').endswith('playlist') else '1')
|
||||
child.setAttribute('id', str(cid))
|
||||
cid += 1
|
||||
|
||||
for item in db.playlist_items():
|
||||
oid = item.getAttribute('id')
|
||||
try:
|
||||
item.setAttribute('id', id_map[oid])
|
||||
except KeyError:
|
||||
item.parentNode.removeChild(item)
|
||||
item.unlink()
|
||||
db.reorder_playlists()
|
||||
db.sony_id_cache = {}
|
||||
for child in db.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
db.sony_id_cache[child.getAttribute('id')] = child.getAttribute('path')
|
||||
|
||||
|
||||
regen_ids(main)
|
||||
regen_ids(carda)
|
||||
regen_ids(cardb)
|
||||
|
||||
main.set_next_id(str(main.max_id()+1))
|
@ -13,9 +13,9 @@ import os
|
||||
import re
|
||||
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
from calibre.devices.prs505.books import BookList as PRS_BookList, fix_ids
|
||||
from calibre.devices.prs505 import MEDIA_XML
|
||||
from calibre.devices.prs505 import CACHE_XML
|
||||
from calibre.devices.prs505.sony_cache import XMLCache
|
||||
from calibre import __appname__
|
||||
|
||||
class PRS505(USBMS):
|
||||
@ -27,8 +27,6 @@ class PRS505(USBMS):
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
path_sep = '/'
|
||||
|
||||
booklist_class = PRS_BookList # See usbms.driver for some explanation of this
|
||||
|
||||
FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt']
|
||||
|
||||
VENDOR_ID = [0x054c] #: SONY Vendor Id
|
||||
@ -72,23 +70,34 @@ class PRS505(USBMS):
|
||||
fname = base + suffix + '.' + fname.rpartition('.')[-1]
|
||||
return fname
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
fix_ids(*booklists)
|
||||
if not os.path.exists(self._main_prefix):
|
||||
os.makedirs(self._main_prefix)
|
||||
with open(self._main_prefix + MEDIA_XML, 'wb') as f:
|
||||
booklists[0].write(f)
|
||||
def initialize_XML_cache(self):
|
||||
paths = {}
|
||||
for prefix, path, source_id in [
|
||||
('main', MEDIA_XML, 0),
|
||||
('card_a', CACHE_XML, 1),
|
||||
('card_b', CACHE_XML, 2)
|
||||
]:
|
||||
prefix = getattr(self, '_%s_prefix'%prefix)
|
||||
if prefix is not None and os.path.exists(prefix):
|
||||
paths[source_id] = os.path.join(prefix, *(path.split('/')))
|
||||
d = os.path.dirname(paths[source_id])
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
return XMLCache(paths)
|
||||
|
||||
def write_card_prefix(prefix, listid):
|
||||
if prefix is not None and hasattr(booklists[listid], 'write'):
|
||||
tgt = os.path.join(prefix, *(CACHE_XML.split('/')))
|
||||
base = os.path.dirname(tgt)
|
||||
if not os.path.exists(base):
|
||||
os.makedirs(base)
|
||||
with open(tgt, 'wb') as f:
|
||||
booklists[listid].write(f)
|
||||
write_card_prefix(self._card_a_prefix, 1)
|
||||
write_card_prefix(self._card_b_prefix, 2)
|
||||
def books(self, oncard=None, end_session=True):
|
||||
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
||||
c = self.initialize_XML_cache()
|
||||
c.update_booklist(bl, {'carda':1, 'cardb':2}.get(oncard, 0))
|
||||
return bl
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
c = self.initialize_XML_cache()
|
||||
blists = {}
|
||||
for i in c.paths:
|
||||
blists[i] = booklists[i]
|
||||
c.update(blists)
|
||||
c.write()
|
||||
|
||||
USBMS.sync_booklists(self, booklists, end_session)
|
||||
|
||||
|
249
src/calibre/devices/prs505/sony_cache.py
Normal file
249
src/calibre/devices/prs505/sony_cache.py
Normal file
@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
from pprint import pprint
|
||||
from base64 import b64decode
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre import prints
|
||||
from calibre.devices.errors import DeviceError
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.ebooks.metadata import string_to_authors
|
||||
|
||||
EMPTY_CARD_CACHE = '''\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cache xmlns="http://www.kinoma.com/FskCache/1">
|
||||
</cache>
|
||||
'''
|
||||
|
||||
class XMLCache(object):
|
||||
|
||||
def __init__(self, paths):
|
||||
if DEBUG:
|
||||
pprint(paths)
|
||||
self.paths = paths
|
||||
parser = etree.XMLParser(recover=True)
|
||||
self.roots = {}
|
||||
for source_id, path in paths.items():
|
||||
if source_id == 0:
|
||||
if not os.path.exists(path):
|
||||
raise DeviceError('The SONY XML cache media.xml does not exist. Try'
|
||||
' disconnecting and reconnecting your reader.')
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
else:
|
||||
raw = EMPTY_CARD_CACHE
|
||||
if os.access(path, os.R_OK):
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
self.roots[source_id] = etree.fromstring(xml_to_unicode(
|
||||
raw, strip_encoding_pats=True, assume_utf8=True,
|
||||
verbose=DEBUG)[0],
|
||||
parser=parser)
|
||||
|
||||
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
||||
if not recs:
|
||||
raise DeviceError('The SONY XML database is corrupted (no <records>)')
|
||||
self.record_roots = {}
|
||||
self.record_roots.update(self.roots)
|
||||
self.record_roots[0] = recs[0]
|
||||
|
||||
self.detect_namespaces()
|
||||
|
||||
|
||||
# Playlist management {{{
|
||||
def purge_broken_playlist_items(self, root):
|
||||
for item in root.xpath(
|
||||
'//*[local-name()="playlist"]/*[local-name()="item"]'):
|
||||
id_ = item.get('id', None)
|
||||
if id_ is None or not root.xpath(
|
||||
'//*[local-name()!="item" and @id="%s"]'%id_):
|
||||
if DEBUG:
|
||||
prints('Purging broken playlist item:',
|
||||
etree.tostring(item, with_tail=False))
|
||||
item.getparent().remove(item)
|
||||
|
||||
|
||||
def prune_empty_playlists(self):
|
||||
for i, root in self.record_roots.items():
|
||||
self.purge_broken_playlist_items(root)
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
if len(playlist) == 0:
|
||||
if DEBUG:
|
||||
prints('Removing playlist:', playlist.get('id', None))
|
||||
playlist.getparent().remove(playlist)
|
||||
|
||||
# }}}
|
||||
|
||||
def fix_ids(self): # {{{
|
||||
|
||||
def ensure_numeric_ids(root):
|
||||
idmap = {}
|
||||
for x in root.xpath('//*[@id]'):
|
||||
id_ = x.get('id')
|
||||
try:
|
||||
id_ = int(id_)
|
||||
except:
|
||||
x.set('id', '-1')
|
||||
idmap[id_] = '-1'
|
||||
|
||||
if DEBUG and idmap:
|
||||
prints('Found non numeric ids:')
|
||||
prints(list(idmap.keys()))
|
||||
return idmap
|
||||
|
||||
def remap_playlist_references(root, idmap):
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
for item in playlist.xpath(
|
||||
'descendant::*[@id and local-name()="item"]'):
|
||||
id_ = item.get('id')
|
||||
if id_ in idmap:
|
||||
item.set('id', idmap[id_])
|
||||
if DEBUG:
|
||||
prints('Remapping id %s to %s'%(id_, idmap[id_]))
|
||||
|
||||
def ensure_media_xml_base_ids(root):
|
||||
for num, tag in enumerate(('library', 'watchSpecial')):
|
||||
for x in root.xpath('//*[local-name()="%s"]'%tag):
|
||||
x.set('id', str(num))
|
||||
|
||||
def rebase_ids(root, base, sourceid, pl_sourceid):
|
||||
'Rebase all ids and also make them consecutive'
|
||||
for item in root.xpath('//*[@sourceid]'):
|
||||
sid = pl_sourceid if item.tag.endswith('playlist') else sourceid
|
||||
item.set('sourceid', str(sid))
|
||||
items = root.xpath('//*[@id]')
|
||||
items.sort(cmp=lambda x,y:cmp(int(x.get('id')), int(y.get('id'))))
|
||||
idmap = {}
|
||||
for i, item in enumerate(items):
|
||||
old = int(item.get('id'))
|
||||
new = base + i
|
||||
if old != new:
|
||||
item.set('id', str(new))
|
||||
idmap[old] = str(new)
|
||||
return idmap
|
||||
|
||||
self.prune_empty_playlists()
|
||||
|
||||
for i in sorted(self.roots.keys()):
|
||||
root = self.roots[i]
|
||||
if i == 0:
|
||||
ensure_media_xml_base_ids(root)
|
||||
|
||||
idmap = ensure_numeric_ids(root)
|
||||
remap_playlist_references(root, idmap)
|
||||
if i == 0:
|
||||
sourceid, playlist_sid = 1, 0
|
||||
base = 0
|
||||
else:
|
||||
previous = i-1
|
||||
if previous not in self.roots:
|
||||
previous = 0
|
||||
max_id = self.max_id(self.roots[previous])
|
||||
sourceid = playlist_sid = max_id + 1
|
||||
base = max_id + 2
|
||||
idmap = rebase_ids(root, base, sourceid, playlist_sid)
|
||||
remap_playlist_references(root, idmap)
|
||||
|
||||
last_bl = max(self.roots.keys())
|
||||
max_id = self.max_id(self.roots[last_bl])
|
||||
self.roots[0].set('nextID', str(max_id+1))
|
||||
# }}}
|
||||
|
||||
def update_booklist(self, bl, bl_index): # {{{
|
||||
if bl_index not in self.record_roots:
|
||||
return
|
||||
root = self.record_roots[bl_index]
|
||||
for book in bl:
|
||||
record = self.book_by_lpath(book.lpath, root)
|
||||
if record is not None:
|
||||
title = record.get('title', None)
|
||||
if title is not None and title != book.title:
|
||||
if DEBUG:
|
||||
prints('Renaming title', book.title, 'to', title)
|
||||
book.title = title
|
||||
authors = record.get('author', None)
|
||||
if authors is not None:
|
||||
authors = string_to_authors(authors)
|
||||
if authors != book.authors:
|
||||
if DEBUG:
|
||||
prints('Renaming authors', book.authors, 'to',
|
||||
authors)
|
||||
book.authors = authors
|
||||
for thumbnail in record.xpath(
|
||||
'descendant::*[local-name()="thumbnail"]'):
|
||||
for img in thumbnail.xpath(
|
||||
'descendant::*[local-name()="jpeg"]|'
|
||||
'descendant::*[local-name()="png"]'):
|
||||
if img.text:
|
||||
raw = b64decode(img.text.strip())
|
||||
ext = img.tag.split('}')[-1]
|
||||
book.cover_data = [ext, raw]
|
||||
break
|
||||
break
|
||||
# }}}
|
||||
|
||||
def update(self, booklists):
|
||||
pass
|
||||
|
||||
def write(self):
|
||||
return
|
||||
for i, path in self.paths.items():
|
||||
raw = etree.tostring(self.roots[i], encoding='utf-8',
|
||||
xml_declaration=True)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(raw)
|
||||
|
||||
def book_by_lpath(self, lpath, root):
|
||||
matches = root.xpath(u'//*[local-name()="text" and @path="%s"]'%lpath)
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
|
||||
def max_id(self, root):
|
||||
ans = -1
|
||||
for x in root.xpath('//*[@id]'):
|
||||
id_ = x.get('id')
|
||||
try:
|
||||
num = int(id_)
|
||||
if num > ans:
|
||||
ans = num
|
||||
except:
|
||||
continue
|
||||
return ans
|
||||
|
||||
def detect_namespaces(self):
|
||||
self.nsmaps = {}
|
||||
for i, root in self.roots.items():
|
||||
self.nsmaps[i] = root.nsmap
|
||||
|
||||
self.namespaces = {}
|
||||
for i in self.roots:
|
||||
for c in ('library', 'text', 'image', 'playlist', 'thumbnail',
|
||||
'watchSpecial'):
|
||||
matches = self.record_roots[i].xpath('//*[local-name()="%s"]'%c)
|
||||
if matches:
|
||||
e = matches[0]
|
||||
self.namespaces[i] = e.nsmap[e.prefix]
|
||||
break
|
||||
if i not in self.namespaces:
|
||||
ns = self.nsmaps[i].get(None, None)
|
||||
for prefix in self.nsmaps[i]:
|
||||
if prefix is not None:
|
||||
ns = self.nsmaps[i][prefix]
|
||||
break
|
||||
self.namespaces[i] = ns
|
||||
|
||||
if DEBUG:
|
||||
prints('Found nsmaps:')
|
||||
pprint(self.nsmaps)
|
||||
prints('Found namespaces:')
|
||||
pprint(self.namespaces)
|
||||
|
@ -8,7 +8,7 @@ from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSiz
|
||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||
QEvent, QTimer, pyqtSignal, QDate
|
||||
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||
QIcon, QTableView, QApplication, QDialog, QPushButton
|
||||
QIcon, QApplication, QDialog, QPushButton
|
||||
|
||||
ORG_NAME = 'KovidsBrain'
|
||||
APP_UID = 'libprs500'
|
||||
@ -294,34 +294,6 @@ class GetMetadata(QObject):
|
||||
mi = MetaInformation('', [_('Unknown')])
|
||||
self.emit(SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'), id, mi)
|
||||
|
||||
class TableView(QTableView):
|
||||
|
||||
def __init__(self, parent):
|
||||
QTableView.__init__(self, parent)
|
||||
self.read_settings()
|
||||
|
||||
def read_settings(self):
|
||||
self.cw = dynamic[self.__class__.__name__+'column width map']
|
||||
|
||||
def write_settings(self):
|
||||
m = dynamic[self.__class__.__name__+'column width map']
|
||||
if m is None:
|
||||
m = {}
|
||||
cmap = getattr(self.model(), 'column_map', None)
|
||||
if cmap is not None:
|
||||
for i,c in enumerate(cmap):
|
||||
m[c] = self.columnWidth(i)
|
||||
dynamic[self.__class__.__name__+'column width map'] = m
|
||||
self.cw = m
|
||||
|
||||
def restore_column_widths(self):
|
||||
if self.cw and len(self.cw):
|
||||
for i,c in enumerate(self.model().column_map):
|
||||
if c in self.cw:
|
||||
self.setColumnWidth(i, self.cw[c])
|
||||
return True
|
||||
return False
|
||||
|
||||
class FileIconProvider(QFileIconProvider):
|
||||
|
||||
ICONS = {
|
||||
|
@ -327,15 +327,17 @@ class DeviceManager(Thread):
|
||||
|
||||
class DeviceAction(QAction):
|
||||
|
||||
a_s = pyqtSignal(object)
|
||||
|
||||
def __init__(self, dest, delete, specific, icon_path, text, parent=None):
|
||||
if delete:
|
||||
text += ' ' + _('and delete from library')
|
||||
QAction.__init__(self, QIcon(icon_path), text, parent)
|
||||
self.dest = dest
|
||||
self.delete = delete
|
||||
self.specific = specific
|
||||
self.connect(self, SIGNAL('triggered(bool)'),
|
||||
lambda x : self.emit(SIGNAL('a_s(QAction)'), self))
|
||||
self.triggered.connect(self.emit_triggered)
|
||||
|
||||
def emit_triggered(self, *args):
|
||||
self.a_s.emit(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
||||
@ -354,8 +356,9 @@ class DeviceMenu(QMenu):
|
||||
self.actions = []
|
||||
self._memory = []
|
||||
|
||||
self.set_default_menu = self.addMenu(_('Set default send to device'
|
||||
' action'))
|
||||
self.set_default_menu = QMenu(_('Set default send to device action'))
|
||||
self.set_default_menu.setIcon(QIcon(I('config.svg')))
|
||||
|
||||
opts = email_config().parse()
|
||||
default_account = None
|
||||
if opts.accounts:
|
||||
@ -379,51 +382,65 @@ class DeviceMenu(QMenu):
|
||||
self.connect(action2, SIGNAL('a_s(QAction)'),
|
||||
self.action_triggered)
|
||||
|
||||
_actions = [
|
||||
basic_actions = [
|
||||
('main:', False, False, I('reader.svg'),
|
||||
_('Send to main memory')),
|
||||
('carda:0', False, False, I('sd.svg'),
|
||||
_('Send to storage card A')),
|
||||
('cardb:0', False, False, I('sd.svg'),
|
||||
_('Send to storage card B')),
|
||||
'-----',
|
||||
]
|
||||
|
||||
delete_actions = [
|
||||
('main:', True, False, I('reader.svg'),
|
||||
_('Send to main memory')),
|
||||
_('Main Memory')),
|
||||
('carda:0', True, False, I('sd.svg'),
|
||||
_('Send to storage card A')),
|
||||
_('Storage Card A')),
|
||||
('cardb:0', True, False, I('sd.svg'),
|
||||
_('Send to storage card B')),
|
||||
'-----',
|
||||
_('Storage Card B')),
|
||||
]
|
||||
|
||||
specific_actions = [
|
||||
('main:', False, True, I('reader.svg'),
|
||||
_('Send specific format to main memory')),
|
||||
_('Main Memory')),
|
||||
('carda:0', False, True, I('sd.svg'),
|
||||
_('Send specific format to storage card A')),
|
||||
_('Storage Card A')),
|
||||
('cardb:0', False, True, I('sd.svg'),
|
||||
_('Send specific format to storage card B')),
|
||||
_('Storage Card B')),
|
||||
]
|
||||
|
||||
|
||||
]
|
||||
if default_account is not None:
|
||||
_actions.insert(2, default_account)
|
||||
_actions.insert(6, list(default_account))
|
||||
_actions[6][1] = True
|
||||
for round in (0, 1):
|
||||
for dest, delete, specific, icon, text in _actions:
|
||||
if dest == '-':
|
||||
(self.set_default_menu if round else self).addSeparator()
|
||||
continue
|
||||
action = DeviceAction(dest, delete, specific, icon, text, self)
|
||||
self._memory.append(action)
|
||||
if round == 1:
|
||||
action.setCheckable(True)
|
||||
action.setText(action.text())
|
||||
self.group.addAction(action)
|
||||
self.set_default_menu.addAction(action)
|
||||
else:
|
||||
self.connect(action, SIGNAL('a_s(QAction)'),
|
||||
self.action_triggered)
|
||||
self.actions.append(action)
|
||||
self.addAction(action)
|
||||
for x in (basic_actions, delete_actions):
|
||||
ac = list(default_account)
|
||||
if x is delete_actions:
|
||||
ac[1] = True
|
||||
x.insert(1, tuple(ac))
|
||||
|
||||
for menu in (self, self.set_default_menu):
|
||||
for actions, desc in (
|
||||
(basic_actions, ''),
|
||||
(delete_actions, _('Send and delete from library')),
|
||||
(specific_actions, _('Send specific format'))
|
||||
):
|
||||
mdest = menu
|
||||
if actions is not basic_actions:
|
||||
mdest = menu.addMenu(desc)
|
||||
self._memory.append(mdest)
|
||||
|
||||
for dest, delete, specific, icon, text in actions:
|
||||
action = DeviceAction(dest, delete, specific, icon, text, self)
|
||||
self._memory.append(action)
|
||||
if menu is self.set_default_menu:
|
||||
action.setCheckable(True)
|
||||
action.setText(action.text())
|
||||
self.group.addAction(action)
|
||||
else:
|
||||
action.a_s.connect(self.action_triggered)
|
||||
self.actions.append(action)
|
||||
mdest.addAction(action)
|
||||
if actions is not specific_actions:
|
||||
menu.addSeparator()
|
||||
|
||||
da = config['default_send_to_device_action']
|
||||
done = False
|
||||
@ -437,8 +454,7 @@ class DeviceMenu(QMenu):
|
||||
action.setChecked(True)
|
||||
config['default_send_to_device_action'] = repr(action)
|
||||
|
||||
self.connect(self.group, SIGNAL('triggered(QAction*)'),
|
||||
self.change_default_action)
|
||||
self.group.triggered.connect(self.change_default_action)
|
||||
if opts.accounts:
|
||||
self.addSeparator()
|
||||
self.addMenu(self.email_to_menu)
|
||||
@ -454,6 +470,8 @@ class DeviceMenu(QMenu):
|
||||
mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit())
|
||||
self.disconnect_from_folder_action = mitem
|
||||
|
||||
self.addSeparator()
|
||||
self.addMenu(self.set_default_menu)
|
||||
self.addSeparator()
|
||||
annot = self.addAction(_('Fetch annotations (experimental)'))
|
||||
annot.setEnabled(False)
|
||||
|
@ -1,6 +1,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, re, time, textwrap, copy
|
||||
|
||||
import os, re, time, textwrap, copy, sys
|
||||
|
||||
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||
@ -10,7 +11,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
|
||||
QProgressDialog
|
||||
|
||||
from calibre.constants import iswindows, isosx, preferred_encoding
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||
from calibre.gui2 import choose_dir, error_dialog, config, \
|
||||
@ -330,7 +331,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
def category_current_changed(self, n, p):
|
||||
self.stackedWidget.setCurrentIndex(n.row())
|
||||
|
||||
def __init__(self, parent, model, server=None):
|
||||
def __init__(self, parent, library_view, server=None):
|
||||
ResizableDialog.__init__(self, parent)
|
||||
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
||||
self._category_model = CategoryModel()
|
||||
@ -338,8 +339,9 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.category_view.currentChanged = self.category_current_changed
|
||||
self.category_view.setModel(self._category_model)
|
||||
self.parent = parent
|
||||
self.model = model
|
||||
self.db = model.db
|
||||
self.library_view = library_view
|
||||
self.model = library_view.model()
|
||||
self.db = self.model.db
|
||||
self.server = server
|
||||
path = prefs['library_path']
|
||||
self.location.setText(path if path else '')
|
||||
@ -364,26 +366,27 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.new_version_notification.setChecked(config['new_version_notification'])
|
||||
|
||||
# Set up columns
|
||||
# Make copies of maps so that internal changes aren't put into the real maps
|
||||
self.colmap = config['column_map'][:]
|
||||
colmap = list(self.model.column_map)
|
||||
state = self.library_view.get_state()
|
||||
hidden_cols = state['hidden_columns']
|
||||
positions = state['column_positions']
|
||||
colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y]))
|
||||
self.custcols = copy.deepcopy(self.db.custom_column_label_map)
|
||||
cm = [c.decode(preferred_encoding, 'replace') for c in self.colmap]
|
||||
ac = [c.decode(preferred_encoding, 'replace') for c in ALL_COLUMNS]
|
||||
for col in cm + \
|
||||
[i for i in ac if i not in cm] + \
|
||||
[i for i in self.custcols if i not in cm]:
|
||||
if col in ALL_COLUMNS:
|
||||
item = QListWidgetItem(model.orig_headers[col], self.columns)
|
||||
else:
|
||||
item = QListWidgetItem(self.custcols[col]['name'], self.columns)
|
||||
for col in colmap:
|
||||
item = QListWidgetItem(self.model.headers[col], self.columns)
|
||||
item.setData(Qt.UserRole, QVariant(col))
|
||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
||||
item.setCheckState(Qt.Checked if col in self.colmap else Qt.Unchecked)
|
||||
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
||||
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
||||
self.connect(self.del_custcol_button, SIGNAL('clicked()'), self.del_custcol)
|
||||
self.connect(self.add_custcol_button, SIGNAL('clicked()'), self.add_custcol)
|
||||
self.connect(self.edit_custcol_button, SIGNAL('clicked()'), self.edit_custcol)
|
||||
flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable
|
||||
if col != 'ondevice':
|
||||
flags |= Qt.ItemIsUserCheckable
|
||||
item.setFlags(flags)
|
||||
if col != 'ondevice':
|
||||
item.setCheckState(Qt.Unchecked if col in hidden_cols else
|
||||
Qt.Checked)
|
||||
self.column_up.clicked.connect(self.up_column)
|
||||
self.column_down.clicked.connect(self.down_column)
|
||||
self.del_custcol_button.clicked.connect(self.del_custcol)
|
||||
self.add_custcol_button.clicked.connect(self.add_custcol)
|
||||
self.edit_custcol_button.clicked.connect(self.edit_custcol)
|
||||
|
||||
icons = config['toolbar_icon_size']
|
||||
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
|
||||
@ -647,6 +650,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.input_order.insertItem(idx+1, self.input_order.takeItem(idx))
|
||||
self.input_order.setCurrentRow(idx+1)
|
||||
|
||||
# Column settings {{{
|
||||
def up_column(self):
|
||||
idx = self.columns.currentRow()
|
||||
if idx > 0:
|
||||
@ -683,6 +687,53 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
def edit_custcol(self):
|
||||
CreateCustomColumn(self, True, self.model.orig_headers, ALL_COLUMNS)
|
||||
|
||||
def apply_custom_column_changes(self):
|
||||
config_cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString())\
|
||||
for i in range(self.columns.count())]
|
||||
if not config_cols:
|
||||
config_cols = ['title']
|
||||
removed_cols = set(self.model.column_map) - set(config_cols)
|
||||
hidden_cols = set([unicode(self.columns.item(i).data(Qt.UserRole).toString())\
|
||||
for i in range(self.columns.count()) \
|
||||
if self.columns.item(i).checkState()==Qt.Unchecked])
|
||||
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
|
||||
hidden_cols = list(hidden_cols.intersection(set(self.model.column_map)))
|
||||
if 'ondevice' in hidden_cols:
|
||||
hidden_cols.remove('ondevice')
|
||||
def col_pos(x, y):
|
||||
xidx = config_cols.index(x) if x in config_cols else sys.maxint
|
||||
yidx = config_cols.index(y) if y in config_cols else sys.maxint
|
||||
return cmp(xidx, yidx)
|
||||
positions = {}
|
||||
for i, col in enumerate((sorted(self.model.column_map, cmp=col_pos))):
|
||||
positions[col] = i
|
||||
state = {'hidden_columns': hidden_cols, 'column_positions':positions}
|
||||
self.library_view.apply_state(state)
|
||||
self.library_view.save_state()
|
||||
|
||||
must_restart = False
|
||||
for c in self.custcols:
|
||||
if self.custcols[c]['num'] is None:
|
||||
self.db.create_custom_column(
|
||||
label=c,
|
||||
name=self.custcols[c]['name'],
|
||||
datatype=self.custcols[c]['datatype'],
|
||||
is_multiple=self.custcols[c]['is_multiple'],
|
||||
display = self.custcols[c]['display'])
|
||||
must_restart = True
|
||||
elif '*deleteme' in self.custcols[c]:
|
||||
self.db.delete_custom_column(label=c)
|
||||
must_restart = True
|
||||
elif '*edited' in self.custcols[c]:
|
||||
cc = self.custcols[c]
|
||||
self.db.set_custom_column_metadata(cc['num'], name=cc['name'],
|
||||
label=cc['label'],
|
||||
display = self.custcols[c]['display'])
|
||||
if '*must_restart' in self.custcols[c]:
|
||||
must_restart = True
|
||||
return must_restart
|
||||
# }}}
|
||||
|
||||
def view_server_logs(self):
|
||||
from calibre.library.server import log_access_file, log_error_file
|
||||
d = QDialog(self)
|
||||
@ -776,33 +827,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
||||
prefs['input_format_order'] = input_cols
|
||||
|
||||
####### Now deal with changes to columns
|
||||
cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString())\
|
||||
for i in range(self.columns.count()) \
|
||||
if self.columns.item(i).checkState()==Qt.Checked]
|
||||
if not cols:
|
||||
cols = ['title']
|
||||
config['column_map'] = cols
|
||||
must_restart = False
|
||||
for c in self.custcols:
|
||||
if self.custcols[c]['num'] is None:
|
||||
self.db.create_custom_column(
|
||||
label=c,
|
||||
name=self.custcols[c]['name'],
|
||||
datatype=self.custcols[c]['datatype'],
|
||||
is_multiple=self.custcols[c]['is_multiple'],
|
||||
display = self.custcols[c]['display'])
|
||||
must_restart = True
|
||||
elif '*deleteme' in self.custcols[c]:
|
||||
self.db.delete_custom_column(label=c)
|
||||
must_restart = True
|
||||
elif '*edited' in self.custcols[c]:
|
||||
cc = self.custcols[c]
|
||||
self.db.set_custom_column_metadata(cc['num'], name=cc['name'],
|
||||
label=cc['label'],
|
||||
display = self.custcols[c]['display'])
|
||||
if '*must_restart' in self.custcols[c]:
|
||||
must_restart = True
|
||||
must_restart = self.apply_custom_column_changes()
|
||||
|
||||
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
||||
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
||||
|
@ -123,7 +123,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
if ':' in col or ' ' in col or col.lower() != col:
|
||||
return self.simple_error('', _('The lookup name must be lower case and cannot contain ":"s or spaces'))
|
||||
|
||||
date_format = None
|
||||
date_format = {}
|
||||
if col_type == 'datetime':
|
||||
if self.date_format_box.text():
|
||||
date_format = {'date_format':unicode(self.date_format_box.text())}
|
||||
|
@ -14,12 +14,12 @@
|
||||
<string>Active Jobs</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../work/calibre/resources/images.qrc">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/jobs.svg</normaloff>:/images/jobs.svg</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="JobsView" name="jobs_view">
|
||||
<widget class="QTableView" name="jobs_view">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::NoContextMenu</enum>
|
||||
</property>
|
||||
@ -66,15 +66,8 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>JobsView</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../work/calibre/resources/images.qrc"/>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -15,10 +15,11 @@ from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
||||
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
from calibre.gui2 import Dispatcher, error_dialog, NONE, config
|
||||
from calibre.gui2 import Dispatcher, error_dialog, NONE, config, gprefs
|
||||
from calibre.gui2.device import DeviceJob
|
||||
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
||||
from calibre import __appname__
|
||||
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
||||
|
||||
class JobManager(QAbstractTableModel):
|
||||
|
||||
@ -243,7 +244,32 @@ class ProgressBarDelegate(QAbstractItemDelegate):
|
||||
opts.text = QString(_('Unavailable') if percent == 0 else '%d%%'%percent)
|
||||
QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)
|
||||
|
||||
class DetailView(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, parent, job):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(job.description)
|
||||
self.job = job
|
||||
self.next_pos = 0
|
||||
self.update()
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update)
|
||||
self.timer.start(1000)
|
||||
|
||||
|
||||
def update(self):
|
||||
f = self.job.log_file
|
||||
f.seek(self.next_pos)
|
||||
more = f.read()
|
||||
self.next_pos = f.tell()
|
||||
if more:
|
||||
self.log.appendPlainText(more.decode('utf-8', 'replace'))
|
||||
|
||||
|
||||
|
||||
class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
|
||||
def __init__(self, window, model):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_JobsDialog.__init__(self)
|
||||
@ -252,8 +278,6 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
self.model = model
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
self.setWindowTitle(__appname__ + _(' - Jobs'))
|
||||
self.connect(self.jobs_view.model(), SIGNAL('modelReset()'),
|
||||
self.jobs_view.resizeColumnsToContents)
|
||||
self.connect(self.kill_button, SIGNAL('clicked()'),
|
||||
self.kill_job)
|
||||
self.connect(self.details_button, SIGNAL('clicked()'),
|
||||
@ -264,7 +288,21 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
self.jobs_view.model().kill_job)
|
||||
self.pb_delegate = ProgressBarDelegate(self)
|
||||
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
|
||||
self.jobs_view.doubleClicked.connect(self.show_job_details)
|
||||
self.jobs_view.horizontalHeader().setMovable(True)
|
||||
state = gprefs.get('jobs view column layout', None)
|
||||
if state is not None:
|
||||
try:
|
||||
self.jobs_view.horizontalHeader().restoreState(bytes(state))
|
||||
except:
|
||||
pass
|
||||
|
||||
def show_job_details(self, index):
|
||||
row = index.row()
|
||||
job = self.jobs_view.model().row_to_job(row)
|
||||
d = DetailView(self, job)
|
||||
d.exec_()
|
||||
d.timer.stop()
|
||||
|
||||
def kill_job(self):
|
||||
for index in self.jobs_view.selectedIndexes():
|
||||
@ -281,5 +319,9 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
self.model.kill_all_jobs()
|
||||
|
||||
def closeEvent(self, e):
|
||||
self.jobs_view.write_settings()
|
||||
try:
|
||||
state = bytearray(self.jobs_view.horizontalHeader().saveState())
|
||||
gprefs['jobs view column layout'] = state
|
||||
except:
|
||||
pass
|
||||
e.accept()
|
||||
|
@ -1011,6 +1011,12 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
||||
if self.db[self.map[row]].in_library:
|
||||
return QVariant(self.bool_yes_icon)
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
cname = self.column_map[index.column()]
|
||||
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
||||
'left')]
|
||||
return QVariant(ans)
|
||||
|
||||
|
||||
return NONE
|
||||
|
||||
|
@ -44,6 +44,7 @@ class BooksView(QTableView): # {{{
|
||||
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
||||
|
||||
# {{{ Column Header setup
|
||||
self.was_restored = False
|
||||
self.column_header = self.horizontalHeader()
|
||||
self.column_header.setMovable(True)
|
||||
self.column_header.sectionMoved.connect(self.save_state)
|
||||
@ -99,19 +100,28 @@ class BooksView(QTableView): # {{{
|
||||
column=col))
|
||||
m = self.column_header_context_menu.addMenu(
|
||||
_('Sort on %s') % name)
|
||||
m.addAction(_('Ascending'),
|
||||
a = m.addAction(_('Ascending'),
|
||||
partial(self.column_header_context_handler,
|
||||
action='ascending', column=col))
|
||||
m.addAction(_('Descending'),
|
||||
d = m.addAction(_('Descending'),
|
||||
partial(self.column_header_context_handler,
|
||||
action='descending', column=col))
|
||||
if self._model.sorted_on[0] == col:
|
||||
ac = a if self._model.sorted_on[1] == Qt.AscendingOrder else d
|
||||
ac.setCheckable(True)
|
||||
ac.setChecked(True)
|
||||
m = self.column_header_context_menu.addMenu(
|
||||
_('Change text alignment for %s') % name)
|
||||
al = self._model.alignment_map.get(col, 'left')
|
||||
for x, t in (('left', _('Left')), ('right', _('Right')), ('center',
|
||||
_('Center'))):
|
||||
m.addAction(t,
|
||||
a = m.addAction(t,
|
||||
partial(self.column_header_context_handler,
|
||||
action='align_'+x, column=col))
|
||||
if al == x:
|
||||
a.setCheckable(True)
|
||||
a.setChecked(True)
|
||||
|
||||
|
||||
|
||||
hidden_cols = [self.column_map[i] for i in
|
||||
@ -189,7 +199,7 @@ class BooksView(QTableView): # {{{
|
||||
|
||||
def save_state(self):
|
||||
# Only save if we have been initialized (set_database called)
|
||||
if len(self.column_map) > 0:
|
||||
if len(self.column_map) > 0 and self.was_restored:
|
||||
state = self.get_state()
|
||||
name = unicode(self.objectName())
|
||||
if name:
|
||||
@ -278,6 +288,7 @@ class BooksView(QTableView): # {{{
|
||||
old_state['sort_history'] = tweaks['sort_columns_at_startup']
|
||||
|
||||
self.apply_state(old_state)
|
||||
self.was_restored = True
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -33,16 +33,12 @@ class JobsButton(QFrame):
|
||||
|
||||
def initialize(self, jobs_dialog):
|
||||
self.jobs_dialog = jobs_dialog
|
||||
self.jobs_dialog.jobs_view.restore_column_widths()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.jobs_dialog.isVisible():
|
||||
self.jobs_dialog.jobs_view.write_settings()
|
||||
self.jobs_dialog.hide()
|
||||
else:
|
||||
self.jobs_dialog.jobs_view.read_settings()
|
||||
self.jobs_dialog.show()
|
||||
self.jobs_dialog.jobs_view.restore_column_widths()
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
|
@ -2237,7 +2237,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
_('Cannot configure before calibre is restarted.'))
|
||||
d.exec_()
|
||||
return
|
||||
d = ConfigDialog(self, self.library_view.model(),
|
||||
d = ConfigDialog(self, self.library_view,
|
||||
server=self.content_server)
|
||||
|
||||
d.exec_()
|
||||
@ -2255,9 +2255,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.save_menu.actions()[3].setText(
|
||||
_('Save only %s format to disk in a single directory')%
|
||||
prefs['output_format'].upper())
|
||||
self.library_view.model().read_config()
|
||||
self.library_view.model().refresh()
|
||||
self.library_view.model().research()
|
||||
self.tags_view.set_new_model() # in case columns changed
|
||||
self.tags_view.recount()
|
||||
self.create_device_menu()
|
||||
|
@ -7,15 +7,15 @@ import re, os, traceback
|
||||
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
||||
QListWidgetItem, QTextCharFormat, QApplication, \
|
||||
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
||||
QPixmap, QPalette, QTimer, QDialog, QSplitterHandle, \
|
||||
QPixmap, QPalette, QSplitterHandle, \
|
||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
||||
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
|
||||
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
||||
QMenu, QStringListModel, QCompleter, QStringList
|
||||
|
||||
from calibre.gui2 import human_readable, NONE, TableView, \
|
||||
from calibre.gui2 import human_readable, NONE, \
|
||||
error_dialog, pixmap_to_data, dynamic
|
||||
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
||||
|
||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||
from calibre import fit_image
|
||||
from calibre.utils.fonts import fontconfig
|
||||
@ -399,41 +399,6 @@ class EjectButton(QAbstractButton):
|
||||
painter.drawPixmap(0, 0, image)
|
||||
|
||||
|
||||
class DetailView(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, parent, job):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(job.description)
|
||||
self.job = job
|
||||
self.next_pos = 0
|
||||
self.update()
|
||||
self.timer = QTimer(self)
|
||||
self.connect(self.timer, SIGNAL('timeout()'), self.update)
|
||||
self.timer.start(1000)
|
||||
|
||||
|
||||
def update(self):
|
||||
f = self.job.log_file
|
||||
f.seek(self.next_pos)
|
||||
more = f.read()
|
||||
self.next_pos = f.tell()
|
||||
if more:
|
||||
self.log.appendPlainText(more.decode('utf-8', 'replace'))
|
||||
|
||||
|
||||
class JobsView(TableView):
|
||||
|
||||
def __init__(self, parent):
|
||||
TableView.__init__(self, parent)
|
||||
self.connect(self, SIGNAL('doubleClicked(QModelIndex)'), self.show_details)
|
||||
|
||||
def show_details(self, index):
|
||||
row = index.row()
|
||||
job = self.model().row_to_job(row)
|
||||
d = DetailView(self, job)
|
||||
d.exec_()
|
||||
d.timer.stop()
|
||||
|
||||
|
||||
class FontFamilyModel(QAbstractListModel):
|
||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
||||
'''
|
||||
Manage application-wide preferences.
|
||||
'''
|
||||
import os, re, cPickle, textwrap, traceback, plistlib, json
|
||||
import os, re, cPickle, textwrap, traceback, plistlib, json, base64
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from optparse import OptionParser as _OptionParser
|
||||
@ -636,11 +636,31 @@ class JSONConfig(XMLConfig):
|
||||
|
||||
EXTENSION = '.json'
|
||||
|
||||
def to_json(self, obj):
|
||||
if isinstance(obj, bytearray):
|
||||
return {'__class__': 'bytearray',
|
||||
'__value__': base64.standard_b64encode(bytes(obj))}
|
||||
raise TypeError(repr(obj) + ' is not JSON serializable')
|
||||
|
||||
def from_json(self, obj):
|
||||
if '__class__' in obj:
|
||||
if obj['__class__'] == 'bytearray':
|
||||
return bytearray(base64.standard_b64decode(obj['__value__']))
|
||||
return obj
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
return json.loads(raw.decode('utf-8'))
|
||||
return json.loads(raw.decode('utf-8'), object_hook=self.from_json)
|
||||
|
||||
def to_raw(self):
|
||||
return json.dumps(self, indent=2)
|
||||
return json.dumps(self, indent=2, default=self.to_json)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
dict.__setitem__(self, key, val)
|
||||
self.commit()
|
||||
|
||||
|
||||
|
||||
def _prefs():
|
||||
|
Loading…
x
Reference in New Issue
Block a user