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
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
#: Icon for this device
|
#: Icon for this device
|
||||||
icon = I('sd.svg')
|
icon = I('devices/folder.svg')
|
||||||
METADATA_CACHE = '.metadata.calibre'
|
METADATA_CACHE = '.metadata.calibre'
|
||||||
|
|
||||||
_main_prefix = ''
|
_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
|
import re
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
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 MEDIA_XML
|
||||||
from calibre.devices.prs505 import CACHE_XML
|
from calibre.devices.prs505 import CACHE_XML
|
||||||
|
from calibre.devices.prs505.sony_cache import XMLCache
|
||||||
from calibre import __appname__
|
from calibre import __appname__
|
||||||
|
|
||||||
class PRS505(USBMS):
|
class PRS505(USBMS):
|
||||||
@ -27,8 +27,6 @@ class PRS505(USBMS):
|
|||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
path_sep = '/'
|
path_sep = '/'
|
||||||
|
|
||||||
booklist_class = PRS_BookList # See usbms.driver for some explanation of this
|
|
||||||
|
|
||||||
FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt']
|
FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt']
|
||||||
|
|
||||||
VENDOR_ID = [0x054c] #: SONY Vendor Id
|
VENDOR_ID = [0x054c] #: SONY Vendor Id
|
||||||
@ -72,23 +70,34 @@ class PRS505(USBMS):
|
|||||||
fname = base + suffix + '.' + fname.rpartition('.')[-1]
|
fname = base + suffix + '.' + fname.rpartition('.')[-1]
|
||||||
return fname
|
return fname
|
||||||
|
|
||||||
def sync_booklists(self, booklists, end_session=True):
|
def initialize_XML_cache(self):
|
||||||
fix_ids(*booklists)
|
paths = {}
|
||||||
if not os.path.exists(self._main_prefix):
|
for prefix, path, source_id in [
|
||||||
os.makedirs(self._main_prefix)
|
('main', MEDIA_XML, 0),
|
||||||
with open(self._main_prefix + MEDIA_XML, 'wb') as f:
|
('card_a', CACHE_XML, 1),
|
||||||
booklists[0].write(f)
|
('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):
|
def books(self, oncard=None, end_session=True):
|
||||||
if prefix is not None and hasattr(booklists[listid], 'write'):
|
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
||||||
tgt = os.path.join(prefix, *(CACHE_XML.split('/')))
|
c = self.initialize_XML_cache()
|
||||||
base = os.path.dirname(tgt)
|
c.update_booklist(bl, {'carda':1, 'cardb':2}.get(oncard, 0))
|
||||||
if not os.path.exists(base):
|
return bl
|
||||||
os.makedirs(base)
|
|
||||||
with open(tgt, 'wb') as f:
|
def sync_booklists(self, booklists, end_session=True):
|
||||||
booklists[listid].write(f)
|
c = self.initialize_XML_cache()
|
||||||
write_card_prefix(self._card_a_prefix, 1)
|
blists = {}
|
||||||
write_card_prefix(self._card_b_prefix, 2)
|
for i in c.paths:
|
||||||
|
blists[i] = booklists[i]
|
||||||
|
c.update(blists)
|
||||||
|
c.write()
|
||||||
|
|
||||||
USBMS.sync_booklists(self, booklists, end_session)
|
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, \
|
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||||
QEvent, QTimer, pyqtSignal, QDate
|
QEvent, QTimer, pyqtSignal, QDate
|
||||||
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||||
QIcon, QTableView, QApplication, QDialog, QPushButton
|
QIcon, QApplication, QDialog, QPushButton
|
||||||
|
|
||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
@ -294,34 +294,6 @@ class GetMetadata(QObject):
|
|||||||
mi = MetaInformation('', [_('Unknown')])
|
mi = MetaInformation('', [_('Unknown')])
|
||||||
self.emit(SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'), id, mi)
|
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):
|
class FileIconProvider(QFileIconProvider):
|
||||||
|
|
||||||
ICONS = {
|
ICONS = {
|
||||||
|
@ -327,15 +327,17 @@ class DeviceManager(Thread):
|
|||||||
|
|
||||||
class DeviceAction(QAction):
|
class DeviceAction(QAction):
|
||||||
|
|
||||||
|
a_s = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, dest, delete, specific, icon_path, text, parent=None):
|
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)
|
QAction.__init__(self, QIcon(icon_path), text, parent)
|
||||||
self.dest = dest
|
self.dest = dest
|
||||||
self.delete = delete
|
self.delete = delete
|
||||||
self.specific = specific
|
self.specific = specific
|
||||||
self.connect(self, SIGNAL('triggered(bool)'),
|
self.triggered.connect(self.emit_triggered)
|
||||||
lambda x : self.emit(SIGNAL('a_s(QAction)'), self))
|
|
||||||
|
def emit_triggered(self, *args):
|
||||||
|
self.a_s.emit(self)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
||||||
@ -354,8 +356,9 @@ class DeviceMenu(QMenu):
|
|||||||
self.actions = []
|
self.actions = []
|
||||||
self._memory = []
|
self._memory = []
|
||||||
|
|
||||||
self.set_default_menu = self.addMenu(_('Set default send to device'
|
self.set_default_menu = QMenu(_('Set default send to device action'))
|
||||||
' action'))
|
self.set_default_menu.setIcon(QIcon(I('config.svg')))
|
||||||
|
|
||||||
opts = email_config().parse()
|
opts = email_config().parse()
|
||||||
default_account = None
|
default_account = None
|
||||||
if opts.accounts:
|
if opts.accounts:
|
||||||
@ -379,51 +382,65 @@ class DeviceMenu(QMenu):
|
|||||||
self.connect(action2, SIGNAL('a_s(QAction)'),
|
self.connect(action2, SIGNAL('a_s(QAction)'),
|
||||||
self.action_triggered)
|
self.action_triggered)
|
||||||
|
|
||||||
_actions = [
|
basic_actions = [
|
||||||
('main:', False, False, I('reader.svg'),
|
('main:', False, False, I('reader.svg'),
|
||||||
_('Send to main memory')),
|
_('Send to main memory')),
|
||||||
('carda:0', False, False, I('sd.svg'),
|
('carda:0', False, False, I('sd.svg'),
|
||||||
_('Send to storage card A')),
|
_('Send to storage card A')),
|
||||||
('cardb:0', False, False, I('sd.svg'),
|
('cardb:0', False, False, I('sd.svg'),
|
||||||
_('Send to storage card B')),
|
_('Send to storage card B')),
|
||||||
'-----',
|
|
||||||
('main:', True, False, I('reader.svg'),
|
|
||||||
_('Send to main memory')),
|
|
||||||
('carda:0', True, False, I('sd.svg'),
|
|
||||||
_('Send to storage card A')),
|
|
||||||
('cardb:0', True, False, I('sd.svg'),
|
|
||||||
_('Send to storage card B')),
|
|
||||||
'-----',
|
|
||||||
('main:', False, True, I('reader.svg'),
|
|
||||||
_('Send specific format to main memory')),
|
|
||||||
('carda:0', False, True, I('sd.svg'),
|
|
||||||
_('Send specific format to storage card A')),
|
|
||||||
('cardb:0', False, True, I('sd.svg'),
|
|
||||||
_('Send specific format to storage card B')),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
delete_actions = [
|
||||||
|
('main:', True, False, I('reader.svg'),
|
||||||
|
_('Main Memory')),
|
||||||
|
('carda:0', True, False, I('sd.svg'),
|
||||||
|
_('Storage Card A')),
|
||||||
|
('cardb:0', True, False, I('sd.svg'),
|
||||||
|
_('Storage Card B')),
|
||||||
|
]
|
||||||
|
|
||||||
|
specific_actions = [
|
||||||
|
('main:', False, True, I('reader.svg'),
|
||||||
|
_('Main Memory')),
|
||||||
|
('carda:0', False, True, I('sd.svg'),
|
||||||
|
_('Storage Card A')),
|
||||||
|
('cardb:0', False, True, I('sd.svg'),
|
||||||
|
_('Storage Card B')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
if default_account is not None:
|
if default_account is not None:
|
||||||
_actions.insert(2, default_account)
|
for x in (basic_actions, delete_actions):
|
||||||
_actions.insert(6, list(default_account))
|
ac = list(default_account)
|
||||||
_actions[6][1] = True
|
if x is delete_actions:
|
||||||
for round in (0, 1):
|
ac[1] = True
|
||||||
for dest, delete, specific, icon, text in _actions:
|
x.insert(1, tuple(ac))
|
||||||
if dest == '-':
|
|
||||||
(self.set_default_menu if round else self).addSeparator()
|
for menu in (self, self.set_default_menu):
|
||||||
continue
|
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)
|
action = DeviceAction(dest, delete, specific, icon, text, self)
|
||||||
self._memory.append(action)
|
self._memory.append(action)
|
||||||
if round == 1:
|
if menu is self.set_default_menu:
|
||||||
action.setCheckable(True)
|
action.setCheckable(True)
|
||||||
action.setText(action.text())
|
action.setText(action.text())
|
||||||
self.group.addAction(action)
|
self.group.addAction(action)
|
||||||
self.set_default_menu.addAction(action)
|
|
||||||
else:
|
else:
|
||||||
self.connect(action, SIGNAL('a_s(QAction)'),
|
action.a_s.connect(self.action_triggered)
|
||||||
self.action_triggered)
|
|
||||||
self.actions.append(action)
|
self.actions.append(action)
|
||||||
self.addAction(action)
|
mdest.addAction(action)
|
||||||
|
if actions is not specific_actions:
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
da = config['default_send_to_device_action']
|
da = config['default_send_to_device_action']
|
||||||
done = False
|
done = False
|
||||||
@ -437,8 +454,7 @@ class DeviceMenu(QMenu):
|
|||||||
action.setChecked(True)
|
action.setChecked(True)
|
||||||
config['default_send_to_device_action'] = repr(action)
|
config['default_send_to_device_action'] = repr(action)
|
||||||
|
|
||||||
self.connect(self.group, SIGNAL('triggered(QAction*)'),
|
self.group.triggered.connect(self.change_default_action)
|
||||||
self.change_default_action)
|
|
||||||
if opts.accounts:
|
if opts.accounts:
|
||||||
self.addSeparator()
|
self.addSeparator()
|
||||||
self.addMenu(self.email_to_menu)
|
self.addMenu(self.email_to_menu)
|
||||||
@ -454,6 +470,8 @@ class DeviceMenu(QMenu):
|
|||||||
mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit())
|
mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit())
|
||||||
self.disconnect_from_folder_action = mitem
|
self.disconnect_from_folder_action = mitem
|
||||||
|
|
||||||
|
self.addSeparator()
|
||||||
|
self.addMenu(self.set_default_menu)
|
||||||
self.addSeparator()
|
self.addSeparator()
|
||||||
annot = self.addAction(_('Fetch annotations (experimental)'))
|
annot = self.addAction(_('Fetch annotations (experimental)'))
|
||||||
annot.setEnabled(False)
|
annot.setEnabled(False)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__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, \
|
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||||
@ -10,7 +11,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
|||||||
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
|
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
|
||||||
QProgressDialog
|
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.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||||
from calibre.gui2 import choose_dir, error_dialog, config, \
|
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):
|
def category_current_changed(self, n, p):
|
||||||
self.stackedWidget.setCurrentIndex(n.row())
|
self.stackedWidget.setCurrentIndex(n.row())
|
||||||
|
|
||||||
def __init__(self, parent, model, server=None):
|
def __init__(self, parent, library_view, server=None):
|
||||||
ResizableDialog.__init__(self, parent)
|
ResizableDialog.__init__(self, parent)
|
||||||
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
||||||
self._category_model = CategoryModel()
|
self._category_model = CategoryModel()
|
||||||
@ -338,8 +339,9 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.category_view.currentChanged = self.category_current_changed
|
self.category_view.currentChanged = self.category_current_changed
|
||||||
self.category_view.setModel(self._category_model)
|
self.category_view.setModel(self._category_model)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.model = model
|
self.library_view = library_view
|
||||||
self.db = model.db
|
self.model = library_view.model()
|
||||||
|
self.db = self.model.db
|
||||||
self.server = server
|
self.server = server
|
||||||
path = prefs['library_path']
|
path = prefs['library_path']
|
||||||
self.location.setText(path if path else '')
|
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'])
|
self.new_version_notification.setChecked(config['new_version_notification'])
|
||||||
|
|
||||||
# Set up columns
|
# Set up columns
|
||||||
# Make copies of maps so that internal changes aren't put into the real maps
|
colmap = list(self.model.column_map)
|
||||||
self.colmap = config['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)
|
self.custcols = copy.deepcopy(self.db.custom_column_label_map)
|
||||||
cm = [c.decode(preferred_encoding, 'replace') for c in self.colmap]
|
for col in colmap:
|
||||||
ac = [c.decode(preferred_encoding, 'replace') for c in ALL_COLUMNS]
|
item = QListWidgetItem(self.model.headers[col], self.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)
|
|
||||||
item.setData(Qt.UserRole, QVariant(col))
|
item.setData(Qt.UserRole, QVariant(col))
|
||||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable
|
||||||
item.setCheckState(Qt.Checked if col in self.colmap else Qt.Unchecked)
|
if col != 'ondevice':
|
||||||
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
flags |= Qt.ItemIsUserCheckable
|
||||||
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
item.setFlags(flags)
|
||||||
self.connect(self.del_custcol_button, SIGNAL('clicked()'), self.del_custcol)
|
if col != 'ondevice':
|
||||||
self.connect(self.add_custcol_button, SIGNAL('clicked()'), self.add_custcol)
|
item.setCheckState(Qt.Unchecked if col in hidden_cols else
|
||||||
self.connect(self.edit_custcol_button, SIGNAL('clicked()'), self.edit_custcol)
|
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']
|
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)
|
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.insertItem(idx+1, self.input_order.takeItem(idx))
|
||||||
self.input_order.setCurrentRow(idx+1)
|
self.input_order.setCurrentRow(idx+1)
|
||||||
|
|
||||||
|
# Column settings {{{
|
||||||
def up_column(self):
|
def up_column(self):
|
||||||
idx = self.columns.currentRow()
|
idx = self.columns.currentRow()
|
||||||
if idx > 0:
|
if idx > 0:
|
||||||
@ -683,6 +687,53 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
def edit_custcol(self):
|
def edit_custcol(self):
|
||||||
CreateCustomColumn(self, True, self.model.orig_headers, ALL_COLUMNS)
|
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):
|
def view_server_logs(self):
|
||||||
from calibre.library.server import log_access_file, log_error_file
|
from calibre.library.server import log_access_file, log_error_file
|
||||||
d = QDialog(self)
|
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())]
|
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
|
prefs['input_format_order'] = input_cols
|
||||||
|
|
||||||
####### Now deal with changes to columns
|
must_restart = self.apply_custom_column_changes()
|
||||||
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
|
|
||||||
|
|
||||||
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
||||||
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
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:
|
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'))
|
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 col_type == 'datetime':
|
||||||
if self.date_format_box.text():
|
if self.date_format_box.text():
|
||||||
date_format = {'date_format':unicode(self.date_format_box.text())}
|
date_format = {'date_format':unicode(self.date_format_box.text())}
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
<string>Active Jobs</string>
|
<string>Active Jobs</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset resource="../../../work/calibre/resources/images.qrc">
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
<normaloff>:/images/jobs.svg</normaloff>:/images/jobs.svg</iconset>
|
<normaloff>:/images/jobs.svg</normaloff>:/images/jobs.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="JobsView" name="jobs_view">
|
<widget class="QTableView" name="jobs_view">
|
||||||
<property name="contextMenuPolicy">
|
<property name="contextMenuPolicy">
|
||||||
<enum>Qt::NoContextMenu</enum>
|
<enum>Qt::NoContextMenu</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -66,15 +66,8 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
|
||||||
<customwidget>
|
|
||||||
<class>JobsView</class>
|
|
||||||
<extends>QTableView</extends>
|
|
||||||
<header>widgets.h</header>
|
|
||||||
</customwidget>
|
|
||||||
</customwidgets>
|
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../work/calibre/resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
<connections/>
|
<connections/>
|
||||||
</ui>
|
</ui>
|
||||||
|
@ -15,10 +15,11 @@ from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
|||||||
|
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.utils.ipc.job import ParallelJob
|
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.device import DeviceJob
|
||||||
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
||||||
from calibre import __appname__
|
from calibre import __appname__
|
||||||
|
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
||||||
|
|
||||||
class JobManager(QAbstractTableModel):
|
class JobManager(QAbstractTableModel):
|
||||||
|
|
||||||
@ -243,7 +244,32 @@ class ProgressBarDelegate(QAbstractItemDelegate):
|
|||||||
opts.text = QString(_('Unavailable') if percent == 0 else '%d%%'%percent)
|
opts.text = QString(_('Unavailable') if percent == 0 else '%d%%'%percent)
|
||||||
QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)
|
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):
|
class JobsDialog(QDialog, Ui_JobsDialog):
|
||||||
|
|
||||||
def __init__(self, window, model):
|
def __init__(self, window, model):
|
||||||
QDialog.__init__(self, window)
|
QDialog.__init__(self, window)
|
||||||
Ui_JobsDialog.__init__(self)
|
Ui_JobsDialog.__init__(self)
|
||||||
@ -252,8 +278,6 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.setWindowModality(Qt.NonModal)
|
self.setWindowModality(Qt.NonModal)
|
||||||
self.setWindowTitle(__appname__ + _(' - Jobs'))
|
self.setWindowTitle(__appname__ + _(' - Jobs'))
|
||||||
self.connect(self.jobs_view.model(), SIGNAL('modelReset()'),
|
|
||||||
self.jobs_view.resizeColumnsToContents)
|
|
||||||
self.connect(self.kill_button, SIGNAL('clicked()'),
|
self.connect(self.kill_button, SIGNAL('clicked()'),
|
||||||
self.kill_job)
|
self.kill_job)
|
||||||
self.connect(self.details_button, SIGNAL('clicked()'),
|
self.connect(self.details_button, SIGNAL('clicked()'),
|
||||||
@ -264,7 +288,21 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
self.jobs_view.model().kill_job)
|
self.jobs_view.model().kill_job)
|
||||||
self.pb_delegate = ProgressBarDelegate(self)
|
self.pb_delegate = ProgressBarDelegate(self)
|
||||||
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
|
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):
|
def kill_job(self):
|
||||||
for index in self.jobs_view.selectedIndexes():
|
for index in self.jobs_view.selectedIndexes():
|
||||||
@ -281,5 +319,9 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
self.model.kill_all_jobs()
|
self.model.kill_all_jobs()
|
||||||
|
|
||||||
def closeEvent(self, e):
|
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()
|
e.accept()
|
||||||
|
@ -1011,6 +1011,12 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
||||||
if self.db[self.map[row]].in_library:
|
if self.db[self.map[row]].in_library:
|
||||||
return QVariant(self.bool_yes_icon)
|
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
|
return NONE
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
||||||
|
|
||||||
# {{{ Column Header setup
|
# {{{ Column Header setup
|
||||||
|
self.was_restored = False
|
||||||
self.column_header = self.horizontalHeader()
|
self.column_header = self.horizontalHeader()
|
||||||
self.column_header.setMovable(True)
|
self.column_header.setMovable(True)
|
||||||
self.column_header.sectionMoved.connect(self.save_state)
|
self.column_header.sectionMoved.connect(self.save_state)
|
||||||
@ -99,19 +100,28 @@ class BooksView(QTableView): # {{{
|
|||||||
column=col))
|
column=col))
|
||||||
m = self.column_header_context_menu.addMenu(
|
m = self.column_header_context_menu.addMenu(
|
||||||
_('Sort on %s') % name)
|
_('Sort on %s') % name)
|
||||||
m.addAction(_('Ascending'),
|
a = m.addAction(_('Ascending'),
|
||||||
partial(self.column_header_context_handler,
|
partial(self.column_header_context_handler,
|
||||||
action='ascending', column=col))
|
action='ascending', column=col))
|
||||||
m.addAction(_('Descending'),
|
d = m.addAction(_('Descending'),
|
||||||
partial(self.column_header_context_handler,
|
partial(self.column_header_context_handler,
|
||||||
action='descending', column=col))
|
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(
|
m = self.column_header_context_menu.addMenu(
|
||||||
_('Change text alignment for %s') % name)
|
_('Change text alignment for %s') % name)
|
||||||
|
al = self._model.alignment_map.get(col, 'left')
|
||||||
for x, t in (('left', _('Left')), ('right', _('Right')), ('center',
|
for x, t in (('left', _('Left')), ('right', _('Right')), ('center',
|
||||||
_('Center'))):
|
_('Center'))):
|
||||||
m.addAction(t,
|
a = m.addAction(t,
|
||||||
partial(self.column_header_context_handler,
|
partial(self.column_header_context_handler,
|
||||||
action='align_'+x, column=col))
|
action='align_'+x, column=col))
|
||||||
|
if al == x:
|
||||||
|
a.setCheckable(True)
|
||||||
|
a.setChecked(True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
hidden_cols = [self.column_map[i] for i in
|
hidden_cols = [self.column_map[i] for i in
|
||||||
@ -189,7 +199,7 @@ class BooksView(QTableView): # {{{
|
|||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
# Only save if we have been initialized (set_database called)
|
# 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()
|
state = self.get_state()
|
||||||
name = unicode(self.objectName())
|
name = unicode(self.objectName())
|
||||||
if name:
|
if name:
|
||||||
@ -278,6 +288,7 @@ class BooksView(QTableView): # {{{
|
|||||||
old_state['sort_history'] = tweaks['sort_columns_at_startup']
|
old_state['sort_history'] = tweaks['sort_columns_at_startup']
|
||||||
|
|
||||||
self.apply_state(old_state)
|
self.apply_state(old_state)
|
||||||
|
self.was_restored = True
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -33,16 +33,12 @@ class JobsButton(QFrame):
|
|||||||
|
|
||||||
def initialize(self, jobs_dialog):
|
def initialize(self, jobs_dialog):
|
||||||
self.jobs_dialog = jobs_dialog
|
self.jobs_dialog = jobs_dialog
|
||||||
self.jobs_dialog.jobs_view.restore_column_widths()
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
if self.jobs_dialog.isVisible():
|
if self.jobs_dialog.isVisible():
|
||||||
self.jobs_dialog.jobs_view.write_settings()
|
|
||||||
self.jobs_dialog.hide()
|
self.jobs_dialog.hide()
|
||||||
else:
|
else:
|
||||||
self.jobs_dialog.jobs_view.read_settings()
|
|
||||||
self.jobs_dialog.show()
|
self.jobs_dialog.show()
|
||||||
self.jobs_dialog.jobs_view.restore_column_widths()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
|
@ -2237,7 +2237,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
_('Cannot configure before calibre is restarted.'))
|
_('Cannot configure before calibre is restarted.'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
d = ConfigDialog(self, self.library_view.model(),
|
d = ConfigDialog(self, self.library_view,
|
||||||
server=self.content_server)
|
server=self.content_server)
|
||||||
|
|
||||||
d.exec_()
|
d.exec_()
|
||||||
@ -2255,9 +2255,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.save_menu.actions()[3].setText(
|
self.save_menu.actions()[3].setText(
|
||||||
_('Save only %s format to disk in a single directory')%
|
_('Save only %s format to disk in a single directory')%
|
||||||
prefs['output_format'].upper())
|
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.set_new_model() # in case columns changed
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
self.create_device_menu()
|
self.create_device_menu()
|
||||||
|
@ -7,15 +7,15 @@ import re, os, traceback
|
|||||||
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
||||||
QListWidgetItem, QTextCharFormat, QApplication, \
|
QListWidgetItem, QTextCharFormat, QApplication, \
|
||||||
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
||||||
QPixmap, QPalette, QTimer, QDialog, QSplitterHandle, \
|
QPixmap, QPalette, QSplitterHandle, \
|
||||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
||||||
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
|
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
|
||||||
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
||||||
QMenu, QStringListModel, QCompleter, QStringList
|
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
|
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.gui2.filename_pattern_ui import Ui_Form
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts import fontconfig
|
||||||
@ -399,41 +399,6 @@ class EjectButton(QAbstractButton):
|
|||||||
painter.drawPixmap(0, 0, image)
|
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):
|
class FontFamilyModel(QAbstractListModel):
|
||||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
Manage application-wide preferences.
|
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 copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from optparse import OptionParser as _OptionParser
|
from optparse import OptionParser as _OptionParser
|
||||||
@ -636,11 +636,31 @@ class JSONConfig(XMLConfig):
|
|||||||
|
|
||||||
EXTENSION = '.json'
|
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):
|
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):
|
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():
|
def _prefs():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user