Sync to trunk.
@ -4,6 +4,83 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.18
|
||||||
|
date: 2010-09-10
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "All new Preferences dialog, with nicer layout and the ability to restore settings to defaults"
|
||||||
|
type: major
|
||||||
|
|
||||||
|
- title: "Add series info when available to generated cover. Also auto-resize the logo on the cover to ensure all text fits"
|
||||||
|
tickets: [6724]
|
||||||
|
|
||||||
|
- title: "On device column: Now indicates when multiple copies of the same book are present on the device"
|
||||||
|
|
||||||
|
- title: "Driver for the Gemei GM2000"
|
||||||
|
|
||||||
|
- title: "Extract fb2 files from zip container when adding to calibre. Can be disables by disabling the Archive Extract file type plugin."
|
||||||
|
tickets: [6739]
|
||||||
|
|
||||||
|
- title: "Switch to using raster icons for a small speedup in startup time"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "On device column: Fix matching bug when multiple books in the library have the same title and author"
|
||||||
|
|
||||||
|
- title: "Content server: Use /mobile version for Kindle browser"
|
||||||
|
|
||||||
|
- title: "E-book viewer: When adding a bookmark, a default name is generated"
|
||||||
|
tickets: [6450]
|
||||||
|
|
||||||
|
- title: "Hide visible menus before clearing toolbar."
|
||||||
|
tickets: [6706]
|
||||||
|
|
||||||
|
- title: "Batch conversion: Don't overwrite the insert page break before setting"
|
||||||
|
tickets: [6729]
|
||||||
|
|
||||||
|
- title: "Catalog generation: Fixed improper title display in catalog when title contains ':'. Added 'ondevice' field to CSV/XML catalog output (only with connected device|folder|iTunes). Added optional 'Series' section to generated catalogs with hyperlinks between books and series. Tweaks to catalog formatting."
|
||||||
|
|
||||||
|
- title: "Fix regression when checking database integrity with custom columns introduced in 0.7.17"
|
||||||
|
|
||||||
|
- title: "Sending email: Ignore geenric records when trying to resolve domain"
|
||||||
|
tickets: [6723]
|
||||||
|
|
||||||
|
- title: "Fix a bug where the open state of the splitter was not being saved on shutdown if the splitter had been closed at startup and was opened by dragging the center line"
|
||||||
|
|
||||||
|
- title: "MOBI Output: Fix bug generating index when chapter names contained non ASCII characters"
|
||||||
|
tickets: [6595]
|
||||||
|
|
||||||
|
- title: "PDF Input: Fix handling of more non ascii characters"
|
||||||
|
|
||||||
|
- title: "Content server: Triple AJAX timeout for main book list to 30 seconds"
|
||||||
|
|
||||||
|
- title: "Use ImageMagick instead of Qt to generate thumbnails when sending covers to device. Should fix corrupted nook covers on some windows installs"
|
||||||
|
|
||||||
|
- title: "FB2 Output: Improve creation of sections and fix a couple of bugs that could result in invalid output"
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "Journal Gazette"
|
||||||
|
author: cynvision
|
||||||
|
|
||||||
|
- title: "Milenio"
|
||||||
|
author: bmsleight
|
||||||
|
|
||||||
|
- title: "Winnipeg Free Press"
|
||||||
|
author: buyo
|
||||||
|
|
||||||
|
- title: "Buckmasters in the kitchen, The Walrus Magazine and Kansas City Star"
|
||||||
|
author: Tony Stegall
|
||||||
|
|
||||||
|
- title: "Europa Sur"
|
||||||
|
author: "Darko Miletic"
|
||||||
|
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- El Pais
|
||||||
|
- La Jornada
|
||||||
|
- nrcnext
|
||||||
|
- WSJ (free)
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.17
|
- version: 0.7.17
|
||||||
date: 2010-09-03
|
date: 2010-09-03
|
||||||
|
|
||||||
|
@ -1,58 +1,208 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) --><svg height="253.5" id="Layer_1" inkscape:version="0.40+cvs" sodipodi:docbase="F:\openclip\svg3" sodipodi:docname="Capitello modanatura moulure.svg" sodipodi:version="0.32" style="overflow:visible;enable-background:new 0 0 277.433 253.5;" version="1.0" viewBox="0 0 277.433 253.5" width="277.433" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xml="http://www.w3.org/XML/1998/namespace"><metadata><rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><cc:Work rdf:about=""><dc:title>Capitello modanatura modanature moulure moulures</dc:title><dc:description></dc:description><dc:subject><rdf:Bag><rdf:li>building</rdf:li></rdf:Bag></dc:subject><dc:publisher><cc:Agent rdf:about="http://www.openclipart.org"><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:publisher><dc:creator><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:creator><dc:rights><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:rights><dc:date></dc:date><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://web.resource.org/cc/PublicDomain"/><dc:language>en</dc:language></cc:Work><cc:License rdf:about="http://web.resource.org/cc/PublicDomain"><cc:permits rdf:resource="http://web.resource.org/cc/Reproduction"/><cc:permits rdf:resource="http://web.resource.org/cc/Distribution"/><cc:permits rdf:resource="http://web.resource.org/cc/DerivativeWorks"/></cc:License></rdf:RDF></metadata>
|
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
|
||||||
<defs id="defs56"></defs>
|
|
||||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="Layer_1" inkscape:cx="138.71651" inkscape:cy="126.75000" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="540" inkscape:window-width="640" inkscape:window-x="22" inkscape:window-y="22" inkscape:zoom="1.4911243" pagecolor="#ffffff"></sodipodi:namedview>
|
|
||||||
|
|
||||||
<g id="g3">
|
<svg
|
||||||
<path d="M269.987,16.446c11.514,10.547,7.726,33.31,1.663,45.651c-9.559,19.459-32.822,21.546-51.841,19.009 c-2.021,22.636-2.369,45.377-2.399,68.09c-0.015,10.891,0.025,21.781,0.025,32.672c0,5.381,0,10.762,0,16.143 c0,4.35,1.145,5.354,5.544,5.098c2.975-0.172,12.557-3.239,12.893,1.706c0.221,3.245-2.241,9.014-0.016,11.713 c1.997,2.421,5.807,3.181,8.67,3.993c3.729,1.058,7.776,3.027,11.667,3.265c2.984,0.182,1.435,5.441,1.368,7.535 c-0.148,4.68,0.078,9.268,0.319,13.928c0.24,4.638-1.359,4.189-5.543,4.594c-21.966,2.125-44.538,0.341-66.583,0.291 c-22.807-0.052-45.619,0.078-68.409,1.014c-22.082,0.906-44.168,0.835-66.252,1.523c-9,0.28-18.09,0.771-27.071,0.831 c-3.313,0.022-3.382-1.347-2.935-4.079c0.726-4.434,0.467-8.914,0.467-13.398c0-2.373-1.074-7.706,1.34-9.398 c2.938-2.061,7.151-2.742,10.401-4.265c9.135-4.276,6.441-7.791,6.591-16.34c0.073-4.137,5.426-1.096,8.395-1.154 c4.361-0.087,2.593-4.402,2.559-7.859c-0.064-6.333-0.206-12.665-0.382-18.995c-0.639-23.031-1.182-46.073-2.019-69.097 c-0.196-5.4-0.352-10.801-0.472-16.204c-0.058-2.589,1.452-11.419-2.132-11.275c-8.722,0.349-17.113-0.684-25.05-4.56 C-12.651,60.547-3.602,8.372,32.616,2.474c9.309-1.516,19.017-0.612,28.399-0.511c11.773,0.126,23.546,0.163,35.32,0.157 c23.119-0.013,46.238-0.115,69.354-0.556C185.921,1.177,206.175-0.019,226.411,0C242.655,0.015,259.596,3.013,269.987,16.446" id="path5"></path>
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
height="277.42999"
|
||||||
|
id="Layer_1"
|
||||||
|
inkscape:version="0.48.0 r9654"
|
||||||
|
sodipodi:docname="column.svg"
|
||||||
|
sodipodi:version="0.32"
|
||||||
|
style="overflow:visible"
|
||||||
|
version="1.0"
|
||||||
|
viewBox="0 0 277.42997 277.42999"
|
||||||
|
width="277.42999"
|
||||||
|
xml:space="preserve"><metadata
|
||||||
|
id="metadata3"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:title>Capitello modanatura modanature moulure moulures</dc:title><dc:description /><dc:subject><rdf:Bag><rdf:li>building</rdf:li></rdf:Bag></dc:subject><dc:publisher><cc:Agent
|
||||||
|
rdf:about="http://www.openclipart.org"><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:publisher><dc:creator><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:creator><dc:rights><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:rights><dc:date /><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||||
|
rdf:resource="http://web.resource.org/cc/PublicDomain" /><dc:language>en</dc:language></cc:Work><cc:License
|
||||||
|
rdf:about="http://web.resource.org/cc/PublicDomain"><cc:permits
|
||||||
|
rdf:resource="http://web.resource.org/cc/Reproduction" /><cc:permits
|
||||||
|
rdf:resource="http://web.resource.org/cc/Distribution" /><cc:permits
|
||||||
|
rdf:resource="http://web.resource.org/cc/DerivativeWorks" /></cc:License></rdf:RDF></metadata>
|
||||||
|
<defs
|
||||||
|
id="defs56" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
id="base"
|
||||||
|
inkscape:current-layer="Layer_1"
|
||||||
|
inkscape:cx="138.71675"
|
||||||
|
inkscape:cy="137.75228"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-height="997"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-x="-4"
|
||||||
|
inkscape:window-y="30"
|
||||||
|
inkscape:zoom="1.4911243"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="12"
|
||||||
|
fit-margin-bottom="11"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
|
||||||
<path d="M263.497,12.805c17.229,17.231,13.408,52.826-9.883,63.111c-11.111,4.906-29.468,6.652-39.829-0.784 c-4.768-3.42-10.636-8.188-13.193-13.588c-2.79-5.893-0.708-14.659,1.955-20.272c5.144-10.839,15.963-19.15,28.248-18.245 c13.067,0.963,21.35,10.229,19.214,23.444c-1.684,10.42-12.965,22.841-24.252,15.978c-4.984-3.032-8.286-9.521-6.396-15.307 c1.08-3.303,8.425-8.779,10.105-2.996c-3.617-0.933-3.468,5.562-2.88,7.464c1.694,5.481,8.471,5.282,12.069,1.689 c10.924-10.907-4.807-28.457-17.827-22.066c-15.255,7.487-15.464,27.982-1.153,36.475c13.494,8.008,31.806,1.371,39.313-11.699 c6.751-11.753,4.543-29.552-5.98-38.545c-6.412-5.48-16.133-5.78-24.13-6.6C217.514,9.7,206.07,9.611,194.659,9.88 c-45.654,1.077-91.252,2.635-136.93,2.865c-10.516,0.053-26.706-3.068-35.7,3.59C13.072,22.966,7.624,34.89,8.907,45.965 c2.465,21.27,26.852,34.439,45.764,23.854C63.05,65.13,69.272,56.151,68.154,46.26c-1.048-9.266-8.71-17.296-18.334-17.278 c-10.437,0.02-18.804,10.469-13.622,20.189c2.062,3.867,8.494,5.687,11.636,1.998c2.241-2.63,1.254-7.959-2.931-7.419 c4.833-5.223,11.914,1.513,11.38,7.142c-0.623,6.566-7.648,11.135-13.593,12.024c-15.159,2.267-23.777-11.625-20.532-25.271 c3.998-16.809,21.359-21.542,36.573-17.014c17.902,5.329,19.484,20.76,13.54,36.295C59.813,89.49,7.194,84.666,2.164,49.499 c-2.4-16.776,6.42-36.324,22.822-42.628c7.058-2.713,14.77-2.768,22.225-2.692C58.154,4.29,69.098,4.427,80.042,4.491 c32.615,0.19,65.303,0.147,97.908-0.728c16.363-0.439,32.709-1.273,49.082-0.806C239.347,3.309,254.146,3.507,263.497,12.805" id="path7" style="fill:#BFBFBF;"></path>
|
<g
|
||||||
|
id="g3"
|
||||||
|
transform="translate(2.4435697e-4,12.927759)">
|
||||||
|
<path
|
||||||
|
d="m 269.987,16.446 c 11.514,10.547 7.726,33.31 1.663,45.651 -9.559,19.459 -32.822,21.546 -51.841,19.009 -2.021,22.636 -2.369,45.377 -2.399,68.09 -0.015,10.891 0.025,21.781 0.025,32.672 0,5.381 0,10.762 0,16.143 0,4.35 1.145,5.354 5.544,5.098 2.975,-0.172 12.557,-3.239 12.893,1.706 0.221,3.245 -2.241,9.014 -0.016,11.713 1.997,2.421 5.807,3.181 8.67,3.993 3.729,1.058 7.776,3.027 11.667,3.265 2.984,0.182 1.435,5.441 1.368,7.535 -0.148,4.68 0.078,9.268 0.319,13.928 0.24,4.638 -1.359,4.189 -5.543,4.594 -21.966,2.125 -44.538,0.341 -66.583,0.291 -22.807,-0.052 -45.619,0.078 -68.409,1.014 -22.082,0.906 -44.168,0.835 -66.252,1.523 -9,0.28 -18.09,0.771 -27.071,0.831 -3.313,0.022 -3.382,-1.347 -2.935,-4.079 0.726,-4.434 0.467,-8.914 0.467,-13.398 0,-2.373 -1.074,-7.706 1.34,-9.398 2.938,-2.061 7.151,-2.742 10.401,-4.265 9.135,-4.276 6.441,-7.791 6.591,-16.34 0.073,-4.137 5.426,-1.096 8.395,-1.154 4.361,-0.087 2.593,-4.402 2.559,-7.859 -0.064,-6.333 -0.206,-12.665 -0.382,-18.995 -0.639,-23.031 -1.182,-46.073 -2.019,-69.097 -0.196,-5.4 -0.352,-10.801 -0.472,-16.204 C 47.909,90.124 49.419,81.294 45.835,81.438 37.113,81.787 28.722,80.754 20.785,76.878 -12.651,60.547 -3.602,8.372 32.616,2.474 41.925,0.958 51.633,1.862 61.015,1.963 72.788,2.089 84.561,2.126 96.335,2.12 119.454,2.107 142.573,2.005 165.689,1.564 185.921,1.177 206.175,-0.019 226.411,0 c 16.244,0.015 33.185,3.013 43.576,16.446"
|
||||||
|
id="path5"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M258.749,31.246c3.035,11.892-0.551,27.027-11.674,33.584c-10.348,6.101-28.37,4.505-33.847-7.551 c-4.545-10.002,2.436-25.982,14.791-24.727c6.887,0.7,10.927,5.838,10.588,12.623c-0.167,3.326-0.945,6.588-4.631,7.439 c-3.448,0.796-6.992-1.519-4.906-5.223c4.045,5.535,5.103-6.75-1.358-8.312c-6.246-1.511-10.79,3.854-10.992,9.564 c-0.493,13.941,14.36,23.351,26.353,15.28c11.628-7.826,13.93-28.233,3.285-37.841c-6.293-5.681-15.592-6.508-23.673-6.923 c-11.101-0.57-22.227,0.536-33.31,1.041c-22.333,1.019-44.661,2.149-66.996,3.14c-10.522,0.466-21.048,0.829-31.577,1.124 c-5.074,0.142-10.148,0.27-15.223,0.378c-5.138,0.109-9.622-3.251-14.107-5.271c-13.721-6.181-33.438-2.347-40.341,11.952 c-5.646,11.696-1.719,30.13,12.218,33.742c10.756,2.788,31.213-5.459,25.068-19.708c-2.384-5.527-9.835-7.564-14.897-4.53 c-1.53,0.917-5.698,8.159-1.748,6.881c1.042-0.337,1.828-1.721,3.053-1.544c2.031,0.294,1.867,1.657,0.453,2.672 c-6.196,4.449-9.433-4.01-7.865-8.951c2.52-7.944,13.175-10.17,19.805-6.519c13.573,7.474,8.818,25.74-2.296,32.597 c-14.444,8.912-34.788,3.045-41.537-12.791C7.439,39.422,15.236,19.093,30.86,15.584c9.506-2.136,20.457-0.466,30.129-0.384 c12.077,0.103,24.155,0.117,36.232,0.044c23.102-0.14,46.172-0.626,69.248-1.748c19.153-0.93,38.758-2.352,57.946-1.18 C237.174,13.095,255.954,15.957,258.749,31.246" id="path9" style="fill:#808080;"></path>
|
<path
|
||||||
|
d="m 263.497,12.805 c 17.229,17.231 13.408,52.826 -9.883,63.111 -11.111,4.906 -29.468,6.652 -39.829,-0.784 -4.768,-3.42 -10.636,-8.188 -13.193,-13.588 -2.79,-5.893 -0.708,-14.659 1.955,-20.272 5.144,-10.839 15.963,-19.15 28.248,-18.245 13.067,0.963 21.35,10.229 19.214,23.444 -1.684,10.42 -12.965,22.841 -24.252,15.978 -4.984,-3.032 -8.286,-9.521 -6.396,-15.307 1.08,-3.303 8.425,-8.779 10.105,-2.996 -3.617,-0.933 -3.468,5.562 -2.88,7.464 1.694,5.481 8.471,5.282 12.069,1.689 10.924,-10.907 -4.807,-28.457 -17.827,-22.066 -15.255,7.487 -15.464,27.982 -1.153,36.475 13.494,8.008 31.806,1.371 39.313,-11.699 6.751,-11.753 4.543,-29.552 -5.98,-38.545 -6.412,-5.48 -16.133,-5.78 -24.13,-6.6 C 217.514,9.7 206.07,9.611 194.659,9.88 149.005,10.957 103.407,12.515 57.729,12.745 47.213,12.798 31.023,9.677 22.029,16.335 13.072,22.966 7.624,34.89 8.907,45.965 11.372,67.235 35.759,80.404 54.671,69.819 63.05,65.13 69.272,56.151 68.154,46.26 67.106,36.994 59.444,28.964 49.82,28.982 39.383,29.002 31.016,39.451 36.198,49.171 c 2.062,3.867 8.494,5.687 11.636,1.998 2.241,-2.63 1.254,-7.959 -2.931,-7.419 4.833,-5.223 11.914,1.513 11.38,7.142 C 55.66,57.458 48.635,62.027 42.69,62.916 27.531,65.183 18.913,51.291 22.158,37.645 26.156,20.836 43.517,16.103 58.731,20.631 76.633,25.96 78.215,41.391 72.271,56.926 59.813,89.49 7.194,84.666 2.164,49.499 -0.236,32.723 8.584,13.175 24.986,6.871 32.044,4.158 39.756,4.103 47.211,4.179 58.154,4.29 69.098,4.427 80.042,4.491 c 32.615,0.19 65.303,0.147 97.908,-0.728 16.363,-0.439 32.709,-1.273 49.082,-0.806 12.315,0.352 27.114,0.55 36.465,9.848"
|
||||||
|
id="path7"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M216.96,22.065c-4.189,1.457-6.926,5.1-10.035,8.035c-3.926,3.706-7.758,3.477-12.894,3.057 c-13.75-1.124-27.989-0.009-41.786,0.081c-14.754,0.095-29.439,0.667-44.163,1.609c-7.355,0.471-14.71,0.961-22.071,1.337 c-3.388,0.173-7.721,1.855-8.424-1.65c-0.532-2.656-2.151-4.901-3.401-7.245c25.986-0.478,51.909-1.521,77.857-2.958 c12.748-0.706,25.585-1.691,38.35-1.883c5.152-0.078,10.293,0.221,15.423-0.352C209.098,21.729,213.818,20.556,216.96,22.065" id="path11" style="fill:#5E5E5E;"></path>
|
<path
|
||||||
|
d="m 258.749,31.246 c 3.035,11.892 -0.551,27.027 -11.674,33.584 -10.348,6.101 -28.37,4.505 -33.847,-7.551 -4.545,-10.002 2.436,-25.982 14.791,-24.727 6.887,0.7 10.927,5.838 10.588,12.623 -0.167,3.326 -0.945,6.588 -4.631,7.439 -3.448,0.796 -6.992,-1.519 -4.906,-5.223 4.045,5.535 5.103,-6.75 -1.358,-8.312 -6.246,-1.511 -10.79,3.854 -10.992,9.564 -0.493,13.941 14.36,23.351 26.353,15.28 11.628,-7.826 13.93,-28.233 3.285,-37.841 -6.293,-5.681 -15.592,-6.508 -23.673,-6.923 -11.101,-0.57 -22.227,0.536 -33.31,1.041 -22.333,1.019 -44.661,2.149 -66.996,3.14 -10.522,0.466 -21.048,0.829 -31.577,1.124 -5.074,0.142 -10.148,0.27 -15.223,0.378 C 70.441,24.951 65.957,21.591 61.472,19.571 47.751,13.39 28.034,17.224 21.131,31.523 15.485,43.219 19.412,61.653 33.349,65.265 44.105,68.053 64.562,59.806 58.417,45.557 56.033,40.03 48.582,37.993 43.52,41.027 c -1.53,0.917 -5.698,8.159 -1.748,6.881 1.042,-0.337 1.828,-1.721 3.053,-1.544 2.031,0.294 1.867,1.657 0.453,2.672 -6.196,4.449 -9.433,-4.01 -7.865,-8.951 2.52,-7.944 13.175,-10.17 19.805,-6.519 13.573,7.474 8.818,25.74 -2.296,32.597 C 40.478,75.075 20.134,69.208 13.385,53.372 7.439,39.422 15.236,19.093 30.86,15.584 40.366,13.448 51.317,15.118 60.989,15.2 c 12.077,0.103 24.155,0.117 36.232,0.044 23.102,-0.14 46.172,-0.626 69.248,-1.748 19.153,-0.93 38.758,-2.352 57.946,-1.18 12.759,0.779 31.539,3.641 34.334,18.93"
|
||||||
|
id="path9"
|
||||||
|
style="fill:#808080"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M202.556,35.282c-3.06,6.708-6.111,13.132-5.917,20.674c0.065,2.534,0.354,5.179,1.679,7.404 c1.462,2.459,4.185,3.993,4.92,6.893c1.413,5.582-0.519,12.133-0.678,17.817c-0.233,8.271-0.503,16.542-0.691,24.813 c-0.373,16.291-0.483,32.586-0.646,48.88c-0.081,8.149-0.175,16.3-0.321,24.449c-0.058,3.206,1.931,16.342-2.582,17.381 c-4.207,0.97-9.048,1.308-9.19-3.83c-0.206-7.431,0.059-14.804,0.187-22.237c0.296-17.278,0.149-34.562,0.442-51.841 c0.278-16.383,0.703-32.763,1.037-49.146c0.156-7.656,0.222-15.316,0.411-22.972c0.161-6.488-5.237-11.803-7.485-17.813 C189.794,33.726,196.337,35.792,202.556,35.282" id="path13" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 216.96,22.065 c -4.189,1.457 -6.926,5.1 -10.035,8.035 -3.926,3.706 -7.758,3.477 -12.894,3.057 -13.75,-1.124 -27.989,-0.009 -41.786,0.081 -14.754,0.095 -29.439,0.667 -44.163,1.609 -7.355,0.471 -14.71,0.961 -22.071,1.337 -3.388,0.173 -7.721,1.855 -8.424,-1.65 -0.532,-2.656 -2.151,-4.901 -3.401,-7.245 25.986,-0.478 51.909,-1.521 77.857,-2.958 12.748,-0.706 25.585,-1.691 38.35,-1.883 5.152,-0.078 10.293,0.221 15.423,-0.352 3.282,-0.367 8.002,-1.54 11.144,-0.031"
|
||||||
|
id="path11"
|
||||||
|
style="fill:#5e5e5e"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M149.926,35.678c-1.179,5.098-7.83,6.24-8.979,10.95c-1.648,6.761-0.583,15.063-0.745,22.007 c-0.406,17.411-0.884,34.822-0.57,52.239c0.308,17.057,0.717,34.095,0.717,51.156c0,8.01,0,16.02,0,24.028 c0,7.207,0.133,7.996-7.093,7.5c-6.914-0.475-3.747-15.828-3.729-20.809c0.059-16.925,0.119-33.849,0.178-50.773 c0.061-17.404-0.096-34.828,0.439-52.226c0.247-8.041,0.696-16.035,0.593-24.084c-0.11-8.56-2.827-11.851-8.037-18.326 C131.735,36.181,140.884,36.717,149.926,35.678" id="path15" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 202.556,35.282 c -3.06,6.708 -6.111,13.132 -5.917,20.674 0.065,2.534 0.354,5.179 1.679,7.404 1.462,2.459 4.185,3.993 4.92,6.893 1.413,5.582 -0.519,12.133 -0.678,17.817 -0.233,8.271 -0.503,16.542 -0.691,24.813 -0.373,16.291 -0.483,32.586 -0.646,48.88 -0.081,8.149 -0.175,16.3 -0.321,24.449 -0.058,3.206 1.931,16.342 -2.582,17.381 -4.207,0.97 -9.048,1.308 -9.19,-3.83 -0.206,-7.431 0.059,-14.804 0.187,-22.237 0.296,-17.278 0.149,-34.562 0.442,-51.841 0.278,-16.383 0.703,-32.763 1.037,-49.146 0.156,-7.656 0.222,-15.316 0.411,-22.972 0.161,-6.488 -5.237,-11.803 -7.485,-17.813 6.072,-2.028 12.615,0.038 18.834,-0.472"
|
||||||
|
id="path13"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M180.872,35.678c-0.979,4.869-5.544,7.579-8.596,11.063c-4.358,4.976-2.346,12.479-2.414,18.523 c-0.192,17.04-1.516,33.989-1.317,51.069c0.191,16.582,0.597,33.16,0.705,49.742c0.054,8.282,0.034,16.565-0.123,24.847 c-0.069,3.652,0.211,7.544-0.281,11.169c-0.41,3.021-5.537,2.033-7.736,2.147c-1.793,0.094-1.443-80.056-1.404-87.238 c0.085-16.094,0.108-32.21,0.636-48.297c0.19-5.829,0.481-11.56,1.162-17.345c0.758-6.434-3.308-10.796-7.856-15.206 c3.301-1.833,8.131-0.664,11.682-0.447C170.432,36.017,175.776,36.123,180.872,35.678" id="path17" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 149.926,35.678 c -1.179,5.098 -7.83,6.24 -8.979,10.95 -1.648,6.761 -0.583,15.063 -0.745,22.007 -0.406,17.411 -0.884,34.822 -0.57,52.239 0.308,17.057 0.717,34.095 0.717,51.156 0,8.01 0,16.02 0,24.028 0,7.207 0.133,7.996 -7.093,7.5 -6.914,-0.475 -3.747,-15.828 -3.729,-20.809 0.059,-16.925 0.119,-33.849 0.178,-50.773 0.061,-17.404 -0.096,-34.828 0.439,-52.226 0.247,-8.041 0.696,-16.035 0.593,-24.084 -0.11,-8.56 -2.827,-11.851 -8.037,-18.326 9.035,-1.159 18.184,-0.623 27.226,-1.662"
|
||||||
|
id="path15"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M118.269,37.735c-1.283,2.154-2.983,4.106-4.986,5.619c-1.565,1.183-3.853,1.641-4.952,3.395 c-1.47,2.347,0.599,5.242,0.599,7.685c0,4.216-0.604,8.428-0.717,12.647c-0.438,16.338-0.463,32.68-0.272,49.021 c0.194,16.553,0.592,33.098,1.683,49.619c0.389,5.881,5.109,37.916-2.616,38.886c-3.479,0.438-8.817,1.344-9.113-3.157 c-0.499-7.577-0.5-15.216-0.751-22.806c-0.55-16.556-1.148-33.111-1.437-49.675c-0.284-16.31-0.281-32.628,0.23-48.934 c0.255-8.16,0.637-16.315,1.172-24.461c0.475-7.23-3.972-10.534-5.669-17.048C100.385,38.349,109.315,37.358,118.269,37.735" id="path19" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 180.872,35.678 c -0.979,4.869 -5.544,7.579 -8.596,11.063 -4.358,4.976 -2.346,12.479 -2.414,18.523 -0.192,17.04 -1.516,33.989 -1.317,51.069 0.191,16.582 0.597,33.16 0.705,49.742 0.054,8.282 0.034,16.565 -0.123,24.847 -0.069,3.652 0.211,7.544 -0.281,11.169 -0.41,3.021 -5.537,2.033 -7.736,2.147 -1.793,0.094 -1.443,-80.056 -1.404,-87.238 0.085,-16.094 0.108,-32.21 0.636,-48.297 0.19,-5.829 0.481,-11.56 1.162,-17.345 0.758,-6.434 -3.308,-10.796 -7.856,-15.206 3.301,-1.833 8.131,-0.664 11.682,-0.447 5.102,0.312 10.446,0.418 15.542,-0.027"
|
||||||
|
id="path17"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M158.395,46.125c-0.533,31.886-1.188,63.758-1.188,95.65c0,15.317,0,30.634,0,45.95 c0,4.617,0.757,9.24,0.664,13.833c-0.068,3.371-8.199,3.735-8.344,0.423c-1.413-32.226-1.844-64.548-1.556-96.803 c0.143-15.907,0.771-31.809,1.218-47.709c0.135-4.806-1.466-13.862,1.021-18.2C152.52,35.243,157.134,45.118,158.395,46.125" id="path21" style="fill:#969696;"></path>
|
<path
|
||||||
|
d="m 118.269,37.735 c -1.283,2.154 -2.983,4.106 -4.986,5.619 -1.565,1.183 -3.853,1.641 -4.952,3.395 -1.47,2.347 0.599,5.242 0.599,7.685 0,4.216 -0.604,8.428 -0.717,12.647 -0.438,16.338 -0.463,32.68 -0.272,49.021 0.194,16.553 0.592,33.098 1.683,49.619 0.389,5.881 5.109,37.916 -2.616,38.886 -3.479,0.438 -8.817,1.344 -9.113,-3.157 -0.499,-7.577 -0.5,-15.216 -0.751,-22.806 -0.55,-16.556 -1.148,-33.111 -1.437,-49.675 -0.284,-16.31 -0.281,-32.628 0.23,-48.934 0.255,-8.16 0.637,-16.315 1.172,-24.461 0.475,-7.23 -3.972,-10.534 -5.669,-17.048 8.945,-0.177 17.875,-1.168 26.829,-0.791"
|
||||||
|
id="path19"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M86.137,40.11c-1.802,4.444-6.351,6.834-8.785,10.843c0.118-3.233-1.075-7.139,0.521-10.162 C78.82,38.997,86.6,37.518,86.137,40.11" id="path23" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 158.395,46.125 c -0.533,31.886 -1.188,63.758 -1.188,95.65 0,15.317 0,30.634 0,45.95 0,4.617 0.757,9.24 0.664,13.833 -0.068,3.371 -8.199,3.735 -8.344,0.423 -1.413,-32.226 -1.844,-64.548 -1.556,-96.803 0.143,-15.907 0.771,-31.809 1.218,-47.709 0.135,-4.806 -1.466,-13.862 1.021,-18.2 2.31,-4.026 6.924,5.849 8.185,6.856"
|
||||||
|
id="path21"
|
||||||
|
style="fill:#969696"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M127.924,47.708c-0.864,29.137-0.867,58.291-0.963,87.438c-0.048,14.845-0.097,29.689-0.146,44.534 c-0.023,6.938,0.175,13.919-0.089,20.853c-0.133,3.509-2.079,3.183-5.074,3.256c-3.186,0.077-1.998-5.488-2.133-8.231 c-0.182-3.69-0.362-7.381-0.538-11.071c-0.351-7.383-0.684-14.767-0.974-22.152c-0.581-14.771-0.985-29.548-1.057-44.33 c-0.071-14.73,0.919-29.391,1.103-44.1c0.088-7.041-0.18-14.086-0.18-21.128c0-5.268-1.026-9.582,2.85-13.457 C123.231,42.02,125.295,45.115,127.924,47.708" id="path25" style="fill:#969696;"></path>
|
<path
|
||||||
|
d="M 86.137,40.11 C 84.335,44.554 79.786,46.944 77.352,50.953 77.47,47.72 76.277,43.814 77.873,40.791 78.82,38.997 86.6,37.518 86.137,40.11"
|
||||||
|
id="path23"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M188.469,83.005c-0.868,27.782-1.484,55.573-1.786,83.367c-0.124,11.389,0.632,23.096-0.191,34.44 c-0.213,2.944-4.001,4.863-6.649,2.729c-2.821-2.274-2.137-11.711-2.193-14.737c-0.244-12.991,0.978-25.921,0.54-38.927 c-0.438-12.993-0.009-25.956,0.292-38.947c0.301-13.003,0.601-26.007,0.907-39.01c0.25-10.571-2.265-22.762,2.749-32.602 C192.517,51.294,189.273,68.576,188.469,83.005" id="path27" style="fill:#969696;"></path>
|
<path
|
||||||
|
d="m 127.924,47.708 c -0.864,29.137 -0.867,58.291 -0.963,87.438 -0.048,14.845 -0.097,29.689 -0.146,44.534 -0.023,6.938 0.175,13.919 -0.089,20.853 -0.133,3.509 -2.079,3.183 -5.074,3.256 -3.186,0.077 -1.998,-5.488 -2.133,-8.231 -0.182,-3.69 -0.362,-7.381 -0.538,-11.071 -0.351,-7.383 -0.684,-14.767 -0.974,-22.152 -0.581,-14.771 -0.985,-29.548 -1.057,-44.33 -0.071,-14.73 0.919,-29.391 1.103,-44.1 0.088,-7.041 -0.18,-14.086 -0.18,-21.128 0,-5.268 -1.026,-9.582 2.85,-13.457 2.508,2.7 4.572,5.795 7.201,8.388"
|
||||||
|
id="path25"
|
||||||
|
style="fill:#969696"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M95.001,51.744c-1.75,28.572-2.194,57.212-1.738,85.831c0.216,13.561,0.746,27.112,1.23,40.665 c0.248,6.934,0.492,13.867,0.735,20.801c0.229,6.533-2.363,4.685-7.43,5.211c-1.578-28.564-2.779-57.147-2.978-85.757 c-0.093-13.326-0.059-26.656,0.183-39.98c0.121-6.69,0.447-13.376,0.565-20.066c0.106-6.027,0.059-12.19,3.418-17.468 C90.991,44.568,92.996,48.156,95.001,51.744" id="path29" style="fill:#969696;"></path>
|
<path
|
||||||
|
d="m 188.469,83.005 c -0.868,27.782 -1.484,55.573 -1.786,83.367 -0.124,11.389 0.632,23.096 -0.191,34.44 -0.213,2.944 -4.001,4.863 -6.649,2.729 -2.821,-2.274 -2.137,-11.711 -2.193,-14.737 -0.244,-12.991 0.978,-25.921 0.54,-38.927 -0.438,-12.993 -0.009,-25.956 0.292,-38.947 0.301,-13.003 0.601,-26.007 0.907,-39.01 0.25,-10.571 -2.265,-22.762 2.749,-32.602 10.379,11.976 7.135,29.258 6.331,43.687"
|
||||||
|
id="path27"
|
||||||
|
style="fill:#969696"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M145.177,87.437c0.988,25.058,1.155,50.139,1.183,75.214c0.013,11.358,0.005,22.718,0.005,34.077 c0,1.782,0.651,4.979-0.428,6.579c-2.152,3.19-2.427-2.293-2.446-2.981c-0.175-6.101-0.312-12.202-0.37-18.306 c-0.233-24.164-0.411-48.273-0.979-72.433c-0.275-11.732,0.069-23.448,0.522-35.172c0.243-6.293,0.513-12.586,0.71-18.882 c0.128-4.116-0.891-8.615,2.99-11.388C146.64,58.581,146.323,73.043,145.177,87.437" id="path31" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 95.001,51.744 c -1.75,28.572 -2.194,57.212 -1.738,85.831 0.216,13.561 0.746,27.112 1.23,40.665 0.248,6.934 0.492,13.867 0.735,20.801 0.229,6.533 -2.363,4.685 -7.43,5.211 -1.578,-28.564 -2.779,-57.147 -2.978,-85.757 -0.093,-13.326 -0.059,-26.656 0.183,-39.98 0.121,-6.69 0.447,-13.376 0.565,-20.066 0.106,-6.027 0.059,-12.19 3.418,-17.468 2.005,3.587 4.01,7.175 6.015,10.763"
|
||||||
|
id="path29"
|
||||||
|
style="fill:#969696"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M115.42,50.953c-1.174,28.427-1.397,56.849-0.762,85.292c0.306,13.686,0.66,27.37,1.144,41.051 c0.247,6.976,0.493,13.949,0.709,20.926c0.113,3.651,0.938,5.553-3.386,6.031c-0.947-29.533-2.22-59.063-2.748-88.608 c-0.248-13.869-0.502-27.894,0.314-41.748c0.369-6.252,1.427-12.552,0.738-18.821c-0.473-4.301-1.087-7.463,3.278-9.742 C114.97,47.199,115.34,49.065,115.42,50.953" id="path33" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 145.177,87.437 c 0.988,25.058 1.155,50.139 1.183,75.214 0.013,11.358 0.005,22.718 0.005,34.077 0,1.782 0.651,4.979 -0.428,6.579 -2.152,3.19 -2.427,-2.293 -2.446,-2.981 -0.175,-6.101 -0.312,-12.202 -0.37,-18.306 -0.233,-24.164 -0.411,-48.273 -0.979,-72.433 -0.275,-11.732 0.069,-23.448 0.522,-35.172 0.243,-6.293 0.513,-12.586 0.71,-18.882 0.128,-4.116 -0.891,-8.615 2.99,-11.388 0.276,14.436 -0.041,28.898 -1.187,43.292"
|
||||||
|
id="path31"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M177.626,47.312c-1.061,29.439-2.265,58.83-1.941,88.295c0.16,14.534-0.268,29.064-0.391,43.597 c-0.057,6.632-0.034,13.265,0.191,19.893c0.047,1.372,0.716,3.423-0.136,4.674c-0.996,1.462-2.972,0.332-3.359-0.947 c-1.224-4.041-0.131-9.819-0.212-13.987c-0.144-7.308-0.277-14.616-0.389-21.924c-0.224-14.616-0.359-29.235-0.309-43.853 c0.051-14.634,0.195-29.277,0.585-43.906c0.269-10.124-2.24-25.542,5.248-33.028C177.136,46.464,177.512,46.979,177.626,47.312" id="path35" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 115.42,50.953 c -1.174,28.427 -1.397,56.849 -0.762,85.292 0.306,13.686 0.66,27.37 1.144,41.051 0.247,6.976 0.493,13.949 0.709,20.926 0.113,3.651 0.938,5.553 -3.386,6.031 -0.947,-29.533 -2.22,-59.063 -2.748,-88.608 -0.248,-13.869 -0.502,-27.894 0.314,-41.748 0.369,-6.252 1.427,-12.552 0.738,-18.821 -0.473,-4.301 -1.087,-7.463 3.278,-9.742 0.263,1.865 0.633,3.731 0.713,5.619"
|
||||||
|
id="path33"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M82.18,85.063c0.151,25.021-0.078,50.047,0.583,75.063c0.338,12.787,1.207,25.561,1.396,38.35 c0.032,2.203,1.505,6.316-2.127,5.501c-1.73-0.388-1.261-14.663-1.35-16.759c-1.034-24.321-2.365-48.646-2.663-72.991 c-0.149-12.221-0.477-24.412,0.259-36.618c0.576-9.556-1.54-21.67,4.694-29.426C82.115,60.46,82.981,72.783,82.18,85.063" id="path37" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 177.626,47.312 c -1.061,29.439 -2.265,58.83 -1.941,88.295 0.16,14.534 -0.268,29.064 -0.391,43.597 -0.057,6.632 -0.034,13.265 0.191,19.893 0.047,1.372 0.716,3.423 -0.136,4.674 -0.996,1.462 -2.972,0.332 -3.359,-0.947 -1.224,-4.041 -0.131,-9.819 -0.212,-13.987 -0.144,-7.308 -0.277,-14.616 -0.389,-21.924 -0.224,-14.616 -0.359,-29.235 -0.309,-43.853 0.051,-14.634 0.195,-29.277 0.585,-43.906 0.269,-10.124 -2.24,-25.542 5.248,-33.028 0.223,0.338 0.599,0.853 0.713,1.186"
|
||||||
|
id="path35"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M76.56,148.457c0.157,10.251,0.532,20.496,0.961,30.738c0.296,7.057,2.459,16.297,1.102,23.311 c-0.727,3.755-9.991,3.729-10.081-0.096c-0.255-10.852-0.904-21.675-1.379-32.518c-0.936-21.354-1.054-42.732-1.057-64.104 c-0.001-9.405-0.544-18.909-0.314-28.295c0.123-5.044,2.847-7.453,5.167-11.591c2.299-4.102,3.548-8.677,5.603-12.893 C75.467,84.813,75.458,116.654,76.56,148.457" id="path39" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 82.18,85.063 c 0.151,25.021 -0.078,50.047 0.583,75.063 0.338,12.787 1.207,25.561 1.396,38.35 0.032,2.203 1.505,6.316 -2.127,5.501 -1.73,-0.388 -1.261,-14.663 -1.35,-16.759 -1.034,-24.321 -2.365,-48.646 -2.663,-72.991 -0.149,-12.221 -0.477,-24.412 0.259,-36.618 0.576,-9.556 -1.54,-21.67 4.694,-29.426 -0.857,12.277 0.009,24.6 -0.792,36.88"
|
||||||
|
id="path37"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M207.781,73.825c0.317,21.825-0.792,43.493-0.792,65.278c0,10.738,0,21.478,0,32.216 c0,5.481,0,10.963,0,16.445c0,2.65,1.686,12.848-0.612,14.655c-4.082,3.21-2.532-8.896-2.516-10.782 c0.052-5.517,0.092-11.034,0.129-16.552c0.074-11.035,0.133-22.071,0.23-33.106c0.202-22.987,0.569-45.978,1.582-68.945 C206.461,73.297,207.122,73.561,207.781,73.825" id="path41" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 76.56,148.457 c 0.157,10.251 0.532,20.496 0.961,30.738 0.296,7.057 2.459,16.297 1.102,23.311 -0.727,3.755 -9.991,3.729 -10.081,-0.096 -0.255,-10.852 -0.904,-21.675 -1.379,-32.518 -0.936,-21.354 -1.054,-42.732 -1.057,-64.104 -10e-4,-9.405 -0.544,-18.909 -0.314,-28.295 0.123,-5.044 2.847,-7.453 5.167,-11.591 2.299,-4.102 3.548,-8.677 5.603,-12.893 -1.095,31.804 -1.104,63.645 -0.002,95.448"
|
||||||
|
id="path39"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M63.66,154.472c0.783,16.721,1.787,33.437,2.058,50.177c-2.401-0.132-4.802-0.265-7.203-0.396 c4.111-2.813,0.355-17.984,0.207-22.342c-0.486-14.264-0.922-28.527-1.294-42.794c-0.371-14.189-1.13-28.355-1.666-42.537 c-0.121-3.195-2.525-15.268-0.633-17.566c1.9-2.309,5.626-3.449,8.214-4.792c0.066,13.375,0.132,26.75,0.198,40.125 C63.607,127.713,64.891,141.124,63.66,154.472" id="path43" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 207.781,73.825 c 0.317,21.825 -0.792,43.493 -0.792,65.278 0,10.738 0,21.478 0,32.216 0,5.481 0,10.963 0,16.445 0,2.65 1.686,12.848 -0.612,14.655 -4.082,3.21 -2.532,-8.896 -2.516,-10.782 0.052,-5.517 0.092,-11.034 0.129,-16.552 0.074,-11.035 0.133,-22.071 0.23,-33.106 0.202,-22.987 0.569,-45.978 1.582,-68.945 0.659,0.263 1.32,0.527 1.979,0.791"
|
||||||
|
id="path41"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M216.96,80.235c0.446,21.664-0.711,43.313-1.313,64.962c-0.296,10.651-0.357,21.305-0.45,31.959 c-0.03,3.503,1.627,25.193-1.97,25.515c-5.911,0.527-3.371-20.418-3.351-24.031c0.063-10.85,0.132-21.698,0.181-32.548 c0.104-23.14,0.117-46.282,0.571-69.418C213.076,77.254,214.519,79.697,216.96,80.235" id="path45" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 63.66,154.472 c 0.783,16.721 1.787,33.437 2.058,50.177 -2.401,-0.132 -4.802,-0.265 -7.203,-0.396 4.111,-2.813 0.355,-17.984 0.207,-22.342 C 58.236,167.647 57.8,153.384 57.428,139.117 57.057,124.928 56.298,110.762 55.762,96.58 55.641,93.385 53.237,81.312 55.129,79.014 c 1.9,-2.309 5.626,-3.449 8.214,-4.792 0.066,13.375 0.132,26.75 0.198,40.125 0.066,13.366 1.35,26.777 0.119,40.125"
|
||||||
|
id="path43"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M52.421,79.84c0.88,22.406,1.916,44.805,2.742,67.213c0.432,11.725,0.802,23.45,1.042,35.18 c0.088,4.317-2.453,18.714,1.124,22.02c-4.652,0-3.771-2.246-3.999-6.369c-0.426-7.703-0.767-15.425-0.831-23.14 c-0.122-14.887-0.063-29.809-0.743-44.682C51.631,127.329,46.842,79.502,52.421,79.84" id="path47" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 216.96,80.235 c 0.446,21.664 -0.711,43.313 -1.313,64.962 -0.296,10.651 -0.357,21.305 -0.45,31.959 -0.03,3.503 1.627,25.193 -1.97,25.515 -5.911,0.527 -3.371,-20.418 -3.351,-24.031 0.063,-10.85 0.132,-21.698 0.181,-32.548 0.104,-23.14 0.117,-46.282 0.571,-69.418 2.448,0.58 3.891,3.023 6.332,3.561"
|
||||||
|
id="path45"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M233.027,205.044c2.539,9.07-2.377,10.524-9.861,11.079c-9.534,0.707-19.151,0.729-28.707,0.77 c-20.313,0.088-40.627,0.037-60.94,0.133c-19.374,0.091-38.575,1.29-57.922,1.899c-9.106,0.287-18.38,0.342-27.438-0.75 c-3.971-0.479-5.018,0.146-5.204-3.817c-0.107-2.286-0.019-4.574-0.109-6.859c31.892,0,63.831-1.94,95.708-1.301 c16.213,0.325,32.394,0.372,48.607,0.431C202.487,206.683,217.678,205.044,233.027,205.044" id="path49" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 52.421,79.84 c 0.88,22.406 1.916,44.805 2.742,67.213 0.432,11.725 0.802,23.45 1.042,35.18 0.088,4.317 -2.453,18.714 1.124,22.02 -4.652,0 -3.771,-2.246 -3.999,-6.369 -0.426,-7.703 -0.767,-15.425 -0.831,-23.14 -0.122,-14.887 -0.063,-29.809 -0.743,-44.682 -0.125,-2.733 -4.914,-50.56 0.665,-50.222"
|
||||||
|
id="path47"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M249.092,224.275c-37.66,2.193-75.668-0.496-113.257,2.421c-36.526,2.834-73.583,2.146-110.243,1.22 c14.839-9.382,31.379-6.451,47.913-6.075c20.089,0.457,40.228-2.287,60.35-2.391c20.478-0.105,40.953,0.454,61.432,0.33 c10.018-0.061,19.98-0.497,29.972-1.229C234.292,217.891,240.738,221.712,249.092,224.275" id="path51" style="fill:#E3E3E3;"></path>
|
<path
|
||||||
|
d="m 233.027,205.044 c 2.539,9.07 -2.377,10.524 -9.861,11.079 -9.534,0.707 -19.151,0.729 -28.707,0.77 -20.313,0.088 -40.627,0.037 -60.94,0.133 -19.374,0.091 -38.575,1.29 -57.922,1.899 -9.106,0.287 -18.38,0.342 -27.438,-0.75 -3.971,-0.479 -5.018,0.146 -5.204,-3.817 -0.107,-2.286 -0.019,-4.574 -0.109,-6.859 31.892,0 63.831,-1.94 95.708,-1.301 16.213,0.325 32.394,0.372 48.607,0.431 15.326,0.054 30.517,-1.585 45.866,-1.585"
|
||||||
|
id="path49"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
<path d="M255.108,245.961c-1.281,0.09-1.54,1.658-2.793,1.662c-2.189,0.008-4.379,0.015-6.568,0.022 c-5.926,0.021-11.852,0.04-17.777,0.061c-11.854,0.041-23.707,0.08-35.561,0.121c-23.952,0.081-47.894,0.144-71.839,0.773 c-23.992,0.63-47.979,1.343-71.98,1.604c-6.661,0.072-13.326,0.174-19.988,0.186c-3.327,0.005-4.571,1.054-4.644-2.605 c-0.115-5.806-0.229-11.61-0.344-17.415c23.308,2.299,47.056,0.97,70.449,1.107c23.82,0.141,47.644-2.438,71.469-3.03 c23.765-0.591,47.532-0.933,71.303-0.928c3.688,0.001,17.249-3.522,18.395,1.103C256.617,234.223,255.108,240.288,255.108,245.961" id="path53" style="fill:#BFBFBF;"></path>
|
<path
|
||||||
|
d="m 249.092,224.275 c -37.66,2.193 -75.668,-0.496 -113.257,2.421 -36.526,2.834 -73.583,2.146 -110.243,1.22 14.839,-9.382 31.379,-6.451 47.913,-6.075 20.089,0.457 40.228,-2.287 60.35,-2.391 20.478,-0.105 40.953,0.454 61.432,0.33 10.018,-0.061 19.98,-0.497 29.972,-1.229 9.033,-0.66 15.479,3.161 23.833,5.724"
|
||||||
|
id="path51"
|
||||||
|
style="fill:#e3e3e3"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="m 255.108,245.961 c -1.281,0.09 -1.54,1.658 -2.793,1.662 -2.189,0.008 -4.379,0.015 -6.568,0.022 -5.926,0.021 -11.852,0.04 -17.777,0.061 -11.854,0.041 -23.707,0.08 -35.561,0.121 -23.952,0.081 -47.894,0.144 -71.839,0.773 -23.992,0.63 -47.979,1.343 -71.98,1.604 -6.661,0.072 -13.326,0.174 -19.988,0.186 -3.327,0.005 -4.571,1.054 -4.644,-2.605 -0.115,-5.806 -0.229,-11.61 -0.344,-17.415 23.308,2.299 47.056,0.97 70.449,1.107 23.82,0.141 47.644,-2.438 71.469,-3.03 23.765,-0.591 47.532,-0.933 71.303,-0.928 3.688,0.001 17.249,-3.522 18.395,1.103 1.387,5.601 -0.122,11.666 -0.122,17.339"
|
||||||
|
id="path53"
|
||||||
|
style="fill:#bfbfbf"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
5181
imgsrc/drawer.svg
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 297 KiB |
@ -6,7 +6,7 @@ p.title {
|
|||||||
text-align:center;
|
text-align:center;
|
||||||
font-style:italic;
|
font-style:italic;
|
||||||
font-size:xx-large;
|
font-size:xx-large;
|
||||||
border-bottom: solid black 4px;
|
border-bottom: solid black 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.author {
|
p.author {
|
||||||
@ -17,6 +17,15 @@ p.author {
|
|||||||
font-size:large;
|
font-size:large;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.author_index {
|
||||||
|
font-size:large;
|
||||||
|
font-weight:bold;
|
||||||
|
text-align:left;
|
||||||
|
margin-top:0px;
|
||||||
|
margin-bottom:-2px;
|
||||||
|
text-indent: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
p.tags {
|
p.tags {
|
||||||
margin-top:0em;
|
margin-top:0em;
|
||||||
margin-bottom:0em;
|
margin-bottom:0em;
|
||||||
@ -47,19 +56,12 @@ p.letter_index {
|
|||||||
margin-bottom:0px;
|
margin-bottom:0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.author_index {
|
|
||||||
font-size:large;
|
|
||||||
text-align:left;
|
|
||||||
margin-top:0px;
|
|
||||||
margin-bottom:0px;
|
|
||||||
text-indent: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.series {
|
p.series {
|
||||||
text-align: left;
|
font-style:italic;
|
||||||
margin-top:0px;
|
margin-top:2px;
|
||||||
margin-bottom:0px;
|
margin-bottom:0px;
|
||||||
margin-left:2em;
|
margin-left:2em;
|
||||||
|
text-align:left;
|
||||||
text-indent:-2em;
|
text-indent:-2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,11 +89,13 @@ p.date_read {
|
|||||||
text-indent:-6em;
|
text-indent:-6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr.series_divider {
|
hr.description_divider {
|
||||||
width:50%;
|
width:90%;
|
||||||
margin-left:1em;
|
margin-left:5%;
|
||||||
margin-top:0em;
|
border-top: solid white 0px;
|
||||||
margin-bottom:0em;
|
border-right: solid white 0px;
|
||||||
|
border-bottom: solid black 1px;
|
||||||
|
border-left: solid white 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr.annotations_divider {
|
hr.annotations_divider {
|
||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 28 KiB |
BIN
resources/images/news/journalgazette.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
resources/images/news/kstar.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
resources/images/news/walrusmag.png
Normal file
After Width: | Height: | Size: 569 B |
Before Width: | Height: | Size: 161 B After Width: | Height: | Size: 13 KiB |
42
resources/recipes/buckmasters.recipe
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from calibre.ebooks.BeautifulSoup import Tag
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1282101454(BasicNewsRecipe):
|
||||||
|
title = 'BuckMasters In The Kitchen'
|
||||||
|
language = 'en'
|
||||||
|
__author__ = 'TonytheBookworm & Starson17'
|
||||||
|
description = 'Learn how to cook all those outdoor varments'
|
||||||
|
publisher = 'BuckMasters.com'
|
||||||
|
category = 'food,cooking,recipes'
|
||||||
|
oldest_article = 365
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
conversion_options = {'linearize_tables' : True}
|
||||||
|
masthead_url = 'http://www.buckmasters.com/Portals/_default/Skins/BM_10/images/header_bg.jpg'
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='table', attrs={'class':['containermaster_black']})
|
||||||
|
]
|
||||||
|
remove_tags_after = [dict(name='div', attrs={'align':['left']})]
|
||||||
|
feeds = [
|
||||||
|
('Recipes', 'http://www.buckmasters.com/DesktopModules/DnnForge%20-%20NewsArticles/RSS.aspx?TabID=292&ModuleID=658&MaxCount=25'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
item = soup.find('a', attrs={'class':['MenuTopSelected']})
|
||||||
|
if item:
|
||||||
|
item.parent.extract()
|
||||||
|
for img_tag in soup.findAll('img'):
|
||||||
|
parent_tag = img_tag.parent
|
||||||
|
if parent_tag.name == 'a':
|
||||||
|
new_tag = Tag(soup,'p')
|
||||||
|
new_tag.insert(0,img_tag)
|
||||||
|
parent_tag.replaceWith(new_tag)
|
||||||
|
elif parent_tag.name == 'p':
|
||||||
|
if not self.tag_to_string(parent_tag) == '':
|
||||||
|
new_div = Tag(soup,'div')
|
||||||
|
new_tag = Tag(soup,'p')
|
||||||
|
new_tag.insert(0,img_tag)
|
||||||
|
parent_tag.replaceWith(new_div)
|
||||||
|
new_div.insert(0,new_tag)
|
||||||
|
new_div.insert(1,parent_tag)
|
||||||
|
return soup
|
||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__author__ = 'Lorenzo Vigentini, based on earlier version by Kovid Goyal'
|
__author__ = 'Jordi Balcells, based on an earlier version by Lorenzo Vigentini & Kovid Goyal'
|
||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
description = 'Main daily newspaper from Spain - v1.02 (10, January 2010)'
|
description = 'Main daily newspaper from Spain - v1.03 (03, September 2010)'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -12,12 +12,12 @@ elpais.es
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class ElPais(BasicNewsRecipe):
|
class ElPais(BasicNewsRecipe):
|
||||||
__author__ = 'Kovid Goyal & Lorenzo Vigentini'
|
__author__ = 'Kovid Goyal & Lorenzo Vigentini & Jordi Balcells'
|
||||||
description = 'Main daily newspaper from Spain'
|
description = 'Main daily newspaper from Spain'
|
||||||
|
|
||||||
cover_url = 'http://www.elpais.com/im/tit_logo_global.gif'
|
cover_url = 'http://www.elpais.com/im/tit_logo_global.gif'
|
||||||
title = u'El Pais'
|
title = u'El Pais'
|
||||||
publisher = 'Ediciones El Pais SL'
|
publisher = u'Ediciones El Pa\xeds SL'
|
||||||
category = 'News, politics, culture, economy, general interest'
|
category = 'News, politics, culture, economy, general interest'
|
||||||
|
|
||||||
language = 'es'
|
language = 'es'
|
||||||
@ -32,7 +32,8 @@ class ElPais(BasicNewsRecipe):
|
|||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia','cabecera_noticia_reportaje','contenido_noticia','caja_despiece','presentacion']})]
|
keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia','cabecera_noticia_reportaje','cabecera_noticia_opinion','contenido_noticia','caja_despiece','presentacion']})]
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
p{style:normal size:12 serif}
|
p{style:normal size:12 serif}
|
||||||
|
|
||||||
@ -40,25 +41,29 @@ class ElPais(BasicNewsRecipe):
|
|||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':['zona_superior','pie_enlaces_inferiores','contorno_f','ampliar']}),
|
dict(name='div', attrs={'class':['zona_superior','pie_enlaces_inferiores','contorno_f','ampliar']}),
|
||||||
dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos','info_complementa','info_relacionada']}),
|
dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos','info_complementa','info_relacionada','buscador_m','nav_ant_sig']}),
|
||||||
dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas']})
|
dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas','calendar']}),
|
||||||
|
dict(name='p', attrs={'class':'nav_meses'}),
|
||||||
|
dict(attrs={'class':['enlaces_m','miniaturas_m']})
|
||||||
]
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Titulares de portada', u'http://www.elpais.com/rss/feed.html?feedId=1022'),
|
(u'Titulares de portada', u'http://www.elpais.com/rss/feed.html?feedId=1022'),
|
||||||
(u'Internacional', u'http://www.elpais.com/rss/feed.html?feedId=1001'),
|
(u'Internacional', u'http://www.elpais.com/rss/feed.html?feedId=1001'),
|
||||||
(u'Espana', u'http://www.elpais.com/rss/feed.html?feedId=1002'),
|
(u'Espa\xf1a', u'http://www.elpais.com/rss/feed.html?feedId=1002'),
|
||||||
(u'Deportes', u'http://www.elpais.com/rss/feed.html?feedId=1007'),
|
(u'Deportes', u'http://www.elpais.com/rss/feed.html?feedId=1007'),
|
||||||
(u'Economia', u'http://www.elpais.com/rss/feed.html?feedId=1006'),
|
(u'Econom\xeda', u'http://www.elpais.com/rss/feed.html?feedId=1006'),
|
||||||
(u'Politica', u'http://www.elpais.com/rss/feed.html?feedId=17073'),
|
(u'Pol\xedtica', u'http://www.elpais.com/rss/feed.html?feedId=17073'),
|
||||||
(u'Tecnologia', u'http://www.elpais.com/rss/feed.html?feedId=1005'),
|
(u'Tecnolog\xeda', u'http://www.elpais.com/rss/feed.html?feedId=1005'),
|
||||||
(u'Cultura', u'http://www.elpais.com/rss/feed.html?feedId=1008'),
|
(u'Cultura', u'http://www.elpais.com/rss/feed.html?feedId=1008'),
|
||||||
(u'Gente', u'http://www.elpais.com/rss/feed.html?feedId=1009'),
|
(u'Gente', u'http://www.elpais.com/rss/feed.html?feedId=1009'),
|
||||||
(u'Sociedad', u'http://www.elpais.com/rss/feed.html?feedId=1004'),
|
(u'Sociedad', u'http://www.elpais.com/rss/feed.html?feedId=1004'),
|
||||||
(u'Opinion', u'http://www.elpais.com/rss/feed.html?feedId=1003'),
|
(u'Opini\xf3n', u'http://www.elpais.com/rss/feed.html?feedId=1003'),
|
||||||
(u'Ciencia', u'http://www.elpais.com/rss/feed.html?feedId=17068'),
|
(u'Ciencia', u'http://www.elpais.com/rss/feed.html?feedId=17068'),
|
||||||
(u'Justicia y leyes', u'http://www.elpais.com/rss/feed.html?feedId=17069'),
|
(u'Justicia y leyes', u'http://www.elpais.com/rss/feed.html?feedId=17069'),
|
||||||
]
|
(u'Medio ambiente', u'http://www.elpais.com/rss/feed.html?feedId=17071'),
|
||||||
|
(u'Vi\xf1etas', u'http://www.elpais.com/rss/feed.html?feedId=17058')
|
||||||
|
]
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
url = url+'?print=1'
|
url = url+'?print=1'
|
||||||
|
64
resources/recipes/journalgazette.recipe
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'somedayson & TonytheBookworm, revised by Cynthia Clavey'
|
||||||
|
__copyright__ = '2010, Cynthia Clavey cynvision@yahoo.com'
|
||||||
|
__version__ = '1.02'
|
||||||
|
__date__ = '05, september 2010'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1283666183(BasicNewsRecipe):
|
||||||
|
title = u'Journal Gazette Ft. Wayne IN'
|
||||||
|
__author__ = 'cynvision'
|
||||||
|
oldest_article = 1
|
||||||
|
max_articles_per_feed = 8
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':'mainContent'})]
|
||||||
|
extra_css = '#copyinfo { font-size: 6 ;} \n #photocredit { font-size: 6 ;} \n .pubinfo { font-size: 6 ;}'
|
||||||
|
masthead_url = 'http://www.journalgazette.net/img/icons/jgmini.gif'
|
||||||
|
# cover_url = 'http://www.journalgazette.net/img/icons/jgmini.gif'
|
||||||
|
encoding = 'cp1252'
|
||||||
|
|
||||||
|
feeds = [(u'Opinion', u'http://journalgazette.net/apps/pbcs.dll/section?Category=EDIT&template=blogrss&mime=xml'),
|
||||||
|
(u'Local News',u'http://journalgazette.net/apps/pbcs.dll/section?Category=LOCAL&template=blogrss&mime=xml') ,
|
||||||
|
(u'Sports',u'http://journalgazette.net/apps/pbcs.dll/section?Category=SPORTS&template=blogrss&mime=xml' ),
|
||||||
|
(u'Features',u'http://journalgazette.net/apps/pbcs.dll/section?Category=FEAT&template=blogrss&mime=xml'),
|
||||||
|
(u'Business',u'http://journalgazette.net/apps/pbcs.dll/section?Category=BIZ&template=blogrss&mime=xml'),
|
||||||
|
(u'Ice Chips',u'http://journalgazette.net/apps/pbcs.dll/section?Category=BLOGS11&template=blogrss&mime=xml '),
|
||||||
|
(u'Entertainment',u'http://journalgazette.net/apps/pbcs.dll/section?Category=ENT&template=blogrss&mime=xml'),
|
||||||
|
(u'Food',u'http://journalgazette.net/apps/pbcs.dll/section?Category=FOOD&template=blogrss&mime=xml')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
split1 = url.split("/")
|
||||||
|
#print 'THE SPLIT IS: ', split1
|
||||||
|
#url1 = split1[0]
|
||||||
|
#url2 = split1[1]
|
||||||
|
url3 = split1[2]
|
||||||
|
#url4 = split1[3]
|
||||||
|
url5 = split1[4]
|
||||||
|
url6 = split1[5]
|
||||||
|
url7 = split1[6]
|
||||||
|
#url8 = split1[7]
|
||||||
|
|
||||||
|
#need to convert to print_version
|
||||||
|
#originalversion is : http://www.journalgazette.net/article/20100905/EDIT10/309059959/1021/EDIT
|
||||||
|
#printversion should be: http://www.journalgazette.net/apps/pbcs.dll/article?AID=/20100905/EDIT10/309059959/-1/EDIT01&template=printart
|
||||||
|
#results of the split
|
||||||
|
#THE SPLIT IS: [u'http:', u'', u'www.journalgazette.net', u'article', u'20100905', u'EDIT10', u'309059959', u'1021', u'EDIT']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print_url = 'http://' + url3 + '/apps/pbcs.dll/article?AID=/' + url5 + '/' + url6 + '/' + url7 + '/-1/EDIT01&template=printart'
|
||||||
|
#print 'THIS URL WILL PRINT: ', print_url # this is a test string to see what the url is it will return
|
||||||
|
return print_url
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
58
resources/recipes/kstar.recipe
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1282101454(BasicNewsRecipe):
|
||||||
|
title = 'Kansascity Star'
|
||||||
|
language = 'en'
|
||||||
|
__author__ = 'TonytheBookworm'
|
||||||
|
description = 'www.kansascity.com feed'
|
||||||
|
publisher = 'Tony Stegall'
|
||||||
|
category = 'news, politics, USA, kansascity'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
masthead_url = 'http://media.kansascity.com/images/site_logo_340x60.gif'
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(attrs={'id':['storyTitle','sub_headline','byLine']})
|
||||||
|
,dict(name='div', attrs={'id':['storyDate-Links','storyBody']})
|
||||||
|
|
||||||
|
]
|
||||||
|
feeds = [
|
||||||
|
('Kansas News', 'http://www.kansascity.com/105/index.xml'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
split1 = url.split("/")
|
||||||
|
#url1 = split1[0]
|
||||||
|
#url2 = split1[1]
|
||||||
|
url3 = split1[2]
|
||||||
|
url4 = split1[3]
|
||||||
|
url5 = split1[4]
|
||||||
|
url6 = split1[5]
|
||||||
|
url7 = split1[6]
|
||||||
|
url8 = split1[7]
|
||||||
|
|
||||||
|
|
||||||
|
#example of link to convert
|
||||||
|
#Original link: http://www.kansascity.com/2010/09/04/2199362/lees-summit-school-appears-to.html
|
||||||
|
#print version: http://www.kansascity.com/2010/09/04/v-print/2199362/lees-summit-school-appears-to.html
|
||||||
|
|
||||||
|
print_url = 'http://' + url3 + '/' + url4 + '/' + url5 + '/' + url6 + '/v-print/' + url7 + '/' + url8
|
||||||
|
|
||||||
|
return print_url
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>, Rogelio Domínguez <rogelio.dominguez@gmail.com>'
|
||||||
'''
|
'''
|
||||||
www.jornada.unam.mx
|
www.jornada.unam.mx
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class LaJornada_mx(BasicNewsRecipe):
|
class LaJornada_mx(BasicNewsRecipe):
|
||||||
title = 'La Jornada (Mexico)'
|
title = 'La Jornada (Mexico)'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic/Rogelio Domínguez'
|
||||||
description = 'Noticias del diario mexicano La Jornada'
|
description = 'Noticias del diario mexicano La Jornada'
|
||||||
publisher = 'DEMOS, Desarrollo de Medios, S.A. de C.V.'
|
publisher = 'DEMOS, Desarrollo de Medios, S.A. de C.V.'
|
||||||
category = 'news, Mexico'
|
category = 'news, Mexico'
|
||||||
@ -20,12 +21,26 @@ class LaJornada_mx(BasicNewsRecipe):
|
|||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'es'
|
language = 'es'
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/planitas/portadita.jpg")
|
cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/portada.pdf")
|
||||||
masthead_url = 'http://www.jornada.unam.mx/v7.0/imagenes/la-jornada-trans.png'
|
masthead_url = 'http://www.jornada.unam.mx/v7.0/imagenes/la-jornada-trans.png'
|
||||||
|
publication_type = 'newspaper'
|
||||||
extra_css = """
|
extra_css = """
|
||||||
body{font-family: "Times New Roman",serif }
|
body{font-family: "Times New Roman",serif }
|
||||||
.cabeza{font-size: xx-large; font-weight: bold }
|
.cabeza{font-size: xx-large; font-weight: bold }
|
||||||
.credito-articulo{font-size: 1.3em}
|
.documentFirstHeading{font-size: xx-large; font-weight: bold }
|
||||||
|
.credito-articulo{font-variant: small-caps; font-weight: bold }
|
||||||
|
.foto{text-align: center}
|
||||||
|
.pie-foto{font-size: 0.9em}
|
||||||
|
.credito{font-weight: bold; margin-left: 1em}
|
||||||
|
.credito-autor{font-variant: small-caps; font-weight: bold }
|
||||||
|
.credito-titulo{text-align: right}
|
||||||
|
.hemero{text-align: right; font-size: 0.9em; margin-bottom: 0.5em }
|
||||||
|
.loc{font-weight: bold}
|
||||||
|
.carton{text-align: center}
|
||||||
|
.credit{font-weight: bold}
|
||||||
|
.text{margin-top: 1.4em}
|
||||||
|
p.inicial{display: inline; font-size: xx-large; font-weight: bold}
|
||||||
|
p.s-s{display: inline; text-indent: 0}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
@ -35,15 +50,21 @@ class LaJornada_mx(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preprocess_regexps = [
|
||||||
|
(re.compile( r'<div class="inicial">(.*)</div><p class="s-s">'
|
||||||
|
,re.DOTALL|re.IGNORECASE)
|
||||||
|
,lambda match: '<p class="inicial">' + match.group(1) + '</p><p class="s-s">')
|
||||||
|
]
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','text']})
|
dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','credito-articulo','text','carton']})
|
||||||
,dict(name='div', attrs={'id':'renderComments'})
|
,dict(name='div', attrs={'id':'renderComments'})
|
||||||
]
|
]
|
||||||
remove_tags = [dict(name='div', attrs={'class':'buttonbar'})]
|
remove_tags = [dict(name='div', attrs={'class':['buttonbar','comment-cont']})]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' )
|
(u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' )
|
||||||
,(u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' )
|
,(u'Cartones' , u'http://www.jornada.unam.mx/rss/cartones.xml' )
|
||||||
,(u'Politica' , u'http://www.jornada.unam.mx/rss/politica.xml' )
|
,(u'Politica' , u'http://www.jornada.unam.mx/rss/politica.xml' )
|
||||||
,(u'Economia' , u'http://www.jornada.unam.mx/rss/economia.xml' )
|
,(u'Economia' , u'http://www.jornada.unam.mx/rss/economia.xml' )
|
||||||
,(u'Mundo' , u'http://www.jornada.unam.mx/rss/mundo.xml' )
|
,(u'Mundo' , u'http://www.jornada.unam.mx/rss/mundo.xml' )
|
||||||
@ -55,6 +76,7 @@ class LaJornada_mx(BasicNewsRecipe):
|
|||||||
,(u'Gastronomia' , u'http://www.jornada.unam.mx/rss/gastronomia.xml' )
|
,(u'Gastronomia' , u'http://www.jornada.unam.mx/rss/gastronomia.xml' )
|
||||||
,(u'Espectaculos' , u'http://www.jornada.unam.mx/rss/espectaculos.xml' )
|
,(u'Espectaculos' , u'http://www.jornada.unam.mx/rss/espectaculos.xml' )
|
||||||
,(u'Deportes' , u'http://www.jornada.unam.mx/rss/deportes.xml' )
|
,(u'Deportes' , u'http://www.jornada.unam.mx/rss/deportes.xml' )
|
||||||
|
,(u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' )
|
||||||
]
|
]
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
@ -62,3 +84,7 @@ class LaJornada_mx(BasicNewsRecipe):
|
|||||||
del item['style']
|
del item['style']
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
rurl = article.get('link', None)
|
||||||
|
return rurl.rpartition('&partner=')[0]
|
||||||
|
|
||||||
|
@ -22,10 +22,19 @@ class NrcNextRecipe(BasicNewsRecipe):
|
|||||||
|
|
||||||
remove_tags = []
|
remove_tags = []
|
||||||
remove_tags.append(dict(name = 'div', attrs = {'class' : 'meta'}))
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'meta'}))
|
||||||
|
remove_tags.append(dict(name = 'p', attrs = {'class' : 'meta'}))
|
||||||
remove_tags.append(dict(name = 'div', attrs = {'class' : 'datumlabel'}))
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'datumlabel'}))
|
||||||
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'sharing-is-caring'}))
|
||||||
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'navigation'}))
|
||||||
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'reageer'}))
|
||||||
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'comment odd alt thread-odd thread-alt depth-1 reactie '}))
|
||||||
|
remove_tags.append(dict(name = 'div', attrs = {'class' : 'comment even thread-even depth-1 reactie '}))
|
||||||
remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats single'}))
|
remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats single'}))
|
||||||
remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats onderwerpen'}))
|
remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats onderwerpen'}))
|
||||||
remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats rubrieken'}))
|
remove_tags.append(dict(name = 'ul', attrs = {'class' : 'cats rubrieken'}))
|
||||||
|
remove_tags.append(dict(name = 'h3', attrs = {'class' : 'reacties'}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
body {font-family: verdana, arial, helvetica, geneva, sans-serif; text-align: left;}
|
body {font-family: verdana, arial, helvetica, geneva, sans-serif; text-align: left;}
|
||||||
@ -41,20 +50,18 @@ class NrcNextRecipe(BasicNewsRecipe):
|
|||||||
feeds[u'koken'] = u'http://www.nrcnext.nl/koken/'
|
feeds[u'koken'] = u'http://www.nrcnext.nl/koken/'
|
||||||
feeds[u'geld & werk'] = u'http://www.nrcnext.nl/geld-en-werk/'
|
feeds[u'geld & werk'] = u'http://www.nrcnext.nl/geld-en-werk/'
|
||||||
feeds[u'vandaag'] = u'http://www.nrcnext.nl'
|
feeds[u'vandaag'] = u'http://www.nrcnext.nl'
|
||||||
feeds[u'city life in afrika'] = u'http://www.nrcnext.nl/city-life-in-afrika/'
|
# feeds[u'city life in afrika'] = u'http://www.nrcnext.nl/city-life-in-afrika/'
|
||||||
answer = []
|
answer = []
|
||||||
articles = {}
|
articles = {}
|
||||||
indices = []
|
indices = []
|
||||||
|
|
||||||
for index, feed in feeds.items() :
|
for index, feed in feeds.items() :
|
||||||
soup = self.index_to_soup(feed)
|
soup = self.index_to_soup(feed)
|
||||||
|
for post in soup.findAll(True, attrs={'class' : 'post '}) :
|
||||||
for post in soup.findAll(True, attrs={'class' : 'post'}) :
|
|
||||||
# Find the links to the actual articles and rember the location they're pointing to and the title
|
# Find the links to the actual articles and rember the location they're pointing to and the title
|
||||||
a = post.find('a', attrs={'rel' : 'bookmark'})
|
a = post.find('a', attrs={'rel' : 'bookmark'})
|
||||||
href = a['href']
|
href = a['href']
|
||||||
title = self.tag_to_string(a)
|
title = self.tag_to_string(a)
|
||||||
|
|
||||||
if index == 'columnisten' :
|
if index == 'columnisten' :
|
||||||
# In this feed/page articles can be written by more than one author.
|
# In this feed/page articles can be written by more than one author.
|
||||||
# It is nice to see their names in the titles.
|
# It is nice to see their names in the titles.
|
||||||
@ -74,7 +81,8 @@ class NrcNextRecipe(BasicNewsRecipe):
|
|||||||
indices.append(index)
|
indices.append(index)
|
||||||
|
|
||||||
# Now, sort the temporary list of feeds in the order they appear on the website
|
# Now, sort the temporary list of feeds in the order they appear on the website
|
||||||
indices = self.sort_index_by(indices, {u'columnisten' : 1, u'koken' : 3, u'geld & werk' : 2, u'vandaag' : 0, u'city life in afrika' : 4})
|
# indices = self.sort_index_by(indices, {u'columnisten' : 1, u'koken' : 3, u'geld & werk' : 2, u'vandaag' : 0, u'city life in afrika' : 4})
|
||||||
|
indices = self.sort_index_by(indices, {u'columnisten' : 1, u'koken' : 3, u'geld & werk' : 2, u'vandaag' : 0})
|
||||||
# Apply this sort order to the actual list of feeds and articles
|
# Apply this sort order to the actual list of feeds and articles
|
||||||
answer = [(key, articles[key]) for key in indices if articles.has_key(key)]
|
answer = [(key, articles[key]) for key in indices if articles.has_key(key)]
|
||||||
|
|
||||||
|
@ -26,13 +26,13 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
#TO LOGIN
|
#TO LOGIN
|
||||||
def get_browser(self):
|
def get_browser(self):
|
||||||
br = BasicNewsRecipe.get_browser()
|
br = BasicNewsRecipe.get_browser()
|
||||||
br.open('http://content.nejm.org/cgi/login?uri=/')
|
br.open('http://www.nejm.org/action/showLogin?uri=http://www.nejm.org/')
|
||||||
br.select_form(nr=0)
|
br.select_form(name='frmLogin')
|
||||||
br['username'] = self.username
|
br['login'] = self.username
|
||||||
br['code'] = self.password
|
br['password'] = self.password
|
||||||
response = br.submit()
|
response = br.submit()
|
||||||
raw = response.read()
|
raw = response.read()
|
||||||
if '<strong>Welcome' not in raw:
|
if '>Sign Out<' not in raw:
|
||||||
raise Exception('Login failed. Check your username and password')
|
raise Exception('Login failed. Check your username and password')
|
||||||
return br
|
return br
|
||||||
|
|
||||||
|
@ -13,50 +13,51 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
|
|
||||||
class TazDigiabo(BasicNewsRecipe):
|
class TazDigiabo(BasicNewsRecipe):
|
||||||
|
|
||||||
title = u'Taz Digiabo'
|
title = u'Taz Digiabo'
|
||||||
description = u'Das EPUB DigiAbo der Taz'
|
description = u'Das EPUB DigiAbo der Taz'
|
||||||
language = 'de'
|
language = 'de'
|
||||||
lang = 'de-DE'
|
lang = 'de-DE'
|
||||||
|
|
||||||
__author__ = 'Lars Jacob'
|
__author__ = 'Lars Jacob'
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'no_default_epub_cover' : True
|
'no_default_epub_cover' : True
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_index(self):
|
def build_index(self):
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
domain = "http://www.taz.de"
|
domain = "http://www.taz.de"
|
||||||
|
|
||||||
url = domain + "/epub/"
|
url = domain + "/epub/"
|
||||||
|
|
||||||
auth_handler = urllib2.HTTPBasicAuthHandler()
|
auth_handler = urllib2.HTTPBasicAuthHandler()
|
||||||
auth_handler.add_password(realm='TAZ-ABO',
|
auth_handler.add_password(realm='TAZ-ABO',
|
||||||
uri=url,
|
uri=url,
|
||||||
user=self.username,
|
user=self.username,
|
||||||
passwd=self.password)
|
passwd=self.password)
|
||||||
opener = urllib2.build_opener(auth_handler)
|
opener = urllib2.build_opener(auth_handler)
|
||||||
urllib2.install_opener(opener)
|
urllib2.install_opener(opener)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = urllib2.urlopen(url)
|
f = urllib2.urlopen(url)
|
||||||
except urllib2.HTTPError:
|
except urllib2.HTTPError:
|
||||||
self.report_progress(0,_('Can\'t login to download issue'))
|
self.report_progress(0,_('Can\'t login to download issue'))
|
||||||
return
|
raise ValueError('Failed to login, check your username and'
|
||||||
|
' password')
|
||||||
|
|
||||||
tmp = tempfile.TemporaryFile()
|
tmp = tempfile.TemporaryFile()
|
||||||
self.report_progress(0,_('downloading epub'))
|
self.report_progress(0,_('downloading epub'))
|
||||||
tmp.write(f.read())
|
tmp.write(f.read())
|
||||||
|
|
||||||
zfile = zipfile.ZipFile(tmp, 'r')
|
zfile = zipfile.ZipFile(tmp, 'r')
|
||||||
self.report_progress(0,_('extracting epub'))
|
self.report_progress(0,_('extracting epub'))
|
||||||
|
|
||||||
zfile.extractall(self.output_dir)
|
zfile.extractall(self.output_dir)
|
||||||
|
|
||||||
tmp.close()
|
tmp.close()
|
||||||
index = os.path.join(self.output_dir, 'content.opf')
|
index = os.path.join(self.output_dir, 'content.opf')
|
||||||
|
|
||||||
self.report_progress(1,_('epub downloaded and extracted'))
|
self.report_progress(1,_('epub downloaded and extracted'))
|
||||||
|
|
||||||
return index
|
return index
|
||||||
|
46
resources/recipes/walrusmag.recipe
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1282101454(BasicNewsRecipe):
|
||||||
|
title = 'The Walrus Mag'
|
||||||
|
language = 'en'
|
||||||
|
__author__ = 'TonytheBookworm'
|
||||||
|
description = 'national general interest magazine about Canada and its place in the world'
|
||||||
|
publisher = 'Tony Stegall'
|
||||||
|
category = 'Canada, news'
|
||||||
|
oldest_article = 365
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
masthead_url = 'http://www.walrusmagazine.com/images/wordmark.png'
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h1'),
|
||||||
|
dict(name='div', attrs={'id':['prbody']})
|
||||||
|
# ,dict(attrs={'id':['cxArticleText','cxArticleBodyText']})
|
||||||
|
]
|
||||||
|
feeds = [
|
||||||
|
('Walrus Magazine', 'http://feeds.feedburner.com/WalrusFeatureArticles?format=xml'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
split1 = url.split("/articles/")
|
||||||
|
#print 'THE SPLIT IS: ', split1
|
||||||
|
url1 = split1[0]
|
||||||
|
#print 'url1 is: ',url1
|
||||||
|
url2 = split1[1]
|
||||||
|
#print 'url2 is: ',url2
|
||||||
|
|
||||||
|
|
||||||
|
#need to convert to print_version
|
||||||
|
#originalversion is : http://www.walrusmagazine.com/articles/2010.09-frontier-no-one-can-hear-you-scream/
|
||||||
|
#printversion should be: http://www.walrusmagazine.com/print/2010.09-frontier-no-one-can-hear-you-scream/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print_url = url1 + '/print/' + url2
|
||||||
|
#print 'THIS URL WILL PRINT: ', print_url # this is a test string to see what the url is it will return
|
||||||
|
return print_url
|
||||||
|
|
153
resources/recipes/wsj_free.recipe
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
import copy
|
||||||
|
|
||||||
|
class WallStreetJournal(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = 'Wall Street Journal (free)'
|
||||||
|
__author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17'
|
||||||
|
description = 'News and current affairs'
|
||||||
|
language = 'en'
|
||||||
|
cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG'
|
||||||
|
max_articles_per_feed = 1000
|
||||||
|
timefmt = ' [%a, %b %d, %Y]'
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
extra_css = '''h1{color:#093D72 ; font-size:large ; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; }
|
||||||
|
h2{color:#474537; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
|
||||||
|
.subhead{color:gray; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
|
||||||
|
.insettipUnit {color:#666666; font-family:Arial,Sans-serif;font-size:xx-small }
|
||||||
|
.targetCaption{ font-size:x-small; color:#333333; font-family:Arial,Helvetica,sans-serif}
|
||||||
|
.article{font-family :Arial,Helvetica,sans-serif; font-size:x-small}
|
||||||
|
.tagline {color:#333333; font-size:xx-small}
|
||||||
|
.dateStamp {color:#666666; font-family:Arial,Helvetica,sans-serif}
|
||||||
|
h3{color:blue ;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
|
||||||
|
.byline{color:blue;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
|
||||||
|
h6{color:#333333; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small;font-style:italic; }
|
||||||
|
.paperLocation{color:#666666; font-size:xx-small}'''
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='h1')
|
||||||
|
remove_tags = [
|
||||||
|
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
|
||||||
|
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
||||||
|
dict(name='div', attrs={'data-flash-settings':True}),
|
||||||
|
{'class':['insetContent embedType-interactive insetCol3wide','insetCol6wide','insettipUnit']},
|
||||||
|
dict(rel='shortcut icon'),
|
||||||
|
]
|
||||||
|
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
||||||
|
|
||||||
|
def postprocess_html(self, soup, first):
|
||||||
|
for tag in soup.findAll(name=['table', 'tr', 'td']):
|
||||||
|
tag.name = 'div'
|
||||||
|
|
||||||
|
for tag in soup.findAll('div', dict(id=["articleThumbnail_1", "articleThumbnail_2", "articleThumbnail_3", "articleThumbnail_4", "articleThumbnail_5", "articleThumbnail_6", "articleThumbnail_7"])):
|
||||||
|
tag.extract()
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def wsj_get_index(self):
|
||||||
|
return self.index_to_soup('http://online.wsj.com/itp')
|
||||||
|
|
||||||
|
def wsj_add_feed(self,feeds,title,url):
|
||||||
|
self.log('Found section:', title)
|
||||||
|
if url.endswith('whatsnews'):
|
||||||
|
articles = self.wsj_find_wn_articles(url)
|
||||||
|
else:
|
||||||
|
articles = self.wsj_find_articles(url)
|
||||||
|
if articles:
|
||||||
|
feeds.append((title, articles))
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
soup = self.wsj_get_index()
|
||||||
|
|
||||||
|
date = soup.find('span', attrs={'class':'date-date'})
|
||||||
|
if date is not None:
|
||||||
|
self.timefmt = ' [%s]'%self.tag_to_string(date)
|
||||||
|
|
||||||
|
feeds = []
|
||||||
|
div = soup.find('div', attrs={'class':'itpHeader'})
|
||||||
|
div = div.find('ul', attrs={'class':'tab'})
|
||||||
|
for a in div.findAll('a', href=lambda x: x and '/itp/' in x):
|
||||||
|
pageone = a['href'].endswith('pageone')
|
||||||
|
if pageone:
|
||||||
|
title = 'Front Section'
|
||||||
|
url = 'http://online.wsj.com' + a['href']
|
||||||
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
|
title = 'What''s News'
|
||||||
|
url = url.replace('pageone','whatsnews')
|
||||||
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
|
else:
|
||||||
|
title = self.tag_to_string(a)
|
||||||
|
url = 'http://online.wsj.com' + a['href']
|
||||||
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def wsj_find_wn_articles(self, url):
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
articles = []
|
||||||
|
|
||||||
|
whats_news = soup.find('div', attrs={'class':lambda x: x and 'whatsNews-simple' in x})
|
||||||
|
if whats_news is not None:
|
||||||
|
for a in whats_news.findAll('a', href=lambda x: x and '/article/' in x):
|
||||||
|
container = a.findParent(['p'])
|
||||||
|
meta = a.find(attrs={'class':'meta_sectionName'})
|
||||||
|
if meta is not None:
|
||||||
|
meta.extract()
|
||||||
|
title = self.tag_to_string(a).strip()
|
||||||
|
url = a['href']
|
||||||
|
desc = ''
|
||||||
|
if container is not None:
|
||||||
|
desc = self.tag_to_string(container)
|
||||||
|
|
||||||
|
articles.append({'title':title, 'url':url,
|
||||||
|
'description':desc, 'date':''})
|
||||||
|
|
||||||
|
self.log('\tFound WN article:', title)
|
||||||
|
|
||||||
|
return articles
|
||||||
|
|
||||||
|
def wsj_find_articles(self, url):
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
|
||||||
|
whats_news = soup.find('div', attrs={'class':lambda x: x and 'whatsNews-simple' in x})
|
||||||
|
if whats_news is not None:
|
||||||
|
whats_news.extract()
|
||||||
|
|
||||||
|
articles = []
|
||||||
|
|
||||||
|
flavorarea = soup.find('div', attrs={'class':lambda x: x and 'ahed' in x})
|
||||||
|
if flavorarea is not None:
|
||||||
|
flavorstory = flavorarea.find('a', href=lambda x: x and x.startswith('/article'))
|
||||||
|
if flavorstory is not None:
|
||||||
|
flavorstory['class'] = 'mjLinkItem'
|
||||||
|
metapage = soup.find('span', attrs={'class':lambda x: x and 'meta_sectionName' in x})
|
||||||
|
if metapage is not None:
|
||||||
|
flavorstory.append( copy.copy(metapage) ) #metapage should always be A1 because that should be first on the page
|
||||||
|
|
||||||
|
for a in soup.findAll('a', attrs={'class':'mjLinkItem'}, href=True):
|
||||||
|
container = a.findParent(['li', 'div'])
|
||||||
|
meta = a.find(attrs={'class':'meta_sectionName'})
|
||||||
|
if meta is not None:
|
||||||
|
meta.extract()
|
||||||
|
title = self.tag_to_string(a).strip() + ' [%s]'%self.tag_to_string(meta)
|
||||||
|
url = 'http://online.wsj.com'+a['href']
|
||||||
|
desc = ''
|
||||||
|
p = container.find('p')
|
||||||
|
if p is not None:
|
||||||
|
desc = self.tag_to_string(p)
|
||||||
|
|
||||||
|
articles.append({'title':title, 'url':url,
|
||||||
|
'description':desc, 'date':''})
|
||||||
|
|
||||||
|
self.log('\tFound article:', title)
|
||||||
|
|
||||||
|
return articles
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
|
||||||
|
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.17'
|
__version__ = '0.7.18'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -294,7 +294,7 @@ class CatalogPlugin(Plugin): # {{{
|
|||||||
# Return a list of requested fields, with opts.sort_by first
|
# Return a list of requested fields, with opts.sort_by first
|
||||||
all_fields = set(
|
all_fields = set(
|
||||||
['author_sort','authors','comments','cover','formats',
|
['author_sort','authors','comments','cover','formats',
|
||||||
'id','isbn','pubdate','publisher','rating',
|
'id','isbn','ondevice','pubdate','publisher','rating',
|
||||||
'series_index','series','size','tags','timestamp',
|
'series_index','series','size','tags','timestamp',
|
||||||
'title','uuid'])
|
'title','uuid'])
|
||||||
|
|
||||||
@ -306,6 +306,9 @@ class CatalogPlugin(Plugin): # {{{
|
|||||||
else:
|
else:
|
||||||
fields = list(all_fields)
|
fields = list(all_fields)
|
||||||
|
|
||||||
|
if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
|
||||||
|
fields.pop(int(fields.index('ondevice')))
|
||||||
|
|
||||||
fields.sort()
|
fields.sort()
|
||||||
if opts.sort_by and opts.sort_by in fields:
|
if opts.sort_by and opts.sort_by in fields:
|
||||||
fields.insert(0,fields.pop(int(fields.index(opts.sort_by))))
|
fields.insert(0,fields.pop(int(fields.index(opts.sort_by))))
|
||||||
@ -371,6 +374,13 @@ class InterfaceActionBase(Plugin): # {{{
|
|||||||
|
|
||||||
class PreferencesPlugin(Plugin): # {{{
|
class PreferencesPlugin(Plugin): # {{{
|
||||||
|
|
||||||
|
'''
|
||||||
|
A plugin representing a widget displayed in the Preferences dialog.
|
||||||
|
|
||||||
|
This plugin has only one important method :meth:`create_widget`. The
|
||||||
|
various fields of the plugin control how it is categorized in the UI.
|
||||||
|
'''
|
||||||
|
|
||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
author = 'Kovid Goyal'
|
author = 'Kovid Goyal'
|
||||||
type = _('Preferences')
|
type = _('Preferences')
|
||||||
@ -393,13 +403,21 @@ class PreferencesPlugin(Plugin): # {{{
|
|||||||
|
|
||||||
#: The category name displayed to the user for this plugin
|
#: The category name displayed to the user for this plugin
|
||||||
gui_category = None
|
gui_category = None
|
||||||
|
|
||||||
#: The name displayed to the user for this plugin
|
#: The name displayed to the user for this plugin
|
||||||
gui_name = None
|
gui_name = None
|
||||||
|
|
||||||
|
#: The icon for this plugin, should be an absolute path
|
||||||
|
icon = None
|
||||||
|
|
||||||
|
#: The description used for tooltips and the like
|
||||||
|
description = None
|
||||||
|
|
||||||
def create_widget(self, parent=None):
|
def create_widget(self, parent=None):
|
||||||
'''
|
'''
|
||||||
Create and return the actual Qt widget used for setting this group of
|
Create and return the actual Qt widget used for setting this group of
|
||||||
preferences. The widget must implement the ConfigWidgetInterface.
|
preferences. The widget must implement the
|
||||||
|
:class:`calibre.gui2.preferences.ConfigWidgetInterface`.
|
||||||
|
|
||||||
The default implementation uses :attr:`config_widget` to instantiate
|
The default implementation uses :attr:`config_widget` to instantiate
|
||||||
the widget.
|
the widget.
|
||||||
|
@ -461,7 +461,7 @@ from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
|||||||
from calibre.devices.edge.driver import EDGE
|
from calibre.devices.edge.driver import EDGE
|
||||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
|
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
|
||||||
from calibre.devices.sne.driver import SNE
|
from calibre.devices.sne.driver import SNE
|
||||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN
|
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, GEMEI
|
||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
from calibre.devices.kobo.driver import KOBO
|
||||||
|
|
||||||
@ -570,6 +570,7 @@ plugins += [
|
|||||||
KOGAN,
|
KOGAN,
|
||||||
PDNOVEL,
|
PDNOVEL,
|
||||||
SPECTRA,
|
SPECTRA,
|
||||||
|
GEMEI,
|
||||||
ITUNES,
|
ITUNES,
|
||||||
]
|
]
|
||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||||
@ -678,78 +679,181 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
|||||||
|
|
||||||
class LookAndFeel(PreferencesPlugin):
|
class LookAndFeel(PreferencesPlugin):
|
||||||
name = 'Look & Feel'
|
name = 'Look & Feel'
|
||||||
|
icon = I('lookfeel.png')
|
||||||
gui_name = _('Look and Feel')
|
gui_name = _('Look and Feel')
|
||||||
category = 'Interface'
|
category = 'Interface'
|
||||||
gui_category = _('Interface')
|
gui_category = _('Interface')
|
||||||
category_order = 1
|
category_order = 1
|
||||||
name_order = 1
|
name_order = 1
|
||||||
config_widget = 'calibre.gui2.preferences.look_feel'
|
config_widget = 'calibre.gui2.preferences.look_feel'
|
||||||
|
description = _('Adjust the look and feel of the calibre interface'
|
||||||
|
' to suit your tastes')
|
||||||
|
|
||||||
class Behavior(PreferencesPlugin):
|
class Behavior(PreferencesPlugin):
|
||||||
name = 'Behavior'
|
name = 'Behavior'
|
||||||
|
icon = I('config.png')
|
||||||
gui_name = _('Behavior')
|
gui_name = _('Behavior')
|
||||||
category = 'Interface'
|
category = 'Interface'
|
||||||
gui_category = _('Interface')
|
gui_category = _('Interface')
|
||||||
category_order = 1
|
category_order = 1
|
||||||
name_order = 2
|
name_order = 2
|
||||||
config_widget = 'calibre.gui2.preferences.behavior'
|
config_widget = 'calibre.gui2.preferences.behavior'
|
||||||
|
description = _('Change the way calibre behaves')
|
||||||
|
|
||||||
class Columns(PreferencesPlugin):
|
class Columns(PreferencesPlugin):
|
||||||
name = 'Custom Columns'
|
name = 'Custom Columns'
|
||||||
|
icon = I('column.png')
|
||||||
gui_name = _('Add your own columns')
|
gui_name = _('Add your own columns')
|
||||||
category = 'Interface'
|
category = 'Interface'
|
||||||
gui_category = _('Interface')
|
gui_category = _('Interface')
|
||||||
category_order = 1
|
category_order = 1
|
||||||
name_order = 3
|
name_order = 3
|
||||||
config_widget = 'calibre.gui2.preferences.columns'
|
config_widget = 'calibre.gui2.preferences.columns'
|
||||||
|
description = _('Add/remove your own columns to the calibre book list')
|
||||||
|
|
||||||
class Toolbar(PreferencesPlugin):
|
class Toolbar(PreferencesPlugin):
|
||||||
name = 'Toolbar'
|
name = 'Toolbar'
|
||||||
|
icon = I('wizard.png')
|
||||||
gui_name = _('Customize the toolbar')
|
gui_name = _('Customize the toolbar')
|
||||||
category = 'Interface'
|
category = 'Interface'
|
||||||
gui_category = _('Interface')
|
gui_category = _('Interface')
|
||||||
category_order = 1
|
category_order = 1
|
||||||
name_order = 4
|
name_order = 4
|
||||||
config_widget = 'calibre.gui2.preferences.toolbar'
|
config_widget = 'calibre.gui2.preferences.toolbar'
|
||||||
|
description = _('Customize the toolbars and context menus, changing which'
|
||||||
|
' actions are available in each')
|
||||||
|
|
||||||
class InputOptions(PreferencesPlugin):
|
class InputOptions(PreferencesPlugin):
|
||||||
name = 'Input Options'
|
name = 'Input Options'
|
||||||
|
icon = I('arrow-down.png')
|
||||||
gui_name = _('Input Options')
|
gui_name = _('Input Options')
|
||||||
category = 'Conversion'
|
category = 'Conversion'
|
||||||
gui_category = _('Conversion')
|
gui_category = _('Conversion')
|
||||||
category_order = 2
|
category_order = 2
|
||||||
name_order = 1
|
name_order = 1
|
||||||
config_widget = 'calibre.gui2.preferences.conversion:InputOptions'
|
config_widget = 'calibre.gui2.preferences.conversion:InputOptions'
|
||||||
|
description = _('Set conversion options specific to each input format')
|
||||||
|
|
||||||
class CommonOptions(PreferencesPlugin):
|
class CommonOptions(PreferencesPlugin):
|
||||||
name = 'Common Options'
|
name = 'Common Options'
|
||||||
|
icon = I('convert.png')
|
||||||
gui_name = _('Common Options')
|
gui_name = _('Common Options')
|
||||||
category = 'Conversion'
|
category = 'Conversion'
|
||||||
gui_category = _('Conversion')
|
gui_category = _('Conversion')
|
||||||
category_order = 2
|
category_order = 2
|
||||||
name_order = 2
|
name_order = 2
|
||||||
config_widget = 'calibre.gui2.preferences.conversion:CommonOptions'
|
config_widget = 'calibre.gui2.preferences.conversion:CommonOptions'
|
||||||
|
description = _('Set conversion options common to all formats')
|
||||||
|
|
||||||
class OutputOptions(PreferencesPlugin):
|
class OutputOptions(PreferencesPlugin):
|
||||||
name = 'Output Options'
|
name = 'Output Options'
|
||||||
|
icon = I('arrow-up.png')
|
||||||
gui_name = _('Output Options')
|
gui_name = _('Output Options')
|
||||||
category = 'Conversion'
|
category = 'Conversion'
|
||||||
gui_category = _('Conversion')
|
gui_category = _('Conversion')
|
||||||
category_order = 2
|
category_order = 2
|
||||||
name_order = 3
|
name_order = 3
|
||||||
config_widget = 'calibre.gui2.preferences.conversion:OutputOptions'
|
config_widget = 'calibre.gui2.preferences.conversion:OutputOptions'
|
||||||
|
description = _('Set conversion options specific to each output format')
|
||||||
|
|
||||||
class Adding(PreferencesPlugin):
|
class Adding(PreferencesPlugin):
|
||||||
name = 'Adding'
|
name = 'Adding'
|
||||||
|
icon = I('add_book.png')
|
||||||
gui_name = _('Adding books')
|
gui_name = _('Adding books')
|
||||||
category = 'Import/Export'
|
category = 'Import/Export'
|
||||||
gui_category = _('Import/Export')
|
gui_category = _('Import/Export')
|
||||||
category_order = 3
|
category_order = 3
|
||||||
name_order = 1
|
name_order = 1
|
||||||
config_widget = 'calibre.gui2.preferences.adding'
|
config_widget = 'calibre.gui2.preferences.adding'
|
||||||
|
description = _('Control how calibre reads metadata from files when '
|
||||||
|
'adding books')
|
||||||
|
|
||||||
|
class Saving(PreferencesPlugin):
|
||||||
|
name = 'Saving'
|
||||||
|
icon = I('save.png')
|
||||||
|
gui_name = _('Saving books to disk')
|
||||||
|
category = 'Import/Export'
|
||||||
|
gui_category = _('Import/Export')
|
||||||
|
category_order = 3
|
||||||
|
name_order = 2
|
||||||
|
config_widget = 'calibre.gui2.preferences.saving'
|
||||||
|
description = _('Control how calibre exports files from its database '
|
||||||
|
'to disk when using Save to disk')
|
||||||
|
|
||||||
|
class Sending(PreferencesPlugin):
|
||||||
|
name = 'Sending'
|
||||||
|
icon = I('sync.png')
|
||||||
|
gui_name = _('Sending books to devices')
|
||||||
|
category = 'Import/Export'
|
||||||
|
gui_category = _('Import/Export')
|
||||||
|
category_order = 3
|
||||||
|
name_order = 3
|
||||||
|
config_widget = 'calibre.gui2.preferences.sending'
|
||||||
|
description = _('Control how calibre transfers files to your '
|
||||||
|
'ebook reader')
|
||||||
|
|
||||||
|
class Email(PreferencesPlugin):
|
||||||
|
name = 'Email'
|
||||||
|
icon = I('mail.png')
|
||||||
|
gui_name = _('Sharing books by email')
|
||||||
|
category = 'Sharing'
|
||||||
|
gui_category = _('Sharing')
|
||||||
|
category_order = 4
|
||||||
|
name_order = 1
|
||||||
|
config_widget = 'calibre.gui2.preferences.emailp'
|
||||||
|
description = _('Setup sharing of books via email. Can be used '
|
||||||
|
'for automatic sending of downloaded news to your devices')
|
||||||
|
|
||||||
|
class Server(PreferencesPlugin):
|
||||||
|
name = 'Server'
|
||||||
|
icon = I('network-server.png')
|
||||||
|
gui_name = _('Sharing over the net')
|
||||||
|
category = 'Sharing'
|
||||||
|
gui_category = _('Sharing')
|
||||||
|
category_order = 4
|
||||||
|
name_order = 2
|
||||||
|
config_widget = 'calibre.gui2.preferences.server'
|
||||||
|
description = _('Setup the calibre Content Server which will '
|
||||||
|
'give you access to your calibre library from anywhere, '
|
||||||
|
'on any device, over the internet')
|
||||||
|
|
||||||
|
class Plugins(PreferencesPlugin):
|
||||||
|
name = 'Plugins'
|
||||||
|
icon = I('plugins.png')
|
||||||
|
gui_name = _('Plugins')
|
||||||
|
category = 'Advanced'
|
||||||
|
gui_category = _('Advanced')
|
||||||
|
category_order = 5
|
||||||
|
name_order = 1
|
||||||
|
config_widget = 'calibre.gui2.preferences.plugins'
|
||||||
|
description = _('Add/remove/customize various bits of calibre '
|
||||||
|
'functionality')
|
||||||
|
|
||||||
|
class Tweaks(PreferencesPlugin):
|
||||||
|
name = 'Tweaks'
|
||||||
|
icon = I('drawer.png')
|
||||||
|
gui_name = _('Tweaks')
|
||||||
|
category = 'Advanced'
|
||||||
|
gui_category = _('Advanced')
|
||||||
|
category_order = 5
|
||||||
|
name_order = 2
|
||||||
|
config_widget = 'calibre.gui2.preferences.tweaks'
|
||||||
|
description = _('Fine tune how calibre behaves in various contexts')
|
||||||
|
|
||||||
|
class Misc(PreferencesPlugin):
|
||||||
|
name = 'Misc'
|
||||||
|
icon = I('exec.png')
|
||||||
|
gui_name = _('Miscellaneous')
|
||||||
|
category = 'Advanced'
|
||||||
|
gui_category = _('Advanced')
|
||||||
|
category_order = 5
|
||||||
|
name_order = 3
|
||||||
|
config_widget = 'calibre.gui2.preferences.misc'
|
||||||
|
description = _('Miscellaneous advanced configuration')
|
||||||
|
|
||||||
plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions,
|
plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions,
|
||||||
CommonOptions, OutputOptions, Adding]
|
CommonOptions, OutputOptions, Adding, Saving, Sending, Email, Server,
|
||||||
|
Plugins, Tweaks, Misc]
|
||||||
|
|
||||||
#}}}
|
#}}}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ class ITUNES(DriverBase):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
name = 'Apple device interface'
|
name = 'Apple device interface'
|
||||||
gui_name = 'Apple device'
|
gui_name = _('Apple device')
|
||||||
icon = I('devices/ipad.png')
|
icon = I('devices/ipad.png')
|
||||||
description = _('Communicate with iTunes/iBooks.')
|
description = _('Communicate with iTunes/iBooks.')
|
||||||
supported_platforms = ['osx','windows']
|
supported_platforms = ['osx','windows']
|
||||||
@ -2303,9 +2303,9 @@ class ITUNES(DriverBase):
|
|||||||
# Delete existing from Device|Books, add to self.update_list
|
# Delete existing from Device|Books, add to self.update_list
|
||||||
# for deletion from booklist[0] during add_books_to_metadata
|
# for deletion from booklist[0] during add_books_to_metadata
|
||||||
for book in self.cached_books:
|
for book in self.cached_books:
|
||||||
if self.cached_books[book]['uuid'] == metadata.uuid and \
|
if self.cached_books[book]['uuid'] == metadata.uuid or \
|
||||||
self.cached_books[book]['title'] == metadata.title and \
|
(self.cached_books[book]['title'] == metadata.title and \
|
||||||
self.cached_books[book]['author'] == metadata.authors[0]:
|
self.cached_books[book]['author'] == metadata.authors[0]):
|
||||||
self.update_list.append(self.cached_books[book])
|
self.update_list.append(self.cached_books[book])
|
||||||
self._remove_from_device(self.cached_books[book])
|
self._remove_from_device(self.cached_books[book])
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -2322,9 +2322,9 @@ class ITUNES(DriverBase):
|
|||||||
# Delete existing from Library|Books, add to self.update_list
|
# Delete existing from Library|Books, add to self.update_list
|
||||||
# for deletion from booklist[0] during add_books_to_metadata
|
# for deletion from booklist[0] during add_books_to_metadata
|
||||||
for book in self.cached_books:
|
for book in self.cached_books:
|
||||||
if self.cached_books[book]['uuid'] == metadata.uuid and \
|
if self.cached_books[book]['uuid'] == metadata.uuid or \
|
||||||
self.cached_books[book]['title'] == metadata.title and \
|
(self.cached_books[book]['title'] == metadata.title and \
|
||||||
self.cached_books[book]['author'] == metadata.authors[0]:
|
self.cached_books[book]['author'] == metadata.authors[0]):
|
||||||
self.update_list.append(self.cached_books[book])
|
self.update_list.append(self.cached_books[book])
|
||||||
self._remove_from_iTunes(self.cached_books[book])
|
self._remove_from_iTunes(self.cached_books[book])
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -2488,7 +2488,8 @@ class ITUNES(DriverBase):
|
|||||||
zf_opf.close()
|
zf_opf.close()
|
||||||
|
|
||||||
# If 'News' in tags, tweak the title/author for friendlier display in iBooks
|
# If 'News' in tags, tweak the title/author for friendlier display in iBooks
|
||||||
if _('News') in metadata.tags:
|
if _('News') in metadata.tags or \
|
||||||
|
_('Catalog') in metadata.tags:
|
||||||
if metadata.title.find('[') > 0:
|
if metadata.title.find('[') > 0:
|
||||||
metadata.title = metadata.title[:metadata.title.find('[')-1]
|
metadata.title = metadata.title[:metadata.title.find('[')-1]
|
||||||
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||||
|
@ -87,7 +87,7 @@ class MIBUK(USBMS):
|
|||||||
name = 'MiBuk Wolder Device Interface'
|
name = 'MiBuk Wolder Device Interface'
|
||||||
description = _('Communicate with the MiBuk Wolder reader.')
|
description = _('Communicate with the MiBuk Wolder reader.')
|
||||||
author = 'Kovid Goyal'
|
author = 'Kovid Goyal'
|
||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows']
|
||||||
|
|
||||||
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'txt', 'rtf', 'pdf']
|
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'txt', 'rtf', 'pdf']
|
||||||
|
|
||||||
|
@ -44,16 +44,17 @@ class Book(MetaInformation):
|
|||||||
self.mime = mime
|
self.mime = mime
|
||||||
|
|
||||||
self.size = size # will be set later if None
|
self.size = size # will be set later if None
|
||||||
try:
|
|
||||||
if ContentType == '6':
|
|
||||||
self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")
|
|
||||||
else:
|
|
||||||
self.datetime = time.gmtime(os.path.getctime(self.path))
|
|
||||||
except:
|
|
||||||
self.datetime = time.gmtime()
|
|
||||||
|
|
||||||
if thumbnail_name is not None:
|
if ContentType == '6':
|
||||||
self.thumbnail = ImageWrapper(thumbnail_name)
|
self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.datetime = time.gmtime(os.path.getctime(self.path))
|
||||||
|
except:
|
||||||
|
self.datetime = time.gmtime()
|
||||||
|
|
||||||
|
if thumbnail_name is not None:
|
||||||
|
self.thumbnail = ImageWrapper(thumbnail_name)
|
||||||
self.tags = []
|
self.tags = []
|
||||||
if other:
|
if other:
|
||||||
self.smart_update(other)
|
self.smart_update(other)
|
||||||
|
@ -106,11 +106,14 @@ class KOBO(USBMS):
|
|||||||
changed = True
|
changed = True
|
||||||
bl[idx].device_collections = playlist_map.get(lpath, [])
|
bl[idx].device_collections = playlist_map.get(lpath, [])
|
||||||
else:
|
else:
|
||||||
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
|
if ContentType == '6':
|
||||||
|
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
||||||
|
else:
|
||||||
|
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
|
||||||
# print 'Update booklist'
|
# print 'Update booklist'
|
||||||
|
book.device_collections = playlist_map.get(book.lpath, [])
|
||||||
if bl.add_book(book, replace_metadata=False):
|
if bl.add_book(book, replace_metadata=False):
|
||||||
changed = True
|
changed = True
|
||||||
book.device_collections = playlist_map.get(book.lpath, [])
|
|
||||||
except: # Probably a path encoding error
|
except: # Probably a path encoding error
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -231,21 +234,9 @@ class KOBO(USBMS):
|
|||||||
path = self.normalize_path(path)
|
path = self.normalize_path(path)
|
||||||
# print "Delete file normalized path: " + path
|
# print "Delete file normalized path: " + path
|
||||||
extension = os.path.splitext(path)[1]
|
extension = os.path.splitext(path)[1]
|
||||||
|
ContentType = self.get_content_type_from_extension(extension)
|
||||||
|
|
||||||
if extension == '.kobo':
|
ContentID = self.contentid_from_path(path, ContentType)
|
||||||
# Kobo books do not have book files. They do have some images though
|
|
||||||
#print "kobo book"
|
|
||||||
ContentType = 6
|
|
||||||
ContentID = self.contentid_from_path(path, ContentType)
|
|
||||||
elif extension == '.pdf' or extension == '.epub':
|
|
||||||
# print "ePub or pdf"
|
|
||||||
ContentType = 16
|
|
||||||
#print "Path: " + path
|
|
||||||
ContentID = self.contentid_from_path(path, ContentType)
|
|
||||||
# print "ContentID: " + ContentID
|
|
||||||
else: # if extension == '.html' or extension == '.txt':
|
|
||||||
ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored
|
|
||||||
ContentID = self.contentid_from_path(path, ContentType)
|
|
||||||
|
|
||||||
ImageID = self.delete_via_sql(ContentID, ContentType)
|
ImageID = self.delete_via_sql(ContentID, ContentType)
|
||||||
#print " We would now delete the Images for" + ImageID
|
#print " We would now delete the Images for" + ImageID
|
||||||
@ -343,6 +334,17 @@ class KOBO(USBMS):
|
|||||||
ContentID = ContentID.replace("\\", '/')
|
ContentID = ContentID.replace("\\", '/')
|
||||||
return ContentID
|
return ContentID
|
||||||
|
|
||||||
|
def get_content_type_from_extension(self, extension):
|
||||||
|
if extension == '.kobo':
|
||||||
|
# Kobo books do not have book files. They do have some images though
|
||||||
|
#print "kobo book"
|
||||||
|
ContentType = 6
|
||||||
|
elif extension == '.pdf' or extension == '.epub':
|
||||||
|
# print "ePub or pdf"
|
||||||
|
ContentType = 16
|
||||||
|
else: # if extension == '.html' or extension == '.txt':
|
||||||
|
ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored
|
||||||
|
return ContentType
|
||||||
|
|
||||||
def path_from_contentid(self, ContentID, ContentType, oncard):
|
def path_from_contentid(self, ContentID, ContentType, oncard):
|
||||||
path = ContentID
|
path = ContentID
|
||||||
|
@ -108,4 +108,23 @@ class PDNOVEL(USBMS):
|
|||||||
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||||
coverfile.write(coverdata[2])
|
coverfile.write(coverdata[2])
|
||||||
|
|
||||||
|
class GEMEI(USBMS):
|
||||||
|
name = 'Gemei Device Interface'
|
||||||
|
gui_name = 'GM2000'
|
||||||
|
description = _('Communicate with the GM2000')
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
|
||||||
|
# Ordered list of supported formats
|
||||||
|
FORMATS = ['epub', 'chm', 'html', 'pdb', 'pdf', 'txt']
|
||||||
|
|
||||||
|
VENDOR_ID = [0x07c4]
|
||||||
|
PRODUCT_ID = [0xa4a5]
|
||||||
|
BCD = None
|
||||||
|
|
||||||
|
VENDOR_NAME = 'CHINA'
|
||||||
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'CHIP'
|
||||||
|
|
||||||
|
EBOOK_DIR_MAIN = 'eBooks'
|
||||||
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
|
@ -89,6 +89,10 @@ class XMLCache(object):
|
|||||||
raw, strip_encoding_pats=True, assume_utf8=True,
|
raw, strip_encoding_pats=True, assume_utf8=True,
|
||||||
verbose=DEBUG)[0],
|
verbose=DEBUG)[0],
|
||||||
parser=parser)
|
parser=parser)
|
||||||
|
if self.roots[source_id] is None:
|
||||||
|
raise Exception(('The SONY database at %s is corrupted. Try '
|
||||||
|
' disconnecting and reconnecting your reader.')%path)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
||||||
|
@ -138,3 +138,11 @@ def check_ebook_format(stream, current_guess):
|
|||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def calibre_cover(title, author_string, series_string=None,
|
||||||
|
output_format='jpg', title_size=46, author_size=36):
|
||||||
|
from calibre.utils.magick.draw import create_cover_page, TextLine
|
||||||
|
lines = [TextLine(title, title_size), TextLine(author_string, author_size)]
|
||||||
|
if series_string:
|
||||||
|
lines.append(TextLine(series_string, author_size))
|
||||||
|
return create_cover_page(lines, I('library.png'), output_format='jpg')
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class ArchiveExtract(FileTypePlugin):
|
|||||||
fname = fnames[0]
|
fname = fnames[0]
|
||||||
ext = os.path.splitext(fname)[1][1:]
|
ext = os.path.splitext(fname)[1][1:]
|
||||||
if ext.lower() not in ('lit', 'epub', 'mobi', 'prc', 'rtf', 'pdf',
|
if ext.lower() not in ('lit', 'epub', 'mobi', 'prc', 'rtf', 'pdf',
|
||||||
'mp3', 'pdb', 'azw', 'azw1'):
|
'mp3', 'pdb', 'azw', 'azw1', 'fb2'):
|
||||||
return archive
|
return archive
|
||||||
|
|
||||||
of = self.temporary_file('_archive_extract.'+ext)
|
of = self.temporary_file('_archive_extract.'+ext)
|
||||||
|
@ -1105,7 +1105,8 @@ class OPFCreator(MetaInformation):
|
|||||||
spine.set('toc', 'ncx')
|
spine.set('toc', 'ncx')
|
||||||
if self.spine is not None:
|
if self.spine is not None:
|
||||||
for ref in self.spine:
|
for ref in self.spine:
|
||||||
spine.append(E.itemref(idref=ref.id))
|
if ref.id is not None:
|
||||||
|
spine.append(E.itemref(idref=ref.id))
|
||||||
guide = E.guide()
|
guide = E.guide()
|
||||||
if self.guide is not None:
|
if self.guide is not None:
|
||||||
for ref in self.guide:
|
for ref in self.guide:
|
||||||
|
@ -1695,12 +1695,11 @@ class MobiWriter(object):
|
|||||||
header.write(pack('>I', 1))
|
header.write(pack('>I', 1))
|
||||||
|
|
||||||
# 0x1c - 0x1f : Text encoding ?
|
# 0x1c - 0x1f : Text encoding ?
|
||||||
# header.write(pack('>I', 650001))
|
# GR: Language encoding for NCX entries (latin_1)
|
||||||
# GR: This needs to be either 0xFDE9 or 0x4E4
|
header.write(pack('>I', 0x4e4))
|
||||||
header.write(pack('>I', 0xFDE9))
|
|
||||||
|
|
||||||
# 0x20 - 0x23 : Language code?
|
# 0x20 - 0x23 : Mimicking kindleGen
|
||||||
header.write(iana2mobi(str(self._oeb.metadata.language[0])))
|
header.write(pack('>I', 0xFFFFFFFF))
|
||||||
|
|
||||||
# 0x24 - 0x27 : Number of TOC entries in INDX1
|
# 0x24 - 0x27 : Number of TOC entries in INDX1
|
||||||
header.write(pack('>I', indxt_count + 1))
|
header.write(pack('>I', indxt_count + 1))
|
||||||
@ -1795,13 +1794,12 @@ class MobiWriter(object):
|
|||||||
self._oeb.log.debug('Index records dumped to', t)
|
self._oeb.log.debug('Index records dumped to', t)
|
||||||
|
|
||||||
def _clean_text_value(self, text):
|
def _clean_text_value(self, text):
|
||||||
if text is not None and text.strip() :
|
if not text:
|
||||||
text = text.strip()
|
text = u'(none)'
|
||||||
if not isinstance(text, unicode):
|
text = text.strip()
|
||||||
text = text.decode('utf-8', 'replace')
|
if not isinstance(text, unicode):
|
||||||
text = text.encode('utf-8')
|
text = text.decode('utf-8', 'replace')
|
||||||
else :
|
text = text.encode('cp1252','replace')
|
||||||
text = "(none)".encode('utf-8')
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _add_to_ctoc(self, ctoc_str, record_offset):
|
def _add_to_ctoc(self, ctoc_str, record_offset):
|
||||||
@ -2151,6 +2149,26 @@ class MobiWriter(object):
|
|||||||
indxt.write(decint(self._ctoc_map[index]['titleOffset'], DECINT_FORWARD)) # vwi title offset in CNCX
|
indxt.write(decint(self._ctoc_map[index]['titleOffset'], DECINT_FORWARD)) # vwi title offset in CNCX
|
||||||
indxt.write(decint(0, DECINT_FORWARD)) # unknown byte
|
indxt.write(decint(0, DECINT_FORWARD)) # unknown byte
|
||||||
|
|
||||||
|
def _write_subchapter_node(self, indxt, indices, index, offset, length, count):
|
||||||
|
# This style works without a parent chapter, mimicking what KindleGen does,
|
||||||
|
# using a value of 0x0B for parentIndex
|
||||||
|
# Writes an INDX1 NCXEntry of entryType 0x1F - subchapter
|
||||||
|
if self.opts.verbose > 2:
|
||||||
|
# *** GR: Turn this off while I'm developing my code
|
||||||
|
#self._oeb.log.debug('Writing TOC node to IDXT:', node.title, 'href:', node.href)
|
||||||
|
pass
|
||||||
|
|
||||||
|
pos = 0xc0 + indxt.tell()
|
||||||
|
indices.write(pack('>H', pos)) # Save the offset for IDXTIndices
|
||||||
|
name = "%04X"%count
|
||||||
|
indxt.write(chr(len(name)) + name) # Write the name
|
||||||
|
indxt.write(INDXT['subchapter']) # entryType [0x0F | 0xDF | 0xFF | 0x3F]
|
||||||
|
indxt.write(decint(offset, DECINT_FORWARD)) # offset
|
||||||
|
indxt.write(decint(length, DECINT_FORWARD)) # length
|
||||||
|
indxt.write(decint(self._ctoc_map[index]['titleOffset'], DECINT_FORWARD)) # vwi title offset in CNCX
|
||||||
|
indxt.write(decint(0, DECINT_FORWARD)) # unknown byte
|
||||||
|
indxt.write(decint(0xb, DECINT_FORWARD)) # parentIndex - null
|
||||||
|
|
||||||
def _compute_offset_length(self, i, node, entries) :
|
def _compute_offset_length(self, i, node, entries) :
|
||||||
h = node.href
|
h = node.href
|
||||||
if h not in self._id_offsets:
|
if h not in self._id_offsets:
|
||||||
|
@ -89,19 +89,22 @@ class CoverManager(object):
|
|||||||
'''
|
'''
|
||||||
Create a generic cover for books that dont have a cover
|
Create a generic cover for books that dont have a cover
|
||||||
'''
|
'''
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string, fmt_sidx
|
||||||
if self.no_default_cover:
|
if self.no_default_cover:
|
||||||
return None
|
return None
|
||||||
self.log('Generating default cover')
|
self.log('Generating default cover')
|
||||||
m = self.oeb.metadata
|
m = self.oeb.metadata
|
||||||
title = unicode(m.title[0])
|
title = unicode(m.title[0])
|
||||||
authors = [unicode(x) for x in m.creator if x.role == 'aut']
|
authors = [unicode(x) for x in m.creator if x.role == 'aut']
|
||||||
|
series_string = None
|
||||||
|
if m.series and m.series_index:
|
||||||
|
series_string = _('Book %s of %s')%(
|
||||||
|
fmt_sidx(m.series_index[0], use_roman=True), m.series[0])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.utils.magick.draw import create_cover_page, TextLine
|
from calibre.ebooks import calibre_cover
|
||||||
lines = [TextLine(title, 44), TextLine(authors_to_string(authors),
|
img_data = calibre_cover(title, authors_to_string(authors),
|
||||||
32)]
|
series_string=series_string)
|
||||||
img_data = create_cover_page(lines, I('library.png'))
|
|
||||||
id, href = self.oeb.manifest.generate('cover_image',
|
id, href = self.oeb.manifest.generate('cover_image',
|
||||||
'cover_image.jpg')
|
'cover_image.jpg')
|
||||||
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
|
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
|
||||||
|
@ -26,14 +26,18 @@ class GenerateCatalogAction(InterfaceAction):
|
|||||||
rows = xrange(self.gui.library_view.model().rowCount(QModelIndex()))
|
rows = xrange(self.gui.library_view.model().rowCount(QModelIndex()))
|
||||||
ids = map(self.gui.library_view.model().id, rows)
|
ids = map(self.gui.library_view.model().id, rows)
|
||||||
|
|
||||||
dbspec = None
|
|
||||||
if not ids:
|
if not ids:
|
||||||
return error_dialog(self.gui, _('No books selected'),
|
return error_dialog(self.gui, _('No books selected'),
|
||||||
_('No books selected to generate catalog for'),
|
_('No books selected to generate catalog for'),
|
||||||
show=True)
|
show=True)
|
||||||
|
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
dbspec = {}
|
||||||
|
for id in ids:
|
||||||
|
dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)}
|
||||||
|
|
||||||
# Calling gui2.tools:generate_catalog()
|
# Calling gui2.tools:generate_catalog()
|
||||||
ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager.device)
|
ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager)
|
||||||
if ret is None:
|
if ret is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -177,7 +177,16 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
return error_dialog(self.gui, _('Already exists'),
|
return error_dialog(self.gui, _('Already exists'),
|
||||||
_('The folder %s already exists. Delete it first.') %
|
_('The folder %s already exists. Delete it first.') %
|
||||||
newloc, show=True)
|
newloc, show=True)
|
||||||
os.rename(loc, newloc)
|
try:
|
||||||
|
os.rename(loc, newloc)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
error_dialog(self.gui, _('Rename failed'),
|
||||||
|
_('Failed to rename the library at %s. '
|
||||||
|
'The most common cause for this is if one of the files'
|
||||||
|
' in the library is open in another program.') % loc,
|
||||||
|
det_msg=traceback.format_exc(), show=True)
|
||||||
|
return
|
||||||
self.stats.rename(location, newloc)
|
self.stats.rename(location, newloc)
|
||||||
self.build_menus()
|
self.build_menus()
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ class DeleteAction(InterfaceAction):
|
|||||||
if self.gui.stack.currentIndex() == 0:
|
if self.gui.stack.currentIndex() == 0:
|
||||||
if not confirm('<p>'+_('The selected books will be '
|
if not confirm('<p>'+_('The selected books will be '
|
||||||
'<b>permanently deleted</b> and the files '
|
'<b>permanently deleted</b> and the files '
|
||||||
'removed from your computer. Are you sure?')
|
'removed from your calibre library. Are you sure?')
|
||||||
+'</p>', 'library_delete_books', self.gui):
|
+'</p>', 'library_delete_books', self.gui):
|
||||||
return
|
return
|
||||||
ci = view.currentIndex()
|
ci = view.currentIndex()
|
||||||
|
@ -122,7 +122,7 @@ class ConnectShareAction(InterfaceAction):
|
|||||||
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
|
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
|
||||||
self.share_conn_menu.config_email.connect(partial(
|
self.share_conn_menu.config_email.connect(partial(
|
||||||
self.gui.iactions['Preferences'].do_config,
|
self.gui.iactions['Preferences'].do_config,
|
||||||
initial_category='email'))
|
initial_plugin=('Sharing', 'Email')))
|
||||||
self.qaction.setMenu(self.share_conn_menu)
|
self.qaction.setMenu(self.share_conn_menu)
|
||||||
self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder)
|
self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder)
|
||||||
self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes)
|
self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes)
|
||||||
|
@ -51,7 +51,8 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
self.merge_books)
|
self.merge_books)
|
||||||
mb.addSeparator()
|
mb.addSeparator()
|
||||||
mb.addAction(_('Merge into first selected book - keep others'),
|
mb.addAction(_('Merge into first selected book - keep others'),
|
||||||
partial(self.merge_books, safe_merge=True))
|
partial(self.merge_books, safe_merge=True),
|
||||||
|
Qt.AltModifier+Qt.Key_M)
|
||||||
self.merge_menu = mb
|
self.merge_menu = mb
|
||||||
self.action_merge.setMenu(mb)
|
self.action_merge.setMenu(mb)
|
||||||
md.addSeparator()
|
md.addSeparator()
|
||||||
|
@ -8,8 +8,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
from PyQt4.Qt import QIcon, QMenu
|
from PyQt4.Qt import QIcon, QMenu
|
||||||
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.gui2.dialogs.config import ConfigDialog
|
from calibre.gui2.preferences.main import Preferences
|
||||||
from calibre.gui2 import error_dialog, config
|
from calibre.gui2 import error_dialog
|
||||||
|
|
||||||
class PreferencesAction(InterfaceAction):
|
class PreferencesAction(InterfaceAction):
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ class PreferencesAction(InterfaceAction):
|
|||||||
x.triggered.connect(self.do_config)
|
x.triggered.connect(self.do_config)
|
||||||
|
|
||||||
|
|
||||||
def do_config(self, checked=False, initial_category='general'):
|
def do_config(self, checked=False, initial_plugin=None):
|
||||||
if self.gui.job_manager.has_jobs():
|
if self.gui.job_manager.has_jobs():
|
||||||
d = error_dialog(self.gui, _('Cannot configure'),
|
d = error_dialog(self.gui, _('Cannot configure'),
|
||||||
_('Cannot configure while there are running jobs.'))
|
_('Cannot configure while there are running jobs.'))
|
||||||
@ -39,26 +39,7 @@ class PreferencesAction(InterfaceAction):
|
|||||||
_('Cannot configure before calibre is restarted.'))
|
_('Cannot configure before calibre is restarted.'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
d = ConfigDialog(self.gui, self.gui.library_view,
|
d = Preferences(self.gui, initial_plugin=initial_plugin)
|
||||||
server=self.gui.content_server, initial_category=initial_category)
|
d.show()
|
||||||
|
|
||||||
d.exec_()
|
|
||||||
self.gui.content_server = d.server
|
|
||||||
if self.gui.content_server is not None:
|
|
||||||
self.gui.content_server.state_callback = \
|
|
||||||
self.Dispatcher(self.gui.iactions['Connect Share'].content_server_state_changed)
|
|
||||||
self.gui.content_server.state_callback(self.gui.content_server.is_running)
|
|
||||||
|
|
||||||
if d.result() == d.Accepted:
|
|
||||||
self.gui.search.search_as_you_type(config['search_as_you_type'])
|
|
||||||
self.gui.tags_view.set_new_model() # in case columns changed
|
|
||||||
self.gui.iactions['Save To Disk'].reread_prefs()
|
|
||||||
self.gui.tags_view.recount()
|
|
||||||
self.gui.create_device_menu()
|
|
||||||
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
|
|
||||||
self.gui.tool_bar.build_bar()
|
|
||||||
self.gui.build_context_menus()
|
|
||||||
self.gui.tool_bar.apply_settings()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ class PluginWidget(QWidget,Ui_Form):
|
|||||||
OPTION_FIELDS = [('exclude_genre','\[.+\]'),
|
OPTION_FIELDS = [('exclude_genre','\[.+\]'),
|
||||||
('exclude_tags','~,'+_('Catalog')),
|
('exclude_tags','~,'+_('Catalog')),
|
||||||
('generate_titles', True),
|
('generate_titles', True),
|
||||||
|
('generate_series', True),
|
||||||
('generate_recently_added', True),
|
('generate_recently_added', True),
|
||||||
('note_tag','*'),
|
('note_tag','*'),
|
||||||
('numbers_as_text', False),
|
('numbers_as_text', False),
|
||||||
@ -40,7 +41,7 @@ class PluginWidget(QWidget,Ui_Form):
|
|||||||
# Update dialog fields from stored options
|
# Update dialog fields from stored options
|
||||||
for opt in self.OPTION_FIELDS:
|
for opt in self.OPTION_FIELDS:
|
||||||
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
|
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
|
||||||
if opt[0] in ['numbers_as_text','generate_titles','generate_recently_added']:
|
if opt[0] in ['numbers_as_text','generate_titles','generate_series','generate_recently_added']:
|
||||||
getattr(self, opt[0]).setChecked(opt_value)
|
getattr(self, opt[0]).setChecked(opt_value)
|
||||||
else:
|
else:
|
||||||
getattr(self, opt[0]).setText(opt_value)
|
getattr(self, opt[0]).setText(opt_value)
|
||||||
@ -52,13 +53,13 @@ class PluginWidget(QWidget,Ui_Form):
|
|||||||
# others store as lists
|
# others store as lists
|
||||||
opts_dict = {}
|
opts_dict = {}
|
||||||
for opt in self.OPTION_FIELDS:
|
for opt in self.OPTION_FIELDS:
|
||||||
if opt[0] in ['numbers_as_text','generate_titles','generate_recently_added']:
|
if opt[0] in ['numbers_as_text','generate_titles','generate_series','generate_recently_added']:
|
||||||
opt_value = getattr(self,opt[0]).isChecked()
|
opt_value = getattr(self,opt[0]).isChecked()
|
||||||
else:
|
else:
|
||||||
opt_value = unicode(getattr(self, opt[0]).text())
|
opt_value = unicode(getattr(self, opt[0]).text())
|
||||||
gprefs.set(self.name + '_' + opt[0], opt_value)
|
gprefs.set(self.name + '_' + opt[0], opt_value)
|
||||||
|
|
||||||
if opt[0] in ['exclude_genre','numbers_as_text','generate_titles','generate_recently_added']:
|
if opt[0] in ['exclude_genre','numbers_as_text','generate_titles','generate_series','generate_recently_added']:
|
||||||
opts_dict[opt[0]] = opt_value
|
opts_dict[opt[0]] = opt_value
|
||||||
else:
|
else:
|
||||||
opts_dict[opt[0]] = opt_value.split(',')
|
opts_dict[opt[0]] = opt_value.split(',')
|
||||||
|
@ -108,20 +108,27 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0">
|
<item row="10" column="0">
|
||||||
<widget class="QCheckBox" name="generate_recently_added">
|
<widget class="QCheckBox" name="generate_recently_added">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Include 'Recently Added' Section</string>
|
<string>Include 'Recently Added' Section</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="0">
|
<item row="11" column="0">
|
||||||
<widget class="QCheckBox" name="numbers_as_text">
|
<widget class="QCheckBox" name="numbers_as_text">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Sort numbers as text</string>
|
<string>Sort numbers as text</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="9" column="0">
|
||||||
|
<widget class="QCheckBox" name="generate_series">
|
||||||
|
<property name="text">
|
||||||
|
<string>Include 'Series' Section</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
@ -58,7 +58,8 @@ class BulkConfig(Config):
|
|||||||
output_path = 'dummy.'+output_format
|
output_path = 'dummy.'+output_format
|
||||||
log = Log()
|
log = Log()
|
||||||
log.outputs = []
|
log.outputs = []
|
||||||
self.plumber = Plumber(input_path, output_path, log)
|
self.plumber = Plumber(input_path, output_path, log,
|
||||||
|
merge_plugin_recs=False)
|
||||||
|
|
||||||
def widget_factory(cls):
|
def widget_factory(cls):
|
||||||
return cls(self.stack, self.plumber.get_option_by_name,
|
return cls(self.stack, self.plumber.get_option_by_name,
|
||||||
|
@ -27,13 +27,9 @@ def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, conne
|
|||||||
notification=DummyReporter(), log=None):
|
notification=DummyReporter(), log=None):
|
||||||
if log is None:
|
if log is None:
|
||||||
log = Log()
|
log = Log()
|
||||||
if dbspec is None:
|
from calibre.library import db
|
||||||
from calibre.utils.config import prefs
|
db = db()
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
db.catalog_plugin_on_device_temp_mapping = dbspec
|
||||||
dbpath = prefs['library_path']
|
|
||||||
db = LibraryDatabase2(dbpath)
|
|
||||||
else: # To be implemented in the future
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Create a minimal OptionParser that we can append to
|
# Create a minimal OptionParser that we can append to
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
@ -760,8 +760,8 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
def refresh_ondevice_info(self, device_connected, reset_only = False):
|
def refresh_ondevice_info(self, device_connected, reset_only = False):
|
||||||
'''
|
'''
|
||||||
Force the library view to refresh, taking into consideration
|
Force the library view to refresh, taking into consideration new
|
||||||
books information
|
device books information
|
||||||
'''
|
'''
|
||||||
self.book_on_device(None, reset=True)
|
self.book_on_device(None, reset=True)
|
||||||
if reset_only:
|
if reset_only:
|
||||||
@ -791,12 +791,14 @@ class DeviceMixin(object): # {{{
|
|||||||
self.booklists())
|
self.booklists())
|
||||||
model.paths_deleted(paths)
|
model.paths_deleted(paths)
|
||||||
self.upload_booklists()
|
self.upload_booklists()
|
||||||
# Clear the ondevice info so it will be recomputed
|
# Force recomputation the library's ondevice info. We need to call
|
||||||
|
# set_books_in_library even though books were not added because
|
||||||
|
# the deleted book might have been an exact match.
|
||||||
|
self.set_books_in_library(self.booklists(), reset=True)
|
||||||
self.book_on_device(None, None, reset=True)
|
self.book_on_device(None, None, reset=True)
|
||||||
# We want to reset all the ondevice flags in the library. Use a big
|
# We need to reset the ondevice flags in the library. Use a big hammer,
|
||||||
# hammer, so we don't need to worry about whether some succeeded or not
|
# so we don't need to worry about whether some succeeded or not.
|
||||||
self.library_view.model().refresh()
|
self.refresh_ondevice_info(device_connected=True, reset_only=False)
|
||||||
|
|
||||||
|
|
||||||
def dispatch_sync_event(self, dest, delete, specific):
|
def dispatch_sync_event(self, dest, delete, specific):
|
||||||
rows = self.library_view.selectionModel().selectedRows()
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
@ -1286,8 +1288,14 @@ class DeviceMixin(object): # {{{
|
|||||||
books_to_be_deleted = memory[1]
|
books_to_be_deleted = memory[1]
|
||||||
self.library_view.model().delete_books_by_id(books_to_be_deleted)
|
self.library_view.model().delete_books_by_id(books_to_be_deleted)
|
||||||
|
|
||||||
self.set_books_in_library(self.booklists(),
|
# There are some cases where sending a book to the device overwrites a
|
||||||
reset=bool(books_to_be_deleted))
|
# book already there with a different book. This happens frequently in
|
||||||
|
# news. When this happens, the book match indication will be wrong
|
||||||
|
# because the UUID changed. Force both the device and the library view
|
||||||
|
# to refresh the flags.
|
||||||
|
self.set_books_in_library(self.booklists(), reset=True)
|
||||||
|
self.book_on_device(None, reset=True)
|
||||||
|
self.refresh_ondevice_info(device_connected = True)
|
||||||
|
|
||||||
view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view
|
view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view
|
||||||
view.model().resort(reset=False)
|
view.model().resort(reset=False)
|
||||||
@ -1295,112 +1303,108 @@ class DeviceMixin(object): # {{{
|
|||||||
for f in files:
|
for f in files:
|
||||||
getattr(f, 'close', lambda : True)()
|
getattr(f, 'close', lambda : True)()
|
||||||
|
|
||||||
self.book_on_device(None, reset=True)
|
|
||||||
if metadata:
|
|
||||||
changed = set([])
|
|
||||||
for mi in metadata:
|
|
||||||
id_ = getattr(mi, 'application_id', None)
|
|
||||||
if id_ is not None:
|
|
||||||
changed.add(id_)
|
|
||||||
if changed:
|
|
||||||
self.library_view.model().refresh_ids(list(changed))
|
|
||||||
|
|
||||||
def book_on_device(self, id, format=None, reset=False):
|
def book_on_device(self, id, format=None, reset=False):
|
||||||
loc = [None, None, None]
|
'''
|
||||||
|
Return an indication of whether the given book represented by its db id
|
||||||
|
is on the currently connected device. It returns a 5 element list. The
|
||||||
|
first three elements represent memory locations main, carda, and cardb,
|
||||||
|
and are true if the book is identifiably in that memory. The fourth
|
||||||
|
is a count of how many instances of the book were found across all
|
||||||
|
the memory locations. The fifth is a set of paths to the
|
||||||
|
matching books on the device.
|
||||||
|
'''
|
||||||
|
loc = [None, None, None, 0, set([])]
|
||||||
|
|
||||||
if reset:
|
if reset:
|
||||||
self.book_db_title_cache = None
|
self.book_db_id_cache = None
|
||||||
self.book_db_uuid_cache = None
|
self.book_db_id_counts = None
|
||||||
|
self.book_db_uuid_path_map = None
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.book_db_title_cache is None:
|
if not hasattr(self, 'db_book_uuid_cache'):
|
||||||
self.book_db_title_cache = []
|
return loc
|
||||||
self.book_db_uuid_cache = []
|
|
||||||
|
if self.book_db_id_cache is None:
|
||||||
|
self.book_db_id_cache = []
|
||||||
|
self.book_db_id_counts = {}
|
||||||
|
self.book_db_uuid_path_map = {}
|
||||||
for i, l in enumerate(self.booklists()):
|
for i, l in enumerate(self.booklists()):
|
||||||
self.book_db_title_cache.append({})
|
self.book_db_id_cache.append(set())
|
||||||
self.book_db_uuid_cache.append(set())
|
|
||||||
for book in l:
|
for book in l:
|
||||||
book_title = book.title.lower() if book.title else ''
|
|
||||||
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
|
||||||
if book_title not in self.book_db_title_cache[i]:
|
|
||||||
self.book_db_title_cache[i][book_title] = \
|
|
||||||
{'authors':set(), 'db_ids':set(), 'uuids':set()}
|
|
||||||
book_authors = authors_to_string(book.authors).lower()
|
|
||||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
|
||||||
self.book_db_title_cache[i][book_title]['authors'].add(book_authors)
|
|
||||||
db_id = getattr(book, 'application_id', None)
|
db_id = getattr(book, 'application_id', None)
|
||||||
if db_id is None:
|
if db_id is None:
|
||||||
db_id = book.db_id
|
db_id = book.db_id
|
||||||
if db_id is not None:
|
if db_id is not None:
|
||||||
self.book_db_title_cache[i][book_title]['db_ids'].add(db_id)
|
# increment the count of books on the device with this
|
||||||
uuid = getattr(book, 'uuid', None)
|
# db_id.
|
||||||
if uuid is not None:
|
self.book_db_id_cache[i].add(db_id)
|
||||||
self.book_db_uuid_cache[i].add(uuid)
|
if db_id not in self.book_db_uuid_path_map:
|
||||||
|
self.book_db_uuid_path_map[db_id] = set()
|
||||||
|
if getattr(book, 'lpath', False):
|
||||||
|
self.book_db_uuid_path_map[db_id].add(book.lpath)
|
||||||
|
c = self.book_db_id_counts.get(db_id, 0)
|
||||||
|
self.book_db_id_counts[db_id] = c + 1
|
||||||
|
|
||||||
mi = self.library_view.model().db.get_metadata(id, index_is_id=True)
|
|
||||||
for i, l in enumerate(self.booklists()):
|
for i, l in enumerate(self.booklists()):
|
||||||
if mi.uuid in self.book_db_uuid_cache[i]:
|
if id in self.book_db_id_cache[i]:
|
||||||
loc[i] = True
|
loc[i] = True
|
||||||
continue
|
loc[3] = self.book_db_id_counts.get(id, 0)
|
||||||
db_title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
loc[4] |= self.book_db_uuid_path_map[id]
|
||||||
cache = self.book_db_title_cache[i].get(db_title, None)
|
|
||||||
if cache:
|
|
||||||
if id in cache['db_ids']:
|
|
||||||
loc[i] = True
|
|
||||||
continue
|
|
||||||
if mi.authors and \
|
|
||||||
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
|
|
||||||
in cache['authors']:
|
|
||||||
loc[i] = True
|
|
||||||
continue
|
|
||||||
# Also check author sort, because it can be used as author in
|
|
||||||
# some formats
|
|
||||||
if mi.author_sort and \
|
|
||||||
re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \
|
|
||||||
in cache['authors']:
|
|
||||||
loc[i] = True
|
|
||||||
continue
|
|
||||||
return loc
|
return loc
|
||||||
|
|
||||||
def set_books_in_library(self, booklists, reset=False):
|
def set_books_in_library(self, booklists, reset=False):
|
||||||
|
'''
|
||||||
|
Set the ondevice indications in the device database.
|
||||||
|
This method should be called before book_on_device is called, because
|
||||||
|
it sets the application_id for matched books. Book_on_device uses that
|
||||||
|
to both speed up matching and to count matches.
|
||||||
|
'''
|
||||||
|
|
||||||
|
string_pat = re.compile('(?u)\W|[_]')
|
||||||
|
def clean_string(x):
|
||||||
|
x = x.lower() if x else ''
|
||||||
|
return string_pat.sub('', x)
|
||||||
|
|
||||||
# Force a reset if the caches are not initialized
|
# Force a reset if the caches are not initialized
|
||||||
if reset or not hasattr(self, 'db_book_title_cache'):
|
if reset or not hasattr(self, 'db_book_title_cache'):
|
||||||
# It might be possible to get here without having initialized the
|
# It might be possible to get here without having initialized the
|
||||||
# library view. In this case, simply give up
|
# library view. In this case, simply give up
|
||||||
if not hasattr(self, 'library_view') or self.library_view is None:
|
try:
|
||||||
return
|
db = self.library_view.model().db
|
||||||
db = getattr(self.library_view.model(), 'db', None)
|
except:
|
||||||
if db is None:
|
|
||||||
return
|
return
|
||||||
# Build a cache (map) of the library, so the search isn't On**2
|
# Build a cache (map) of the library, so the search isn't On**2
|
||||||
self.db_book_title_cache = {}
|
self.db_book_title_cache = {}
|
||||||
self.db_book_uuid_cache = {}
|
self.db_book_uuid_cache = {}
|
||||||
for id in db.data.iterallids():
|
for id in db.data.iterallids():
|
||||||
mi = db.get_metadata(id, index_is_id=True)
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
title = clean_string(mi.title)
|
||||||
if title not in self.db_book_title_cache:
|
if title not in self.db_book_title_cache:
|
||||||
self.db_book_title_cache[title] = \
|
self.db_book_title_cache[title] = \
|
||||||
{'authors':{}, 'author_sort':{}, 'db_ids':{}}
|
{'authors':{}, 'author_sort':{}, 'db_ids':{}}
|
||||||
|
# If there are multiple books in the library with the same title
|
||||||
|
# and author, then remember the last one. That is OK, because as
|
||||||
|
# we can't tell the difference between the books, one is as good
|
||||||
|
# as another.
|
||||||
if mi.authors:
|
if mi.authors:
|
||||||
authors = authors_to_string(mi.authors).lower()
|
authors = clean_string(authors_to_string(mi.authors))
|
||||||
authors = re.sub('(?u)\W|[_]', '', authors)
|
|
||||||
self.db_book_title_cache[title]['authors'][authors] = mi
|
self.db_book_title_cache[title]['authors'][authors] = mi
|
||||||
if mi.author_sort:
|
if mi.author_sort:
|
||||||
aus = mi.author_sort.lower()
|
aus = clean_string(mi.author_sort)
|
||||||
aus = re.sub('(?u)\W|[_]', '', aus)
|
|
||||||
self.db_book_title_cache[title]['author_sort'][aus] = mi
|
self.db_book_title_cache[title]['author_sort'][aus] = mi
|
||||||
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
|
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
|
||||||
self.db_book_uuid_cache[mi.uuid] = mi
|
self.db_book_uuid_cache[mi.uuid] = mi
|
||||||
|
|
||||||
# Now iterate through all the books on the device, setting the
|
# Now iterate through all the books on the device, setting the
|
||||||
# in_library field Fastest and most accurate key is the uuid. Second is
|
# in_library field. If the UUID matches a book in the library, then
|
||||||
# the application_id, which is really the db key, but as this can
|
# do not consider that book for other matching. In all cases set
|
||||||
# accidentally match across libraries we also verify the title. The
|
# the application_id to the db_id of the matching book. This value
|
||||||
# db_id exists on Sony devices. Fallback is title and author match
|
# will be used by books_on_device to indicate matches.
|
||||||
|
|
||||||
update_metadata = prefs['manage_device_metadata'] == 'on_connect'
|
update_metadata = prefs['manage_device_metadata'] == 'on_connect'
|
||||||
for booklist in booklists:
|
for booklist in booklists:
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
|
book.in_library = None
|
||||||
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(self.db_book_uuid_cache[book.uuid],
|
book.smart_update(self.db_book_uuid_cache[book.uuid],
|
||||||
@ -1410,39 +1414,56 @@ class DeviceMixin(object): # {{{
|
|||||||
book.application_id = \
|
book.application_id = \
|
||||||
self.db_book_uuid_cache[book.uuid].application_id
|
self.db_book_uuid_cache[book.uuid].application_id
|
||||||
continue
|
continue
|
||||||
|
# No UUID exact match. Try metadata matching.
|
||||||
book_title = book.title.lower() if book.title else ''
|
book_title = clean_string(book.title)
|
||||||
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
|
||||||
book.in_library = None
|
|
||||||
d = self.db_book_title_cache.get(book_title, None)
|
d = self.db_book_title_cache.get(book_title, None)
|
||||||
if d is not None:
|
if d is not None:
|
||||||
|
# At this point we know that the title matches. The book
|
||||||
|
# will match if any of the db_id, author, or author_sort
|
||||||
|
# also match.
|
||||||
if getattr(book, 'application_id', None) in d['db_ids']:
|
if getattr(book, 'application_id', None) in d['db_ids']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
|
# app_id already matches a db_id. No need to set it.
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['db_ids'][book.application_id],
|
book.smart_update(d['db_ids'][book.application_id],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
continue
|
continue
|
||||||
if book.db_id in d['db_ids']:
|
# Sonys know their db_id independent of the application_id
|
||||||
|
# in the metadata cache. Check that as well.
|
||||||
|
if getattr(book, 'db_id', None) in d['db_ids']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
|
book.application_id = \
|
||||||
|
d['db_ids'][book.db_id].application_id
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['db_ids'][book.db_id],
|
book.smart_update(d['db_ids'][book.db_id],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
continue
|
continue
|
||||||
|
# We now know that the application_id is not right. Set it
|
||||||
|
# to None to prevent book_on_device from accidentally
|
||||||
|
# matching on it. It will be set to a correct value below if
|
||||||
|
# the book is matched with one in the library
|
||||||
|
book.application_id = None
|
||||||
if book.authors:
|
if book.authors:
|
||||||
# Compare against both author and author sort, because
|
# Compare against both author and author sort, because
|
||||||
# either can appear as the author
|
# either can appear as the author
|
||||||
book_authors = authors_to_string(book.authors).lower()
|
book_authors = clean_string(authors_to_string(book.authors))
|
||||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
|
||||||
if book_authors in d['authors']:
|
if book_authors in d['authors']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
|
book.application_id = \
|
||||||
|
d['authors'][book_authors].application_id
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['authors'][book_authors],
|
book.smart_update(d['authors'][book_authors],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
elif book_authors in d['author_sort']:
|
elif book_authors in d['author_sort']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
|
book.application_id = \
|
||||||
|
d['author_sort'][book_authors].application_id
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['author_sort'][book_authors],
|
book.smart_update(d['author_sort'][book_authors],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
|
else:
|
||||||
|
# Book definitely not matched. Clear its application ID
|
||||||
|
book.application_id = None
|
||||||
# Set author_sort if it isn't already
|
# Set author_sort if it isn't already
|
||||||
asort = getattr(book, 'author_sort', None)
|
asort = getattr(book, 'author_sort', None)
|
||||||
if not asort and book.authors:
|
if not asort and book.authors:
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
from PyQt4.Qt import QTabWidget
|
|
||||||
|
|
||||||
from calibre.gui2.dialogs.config.add_save_ui import Ui_TabWidget
|
|
||||||
from calibre.library.save_to_disk import config
|
|
||||||
from calibre.utils.config import prefs
|
|
||||||
from calibre.gui2.widgets import FilenamePattern
|
|
||||||
|
|
||||||
class AddSave(QTabWidget, Ui_TabWidget):
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
QTabWidget.__init__(self, parent)
|
|
||||||
self.setupUi(self)
|
|
||||||
while self.count() > 3:
|
|
||||||
self.removeTab(3)
|
|
||||||
c = config()
|
|
||||||
opts = c.parse()
|
|
||||||
for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf',
|
|
||||||
'replace_whitespace', 'to_lowercase'):
|
|
||||||
g = getattr(self, 'opt_'+x)
|
|
||||||
g.setChecked(getattr(opts, x))
|
|
||||||
help = '\n'.join(textwrap.wrap(c.get_option(x).help, 75))
|
|
||||||
g.setToolTip(help)
|
|
||||||
g.setWhatsThis(help)
|
|
||||||
|
|
||||||
for x in ('formats', 'timefmt'):
|
|
||||||
g = getattr(self, 'opt_'+x)
|
|
||||||
g.setText(getattr(opts, x))
|
|
||||||
help = '\n'.join(textwrap.wrap(c.get_option(x).help, 75))
|
|
||||||
g.setToolTip(help)
|
|
||||||
g.setWhatsThis(help)
|
|
||||||
|
|
||||||
|
|
||||||
self.opt_read_metadata_from_filename.setChecked(not prefs['read_file_metadata'])
|
|
||||||
self.filename_pattern = FilenamePattern(self)
|
|
||||||
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
|
|
||||||
self.opt_swap_author_names.setChecked(prefs['swap_author_names'])
|
|
||||||
self.opt_add_formats_to_existing.setChecked(prefs['add_formats_to_existing'])
|
|
||||||
if prefs['manage_device_metadata'] == 'manual':
|
|
||||||
self.manage_device_metadata.setCurrentIndex(0)
|
|
||||||
elif prefs['manage_device_metadata'] == 'on_send':
|
|
||||||
self.manage_device_metadata.setCurrentIndex(1)
|
|
||||||
else:
|
|
||||||
self.manage_device_metadata.setCurrentIndex(2)
|
|
||||||
help = '\n'.join(textwrap.wrap(c.get_option('template').help, 75))
|
|
||||||
self.save_template.initialize('save_to_disk', opts.template, help)
|
|
||||||
self.send_template.initialize('send_to_device', opts.send_template, help)
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
return self.save_template.validate() and self.send_template.validate()
|
|
||||||
|
|
||||||
def save_settings(self):
|
|
||||||
if not self.validate():
|
|
||||||
return False
|
|
||||||
c = config()
|
|
||||||
for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf',
|
|
||||||
'replace_whitespace', 'to_lowercase'):
|
|
||||||
c.set(x, getattr(self, 'opt_'+x).isChecked())
|
|
||||||
for x in ('formats', 'timefmt'):
|
|
||||||
val = unicode(getattr(self, 'opt_'+x).text()).strip()
|
|
||||||
if x == 'formats' and not val:
|
|
||||||
val = 'all'
|
|
||||||
c.set(x, val)
|
|
||||||
self.save_template.save_settings(c, 'template')
|
|
||||||
self.send_template.save_settings(c, 'send_template')
|
|
||||||
prefs['read_file_metadata'] = not bool(self.opt_read_metadata_from_filename.isChecked())
|
|
||||||
pattern = self.filename_pattern.commit()
|
|
||||||
prefs['filename_pattern'] = pattern
|
|
||||||
prefs['swap_author_names'] = bool(self.opt_swap_author_names.isChecked())
|
|
||||||
prefs['add_formats_to_existing'] = bool(self.opt_add_formats_to_existing.isChecked())
|
|
||||||
if self.manage_device_metadata.currentIndex() == 0:
|
|
||||||
prefs['manage_device_metadata'] = 'manual'
|
|
||||||
elif self.manage_device_metadata.currentIndex() == 1:
|
|
||||||
prefs['manage_device_metadata'] = 'on_send'
|
|
||||||
else:
|
|
||||||
prefs['manage_device_metadata'] = 'on_connect'
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from PyQt4.Qt import QApplication
|
|
||||||
app=QApplication([])
|
|
||||||
a = AddSave()
|
|
||||||
a.show()
|
|
||||||
app.exec_()
|
|
||||||
a.save_settings()
|
|
||||||
|
|
@ -1,280 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>TabWidget</class>
|
|
||||||
<widget class="QTabWidget" name="TabWidget">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>671</width>
|
|
||||||
<height>516</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>TabWidget</string>
|
|
||||||
</property>
|
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="tab">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>&Adding books</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QGridLayout" name="gridLayout_3">
|
|
||||||
<item row="0" column="0" colspan="2">
|
|
||||||
<widget class="QLabel" name="label_6">
|
|
||||||
<property name="text">
|
|
||||||
<string>Here you can control how calibre will read metadata from the files you add to it. calibre can either read metadata from the contents of the file, or from the filename.</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QCheckBox" name="opt_read_metadata_from_filename">
|
|
||||||
<property name="text">
|
|
||||||
<string>Read metadata only from &file name</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QCheckBox" name="opt_swap_author_names">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Swap the firstname and lastname of the author. This affects only metadata read from file names.</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>&Swap author firstname and lastname</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0" colspan="2">
|
|
||||||
<widget class="QCheckBox" name="opt_add_formats_to_existing">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>If an existing book with a similar title and author is found that does not have the format being added, the format is added
|
|
||||||
to the existing book, instead of creating a new entry. If the existing book already has the format, then it is silently ignored.
|
|
||||||
|
|
||||||
Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact.</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>If books with similar titles and authors found, &merge the new files automatically</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0" colspan="2">
|
|
||||||
<widget class="QGroupBox" name="metadata_box">
|
|
||||||
<property name="title">
|
|
||||||
<string>&Configure metadata from file name</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>363</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="tab">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>&Saving books</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0" colspan="2">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Here you can control how calibre will save your books when you click the Save to Disk button:</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QCheckBox" name="opt_save_cover">
|
|
||||||
<property name="text">
|
|
||||||
<string>Save &cover separately</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="opt_update_metadata">
|
|
||||||
<property name="text">
|
|
||||||
<string>Update &metadata in saved copies</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0" colspan="2">
|
|
||||||
<widget class="QCheckBox" name="opt_write_opf">
|
|
||||||
<property name="text">
|
|
||||||
<string>Save metadata in &OPF file</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0" colspan="2">
|
|
||||||
<widget class="QCheckBox" name="opt_asciiize">
|
|
||||||
<property name="text">
|
|
||||||
<string>Convert non-English characters to &English equivalents</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>Format &dates as:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_timefmt</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1">
|
|
||||||
<widget class="QLineEdit" name="opt_timefmt"/>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="0">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>File &formats to save:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_formats</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="1">
|
|
||||||
<widget class="QLineEdit" name="opt_formats"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QCheckBox" name="opt_replace_whitespace">
|
|
||||||
<property name="text">
|
|
||||||
<string>Replace space with &underscores</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QCheckBox" name="opt_to_lowercase">
|
|
||||||
<property name="text">
|
|
||||||
<string>Change paths to &lowercase</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="0" colspan="2">
|
|
||||||
<widget class="SaveTemplate" name="save_template" native="true"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="tab_2">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Sending to &device</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QGridLayout" name="gridLayout_2">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label_4">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Metadata &management:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>manage_device_metadata</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QComboBox" name="manage_device_metadata">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string extracomment="foobar">Manual management</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Only on send</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Automatic management</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<spacer name="horizontalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>313</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" colspan="3">
|
|
||||||
<widget class="QLabel" name="label_41">
|
|
||||||
<property name="text">
|
|
||||||
<string><li><b>Manual Management</b>: Calibre updates the metadata and adds collections only when a book is sent. With this option, calibre will never remove a collection.</li>
|
|
||||||
<li><b>Only on send</b>: Calibre updates metadata and adds/removes collections for a book only when it is sent to the device. </li>
|
|
||||||
<li><b>Automatic management</b>: Calibre automatically keeps metadata on the device in sync with the calibre library, on every connect</li></ul></string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_42">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0" colspan="3">
|
|
||||||
<widget class="QLabel" name="label_43">
|
|
||||||
<property name="text">
|
|
||||||
<string>Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Plugins</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0" colspan="3">
|
|
||||||
<widget class="SaveTemplate" name="send_template" native="true"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
<customwidgets>
|
|
||||||
<customwidget>
|
|
||||||
<class>SaveTemplate</class>
|
|
||||||
<extends>QWidget</extends>
|
|
||||||
<header>calibre/gui2/dialogs/config/save_template.h</header>
|
|
||||||
<container>1</container>
|
|
||||||
</customwidget>
|
|
||||||
</customwidgets>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
@ -1,173 +0,0 @@
|
|||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
||||||
|
|
||||||
'''Dialog to create a new custom column'''
|
|
||||||
|
|
||||||
import re
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL
|
|
||||||
from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant
|
|
||||||
|
|
||||||
from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn
|
|
||||||
from calibre.gui2 import error_dialog
|
|
||||||
|
|
||||||
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|
||||||
|
|
||||||
column_types = {
|
|
||||||
0:{'datatype':'text',
|
|
||||||
'text':_('Text, column shown in the tag browser'),
|
|
||||||
'is_multiple':False},
|
|
||||||
1:{'datatype':'*text',
|
|
||||||
'text':_('Comma separated text, like tags, shown in the tag browser'),
|
|
||||||
'is_multiple':True},
|
|
||||||
2:{'datatype':'comments',
|
|
||||||
'text':_('Long text, like comments, not shown in the tag browser'),
|
|
||||||
'is_multiple':False},
|
|
||||||
3:{'datatype':'series',
|
|
||||||
'text':_('Text column for keeping series-like information'),
|
|
||||||
'is_multiple':False},
|
|
||||||
4:{'datatype':'datetime',
|
|
||||||
'text':_('Date'), 'is_multiple':False},
|
|
||||||
5:{'datatype':'float',
|
|
||||||
'text':_('Floating point numbers'), 'is_multiple':False},
|
|
||||||
6:{'datatype':'int',
|
|
||||||
'text':_('Integers'), 'is_multiple':False},
|
|
||||||
7:{'datatype':'rating',
|
|
||||||
'text':_('Ratings, shown with stars'),
|
|
||||||
'is_multiple':False},
|
|
||||||
8:{'datatype':'bool',
|
|
||||||
'text':_('Yes/No'), 'is_multiple':False},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, parent, editing, standard_colheads, standard_colnames):
|
|
||||||
QDialog.__init__(self, parent)
|
|
||||||
Ui_QCreateCustomColumn.__init__(self)
|
|
||||||
self.setupUi(self)
|
|
||||||
# Remove help icon on title bar
|
|
||||||
icon = self.windowIcon()
|
|
||||||
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
|
||||||
self.setWindowIcon(icon)
|
|
||||||
|
|
||||||
self.simple_error = partial(error_dialog, self, show=True,
|
|
||||||
show_copy_button=False)
|
|
||||||
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)
|
|
||||||
self.connect(self.button_box, SIGNAL("rejected()"), self.reject)
|
|
||||||
self.parent = parent
|
|
||||||
self.editing_col = editing
|
|
||||||
self.standard_colheads = standard_colheads
|
|
||||||
self.standard_colnames = standard_colnames
|
|
||||||
for t in self.column_types:
|
|
||||||
self.column_type_box.addItem(self.column_types[t]['text'])
|
|
||||||
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
|
|
||||||
if not self.editing_col:
|
|
||||||
self.datatype_changed()
|
|
||||||
self.exec_()
|
|
||||||
return
|
|
||||||
idx = parent.columns.currentRow()
|
|
||||||
if idx < 0:
|
|
||||||
self.simple_error(_('No column selected'),
|
|
||||||
_('No column has been selected'))
|
|
||||||
return
|
|
||||||
col = unicode(parent.columns.item(idx).data(Qt.UserRole).toString())
|
|
||||||
if col not in parent.custcols:
|
|
||||||
self.simple_error('', _('Selected column is not a user-defined column'))
|
|
||||||
return
|
|
||||||
|
|
||||||
c = parent.custcols[col]
|
|
||||||
self.column_name_box.setText(c['label'])
|
|
||||||
self.column_heading_box.setText(c['name'])
|
|
||||||
ct = c['datatype'] if not c['is_multiple'] else '*text'
|
|
||||||
self.orig_column_number = c['colnum']
|
|
||||||
self.orig_column_name = col
|
|
||||||
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
|
|
||||||
self.column_type_box.setCurrentIndex(column_numbers[ct])
|
|
||||||
self.column_type_box.setEnabled(False)
|
|
||||||
if ct == 'datetime':
|
|
||||||
if c['display'].get('date_format', None):
|
|
||||||
self.date_format_box.setText(c['display'].get('date_format', ''))
|
|
||||||
self.datatype_changed()
|
|
||||||
self.exec_()
|
|
||||||
|
|
||||||
def datatype_changed(self, *args):
|
|
||||||
try:
|
|
||||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
|
||||||
except:
|
|
||||||
col_type = None
|
|
||||||
df_visible = col_type == 'datetime'
|
|
||||||
for x in ('box', 'default_label', 'label'):
|
|
||||||
getattr(self, 'date_format_'+x).setVisible(df_visible)
|
|
||||||
|
|
||||||
|
|
||||||
def accept(self):
|
|
||||||
col = unicode(self.column_name_box.text())
|
|
||||||
if not col:
|
|
||||||
return self.simple_error('', _('No lookup name was provided'))
|
|
||||||
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
|
|
||||||
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
|
|
||||||
if col.endswith('_index'):
|
|
||||||
return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.'))
|
|
||||||
col_heading = unicode(self.column_heading_box.text())
|
|
||||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
|
||||||
if col_type == '*text':
|
|
||||||
col_type='text'
|
|
||||||
is_multiple = True
|
|
||||||
else:
|
|
||||||
is_multiple = False
|
|
||||||
if not col_heading:
|
|
||||||
return self.simple_error('', _('No column heading was provided'))
|
|
||||||
bad_col = False
|
|
||||||
if col in self.parent.custcols:
|
|
||||||
if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number:
|
|
||||||
bad_col = True
|
|
||||||
if bad_col:
|
|
||||||
return self.simple_error('', _('The lookup name %s is already used')%col)
|
|
||||||
bad_head = False
|
|
||||||
for t in self.parent.custcols:
|
|
||||||
if self.parent.custcols[t]['name'] == col_heading:
|
|
||||||
if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number:
|
|
||||||
bad_head = True
|
|
||||||
for t in self.standard_colheads:
|
|
||||||
if self.standard_colheads[t] == col_heading:
|
|
||||||
bad_head = True
|
|
||||||
if bad_head:
|
|
||||||
return self.simple_error('', _('The heading %s is already used')%col_heading)
|
|
||||||
|
|
||||||
date_format = {}
|
|
||||||
if col_type == 'datetime':
|
|
||||||
if self.date_format_box.text():
|
|
||||||
date_format = {'date_format':unicode(self.date_format_box.text())}
|
|
||||||
else:
|
|
||||||
date_format = {'date_format': None}
|
|
||||||
|
|
||||||
key = self.parent.db.field_metadata.custom_field_prefix+col
|
|
||||||
if not self.editing_col:
|
|
||||||
self.parent.db.field_metadata
|
|
||||||
self.parent.custcols[key] = {
|
|
||||||
'label':col,
|
|
||||||
'name':col_heading,
|
|
||||||
'datatype':col_type,
|
|
||||||
'editable':True,
|
|
||||||
'display':date_format,
|
|
||||||
'normalized':None,
|
|
||||||
'colnum':None,
|
|
||||||
'is_multiple':is_multiple,
|
|
||||||
}
|
|
||||||
item = QListWidgetItem(col_heading, self.parent.columns)
|
|
||||||
item.setData(Qt.UserRole, QVariant(key))
|
|
||||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
|
||||||
item.setCheckState(Qt.Checked)
|
|
||||||
else:
|
|
||||||
idx = self.parent.columns.currentRow()
|
|
||||||
item = self.parent.columns.item(idx)
|
|
||||||
item.setData(Qt.UserRole, QVariant(key))
|
|
||||||
item.setText(col_heading)
|
|
||||||
self.parent.custcols[self.orig_column_name]['label'] = col
|
|
||||||
self.parent.custcols[self.orig_column_name]['name'] = col_heading
|
|
||||||
self.parent.custcols[self.orig_column_name]['display'].update(date_format)
|
|
||||||
self.parent.custcols[self.orig_column_name]['*edited'] = True
|
|
||||||
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
|
|
||||||
QDialog.accept(self)
|
|
||||||
|
|
||||||
def reject(self):
|
|
||||||
QDialog.reject(self)
|
|
@ -1,191 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>QCreateCustomColumn</class>
|
|
||||||
<widget class="QDialog" name="QCreateCustomColumn">
|
|
||||||
<property name="windowModality">
|
|
||||||
<enum>Qt::ApplicationModal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>528</width>
|
|
||||||
<height>199</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Create or edit custom columns</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
|
|
||||||
<property name="sizeConstraint">
|
|
||||||
<enum>QLayout::SetDefaultConstraint</enum>
|
|
||||||
</property>
|
|
||||||
<property name="margin">
|
|
||||||
<number>5</number>
|
|
||||||
</property>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<property name="margin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Lookup name</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>column_name_box</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Column &heading</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>column_heading_box</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QLineEdit" name="column_name_box">
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Used for searching the column. Must contain only digits and lower case letters.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLineEdit" name="column_heading_box">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Column heading in the library view and category name in the tag browser</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Column &type</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>column_type_box</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QComboBox" name="column_type_box">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>70</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>What kind of information will be kept in the column.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="date_format_box">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><p>Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.</p>
|
|
||||||
<p>For example:
|
|
||||||
<ul>
|
|
||||||
<li> ddd, d MMM yyyy gives Mon, 5 Jan 2010<li>
|
|
||||||
<li>dd MMMM yy gives 05 January 10</li>
|
|
||||||
</ul> </string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="date_format_default_label">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Use MMM yyyy for month + year, yyyy for year only</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Default: dd MMM yyyy.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
|
||||||
<widget class="QLabel" name="date_format_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Format for &dates</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>date_format_box</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QDialogButtonBox" name="button_box">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
<property name="centerButtons">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_6">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<weight>75</weight>
|
|
||||||
<bold>true</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Create or edit custom columns</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<tabstops>
|
|
||||||
<tabstop>column_name_box</tabstop>
|
|
||||||
<tabstop>column_heading_box</tabstop>
|
|
||||||
<tabstop>column_type_box</tabstop>
|
|
||||||
<tabstop>date_format_box</tabstop>
|
|
||||||
<tabstop>button_box</tabstop>
|
|
||||||
</tabstops>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
@ -1,295 +0,0 @@
|
|||||||
#!/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'
|
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from PyQt4.Qt import QWidget, QAbstractListModel, Qt, QIcon, \
|
|
||||||
QVariant, QItemSelectionModel
|
|
||||||
|
|
||||||
from calibre.gui2.dialogs.config.toolbar_ui import Ui_Form
|
|
||||||
from calibre.gui2 import gprefs, NONE, warning_dialog
|
|
||||||
|
|
||||||
|
|
||||||
class FakeAction(object):
|
|
||||||
|
|
||||||
def __init__(self, name, icon, tooltip=None,
|
|
||||||
dont_add_to=frozenset([]), dont_remove_from=frozenset([])):
|
|
||||||
self.name = name
|
|
||||||
self.action_spec = (name, icon, tooltip, None)
|
|
||||||
self.dont_remove_from = dont_remove_from
|
|
||||||
self.dont_add_to = dont_add_to
|
|
||||||
|
|
||||||
class BaseModel(QAbstractListModel):
|
|
||||||
|
|
||||||
def name_to_action(self, name, gui):
|
|
||||||
if name == 'Donate':
|
|
||||||
return FakeAction(name, 'donate.png',
|
|
||||||
dont_add_to=frozenset(['context-menu',
|
|
||||||
'context-menu-device']))
|
|
||||||
if name == 'Location Manager':
|
|
||||||
return FakeAction(name, None,
|
|
||||||
_('Switch between library and device views'),
|
|
||||||
dont_remove_from=set(['toolbar-device']))
|
|
||||||
if name is None:
|
|
||||||
return FakeAction('--- '+_('Separator')+' ---', None)
|
|
||||||
return gui.iactions[name]
|
|
||||||
|
|
||||||
def rowCount(self, parent):
|
|
||||||
return len(self._data)
|
|
||||||
|
|
||||||
def data(self, index, role):
|
|
||||||
row = index.row()
|
|
||||||
action = self._data[row].action_spec
|
|
||||||
if role == Qt.DisplayRole:
|
|
||||||
text = action[0]
|
|
||||||
text = text.replace('&', '')
|
|
||||||
if text == _('%d books'):
|
|
||||||
text = _('Choose library')
|
|
||||||
return QVariant(text)
|
|
||||||
if role == Qt.DecorationRole:
|
|
||||||
ic = action[1]
|
|
||||||
if ic is None:
|
|
||||||
ic = 'blank.png'
|
|
||||||
return QVariant(QIcon(I(ic)))
|
|
||||||
if role == Qt.ToolTipRole and action[2] is not None:
|
|
||||||
return QVariant(action[2])
|
|
||||||
return NONE
|
|
||||||
|
|
||||||
def names(self, indexes):
|
|
||||||
rows = [i.row() for i in indexes]
|
|
||||||
ans = []
|
|
||||||
for i in rows:
|
|
||||||
n = self._data[i].name
|
|
||||||
if n.startswith('---'):
|
|
||||||
n = None
|
|
||||||
ans.append(n)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
class AllModel(BaseModel):
|
|
||||||
|
|
||||||
def __init__(self, key, gui):
|
|
||||||
BaseModel.__init__(self)
|
|
||||||
self.gprefs_name = 'action-layout-'+key
|
|
||||||
current = gprefs[self.gprefs_name]
|
|
||||||
self.gui = gui
|
|
||||||
self.key = key
|
|
||||||
self._data = self.get_all_actions(current)
|
|
||||||
|
|
||||||
def get_all_actions(self, current):
|
|
||||||
all = list(self.gui.iactions.keys()) + ['Donate']
|
|
||||||
all = [x for x in all if x not in current] + [None]
|
|
||||||
all = [self.name_to_action(x, self.gui) for x in all]
|
|
||||||
all = [x for x in all if self.key not in x.dont_add_to]
|
|
||||||
all.sort()
|
|
||||||
return all
|
|
||||||
|
|
||||||
def add(self, names):
|
|
||||||
actions = []
|
|
||||||
for name in names:
|
|
||||||
if name is None or name.startswith('---'): continue
|
|
||||||
actions.append(self.name_to_action(name, self.gui))
|
|
||||||
self._data.extend(actions)
|
|
||||||
self._data.sort()
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def remove(self, indices, allowed):
|
|
||||||
rows = [i.row() for i in indices]
|
|
||||||
remove = set([])
|
|
||||||
for row in rows:
|
|
||||||
ac = self._data[row]
|
|
||||||
if ac.name.startswith('---'): continue
|
|
||||||
if ac.name in allowed:
|
|
||||||
remove.add(row)
|
|
||||||
ndata = []
|
|
||||||
for i, ac in enumerate(self._data):
|
|
||||||
if i not in remove:
|
|
||||||
ndata.append(ac)
|
|
||||||
self._data = ndata
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def restore_defaults(self):
|
|
||||||
current = gprefs.defaults[self.gprefs_name]
|
|
||||||
self._data = self.get_all_actions(current)
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
class CurrentModel(BaseModel):
|
|
||||||
|
|
||||||
def __init__(self, key, gui):
|
|
||||||
BaseModel.__init__(self)
|
|
||||||
self.gprefs_name = 'action-layout-'+key
|
|
||||||
current = gprefs[self.gprefs_name]
|
|
||||||
self._data = [self.name_to_action(x, gui) for x in current]
|
|
||||||
self.key = key
|
|
||||||
self.gui = gui
|
|
||||||
|
|
||||||
def move(self, idx, delta):
|
|
||||||
row = idx.row()
|
|
||||||
if row < 0 or row >= len(self._data):
|
|
||||||
return
|
|
||||||
nrow = row + delta
|
|
||||||
if nrow < 0 or nrow >= len(self._data):
|
|
||||||
return
|
|
||||||
t = self._data[row]
|
|
||||||
self._data[row] = self._data[nrow]
|
|
||||||
self._data[nrow] = t
|
|
||||||
ni = self.index(nrow)
|
|
||||||
self.dataChanged.emit(idx, idx)
|
|
||||||
self.dataChanged.emit(ni, ni)
|
|
||||||
return ni
|
|
||||||
|
|
||||||
def add(self, names):
|
|
||||||
actions = []
|
|
||||||
reject = set([])
|
|
||||||
for name in names:
|
|
||||||
ac = self.name_to_action(name, self.gui)
|
|
||||||
if self.key in ac.dont_add_to:
|
|
||||||
reject.add(ac)
|
|
||||||
else:
|
|
||||||
actions.append(ac)
|
|
||||||
|
|
||||||
self._data.extend(actions)
|
|
||||||
self.reset()
|
|
||||||
return reject
|
|
||||||
|
|
||||||
def remove(self, indices):
|
|
||||||
rows = [i.row() for i in indices]
|
|
||||||
remove, rejected = set([]), set([])
|
|
||||||
for row in rows:
|
|
||||||
ac = self._data[row]
|
|
||||||
if self.key in ac.dont_remove_from:
|
|
||||||
rejected.add(ac)
|
|
||||||
continue
|
|
||||||
remove.add(row)
|
|
||||||
ndata = []
|
|
||||||
for i, ac in enumerate(self._data):
|
|
||||||
if i not in remove:
|
|
||||||
ndata.append(ac)
|
|
||||||
self._data = ndata
|
|
||||||
self.reset()
|
|
||||||
return rejected
|
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
old = gprefs[self.gprefs_name]
|
|
||||||
new = []
|
|
||||||
for x in self._data:
|
|
||||||
n = x.name
|
|
||||||
if n.startswith('---'):
|
|
||||||
n = None
|
|
||||||
new.append(n)
|
|
||||||
new = tuple(new)
|
|
||||||
if new != old:
|
|
||||||
defaults = gprefs.defaults[self.gprefs_name]
|
|
||||||
if defaults == new:
|
|
||||||
del gprefs[self.gprefs_name]
|
|
||||||
else:
|
|
||||||
gprefs[self.gprefs_name] = new
|
|
||||||
|
|
||||||
def restore_defaults(self):
|
|
||||||
current = gprefs.defaults[self.gprefs_name]
|
|
||||||
self._data = [self.name_to_action(x, self.gui) for x in current]
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
|
|
||||||
class ToolbarLayout(QWidget, Ui_Form):
|
|
||||||
|
|
||||||
LOCATIONS = [
|
|
||||||
('toolbar', _('The main toolbar')),
|
|
||||||
('toolbar-device', _('The main toolbar when a device is connected')),
|
|
||||||
('context-menu', _('The context menu for the books in the '
|
|
||||||
'calibre library')),
|
|
||||||
('context-menu-device', _('The context menu for the books on '
|
|
||||||
'the device'))
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, gui, parent=None):
|
|
||||||
QWidget.__init__(self, parent)
|
|
||||||
self.setupUi(self)
|
|
||||||
|
|
||||||
self.models = {}
|
|
||||||
for key, text in self.LOCATIONS:
|
|
||||||
self.what.addItem(text, key)
|
|
||||||
all_model = AllModel(key, gui)
|
|
||||||
current_model = CurrentModel(key, gui)
|
|
||||||
self.models[key] = (all_model, current_model)
|
|
||||||
self.what.setCurrentIndex(0)
|
|
||||||
self.what.currentIndexChanged[int].connect(self.what_changed)
|
|
||||||
self.what_changed(0)
|
|
||||||
|
|
||||||
self.add_action_button.clicked.connect(self.add_action)
|
|
||||||
self.remove_action_button.clicked.connect(self.remove_action)
|
|
||||||
self.restore_defaults_button.clicked.connect(self.restore_defaults)
|
|
||||||
self.action_up_button.clicked.connect(partial(self.move, -1))
|
|
||||||
self.action_down_button.clicked.connect(partial(self.move, 1))
|
|
||||||
|
|
||||||
def what_changed(self, idx):
|
|
||||||
key = unicode(self.what.itemData(idx).toString())
|
|
||||||
self.all_actions.setModel(self.models[key][0])
|
|
||||||
self.current_actions.setModel(self.models[key][1])
|
|
||||||
|
|
||||||
def add_action(self, *args):
|
|
||||||
x = self.all_actions.selectionModel().selectedIndexes()
|
|
||||||
names = self.all_actions.model().names(x)
|
|
||||||
if names:
|
|
||||||
not_added = self.current_actions.model().add(names)
|
|
||||||
ns = set([x.name for x in not_added])
|
|
||||||
added = set(names) - ns
|
|
||||||
self.all_actions.model().remove(x, added)
|
|
||||||
if not_added:
|
|
||||||
warning_dialog(self, _('Cannot add'),
|
|
||||||
_('Cannot add the actions %s to this location') %
|
|
||||||
','.join([a.action_spec[0] for a in not_added]),
|
|
||||||
show=True)
|
|
||||||
if added:
|
|
||||||
ca = self.current_actions
|
|
||||||
idx = ca.model().index(ca.model().rowCount(None)-1)
|
|
||||||
ca.scrollTo(idx)
|
|
||||||
|
|
||||||
def remove_action(self, *args):
|
|
||||||
x = self.current_actions.selectionModel().selectedIndexes()
|
|
||||||
names = self.current_actions.model().names(x)
|
|
||||||
if names:
|
|
||||||
not_removed = self.current_actions.model().remove(x)
|
|
||||||
ns = set([x.name for x in not_removed])
|
|
||||||
removed = set(names) - ns
|
|
||||||
self.all_actions.model().add(removed)
|
|
||||||
if not_removed:
|
|
||||||
warning_dialog(self, _('Cannot remove'),
|
|
||||||
_('Cannot remove the actions %s from this location') %
|
|
||||||
','.join([a.action_spec[0] for a in not_removed]),
|
|
||||||
show=True)
|
|
||||||
|
|
||||||
def move(self, delta, *args):
|
|
||||||
ci = self.current_actions.currentIndex()
|
|
||||||
m = self.current_actions.model()
|
|
||||||
if ci.isValid():
|
|
||||||
ni = m.move(ci, delta)
|
|
||||||
if ni is not None:
|
|
||||||
self.current_actions.setCurrentIndex(ni)
|
|
||||||
self.current_actions.selectionModel().select(ni,
|
|
||||||
QItemSelectionModel.ClearAndSelect)
|
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
for am, cm in self.models.values():
|
|
||||||
cm.commit()
|
|
||||||
|
|
||||||
def restore_defaults(self, *args):
|
|
||||||
for am, cm in self.models.values():
|
|
||||||
cm.restore_defaults()
|
|
||||||
am.restore_defaults()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from PyQt4.Qt import QApplication
|
|
||||||
from calibre.gui2.ui import Main
|
|
||||||
app=QApplication([])
|
|
||||||
m = Main(None)
|
|
||||||
a = ToolbarLayout(m)
|
|
||||||
a.show()
|
|
||||||
app.exec_()
|
|
||||||
a.commit()
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>Form</class>
|
|
||||||
<widget class="QWidget" name="Form">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>831</width>
|
|
||||||
<height>553</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Form</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Customize the actions in:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1" colspan="3">
|
|
||||||
<widget class="QComboBox" name="what">
|
|
||||||
<property name="sizeAdjustPolicy">
|
|
||||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
|
||||||
</property>
|
|
||||||
<property name="minimumContentsLength">
|
|
||||||
<number>20</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" rowspan="2">
|
|
||||||
<widget class="QGroupBox" name="groupBox">
|
|
||||||
<property name="title">
|
|
||||||
<string>A&vailable actions</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QListView" name="all_actions">
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::MultiSelection</enum>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>32</width>
|
|
||||||
<height>32</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="spacing">
|
|
||||||
<number>10</number>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2" rowspan="2">
|
|
||||||
<widget class="QGroupBox" name="groupBox_2">
|
|
||||||
<property name="title">
|
|
||||||
<string>&Current actions</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QListView" name="current_actions">
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::MultiSelection</enum>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>32</width>
|
|
||||||
<height>32</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="spacing">
|
|
||||||
<number>10</number>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="action_up_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Move selected action up</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>24</width>
|
|
||||||
<height>24</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer_2">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="action_down_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Move selected action down</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>24</width>
|
|
||||||
<height>24</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="shortcut">
|
|
||||||
<string>Ctrl+S</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1" rowspan="2">
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="add_action_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Add selected actions to toolbar</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>24</width>
|
|
||||||
<height>24</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeType">
|
|
||||||
<enum>QSizePolicy::Fixed</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="remove_action_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Remove selected actions from toolbar</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>24</width>
|
|
||||||
<height>24</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="2">
|
|
||||||
<widget class="QPushButton" name="restore_defaults_button">
|
|
||||||
<property name="text">
|
|
||||||
<string>Restore to &default</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources>
|
|
||||||
<include location="../../../../../resources/images.qrc"/>
|
|
||||||
</resources>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
@ -30,7 +30,7 @@ from calibre.ebooks.metadata import MetaInformation
|
|||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
|
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
|
||||||
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
||||||
from calibre.gui2.dialogs.config.social import SocialMetadata
|
from calibre.gui2.preferences.social import SocialMetadata
|
||||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
|
|
||||||
@ -144,15 +144,23 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.cover_data = cover
|
self.cover_data = cover
|
||||||
|
|
||||||
def generate_cover(self, *args):
|
def generate_cover(self, *args):
|
||||||
from calibre.utils.magick.draw import create_cover_page, TextLine
|
from calibre.ebooks import calibre_cover
|
||||||
|
from calibre.ebooks.metadata import fmt_sidx
|
||||||
|
from calibre.gui2 import config
|
||||||
title = unicode(self.title.text()).strip()
|
title = unicode(self.title.text()).strip()
|
||||||
author = unicode(self.authors.text()).strip()
|
author = unicode(self.authors.text()).strip()
|
||||||
if not title or not author:
|
if not title or not author:
|
||||||
return error_dialog(self, _('Specify title and author'),
|
return error_dialog(self, _('Specify title and author'),
|
||||||
_('You must specify a title and author before generating '
|
_('You must specify a title and author before generating '
|
||||||
'a cover'), show=True)
|
'a cover'), show=True)
|
||||||
lines = [TextLine(title, 44), TextLine(author, 32)]
|
series = unicode(self.series.text()).strip()
|
||||||
self.cover_data = create_cover_page(lines, I('library.png'))
|
series_string = None
|
||||||
|
if series:
|
||||||
|
series_string = _('Book %s of %s')%(
|
||||||
|
fmt_sidx(self.series_index.value(),
|
||||||
|
use_roman=config['use_roman_numerals_for_series_number']), series)
|
||||||
|
self.cover_data = calibre_cover(title, author,
|
||||||
|
series_string=series_string)
|
||||||
pix = QPixmap()
|
pix = QPixmap()
|
||||||
pix.loadFromData(self.cover_data)
|
pix.loadFromData(self.cover_data)
|
||||||
self.cover.setPixmap(pix)
|
self.cover.setPixmap(pix)
|
||||||
|
@ -646,7 +646,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="fetch_cover_button">
|
<widget class="QPushButton" name="fetch_cover_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Download &cover</string>
|
<string>Download co&ver</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -32,6 +32,9 @@ class LibraryViewMixin(object): # {{{
|
|||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection)
|
self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection)
|
||||||
|
self.library_view.add_column_signal.connect(partial(self.iactions['Preferences'].do_config,
|
||||||
|
initial_plugin=('Interface', 'Custom Columns')),
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
for func, args in [
|
for func, args in [
|
||||||
('connect_to_search_box', (self.search,
|
('connect_to_search_box', (self.search,
|
||||||
self.search_done)),
|
self.search_done)),
|
||||||
@ -145,20 +148,23 @@ class StatusBar(QStatusBar): # {{{
|
|||||||
self._font = QFont()
|
self._font = QFont()
|
||||||
self._font.setBold(True)
|
self._font.setBold(True)
|
||||||
self.setFont(self._font)
|
self.setFont(self._font)
|
||||||
|
self.defmsg = QLabel(self.default_message)
|
||||||
|
self.defmsg.setFont(self._font)
|
||||||
|
self.addWidget(self.defmsg)
|
||||||
|
|
||||||
def initialize(self, systray=None):
|
def initialize(self, systray=None):
|
||||||
self.systray = systray
|
self.systray = systray
|
||||||
self.notifier = get_notifier(systray)
|
self.notifier = get_notifier(systray)
|
||||||
self.messageChanged.connect(self.message_changed,
|
|
||||||
type=Qt.QueuedConnection)
|
|
||||||
self.message_changed('')
|
|
||||||
|
|
||||||
def device_connected(self, devname):
|
def device_connected(self, devname):
|
||||||
self.device_string = _('Connected ') + devname
|
self.device_string = _('Connected ') + devname
|
||||||
|
self.defmsg.setText(self.default_message + ' ..::.. ' +
|
||||||
|
self.device_string)
|
||||||
self.clearMessage()
|
self.clearMessage()
|
||||||
|
|
||||||
def device_disconnected(self):
|
def device_disconnected(self):
|
||||||
self.device_string = ''
|
self.device_string = ''
|
||||||
|
self.defmsg.setText(self.default_message)
|
||||||
self.clearMessage()
|
self.clearMessage()
|
||||||
|
|
||||||
def new_version_available(self, ver, url):
|
def new_version_available(self, ver, url):
|
||||||
@ -188,15 +194,6 @@ class StatusBar(QStatusBar): # {{{
|
|||||||
def clear_message(self):
|
def clear_message(self):
|
||||||
self.clearMessage()
|
self.clearMessage()
|
||||||
|
|
||||||
def message_changed(self, msg):
|
|
||||||
if not msg or msg.isEmpty() or msg.isNull() or \
|
|
||||||
not unicode(msg).strip():
|
|
||||||
extra = ''
|
|
||||||
if self.device_string:
|
|
||||||
extra = ' ..::.. ' + self.device_string
|
|
||||||
self.showMessage(self.default_message + extra)
|
|
||||||
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class LayoutMixin(object): # {{{
|
class LayoutMixin(object): # {{{
|
||||||
@ -258,7 +255,9 @@ class LayoutMixin(object): # {{{
|
|||||||
getattr(self, x+'_view').save_state()
|
getattr(self, x+'_view').save_state()
|
||||||
|
|
||||||
for x in ('cb', 'tb', 'bd'):
|
for x in ('cb', 'tb', 'bd'):
|
||||||
getattr(self, x+'_splitter').save_state()
|
s = getattr(self, x+'_splitter')
|
||||||
|
s.update_desired_state()
|
||||||
|
s.save_state()
|
||||||
|
|
||||||
def read_layout_settings(self):
|
def read_layout_settings(self):
|
||||||
# View states are restored automatically when set_database is called
|
# View states are restored automatically when set_database is called
|
||||||
|
@ -215,6 +215,7 @@ class ToolBar(QToolBar): # {{{
|
|||||||
self.location_manager.locations_changed.connect(self.build_bar)
|
self.location_manager.locations_changed.connect(self.build_bar)
|
||||||
donate.setAutoRaise(True)
|
donate.setAutoRaise(True)
|
||||||
donate.setCursor(Qt.PointingHandCursor)
|
donate.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.added_actions = []
|
||||||
self.build_bar()
|
self.build_bar()
|
||||||
self.preferred_width = self.sizeHint().width()
|
self.preferred_width = self.sizeHint().width()
|
||||||
|
|
||||||
@ -237,7 +238,13 @@ class ToolBar(QToolBar): # {{{
|
|||||||
actions = '-device' if showing_device else ''
|
actions = '-device' if showing_device else ''
|
||||||
actions = gprefs['action-layout-toolbar'+actions]
|
actions = gprefs['action-layout-toolbar'+actions]
|
||||||
|
|
||||||
|
for ac in self.added_actions:
|
||||||
|
m = ac.menu()
|
||||||
|
if m is not None:
|
||||||
|
m.setVisible(False)
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
self.added_actions = []
|
||||||
|
|
||||||
for what in actions:
|
for what in actions:
|
||||||
if what is None:
|
if what is None:
|
||||||
@ -245,6 +252,7 @@ class ToolBar(QToolBar): # {{{
|
|||||||
elif what == 'Location Manager':
|
elif what == 'Location Manager':
|
||||||
for ac in self.location_manager.available_actions:
|
for ac in self.location_manager.available_actions:
|
||||||
self.addAction(ac)
|
self.addAction(ac)
|
||||||
|
self.added_actions.append(ac)
|
||||||
self.setup_tool_button(ac, QToolButton.MenuButtonPopup)
|
self.setup_tool_button(ac, QToolButton.MenuButtonPopup)
|
||||||
elif what == 'Donate':
|
elif what == 'Donate':
|
||||||
self.d_widget = QWidget()
|
self.d_widget = QWidget()
|
||||||
@ -255,6 +263,7 @@ class ToolBar(QToolBar): # {{{
|
|||||||
elif what in self.gui.iactions:
|
elif what in self.gui.iactions:
|
||||||
action = self.gui.iactions[what]
|
action = self.gui.iactions[what]
|
||||||
self.addAction(action.qaction)
|
self.addAction(action.qaction)
|
||||||
|
self.added_actions.append(action.qaction)
|
||||||
self.setup_tool_button(action.qaction, action.popup_type)
|
self.setup_tool_button(action.qaction, action.popup_type)
|
||||||
|
|
||||||
def setup_tool_button(self, ac, menu_mode=None):
|
def setup_tool_button(self, ac, menu_mode=None):
|
||||||
|
@ -121,10 +121,11 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
def set_device_connected(self, is_connected):
|
def set_device_connected(self, is_connected):
|
||||||
self.device_connected = is_connected
|
self.device_connected = is_connected
|
||||||
self.db.refresh_ondevice()
|
self.db.refresh_ondevice()
|
||||||
|
self.refresh()
|
||||||
|
self.research()
|
||||||
if is_connected and self.sorted_on[0] == 'ondevice':
|
if is_connected and self.sorted_on[0] == 'ondevice':
|
||||||
self.resort()
|
self.resort()
|
||||||
|
|
||||||
|
|
||||||
def set_book_on_device_func(self, func):
|
def set_book_on_device_func(self, func):
|
||||||
self.book_on_device = func
|
self.book_on_device = func
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import os
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
|
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
|
||||||
QModelIndex
|
QModelIndex, QIcon
|
||||||
|
|
||||||
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
||||||
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
||||||
@ -23,6 +23,7 @@ from calibre.gui2.library import DEFAULT_SORT
|
|||||||
class BooksView(QTableView): # {{{
|
class BooksView(QTableView): # {{{
|
||||||
|
|
||||||
files_dropped = pyqtSignal(object)
|
files_dropped = pyqtSignal(object)
|
||||||
|
add_column_signal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent, modelcls=BooksModel):
|
def __init__(self, parent, modelcls=BooksModel):
|
||||||
QTableView.__init__(self, parent)
|
QTableView.__init__(self, parent)
|
||||||
@ -54,6 +55,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.can_add_columns = True
|
||||||
self.was_restored = False
|
self.was_restored = False
|
||||||
self.column_header = self.horizontalHeader()
|
self.column_header = self.horizontalHeader()
|
||||||
self.column_header.setMovable(True)
|
self.column_header.setMovable(True)
|
||||||
@ -93,6 +95,8 @@ class BooksView(QTableView): # {{{
|
|||||||
self.sortByColumn(idx, Qt.DescendingOrder)
|
self.sortByColumn(idx, Qt.DescendingOrder)
|
||||||
elif action == 'defaults':
|
elif action == 'defaults':
|
||||||
self.apply_state(self.get_default_state())
|
self.apply_state(self.get_default_state())
|
||||||
|
elif action == 'addcustcol':
|
||||||
|
self.add_column_signal.emit()
|
||||||
elif action.startswith('align_'):
|
elif action.startswith('align_'):
|
||||||
alignment = action.partition('_')[-1]
|
alignment = action.partition('_')[-1]
|
||||||
self._model.change_alignment(column, alignment)
|
self._model.change_alignment(column, alignment)
|
||||||
@ -166,6 +170,13 @@ class BooksView(QTableView): # {{{
|
|||||||
partial(self.column_header_context_handler,
|
partial(self.column_header_context_handler,
|
||||||
action='defaults', column=col))
|
action='defaults', column=col))
|
||||||
|
|
||||||
|
if self.can_add_columns:
|
||||||
|
self.column_header_context_menu.addAction(
|
||||||
|
QIcon(I('column.png')),
|
||||||
|
_('Add your own columns'),
|
||||||
|
partial(self.column_header_context_handler,
|
||||||
|
action='addcustcol', column=col))
|
||||||
|
|
||||||
self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos))
|
self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos))
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -494,6 +505,7 @@ class DeviceBooksView(BooksView): # {{{
|
|||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
BooksView.__init__(self, parent, DeviceBooksModel)
|
BooksView.__init__(self, parent, DeviceBooksModel)
|
||||||
|
self.can_add_columns = False
|
||||||
self.columns_resized = False
|
self.columns_resized = False
|
||||||
self.resize_on_select = False
|
self.resize_on_select = False
|
||||||
self.rating_delegate = None
|
self.rating_delegate = None
|
||||||
|
@ -5,25 +5,61 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from PyQt4.Qt import QWidget, pyqtSignal, QCheckBox, QAbstractSpinBox, \
|
from PyQt4.Qt import QWidget, pyqtSignal, QCheckBox, QAbstractSpinBox, \
|
||||||
QLineEdit, QComboBox, QVariant
|
QLineEdit, QComboBox, QVariant
|
||||||
|
|
||||||
from calibre.customize.ui import preferences_plugins
|
from calibre.customize.ui import preferences_plugins
|
||||||
|
from calibre.utils.config import ConfigProxy
|
||||||
|
|
||||||
class AbortCommit(Exception):
|
class AbortCommit(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ConfigWidgetInterface(object):
|
class ConfigWidgetInterface(object):
|
||||||
|
|
||||||
|
'''
|
||||||
|
This class defines the interface that all widgets displayed in the
|
||||||
|
Preferences dialog must implement. See :class:`ConfigWidgetBase` for
|
||||||
|
a base class that implements this interface and defines various conveninece
|
||||||
|
methods as well.
|
||||||
|
'''
|
||||||
|
|
||||||
|
#: This signal must be emitted whenever the user changes a value in this
|
||||||
|
#: widget
|
||||||
changed_signal = None
|
changed_signal = None
|
||||||
|
|
||||||
|
#: Set to True iff the :meth:`restore_to_defaults` method is implemented.
|
||||||
|
supports_restoring_to_defaults = True
|
||||||
|
|
||||||
|
#: The tooltip for the Restore to defaults button
|
||||||
|
restore_defaults_desc = _('Restore settings to default values. '
|
||||||
|
'You have to click Apply to actually save the default settings.')
|
||||||
|
|
||||||
|
#: If True the Preferences dialog will not allow the user to set any more
|
||||||
|
#: preferences. Only has effect if :meth:`commit` returns True.
|
||||||
|
restart_critical = False
|
||||||
|
|
||||||
def genesis(self, gui):
|
def genesis(self, gui):
|
||||||
|
'''
|
||||||
|
Called once before the widget is displayed, should perform any
|
||||||
|
necessary setup.
|
||||||
|
|
||||||
|
:param gui: The main calibre graphical user interface
|
||||||
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
|
'''
|
||||||
|
Should set all config values to their initial values (the values
|
||||||
|
stored in the config files).
|
||||||
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
|
'''
|
||||||
|
Should set all config values to their defaults.
|
||||||
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
@ -31,10 +67,19 @@ class ConfigWidgetInterface(object):
|
|||||||
Save any changed settings. Return True if the changes require a
|
Save any changed settings. Return True if the changes require a
|
||||||
restart, False otherwise. Raise an :class:`AbortCommit` exception
|
restart, False otherwise. Raise an :class:`AbortCommit` exception
|
||||||
to indicate that an error occurred. You are responsible for giving the
|
to indicate that an error occurred. You are responsible for giving the
|
||||||
suer feedback about what the error is and how to correct it.
|
user feedback about what the error is and how to correct it.
|
||||||
'''
|
'''
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def refresh_gui(self, gui):
|
||||||
|
'''
|
||||||
|
Called once after this widget is committed. Responsible for causing the
|
||||||
|
gui to reread any changed settings. Note that by default the GUI
|
||||||
|
re-initializes various elements anyway, so most widgets won't need to
|
||||||
|
use this method.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
class Setting(object):
|
class Setting(object):
|
||||||
|
|
||||||
def __init__(self, name, config_obj, widget, gui_name=None,
|
def __init__(self, name, config_obj, widget, gui_name=None,
|
||||||
@ -65,6 +110,20 @@ class Setting(object):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('Unknown data type')
|
raise ValueError('Unknown data type')
|
||||||
|
|
||||||
|
if isinstance(self.config_obj, ConfigProxy) and \
|
||||||
|
not unicode(self.gui_obj.toolTip()):
|
||||||
|
h = self.config_obj.help(self.name)
|
||||||
|
if h:
|
||||||
|
self.gui_obj.setToolTip(h)
|
||||||
|
tt = unicode(self.gui_obj.toolTip())
|
||||||
|
if tt:
|
||||||
|
if not unicode(self.gui_obj.whatsThis()):
|
||||||
|
self.gui_obj.setWhatsThis(tt)
|
||||||
|
if not unicode(self.gui_obj.statusTip()):
|
||||||
|
self.gui_obj.setStatusTip(tt)
|
||||||
|
tt = '\n'.join(textwrap.wrap(tt, 70))
|
||||||
|
self.gui_obj.setToolTip(tt)
|
||||||
|
|
||||||
def changed(self, *args):
|
def changed(self, *args):
|
||||||
self.widget.changed_signal.emit()
|
self.widget.changed_signal.emit()
|
||||||
|
|
||||||
@ -120,7 +179,7 @@ class Setting(object):
|
|||||||
elif self.datatype == 'number':
|
elif self.datatype == 'number':
|
||||||
val = self.gui_obj.value()
|
val = self.gui_obj.value()
|
||||||
elif self.datatype == 'string':
|
elif self.datatype == 'string':
|
||||||
val = unicode(self.gui_name.text()).strip()
|
val = unicode(self.gui_obj.text()).strip()
|
||||||
if self.empty_string_is_None and not val:
|
if self.empty_string_is_None and not val:
|
||||||
val = None
|
val = None
|
||||||
elif self.datatype == 'choice':
|
elif self.datatype == 'choice':
|
||||||
@ -147,7 +206,23 @@ class CommaSeparatedList(Setting):
|
|||||||
|
|
||||||
class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
|
class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Base class that contains code to easily add standard config widgets like
|
||||||
|
checkboxes, combo boxes, text fields and so on. See the :meth:`register`
|
||||||
|
method.
|
||||||
|
|
||||||
|
This class automatically handles change notification, resetting to default,
|
||||||
|
translation between gui objects and config objects, etc. for registered
|
||||||
|
settings.
|
||||||
|
|
||||||
|
If your config widget inherits from this class but includes setting that
|
||||||
|
are not registered, you should override the :class:`ConfigWidgetInterface` methods
|
||||||
|
and call the base class methods inside the overrides.
|
||||||
|
'''
|
||||||
|
|
||||||
changed_signal = pyqtSignal()
|
changed_signal = pyqtSignal()
|
||||||
|
supports_restoring_to_defaults = True
|
||||||
|
restart_critical = False
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
@ -156,9 +231,25 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
|
|||||||
self.settings = {}
|
self.settings = {}
|
||||||
|
|
||||||
def register(self, name, config_obj, gui_name=None, choices=None,
|
def register(self, name, config_obj, gui_name=None, choices=None,
|
||||||
restart_required=False, setting=Setting):
|
restart_required=False, empty_string_is_None=True, setting=Setting):
|
||||||
|
'''
|
||||||
|
Register a setting.
|
||||||
|
|
||||||
|
:param name: The setting name
|
||||||
|
:param config: The config object that reads/writes the setting
|
||||||
|
:param gui_name: The name of the GUI object that presents an interface
|
||||||
|
to change the setting. By default it is assumed to be
|
||||||
|
``'opt_' + name``.
|
||||||
|
:param choices: If this setting is a multiple choice (combobox) based
|
||||||
|
setting, the list of choices. The list is a list of two
|
||||||
|
element tuples of the form: ``[(gui name, value), ...]``
|
||||||
|
:param setting: The class responsible for managing this setting. The
|
||||||
|
default class handles almost all cases, so this param
|
||||||
|
is rarely used.
|
||||||
|
'''
|
||||||
setting = setting(name, config_obj, self, gui_name=gui_name,
|
setting = setting(name, config_obj, self, gui_name=gui_name,
|
||||||
choices=choices, restart_required=restart_required)
|
choices=choices, restart_required=restart_required,
|
||||||
|
empty_string_is_None=empty_string_is_None)
|
||||||
return self.register_setting(setting)
|
return self.register_setting(setting)
|
||||||
|
|
||||||
def register_setting(self, setting):
|
def register_setting(self, setting):
|
||||||
@ -182,7 +273,6 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
|
|||||||
setting.restore_defaults()
|
setting.restore_defaults()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_plugin(category, name):
|
def get_plugin(category, name):
|
||||||
for plugin in preferences_plugins():
|
for plugin in preferences_plugins():
|
||||||
if plugin.category == category and plugin.name == name:
|
if plugin.category == category and plugin.name == name:
|
||||||
@ -191,13 +281,27 @@ def get_plugin(category, name):
|
|||||||
'No Preferences Plugin with category: %s and name: %s found' %
|
'No Preferences Plugin with category: %s and name: %s found' %
|
||||||
(category, name))
|
(category, name))
|
||||||
|
|
||||||
def test_widget(category, name, gui=None): # {{{
|
# Testing {{{
|
||||||
|
|
||||||
|
def init_gui():
|
||||||
|
from calibre.gui2.ui import Main
|
||||||
|
from calibre.gui2.main import option_parser
|
||||||
|
from calibre.library import db
|
||||||
|
parser = option_parser()
|
||||||
|
opts, args = parser.parse_args([])
|
||||||
|
actions = tuple(Main.create_application_menubar())
|
||||||
|
db = db()
|
||||||
|
gui = Main(opts)
|
||||||
|
gui.initialize(db.library_path, db, None, actions, show_gui=False)
|
||||||
|
return gui
|
||||||
|
|
||||||
|
def test_widget(category, name, gui=None):
|
||||||
from PyQt4.Qt import QDialog, QVBoxLayout, QDialogButtonBox
|
from PyQt4.Qt import QDialog, QVBoxLayout, QDialogButtonBox
|
||||||
class Dialog(QDialog):
|
class Dialog(QDialog):
|
||||||
def set_widget(self, w): self.w = w
|
def set_widget(self, w): self.w = w
|
||||||
def accept(self):
|
def accept(self):
|
||||||
try:
|
try:
|
||||||
self.w.commit()
|
self.restart_required = self.w.commit()
|
||||||
except AbortCommit:
|
except AbortCommit:
|
||||||
return
|
return
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
@ -213,6 +317,7 @@ def test_widget(category, name, gui=None): # {{{
|
|||||||
w = pl.create_widget(d)
|
w = pl.create_widget(d)
|
||||||
d.set_widget(w)
|
d.set_widget(w)
|
||||||
bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults)
|
bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults)
|
||||||
|
bb.button(bb.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults)
|
||||||
bb.button(bb.Apply).setEnabled(False)
|
bb.button(bb.Apply).setEnabled(False)
|
||||||
bb.button(bb.Apply).clicked.connect(d.accept)
|
bb.button(bb.Apply).clicked.connect(d.accept)
|
||||||
w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True))
|
w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True))
|
||||||
@ -222,24 +327,28 @@ def test_widget(category, name, gui=None): # {{{
|
|||||||
l.addWidget(bb)
|
l.addWidget(bb)
|
||||||
mygui = gui is None
|
mygui = gui is None
|
||||||
if gui is None:
|
if gui is None:
|
||||||
from calibre.gui2.ui import Main
|
gui = init_gui()
|
||||||
from calibre.gui2.main import option_parser
|
mygui = True
|
||||||
from calibre.library import db
|
|
||||||
parser = option_parser()
|
|
||||||
opts, args = parser.parse_args([])
|
|
||||||
actions = tuple(Main.create_application_menubar())
|
|
||||||
db = db()
|
|
||||||
gui = Main(opts)
|
|
||||||
gui.initialize(db.library_path, db, None, actions, show_gui=False)
|
|
||||||
w.genesis(gui)
|
w.genesis(gui)
|
||||||
w.initialize()
|
w.initialize()
|
||||||
restart_required = False
|
d.exec_()
|
||||||
if d.exec_() == QDialog.Accepted:
|
if getattr(d, 'restart_required', False):
|
||||||
restart_required = w.commit()
|
|
||||||
if restart_required:
|
|
||||||
from calibre.gui2 import warning_dialog
|
from calibre.gui2 import warning_dialog
|
||||||
warning_dialog(gui, 'Restart required', 'Restart required', show=True)
|
warning_dialog(gui, 'Restart required', 'Restart required', show=True)
|
||||||
if mygui:
|
if mygui:
|
||||||
gui.shutdown()
|
gui.shutdown()
|
||||||
|
|
||||||
|
def test_all():
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
app
|
||||||
|
gui = init_gui()
|
||||||
|
for plugin in preferences_plugins():
|
||||||
|
test_widget(plugin.category, plugin.name, gui=gui)
|
||||||
|
gui.shutdown()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_all()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS
|
|||||||
|
|
||||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
restart_critical = True
|
||||||
|
|
||||||
def genesis(self, gui):
|
def genesis(self, gui):
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
@ -103,7 +105,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
return
|
return
|
||||||
self.opt_columns.item(idx).setCheckState(False)
|
self.opt_columns.item(idx).setCheckState(False)
|
||||||
self.opt_columns.takeItem(idx)
|
self.opt_columns.takeItem(idx)
|
||||||
self.custcols[col]['*deleteme'] = True
|
if self.custcols[col]['colnum'] is None:
|
||||||
|
del self.custcols[col] # A newly-added column was deleted
|
||||||
|
else:
|
||||||
|
self.custcols[col]['*deleteme'] = True
|
||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
|
|
||||||
def add_custcol(self):
|
def add_custcol(self):
|
||||||
|
@ -34,6 +34,10 @@ class Model(QStringListModel):
|
|||||||
|
|
||||||
class Base(ConfigWidgetBase, Ui_Form):
|
class Base(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
restore_defaults_desc = _('Restore settings to default values. '
|
||||||
|
'Only settings for the currently selected section '
|
||||||
|
'are restored.')
|
||||||
|
|
||||||
def genesis(self, gui):
|
def genesis(self, gui):
|
||||||
log = Log()
|
log = Log()
|
||||||
log.outputs = []
|
log.outputs = []
|
||||||
@ -108,6 +112,6 @@ if __name__ == '__main__':
|
|||||||
from PyQt4.Qt import QApplication
|
from PyQt4.Qt import QApplication
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
#test_widget('Conversion', 'Input Options')
|
#test_widget('Conversion', 'Input Options')
|
||||||
#test_widget('Conversion', 'Common Options')
|
test_widget('Conversion', 'Common Options')
|
||||||
test_widget('Conversion', 'Output Options')
|
#test_widget('Conversion', 'Output Options')
|
||||||
|
|
||||||
|
@ -161,7 +161,6 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
else:
|
else:
|
||||||
idx = self.parent.opt_columns.currentRow()
|
idx = self.parent.opt_columns.currentRow()
|
||||||
item = self.parent.opt_columns.item(idx)
|
item = self.parent.opt_columns.item(idx)
|
||||||
item.setData(Qt.UserRole, QVariant(key))
|
|
||||||
item.setText(col_heading)
|
item.setText(col_heading)
|
||||||
self.parent.custcols[self.orig_column_name]['label'] = col
|
self.parent.custcols[self.orig_column_name]['label'] = col
|
||||||
self.parent.custcols[self.orig_column_name]['name'] = col_heading
|
self.parent.custcols[self.orig_column_name]['name'] = col_heading
|
||||||
|
112
src/calibre/gui2/preferences/email.ui
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>701</width>
|
||||||
|
<height>494</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_22">
|
||||||
|
<property name="text">
|
||||||
|
<string>calibre can send your books to you (or your reader) by email. Emails will be automatically sent for downloaded news to all email addresses that have Auto-send checked.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||||
|
<item>
|
||||||
|
<widget class="QTableView" name="email_view">
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="email_add">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Add an email address to which to send books</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Add email</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/plus.png</normaloff>:/images/plus.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolButtonStyle">
|
||||||
|
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="email_make_default">
|
||||||
|
<property name="text">
|
||||||
|
<string>Make &default</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="email_remove">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Remove email</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolButtonStyle">
|
||||||
|
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="SendEmail" name="send_email_widget" native="true"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>SendEmail</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>calibre/gui2/wizard/send_email.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
196
src/calibre/gui2/preferences/emailp.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#!/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'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QAbstractTableModel, QVariant, QFont, Qt
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
|
||||||
|
AbortCommit
|
||||||
|
from calibre.gui2.preferences.email_ui import Ui_Form
|
||||||
|
from calibre.utils.config import ConfigProxy
|
||||||
|
from calibre.gui2 import NONE
|
||||||
|
from calibre.utils.smtp import config as smtp_prefs
|
||||||
|
|
||||||
|
class EmailAccounts(QAbstractTableModel): # {{{
|
||||||
|
|
||||||
|
def __init__(self, accounts):
|
||||||
|
QAbstractTableModel.__init__(self)
|
||||||
|
self.accounts = accounts
|
||||||
|
self.account_order = sorted(self.accounts.keys())
|
||||||
|
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')])
|
||||||
|
self.default_font = QFont()
|
||||||
|
self.default_font.setBold(True)
|
||||||
|
self.default_font = QVariant(self.default_font)
|
||||||
|
self.tooltips =[NONE] + map(QVariant,
|
||||||
|
[_('Formats to email. The first matching format will be sent.'),
|
||||||
|
'<p>'+_('If checked, downloaded news will be automatically '
|
||||||
|
'mailed <br>to this email address '
|
||||||
|
'(provided it is in one of the listed formats).')])
|
||||||
|
|
||||||
|
def rowCount(self, *args):
|
||||||
|
return len(self.account_order)
|
||||||
|
|
||||||
|
def columnCount(self, *args):
|
||||||
|
return 3
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
|
return self.headers[section]
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
if row < 0 or row >= self.rowCount():
|
||||||
|
return NONE
|
||||||
|
account = self.account_order[row]
|
||||||
|
if role == Qt.UserRole:
|
||||||
|
return (account, self.accounts[account])
|
||||||
|
if role == Qt.ToolTipRole:
|
||||||
|
return self.tooltips[col]
|
||||||
|
if role in [Qt.DisplayRole, Qt.EditRole]:
|
||||||
|
if col == 0:
|
||||||
|
return QVariant(account)
|
||||||
|
if col == 1:
|
||||||
|
return QVariant(self.accounts[account][0])
|
||||||
|
if role == Qt.FontRole and self.accounts[account][2]:
|
||||||
|
return self.default_font
|
||||||
|
if role == Qt.CheckStateRole and col == 2:
|
||||||
|
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
if index.column() == 2:
|
||||||
|
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
|
||||||
|
else:
|
||||||
|
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
|
||||||
|
|
||||||
|
def setData(self, index, value, role):
|
||||||
|
if not index.isValid():
|
||||||
|
return False
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
account = self.account_order[row]
|
||||||
|
if col == 2:
|
||||||
|
self.accounts[account][1] ^= True
|
||||||
|
elif col == 1:
|
||||||
|
self.accounts[account][0] = unicode(value.toString()).upper()
|
||||||
|
else:
|
||||||
|
na = unicode(value.toString())
|
||||||
|
from email.utils import parseaddr
|
||||||
|
addr = parseaddr(na)[-1]
|
||||||
|
if not addr:
|
||||||
|
return False
|
||||||
|
self.accounts[na] = self.accounts.pop(account)
|
||||||
|
self.account_order[row] = na
|
||||||
|
if '@kindle.com' in addr:
|
||||||
|
self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'
|
||||||
|
|
||||||
|
self.dataChanged.emit(
|
||||||
|
self.index(index.row(), 0), self.index(index.row(), 2))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def make_default(self, index):
|
||||||
|
if index.isValid():
|
||||||
|
row = index.row()
|
||||||
|
for x in self.accounts.values():
|
||||||
|
x[2] = False
|
||||||
|
self.accounts[self.account_order[row]][2] = True
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
x = _('new email address')
|
||||||
|
y = x
|
||||||
|
c = 0
|
||||||
|
while y in self.accounts:
|
||||||
|
c += 1
|
||||||
|
y = x + str(c)
|
||||||
|
auto_send = len(self.accounts) < 1
|
||||||
|
self.accounts[y] = ['MOBI, EPUB', auto_send,
|
||||||
|
len(self.account_order) == 0]
|
||||||
|
self.account_order = sorted(self.accounts.keys())
|
||||||
|
self.reset()
|
||||||
|
return self.index(self.account_order.index(y), 0)
|
||||||
|
|
||||||
|
def remove(self, index):
|
||||||
|
if index.isValid():
|
||||||
|
row = index.row()
|
||||||
|
account = self.account_order[row]
|
||||||
|
self.accounts.pop(account)
|
||||||
|
self.account_order = sorted(self.accounts.keys())
|
||||||
|
has_default = False
|
||||||
|
for account in self.account_order:
|
||||||
|
if self.accounts[account][2]:
|
||||||
|
has_default = True
|
||||||
|
break
|
||||||
|
if not has_default and self.account_order:
|
||||||
|
self.accounts[self.account_order[0]][2] = True
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
supports_restoring_to_defaults = False
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self.proxy = ConfigProxy(smtp_prefs())
|
||||||
|
|
||||||
|
self.send_email_widget.initialize(self.preferred_to_address)
|
||||||
|
self.send_email_widget.changed_signal.connect(self.changed_signal.emit)
|
||||||
|
opts = self.send_email_widget.smtp_opts
|
||||||
|
self._email_accounts = EmailAccounts(opts.accounts)
|
||||||
|
self._email_accounts.dataChanged.connect(lambda x,y:
|
||||||
|
self.changed_signal.emit())
|
||||||
|
self.email_view.setModel(self._email_accounts)
|
||||||
|
|
||||||
|
self.email_add.clicked.connect(self.add_email_account)
|
||||||
|
self.email_make_default.clicked.connect(self.make_default)
|
||||||
|
self.email_view.resizeColumnsToContents()
|
||||||
|
self.email_remove.clicked.connect(self.remove_email_account)
|
||||||
|
|
||||||
|
def preferred_to_address(self):
|
||||||
|
if self._email_accounts.account_order:
|
||||||
|
return self._email_accounts.account_order[0]
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
ConfigWidgetBase.initialize(self)
|
||||||
|
# Initializing all done in genesis
|
||||||
|
|
||||||
|
def restore_defaults(self):
|
||||||
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
|
# No defaults to restore to
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
to_set = bool(self._email_accounts.accounts)
|
||||||
|
if not self.send_email_widget.set_email_settings(to_set):
|
||||||
|
raise AbortCommit('abort')
|
||||||
|
self.proxy['accounts'] = self._email_accounts.accounts
|
||||||
|
return ConfigWidgetBase.commit(self)
|
||||||
|
|
||||||
|
def make_default(self, *args):
|
||||||
|
self._email_accounts.make_default(self.email_view.currentIndex())
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
|
def add_email_account(self, *args):
|
||||||
|
index = self._email_accounts.add()
|
||||||
|
self.email_view.setCurrentIndex(index)
|
||||||
|
self.email_view.resizeColumnsToContents()
|
||||||
|
self.email_view.edit(index)
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
|
def remove_email_account(self, *args):
|
||||||
|
idx = self.email_view.currentIndex()
|
||||||
|
self._email_accounts.remove(idx)
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Sharing', 'Email')
|
||||||
|
|
@ -6,6 +6,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, QStringList, Qt
|
from PyQt4.Qt import QComboBox, QStringList, Qt
|
||||||
|
|
||||||
from calibre.gui2 import config as gui_conf
|
from calibre.gui2 import config as gui_conf
|
||||||
@ -17,15 +19,22 @@ class HistoryBox(QComboBox):
|
|||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
|
|
||||||
def initialize(self, opt_name, default, help=None):
|
def initialize(self, opt_name, default, help=None):
|
||||||
history = gui_conf[opt_name]
|
self.opt_name = opt_name
|
||||||
if default not in history:
|
self.set_value(default)
|
||||||
history.append(default)
|
if help:
|
||||||
self.addItems(QStringList(history))
|
self.setStatusTip(help)
|
||||||
self.setCurrentIndex(self.findText(default, Qt.MatchFixedString))
|
help = '\n'.join(textwrap.wrap(help))
|
||||||
if help is not None:
|
|
||||||
self.setToolTip(help)
|
self.setToolTip(help)
|
||||||
self.setWhatsThis(help)
|
self.setWhatsThis(help)
|
||||||
|
|
||||||
|
def set_value(self, val):
|
||||||
|
history = gui_conf[self.opt_name]
|
||||||
|
if val not in history:
|
||||||
|
history.append(val)
|
||||||
|
self.clear()
|
||||||
|
self.addItems(QStringList(history))
|
||||||
|
self.setCurrentIndex(self.findText(val, Qt.MatchFixedString))
|
||||||
|
|
||||||
def save_history(self, opt_name):
|
def save_history(self, opt_name):
|
||||||
history = [unicode(self.itemText(i)) for i in range(self.count())]
|
history = [unicode(self.itemText(i)) for i in range(self.count())]
|
||||||
ct = self.text()
|
ct = self.text()
|
@ -23,7 +23,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('gui_layout', config, restart_required=True, choices=
|
r('gui_layout', config, restart_required=True, choices=
|
||||||
[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
|
[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
|
||||||
|
|
||||||
r('cover_flow_queue_length', config)
|
r('cover_flow_queue_length', config, restart_required=True)
|
||||||
|
|
||||||
lang = get_lang()
|
lang = get_lang()
|
||||||
if lang is None or lang not in available_translations():
|
if lang is None or lang not in available_translations():
|
||||||
@ -55,6 +55,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
(_('Never'), 'never')]
|
(_('Never'), 'never')]
|
||||||
r('toolbar_text', gprefs, choices=choices)
|
r('toolbar_text', gprefs, choices=choices)
|
||||||
|
|
||||||
|
def refresh_gui(self, gui):
|
||||||
|
gui.search.search_as_you_type(config['search_as_you_type'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from PyQt4.Qt import QApplication
|
from PyQt4.Qt import QApplication
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
|
340
src/calibre/gui2/preferences/main.py
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
#!/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 textwrap
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QMainWindow, Qt, QIcon, QStatusBar, QFont, QWidget, \
|
||||||
|
QScrollArea, QStackedWidget, QVBoxLayout, QLabel, QFrame, QKeySequence, \
|
||||||
|
QToolBar, QSize, pyqtSignal, QPixmap, QToolButton, QAction, \
|
||||||
|
QDialogButtonBox, QHBoxLayout
|
||||||
|
|
||||||
|
from calibre.constants import __appname__, __version__, islinux
|
||||||
|
from calibre.gui2 import gprefs, min_available_height, available_width, \
|
||||||
|
warning_dialog
|
||||||
|
from calibre.gui2.preferences import init_gui, AbortCommit, get_plugin
|
||||||
|
from calibre.customize.ui import preferences_plugins
|
||||||
|
from calibre.utils.ordered_dict import OrderedDict
|
||||||
|
|
||||||
|
ICON_SIZE = 32
|
||||||
|
|
||||||
|
class StatusBar(QStatusBar): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QStatusBar.__init__(self, parent)
|
||||||
|
self.default_message = __appname__ + ' ' + _('version') + ' ' + \
|
||||||
|
__version__ + ' ' + _('created by Kovid Goyal')
|
||||||
|
self.device_string = ''
|
||||||
|
self._font = QFont()
|
||||||
|
self._font.setBold(True)
|
||||||
|
self.setFont(self._font)
|
||||||
|
|
||||||
|
self.w = QLabel(self.default_message)
|
||||||
|
self.w.setFont(self._font)
|
||||||
|
self.addWidget(self.w)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class BarTitle(QWidget): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self._layout = QHBoxLayout()
|
||||||
|
self.setLayout(self._layout)
|
||||||
|
self._layout.addStretch(10)
|
||||||
|
self.icon = QLabel('')
|
||||||
|
self._layout.addWidget(self.icon)
|
||||||
|
self.title = QLabel('')
|
||||||
|
self.title.setStyleSheet('QLabel { font-weight: bold }')
|
||||||
|
self.title.setAlignment(Qt.AlignLeft | Qt.AlignCenter)
|
||||||
|
self._layout.addWidget(self.title)
|
||||||
|
self._layout.addStretch(10)
|
||||||
|
|
||||||
|
def show_plugin(self, plugin):
|
||||||
|
self.pmap = QPixmap(plugin.icon).scaled(ICON_SIZE, ICON_SIZE,
|
||||||
|
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
self.icon.setPixmap(self.pmap)
|
||||||
|
self.title.setText(plugin.gui_name)
|
||||||
|
tt = plugin.description
|
||||||
|
self.setStatusTip(tt)
|
||||||
|
tt = textwrap.fill(tt)
|
||||||
|
self.setToolTip(tt)
|
||||||
|
self.setWhatsThis(tt)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Category(QWidget): # {{{
|
||||||
|
|
||||||
|
plugin_activated = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, name, plugins, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self.setLayout(self._layout)
|
||||||
|
self.label = QLabel(name)
|
||||||
|
self.sep = QFrame(self)
|
||||||
|
self.bf = QFont()
|
||||||
|
self.bf.setBold(True)
|
||||||
|
self.label.setFont(self.bf)
|
||||||
|
self.sep.setFrameShape(QFrame.HLine)
|
||||||
|
self._layout.addWidget(self.label)
|
||||||
|
self._layout.addWidget(self.sep)
|
||||||
|
|
||||||
|
self.plugins = plugins
|
||||||
|
|
||||||
|
self.bar = QToolBar(self)
|
||||||
|
self.bar.setIconSize(QSize(48, 48))
|
||||||
|
self.bar.setMovable(False)
|
||||||
|
self.bar.setFloatable(False)
|
||||||
|
self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||||
|
self._layout.addWidget(self.bar)
|
||||||
|
self.actions = []
|
||||||
|
for p in plugins:
|
||||||
|
target = partial(self.triggered, p)
|
||||||
|
ac = self.bar.addAction(QIcon(p.icon), p.gui_name, target)
|
||||||
|
ac.setToolTip(textwrap.fill(p.description))
|
||||||
|
ac.setWhatsThis(textwrap.fill(p.description))
|
||||||
|
ac.setStatusTip(p.description)
|
||||||
|
self.actions.append(ac)
|
||||||
|
w = self.bar.widgetForAction(ac)
|
||||||
|
w.setStyleSheet('QToolButton { margin-right: 20px; min-width: 100px }')
|
||||||
|
w.setCursor(Qt.PointingHandCursor)
|
||||||
|
w.setAutoRaise(True)
|
||||||
|
|
||||||
|
def triggered(self, plugin, *args):
|
||||||
|
self.plugin_activated.emit(plugin)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Browser(QScrollArea): # {{{
|
||||||
|
|
||||||
|
show_plugin = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QScrollArea.__init__(self, parent)
|
||||||
|
self.setWidgetResizable(True)
|
||||||
|
|
||||||
|
category_map = {}
|
||||||
|
for plugin in preferences_plugins():
|
||||||
|
if plugin.category not in category_map:
|
||||||
|
category_map[plugin.category] = plugin.category_order
|
||||||
|
if category_map[plugin.category] < plugin.category_order:
|
||||||
|
category_map[plugin.category] = plugin.category_order
|
||||||
|
|
||||||
|
categories = list(category_map.keys())
|
||||||
|
categories.sort(cmp=lambda x, y: cmp(category_map[x], category_map[y]))
|
||||||
|
|
||||||
|
self.category_map = OrderedDict()
|
||||||
|
for c in categories:
|
||||||
|
self.category_map[c] = []
|
||||||
|
|
||||||
|
for plugin in preferences_plugins():
|
||||||
|
self.category_map[plugin.category].append(plugin)
|
||||||
|
|
||||||
|
for plugins in self.category_map.values():
|
||||||
|
plugins.sort(cmp=lambda x, y: cmp(x.name_order, y.name_order))
|
||||||
|
|
||||||
|
self.widgets = []
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self.container = QWidget(self)
|
||||||
|
self.container.setLayout(self._layout)
|
||||||
|
self.setWidget(self.container)
|
||||||
|
|
||||||
|
for name, plugins in self.category_map.items():
|
||||||
|
w = Category(name, plugins, self)
|
||||||
|
self.widgets.append(w)
|
||||||
|
self._layout.addWidget(w)
|
||||||
|
w.plugin_activated.connect(self.show_plugin.emit)
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Preferences(QMainWindow):
|
||||||
|
|
||||||
|
def __init__(self, gui, initial_plugin=None):
|
||||||
|
QMainWindow.__init__(self, gui)
|
||||||
|
self.gui = gui
|
||||||
|
self.must_restart = False
|
||||||
|
self.committed = False
|
||||||
|
|
||||||
|
self.resize(900, 720)
|
||||||
|
nh, nw = min_available_height()-25, available_width()-10
|
||||||
|
if nh < 0:
|
||||||
|
nh = 800
|
||||||
|
if nw < 0:
|
||||||
|
nw = 600
|
||||||
|
nh = min(self.height(), nh)
|
||||||
|
nw = min(self.width(), nw)
|
||||||
|
self.resize(nw, nh)
|
||||||
|
self.esc_action = QAction(self)
|
||||||
|
self.addAction(self.esc_action)
|
||||||
|
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
|
||||||
|
self.esc_action.triggered.connect(self.esc)
|
||||||
|
|
||||||
|
geom = gprefs.get('preferences_window_geometry', None)
|
||||||
|
if geom is not None:
|
||||||
|
self.restoreGeometry(geom)
|
||||||
|
|
||||||
|
# Center
|
||||||
|
if islinux:
|
||||||
|
self.move(gui.rect().center() - self.rect().center())
|
||||||
|
|
||||||
|
self.setWindowModality(Qt.WindowModal)
|
||||||
|
self.setWindowTitle(__appname__ + ' - ' + _('Preferences'))
|
||||||
|
self.setWindowIcon(QIcon(I('config.png')))
|
||||||
|
|
||||||
|
self.status_bar = StatusBar(self)
|
||||||
|
self.setStatusBar(self.status_bar)
|
||||||
|
|
||||||
|
self.stack = QStackedWidget(self)
|
||||||
|
self.cw = QWidget(self)
|
||||||
|
self.cw.setLayout(QVBoxLayout())
|
||||||
|
self.cw.layout().addWidget(self.stack)
|
||||||
|
self.bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||||
|
self.cw.layout().addWidget(self.bb)
|
||||||
|
self.bb.rejected.connect(self.close, type=Qt.QueuedConnection)
|
||||||
|
self.setCentralWidget(self.cw)
|
||||||
|
self.browser = Browser(self)
|
||||||
|
self.browser.show_plugin.connect(self.show_plugin)
|
||||||
|
self.stack.addWidget(self.browser)
|
||||||
|
self.scroll_area = QScrollArea(self)
|
||||||
|
self.stack.addWidget(self.scroll_area)
|
||||||
|
self.scroll_area.setWidgetResizable(True)
|
||||||
|
|
||||||
|
self.bar = QToolBar(self)
|
||||||
|
self.addToolBar(self.bar)
|
||||||
|
self.bar.setVisible(False)
|
||||||
|
self.bar.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||||
|
self.bar.setMovable(False)
|
||||||
|
self.bar.setFloatable(False)
|
||||||
|
self.bar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
||||||
|
self.apply_action = self.bar.addAction(QIcon(I('ok.png')), _('&Apply'),
|
||||||
|
self.commit)
|
||||||
|
self.cancel_action = self.bar.addAction(QIcon(I('window-close.png')),
|
||||||
|
_('&Cancel'), self.cancel)
|
||||||
|
self.bar_title = BarTitle(self.bar)
|
||||||
|
self.bar.addWidget(self.bar_title)
|
||||||
|
self.restore_action = self.bar.addAction(QIcon(I('clear_left.png')),
|
||||||
|
_('Restore &defaults'), self.restore_defaults)
|
||||||
|
for ac, tt in [('apply', _('Save changes')),
|
||||||
|
('cancel', _('Cancel and return to overview'))]:
|
||||||
|
ac = getattr(self, ac+'_action')
|
||||||
|
ac.setToolTip(tt)
|
||||||
|
ac.setWhatsThis(tt)
|
||||||
|
ac.setStatusTip(tt)
|
||||||
|
|
||||||
|
for ch in self.bar.children():
|
||||||
|
if isinstance(ch, QToolButton):
|
||||||
|
ch.setCursor(Qt.PointingHandCursor)
|
||||||
|
ch.setAutoRaise(True)
|
||||||
|
|
||||||
|
self.stack.setCurrentIndex(0)
|
||||||
|
|
||||||
|
if initial_plugin is not None:
|
||||||
|
category, name = initial_plugin
|
||||||
|
plugin = get_plugin(category, name)
|
||||||
|
if plugin is not None:
|
||||||
|
self.show_plugin(plugin)
|
||||||
|
|
||||||
|
|
||||||
|
def show_plugin(self, plugin):
|
||||||
|
self.showing_widget = plugin.create_widget(self.scroll_area)
|
||||||
|
self.showing_widget.genesis(self.gui)
|
||||||
|
self.showing_widget.initialize()
|
||||||
|
self.scroll_area.setWidget(self.showing_widget)
|
||||||
|
self.stack.setCurrentIndex(1)
|
||||||
|
self.showing_widget.show()
|
||||||
|
self.setWindowTitle(__appname__ + ' - ' + _('Preferences') + ' - ' +
|
||||||
|
plugin.gui_name)
|
||||||
|
self.apply_action.setEnabled(False)
|
||||||
|
self.showing_widget.changed_signal.connect(lambda :
|
||||||
|
self.apply_action.setEnabled(True))
|
||||||
|
self.restore_action.setEnabled(self.showing_widget.supports_restoring_to_defaults)
|
||||||
|
tt = self.showing_widget.restore_defaults_desc
|
||||||
|
if not self.restore_action.isEnabled():
|
||||||
|
tt = _('Restoring to defaults not supported for') + ' ' + \
|
||||||
|
plugin.gui_name
|
||||||
|
self.restore_action.setToolTip(textwrap.fill(tt))
|
||||||
|
self.restore_action.setWhatsThis(textwrap.fill(tt))
|
||||||
|
self.restore_action.setStatusTip(tt)
|
||||||
|
self.bar_title.show_plugin(plugin)
|
||||||
|
self.setWindowIcon(QIcon(plugin.icon))
|
||||||
|
self.bar.setVisible(True)
|
||||||
|
self.bb.setVisible(False)
|
||||||
|
|
||||||
|
|
||||||
|
def hide_plugin(self):
|
||||||
|
self.showing_widget = QWidget(self.scroll_area)
|
||||||
|
self.scroll_area.setWidget(self.showing_widget)
|
||||||
|
self.setWindowTitle(__appname__ + ' - ' + _('Preferences'))
|
||||||
|
self.bar.setVisible(False)
|
||||||
|
self.stack.setCurrentIndex(0)
|
||||||
|
self.setWindowIcon(QIcon(I('config.png')))
|
||||||
|
self.bb.setVisible(True)
|
||||||
|
|
||||||
|
def esc(self, *args):
|
||||||
|
if self.stack.currentIndex() == 1:
|
||||||
|
self.hide_plugin()
|
||||||
|
elif self.stack.currentIndex() == 0:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def commit(self, *args):
|
||||||
|
try:
|
||||||
|
must_restart = self.showing_widget.commit()
|
||||||
|
except AbortCommit:
|
||||||
|
return
|
||||||
|
rc = self.showing_widget.restart_critical
|
||||||
|
self.committed = True
|
||||||
|
if must_restart:
|
||||||
|
self.must_restart = True
|
||||||
|
msg = _('Some of the changes you made require a restart.'
|
||||||
|
' Please restart calibre as soon as possible.')
|
||||||
|
if rc:
|
||||||
|
msg = _('The changes you have made require calibre be '
|
||||||
|
'restarted immediately. You will not be allowed '
|
||||||
|
'set any more preferences, until you restart.')
|
||||||
|
|
||||||
|
|
||||||
|
warning_dialog(self, _('Restart needed'), msg, show=True,
|
||||||
|
show_copy_button=False)
|
||||||
|
self.showing_widget.refresh_gui(self.gui)
|
||||||
|
self.hide_plugin()
|
||||||
|
if must_restart and rc:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cancel(self, *args):
|
||||||
|
self.hide_plugin()
|
||||||
|
|
||||||
|
def restore_defaults(self, *args):
|
||||||
|
self.showing_widget.restore_defaults()
|
||||||
|
|
||||||
|
def closeEvent(self, *args):
|
||||||
|
gprefs.set('preferences_window_geometry',
|
||||||
|
bytearray(self.saveGeometry()))
|
||||||
|
if self.committed:
|
||||||
|
self.gui.must_restart_before_config = self.must_restart
|
||||||
|
self.gui.tags_view.set_new_model() # in case columns changed
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
self.gui.create_device_menu()
|
||||||
|
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
|
||||||
|
self.gui.tool_bar.build_bar()
|
||||||
|
self.gui.build_context_menus()
|
||||||
|
self.gui.tool_bar.apply_settings()
|
||||||
|
|
||||||
|
return QMainWindow.closeEvent(self, *args)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
app
|
||||||
|
gui = init_gui()
|
||||||
|
|
||||||
|
p = Preferences(gui)
|
||||||
|
p.show()
|
||||||
|
app.exec_()
|
||||||
|
gui.shutdown()
|
127
src/calibre/gui2/preferences/misc.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
#!/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'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QProgressDialog, QThread, Qt, pyqtSignal
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
|
from calibre.gui2.preferences.misc_ui import Ui_Form
|
||||||
|
from calibre.gui2 import error_dialog, config, warning_dialog, \
|
||||||
|
open_local_file, info_dialog
|
||||||
|
from calibre.constants import isosx
|
||||||
|
|
||||||
|
# Check Integrity {{{
|
||||||
|
|
||||||
|
class VacThread(QThread):
|
||||||
|
|
||||||
|
check_done = pyqtSignal(object, object)
|
||||||
|
callback = pyqtSignal(object, object)
|
||||||
|
|
||||||
|
def __init__(self, parent, db):
|
||||||
|
QThread.__init__(self, parent)
|
||||||
|
self.db = db
|
||||||
|
self._parent = parent
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
err = bad = None
|
||||||
|
try:
|
||||||
|
bad = self.db.check_integrity(self.callbackf)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
err = traceback.format_exc()
|
||||||
|
self.check_done.emit(bad, err)
|
||||||
|
|
||||||
|
def callbackf(self, progress, msg):
|
||||||
|
self.callback.emit(progress, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckIntegrity(QProgressDialog):
|
||||||
|
|
||||||
|
def __init__(self, db, parent=None):
|
||||||
|
QProgressDialog.__init__(self, parent)
|
||||||
|
self.db = db
|
||||||
|
self.setCancelButton(None)
|
||||||
|
self.setMinimum(0)
|
||||||
|
self.setMaximum(100)
|
||||||
|
self.setWindowTitle(_('Checking database integrity'))
|
||||||
|
self.setAutoReset(False)
|
||||||
|
self.setValue(0)
|
||||||
|
|
||||||
|
self.vthread = VacThread(self, db)
|
||||||
|
self.vthread.check_done.connect(self.check_done,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection)
|
||||||
|
self.vthread.start()
|
||||||
|
|
||||||
|
def callback(self, progress, msg):
|
||||||
|
self.setLabelText(msg)
|
||||||
|
self.setValue(int(100*progress))
|
||||||
|
|
||||||
|
def check_done(self, bad, err):
|
||||||
|
if err:
|
||||||
|
error_dialog(self, _('Error'),
|
||||||
|
_('Failed to check database integrity'),
|
||||||
|
det_msg=err, show=True)
|
||||||
|
elif bad:
|
||||||
|
titles = [self.db.title(x, index_is_id=True) for x in bad]
|
||||||
|
det_msg = '\n'.join(titles)
|
||||||
|
warning_dialog(self, _('Some inconsistencies found'),
|
||||||
|
_('The following books had formats listed in the '
|
||||||
|
'database that are not actually available. '
|
||||||
|
'The entries for the formats have been removed. '
|
||||||
|
'You should check them manually. This can '
|
||||||
|
'happen if you manipulate the files in the '
|
||||||
|
'library folder directly.'), det_msg=det_msg, show=True)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
r = self.register
|
||||||
|
r('worker_limit', config, restart_required=True)
|
||||||
|
r('enforce_cpu_limit', config, restart_required=True)
|
||||||
|
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
||||||
|
self.compact_button.clicked.connect(self.compact)
|
||||||
|
self.button_open_config_dir.clicked.connect(self.open_config_dir)
|
||||||
|
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
|
||||||
|
self.button_osx_symlinks.setVisible(isosx)
|
||||||
|
|
||||||
|
def debug_device_detection(self, *args):
|
||||||
|
from calibre.gui2.preferences.device_debug import DebugDevice
|
||||||
|
d = DebugDevice(self)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def compact(self, *args):
|
||||||
|
d = CheckIntegrity(self.gui.library_view.model().db, self)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def open_config_dir(self, *args):
|
||||||
|
from calibre.utils.config import config_dir
|
||||||
|
open_local_file(config_dir)
|
||||||
|
|
||||||
|
def create_symlinks(self):
|
||||||
|
from calibre.utils.osx_symlinks import create_symlinks
|
||||||
|
loc, paths = create_symlinks()
|
||||||
|
if loc is None:
|
||||||
|
error_dialog(self, _('Error'),
|
||||||
|
_('Failed to install command line tools.'),
|
||||||
|
det_msg=paths, show=True)
|
||||||
|
else:
|
||||||
|
info_dialog(self, _('Command line tools installed'),
|
||||||
|
'<p>'+_('Command line tools installed in')+' '+loc+
|
||||||
|
'<br>'+ _('If you move calibre.app, you have to re-install '
|
||||||
|
'the command line tools.'),
|
||||||
|
det_msg='\n'.join(paths), show=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Advanced', 'Misc')
|
||||||
|
|
144
src/calibre/gui2/preferences/misc.ui
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>502</width>
|
||||||
|
<height>314</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Maximum number of waiting worker processes (needs restart):</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_worker_limit</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_worker_limit">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>2</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>10000</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>2</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="opt_enforce_cpu_limit">
|
||||||
|
<property name="text">
|
||||||
|
<string>Limit the max. simultaneous jobs to the available CPU &cores</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<spacer name="verticalSpacer_4">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>18</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="2">
|
||||||
|
<widget class="QPushButton" name="device_detection_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Debug &device detection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<spacer name="verticalSpacer_6">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>19</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0" colspan="2">
|
||||||
|
<widget class="QPushButton" name="compact_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Check database integrity</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<spacer name="verticalSpacer_7">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>18</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="0" colspan="2">
|
||||||
|
<widget class="QPushButton" name="button_open_config_dir">
|
||||||
|
<property name="text">
|
||||||
|
<string>Open calibre &configuration directory</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="0">
|
||||||
|
<spacer name="verticalSpacer_8">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>19</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="9" column="0" colspan="2">
|
||||||
|
<widget class="QPushButton" name="button_osx_symlinks">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Install command line tools</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="10" column="0">
|
||||||
|
<spacer name="verticalSpacer_9">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>18</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
241
src/calibre/gui2/preferences/plugins.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
#!/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 textwrap, os
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
|
||||||
|
QBrush, QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QLineEdit
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
|
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
||||||
|
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
||||||
|
disable_plugin, customize_plugin, \
|
||||||
|
plugin_customization, add_plugin, \
|
||||||
|
remove_plugin
|
||||||
|
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files
|
||||||
|
|
||||||
|
class PluginModel(QAbstractItemModel): # {{{
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
QAbstractItemModel.__init__(self, *args)
|
||||||
|
self.icon = QVariant(QIcon(I('plugins.png')))
|
||||||
|
p = QIcon(self.icon).pixmap(32, 32, QIcon.Disabled, QIcon.On)
|
||||||
|
self.disabled_icon = QVariant(QIcon(p))
|
||||||
|
self._p = p
|
||||||
|
self.populate()
|
||||||
|
|
||||||
|
def populate(self):
|
||||||
|
self._data = {}
|
||||||
|
for plugin in initialized_plugins():
|
||||||
|
if plugin.type not in self._data:
|
||||||
|
self._data[plugin.type] = [plugin]
|
||||||
|
else:
|
||||||
|
self._data[plugin.type].append(plugin)
|
||||||
|
self.categories = sorted(self._data.keys())
|
||||||
|
|
||||||
|
for plugins in self._data.values():
|
||||||
|
plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower()))
|
||||||
|
|
||||||
|
def index(self, row, column, parent):
|
||||||
|
if not self.hasIndex(row, column, parent):
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
if parent.isValid():
|
||||||
|
return self.createIndex(row, column, 1+parent.row())
|
||||||
|
else:
|
||||||
|
return self.createIndex(row, column, 0)
|
||||||
|
|
||||||
|
def parent(self, index):
|
||||||
|
if not index.isValid() or index.internalId() == 0:
|
||||||
|
return QModelIndex()
|
||||||
|
return self.createIndex(index.internalId()-1, 0, 0)
|
||||||
|
|
||||||
|
def rowCount(self, parent):
|
||||||
|
if not parent.isValid():
|
||||||
|
return len(self.categories)
|
||||||
|
if parent.internalId() == 0:
|
||||||
|
category = self.categories[parent.row()]
|
||||||
|
return len(self._data[category])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def columnCount(self, parent):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def index_to_plugin(self, index):
|
||||||
|
category = self.categories[index.parent().row()]
|
||||||
|
return self._data[category][index.row()]
|
||||||
|
|
||||||
|
def plugin_to_index(self, plugin):
|
||||||
|
for i, category in enumerate(self.categories):
|
||||||
|
parent = self.index(i, 0, QModelIndex())
|
||||||
|
for j, p in enumerate(self._data[category]):
|
||||||
|
if plugin == p:
|
||||||
|
return self.index(j, 0, parent)
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
def refresh_plugin(self, plugin, rescan=False):
|
||||||
|
if rescan:
|
||||||
|
self.populate()
|
||||||
|
idx = self.plugin_to_index(plugin)
|
||||||
|
self.dataChanged.emit(idx, idx)
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
if not index.isValid():
|
||||||
|
return 0
|
||||||
|
if index.internalId() == 0:
|
||||||
|
return Qt.ItemIsEnabled
|
||||||
|
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
if not index.isValid():
|
||||||
|
return NONE
|
||||||
|
if index.internalId() == 0:
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
category = self.categories[index.row()]
|
||||||
|
return QVariant(_("%(plugin_type)s %(plugins)s")%\
|
||||||
|
dict(plugin_type=category, plugins=_('plugins')))
|
||||||
|
else:
|
||||||
|
plugin = self.index_to_plugin(index)
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
ver = '.'.join(map(str, plugin.version))
|
||||||
|
desc = '\n'.join(textwrap.wrap(plugin.description, 50))
|
||||||
|
ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), plugin.author, desc)
|
||||||
|
c = plugin_customization(plugin)
|
||||||
|
if c:
|
||||||
|
ans += _('\nCustomization: ')+c
|
||||||
|
return QVariant(ans)
|
||||||
|
if role == Qt.DecorationRole:
|
||||||
|
return self.disabled_icon if is_disabled(plugin) else self.icon
|
||||||
|
if role == Qt.ForegroundRole and is_disabled(plugin):
|
||||||
|
return QVariant(QBrush(Qt.gray))
|
||||||
|
if role == Qt.UserRole:
|
||||||
|
return plugin
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
supports_restoring_to_defaults = False
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self._plugin_model = PluginModel()
|
||||||
|
self.plugin_view.setModel(self._plugin_model)
|
||||||
|
self.plugin_view.setStyleSheet(
|
||||||
|
"QTreeView::item { padding-bottom: 10px;}")
|
||||||
|
self.toggle_plugin_button.clicked.connect(self.toggle_plugin)
|
||||||
|
self.customize_plugin_button.clicked.connect(self.customize_plugin)
|
||||||
|
self.remove_plugin_button.clicked.connect(self.remove_plugin)
|
||||||
|
self.button_plugin_browse.clicked.connect(self.find_plugin)
|
||||||
|
self.button_plugin_add.clicked.connect(self.add_plugin)
|
||||||
|
|
||||||
|
def toggle_plugin(self, *args):
|
||||||
|
self.modify_plugin(op='toggle')
|
||||||
|
|
||||||
|
def customize_plugin(self, *args):
|
||||||
|
self.modify_plugin(op='customize')
|
||||||
|
|
||||||
|
def remove_plugin(self, *args):
|
||||||
|
self.modify_plugin(op='remove')
|
||||||
|
|
||||||
|
def add_plugin(self):
|
||||||
|
path = unicode(self.plugin_path.text())
|
||||||
|
if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'):
|
||||||
|
add_plugin(path)
|
||||||
|
self._plugin_model.populate()
|
||||||
|
self._plugin_model.reset()
|
||||||
|
self.changed_signal.emit()
|
||||||
|
else:
|
||||||
|
error_dialog(self, _('No valid plugin path'),
|
||||||
|
_('%s is not a valid plugin path')%path).exec_()
|
||||||
|
|
||||||
|
def find_plugin(self):
|
||||||
|
path = choose_files(self, 'choose plugin dialog', _('Choose plugin'),
|
||||||
|
filters=[('Plugins', ['zip'])], all_files=False,
|
||||||
|
select_only_single_file=True)
|
||||||
|
if path:
|
||||||
|
self.plugin_path.setText(path[0])
|
||||||
|
|
||||||
|
def modify_plugin(self, op=''):
|
||||||
|
index = self.plugin_view.currentIndex()
|
||||||
|
if index.isValid():
|
||||||
|
plugin = self._plugin_model.index_to_plugin(index)
|
||||||
|
if op == 'toggle':
|
||||||
|
if not plugin.can_be_disabled:
|
||||||
|
error_dialog(self,_('Plugin cannot be disabled'),
|
||||||
|
_('The plugin: %s cannot be disabled')%plugin.name).exec_()
|
||||||
|
return
|
||||||
|
if is_disabled(plugin):
|
||||||
|
enable_plugin(plugin)
|
||||||
|
else:
|
||||||
|
disable_plugin(plugin)
|
||||||
|
self._plugin_model.refresh_plugin(plugin)
|
||||||
|
self.changed_signal.emit()
|
||||||
|
if op == 'customize':
|
||||||
|
if not plugin.is_customizable():
|
||||||
|
info_dialog(self, _('Plugin not customizable'),
|
||||||
|
_('Plugin: %s does not need customization')%plugin.name).exec_()
|
||||||
|
return
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
|
config_dialog = QDialog(self)
|
||||||
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
v = QVBoxLayout(config_dialog)
|
||||||
|
|
||||||
|
button_box.accepted.connect(config_dialog.accept)
|
||||||
|
button_box.rejected.connect(config_dialog.reject)
|
||||||
|
config_dialog.setWindowTitle(_('Customize') + ' ' + plugin.name)
|
||||||
|
|
||||||
|
if hasattr(plugin, 'config_widget'):
|
||||||
|
config_widget = plugin.config_widget()
|
||||||
|
v.addWidget(config_widget)
|
||||||
|
v.addWidget(button_box)
|
||||||
|
config_dialog.exec_()
|
||||||
|
|
||||||
|
if config_dialog.result() == QDialog.Accepted:
|
||||||
|
plugin.save_settings(config_widget)
|
||||||
|
self._plugin_model.refresh_plugin(plugin)
|
||||||
|
else:
|
||||||
|
help_text = plugin.customization_help(gui=True)
|
||||||
|
help_text = QLabel(help_text, config_dialog)
|
||||||
|
help_text.setWordWrap(True)
|
||||||
|
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse
|
||||||
|
| Qt.LinksAccessibleByKeyboard)
|
||||||
|
help_text.setOpenExternalLinks(True)
|
||||||
|
v.addWidget(help_text)
|
||||||
|
sc = plugin_customization(plugin)
|
||||||
|
if not sc:
|
||||||
|
sc = ''
|
||||||
|
sc = sc.strip()
|
||||||
|
sc = QLineEdit(sc, config_dialog)
|
||||||
|
v.addWidget(sc)
|
||||||
|
v.addWidget(button_box)
|
||||||
|
config_dialog.exec_()
|
||||||
|
|
||||||
|
if config_dialog.result() == QDialog.Accepted:
|
||||||
|
sc = unicode(sc.text()).strip()
|
||||||
|
customize_plugin(plugin, sc)
|
||||||
|
|
||||||
|
self._plugin_model.refresh_plugin(plugin)
|
||||||
|
elif op == 'remove':
|
||||||
|
if remove_plugin(plugin):
|
||||||
|
self._plugin_model.populate()
|
||||||
|
self._plugin_model.reset()
|
||||||
|
self.changed_signal.emit()
|
||||||
|
else:
|
||||||
|
error_dialog(self, _('Cannot remove builtin plugin'),
|
||||||
|
plugin.name + _(' cannot be removed. It is a '
|
||||||
|
'builtin plugin. Try disabling it instead.')).exec_()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Advanced', 'Plugins')
|
||||||
|
|
141
src/calibre/gui2/preferences/plugins.ui
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>723</width>
|
||||||
|
<height>540</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="text">
|
||||||
|
<string>Here you can customize the behavior of Calibre by controlling what plugins it uses.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeView" name="plugin_view">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="animated">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="headerHidden">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="toggle_plugin_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable/&Disable plugin</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="customize_plugin_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Customize plugin</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="remove_plugin_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Remove plugin</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_4">
|
||||||
|
<property name="title">
|
||||||
|
<string>Add new plugin</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_14">
|
||||||
|
<property name="text">
|
||||||
|
<string>Plugin &file:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>plugin_path</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="plugin_path"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="button_plugin_browse">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_plugin_add">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Add</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -6,15 +6,17 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import QWidget
|
from PyQt4.Qt import QWidget, pyqtSignal
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.dialogs.config.save_template_ui import Ui_Form
|
from calibre.gui2.preferences.save_template_ui import Ui_Form
|
||||||
from calibre.library.save_to_disk import FORMAT_ARG_DESCS, \
|
from calibre.library.save_to_disk import FORMAT_ARG_DESCS, \
|
||||||
preprocess_template
|
preprocess_template
|
||||||
|
|
||||||
class SaveTemplate(QWidget, Ui_Form):
|
class SaveTemplate(QWidget, Ui_Form):
|
||||||
|
|
||||||
|
changed_signal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QWidget.__init__(self, *args)
|
QWidget.__init__(self, *args)
|
||||||
Ui_Form.__init__(self)
|
Ui_Form.__init__(self)
|
||||||
@ -31,8 +33,13 @@ class SaveTemplate(QWidget, Ui_Form):
|
|||||||
|
|
||||||
self.opt_template.initialize(name+'_template_history',
|
self.opt_template.initialize(name+'_template_history',
|
||||||
default, help)
|
default, help)
|
||||||
|
self.opt_template.editTextChanged.connect(self.changed)
|
||||||
|
self.opt_template.currentIndexChanged.connect(self.changed)
|
||||||
self.option_name = name
|
self.option_name = name
|
||||||
|
|
||||||
|
def changed(self, *args):
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
tmpl = preprocess_template(self.opt_template.text())
|
tmpl = preprocess_template(self.opt_template.text())
|
||||||
fa = {}
|
fa = {}
|
||||||
@ -47,6 +54,9 @@ class SaveTemplate(QWidget, Ui_Form):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_value(self, val):
|
||||||
|
self.opt_template.set_value(val)
|
||||||
|
|
||||||
def save_settings(self, config, name):
|
def save_settings(self, config, name):
|
||||||
val = unicode(self.opt_template.text())
|
val = unicode(self.opt_template.text())
|
||||||
config.set(name, val)
|
config.set(name, val)
|
@ -52,7 +52,7 @@
|
|||||||
<customwidget>
|
<customwidget>
|
||||||
<class>HistoryBox</class>
|
<class>HistoryBox</class>
|
||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>calibre/gui2/dialogs/config/history.h</header>
|
<header>calibre/gui2/preferences/history.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
56
src/calibre/gui2/preferences/saving.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
|
||||||
|
AbortCommit
|
||||||
|
from calibre.gui2.preferences.saving_ui import Ui_Form
|
||||||
|
from calibre.utils.config import ConfigProxy
|
||||||
|
from calibre.library.save_to_disk import config
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self.proxy = ConfigProxy(config())
|
||||||
|
|
||||||
|
r = self.register
|
||||||
|
|
||||||
|
for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf',
|
||||||
|
'replace_whitespace', 'to_lowercase', 'formats', 'timefmt'):
|
||||||
|
r(x, self.proxy)
|
||||||
|
|
||||||
|
self.save_template.changed_signal.connect(self.changed_signal.emit)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
ConfigWidgetBase.initialize(self)
|
||||||
|
self.save_template.blockSignals(True)
|
||||||
|
self.save_template.initialize('save_to_disk', self.proxy['template'],
|
||||||
|
self.proxy.help('template'))
|
||||||
|
self.save_template.blockSignals(False)
|
||||||
|
|
||||||
|
def restore_defaults(self):
|
||||||
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
|
self.save_template.set_value(self.proxy.defaults['template'])
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
if not self.save_template.validate():
|
||||||
|
raise AbortCommit('abort')
|
||||||
|
self.save_template.save_settings(self.proxy, 'template')
|
||||||
|
return ConfigWidgetBase.commit(self)
|
||||||
|
|
||||||
|
def refresh_gui(self, gui):
|
||||||
|
gui.iactions['Save To Disk'].reread_prefs()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Import/Export', 'Saving')
|
||||||
|
|
110
src/calibre/gui2/preferences/saving.ui
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>707</width>
|
||||||
|
<height>340</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Here you can control how calibre will save your books when you click the Save to Disk button:</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_save_cover">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save &cover separately</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QCheckBox" name="opt_replace_whitespace">
|
||||||
|
<property name="text">
|
||||||
|
<string>Replace space with &underscores</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_update_metadata">
|
||||||
|
<property name="text">
|
||||||
|
<string>Update &metadata in saved copies</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QCheckBox" name="opt_to_lowercase">
|
||||||
|
<property name="text">
|
||||||
|
<string>Change paths to &lowercase</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Format &dates as:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_timefmt</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QLineEdit" name="opt_timefmt"/>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>File &formats to save:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_formats</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="1">
|
||||||
|
<widget class="QLineEdit" name="opt_formats"/>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="0" colspan="2">
|
||||||
|
<widget class="SaveTemplate" name="save_template" native="true"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QCheckBox" name="opt_asciiize">
|
||||||
|
<property name="text">
|
||||||
|
<string>Convert non-English characters to &English equivalents</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_write_opf">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save metadata in &OPF file</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>SaveTemplate</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>calibre/gui2/preferences/save_template.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
54
src/calibre/gui2/preferences/sending.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
|
||||||
|
AbortCommit
|
||||||
|
from calibre.gui2.preferences.sending_ui import Ui_Form
|
||||||
|
from calibre.utils.config import ConfigProxy
|
||||||
|
from calibre.library.save_to_disk import config
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self.proxy = ConfigProxy(config())
|
||||||
|
|
||||||
|
r = self.register
|
||||||
|
|
||||||
|
choices = [(_('Manual management'), 'manual'),
|
||||||
|
(_('Only on send'), 'on_send'),
|
||||||
|
(_('Automatic management'), 'on_connect')]
|
||||||
|
r('manage_device_metadata', prefs, choices=choices)
|
||||||
|
|
||||||
|
self.send_template.changed_signal.connect(self.changed_signal.emit)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
ConfigWidgetBase.initialize(self)
|
||||||
|
self.send_template.blockSignals(True)
|
||||||
|
self.send_template.initialize('send_to_device', self.proxy['send_template'],
|
||||||
|
self.proxy.help('send_template'))
|
||||||
|
self.send_template.blockSignals(False)
|
||||||
|
|
||||||
|
def restore_defaults(self):
|
||||||
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
|
self.send_template.set_value(self.proxy.defaults['send_template'])
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
if not self.send_template.validate():
|
||||||
|
raise AbortCommit('abort')
|
||||||
|
self.send_template.save_settings(self.proxy, 'send_template')
|
||||||
|
return ConfigWidgetBase.commit(self)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Import/Export', 'Sending')
|
||||||
|
|
108
src/calibre/gui2/preferences/sending.ui
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>807</width>
|
||||||
|
<height>331</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Metadata &management:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_manage_device_metadata</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="opt_manage_device_metadata">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string extracomment="foobar">Manual management</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Only on send</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Automatic management</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>457</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="3">
|
||||||
|
<widget class="QLabel" name="label_41">
|
||||||
|
<property name="text">
|
||||||
|
<string><li><b>Manual management</b>: Calibre updates the metadata and adds collections only when a book is sent. With this option, calibre will never remove a collection.</li>
|
||||||
|
<li><b>Only on send</b>: Calibre updates metadata and adds/removes collections for a book only when it is sent to the device. </li>
|
||||||
|
<li><b>Automatic management</b>: Calibre automatically keeps metadata on the device in sync with the calibre library, on every connect</li></ul></string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="3">
|
||||||
|
<widget class="QLabel" name="label_43">
|
||||||
|
<property name="text">
|
||||||
|
<string>Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Advanced->Plugins</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="3">
|
||||||
|
<widget class="SaveTemplate" name="send_template" native="true"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>SaveTemplate</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>calibre/gui2/preferences/save_template.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
137
src/calibre/gui2/preferences/server.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
#!/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 time
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \
|
||||||
|
QPlainTextEdit, QDialogButtonBox
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
|
from calibre.gui2.preferences.server_ui import Ui_Form
|
||||||
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
from calibre.library.server import server_config
|
||||||
|
from calibre.utils.config import ConfigProxy
|
||||||
|
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
|
||||||
|
Dispatcher
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self.proxy = ConfigProxy(server_config())
|
||||||
|
db = self.db = gui.library_view.model().db
|
||||||
|
self.server = self.gui.content_server
|
||||||
|
|
||||||
|
r = self.register
|
||||||
|
|
||||||
|
r('port', self.proxy)
|
||||||
|
r('username', self.proxy)
|
||||||
|
r('password', self.proxy)
|
||||||
|
r('max_cover', self.proxy)
|
||||||
|
r('max_opds_items', self.proxy)
|
||||||
|
r('max_opds_ungrouped_items', self.proxy)
|
||||||
|
|
||||||
|
self.show_server_password.stateChanged[int].connect(
|
||||||
|
lambda s: self.opt_password.setEchoMode(
|
||||||
|
self.opt_password.Normal if s == Qt.Checked
|
||||||
|
else self.opt_password.Password))
|
||||||
|
self.opt_password.setEchoMode(self.opt_password.Password)
|
||||||
|
|
||||||
|
restrictions = sorted(saved_searches().names(),
|
||||||
|
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||||
|
choices = [('', '')] + [(x, x) for x in restrictions]
|
||||||
|
r('cs_restriction', db.prefs, choices=choices)
|
||||||
|
|
||||||
|
self.start_button.setEnabled(not getattr(self.server, 'is_running', False))
|
||||||
|
self.test_button.setEnabled(not self.start_button.isEnabled())
|
||||||
|
self.stop_button.setDisabled(self.start_button.isEnabled())
|
||||||
|
self.start_button.clicked.connect(self.start_server)
|
||||||
|
self.stop_button.clicked.connect(self.stop_server)
|
||||||
|
self.test_button.clicked.connect(self.test_server)
|
||||||
|
self.view_logs.clicked.connect(self.view_server_logs)
|
||||||
|
|
||||||
|
r('autolaunch_server', config)
|
||||||
|
|
||||||
|
def set_server_options(self):
|
||||||
|
c = self.proxy
|
||||||
|
c.set('port', self.opt_port.value())
|
||||||
|
c.set('username', unicode(self.opt_username.text()).strip())
|
||||||
|
p = unicode(self.opt_password.text()).strip()
|
||||||
|
if not p:
|
||||||
|
p = None
|
||||||
|
c.set('password', p)
|
||||||
|
|
||||||
|
def start_server(self):
|
||||||
|
self.set_server_options()
|
||||||
|
from calibre.library.server.main import start_threaded_server
|
||||||
|
self.server = start_threaded_server(self.db, server_config().parse())
|
||||||
|
while not self.server.is_running and self.server.exception is None:
|
||||||
|
time.sleep(1)
|
||||||
|
if self.server.exception is not None:
|
||||||
|
error_dialog(self, _('Failed to start content server'),
|
||||||
|
unicode(self.server.exception)).exec_()
|
||||||
|
return
|
||||||
|
self.start_button.setEnabled(False)
|
||||||
|
self.test_button.setEnabled(True)
|
||||||
|
self.stop_button.setEnabled(True)
|
||||||
|
|
||||||
|
def stop_server(self):
|
||||||
|
from calibre.library.server.main import stop_threaded_server
|
||||||
|
stop_threaded_server(self.server)
|
||||||
|
self.server = None
|
||||||
|
self.start_button.setEnabled(True)
|
||||||
|
self.test_button.setEnabled(False)
|
||||||
|
self.stop_button.setEnabled(False)
|
||||||
|
|
||||||
|
def test_server(self):
|
||||||
|
open_url(QUrl('http://127.0.0.1:'+str(self.opt_port.value())))
|
||||||
|
|
||||||
|
def view_server_logs(self):
|
||||||
|
from calibre.library.server import log_access_file, log_error_file
|
||||||
|
d = QDialog(self)
|
||||||
|
d.resize(QSize(800, 600))
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
d.setLayout(layout)
|
||||||
|
layout.addWidget(QLabel(_('Error log:')))
|
||||||
|
el = QPlainTextEdit(d)
|
||||||
|
layout.addWidget(el)
|
||||||
|
try:
|
||||||
|
el.setPlainText(open(log_error_file, 'rb').read().decode('utf8', 'replace'))
|
||||||
|
except IOError:
|
||||||
|
el.setPlainText('No error log found')
|
||||||
|
layout.addWidget(QLabel(_('Access log:')))
|
||||||
|
al = QPlainTextEdit(d)
|
||||||
|
layout.addWidget(al)
|
||||||
|
try:
|
||||||
|
al.setPlainText(open(log_access_file, 'rb').read().decode('utf8', 'replace'))
|
||||||
|
except IOError:
|
||||||
|
al.setPlainText('No access log found')
|
||||||
|
bx = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||||
|
layout.addWidget(bx)
|
||||||
|
bx.accepted.connect(d.accept)
|
||||||
|
d.show()
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
ConfigWidgetBase.commit(self)
|
||||||
|
warning_dialog(self, _('Restart needed'),
|
||||||
|
_('You need to restart the server for changes to'
|
||||||
|
' take effect'), show=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def refresh_gui(self, gui):
|
||||||
|
gui.content_server = self.server
|
||||||
|
if gui.content_server is not None:
|
||||||
|
gui.content_server.state_callback = \
|
||||||
|
Dispatcher(gui.iactions['Connect Share'].content_server_state_changed)
|
||||||
|
gui.content_server.state_callback(gui.content_server.is_running)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Sharing', 'Server')
|
||||||
|
|
261
src/calibre/gui2/preferences/server.ui
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>641</width>
|
||||||
|
<height>563</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_5">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_10">
|
||||||
|
<property name="text">
|
||||||
|
<string>Server &port:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_port</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_port">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>65535</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>8080</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_11">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Username:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_username</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="opt_username"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_12">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Password:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_password</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLineEdit" name="opt_password">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>If you leave the password blank, anyone will be able to access your book collection using the web interface.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QLineEdit" name="opt_max_cover">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>The maximum size (widthxheight) for displayed covers. Larger covers are resized. </string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max. &cover size:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_max_cover</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QCheckBox" name="show_server_password">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Show password</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="label_15">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max. &OPDS items per query:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_max_opds_items</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_max_opds_items">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>10</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>10000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>25</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1000000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QLabel" name="label_16">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max. OPDS &ungrouped items:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_max_opds_ungrouped_items</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="0">
|
||||||
|
<widget class="QLabel" name="label_164">
|
||||||
|
<property name="text">
|
||||||
|
<string>Restriction (saved search) to apply:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="1">
|
||||||
|
<widget class="QComboBox" name="opt_cs_restriction">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>This restriction (based on a saved search) will restrict the books the content server makes available to those matching the search. This setting is per library (i.e. you can have a different restriction per library).</string>
|
||||||
|
</property>
|
||||||
|
<property name="sizeAdjustPolicy">
|
||||||
|
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||||
|
</property>
|
||||||
|
<property name="minimumContentsLength">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="start_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Start Server</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="stop_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>St&op Server</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="test_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Test Server</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_9">
|
||||||
|
<property name="text">
|
||||||
|
<string>calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="opt_autolaunch_server">
|
||||||
|
<property name="text">
|
||||||
|
<string>Run server &automatically on startup</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="view_logs">
|
||||||
|
<property name="text">
|
||||||
|
<string>View &server logs</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>36</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_13">
|
||||||
|
<property name="text">
|
||||||
|
<string><p>Remember to leave calibre running as the server only runs as long as calibre is running.
|
||||||
|
<p>Stanza should see your calibre collection automatically. If not, try adding the URL http://myhostname:8080 as a new catalog in the Stanza reader on your iPhone. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>37</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
58
src/calibre/gui2/preferences/tweaks.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#!/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'
|
||||||
|
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
|
||||||
|
from calibre.gui2.preferences.tweaks_ui import Ui_Form
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
from calibre.utils.config import read_raw_tweaks, write_tweaks
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self.current_tweaks.textChanged.connect(self.changed)
|
||||||
|
|
||||||
|
def changed(self, *args):
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
deft, curt = read_raw_tweaks()
|
||||||
|
self.current_tweaks.blockSignals(True)
|
||||||
|
self.current_tweaks.setPlainText(curt.decode('utf-8'))
|
||||||
|
self.current_tweaks.blockSignals(False)
|
||||||
|
|
||||||
|
self.default_tweaks.setPlainText(deft.decode('utf-8'))
|
||||||
|
|
||||||
|
def restore_defaults(self):
|
||||||
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
|
deft, curt = read_raw_tweaks()
|
||||||
|
self.current_tweaks.setPlainText(deft.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
raw = unicode(self.current_tweaks.toPlainText()).encode('utf-8')
|
||||||
|
try:
|
||||||
|
exec raw
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
error_dialog(self, _('Invalid tweaks'),
|
||||||
|
_('The tweaks you entered are invalid, try resetting the'
|
||||||
|
' tweaks to default and changing them one by one until'
|
||||||
|
' you find the invalid setting.'),
|
||||||
|
det_msg=traceback.format_exc(), show=True)
|
||||||
|
raise AbortCommit('abort')
|
||||||
|
write_tweaks(raw)
|
||||||
|
ConfigWidgetBase.commit(self)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Advanced', 'Tweaks')
|
||||||
|
|
59
src/calibre/gui2/preferences/tweaks.ui
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>660</width>
|
||||||
|
<height>351</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_18">
|
||||||
|
<property name="text">
|
||||||
|
<string>Values for the tweaks are shown below. Edit them to change the behavior of calibre. Your changes will only take effect after a restart of calibre.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_6">
|
||||||
|
<property name="title">
|
||||||
|
<string>All available tweaks</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_11">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QPlainTextEdit" name="default_tweaks">
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_7">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Current tweaks</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_10">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QPlainTextEdit" name="current_tweaks"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -424,10 +424,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.categories = []
|
self.categories = []
|
||||||
|
|
||||||
# Reconstruct the user categories, putting them into metadata
|
# Reconstruct the user categories, putting them into metadata
|
||||||
|
self.db.field_metadata.remove_dynamic_categories()
|
||||||
tb_cats = self.db.field_metadata
|
tb_cats = self.db.field_metadata
|
||||||
for k in tb_cats.keys():
|
|
||||||
if tb_cats[k]['kind'] in ['user', 'search']:
|
|
||||||
del tb_cats[k]
|
|
||||||
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys()):
|
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys()):
|
||||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||||
@ -514,7 +512,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
_('The saved search name %s is already used.')%val).exec_()
|
_('The saved search name %s is already used.')%val).exec_()
|
||||||
return False
|
return False
|
||||||
saved_searches().rename(unicode(item.data(role).toString()), val)
|
saved_searches().rename(unicode(item.data(role).toString()), val)
|
||||||
self.tags_view.search_item_renamed.emit()
|
item.tag.name = val
|
||||||
|
self.tags_view.search_item_renamed.emit() # Does a refresh
|
||||||
else:
|
else:
|
||||||
if key == 'series':
|
if key == 'series':
|
||||||
self.db.rename_series(item.tag.id, val)
|
self.db.rename_series(item.tag.id, val)
|
||||||
@ -528,8 +527,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.db.rename_custom_item(item.tag.id, val,
|
self.db.rename_custom_item(item.tag.id, val,
|
||||||
label=self.db.field_metadata[key]['label'])
|
label=self.db.field_metadata[key]['label'])
|
||||||
self.tags_view.tag_item_renamed.emit()
|
self.tags_view.tag_item_renamed.emit()
|
||||||
item.tag.name = val
|
item.tag.name = val
|
||||||
self.refresh() # Should work, because no categories can have disappeared
|
self.refresh() # Should work, because no categories can have disappeared
|
||||||
if path:
|
if path:
|
||||||
idx = self.index_for_path(path)
|
idx = self.index_for_path(path)
|
||||||
if idx.isValid():
|
if idx.isValid():
|
||||||
@ -671,7 +670,7 @@ class TagBrowserMixin(object): # {{{
|
|||||||
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||||
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
|
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
|
||||||
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
||||||
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
|
self.tags_view.search_item_renamed.connect(self.saved_searches_changed)
|
||||||
self.edit_categories.clicked.connect(lambda x:
|
self.edit_categories.clicked.connect(lambda x:
|
||||||
self.do_user_categories_edit())
|
self.do_user_categories_edit())
|
||||||
|
|
||||||
|
@ -238,7 +238,7 @@ def fetch_scheduled_recipe(arg):
|
|||||||
|
|
||||||
return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt]
|
return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt]
|
||||||
|
|
||||||
def generate_catalog(parent, dbspec, ids, device):
|
def generate_catalog(parent, dbspec, ids, device_manager):
|
||||||
from calibre.gui2.dialogs.catalog import Catalog
|
from calibre.gui2.dialogs.catalog import Catalog
|
||||||
|
|
||||||
# Build the Catalog dialog in gui2.dialogs.catalog
|
# Build the Catalog dialog in gui2.dialogs.catalog
|
||||||
@ -252,9 +252,18 @@ def generate_catalog(parent, dbspec, ids, device):
|
|||||||
|
|
||||||
# Profile the connected device
|
# Profile the connected device
|
||||||
# Parallel initialization in calibre.library.cli:command_catalog()
|
# Parallel initialization in calibre.library.cli:command_catalog()
|
||||||
connected_device = { 'storage':None,'serial':None,'save_template':None,'name':None}
|
connected_device = {
|
||||||
|
'is_device_connected': device_manager.is_device_connected,
|
||||||
|
'kind': device_manager.connected_device_kind,
|
||||||
|
'name': None,
|
||||||
|
'save_template': None,
|
||||||
|
'serial': None,
|
||||||
|
'storage': None
|
||||||
|
}
|
||||||
|
|
||||||
if device:
|
if device_manager.is_device_connected:
|
||||||
|
device = device_manager.device
|
||||||
|
connected_device['name'] = device.gui_name
|
||||||
try:
|
try:
|
||||||
storage = []
|
storage = []
|
||||||
if device._main_prefix:
|
if device._main_prefix:
|
||||||
@ -263,11 +272,10 @@ def generate_catalog(parent, dbspec, ids, device):
|
|||||||
storage.append(os.path.join(device._card_a_prefix, device.EBOOK_DIR_CARD_A))
|
storage.append(os.path.join(device._card_a_prefix, device.EBOOK_DIR_CARD_A))
|
||||||
if device._card_b_prefix:
|
if device._card_b_prefix:
|
||||||
storage.append(os.path.join(device._card_b_prefix, device.EBOOK_DIR_CARD_B))
|
storage.append(os.path.join(device._card_b_prefix, device.EBOOK_DIR_CARD_B))
|
||||||
connected_device = { 'storage': storage,
|
connected_device['storage'] = storage
|
||||||
'serial': device.detected_device.serial if \
|
connected_device['serial'] = device.detected_device.serial if \
|
||||||
hasattr(device.detected_device,'serial') else None,
|
hasattr(device.detected_device,'serial') else None
|
||||||
'save_template': device.save_template(),
|
connected_device['save_template'] = device.save_template()
|
||||||
'name': device.gui_name}
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -522,6 +522,16 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
|
|
||||||
|
|
||||||
def shutdown(self, write_settings=True):
|
def shutdown(self, write_settings=True):
|
||||||
|
try:
|
||||||
|
db = self.library_view.model().db
|
||||||
|
cf = db.clean
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
cf()
|
||||||
|
# Save the current field_metadata for applications like calibre2opds
|
||||||
|
# Goes here, because if cf is valid, db is valid.
|
||||||
|
db.prefs['field_metadata'] = db.field_metadata.all_metadata()
|
||||||
for action in self.iactions.values():
|
for action in self.iactions.values():
|
||||||
if not action.shutting_down():
|
if not action.shutting_down():
|
||||||
return
|
return
|
||||||
|
@ -173,6 +173,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.pending_anchor = None
|
self.pending_anchor = None
|
||||||
self.pending_reference = None
|
self.pending_reference = None
|
||||||
self.pending_bookmark = None
|
self.pending_bookmark = None
|
||||||
|
self.existing_bookmarks= []
|
||||||
self.selected_text = None
|
self.selected_text = None
|
||||||
self.read_settings()
|
self.read_settings()
|
||||||
self.dictionary_box.hide()
|
self.dictionary_box.hide()
|
||||||
@ -415,15 +416,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.action_font_size_smaller.setEnabled(self.view.multiplier() > 0.2)
|
self.action_font_size_smaller.setEnabled(self.view.multiplier() > 0.2)
|
||||||
self.set_page_number(frac)
|
self.set_page_number(frac)
|
||||||
|
|
||||||
def bookmark(self, *args):
|
|
||||||
title, ok = QInputDialog.getText(self, _('Add bookmark'), _('Enter title for bookmark:'))
|
|
||||||
title = unicode(title).strip()
|
|
||||||
if ok and title:
|
|
||||||
pos = self.view.bookmark()
|
|
||||||
bookmark = '%d#%s'%(self.current_index, pos)
|
|
||||||
self.iterator.add_bookmark((title, bookmark))
|
|
||||||
self.set_bookmarks(self.iterator.bookmarks)
|
|
||||||
|
|
||||||
|
|
||||||
def find(self, text, repeat=False, backwards=False):
|
def find(self, text, repeat=False, backwards=False):
|
||||||
if not text:
|
if not text:
|
||||||
@ -539,15 +531,34 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
getattr(self, o).setEnabled(False)
|
getattr(self, o).setEnabled(False)
|
||||||
self.setCursor(Qt.BusyCursor)
|
self.setCursor(Qt.BusyCursor)
|
||||||
|
|
||||||
|
def bookmark(self, *args):
|
||||||
|
num = 1
|
||||||
|
bm = None
|
||||||
|
while True:
|
||||||
|
bm = _('Bookmark #%d')%num
|
||||||
|
if bm not in self.existing_bookmarks:
|
||||||
|
break
|
||||||
|
num += 1
|
||||||
|
title, ok = QInputDialog.getText(self, _('Add bookmark'),
|
||||||
|
_('Enter title for bookmark:'), text=bm)
|
||||||
|
title = unicode(title).strip()
|
||||||
|
if ok and title:
|
||||||
|
pos = self.view.bookmark()
|
||||||
|
bookmark = '%d#%s'%(self.current_index, pos)
|
||||||
|
self.iterator.add_bookmark((title, bookmark))
|
||||||
|
self.set_bookmarks(self.iterator.bookmarks)
|
||||||
|
|
||||||
def set_bookmarks(self, bookmarks):
|
def set_bookmarks(self, bookmarks):
|
||||||
self.bookmarks_menu.clear()
|
self.bookmarks_menu.clear()
|
||||||
self.bookmarks_menu.addAction(_("Manage Bookmarks"), self.manage_bookmarks)
|
self.bookmarks_menu.addAction(_("Manage Bookmarks"), self.manage_bookmarks)
|
||||||
self.bookmarks_menu.addSeparator()
|
self.bookmarks_menu.addSeparator()
|
||||||
current_page = None
|
current_page = None
|
||||||
|
self.existing_bookmarks = []
|
||||||
for bm in bookmarks:
|
for bm in bookmarks:
|
||||||
if bm[0] == 'calibre_current_page_bookmark':
|
if bm[0] == 'calibre_current_page_bookmark':
|
||||||
current_page = bm
|
current_page = bm
|
||||||
else:
|
else:
|
||||||
|
self.existing_bookmarks.append(bm[0])
|
||||||
self.bookmarks_menu.addAction(bm[0], partial(self.goto_bookmark, bm))
|
self.bookmarks_menu.addAction(bm[0], partial(self.goto_bookmark, bm))
|
||||||
return current_page
|
return current_page
|
||||||
|
|
||||||
|
@ -1032,6 +1032,9 @@ class Splitter(QSplitter):
|
|||||||
|
|
||||||
# Public API {{{
|
# Public API {{{
|
||||||
|
|
||||||
|
def update_desired_state(self):
|
||||||
|
self.desired_show = not self.is_side_index_hidden
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
if self.count() > 1:
|
if self.count() > 1:
|
||||||
gprefs[self.save_name+'_state'] = self.get_state()
|
gprefs[self.save_name+'_state'] = self.get_state()
|
||||||
|
@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import cStringIO, sys
|
import cStringIO, sys
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
|
||||||
from PyQt4.Qt import QWidget, SIGNAL, QDialog, Qt
|
from PyQt4.Qt import QWidget, pyqtSignal, QDialog, Qt
|
||||||
|
|
||||||
from calibre.gui2.wizard.send_email_ui import Ui_Form
|
from calibre.gui2.wizard.send_email_ui import Ui_Form
|
||||||
from calibre.utils.smtp import config as smtp_prefs
|
from calibre.utils.smtp import config as smtp_prefs
|
||||||
@ -24,7 +24,7 @@ class TestEmail(QDialog, TE_Dialog):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
opts = smtp_prefs().parse()
|
opts = smtp_prefs().parse()
|
||||||
self.test_func = parent.test_email_settings
|
self.test_func = parent.test_email_settings
|
||||||
self.connect(self.test_button, SIGNAL('clicked(bool)'), self.test)
|
self.test_button.clicked.connect(self.test)
|
||||||
self.from_.setText(unicode(self.from_.text())%opts.from_)
|
self.from_.setText(unicode(self.from_.text())%opts.from_)
|
||||||
if pa:
|
if pa:
|
||||||
self.to.setText(pa)
|
self.to.setText(pa)
|
||||||
@ -33,7 +33,7 @@ class TestEmail(QDialog, TE_Dialog):
|
|||||||
(opts.relay_username, unhexlify(opts.relay_password),
|
(opts.relay_username, unhexlify(opts.relay_password),
|
||||||
opts.relay_host, opts.relay_port, opts.encryption))
|
opts.relay_host, opts.relay_port, opts.encryption))
|
||||||
|
|
||||||
def test(self):
|
def test(self, *args):
|
||||||
self.log.setPlainText(_('Sending...'))
|
self.log.setPlainText(_('Sending...'))
|
||||||
self.test_button.setEnabled(False)
|
self.test_button.setEnabled(False)
|
||||||
try:
|
try:
|
||||||
@ -47,6 +47,8 @@ class TestEmail(QDialog, TE_Dialog):
|
|||||||
|
|
||||||
class SendEmail(QWidget, Ui_Form):
|
class SendEmail(QWidget, Ui_Form):
|
||||||
|
|
||||||
|
changed_signal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -57,23 +59,31 @@ class SendEmail(QWidget, Ui_Form):
|
|||||||
self.smtp_opts = opts
|
self.smtp_opts = opts
|
||||||
if opts.from_:
|
if opts.from_:
|
||||||
self.email_from.setText(opts.from_)
|
self.email_from.setText(opts.from_)
|
||||||
|
self.email_from.textChanged.connect(self.changed)
|
||||||
if opts.relay_host:
|
if opts.relay_host:
|
||||||
self.relay_host.setText(opts.relay_host)
|
self.relay_host.setText(opts.relay_host)
|
||||||
|
self.relay_host.textChanged.connect(self.changed)
|
||||||
self.relay_port.setValue(opts.relay_port)
|
self.relay_port.setValue(opts.relay_port)
|
||||||
|
self.relay_port.valueChanged.connect(self.changed)
|
||||||
if opts.relay_username:
|
if opts.relay_username:
|
||||||
self.relay_username.setText(opts.relay_username)
|
self.relay_username.setText(opts.relay_username)
|
||||||
|
self.relay_username.textChanged.connect(self.changed)
|
||||||
if opts.relay_password:
|
if opts.relay_password:
|
||||||
self.relay_password.setText(unhexlify(opts.relay_password))
|
self.relay_password.setText(unhexlify(opts.relay_password))
|
||||||
|
self.relay_password.textChanged.connect(self.changed)
|
||||||
(self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True)
|
(self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True)
|
||||||
self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'),
|
self.relay_tls.toggled.connect(self.changed)
|
||||||
self.create_gmail_relay)
|
|
||||||
self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'),
|
|
||||||
lambda
|
|
||||||
state:self.relay_password.setEchoMode(self.relay_password.Password if
|
|
||||||
state == 0 else self.relay_password.Normal))
|
|
||||||
self.connect(self.test_email_button, SIGNAL('clicked(bool)'),
|
|
||||||
self.test_email)
|
|
||||||
|
|
||||||
|
self.relay_use_gmail.clicked.connect(
|
||||||
|
self.create_gmail_relay)
|
||||||
|
self.relay_show_password.stateChanged.connect(
|
||||||
|
lambda state : self.relay_password.setEchoMode(
|
||||||
|
self.relay_password.Password if
|
||||||
|
state == 0 else self.relay_password.Normal))
|
||||||
|
self.test_email_button.clicked.connect(self.test_email)
|
||||||
|
|
||||||
|
def changed(self, *args):
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
def test_email(self, *args):
|
def test_email(self, *args):
|
||||||
pa = self.preferred_to_address()
|
pa = self.preferred_to_address()
|
||||||
|
@ -324,6 +324,7 @@ def do_remove(db, ids):
|
|||||||
db.delete_book(y)
|
db.delete_book(y)
|
||||||
|
|
||||||
send_message()
|
send_message()
|
||||||
|
db.clean()
|
||||||
|
|
||||||
def remove_option_parser():
|
def remove_option_parser():
|
||||||
return get_parser(_(
|
return get_parser(_(
|
||||||
@ -449,6 +450,7 @@ def command_show_metadata(args, dbpath):
|
|||||||
def do_set_metadata(db, id, stream):
|
def do_set_metadata(db, id, stream):
|
||||||
mi = OPF(stream)
|
mi = OPF(stream)
|
||||||
db.set_metadata(id, mi)
|
db.set_metadata(id, mi)
|
||||||
|
db.clean()
|
||||||
do_show_metadata(db, id, False)
|
do_show_metadata(db, id, False)
|
||||||
send_message()
|
send_message()
|
||||||
|
|
||||||
@ -574,6 +576,9 @@ def command_add_custom_column(args, dbpath):
|
|||||||
return 1
|
return 1
|
||||||
do_add_custom_column(get_db(dbpath, opts), args[0], args[1], args[2],
|
do_add_custom_column(get_db(dbpath, opts), args[0], args[1], args[2],
|
||||||
opts.is_multiple, json.loads(opts.display))
|
opts.is_multiple, json.loads(opts.display))
|
||||||
|
# Re-open the DB so that field_metadata is reflects the column changes
|
||||||
|
db = get_db(dbpath, opts)
|
||||||
|
db.prefs['field_metadata'] = db.field_metadata.all_metadata()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def catalog_option_parser(args):
|
def catalog_option_parser(args):
|
||||||
@ -672,7 +677,14 @@ def command_catalog(args, dbpath):
|
|||||||
|
|
||||||
# No support for connected device in CLI environment
|
# No support for connected device in CLI environment
|
||||||
# Parallel initialization in calibre.gui2.tools:generate_catalog()
|
# Parallel initialization in calibre.gui2.tools:generate_catalog()
|
||||||
opts.connected_device = { 'storage':None,'serial':None,'save_template':None,'name':None}
|
opts.connected_device = {
|
||||||
|
'is_device_connected': False,
|
||||||
|
'kind': None,
|
||||||
|
'name': None,
|
||||||
|
'save_template': None,
|
||||||
|
'serial': None,
|
||||||
|
'storage': None,
|
||||||
|
}
|
||||||
|
|
||||||
with plugin:
|
with plugin:
|
||||||
plugin.run(args[1], opts, get_db(dbpath, opts))
|
plugin.run(args[1], opts, get_db(dbpath, opts))
|
||||||
@ -797,6 +809,9 @@ def command_remove_custom_column(args, dbpath):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
do_remove_custom_column(get_db(dbpath, opts), args[0], opts.force)
|
do_remove_custom_column(get_db(dbpath, opts), args[0], opts.force)
|
||||||
|
# Re-open the DB so that field_metadata is reflects the column changes
|
||||||
|
db = get_db(dbpath, opts)
|
||||||
|
db.prefs['field_metadata'] = db.field_metadata.all_metadata()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def saved_searches_option_parser():
|
def saved_searches_option_parser():
|
||||||
|
@ -144,6 +144,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.initialize_dynamic()
|
self.initialize_dynamic()
|
||||||
|
|
||||||
def initialize_dynamic(self):
|
def initialize_dynamic(self):
|
||||||
|
self.field_metadata = FieldMetadata() #Ensure we start with a clean copy
|
||||||
self.prefs = DBPrefs(self)
|
self.prefs = DBPrefs(self)
|
||||||
defs = self.prefs.defaults
|
defs = self.prefs.defaults
|
||||||
defs['gui_restriction'] = defs['cs_restriction'] = ''
|
defs['gui_restriction'] = defs['cs_restriction'] = ''
|
||||||
@ -289,10 +290,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
# Reconstruct the user categories, putting them into field_metadata
|
# Reconstruct the user categories, putting them into field_metadata
|
||||||
# Assumption is that someone else will fix them if they change.
|
# Assumption is that someone else will fix them if they change.
|
||||||
|
self.field_metadata.remove_dynamic_categories()
|
||||||
tb_cats = self.field_metadata
|
tb_cats = self.field_metadata
|
||||||
for k in tb_cats.keys():
|
|
||||||
if tb_cats[k]['kind'] in ['user', 'search']:
|
|
||||||
del tb_cats[k]
|
|
||||||
for user_cat in sorted(self.prefs.get('user_categories', {}).keys()):
|
for user_cat in sorted(self.prefs.get('user_categories', {}).keys()):
|
||||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||||
@ -331,7 +330,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn',
|
for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn',
|
||||||
'publisher', 'rating', 'series', 'series_index', 'tags',
|
'publisher', 'rating', 'series', 'series_index', 'tags',
|
||||||
'title', 'timestamp', 'uuid', 'pubdate'):
|
'title', 'timestamp', 'uuid', 'pubdate', 'ondevice'):
|
||||||
setattr(self, prop, functools.partial(get_property,
|
setattr(self, prop, functools.partial(get_property,
|
||||||
loc=self.FIELD_MAP['comments' if prop == 'comment' else prop]))
|
loc=self.FIELD_MAP['comments' if prop == 'comment' else prop]))
|
||||||
setattr(self, 'title_sort', functools.partial(get_property,
|
setattr(self, 'title_sort', functools.partial(get_property,
|
||||||
@ -639,16 +638,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
def book_on_device_string(self, id):
|
def book_on_device_string(self, id):
|
||||||
loc = []
|
loc = []
|
||||||
|
count = 0
|
||||||
on = self.book_on_device(id)
|
on = self.book_on_device(id)
|
||||||
if on is not None:
|
if on is not None:
|
||||||
m, a, b = on
|
m, a, b, count = on[:4]
|
||||||
if m is not None:
|
if m is not None:
|
||||||
loc.append(_('Main'))
|
loc.append(_('Main'))
|
||||||
if a is not None:
|
if a is not None:
|
||||||
loc.append(_('Card A'))
|
loc.append(_('Card A'))
|
||||||
if b is not None:
|
if b is not None:
|
||||||
loc.append(_('Card B'))
|
loc.append(_('Card B'))
|
||||||
return ', '.join(loc)
|
return ', '.join(loc) + ((' (%s books)'%count) if count > 1 else '')
|
||||||
|
|
||||||
def set_book_on_device_func(self, func):
|
def set_book_on_device_func(self, func):
|
||||||
self.book_on_device_func = func
|
self.book_on_device_func = func
|
||||||
@ -1125,7 +1125,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if not authors:
|
if not authors:
|
||||||
authors = [_('Unknown')]
|
authors = [_('Unknown')]
|
||||||
self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,))
|
self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,))
|
||||||
self.conn.execute('DELETE FROM authors WHERE (SELECT COUNT(id) FROM books_authors_link WHERE author=authors.id) < 1')
|
|
||||||
for a in authors:
|
for a in authors:
|
||||||
if not a:
|
if not a:
|
||||||
continue
|
continue
|
||||||
@ -2151,8 +2150,6 @@ books_series_link feeds
|
|||||||
os.remove(self.dbpath)
|
os.remove(self.dbpath)
|
||||||
shutil.copyfile(dest, self.dbpath)
|
shutil.copyfile(dest, self.dbpath)
|
||||||
self.connect()
|
self.connect()
|
||||||
self.field_metadata.remove_dynamic_categories()
|
|
||||||
self.field_metadata.remove_custom_fields()
|
|
||||||
self.initialize_dynamic()
|
self.initialize_dynamic()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
|
@ -306,7 +306,7 @@ class FieldMetadata(dict):
|
|||||||
self._tb_cats[k]['label'] = k
|
self._tb_cats[k]['label'] = k
|
||||||
self._tb_cats[k]['display'] = {}
|
self._tb_cats[k]['display'] = {}
|
||||||
self._tb_cats[k]['is_editable'] = True
|
self._tb_cats[k]['is_editable'] = True
|
||||||
self._add_search_terms_to_map(k, self._tb_cats[k]['search_terms'])
|
self._add_search_terms_to_map(k, v['search_terms'])
|
||||||
self.custom_field_prefix = '#'
|
self.custom_field_prefix = '#'
|
||||||
self.get = self._tb_cats.get
|
self.get = self._tb_cats.get
|
||||||
|
|
||||||
@ -371,6 +371,12 @@ class FieldMetadata(dict):
|
|||||||
def get_custom_fields(self):
|
def get_custom_fields(self):
|
||||||
return [l for l in self._tb_cats if self._tb_cats[l]['is_custom']]
|
return [l for l in self._tb_cats if self._tb_cats[l]['is_custom']]
|
||||||
|
|
||||||
|
def all_metadata(self):
|
||||||
|
l = {}
|
||||||
|
for k in self._tb_cats:
|
||||||
|
l[k] = self._tb_cats[k]
|
||||||
|
return l
|
||||||
|
|
||||||
def get_custom_field_metadata(self):
|
def get_custom_field_metadata(self):
|
||||||
l = {}
|
l = {}
|
||||||
for k in self._tb_cats:
|
for k in self._tb_cats:
|
||||||
@ -408,10 +414,6 @@ class FieldMetadata(dict):
|
|||||||
self._add_search_terms_to_map(key, [key])
|
self._add_search_terms_to_map(key, [key])
|
||||||
self.custom_label_to_key_map[label+'_index'] = key
|
self.custom_label_to_key_map[label+'_index'] = key
|
||||||
|
|
||||||
def remove_custom_fields(self):
|
|
||||||
for key in self.get_custom_fields():
|
|
||||||
del self._tb_cats[key]
|
|
||||||
|
|
||||||
def remove_dynamic_categories(self):
|
def remove_dynamic_categories(self):
|
||||||
for key in list(self._tb_cats.keys()):
|
for key in list(self._tb_cats.keys()):
|
||||||
val = self._tb_cats[key]
|
val = self._tb_cats[key]
|
||||||
|
@ -61,7 +61,7 @@ def config(defaults=None):
|
|||||||
'actual e-book file(s).'))
|
'actual e-book file(s).'))
|
||||||
x('formats', default='all',
|
x('formats', default='all',
|
||||||
help=_('Comma separated list of formats to save for each book.'
|
help=_('Comma separated list of formats to save for each book.'
|
||||||
' By default all available books are saved.'))
|
' By default all available formats are saved.'))
|
||||||
x('template', default=DEFAULT_TEMPLATE,
|
x('template', default=DEFAULT_TEMPLATE,
|
||||||
help=_('The template to control the filename and directory structure of the saved files. '
|
help=_('The template to control the filename and directory structure of the saved files. '
|
||||||
'Default is "%s" which will save books into a per-author '
|
'Default is "%s" which will save books into a per-author '
|
||||||
|
@ -28,7 +28,7 @@ Command Line Interface
|
|||||||
|
|
||||||
.. image:: ../images/cli.png
|
.. image:: ../images/cli.png
|
||||||
|
|
||||||
On OS X you have to go to Preferences->Advanced and click install command line
|
On OS X you have to go to Preferences->Advanced->Miscellaneous and click install command line
|
||||||
tools to make the command line tools available. On other platforms, just start
|
tools to make the command line tools available. On other platforms, just start
|
||||||
a terminal and type the command.
|
a terminal and type the command.
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ Customizing |app|
|
|||||||
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
|
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
|
||||||
first, how to use environment variables and *tweaks* to customize |app|'s behavior, and then how to
|
first, how to use environment variables and *tweaks* to customize |app|'s behavior, and then how to
|
||||||
specify your own static resources like icons and templates to override the defaults and finally how to
|
specify your own static resources like icons and templates to override the defaults and finally how to
|
||||||
use *plugins* to add funtionality to |app|.
|
use *plugins* to add functionality to |app|.
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
:depth: 2
|
:depth: 2
|
||||||
@ -45,7 +45,7 @@ All static resources are stored in the resources sub-folder of the calibre insta
|
|||||||
from the calibre website it will be :file:`/opt/calibre/resources`. These paths can change depending on where you choose to install |app|.
|
from the calibre website it will be :file:`/opt/calibre/resources`. These paths can change depending on where you choose to install |app|.
|
||||||
|
|
||||||
You should not change the files in this resources folder, as your changes will get overwritten the next time you update |app|. Instead, go to
|
You should not change the files in this resources folder, as your changes will get overwritten the next time you update |app|. Instead, go to
|
||||||
:guilabel:`Preferences->Advanced` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. Place the files in the appropriate sub folders, for example place images in :file:`resources/images`, etc.
|
:guilabel:`Preferences->Advanced->Miscellaneous` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. Place the files in the appropriate sub folders, for example place images in :file:`resources/images`, etc.
|
||||||
|app| will automatically use your custom file in preference to the builtin one the next time it is started.
|
|app| will automatically use your custom file in preference to the builtin one the next time it is started.
|
||||||
|
|
||||||
For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is
|
For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is
|
||||||
|
@ -123,7 +123,7 @@ the previously checked out calibre code directory, for example::
|
|||||||
|
|
||||||
cd /Users/kovid/work/calibre
|
cd /Users/kovid/work/calibre
|
||||||
|
|
||||||
calibre is the directory that contains the src and resources sub directories. Ensure you have installed the |app| commandline tools via Preferences->Advanced in the |app| GUI.
|
calibre is the directory that contains the src and resources sub directories. Ensure you have installed the |app| commandline tools via :guilabel:Preferences->Advanced->Miscellaneous in the |app| GUI.
|
||||||
|
|
||||||
The next step is to set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path to the src directory.
|
The next step is to set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path to the src directory.
|
||||||
So, following the example above, it would be ``/Users/kovid/work/calibre/src``. Apple
|
So, following the example above, it would be ``/Users/kovid/work/calibre/src``. Apple
|
||||||
|
@ -62,7 +62,7 @@ How do I convert my file containing non-English characters, or smart quotes?
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
There are two aspects to this problem:
|
There are two aspects to this problem:
|
||||||
1. Knowing the encoding of the source file: |app| tries to guess what character encoding your source files use, but often, this is impossible, so you need to tell it what encoding to use. This can be done in the GUI via the :guilabel:`Input character encoding` field in the :guilabel:`Look & Feel` section. The command-line tools all have an :option:`--input-encoding` option.
|
1. Knowing the encoding of the source file: |app| tries to guess what character encoding your source files use, but often, this is impossible, so you need to tell it what encoding to use. This can be done in the GUI via the :guilabel:`Input character encoding` field in the :guilabel:`Look & Feel` section. The command-line tools all have an :option:`--input-encoding` option.
|
||||||
2. When adding HTML files to |app|, you may need to tell |app| what encoding the files are in. To do this go to Preferences->Plugins->File Type plugins and customize the HTML2Zip plugin, telling it what encoding your HTML files are in. Now when you add HTML files to |app| they will be correctly processed. HTML files from different sources often have different encodings, so you may have to change this setting repeatedly. A common encoding for many files from the web is ``cp1252`` and I would suggest you try that first. Note that when converting HTML files, leave the input encoding setting mentioned above blank. This is because the HTML2ZIP plugin automatically converts the HTML files to a standard encoding (utf-8).
|
2. When adding HTML files to |app|, you may need to tell |app| what encoding the files are in. To do this go to :guilabel:`Preferences->Advanced->Plugins->File Type plugins` and customize the HTML2Zip plugin, telling it what encoding your HTML files are in. Now when you add HTML files to |app| they will be correctly processed. HTML files from different sources often have different encodings, so you may have to change this setting repeatedly. A common encoding for many files from the web is ``cp1252`` and I would suggest you try that first. Note that when converting HTML files, leave the input encoding setting mentioned above blank. This is because the HTML2ZIP plugin automatically converts the HTML files to a standard encoding (utf-8).
|
||||||
3. Embedding fonts: If you are generating an LRF file to read on your SONY Reader, you are limited by the fact that the Reader only supports a few non-English characters in the fonts it comes pre-loaded with. You can work around this problem by embedding a unicode-aware font that supports the character set your file uses into the LRF file. You should embed atleast a serif and a sans-serif font. Be aware that embedding fonts significantly slows down page-turn speed on the reader.
|
3. Embedding fonts: If you are generating an LRF file to read on your SONY Reader, you are limited by the fact that the Reader only supports a few non-English characters in the fonts it comes pre-loaded with. You can work around this problem by embedding a unicode-aware font that supports the character set your file uses into the LRF file. You should embed atleast a serif and a sans-serif font. Be aware that embedding fonts significantly slows down page-turn speed on the reader.
|
||||||
|
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ We just need some information from you:
|
|||||||
* What e-book formats does your device support?
|
* What e-book formats does your device support?
|
||||||
* Is there a special directory on the device in which all e-book files should be placed?
|
* Is there a special directory on the device in which all e-book files should be placed?
|
||||||
* We also need information about your device that |app| will collect automatically. First, if your
|
* We also need information about your device that |app| will collect automatically. First, if your
|
||||||
device supports SD cards, insert them. Then connect your device. In calibre go to Preferences->Advanced
|
device supports SD cards, insert them. Then connect your device. In calibre go to :guilabel:`Preferences->Advanced->Miscellaneous`
|
||||||
and click the "Debug device detection" button. This will create some debug output. Copy it to a file
|
and click the "Debug device detection" button. This will create some debug output. Copy it to a file
|
||||||
and repeat the process, this time with your device disconnected.
|
and repeat the process, this time with your device disconnected.
|
||||||
* Send both the above outputs to us with the other information and we will write a device driver for your
|
* Send both the above outputs to us with the other information and we will write a device driver for your
|
||||||
@ -109,11 +109,11 @@ of which books are members are shown on the device view.
|
|||||||
|
|
||||||
When you send a book to the reader, |app| will add the book to collections based on the metadata for that book. By
|
When you send a book to the reader, |app| will add the book to collections based on the metadata for that book. By
|
||||||
default, collections are created from tags and series. You can control what metadata is used by going to
|
default, collections are created from tags and series. You can control what metadata is used by going to
|
||||||
Preferences->Plugins->Device Interface plugins and customizing the SONY device interface plugin. If you remove all
|
:guilabel:`Preferences->Advanced->Plugins->Device Interface plugins` and customizing the SONY device interface plugin. If you remove all
|
||||||
values, |app| will not add the book to any collection.
|
values, |app| will not add the book to any collection.
|
||||||
|
|
||||||
Collection management is largely controlled by the 'Metadata management' option found at
|
Collection management is largely controlled by the 'Metadata management' option found at
|
||||||
Preferences->Add/Save->Sending to device. If set to 'Manual' (the default), managing collections is left to
|
:guilabel:`Preferences->Import/Export->Sending books to devices`. If set to 'Manual' (the default), managing collections is left to
|
||||||
the user; |app| will not delete already existing collections for a book on your reader when you resend the
|
the user; |app| will not delete already existing collections for a book on your reader when you resend the
|
||||||
book to the reader, but |app| will add the book to collections if necessary. To ensure that the collections
|
book to the reader, but |app| will add the book to collections if necessary. To ensure that the collections
|
||||||
for a book are based only on current |app| metadata, first delete the books from the reader, then resend the
|
for a book are based only on current |app| metadata, first delete the books from the reader, then resend the
|
||||||
@ -185,8 +185,8 @@ The easiest way to browse your |app| collection on your Apple device (iPad/iPhon
|
|||||||
|
|
||||||
First perform the following steps in |app|
|
First perform the following steps in |app|
|
||||||
|
|
||||||
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under Preferences->General)
|
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under :guilabel:`Preferences->Interface->Behavior`)
|
||||||
* Set the output profile to iPad (this will work for iPhone/iPods as well), under Preferences->Conversion->Page Setup
|
* Set the output profile to iPad (this will work for iPhone/iPods as well), under :guilabel:`Preferences->Conversion->Common Options->Page Setup`
|
||||||
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
||||||
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ Can I access my |app| books using the web browser in my Kindle or other reading
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|app| has a *Content Server* that exports the books in |app| as a web page. You can turn it on under
|
|app| has a *Content Server* that exports the books in |app| as a web page. You can turn it on under
|
||||||
Preferences->Content Server. Then just point the web browser on your device to the computer running
|
:guilabel:`Preferences->Network->Sharing over the net`. Then just point the web browser on your device to the computer running
|
||||||
the Content Server and you will be able to browse your book collection. For example, if the computer running
|
the Content Server and you will be able to browse your book collection. For example, if the computer running
|
||||||
the server has IP address 63.45.128.5, in the browser, you would type::
|
the server has IP address 63.45.128.5, in the browser, you would type::
|
||||||
|
|
||||||
@ -277,14 +277,14 @@ In |app|, you would instead use tags to mark genre and read status and then just
|
|||||||
|
|
||||||
Why doesn't |app| have a column for foo?
|
Why doesn't |app| have a column for foo?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|app| is designed to have columns for the most frequently and widely used fields. In addition, you can add any columns you like. Columns can be added via Preferences->Interface.
|
|app| is designed to have columns for the most frequently and widely used fields. In addition, you can add any columns you like. Columns can be added via :guilabel:`Preferences->Interface->Add your own columns`.
|
||||||
Watch the tutorial `UI Power tips <http://calibre-ebook.com/demo#tutorials>`_ to learn how to create your own columns.
|
Watch the tutorial `UI Power tips <http://calibre-ebook.com/demo#tutorials>`_ to learn how to create your own columns.
|
||||||
|
|
||||||
How do I move my |app| library from one computer to another?
|
How do I move my |app| library from one computer to another?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
|
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
|
||||||
|
|
||||||
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to Preferences->Advanced and click the Check database integrity button. It will warn you about missing files, if any, which you should then transfer by hand.
|
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to :guilabel:`Preferences->Advanced->Miscellaneous` and click the "Check database integrity button". It will warn you about missing files, if any, which you should then transfer by hand.
|
||||||
|
|
||||||
|
|
||||||
Content From The Web
|
Content From The Web
|
||||||
|
@ -345,6 +345,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||||||
- Show book details
|
- Show book details
|
||||||
* - :kbd:`M`
|
* - :kbd:`M`
|
||||||
- Merge selected records
|
- Merge selected records
|
||||||
|
* - :kbd:`Alt+M`
|
||||||
|
- Merge selected records, keeping originals
|
||||||
* - :kbd:`O`
|
* - :kbd:`O`
|
||||||
- Open containing folder
|
- Open containing folder
|
||||||
* - :kbd:`S`
|
* - :kbd:`S`
|
||||||
|