Compare commits
109 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d2d7dc2c8f | ||
|
f622b0c23e | ||
|
f312714a2c | ||
|
c3f26ca225 | ||
|
f6c7307647 | ||
|
b1714b9674 | ||
|
fa030e3060 | ||
|
564db06179 | ||
|
3cd75807f9 | ||
|
f6f9d01060 | ||
|
c6e30b67ad | ||
|
e4be239bf0 | ||
|
1e004774c9 | ||
|
91ca1e2190 | ||
|
d1662b47d9 | ||
|
dfe0fceea9 | ||
|
268014cc3a | ||
|
d1b1d7c59c | ||
|
470fc1078f | ||
|
96f4d9193a | ||
|
7aa42603bd | ||
|
9a6392d1e6 | ||
|
739b84e9f4 | ||
|
66746b4eaa | ||
|
164450a888 | ||
|
191ea7ef3a | ||
|
ca8ddd38a2 | ||
|
a45e183914 | ||
|
506c458544 | ||
|
5e3987dc04 | ||
|
5b8bc1d707 | ||
|
b3e4060661 | ||
|
2185e9fcf7 | ||
|
5d35319164 | ||
|
c6d24fd970 | ||
|
17f39c557b | ||
|
f997bc9c9a | ||
|
930a97a8fa | ||
|
026fff3d7a | ||
|
f9eec130dd | ||
|
d75689ea97 | ||
|
6ea5635d28 | ||
|
4b9221128c | ||
|
b5349315be | ||
|
ee18f157f1 | ||
|
ae325736d5 | ||
|
826da72d4b | ||
|
b5231cd383 | ||
|
5ac843e48c | ||
|
74417319be | ||
|
f8555a6ed5 | ||
|
4346c27adc | ||
|
16adf57dae | ||
|
8534088f4a | ||
|
a81ed537a6 | ||
|
ed5bc0b2b9 | ||
|
8cb8904e58 | ||
|
aa093b8cc2 | ||
|
42b4d0317d | ||
|
a0e463bc58 | ||
|
4a2da61b51 | ||
|
5c481ccafe | ||
|
30760b879e | ||
|
af7868f62a | ||
|
62c44730d8 | ||
|
045d8a3e52 | ||
|
55bee210c6 | ||
|
d3746c8e98 | ||
|
ffcf07414f | ||
|
ffaace2eaa | ||
|
32455dd859 | ||
|
3e54340694 | ||
|
bc6c7d1c36 | ||
|
ebd746b7b2 | ||
|
ebc3ef9f1b | ||
|
7238605441 | ||
|
ea86737970 | ||
|
ab4c586c06 | ||
|
7977bde410 | ||
|
626472dd04 | ||
|
d9efe2da3c | ||
|
ec197f0829 | ||
|
335479bcfb | ||
|
cbf01c6d16 | ||
|
98ca118a60 | ||
|
c7aa0e28ee | ||
|
528c2e387c | ||
|
bc54d6b686 | ||
|
8f298de58e | ||
|
366859ebe0 | ||
|
8c51cc047e | ||
|
5081a31f1a | ||
|
aff69d95c1 | ||
|
0b8427c864 | ||
|
43dd6a34d9 | ||
|
2f4adfc183 | ||
|
0d015ad72e | ||
|
406ca0485f | ||
|
ab6760226e | ||
|
66c8626d43 | ||
|
5fa724ae69 | ||
|
d417a94829 | ||
|
9c85a1075e | ||
|
dd4b502861 | ||
|
0f963b20f9 | ||
|
f63b6627b2 | ||
|
00db5d5e0f | ||
|
5e53d40e68 | ||
|
6ffa6934ed |
28
Lector.pro
@@ -1,5 +1,5 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2018 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,19 +15,23 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SOURCES += lector/__main__.py \
|
||||
lector/annotations.py \
|
||||
lector/contentwidgets.py \
|
||||
lector/definitionsdialog.py \
|
||||
lector/dockwidgets.py \
|
||||
lector/library.py \
|
||||
lector/metadatadialog.py \
|
||||
lector/models.py \
|
||||
lector/widgets.py \
|
||||
lector/library.py \
|
||||
lector/toolbars.py \
|
||||
lector/settingsdialog.py \
|
||||
resources/definitions.py \
|
||||
resources/settingswindow.py \
|
||||
resources/metadata.py \
|
||||
resources/mainwindow.py
|
||||
lector/toolbars.py \
|
||||
lector/widgets.py \
|
||||
lector/resources/definitions.py \
|
||||
lector/resources/settingswindow.py \
|
||||
lector/resources/metadata.py \
|
||||
lector/resources/mainwindow.py
|
||||
|
||||
TRANSLATIONS += resources/translations/Lector_es.ts \
|
||||
resources/translations/Lector_fr.ts \
|
||||
resources/translations/Lector_de.ts \
|
||||
resources/translations/SAMPLE.ts
|
||||
TRANSLATIONS += lector/resources/translations/Lector_es.ts \
|
||||
lector/resources/translations/Lector_fr.ts \
|
||||
lector/resources/translations/Lector_de.ts \
|
||||
lector/resources/translations/Lector_zh.ts \
|
||||
lector/resources/translations/SAMPLE.ts
|
||||
|
67
README.md
@@ -1,27 +1,42 @@
|
||||
# Lector
|
||||
<p align="center"><img src="lector/resources/raw/logo/logotype_horizontal.png" alt="Lector" height="90px"></p>
|
||||
|
||||
Qt based ebook reader
|
||||
|
||||
Currently supports:
|
||||
* pdf
|
||||
* epub
|
||||
* fb2
|
||||
* mobi
|
||||
* azw / azw3 / azw4
|
||||
* cbr / cbz
|
||||
|
||||
Support for a bunch of other formats is coming. Please see the TODO for additional information.
|
||||
## Contribute
|
||||
[Paypal](https://www.paypal.me/supportlector)
|
||||
|
||||
Bitcoin: 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro
|
||||
|
||||
## Requirements
|
||||
### Needed
|
||||
| Package | Version tested |
|
||||
| --- | --- |
|
||||
| Qt5 | 5.10.1 |
|
||||
| Python | 3.6 |
|
||||
| PyQt5 | 5.10.1 |
|
||||
| python-requests | 2.18.4 |
|
||||
| python-lxml | 4.3.0 |
|
||||
| python-beautifulsoup4 | 4.6.0 |
|
||||
| poppler-qt5 | 0.61.1 |
|
||||
| python-poppler-qt5 | 0.24.2 |
|
||||
| python-xmltodict | 0.11.0 |
|
||||
|
||||
poppler-qt5 and python-poppler-qt5 are optional.
|
||||
### Optional
|
||||
| Package | Version tested |
|
||||
| --- | --- |
|
||||
| python-pymupdf | 1.14.5 |
|
||||
|
||||
## Support
|
||||
When reporting issues:
|
||||
* Make sure you're at the latest commit.
|
||||
* Run with `$EXECUTABLEPATH debug`.
|
||||
* Include the log `~/.local/share/Lector/Lector.log` AND terminal output.
|
||||
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
|
||||
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
|
||||
|
||||
## Installation
|
||||
### Manual
|
||||
@@ -34,49 +49,55 @@ poppler-qt5 and python-poppler-qt5 are optional.
|
||||
3. OR launch with `lector/__main__.py`
|
||||
|
||||
### Available packages
|
||||
* [AUR](https://aur.archlinux.org/packages/lector-git/)
|
||||
* [AUR - Releases](https://aur.archlinux.org/packages/lector/)
|
||||
* [AUR - Git](https://aur.archlinux.org/packages/lector-git/)
|
||||
* [Gentoo (unofficial)](https://bitbucket.org/szymonsz/gen2-overlay/src/master/app-text/lector/)
|
||||
* [Fedora (unofficial)](https://copr.fedorainfracloud.org/coprs/bugzy/lector/)
|
||||
* [openSUSE](https://software.opensuse.org/package/lector)
|
||||
|
||||
## Translations
|
||||
1. There is a `SAMPLE.ts` file in `resources/translations`. Open it in `Qt Linguist`.
|
||||
1. There is a `SAMPLE.ts` file [here](https://github.com/BasioMeusPuga/Lector/tree/master/lector/resources/translations). Open it in `Qt Linguist`.
|
||||
2. Pick the language you wish to translate to.
|
||||
3. Translate relevant strings.
|
||||
4. Try to resist the urge to include profanity.
|
||||
5. Save the file as `Lector_<language>` and send it to me, preferably as a pull request.
|
||||
|
||||
Oh, please keep the translations short. There's only so much space for UI elements.
|
||||
Please keep the translations short. There's only so much space for UI elements.
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Main window
|
||||

|
||||

|
||||
|
||||
### Table view
|
||||

|
||||

|
||||
|
||||
### Book reading view
|
||||

|
||||

|
||||
|
||||
### Distraction free view
|
||||

|
||||
|
||||
### Annotation support
|
||||

|
||||
|
||||
### Comic reading view
|
||||

|
||||

|
||||
|
||||
### Bookmark support
|
||||

|
||||

|
||||
|
||||
### View profiles
|
||||

|
||||

|
||||
|
||||
### Metadata editor
|
||||

|
||||

|
||||
|
||||
### In program dictionary
|
||||

|
||||

|
||||
|
||||
## Reporting issues
|
||||
When reporting issues:
|
||||
|
||||
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
|
||||
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
|
||||
### Settings window
|
||||

|
||||
|
||||
## Attributions
|
||||
* [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack)
|
||||
|
66
TODO
@@ -1,8 +1,10 @@
|
||||
TODO
|
||||
General:
|
||||
✓ Internationalization
|
||||
Application icon
|
||||
.desktop file
|
||||
✓ Application icon
|
||||
✓ .desktop file
|
||||
✓ Shift to logging instead of print statements
|
||||
Flatpak and AppImage support
|
||||
Options:
|
||||
✓ Automatic library management
|
||||
✓ Recursive file addition
|
||||
@@ -29,9 +31,14 @@ TODO
|
||||
✓ Information dialog widget
|
||||
✓ Allow editing of database data through the UI + for Bookmarks
|
||||
✓ Include (action) icons with the applications
|
||||
✓ Drag and drop support for the library
|
||||
✓ Tab reordering
|
||||
Additional Settings:
|
||||
✓ Create covers for books without them - VERY SLOW
|
||||
Set focus to newly added file
|
||||
Reading:
|
||||
✓ Drop down for TOC
|
||||
✓ Treeview navigation for TOC
|
||||
✓ Override the keypress event of the textedit
|
||||
✓ Use format* icons for toolbar buttons
|
||||
✓ Implement book view settings with a(nother) toolbar
|
||||
@@ -57,43 +64,74 @@ TODO
|
||||
✓ Paragraph indentation
|
||||
✓ Comic view keyboard shortcuts
|
||||
✓ Comic view context menu
|
||||
✓ Make the bookmark dock float over the reading area
|
||||
✓ Spacebar should not cut off lines at the top
|
||||
✓ Track open bookmark windows so they can be closed quickly at exit
|
||||
✓ Search document using QTextCursor
|
||||
✓ Double page / column view
|
||||
✓ For comics
|
||||
Caching is currently non functional
|
||||
Annotations
|
||||
✓ Text
|
||||
✓ Disable buttons for annotations, search in images
|
||||
Adjust key navigation according to viewport dimensions
|
||||
Search document using QTextCursor?
|
||||
Filetypes:
|
||||
✓ pdf support
|
||||
Parse TOC
|
||||
✓ Parse TOC
|
||||
✓ epub support
|
||||
✓ Homegrown solution please
|
||||
✓ cbz, cbr support
|
||||
✓ Keep font settings enabled but only for background color
|
||||
✓ Double page view
|
||||
✓ Manga mode
|
||||
✓ mobi, azw support
|
||||
Limit the extra files produced by KindleUnpack
|
||||
Have them save to memory
|
||||
✓ fb2 support
|
||||
✓ Images need to show up in their placeholders
|
||||
Other:
|
||||
✓ Define every widget in code
|
||||
Bugs:
|
||||
Slider position change might be acting up
|
||||
Deselecting all directories in the settings dialog also filters out manually added books
|
||||
Last line in QTextBrowser should never be cut off
|
||||
Bookmark name for a page that's not on the TOC and has nothing before
|
||||
Screen position still keeps jumping when inside a paragraph
|
||||
Better recursion needed for fb2 toc
|
||||
Search results should ignore punctuation
|
||||
Keep text size for annotations
|
||||
Sort by new is not working
|
||||
|
||||
Secondary:
|
||||
Annotations
|
||||
Text to speech
|
||||
Definitions dialog needs to respond to escape
|
||||
Zoom slider for comics
|
||||
Tab tooltip
|
||||
Additional Settings:
|
||||
Find definitions on Google
|
||||
Disable progressbar - 20% book addition speed improvement
|
||||
Disable cover loading when reading - Saves ~2M / book
|
||||
Special formatting for each chapter's title
|
||||
Signal end of chapter with some text
|
||||
Graphical themes
|
||||
Change focus rectangle dimensions
|
||||
Tab reordering
|
||||
Universal Ctrl + Tab
|
||||
Allow tabs to detach and form their own windows
|
||||
Goodreads API: Ratings, Read, Recommendations
|
||||
Get ISBN using python-isbnlib
|
||||
Pagination
|
||||
Use embedded fonts + CSS
|
||||
Scrolling: Smooth / By Line
|
||||
Spacebar should not cut off lines at the top
|
||||
Shift to logging instead of print statements
|
||||
txt, doc, chm, djvu, fb2 support
|
||||
txt, doc, chm, djvu support
|
||||
Include icons for filetype emblems
|
||||
Drag and drop support for the library
|
||||
Comic view modes
|
||||
Continuous paging
|
||||
Double pages
|
||||
Ignore a / the / numbers for sorting purposes
|
||||
? Add only one file type if multiple are present
|
||||
? Create emblem per filetype
|
||||
In application notifications
|
||||
Notification in case the filter is filtering out all files with no option in place
|
||||
Option to fit images to viewport
|
||||
|
||||
Need help with:
|
||||
Double page view for books
|
||||
Scrolling: Smooth / By Line
|
||||
Annotation preview in listView
|
||||
Pagination
|
||||
|
143
com.basiomeuspuga.Lector.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"app-id":"com.basiomeuspuga.Lector",
|
||||
"runtime":"org.kde.Platform",
|
||||
"runtime-version":"5.12",
|
||||
"sdk":"org.kde.Sdk",
|
||||
"command":"lector",
|
||||
"rename-icon":"Lector",
|
||||
"rename-desktop-file":"lector.desktop",
|
||||
"rename-appdata-file":"lector.appdata.xml",
|
||||
"finish-args":[
|
||||
"--filesystem=host",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
"--share=ipc",
|
||||
"--share=network"
|
||||
],
|
||||
"build-options":{
|
||||
"cflags":"-O2",
|
||||
"cxxflags":"-O2"
|
||||
},
|
||||
"modules":[
|
||||
{
|
||||
"name": "PyQt5",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --prefix=/app PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
|
||||
],
|
||||
"modules":[
|
||||
{
|
||||
"name":"PyQt5-sip",
|
||||
"sources":[
|
||||
{
|
||||
"type":"file",
|
||||
"url":"https://files.pythonhosted.org/packages/ae/9c/74fba0b62a0756d214f9aded5b0184130f7866def7532fa68823f34feefa/PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl",
|
||||
"sha256":"04bd0bb8b6f8fa03c2dfbdfff0c8c9bfb3f46a21dd4cac73983dae93bf949523"
|
||||
}
|
||||
],
|
||||
"buildsystem":"simple",
|
||||
"build-commands":[
|
||||
"pip3 install --prefix=/app PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/5e/91/9ac8827d0af428e756f461a3aa7bcbc53d6450edfe026e27569f5ff3689e/PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl",
|
||||
"sha256": "fd5946795b39922f971cf92dec799aadc7544b7fa993a79b9f6059f13d817e6e"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"beautifulsoup4",
|
||||
"buildsystem":"simple",
|
||||
"sources":[
|
||||
{
|
||||
"type":"file",
|
||||
"url":"https://files.pythonhosted.org/packages/1d/5d/3260694a59df0ec52f8b4883f5d23b130bc237602a1411fa670eae12351e/beautifulsoup4-4.7.1-py3-none-any.whl",
|
||||
"sha256":"034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858"
|
||||
}
|
||||
],
|
||||
"modules":[
|
||||
{
|
||||
"name": "soupsieve",
|
||||
"sources":[
|
||||
{
|
||||
"type":"file",
|
||||
"url":"https://files.pythonhosted.org/packages/bf/b3/2473abf05c4950c6a829ed5dcbc40d8b56d4351d15d6939c8ffb7c6b1a14/soupsieve-1.7.3-py2.py3-none-any.whl",
|
||||
"sha256":"466910df7561796a60748826781ebe9a888f7a1668a636ae86783f44d10aae73"
|
||||
}
|
||||
],
|
||||
"buildsystem":"simple",
|
||||
"build-commands":[
|
||||
"pip3 install --prefix=/app soupsieve-1.7.3-py2.py3-none-any.whl"
|
||||
]
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"pip3 install --prefix=/app beautifulsoup4-4.7.1-py3-none-any.whl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"xmltodict",
|
||||
"buildsystem":"simple",
|
||||
"sources":[
|
||||
{
|
||||
"type": "file",
|
||||
"url":"https://files.pythonhosted.org/packages/28/fd/30d5c1d3ac29ce229f6bdc40bbc20b28f716e8b363140c26eff19122d8a5/xmltodict-0.12.0-py2.py3-none-any.whl",
|
||||
"sha256":"8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"pip3 install --prefix=/app xmltodict-0.12.0-py2.py3-none-any.whl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"PyMuPDF",
|
||||
"buildsystem":"simple",
|
||||
"build-commands": [
|
||||
"pip3 install --prefix=/app PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl"
|
||||
],
|
||||
"sources":[
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/3c/df/4bfaee2631b505d502c2ba64aa437799f0a64125edb1d4c4c38044ad1ecc/PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl",
|
||||
"sha256": "a49798b58cce00e09b8a4431a5f64a400b11a0959f29507187c471208ce040a5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"lxml",
|
||||
"buildsystem":"simple",
|
||||
"sources":[
|
||||
{
|
||||
"type":"file",
|
||||
"url":"https://files.pythonhosted.org/packages/08/f2/04bf04e42c070f65b64dbde02d2c94851251f19f5e9f803cc8f8bc61ac77/lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl",
|
||||
"sha256":"c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a"
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"pip3 install --prefix=/app lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"lector",
|
||||
"buildsystem":"simple",
|
||||
"ensure-writable":[
|
||||
"/lib/python*/site-packages/easy-install.pth"
|
||||
],
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"https://github.com/BasioMeusPuga/Lector.git"
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"python3 setup.py build",
|
||||
"python3 setup.py install --prefix=/app"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -6,7 +6,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi
|
||||
|
||||
import os
|
||||
|
||||
__path__ = ["lib", os.path.dirname(__file__), "kindleunpack"]
|
||||
__path__ = ["lib", os.path.dirname(os.path.realpath(__file__)), "kindleunpack"]
|
||||
|
||||
import sys
|
||||
import codecs
|
||||
@@ -140,6 +140,8 @@ if PY2:
|
||||
# 0.76 pre-release version only fix name related issues in opf by not using original file name in mobi7
|
||||
# 0.77 bug fix for unpacking HDImages with included Fonts
|
||||
# 0.80 converted to work with both python 2.7 and Python 3.3 and later
|
||||
# 0.81 various fixes
|
||||
# 0.82 Handle calibre-generated mobis that can have skeletons with no fragments
|
||||
|
||||
DUMP = False
|
||||
""" Set to True to dump all possible information. """
|
||||
@@ -847,7 +849,7 @@ def process_all_mobi_headers(files, apnxfile, sect, mhlst, K8Boundary, k8only=Fa
|
||||
return
|
||||
|
||||
|
||||
def unpackBook(infile, outdir, apnxfile=None, epubver='2', use_hd=True, dodump=False, dowriteraw=False, dosplitcombos=False):
|
||||
def unpackBook(infile, outdir, apnxfile=None, epubver='2', use_hd=False, dodump=False, dowriteraw=False, dosplitcombos=False):
|
||||
global DUMP
|
||||
global WRITE_RAW_DATA
|
||||
global SPLIT_COMBO_MOBIS
|
||||
@@ -949,7 +951,7 @@ def main(argv=unicode_argv()):
|
||||
global WRITE_RAW_DATA
|
||||
global SPLIT_COMBO_MOBIS
|
||||
|
||||
print("KindleUnpack v0.80")
|
||||
print("KindleUnpack v0.82")
|
||||
print(" Based on initial mobipocket version Copyright © 2009 Charles M. Hannum <root@ihack.net>")
|
||||
print(" Extensive Extensions and Improvements Copyright © 2009-2014 ")
|
||||
print(" by: P. Durrant, K. Hendricks, S. Siebert, fandrieu, DiapDealer, nickredding, tkeo.")
|
||||
|
@@ -180,9 +180,11 @@ class K8Processor:
|
||||
fragptr = 0
|
||||
baseptr = 0
|
||||
cnt = 0
|
||||
filename = 'part%04d.xhtml' % cnt
|
||||
for [skelnum, skelname, fragcnt, skelpos, skellen] in self.skeltbl:
|
||||
baseptr = skelpos + skellen
|
||||
skeleton = text[skelpos: baseptr]
|
||||
aidtext = "0"
|
||||
for i in range(fragcnt):
|
||||
[insertpos, idtext, filenum, seqnum, startpos, length] = self.fragtbl[fragptr]
|
||||
aidtext = idtext[12:-2]
|
||||
|
@@ -1,525 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
|
||||
# this program works in concert with the output from KindleUnpack
|
||||
|
||||
'''
|
||||
Convert from Mobi ML to XHTML
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
SPECIAL_HANDLING_TAGS = {
|
||||
'?xml' : ('xmlheader', -1),
|
||||
'!--' : ('comment', -3),
|
||||
'!DOCTYPE' : ('doctype', -1),
|
||||
}
|
||||
|
||||
SPECIAL_HANDLING_TYPES = ['xmlheader', 'doctype', 'comment']
|
||||
|
||||
SELF_CLOSING_TAGS = ['br' , 'hr', 'input', 'img', 'image', 'meta', 'spacer', 'link', 'frame', 'base', 'col', 'reference']
|
||||
|
||||
class MobiMLConverter(object):
|
||||
|
||||
PAGE_BREAK_PAT = re.compile(r'(<[/]{0,1}mbp:pagebreak\s*[/]{0,1}>)+', re.IGNORECASE)
|
||||
IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex')
|
||||
|
||||
def __init__(self, filename):
|
||||
self.base_css_rules = 'blockquote { margin: 0em 0em 0em 1.25em }\n'
|
||||
self.base_css_rules += 'p { margin: 0em }\n'
|
||||
self.base_css_rules += '.bold { font-weight: bold }\n'
|
||||
self.base_css_rules += '.italic { font-style: italic }\n'
|
||||
self.base_css_rules += '.mbp_pagebreak { page-break-after: always; margin: 0; display: block }\n'
|
||||
self.tag_css_rules = {}
|
||||
self.tag_css_rule_cnt = 0
|
||||
self.path = []
|
||||
self.filename = filename
|
||||
self.wipml = open(self.filename, 'rb').read()
|
||||
self.pos = 0
|
||||
self.opfname = self.filename.rsplit('.',1)[0] + '.opf'
|
||||
self.opos = 0
|
||||
self.meta = ''
|
||||
self.cssname = os.path.join(os.path.dirname(self.filename),'styles.css')
|
||||
self.current_font_size = 3
|
||||
self.font_history = []
|
||||
|
||||
def cleanup_html(self):
|
||||
self.wipml = re.sub(r'<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.wipml)
|
||||
self.wipml = self.wipml.replace('\r\n', '\n')
|
||||
self.wipml = self.wipml.replace('> <', '>\n<')
|
||||
self.wipml = self.wipml.replace('<mbp: ', '<mbp:')
|
||||
# self.wipml = re.sub(r'<?xml[^>]*>', '', self.wipml)
|
||||
self.wipml = self.wipml.replace('<br></br>','<br/>')
|
||||
|
||||
def replace_page_breaks(self):
|
||||
self.wipml = self.PAGE_BREAK_PAT.sub(
|
||||
'<div class="mbp_pagebreak" />',
|
||||
self.wipml)
|
||||
|
||||
# parse leading text of ml and tag
|
||||
def parseml(self):
|
||||
p = self.pos
|
||||
if p >= len(self.wipml):
|
||||
return None
|
||||
if self.wipml[p] != '<':
|
||||
res = self.wipml.find('<',p)
|
||||
if res == -1 :
|
||||
res = len(self.wipml)
|
||||
self.pos = res
|
||||
return self.wipml[p:res], None
|
||||
# handle comment as a special case to deal with multi-line comments
|
||||
if self.wipml[p:p+4] == '<!--':
|
||||
te = self.wipml.find('-->',p+1)
|
||||
if te != -1:
|
||||
te = te+2
|
||||
else :
|
||||
te = self.wipml.find('>',p+1)
|
||||
ntb = self.wipml.find('<',p+1)
|
||||
if ntb != -1 and ntb < te:
|
||||
self.pos = ntb
|
||||
return self.wipml[p:ntb], None
|
||||
self.pos = te + 1
|
||||
return None, self.wipml[p:te+1]
|
||||
|
||||
# parses string version of tag to identify its name,
|
||||
# its type 'begin', 'end' or 'single',
|
||||
# plus build a hashtable of its attributes
|
||||
# code is written to handle the possiblity of very poor formating
|
||||
def parsetag(self, s):
|
||||
p = 1
|
||||
# get the tag name
|
||||
tname = None
|
||||
ttype = None
|
||||
tattr = {}
|
||||
while s[p:p+1] == ' ' :
|
||||
p += 1
|
||||
if s[p:p+1] == '/':
|
||||
ttype = 'end'
|
||||
p += 1
|
||||
while s[p:p+1] == ' ' :
|
||||
p += 1
|
||||
b = p
|
||||
while s[p:p+1] not in ('>', '/', ' ', '"', "'", "\r", "\n") :
|
||||
p += 1
|
||||
tname=s[b:p].lower()
|
||||
if tname == '!doctype':
|
||||
tname = '!DOCTYPE'
|
||||
# special cases
|
||||
if tname in SPECIAL_HANDLING_TAGS.keys():
|
||||
ttype, backstep = SPECIAL_HANDLING_TAGS[tname]
|
||||
tattr['special'] = s[p:backstep]
|
||||
if ttype is None:
|
||||
# parse any attributes
|
||||
while s.find('=',p) != -1 :
|
||||
while s[p:p+1] == ' ' :
|
||||
p += 1
|
||||
b = p
|
||||
while s[p:p+1] != '=' :
|
||||
p += 1
|
||||
aname = s[b:p].lower()
|
||||
aname = aname.rstrip(' ')
|
||||
p += 1
|
||||
while s[p:p+1] == ' ' :
|
||||
p += 1
|
||||
if s[p:p+1] in ('"', "'") :
|
||||
p = p + 1
|
||||
b = p
|
||||
while s[p:p+1] not in ('"', "'") :
|
||||
p += 1
|
||||
val = s[b:p]
|
||||
p += 1
|
||||
else :
|
||||
b = p
|
||||
while s[p:p+1] not in ('>', '/', ' ') :
|
||||
p += 1
|
||||
val = s[b:p]
|
||||
tattr[aname] = val
|
||||
# label beginning and single tags
|
||||
if ttype is None:
|
||||
ttype = 'begin'
|
||||
if s.find(' /',p) >= 0:
|
||||
ttype = 'single_ext'
|
||||
elif s.find('/',p) >= 0:
|
||||
ttype = 'single'
|
||||
return ttype, tname, tattr
|
||||
|
||||
# main routine to convert from mobi markup language to html
|
||||
def processml(self):
|
||||
|
||||
# are these really needed
|
||||
html_done = False
|
||||
head_done = False
|
||||
body_done = False
|
||||
|
||||
skip = False
|
||||
|
||||
htmlstr = ''
|
||||
self.replace_page_breaks()
|
||||
self.cleanup_html()
|
||||
|
||||
# now parse the cleaned up ml into standard xhtml
|
||||
while True:
|
||||
|
||||
r = self.parseml()
|
||||
if not r:
|
||||
break
|
||||
|
||||
text, tag = r
|
||||
|
||||
if text:
|
||||
if not skip:
|
||||
htmlstr += text
|
||||
|
||||
if tag:
|
||||
ttype, tname, tattr = self.parsetag(tag)
|
||||
|
||||
# If we run into a DTD or xml declarations inside the body ... bail.
|
||||
if tname in SPECIAL_HANDLING_TAGS.keys() and tname != 'comment' and body_done:
|
||||
htmlstr += '\n</body></html>'
|
||||
break
|
||||
|
||||
# make sure self-closing tags actually self-close
|
||||
if ttype == 'begin' and tname in SELF_CLOSING_TAGS:
|
||||
ttype = 'single'
|
||||
|
||||
# make sure any end tags of self-closing tags are discarded
|
||||
if ttype == 'end' and tname in SELF_CLOSING_TAGS:
|
||||
continue
|
||||
|
||||
# remove embedded guide and refernces from old mobis
|
||||
if tname in ('guide', 'ncx', 'reference') and ttype in ('begin', 'single', 'single_ext'):
|
||||
tname = 'removeme:{0}'.format(tname)
|
||||
tattr = None
|
||||
if tname in ('guide', 'ncx', 'reference', 'font', 'span') and ttype == 'end':
|
||||
if self.path[-1] == 'removeme:{0}'.format(tname):
|
||||
tname = 'removeme:{0}'.format(tname)
|
||||
tattr = None
|
||||
|
||||
# Get rid of font tags that only have a color attribute.
|
||||
if tname == 'font' and ttype in ('begin', 'single', 'single_ext'):
|
||||
if 'color' in tattr.keys() and len(tattr.keys()) == 1:
|
||||
tname = 'removeme:{0}'.format(tname)
|
||||
tattr = None
|
||||
|
||||
# Get rid of empty spans in the markup.
|
||||
if tname == 'span' and ttype in ('begin', 'single', 'single_ext') and not len(tattr):
|
||||
tname = 'removeme:{0}'.format(tname)
|
||||
|
||||
# need to handle fonts outside of the normal methods
|
||||
# so fonts tags won't be added to the self.path since we keep track
|
||||
# of font tags separately with self.font_history
|
||||
if tname == 'font' and ttype == 'begin':
|
||||
# check for nested font start tags
|
||||
if len(self.font_history) > 0 :
|
||||
# inject a font end tag
|
||||
taginfo = ('end', 'font', None)
|
||||
htmlstr += self.processtag(taginfo)
|
||||
self.font_history.append((ttype, tname, tattr))
|
||||
# handle the current font start tag
|
||||
taginfo = (ttype, tname, tattr)
|
||||
htmlstr += self.processtag(taginfo)
|
||||
continue
|
||||
|
||||
# check for nested font tags and unnest them
|
||||
if tname == 'font' and ttype == 'end':
|
||||
self.font_history.pop()
|
||||
# handle this font end tag
|
||||
taginfo = ('end', 'font', None)
|
||||
htmlstr += self.processtag(taginfo)
|
||||
# check if we were nested
|
||||
if len(self.font_history) > 0:
|
||||
# inject a copy of the most recent font start tag from history
|
||||
taginfo = self.font_history[-1]
|
||||
htmlstr += self.processtag(taginfo)
|
||||
continue
|
||||
|
||||
# keep track of nesting path
|
||||
if ttype == 'begin':
|
||||
self.path.append(tname)
|
||||
elif ttype == 'end':
|
||||
if tname != self.path[-1]:
|
||||
print ('improper nesting: ', self.path, tname, ttype)
|
||||
if tname not in self.path:
|
||||
# handle case of end tag with no beginning by injecting empty begin tag
|
||||
taginfo = ('begin', tname, None)
|
||||
htmlstr += self.processtag(taginfo)
|
||||
print(" - fixed by injecting empty start tag ", tname)
|
||||
self.path.append(tname)
|
||||
elif len(self.path) > 1 and tname == self.path[-2]:
|
||||
# handle case of dangling missing end
|
||||
taginfo = ('end', self.path[-1], None)
|
||||
htmlstr += self.processtag(taginfo)
|
||||
print(" - fixed by injecting end tag ", self.path[-1])
|
||||
self.path.pop()
|
||||
self.path.pop()
|
||||
|
||||
if tname == 'removeme:{0}'.format(tname):
|
||||
if ttype in ('begin', 'single', 'single_ext'):
|
||||
skip = True
|
||||
else:
|
||||
skip = False
|
||||
else:
|
||||
taginfo = (ttype, tname, tattr)
|
||||
htmlstr += self.processtag(taginfo)
|
||||
|
||||
# handle potential issue of multiple html, head, and body sections
|
||||
if tname == 'html' and ttype == 'begin' and not html_done:
|
||||
htmlstr += '\n'
|
||||
html_done = True
|
||||
|
||||
if tname == 'head' and ttype == 'begin' and not head_done:
|
||||
htmlstr += '\n'
|
||||
# also add in metadata and style link tags
|
||||
htmlstr += self.meta
|
||||
htmlstr += '<link href="styles.css" rel="stylesheet" type="text/css" />\n'
|
||||
head_done = True
|
||||
|
||||
if tname == 'body' and ttype == 'begin' and not body_done:
|
||||
htmlstr += '\n'
|
||||
body_done = True
|
||||
|
||||
# handle issue of possibly missing html, head, and body tags
|
||||
# I have not seen this but the original did something like this so ...
|
||||
if not body_done:
|
||||
htmlstr = '<body>\n' + htmlstr + '</body>\n'
|
||||
if not head_done:
|
||||
headstr = '<head>\n'
|
||||
headstr += self.meta
|
||||
headstr += '<link href="styles.css" rel="stylesheet" type="text/css" />\n'
|
||||
headstr += '</head>\n'
|
||||
htmlstr = headstr + htmlstr
|
||||
if not html_done:
|
||||
htmlstr = '<html>\n' + htmlstr + '</html>\n'
|
||||
|
||||
# finally add DOCTYPE info
|
||||
htmlstr = '<?xml version="1.0"?>\n<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n' + htmlstr
|
||||
|
||||
css = self.base_css_rules
|
||||
for cls, rule in self.tag_css_rules.items():
|
||||
css += '.%s { %s }\n' % (cls, rule)
|
||||
|
||||
return (htmlstr, css, self.cssname)
|
||||
|
||||
def ensure_unit(self, raw, unit='px'):
|
||||
if re.search(r'\d+$', raw) is not None:
|
||||
raw += unit
|
||||
return raw
|
||||
|
||||
# flatten possibly modified tag back to string
|
||||
def taginfo_tostring(self, taginfo):
|
||||
(ttype, tname, tattr) = taginfo
|
||||
if ttype is None or tname is None:
|
||||
return ''
|
||||
if ttype == 'end':
|
||||
return '</%s>' % tname
|
||||
if ttype in SPECIAL_HANDLING_TYPES and tattr is not None and 'special' in tattr.keys():
|
||||
info = tattr['special']
|
||||
if ttype == 'comment':
|
||||
return '<%s %s-->' % tname, info
|
||||
else:
|
||||
return '<%s %s>' % tname, info
|
||||
res = []
|
||||
res.append('<%s' % tname)
|
||||
if tattr is not None:
|
||||
for key in tattr.keys():
|
||||
res.append(' %s="%s"' % (key, tattr[key]))
|
||||
if ttype == 'single':
|
||||
res.append('/>')
|
||||
elif ttype == 'single_ext':
|
||||
res.append(' />')
|
||||
else :
|
||||
res.append('>')
|
||||
return "".join(res)
|
||||
|
||||
# routines to convert from mobi ml tags atributes to xhtml attributes and styles
|
||||
def processtag(self, taginfo):
|
||||
# Converting mobi font sizes to numerics
|
||||
size_map = {
|
||||
'xx-small': '1',
|
||||
'x-small': '2',
|
||||
'small': '3',
|
||||
'medium': '4',
|
||||
'large': '5',
|
||||
'x-large': '6',
|
||||
'xx-large': '7',
|
||||
}
|
||||
|
||||
size_to_em_map = {
|
||||
'1': '.65em',
|
||||
'2': '.75em',
|
||||
'3': '1em',
|
||||
'4': '1.125em',
|
||||
'5': '1.25em',
|
||||
'6': '1.5em',
|
||||
'7': '2em',
|
||||
}
|
||||
|
||||
# current tag to work on
|
||||
(ttype, tname, tattr) = taginfo
|
||||
if not tattr:
|
||||
tattr = {}
|
||||
|
||||
styles = []
|
||||
|
||||
if tname is None or tname.startswith('removeme'):
|
||||
return ''
|
||||
|
||||
# have not seen an example of this yet so keep it here to be safe
|
||||
# until this is better understood
|
||||
if tname in ('country-region', 'place', 'placetype', 'placename',
|
||||
'state', 'city', 'street', 'address', 'content'):
|
||||
tname = 'div' if tname == 'content' else 'span'
|
||||
for key in tattr.keys():
|
||||
tattr.pop(key)
|
||||
|
||||
# handle general case of style, height, width, bgcolor in any tag
|
||||
if 'style' in tattr.keys():
|
||||
style = tattr.pop('style').strip()
|
||||
if style:
|
||||
styles.append(style)
|
||||
|
||||
if 'align' in tattr.keys():
|
||||
align = tattr.pop('align').strip()
|
||||
if align:
|
||||
if tname in ('table', 'td', 'tr'):
|
||||
pass
|
||||
else:
|
||||
styles.append('text-align: %s' % align)
|
||||
|
||||
if 'height' in tattr.keys():
|
||||
height = tattr.pop('height').strip()
|
||||
if height and '<' not in height and '>' not in height and re.search(r'\d+', height):
|
||||
if tname in ('table', 'td', 'tr'):
|
||||
pass
|
||||
elif tname == 'img':
|
||||
tattr['height'] = height
|
||||
else:
|
||||
styles.append('margin-top: %s' % self.ensure_unit(height))
|
||||
|
||||
if 'width' in tattr.keys():
|
||||
width = tattr.pop('width').strip()
|
||||
if width and re.search(r'\d+', width):
|
||||
if tname in ('table', 'td', 'tr'):
|
||||
pass
|
||||
elif tname == 'img':
|
||||
tattr['width'] = width
|
||||
else:
|
||||
styles.append('text-indent: %s' % self.ensure_unit(width))
|
||||
if width.startswith('-'):
|
||||
styles.append('margin-left: %s' % self.ensure_unit(width[1:]))
|
||||
|
||||
if 'bgcolor' in tattr.keys():
|
||||
# no proprietary html allowed
|
||||
if tname == 'div':
|
||||
del tattr['bgcolor']
|
||||
|
||||
elif tname == 'font':
|
||||
# Change font tags to span tags
|
||||
tname = 'span'
|
||||
if ttype in ('begin', 'single', 'single_ext'):
|
||||
# move the face attribute to css font-family
|
||||
if 'face' in tattr.keys():
|
||||
face = tattr.pop('face').strip()
|
||||
styles.append('font-family: "%s"' % face)
|
||||
|
||||
# Monitor the constantly changing font sizes, change them to ems and move
|
||||
# them to css. The following will work for 'flat' font tags, but nested font tags
|
||||
# will cause things to go wonky. Need to revert to the parent font tag's size
|
||||
# when a closing tag is encountered.
|
||||
if 'size' in tattr.keys():
|
||||
sz = tattr.pop('size').strip().lower()
|
||||
try:
|
||||
float(sz)
|
||||
except ValueError:
|
||||
if sz in size_map.keys():
|
||||
sz = size_map[sz]
|
||||
else:
|
||||
if sz.startswith('-') or sz.startswith('+'):
|
||||
sz = self.current_font_size + float(sz)
|
||||
if sz > 7:
|
||||
sz = 7
|
||||
elif sz < 1:
|
||||
sz = 1
|
||||
sz = str(int(sz))
|
||||
styles.append('font-size: %s' % size_to_em_map[sz])
|
||||
self.current_font_size = int(sz)
|
||||
|
||||
elif tname == 'img':
|
||||
for attr in ('width', 'height'):
|
||||
if attr in tattr:
|
||||
val = tattr[attr]
|
||||
if val.lower().endswith('em'):
|
||||
try:
|
||||
nval = float(val[:-2])
|
||||
nval *= 16 * (168.451/72) # Assume this was set using the Kindle profile
|
||||
tattr[attr] = "%dpx"%int(nval)
|
||||
except:
|
||||
del tattr[attr]
|
||||
elif val.lower().endswith('%'):
|
||||
del tattr[attr]
|
||||
|
||||
# convert the anchor tags
|
||||
if 'filepos-id' in tattr:
|
||||
tattr['id'] = tattr.pop('filepos-id')
|
||||
if 'name' in tattr and tattr['name'] != tattr['id']:
|
||||
tattr['name'] = tattr['id']
|
||||
|
||||
if 'filepos' in tattr:
|
||||
filepos = tattr.pop('filepos')
|
||||
try:
|
||||
tattr['href'] = "#filepos%d" % int(filepos)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if styles:
|
||||
ncls = None
|
||||
rule = '; '.join(styles)
|
||||
for sel, srule in self.tag_css_rules.items():
|
||||
if srule == rule:
|
||||
ncls = sel
|
||||
break
|
||||
if ncls is None:
|
||||
self.tag_css_rule_cnt += 1
|
||||
ncls = 'rule_%d' % self.tag_css_rule_cnt
|
||||
self.tag_css_rules[ncls] = rule
|
||||
cls = tattr.get('class', '')
|
||||
cls = cls + (' ' if cls else '') + ncls
|
||||
tattr['class'] = cls
|
||||
|
||||
# convert updated tag back to string representation
|
||||
if len(tattr) == 0:
|
||||
tattr = None
|
||||
taginfo = (ttype, tname, tattr)
|
||||
return self.taginfo_tostring(taginfo)
|
||||
|
||||
''' main only left in for testing outside of plugin '''
|
||||
|
||||
def main(argv=sys.argv):
|
||||
if len(argv) != 2:
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
|
||||
try:
|
||||
print('Converting Mobi Markup Language to XHTML')
|
||||
mlc = MobiMLConverter(infile)
|
||||
print('Processing ...')
|
||||
htmlstr, css, cssname = mlc.processml()
|
||||
outname = infile.rsplit('.',1)[0] + '_converted.html'
|
||||
file(outname, 'wb').write(htmlstr)
|
||||
file(cssname, 'wb').write(css)
|
||||
print('Completed')
|
||||
print('XHTML version of book can be found at: ', outname)
|
||||
|
||||
except ValueError as e:
|
||||
print("Error: %s" % e)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
1210
lector/__main__.py
317
lector/annotations.py
Normal file
@@ -0,0 +1,317 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from lector.resources import annotationswindow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
|
||||
def __init__(self, parent=None):
|
||||
super(AnnotationsUI, self).__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self.parent = parent
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
# Current annotation
|
||||
self.modelIndex = None # The index of the annotations list model in the parent dialog
|
||||
self.current_annotation = {}
|
||||
|
||||
# Populate annotation type
|
||||
textmarkup_string = self._translate('AnnotationsUI', 'Text markup')
|
||||
all_types = [textmarkup_string]
|
||||
for i in all_types:
|
||||
self.typeBox.addItem(i)
|
||||
|
||||
# Init defaults
|
||||
self.default_stylesheet = self.foregroundCheck.styleSheet()
|
||||
self.foregroundColor = QtGui.QColor.fromRgb(0, 0, 0)
|
||||
self.underlineColor = QtGui.QColor.fromRgb(255, 0, 0)
|
||||
self.highlightColor = QtGui.QColor.fromRgb(66, 209, 56)
|
||||
self.underline_styles = {
|
||||
'Solid': QtGui.QTextCharFormat.SingleUnderline,
|
||||
'Dashes': QtGui.QTextCharFormat.DashUnderline,
|
||||
'Dots': QtGui.QTextCharFormat.DotLine,
|
||||
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
|
||||
|
||||
# Push buttons
|
||||
self.foregroundColorButton.clicked.connect(self.modify_annotation)
|
||||
self.highlightColorButton.clicked.connect(self.modify_annotation)
|
||||
self.underlineColorButton.clicked.connect(self.modify_annotation)
|
||||
|
||||
self.okButton.clicked.connect(self.ok_pressed)
|
||||
self.cancelButton.clicked.connect(self.hide)
|
||||
|
||||
# Underline combo box
|
||||
underline_items = ['Solid', 'Dashes', 'Dots', 'Wavy']
|
||||
self.underlineType.addItems(underline_items)
|
||||
self.underlineType.currentIndexChanged.connect(self.modify_annotation)
|
||||
|
||||
# Text markup related checkboxes
|
||||
self.foregroundCheck.clicked.connect(self.modify_annotation)
|
||||
self.highlightCheck.clicked.connect(self.modify_annotation)
|
||||
self.boldCheck.clicked.connect(self.modify_annotation)
|
||||
self.italicCheck.clicked.connect(self.modify_annotation)
|
||||
self.underlineCheck.clicked.connect(self.modify_annotation)
|
||||
|
||||
def show_dialog(self, mode, index=None):
|
||||
# TODO
|
||||
# Account for annotation type here
|
||||
# and point to a relevant set of widgets accordingly
|
||||
|
||||
if mode == 'edit' or mode == 'preview':
|
||||
self.modelIndex = index
|
||||
this_annotation = self.parent.annotationModel.data(
|
||||
index, QtCore.Qt.UserRole)
|
||||
|
||||
annotation_name = this_annotation['name']
|
||||
self.nameEdit.setText(annotation_name)
|
||||
|
||||
annotation_components = this_annotation['components']
|
||||
|
||||
if 'foregroundColor' in annotation_components:
|
||||
self.foregroundCheck.setChecked(True)
|
||||
self.foregroundColor = annotation_components['foregroundColor']
|
||||
self.set_button_background_color(
|
||||
self.foregroundColorButton, annotation_components['foregroundColor'])
|
||||
else:
|
||||
self.foregroundCheck.setChecked(False)
|
||||
|
||||
if 'highlightColor' in annotation_components:
|
||||
self.highlightCheck.setChecked(True)
|
||||
self.highlightColor = annotation_components['highlightColor']
|
||||
self.set_button_background_color(
|
||||
self.highlightColorButton, annotation_components['highlightColor'])
|
||||
else:
|
||||
self.highlightCheck.setChecked(False)
|
||||
|
||||
if 'bold' in annotation_components:
|
||||
self.boldCheck.setChecked(True)
|
||||
else:
|
||||
self.boldCheck.setChecked(False)
|
||||
|
||||
if 'italic' in annotation_components:
|
||||
self.italicCheck.setChecked(True)
|
||||
else:
|
||||
self.italicCheck.setChecked(False)
|
||||
|
||||
if 'underline' in annotation_components:
|
||||
self.underlineCheck.setChecked(True)
|
||||
underline_params = annotation_components['underline']
|
||||
self.underlineType.setCurrentText(underline_params[0])
|
||||
self.set_button_background_color(
|
||||
self.underlineColorButton, underline_params[1])
|
||||
else:
|
||||
self.underlineCheck.setChecked(False)
|
||||
|
||||
elif mode == 'add':
|
||||
new_annotation_string = self._translate('AnnotationsUI', 'New annotation')
|
||||
self.nameEdit.setText(new_annotation_string)
|
||||
|
||||
all_checkboxes = (
|
||||
self.foregroundCheck, self.highlightCheck,
|
||||
self.boldCheck, self.italicCheck, self.underlineCheck)
|
||||
for i in all_checkboxes:
|
||||
i.setChecked(False)
|
||||
|
||||
self.modelIndex = None
|
||||
self.set_button_background_color(
|
||||
self.foregroundColorButton, self.foregroundColor)
|
||||
self.set_button_background_color(
|
||||
self.highlightColorButton, self.highlightColor)
|
||||
self.set_button_background_color(
|
||||
self.underlineColorButton, self.underlineColor)
|
||||
|
||||
self.update_preview()
|
||||
if mode != 'preview':
|
||||
self.show()
|
||||
|
||||
def set_button_background_color(self, button, color):
|
||||
button.setStyleSheet(
|
||||
"QPushButton {{background-color: {0}}}".format(color.name()))
|
||||
|
||||
def update_preview(self):
|
||||
cursor = self.parent.previewView.textCursor()
|
||||
cursor.setPosition(0)
|
||||
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
# TODO
|
||||
# Other kinds of text markup
|
||||
previewCharFormat = QtGui.QTextCharFormat()
|
||||
|
||||
if self.foregroundCheck.isChecked():
|
||||
previewCharFormat.setForeground(self.foregroundColor)
|
||||
|
||||
highlight = QtCore.Qt.transparent
|
||||
if self.highlightCheck.isChecked():
|
||||
highlight = self.highlightColor
|
||||
previewCharFormat.setBackground(highlight)
|
||||
|
||||
font_weight = QtGui.QFont.Normal
|
||||
if self.boldCheck.isChecked():
|
||||
font_weight = QtGui.QFont.Bold
|
||||
previewCharFormat.setFontWeight(font_weight)
|
||||
|
||||
if self.italicCheck.isChecked():
|
||||
previewCharFormat.setFontItalic(True)
|
||||
|
||||
if self.underlineCheck.isChecked():
|
||||
previewCharFormat.setFontUnderline(True)
|
||||
previewCharFormat.setUnderlineColor(self.underlineColor)
|
||||
previewCharFormat.setUnderlineStyle(
|
||||
self.underline_styles[self.underlineType.currentText()])
|
||||
|
||||
previewCharFormat.setFontStyleStrategy(
|
||||
QtGui.QFont.PreferAntialias)
|
||||
|
||||
cursor.setCharFormat(previewCharFormat)
|
||||
cursor.clearSelection()
|
||||
self.parent.previewView.setTextCursor(cursor)
|
||||
|
||||
def modify_annotation(self):
|
||||
sender = self.sender()
|
||||
if isinstance(sender, QtWidgets.QCheckBox):
|
||||
if not sender.isChecked():
|
||||
self.update_preview()
|
||||
return
|
||||
|
||||
new_color = None
|
||||
|
||||
if sender == self.foregroundColorButton:
|
||||
new_color = self.get_color(self.foregroundColor)
|
||||
self.foregroundColor = new_color
|
||||
|
||||
if sender == self.highlightColorButton:
|
||||
new_color = self.get_color(self.highlightColor)
|
||||
self.highlightColor = new_color
|
||||
|
||||
if sender == self.underlineColorButton:
|
||||
new_color = self.get_color(self.underlineColor)
|
||||
self.underlineColor = new_color
|
||||
|
||||
if new_color:
|
||||
self.set_button_background_color(sender, new_color)
|
||||
self.update_preview()
|
||||
|
||||
def get_color(self, current_color):
|
||||
color_dialog = QtWidgets.QColorDialog()
|
||||
new_color = color_dialog.getColor(current_color)
|
||||
if new_color.isValid(): # Returned in case cancel is pressed
|
||||
return new_color
|
||||
else:
|
||||
return current_color
|
||||
|
||||
def ok_pressed(self):
|
||||
annotation_name = self.nameEdit.text()
|
||||
if annotation_name == '':
|
||||
self.nameEdit.setText('Why do you like bugs? WHY?')
|
||||
return
|
||||
|
||||
annotation_components = {}
|
||||
if self.foregroundCheck.isChecked():
|
||||
annotation_components['foregroundColor'] = self.foregroundColor
|
||||
if self.highlightCheck.isChecked():
|
||||
annotation_components['highlightColor'] = self.highlightColor
|
||||
if self.boldCheck.isChecked():
|
||||
annotation_components['bold'] = True
|
||||
if self.italicCheck.isChecked():
|
||||
annotation_components['italic'] = True
|
||||
if self.underlineCheck.isChecked():
|
||||
annotation_components['underline'] = (
|
||||
self.underlineType.currentText(), self.underlineColor)
|
||||
|
||||
self.current_annotation = {
|
||||
'name': annotation_name,
|
||||
'applicable_to': 'text',
|
||||
'type': 'text_markup',
|
||||
'components': annotation_components}
|
||||
|
||||
if self.modelIndex:
|
||||
self.parent.annotationModel.setData(
|
||||
self.modelIndex, annotation_name, QtCore.Qt.DisplayRole)
|
||||
self.parent.annotationModel.setData(
|
||||
self.modelIndex, self.current_annotation, QtCore.Qt.UserRole)
|
||||
else: # New annotation
|
||||
new_annotation_item = QtGui.QStandardItem()
|
||||
new_annotation_item.setText(annotation_name)
|
||||
new_annotation_item.setData(self.current_annotation, QtCore.Qt.UserRole)
|
||||
self.parent.annotationModel.appendRow(new_annotation_item)
|
||||
|
||||
self.hide()
|
||||
|
||||
|
||||
class AnnotationPlacement:
|
||||
def __init__(self):
|
||||
self.annotation_type = None
|
||||
self.annotation_components = None
|
||||
self.underline_styles = {
|
||||
'Solid': QtGui.QTextCharFormat.SingleUnderline,
|
||||
'Dashes': QtGui.QTextCharFormat.DashUnderline,
|
||||
'Dots': QtGui.QTextCharFormat.DotLine,
|
||||
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
|
||||
|
||||
def set_current_annotation(self, annotation_type, annotation_components):
|
||||
# Components expected to be a dictionary
|
||||
self.annotation_type = annotation_type # This is currently unused
|
||||
self.annotation_components = annotation_components
|
||||
|
||||
def format_text(self, cursor, start_here, end_here):
|
||||
# This is applicable only to the PliantQTextBrowser
|
||||
# for the text_markup style of annotation
|
||||
|
||||
# The cursor is the textCursor of the QTextEdit
|
||||
# containing the text that has to be modified
|
||||
|
||||
if not self.annotation_components:
|
||||
return
|
||||
|
||||
cursor.setPosition(start_here)
|
||||
cursor.setPosition(end_here, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
newCharFormat = QtGui.QTextCharFormat()
|
||||
|
||||
if 'foregroundColor' in self.annotation_components:
|
||||
newCharFormat.setForeground(
|
||||
self.annotation_components['foregroundColor'])
|
||||
|
||||
if 'highlightColor' in self.annotation_components:
|
||||
newCharFormat.setBackground(
|
||||
self.annotation_components['highlightColor'])
|
||||
|
||||
if 'bold' in self.annotation_components:
|
||||
newCharFormat.setFontWeight(QtGui.QFont.Bold)
|
||||
|
||||
if 'italic' in self.annotation_components:
|
||||
newCharFormat.setFontItalic(True)
|
||||
|
||||
if 'underline' in self.annotation_components:
|
||||
newCharFormat.setFontUnderline(True)
|
||||
newCharFormat.setUnderlineStyle(
|
||||
self.underline_styles[self.annotation_components['underline'][0]])
|
||||
newCharFormat.setUnderlineColor(
|
||||
self.annotation_components['underline'][1])
|
||||
|
||||
newCharFormat.setFontStyleStrategy(
|
||||
QtGui.QFont.PreferAntialias)
|
||||
|
||||
cursor.setCharFormat(newCharFormat)
|
||||
cursor.clearSelection()
|
||||
return cursor
|
919
lector/contentwidgets.py
Normal file
@@ -0,0 +1,919 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import logging
|
||||
import webbrowser
|
||||
|
||||
try:
|
||||
import fitz
|
||||
from lector.parsers.pdf import render_pdf_page
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
from lector.rarfile import rarfile
|
||||
from lector.threaded import BackGroundCacheRefill
|
||||
from lector.annotations import AnnotationPlacement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
def __init__(self, filepath, main_window, parent=None):
|
||||
super(PliantQGraphicsView, self).__init__(parent)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
self.parent = parent
|
||||
self.main_window = main_window
|
||||
|
||||
self.image_pixmap = None
|
||||
self.image_cache = [None for _ in range(4)]
|
||||
|
||||
self.thread = None
|
||||
|
||||
self.annotation_dict = self.parent.metadata['annotations']
|
||||
|
||||
self.filepath = filepath
|
||||
self.filetype = os.path.splitext(self.filepath)[1][1:]
|
||||
|
||||
if self.filetype == 'cbz':
|
||||
self.book = zipfile.ZipFile(self.filepath)
|
||||
|
||||
elif self.filetype == 'cbr':
|
||||
self.book = rarfile.RarFile(self.filepath)
|
||||
|
||||
elif self.filetype == 'pdf':
|
||||
self.book = fitz.open(self.filepath)
|
||||
|
||||
self.common_functions = PliantWidgetsCommonFunctions(
|
||||
self, self.main_window)
|
||||
|
||||
self.ignore_wheel_event = False
|
||||
self.ignore_wheel_event_number = 0
|
||||
self.setMouseTracking(True)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(
|
||||
self.generate_graphicsview_context_menu)
|
||||
|
||||
def loadImage(self, current_page):
|
||||
all_pages = self.parent.metadata['content']
|
||||
current_page_index = all_pages.index(current_page)
|
||||
|
||||
double_page_mode = False
|
||||
if (self.main_window.settings['double_page_mode']
|
||||
and (current_page_index not in (0, len(all_pages) - 1))):
|
||||
double_page_mode = True
|
||||
|
||||
def load_page(current_page):
|
||||
def page_loader(page):
|
||||
pixmap = QtGui.QPixmap()
|
||||
|
||||
if self.filetype in ('cbz', 'cbr'):
|
||||
page_data = self.book.read(page)
|
||||
pixmap.loadFromData(page_data)
|
||||
|
||||
elif self.filetype == 'pdf':
|
||||
page_data = self.book.loadPage(page)
|
||||
pixmap = render_pdf_page(page_data)
|
||||
|
||||
return pixmap
|
||||
|
||||
firstPixmap = page_loader(current_page)
|
||||
if not double_page_mode:
|
||||
return firstPixmap
|
||||
|
||||
next_page = all_pages[current_page_index + 1]
|
||||
secondPixmap = page_loader(next_page)
|
||||
|
||||
# Pixmap height should be the greater of the 2 images
|
||||
pixmap_height = firstPixmap.height()
|
||||
if secondPixmap.height() > pixmap_height:
|
||||
pixmap_height = secondPixmap.height()
|
||||
|
||||
bigPixmap = QtGui.QPixmap(
|
||||
firstPixmap.width() + secondPixmap.width() + 5,
|
||||
pixmap_height)
|
||||
bigPixmap.fill(QtCore.Qt.transparent)
|
||||
imagePainter = QtGui.QPainter(bigPixmap)
|
||||
|
||||
manga_mode = self.main_window.settings['manga_mode']
|
||||
if manga_mode:
|
||||
imagePainter.drawPixmap(0, 0, secondPixmap)
|
||||
imagePainter.drawPixmap(secondPixmap.width() + 4, 0, firstPixmap)
|
||||
else:
|
||||
imagePainter.drawPixmap(0, 0, firstPixmap)
|
||||
imagePainter.drawPixmap(firstPixmap.width() + 4, 0, secondPixmap)
|
||||
|
||||
imagePainter.end()
|
||||
return bigPixmap
|
||||
|
||||
def generate_image_cache(current_page):
|
||||
logger.info('(Re)building image cache')
|
||||
current_page_index = all_pages.index(current_page)
|
||||
|
||||
# Image caching for single and double page views
|
||||
page_indices = (-1, 0, 1, 2)
|
||||
|
||||
index_modifier = 0
|
||||
if double_page_mode:
|
||||
index_modifier = 1
|
||||
|
||||
for i in page_indices:
|
||||
try:
|
||||
this_page = all_pages[current_page_index + i + index_modifier]
|
||||
this_pixmap = load_page(this_page)
|
||||
self.image_cache[i + 1] = (this_page, this_pixmap)
|
||||
except IndexError:
|
||||
self.image_cache[i + 1] = None
|
||||
|
||||
def refill_cache(remove_value):
|
||||
# Do NOT put a parent in here or the mother of all
|
||||
# memory leaks will result
|
||||
self.thread = BackGroundCacheRefill(
|
||||
self.image_cache, remove_value,
|
||||
self.filetype, self.book, all_pages)
|
||||
self.thread.finished.connect(overwrite_cache)
|
||||
self.thread.start()
|
||||
|
||||
def overwrite_cache():
|
||||
self.image_cache = self.thread.image_cache
|
||||
|
||||
def check_cache(current_page):
|
||||
for i in self.image_cache:
|
||||
if i:
|
||||
if i[0] == current_page:
|
||||
return_pixmap = i[1]
|
||||
refill_cache(i)
|
||||
return return_pixmap
|
||||
|
||||
# No return happened so the image isn't in the cache
|
||||
generate_image_cache(current_page)
|
||||
|
||||
# TODO
|
||||
# Get caching working for double page view
|
||||
if not double_page_mode and self.main_window.settings['caching_enabled']:
|
||||
return_pixmap = None
|
||||
while not return_pixmap:
|
||||
return_pixmap = check_cache(current_page)
|
||||
else:
|
||||
return_pixmap = load_page(current_page)
|
||||
|
||||
if self.main_window.settings['invert_colors']:
|
||||
qImg = return_pixmap.toImage()
|
||||
qImg.invertPixels()
|
||||
if qImg: # Will return None if conversion doesn't work
|
||||
return_pixmap = QtGui.QPixmap().fromImage(qImg)
|
||||
else:
|
||||
logger.error('Color inversion failed: ' + current_page)
|
||||
|
||||
self.image_pixmap = return_pixmap
|
||||
self.resizeEvent()
|
||||
|
||||
def resizeEvent(self, *args):
|
||||
if not self.image_pixmap:
|
||||
return
|
||||
|
||||
zoom_mode = self.main_window.comic_profile['zoom_mode']
|
||||
padding = self.main_window.comic_profile['padding']
|
||||
|
||||
if zoom_mode == 'fitWidth':
|
||||
available_width = self.viewport().width()
|
||||
image_pixmap = self.image_pixmap.scaledToWidth(
|
||||
available_width, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
elif zoom_mode == 'originalSize':
|
||||
image_pixmap = self.image_pixmap
|
||||
|
||||
new_padding = (self.viewport().width() - image_pixmap.width()) // 2
|
||||
if new_padding < 0: # The image is larger than the viewport
|
||||
self.main_window.comic_profile['padding'] = 0
|
||||
else:
|
||||
self.main_window.comic_profile['padding'] = new_padding
|
||||
|
||||
elif zoom_mode == 'bestFit':
|
||||
available_width = self.viewport().width()
|
||||
available_height = self.viewport().height()
|
||||
|
||||
image_pixmap = self.image_pixmap.scaled(
|
||||
available_width, available_height,
|
||||
QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
self.main_window.comic_profile['padding'] = (
|
||||
self.viewport().width() - image_pixmap.width()) // 2
|
||||
|
||||
elif zoom_mode == 'manualZoom':
|
||||
available_width = self.viewport().width() - 2 * padding
|
||||
image_pixmap = self.image_pixmap.scaledToWidth(
|
||||
available_width, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
graphicsScene = QtWidgets.QGraphicsScene()
|
||||
graphicsScene.addPixmap(image_pixmap)
|
||||
|
||||
self.setScene(graphicsScene)
|
||||
self.show()
|
||||
|
||||
# This prevents a partial page scroll on first load
|
||||
self.verticalScrollBar().setValue(0)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
self.common_functions.wheelEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
vertical = self.verticalScrollBar().value()
|
||||
maximum = self.verticalScrollBar().maximum()
|
||||
|
||||
def scroller(increment, move_forward=True):
|
||||
if move_forward:
|
||||
if vertical == maximum:
|
||||
self.common_functions.change_chapter(1, True)
|
||||
else:
|
||||
next_val = vertical + increment
|
||||
if next_val >= .95 * maximum:
|
||||
next_val = maximum
|
||||
self.verticalScrollBar().setValue(next_val)
|
||||
else:
|
||||
if vertical == 0:
|
||||
self.common_functions.change_chapter(-1, False)
|
||||
else:
|
||||
next_val = vertical - increment
|
||||
if next_val <= .05 * maximum:
|
||||
next_val = 0
|
||||
self.verticalScrollBar().setValue(next_val)
|
||||
|
||||
small_increment = maximum //self.main_window.settings['small_increment']
|
||||
big_increment = maximum // self.main_window.settings['large_increment']
|
||||
|
||||
# Scrolling
|
||||
if event.key() == QtCore.Qt.Key_Up:
|
||||
scroller(small_increment, False)
|
||||
if event.key() == QtCore.Qt.Key_Down:
|
||||
scroller(small_increment)
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
scroller(big_increment)
|
||||
|
||||
# Double page mode and manga mode
|
||||
if event.key() in (QtCore.Qt.Key_D, QtCore.Qt.Key_M):
|
||||
self.main_window.change_page_view(event.key())
|
||||
|
||||
# Image fit modes
|
||||
view_modification_keys = (
|
||||
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Minus, QtCore.Qt.Key_Equal,
|
||||
QtCore.Qt.Key_B, QtCore.Qt.Key_W, QtCore.Qt.Key_O)
|
||||
if event.key() in view_modification_keys:
|
||||
self.main_window.modify_comic_view(event.key())
|
||||
|
||||
def record_position(self):
|
||||
self.parent.metadata['position']['is_read'] = False
|
||||
self.common_functions.update_model()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if QtWidgets.QApplication.mouseButtons() == QtCore.Qt.NoButton:
|
||||
self.viewport().setCursor(QtCore.Qt.OpenHandCursor)
|
||||
else:
|
||||
self.viewport().setCursor(QtCore.Qt.ClosedHandCursor)
|
||||
self.parent.mouse_hide_timer.start(2000)
|
||||
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
|
||||
|
||||
def generate_graphicsview_context_menu(self, position):
|
||||
contextMenu = QtWidgets.QMenu()
|
||||
fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
|
||||
|
||||
if self.parent.is_fullscreen:
|
||||
fsToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('view-fullscreen'),
|
||||
self._translate('PliantQGraphicsView', 'Exit fullscreen'))
|
||||
elif not self.main_window.settings['show_bars']:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQGraphicsView', 'Exit Distraction Free mode')
|
||||
|
||||
dfToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('visibility'),
|
||||
distraction_free_prompt)
|
||||
|
||||
saveAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('filesaveas'),
|
||||
self._translate('PliantQGraphicsView', 'Save page as...'))
|
||||
|
||||
view_submenu_string = self._translate('PliantQGraphicsView', 'View')
|
||||
viewSubMenu = contextMenu.addMenu(view_submenu_string)
|
||||
viewSubMenu.setIcon(
|
||||
self.main_window.QImageFactory.get_image('mail-thread-watch'))
|
||||
|
||||
doublePageAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('page-double'),
|
||||
self._translate('PliantQGraphicsView', 'Double page mode (D)'))
|
||||
doublePageAction.setCheckable(True)
|
||||
doublePageAction.setChecked(
|
||||
self.main_window.bookToolBar.doublePageButton.isChecked())
|
||||
|
||||
mangaModeAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('manga-mode'),
|
||||
self._translate('PliantQGraphicsView', 'Manga mode (M)'))
|
||||
mangaModeAction.setCheckable(True)
|
||||
mangaModeAction.setChecked(
|
||||
self.main_window.bookToolBar.mangaModeButton.isChecked())
|
||||
viewSubMenu.addSeparator()
|
||||
|
||||
zoominAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-in'),
|
||||
self._translate('PliantQGraphicsView', 'Zoom in (+)'))
|
||||
|
||||
zoomoutAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-out'),
|
||||
self._translate('PliantQGraphicsView', 'Zoom out (-)'))
|
||||
|
||||
fitWidthAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-fit-width'),
|
||||
self._translate('PliantQGraphicsView', 'Fit width (W)'))
|
||||
|
||||
bestFitAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-fit-best'),
|
||||
self._translate('PliantQGraphicsView', 'Best fit (B)'))
|
||||
|
||||
originalSizeAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-original'),
|
||||
self._translate('PliantQGraphicsView', 'Original size (O)'))
|
||||
|
||||
bookmarksToggleAction = 'Latin quote 2. Electric Boogaloo.'
|
||||
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
|
||||
bookmarksToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('bookmarks'),
|
||||
self._translate('PliantQGraphicsView', 'Bookmarks'))
|
||||
|
||||
self.common_functions.generate_combo_box_action(contextMenu)
|
||||
|
||||
action = contextMenu.exec_(self.sender().mapToGlobal(position))
|
||||
|
||||
if action == doublePageAction:
|
||||
self.main_window.bookToolBar.doublePageButton.trigger()
|
||||
if action == mangaModeAction:
|
||||
self.main_window.bookToolBar.mangaModeButton.trigger()
|
||||
|
||||
if action == saveAction:
|
||||
dialog_prompt = self._translate('Main_UI', 'Save page as...')
|
||||
extension_string = self._translate('Main_UI', 'Images')
|
||||
save_file = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self, dialog_prompt, self.main_window.settings['last_open_path'],
|
||||
f'{extension_string} (*.png *.jpg *.bmp)')
|
||||
|
||||
if save_file:
|
||||
self.image_pixmap.save(save_file[0])
|
||||
|
||||
if action == bookmarksToggleAction:
|
||||
self.parent.toggle_side_dock(1)
|
||||
if action == dfToggleAction:
|
||||
self.main_window.toggle_distraction_free()
|
||||
if action == fsToggleAction:
|
||||
self.parent.exit_fullscreen()
|
||||
|
||||
view_action_dict = {
|
||||
zoominAction: QtCore.Qt.Key_Plus,
|
||||
zoomoutAction: QtCore.Qt.Key_Minus,
|
||||
fitWidthAction: QtCore.Qt.Key_W,
|
||||
bestFitAction: QtCore.Qt.Key_B,
|
||||
originalSizeAction: QtCore.Qt.Key_O}
|
||||
|
||||
if action in view_action_dict:
|
||||
self.main_window.modify_comic_view(view_action_dict[action])
|
||||
|
||||
def closeEvent(self, *args):
|
||||
# In case the program is closed when a contentView is fullscreened
|
||||
self.main_window.closeEvent()
|
||||
|
||||
def toggle_annotation_mode(self):
|
||||
# The graphics view doesn't currently have annotation functionality
|
||||
# Don't delete this because it's still called
|
||||
pass
|
||||
|
||||
|
||||
class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
def __init__(self, main_window, parent=None):
|
||||
super(PliantQTextBrowser, self).__init__(parent)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
self.parent = parent
|
||||
self.main_window = main_window
|
||||
|
||||
self.annotation_mode = False
|
||||
self.annotator = AnnotationPlacement()
|
||||
self.current_annotation = None
|
||||
self.annotation_dict = self.parent.metadata['annotations']
|
||||
|
||||
self.common_functions = PliantWidgetsCommonFunctions(
|
||||
self, self.main_window)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(
|
||||
self.generate_textbrowser_context_menu)
|
||||
|
||||
self.setMouseTracking(True)
|
||||
self.verticalScrollBar().sliderMoved.connect(
|
||||
self.record_position)
|
||||
self.ignore_wheel_event = False
|
||||
self.ignore_wheel_event_number = 0
|
||||
|
||||
self.at_end = False
|
||||
|
||||
def wheelEvent(self, event):
|
||||
self.record_position()
|
||||
self.common_functions.wheelEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
QtWidgets.QTextEdit.keyPressEvent(self, event)
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
if self.verticalScrollBar().value() == self.verticalScrollBar().maximum():
|
||||
if self.at_end: # This makes sure the last lines of the chapter don't get skipped
|
||||
self.common_functions.change_chapter(1, True)
|
||||
self.at_end = True
|
||||
else:
|
||||
self.at_end = False
|
||||
self.set_top_line_cleanly()
|
||||
self.record_position()
|
||||
|
||||
def set_top_line_cleanly(self):
|
||||
# Find the cursor position of the top line and move to it
|
||||
find_cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
|
||||
find_cursor.movePosition(
|
||||
find_cursor.position(), QtGui.QTextCursor.KeepAnchor)
|
||||
self.setTextCursor(find_cursor)
|
||||
self.ensureCursorVisible()
|
||||
|
||||
def record_position(self, return_as_bookmark=False):
|
||||
self.parent.metadata['position']['is_read'] = False
|
||||
|
||||
# The y coordinate is set to 10 because 0 tends to make
|
||||
# cursor position a little finicky
|
||||
cursor = self.cursorForPosition(QtCore.QPoint(0, 10))
|
||||
cursor_position = cursor.position()
|
||||
|
||||
# Current block for progress measurement
|
||||
current_block = cursor.block().blockNumber()
|
||||
current_chapter = self.parent.metadata['position']['current_chapter']
|
||||
|
||||
blocks_per_chapter = self.parent.metadata['position']['blocks_per_chapter']
|
||||
block_sum = sum(blocks_per_chapter[:(current_chapter - 1)])
|
||||
block_sum += current_block
|
||||
|
||||
# This 'current_block' refers to the number of
|
||||
# blocks in the book upto this one
|
||||
self.parent.metadata['position']['current_block'] = block_sum
|
||||
self.common_functions.update_model()
|
||||
|
||||
if return_as_bookmark:
|
||||
return (self.parent.metadata['position']['current_chapter'],
|
||||
cursor_position)
|
||||
else:
|
||||
self.parent.metadata['position']['cursor_position'] = cursor_position
|
||||
|
||||
def toggle_annotation_mode(self):
|
||||
if self.annotation_mode:
|
||||
self.annotation_mode = False
|
||||
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.parent.sideDock.show()
|
||||
self.parent.sideDock.setWindowOpacity(.95)
|
||||
|
||||
self.current_annotation = None
|
||||
self.parent.sideDock.annotations.annotationListView.clearSelection()
|
||||
|
||||
else:
|
||||
self.annotation_mode = True
|
||||
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
||||
self.parent.sideDock.hide()
|
||||
|
||||
selected_index = self.parent.sideDock.annotations.annotationListView.currentIndex()
|
||||
self.current_annotation = self.parent.sideDock.annotationModel.data(
|
||||
selected_index, QtCore.Qt.UserRole)
|
||||
logger.info('Selected annotation: ' + self.current_annotation['name'])
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# This takes care of annotation placement
|
||||
# and addition to the list that holds all current annotations
|
||||
if not self.current_annotation:
|
||||
QtWidgets.QTextBrowser.mouseReleaseEvent(self, event)
|
||||
return
|
||||
|
||||
self.place_annotation(self.current_annotation)
|
||||
self.toggle_annotation_mode()
|
||||
|
||||
def place_annotation(self, annotation):
|
||||
current_chapter = self.parent.metadata['position']['current_chapter']
|
||||
cursor = self.textCursor()
|
||||
cursor_start = cursor.selectionStart()
|
||||
cursor_end = cursor.selectionEnd()
|
||||
annotation_type = 'text_markup'
|
||||
applicable_to = 'text'
|
||||
annotation_components = annotation['components']
|
||||
|
||||
self.annotator.set_current_annotation(
|
||||
annotation_type, annotation_components)
|
||||
|
||||
new_cursor = self.annotator.format_text(
|
||||
cursor, cursor_start, cursor_end)
|
||||
self.setTextCursor(new_cursor)
|
||||
|
||||
# TODO
|
||||
# Maybe use annotation name for a consolidated annotation list
|
||||
|
||||
this_annotation = {
|
||||
'name': annotation['name'],
|
||||
'applicable_to': applicable_to,
|
||||
'type': annotation_type,
|
||||
'cursor': (cursor_start, cursor_end),
|
||||
'components': annotation_components,
|
||||
'note': None}
|
||||
|
||||
try:
|
||||
self.annotation_dict[current_chapter].append(this_annotation)
|
||||
except KeyError:
|
||||
self.annotation_dict[current_chapter] = []
|
||||
self.annotation_dict[current_chapter].append(this_annotation)
|
||||
|
||||
def generate_textbrowser_context_menu(self, position):
|
||||
selection = self.textCursor().selection()
|
||||
selection = selection.toPlainText()
|
||||
|
||||
current_chapter = self.parent.metadata['position']['current_chapter']
|
||||
cursor_at_mouse = self.cursorForPosition(position)
|
||||
annotation_is_present = self.common_functions.annotation_specific(
|
||||
'check', 'text', current_chapter, cursor_at_mouse.position())
|
||||
|
||||
contextMenu = QtWidgets.QMenu()
|
||||
|
||||
# The following cannot be None because a click
|
||||
# outside the menu means that the action variable is None.
|
||||
defineAction = fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
|
||||
searchWikipediaAction = searchYoutubeAction = 'Does anyone know something funny in Latin?'
|
||||
searchAction = searchGoogleAction = bookmarksToggleAction = 'TODO Insert Latin Joke'
|
||||
deleteAnnotationAction = editAnnotationNoteAction = 'Latin quote 2. Electric Boogaloo.'
|
||||
annotationActions = []
|
||||
|
||||
if self.parent.is_fullscreen:
|
||||
fsToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('view-fullscreen'),
|
||||
self._translate('PliantQTextBrowser', 'Exit fullscreen'))
|
||||
elif not self.main_window.settings['show_bars']:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQTextBrowser', 'Exit Distraction Free mode')
|
||||
dfToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('visibility'),
|
||||
distraction_free_prompt)
|
||||
|
||||
if selection and selection != '':
|
||||
first_selected_word = selection.split()[0]
|
||||
elided_selection = selection
|
||||
if len(elided_selection) > 15:
|
||||
elided_selection = elided_selection[:15] + '...'
|
||||
|
||||
define_string = self._translate('PliantQTextBrowser', 'Define')
|
||||
defineAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('view-readermode'),
|
||||
f'{define_string} "{first_selected_word}"')
|
||||
|
||||
search_submenu_string = self._translate('PliantQTextBrowser', 'Search for')
|
||||
searchSubMenu = contextMenu.addMenu(
|
||||
search_submenu_string + f' "{elided_selection}"')
|
||||
searchSubMenu.setIcon(self.main_window.QImageFactory.get_image('search'))
|
||||
|
||||
searchAction = searchSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('search'),
|
||||
self._translate('PliantQTextBrowser', 'In this book'))
|
||||
searchSubMenu.addSeparator()
|
||||
searchGoogleAction = searchSubMenu.addAction(
|
||||
QtGui.QIcon(':/images/Google.png'),
|
||||
'Google')
|
||||
searchWikipediaAction = searchSubMenu.addAction(
|
||||
QtGui.QIcon(':/images/Wikipedia.png'),
|
||||
'Wikipedia')
|
||||
searchYoutubeAction = searchSubMenu.addAction(
|
||||
QtGui.QIcon(':/images/Youtube.png'),
|
||||
'Youtube')
|
||||
|
||||
# Allow adding new annotation from the context menu
|
||||
if not annotation_is_present:
|
||||
annotation_string = self._translate('PliantQTextBrowser', 'Annotate')
|
||||
annotationSubmenu = contextMenu.addMenu(annotation_string)
|
||||
annotationSubmenu.setIcon(
|
||||
self.main_window.QImageFactory.get_image('annotate'))
|
||||
|
||||
saved_annotations = self.parent.main_window.settings['annotations']
|
||||
if not saved_annotations:
|
||||
nope = annotationSubmenu.addAction('<No annotations set>')
|
||||
nope.setEnabled(False)
|
||||
|
||||
for i in saved_annotations:
|
||||
this_action = QtWidgets.QAction(i['name'])
|
||||
# Does not require / support a role
|
||||
this_action.setData(i)
|
||||
annotationActions.append(this_action)
|
||||
annotationSubmenu.addAction(this_action)
|
||||
|
||||
else:
|
||||
searchAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('search'),
|
||||
self._translate('PliantQTextBrowser', 'Search'))
|
||||
|
||||
if annotation_is_present:
|
||||
annotationsubMenu = contextMenu.addMenu('Annotation')
|
||||
annotationsubMenu.setIcon(self.main_window.QImageFactory.get_image('annotate'))
|
||||
|
||||
editAnnotationNoteAction = annotationsubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('edit-rename'),
|
||||
self._translate('PliantQTextBrowser', 'Edit note'))
|
||||
deleteAnnotationAction = annotationsubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('remove'),
|
||||
self._translate('PliantQTextBrowser', 'Delete annotation'))
|
||||
|
||||
add_bookmark_string = self._translate('PliantQTextBrowser', 'Add Bookmark')
|
||||
addBookMarkAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('bookmark-new'),
|
||||
add_bookmark_string)
|
||||
|
||||
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
|
||||
bookmarksToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('bookmarks'),
|
||||
self._translate('PliantQTextBrowser', 'Bookmarks'))
|
||||
|
||||
self.common_functions.generate_combo_box_action(contextMenu)
|
||||
|
||||
action = contextMenu.exec_(self.sender().mapToGlobal(position))
|
||||
|
||||
if action == addBookMarkAction:
|
||||
self.parent.sideDock.bookmarks.add_bookmark(cursor_at_mouse.position())
|
||||
|
||||
if action == defineAction:
|
||||
self.main_window.definitionDialog.find_definition(selection)
|
||||
|
||||
if action == searchAction:
|
||||
if selection and selection != '':
|
||||
self.parent.sideDock.search.searchLineEdit.setText(selection)
|
||||
self.parent.toggle_side_dock(2, True)
|
||||
|
||||
if action == searchGoogleAction:
|
||||
webbrowser.open_new_tab(
|
||||
f'https://www.google.com/search?q={selection}')
|
||||
if action == searchWikipediaAction:
|
||||
webbrowser.open_new_tab(
|
||||
f'https://en.wikipedia.org/wiki/Special:Search?search={selection}')
|
||||
if action == searchYoutubeAction:
|
||||
webbrowser.open_new_tab(
|
||||
f'https://www.youtube.com/results?search_query={selection}')
|
||||
|
||||
if action in annotationActions:
|
||||
self.place_annotation(action.data())
|
||||
|
||||
if action == editAnnotationNoteAction:
|
||||
self.common_functions.annotation_specific(
|
||||
'note', 'text', current_chapter, cursor_at_mouse.position())
|
||||
if action == deleteAnnotationAction:
|
||||
self.common_functions.annotation_specific(
|
||||
'delete', 'text', current_chapter, cursor_at_mouse.position())
|
||||
|
||||
if action == bookmarksToggleAction:
|
||||
self.parent.toggle_side_dock(0)
|
||||
|
||||
if action == fsToggleAction:
|
||||
self.parent.exit_fullscreen()
|
||||
if action == dfToggleAction:
|
||||
self.main_window.toggle_distraction_free()
|
||||
|
||||
def closeEvent(self, *args):
|
||||
self.main_window.closeEvent()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.annotation_mode:
|
||||
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
||||
else:
|
||||
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.parent.mouse_hide_timer.start(2000)
|
||||
QtWidgets.QTextBrowser.mouseMoveEvent(self, event)
|
||||
|
||||
|
||||
class PliantWidgetsCommonFunctions:
|
||||
def __init__(self, parent_widget, main_window):
|
||||
self.pw = parent_widget
|
||||
self.main_window = main_window
|
||||
self.are_we_doing_images_only = self.pw.parent.are_we_doing_images_only
|
||||
|
||||
def wheelEvent(self, event):
|
||||
ignore_events = 20
|
||||
if self.are_we_doing_images_only:
|
||||
ignore_events = 10
|
||||
|
||||
if self.pw.ignore_wheel_event:
|
||||
self.pw.ignore_wheel_event_number += 1
|
||||
if self.pw.ignore_wheel_event_number > ignore_events:
|
||||
self.pw.ignore_wheel_event = False
|
||||
self.pw.ignore_wheel_event_number = 0
|
||||
return
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
QtWidgets.QGraphicsView.wheelEvent(self.pw, event)
|
||||
else:
|
||||
QtWidgets.QTextBrowser.wheelEvent(self.pw, event)
|
||||
|
||||
# Since this is a delta on a mouse move event, it cannot ever be 0
|
||||
vertical_pdelta = event.pixelDelta().y()
|
||||
if vertical_pdelta > 0:
|
||||
moving_up = True
|
||||
elif vertical_pdelta < 0:
|
||||
moving_up = False
|
||||
|
||||
if abs(vertical_pdelta) > 80: # Adjust sensitivity here
|
||||
# Implies that no scrollbar movement is possible
|
||||
if self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum() == 0:
|
||||
if moving_up:
|
||||
self.change_chapter(-1)
|
||||
else:
|
||||
self.change_chapter(1)
|
||||
|
||||
# Implies that the scrollbar is at the bottom
|
||||
elif self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum():
|
||||
if not moving_up:
|
||||
self.change_chapter(1)
|
||||
|
||||
# Implies scrollbar is at the top
|
||||
elif self.pw.verticalScrollBar().value() == 0:
|
||||
if moving_up:
|
||||
self.change_chapter(-1)
|
||||
|
||||
def change_chapter(self, direction, was_button_pressed=None):
|
||||
current_tab = self.pw.parent
|
||||
current_position = current_tab.metadata['position']['current_chapter']
|
||||
final_position = len(current_tab.metadata['content'])
|
||||
|
||||
# Prevent scrolling below page 1
|
||||
if current_position == 1 and direction == -1:
|
||||
return
|
||||
|
||||
# Prevent scrolling beyond last page
|
||||
if (current_position == final_position) and direction == 1:
|
||||
return
|
||||
|
||||
# Special cases for double page view
|
||||
# Page limits are taken care of by the set_content method
|
||||
def get_modifier():
|
||||
if (not self.main_window.settings['double_page_mode']
|
||||
or not self.are_we_doing_images_only):
|
||||
return 0
|
||||
|
||||
if (current_position == 0 or current_position % 2 == 0):
|
||||
return 0
|
||||
|
||||
if current_position % 2 == 1:
|
||||
return direction
|
||||
|
||||
current_tab.set_content(
|
||||
current_position + direction + get_modifier(), True, True)
|
||||
|
||||
# Set page position depending on if the chapter number is increasing or decreasing
|
||||
if direction == 1 or was_button_pressed:
|
||||
self.pw.verticalScrollBar().setValue(0)
|
||||
else:
|
||||
self.pw.verticalScrollBar().setValue(
|
||||
self.pw.verticalScrollBar().maximum())
|
||||
|
||||
if not was_button_pressed:
|
||||
self.pw.ignore_wheel_event = True
|
||||
|
||||
def load_annotations(self, chapter):
|
||||
try:
|
||||
chapter_annotations = self.pw.annotation_dict[chapter]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
for i in chapter_annotations:
|
||||
applicable_to = i['applicable_to']
|
||||
annotation_type = i['type']
|
||||
annotation_components = i['components']
|
||||
|
||||
if not self.are_we_doing_images_only and applicable_to == 'text':
|
||||
cursor = self.pw.textCursor()
|
||||
cursor_start = i['cursor'][0]
|
||||
cursor_end = i['cursor'][1]
|
||||
|
||||
self.pw.annotator.set_current_annotation(
|
||||
annotation_type, annotation_components)
|
||||
|
||||
new_cursor = self.pw.annotator.format_text(
|
||||
cursor, cursor_start, cursor_end)
|
||||
self.pw.setTextCursor(new_cursor)
|
||||
|
||||
def clear_annotations(self):
|
||||
if not self.are_we_doing_images_only:
|
||||
cursor = self.pw.textCursor()
|
||||
cursor.setPosition(0)
|
||||
cursor.movePosition(
|
||||
QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
previewCharFormat = QtGui.QTextCharFormat()
|
||||
previewCharFormat.setFontStyleStrategy(
|
||||
QtGui.QFont.PreferAntialias)
|
||||
cursor.setCharFormat(previewCharFormat)
|
||||
cursor.clearSelection()
|
||||
self.pw.setTextCursor(cursor)
|
||||
|
||||
def annotation_specific(self, mode, annotation_type, chapter, cursor_position):
|
||||
try:
|
||||
chapter_annotations = self.pw.annotation_dict[chapter]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
for i in chapter_annotations:
|
||||
if annotation_type == 'text':
|
||||
cursor_start = i['cursor'][0]
|
||||
cursor_end = i['cursor'][1]
|
||||
|
||||
if cursor_start <= cursor_position <= cursor_end:
|
||||
if mode == 'check':
|
||||
return True
|
||||
if mode == 'delete':
|
||||
self.pw.annotation_dict[chapter].remove(i)
|
||||
if mode == 'note':
|
||||
note = i['note']
|
||||
self.pw.parent.annotationNoteDock.set_annotation(i)
|
||||
self.pw.parent.annotationNoteEdit.setText(note)
|
||||
self.pw.parent.annotationNoteDock.show()
|
||||
|
||||
# Post iteration
|
||||
if mode == 'check':
|
||||
return False
|
||||
if mode == 'delete':
|
||||
scroll_position = self.pw.verticalScrollBar().value()
|
||||
self.clear_annotations()
|
||||
self.load_annotations(chapter)
|
||||
self.pw.verticalScrollBar().setValue(scroll_position)
|
||||
|
||||
def update_model(self):
|
||||
# We're updating the underlying model to have real-time
|
||||
# updates on the read status
|
||||
|
||||
# Set a baseline model index in case the item gets deleted
|
||||
# E.g It's open in a tab and deleted from the library
|
||||
model_index = None
|
||||
start_index = self.main_window.lib_ref.libraryModel.index(0, 0)
|
||||
|
||||
# Find index of the model item that corresponds to the tab
|
||||
model_index = self.main_window.lib_ref.libraryModel.match(
|
||||
start_index,
|
||||
QtCore.Qt.UserRole + 6,
|
||||
self.pw.parent.metadata['hash'],
|
||||
1, QtCore.Qt.MatchExactly)
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
position_percentage = (
|
||||
self.pw.parent.metadata['position']['current_chapter'] /
|
||||
self.pw.parent.metadata['position']['total_chapters'])
|
||||
else:
|
||||
position_percentage = (
|
||||
self.pw.parent.metadata['position']['current_block'] /
|
||||
self.pw.parent.metadata['position']['total_blocks'])
|
||||
|
||||
# Update position percentage
|
||||
if model_index:
|
||||
self.main_window.lib_ref.libraryModel.setData(
|
||||
model_index[0], position_percentage, QtCore.Qt.UserRole + 7)
|
||||
|
||||
def generate_combo_box_action(self, contextMenu):
|
||||
contextMenu.addSeparator()
|
||||
|
||||
def set_toc_position(tocTree):
|
||||
currentIndex = tocTree.currentIndex()
|
||||
required_position = currentIndex.data(QtCore.Qt.UserRole)
|
||||
self.pw.parent.set_content(required_position, True, True)
|
||||
|
||||
# Create the Combobox / Treeview combination
|
||||
tocComboBox = QtWidgets.QComboBox()
|
||||
tocTree = QtWidgets.QTreeView()
|
||||
tocComboBox.setView(tocTree)
|
||||
tocComboBox.setModel(self.pw.parent.tocModel)
|
||||
tocTree.setRootIsDecorated(False)
|
||||
tocTree.setItemsExpandable(False)
|
||||
tocTree.expandAll()
|
||||
|
||||
# Set the position of the QComboBox
|
||||
self.pw.parent.set_tocBox_index(None, tocComboBox)
|
||||
|
||||
# Make clicking do something
|
||||
tocComboBox.currentIndexChanged.connect(
|
||||
lambda: set_toc_position(tocTree))
|
||||
|
||||
comboboxAction = QtWidgets.QWidgetAction(self.pw)
|
||||
comboboxAction.setDefaultWidget(tocComboBox)
|
||||
contextMenu.addAction(comboboxAction)
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,12 +17,15 @@
|
||||
import os
|
||||
import pickle
|
||||
import sqlite3
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseInit:
|
||||
def __init__(self, location_prefix):
|
||||
os.makedirs(location_prefix, exist_ok=True)
|
||||
self.database_path = os.path.join(location_prefix, 'Lector.db')
|
||||
|
||||
self.books_table_columns = {
|
||||
@@ -41,7 +42,8 @@ class DatabaseInit:
|
||||
'LastAccessed': 'BLOB',
|
||||
'Bookmarks': 'BLOB',
|
||||
'CoverImage': 'BLOB',
|
||||
'Addition': 'TEXT'}
|
||||
'Addition': 'TEXT',
|
||||
'Annotations': 'BLOB'}
|
||||
|
||||
self.directories_table_columns = {
|
||||
'id': 'INTEGER PRIMARY KEY',
|
||||
@@ -51,7 +53,7 @@ class DatabaseInit:
|
||||
'CheckState': 'INTEGER'}
|
||||
|
||||
if os.path.exists(self.database_path):
|
||||
self.check_database()
|
||||
self.check_columns()
|
||||
else:
|
||||
self.create_database()
|
||||
|
||||
@@ -70,7 +72,7 @@ class DatabaseInit:
|
||||
self.database.commit()
|
||||
self.database.close()
|
||||
|
||||
def check_database(self):
|
||||
def check_columns(self):
|
||||
self.database = sqlite3.connect(self.database_path)
|
||||
|
||||
database_return = self.database.execute("PRAGMA table_info(books)").fetchall()
|
||||
@@ -81,13 +83,13 @@ class DatabaseInit:
|
||||
for i in self.books_table_columns.items():
|
||||
if i[0] not in database_columns:
|
||||
commit_required = True
|
||||
print(f'Database: Adding column {i[0]}')
|
||||
info_string = f'Database: Adding column "{i[0]}"'
|
||||
logger.info(info_string)
|
||||
sql_command = f"ALTER TABLE books ADD COLUMN {i[0]} {i[1]}"
|
||||
self.database.execute(sql_command)
|
||||
|
||||
if commit_required:
|
||||
self.database.commit()
|
||||
self.database.close()
|
||||
|
||||
|
||||
class DatabaseFunctions:
|
||||
@@ -208,8 +210,9 @@ class DatabaseFunctions:
|
||||
else:
|
||||
return None
|
||||
|
||||
except (KeyError, sqlite3.OperationalError):
|
||||
print('SQLite is in wretched rebellion @ data fetching handling')
|
||||
except Exception as e:
|
||||
error_string = 'SQLite is in wretched rebellion @ data fetching handling'
|
||||
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
|
||||
|
||||
def fetch_covers_only(self, hash_list):
|
||||
parameter_marks = ','.join(['?' for i in hash_list])
|
||||
@@ -220,7 +223,7 @@ class DatabaseFunctions:
|
||||
|
||||
def modify_metadata(self, metadata_dict, book_hash):
|
||||
def generate_binary(column, data):
|
||||
if column in ('Position', 'LastAccessed', 'Bookmarks'):
|
||||
if column in ('Position', 'LastAccessed', 'Bookmarks', 'Annotations'):
|
||||
return sqlite3.Binary(pickle.dumps(data))
|
||||
elif column == 'CoverImage':
|
||||
return sqlite3.Binary(data)
|
||||
@@ -241,8 +244,9 @@ class DatabaseFunctions:
|
||||
try:
|
||||
self.database.execute(
|
||||
sql_command, update_data)
|
||||
except sqlite3.OperationalError:
|
||||
print('SQLite is in wretched rebellion @ metadata handling')
|
||||
except sqlite3.OperationalError as e:
|
||||
error_string = 'SQLite is in wretched rebellion @ metadata handling'
|
||||
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
|
||||
|
||||
self.database.commit()
|
||||
self.database.close()
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,8 +14,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import requests
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia
|
||||
import json
|
||||
import logging
|
||||
import urllib.request
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PyQt5 import QtMultimedia
|
||||
multimedia_available = True
|
||||
except ImportError:
|
||||
error_string = 'QtMultimedia not found. Sounds will not play.'
|
||||
logger.error(error_string)
|
||||
multimedia_available = False
|
||||
|
||||
from lector.resources import definitions
|
||||
|
||||
|
||||
@@ -37,8 +49,12 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
radius = 15
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
|
||||
try:
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
except TypeError: # Required for older versions of Qt
|
||||
pass
|
||||
|
||||
self.definitionView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
@@ -51,25 +67,35 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
self.pronunciation_mp3 = None
|
||||
|
||||
self.okButton.clicked.connect(self.hide)
|
||||
self.pronounceButton.clicked.connect(self.play_pronunciation)
|
||||
self.dialogBackground.clicked.connect(self.color_background)
|
||||
if multimedia_available:
|
||||
self.pronounceButton.clicked.connect(self.play_pronunciation)
|
||||
else:
|
||||
self.pronounceButton.setEnabled(False)
|
||||
|
||||
def api_call(self, url, word):
|
||||
language = self.parent.settings['dictionary_language']
|
||||
url = url + language + '/' + word.lower()
|
||||
|
||||
r = requests.get(
|
||||
url,
|
||||
headers={'app_id': self.app_id, 'app_key': self.app_key})
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header('app_id', self.app_id)
|
||||
req.add_header('app_key', self.app_key)
|
||||
|
||||
if r.status_code != 200:
|
||||
print('A firm nope on the dictionary finding thing')
|
||||
try:
|
||||
response = urllib.request.urlopen(req)
|
||||
if response.getcode() == 200:
|
||||
return_json = json.loads(response.read())
|
||||
return return_json
|
||||
except Exception as e:
|
||||
this_error = f'API access error'
|
||||
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
|
||||
self.parent.display_error_notification(None)
|
||||
return None
|
||||
|
||||
return r.json()
|
||||
|
||||
def find_definition(self, word):
|
||||
word_root_json = self.api_call(self.root_url, word)
|
||||
if not word_root_json:
|
||||
logger.error('Word root json noped out: ' + word)
|
||||
self.set_text(word, None, None, True)
|
||||
return
|
||||
|
||||
@@ -78,6 +104,8 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
|
||||
definition_json = self.api_call(self.define_url, word_root)
|
||||
if not definition_json:
|
||||
logger.error('Definition json noped out: ' + word_root)
|
||||
self.set_text(word, None, None, True)
|
||||
return
|
||||
|
||||
definitions = {}
|
||||
@@ -95,7 +123,7 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
this_definition = j['definitions'][0].capitalize()
|
||||
except KeyError:
|
||||
# The API also reports crossReferenceMarkers here
|
||||
pass
|
||||
this_definition = '<Not found>'
|
||||
|
||||
try:
|
||||
definitions[category].add(this_definition)
|
||||
@@ -137,18 +165,30 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
background = self.parent.settings['dialog_background']
|
||||
else:
|
||||
self.previous_position = self.pos()
|
||||
background = self.parent.get_color()
|
||||
self.parent.get_color()
|
||||
background = self.parent.settings['dialog_background']
|
||||
|
||||
# Calculate inverse color for the background so that
|
||||
# the text doesn't look blank
|
||||
r, g, b, alpha = background.getRgb()
|
||||
inv_average = 255 - (r + g + b) // 3
|
||||
if 100 < inv_average < 150:
|
||||
inv_average = 255
|
||||
|
||||
foreground = QtGui.QColor(
|
||||
inv_average, inv_average, inv_average, alpha)
|
||||
|
||||
self.setStyleSheet(
|
||||
"QDialog {{background-color: {0}}}".format(background.name()))
|
||||
self.definitionView.setStyleSheet(
|
||||
"QTextBrowser {{background-color: {0}}}".format(background.name()))
|
||||
"QTextBrowser {{color:{0}; background-color: {1}}}".format(
|
||||
foreground.name(), background.name()))
|
||||
|
||||
if not set_initial:
|
||||
self.show()
|
||||
|
||||
def play_pronunciation(self):
|
||||
if not self.pronunciation_mp3:
|
||||
if not self.pronunciation_mp3 or not multimedia_available:
|
||||
return
|
||||
|
||||
media_content = QtMultimedia.QMediaContent(
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,9 +14,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
from lector.resources import pie_chart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, temp_dir, parent=None):
|
||||
@@ -34,11 +37,7 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
|
||||
option = option.__class__(option)
|
||||
file_exists = index.data(QtCore.Qt.UserRole + 5)
|
||||
metadata = index.data(QtCore.Qt.UserRole + 3)
|
||||
|
||||
position = metadata['position']
|
||||
if position:
|
||||
is_read = position['is_read']
|
||||
position_percent = index.data(QtCore.Qt.UserRole + 7)
|
||||
|
||||
# The shadow pixmap currently is set to 420 x 600
|
||||
# Only draw the cover shadow in case the setting is enabled
|
||||
@@ -55,55 +54,20 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
if not file_exists:
|
||||
painter.setOpacity(.7)
|
||||
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
||||
read_icon = pie_chart.pixmapper(-1, None, None, 36)
|
||||
painter.setOpacity(1)
|
||||
read_icon = pie_chart.pixmapper(
|
||||
-1, None, self.parent.settings['consider_read_at'], 36)
|
||||
x_draw = option.rect.bottomRight().x() - 30
|
||||
y_draw = option.rect.bottomRight().y() - 35
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
painter.setOpacity(1)
|
||||
return
|
||||
|
||||
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
||||
if position:
|
||||
if is_read:
|
||||
current_chapter = total_chapters = 100
|
||||
else:
|
||||
try:
|
||||
current_chapter = position['current_chapter']
|
||||
total_chapters = position['total_chapters']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if position_percent:
|
||||
read_icon = pie_chart.pixmapper(
|
||||
current_chapter, total_chapters, self.temp_dir, 36)
|
||||
position_percent, self.temp_dir, self.parent.settings['consider_read_at'], 36)
|
||||
|
||||
x_draw = option.rect.bottomRight().x() - 30
|
||||
y_draw = option.rect.bottomRight().y() - 35
|
||||
if current_chapter != 1:
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
|
||||
|
||||
class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, parent=None):
|
||||
super(BookmarkDelegate, self).__init__(parent)
|
||||
self.parent = parent
|
||||
|
||||
def sizeHint(self, *args):
|
||||
dockwidget_width = self.parent.width() - 20
|
||||
return QtCore.QSize(dockwidget_width, 50)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
# TODO
|
||||
# Alignment of the painted item
|
||||
|
||||
option = option.__class__(option)
|
||||
|
||||
chapter_index = index.data(QtCore.Qt.UserRole)
|
||||
chapter_name = self.parent.window().bookToolBar.tocBox.itemText(chapter_index - 1)
|
||||
if len(chapter_name) > 25:
|
||||
chapter_name = chapter_name[:25] + '...'
|
||||
|
||||
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
||||
painter.drawText(
|
||||
option.rect,
|
||||
QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight | QtCore.Qt.TextWordWrap,
|
||||
' ' + chapter_name)
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
|
520
lector/dockwidgets.py
Normal file
@@ -0,0 +1,520 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
from lector.models import BookmarkProxyModel
|
||||
from lector.threaded import BackGroundTextSearch
|
||||
|
||||
|
||||
class PliantDockWidget(QtWidgets.QDockWidget):
|
||||
def __init__(self, main_window, notes_only, contentView, parent=None):
|
||||
super(PliantDockWidget, self).__init__(parent)
|
||||
self.main_window = main_window
|
||||
self.notes_only = notes_only
|
||||
self.contentView = contentView
|
||||
self.current_annotation = None
|
||||
self.parent = parent
|
||||
|
||||
# Models
|
||||
# The following models belong to the sideDock
|
||||
# bookmarkModel, bookmarkProxyModel
|
||||
# annotationModel
|
||||
# searchResultsModel
|
||||
self.bookmarkModel = None
|
||||
self.bookmarkProxyModel = None
|
||||
self.annotationModel = None
|
||||
self.searchResultsModel = None
|
||||
|
||||
# References
|
||||
# All widgets belong to these
|
||||
self.bookmarks = None
|
||||
self.annotations = None
|
||||
self.search = None
|
||||
|
||||
# Widgets
|
||||
# Except this one
|
||||
self.sideDockTabWidget = None
|
||||
|
||||
def showEvent(self, event=None):
|
||||
viewport_topRight = self.contentView.mapToGlobal(
|
||||
self.contentView.viewport().rect().topRight())
|
||||
|
||||
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
|
||||
dock_y = viewport_topRight.y()
|
||||
dock_height = self.contentView.viewport().size().height()
|
||||
|
||||
if self.notes_only:
|
||||
dock_width = dock_height = desktop_size.width() // 5.5
|
||||
dock_x = QtGui.QCursor.pos().x()
|
||||
dock_y = QtGui.QCursor.pos().y()
|
||||
else:
|
||||
dock_width = desktop_size.width() // 5
|
||||
dock_x = viewport_topRight.x() - dock_width + 1
|
||||
|
||||
self.main_window.active_docks.append(self)
|
||||
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
|
||||
|
||||
def hideEvent(self, event=None):
|
||||
if self.notes_only:
|
||||
annotationNoteEdit = self.findChild(QtWidgets.QTextEdit)
|
||||
if self.current_annotation:
|
||||
self.current_annotation['note'] = annotationNoteEdit.toPlainText()
|
||||
|
||||
try:
|
||||
self.main_window.active_docks.remove(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def set_annotation(self, annotation):
|
||||
self.current_annotation = annotation
|
||||
|
||||
def populate(self):
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.setTitleBarWidget(QtWidgets.QWidget(self)) # Removes titlebar
|
||||
self.sideDockTabWidget = QtWidgets.QTabWidget(self)
|
||||
self.setWidget(self.sideDockTabWidget)
|
||||
|
||||
# This order is important
|
||||
self.bookmarkModel = QtGui.QStandardItemModel(self)
|
||||
self.bookmarkProxyModel = BookmarkProxyModel(self)
|
||||
self.bookmarks = Bookmarks(self)
|
||||
self.bookmarks.generate_bookmark_model()
|
||||
|
||||
if not self.parent.are_we_doing_images_only:
|
||||
self.annotationModel = QtGui.QStandardItemModel(self)
|
||||
self.annotations = Annotations(self)
|
||||
self.annotations.generate_annotation_model()
|
||||
|
||||
self.searchResultsModel = QtGui.QStandardItemModel(self)
|
||||
self.search = Search(self)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.hide()
|
||||
# Ignoring this event prevents application closure
|
||||
# when everything is fullscreened
|
||||
event.ignore()
|
||||
|
||||
|
||||
# For the following classes, the parent is the sideDock
|
||||
# The parentTab is the parent... tab. So self.parent.parent
|
||||
class Bookmarks:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.parentTab = self.parent.parent
|
||||
self.bookmarkTreeView = QtWidgets.QTreeView(self.parent)
|
||||
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
self.bookmarks_string = self._translate('SideDock', 'Bookmarks')
|
||||
self.bookmark_default = self._translate('SideDock', 'New bookmark')
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
self.bookmarkTreeView.setHeaderHidden(True)
|
||||
self.bookmarkTreeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.bookmarkTreeView.customContextMenuRequested.connect(
|
||||
self.generate_bookmark_context_menu)
|
||||
self.bookmarkTreeView.clicked.connect(self.navigate_to_bookmark)
|
||||
|
||||
# Add widget to side dock
|
||||
self.parent.sideDockTabWidget.addTab(
|
||||
self.bookmarkTreeView, self.bookmarks_string)
|
||||
|
||||
def add_bookmark(self, position=None):
|
||||
identifier = uuid.uuid4().hex[:10]
|
||||
|
||||
if self.parentTab.are_we_doing_images_only:
|
||||
chapter = self.parentTab.metadata['position']['current_chapter']
|
||||
cursor_position = 0
|
||||
else:
|
||||
chapter, cursor_position = self.parent.contentView.record_position(True)
|
||||
if position: # Should be the case when called from the context menu
|
||||
cursor_position = position
|
||||
|
||||
self.parentTab.metadata['bookmarks'][identifier] = {
|
||||
'chapter': chapter,
|
||||
'cursor_position': cursor_position,
|
||||
'description': self.bookmark_default}
|
||||
|
||||
self.parent.setVisible(True)
|
||||
self.parent.sideDockTabWidget.setCurrentIndex(0)
|
||||
self.add_bookmark_to_model(
|
||||
self.bookmark_default, chapter, cursor_position, identifier, True)
|
||||
|
||||
def add_bookmark_to_model(
|
||||
self, description, chapter_number, cursor_position,
|
||||
identifier, new_bookmark=False):
|
||||
|
||||
def edit_new_bookmark(parent_item):
|
||||
new_child = parent_item.child(parent_item.rowCount() - 1, 0)
|
||||
source_index = self.parent.bookmarkModel.indexFromItem(new_child)
|
||||
edit_index = self.bookmarkTreeView.model().mapFromSource(source_index)
|
||||
self.parent.activateWindow()
|
||||
self.bookmarkTreeView.setFocus()
|
||||
self.bookmarkTreeView.setCurrentIndex(edit_index)
|
||||
self.bookmarkTreeView.edit(edit_index)
|
||||
|
||||
def get_chapter_name(chapter_number):
|
||||
for i in reversed(self.parentTab.metadata['toc']):
|
||||
if i[2] <= chapter_number:
|
||||
return i[1]
|
||||
return 'Unknown'
|
||||
|
||||
bookmark = QtGui.QStandardItem()
|
||||
bookmark.setData(False, QtCore.Qt.UserRole + 10) # Is Parent
|
||||
bookmark.setData(chapter_number, QtCore.Qt.UserRole) # Chapter number
|
||||
bookmark.setData(cursor_position, QtCore.Qt.UserRole + 1) # Cursor Position
|
||||
bookmark.setData(identifier, QtCore.Qt.UserRole + 2) # Identifier
|
||||
bookmark.setData(description, QtCore.Qt.DisplayRole) # Description
|
||||
bookmark_chapter_name = get_chapter_name(chapter_number)
|
||||
|
||||
for i in range(self.parent.bookmarkModel.rowCount()):
|
||||
parentIndex = self.parent.bookmarkModel.index(i, 0)
|
||||
parent_chapter_number = parentIndex.data(QtCore.Qt.UserRole)
|
||||
parent_chapter_name = parentIndex.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
# This prevents duplication of the bookmark in the new
|
||||
# navigation model
|
||||
if ((parent_chapter_number <= chapter_number) and
|
||||
(parent_chapter_name == bookmark_chapter_name)):
|
||||
bookmarkParent = self.parent.bookmarkModel.itemFromIndex(parentIndex)
|
||||
bookmarkParent.appendRow(bookmark)
|
||||
if new_bookmark:
|
||||
edit_new_bookmark(bookmarkParent)
|
||||
return
|
||||
|
||||
# In case no parent item exists
|
||||
bookmarkParent = QtGui.QStandardItem()
|
||||
bookmarkParent.setData(True, QtCore.Qt.UserRole + 10) # Is Parent
|
||||
bookmarkParent.setFlags(bookmarkParent.flags() & ~QtCore.Qt.ItemIsEditable) # Is Editable
|
||||
bookmarkParent.setData(get_chapter_name(chapter_number), QtCore.Qt.DisplayRole)
|
||||
bookmarkParent.setData(chapter_number, QtCore.Qt.UserRole)
|
||||
|
||||
bookmarkParent.appendRow(bookmark)
|
||||
self.parent.bookmarkModel.appendRow(bookmarkParent)
|
||||
if new_bookmark:
|
||||
edit_new_bookmark(bookmarkParent)
|
||||
|
||||
def navigate_to_bookmark(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
is_parent = self.parent.bookmarkProxyModel.data(
|
||||
index, QtCore.Qt.UserRole + 10)
|
||||
if is_parent:
|
||||
chapter_number = self.parent.bookmarkProxyModel.data(
|
||||
index, QtCore.Qt.UserRole)
|
||||
self.parentTab.set_content(chapter_number, True, True)
|
||||
return
|
||||
|
||||
chapter = self.parent.bookmarkProxyModel.data(
|
||||
index, QtCore.Qt.UserRole)
|
||||
cursor_position = self.parent.bookmarkProxyModel.data(
|
||||
index, QtCore.Qt.UserRole + 1)
|
||||
|
||||
self.parentTab.set_content(chapter, True, True)
|
||||
if not self.parentTab.are_we_doing_images_only:
|
||||
self.parentTab.set_cursor_position(cursor_position)
|
||||
|
||||
def generate_bookmark_model(self):
|
||||
for i in self.parentTab.metadata['bookmarks'].items():
|
||||
description = i[1]['description']
|
||||
chapter = i[1]['chapter']
|
||||
cursor_position = i[1]['cursor_position']
|
||||
identifier = i[0]
|
||||
self.add_bookmark_to_model(
|
||||
description, chapter, cursor_position, identifier)
|
||||
|
||||
self.generate_bookmark_proxy_model()
|
||||
|
||||
def generate_bookmark_proxy_model(self):
|
||||
self.parent.bookmarkProxyModel.setSourceModel(self.parent.bookmarkModel)
|
||||
self.parent.bookmarkProxyModel.setSortCaseSensitivity(False)
|
||||
self.parent.bookmarkProxyModel.setSortRole(QtCore.Qt.UserRole)
|
||||
self.parent.bookmarkProxyModel.sort(0)
|
||||
self.bookmarkTreeView.setModel(self.parent.bookmarkProxyModel)
|
||||
|
||||
def generate_bookmark_context_menu(self, position):
|
||||
index = self.bookmarkTreeView.indexAt(position)
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
is_parent = self.parent.bookmarkProxyModel.data(
|
||||
index, QtCore.Qt.UserRole + 10)
|
||||
if is_parent:
|
||||
return
|
||||
|
||||
bookmarkMenu = QtWidgets.QMenu()
|
||||
editAction = bookmarkMenu.addAction(
|
||||
self.parentTab.main_window.QImageFactory.get_image('edit-rename'),
|
||||
self._translate('Tab', 'Edit'))
|
||||
deleteAction = bookmarkMenu.addAction(
|
||||
self.parentTab.main_window.QImageFactory.get_image('trash-empty'),
|
||||
self._translate('Tab', 'Delete'))
|
||||
|
||||
action = bookmarkMenu.exec_(
|
||||
self.bookmarkTreeView.mapToGlobal(position))
|
||||
|
||||
if action == editAction:
|
||||
self.bookmarkTreeView.edit(index)
|
||||
|
||||
if action == deleteAction:
|
||||
child_index = self.parent.bookmarkProxyModel.mapToSource(index)
|
||||
parent_index = child_index.parent()
|
||||
child_rows = self.parent.bookmarkModel.itemFromIndex(
|
||||
parent_index).rowCount()
|
||||
delete_uuid = self.parent.bookmarkModel.data(
|
||||
child_index, QtCore.Qt.UserRole + 2)
|
||||
|
||||
self.parentTab.metadata['bookmarks'].pop(delete_uuid)
|
||||
|
||||
self.parent.bookmarkModel.removeRow(
|
||||
child_index.row(), child_index.parent())
|
||||
if child_rows == 1:
|
||||
self.parent.bookmarkModel.removeRow(parent_index.row())
|
||||
|
||||
|
||||
class Annotations:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.parentTab = self.parent.parent
|
||||
self.annotationListView = QtWidgets.QListView(self.parent)
|
||||
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
self.annotations_string = self._translate('SideDock', 'Annotations')
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
self.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
|
||||
self.annotationListView.doubleClicked.connect(
|
||||
self.parent.contentView.toggle_annotation_mode)
|
||||
|
||||
# Add widget to side dock
|
||||
self.parent.sideDockTabWidget.addTab(
|
||||
self.annotationListView, self.annotations_string)
|
||||
|
||||
def generate_annotation_model(self):
|
||||
# TODO
|
||||
# Annotation previews will require creation of a
|
||||
# QStyledItemDelegate
|
||||
|
||||
saved_annotations = self.parent.main_window.settings['annotations']
|
||||
if not saved_annotations:
|
||||
return
|
||||
|
||||
# Create annotation model
|
||||
for i in saved_annotations:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setText(i['name'])
|
||||
item.setData(i, QtCore.Qt.UserRole)
|
||||
self.parent.annotationModel.appendRow(item)
|
||||
self.annotationListView.setModel(self.parent.annotationModel)
|
||||
|
||||
|
||||
class Search:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.parentTab = self.parent.parent
|
||||
|
||||
self.searchThread = BackGroundTextSearch()
|
||||
self.searchOptionsLayout = QtWidgets.QHBoxLayout()
|
||||
self.searchTabLayout = QtWidgets.QVBoxLayout()
|
||||
self.searchTimer = QtCore.QTimer(self.parent)
|
||||
self.searchLineEdit = QtWidgets.QLineEdit(self.parent)
|
||||
self.searchBookButton = QtWidgets.QToolButton(self.parent)
|
||||
self.caseSensitiveSearchButton = QtWidgets.QToolButton(self.parent)
|
||||
self.matchWholeWordButton = QtWidgets.QToolButton(self.parent)
|
||||
self.searchResultsTreeView = QtWidgets.QTreeView(self.parent)
|
||||
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
self.search_string = self._translate('SideDock', 'Search')
|
||||
self.search_book_string = self._translate('SideDock', 'Search entire book')
|
||||
self.case_sensitive_string = self._translate('SideDock', 'Match case')
|
||||
self.match_word_string = self._translate('SideDock', 'Match word')
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
self.searchThread.finished.connect(self.generate_search_result_model)
|
||||
|
||||
self.searchTimer.setSingleShot(True)
|
||||
self.searchTimer.timeout.connect(self.set_search_options)
|
||||
|
||||
self.searchLineEdit.textChanged.connect(
|
||||
lambda: self.searchLineEdit.setStyleSheet(
|
||||
QtWidgets.QLineEdit.styleSheet(self.parent)))
|
||||
self.searchLineEdit.textChanged.connect(
|
||||
lambda: self.searchTimer.start(500))
|
||||
self.searchBookButton.clicked.connect(
|
||||
lambda: self.searchTimer.start(100))
|
||||
self.caseSensitiveSearchButton.clicked.connect(
|
||||
lambda: self.searchTimer.start(100))
|
||||
self.matchWholeWordButton.clicked.connect(
|
||||
lambda: self.searchTimer.start(100))
|
||||
|
||||
self.searchLineEdit.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.searchLineEdit.setClearButtonEnabled(True)
|
||||
self.searchLineEdit.setPlaceholderText(self.search_string)
|
||||
|
||||
self.searchBookButton.setIcon(
|
||||
self.parent.main_window.QImageFactory.get_image('view-readermode'))
|
||||
self.searchBookButton.setToolTip(self.search_book_string)
|
||||
self.searchBookButton.setCheckable(True)
|
||||
self.searchBookButton.setAutoRaise(True)
|
||||
self.searchBookButton.setIconSize(QtCore.QSize(20, 20))
|
||||
|
||||
self.caseSensitiveSearchButton.setIcon(
|
||||
self.parent.main_window.QImageFactory.get_image('search-case'))
|
||||
self.caseSensitiveSearchButton.setToolTip(self.case_sensitive_string)
|
||||
self.caseSensitiveSearchButton.setCheckable(True)
|
||||
self.caseSensitiveSearchButton.setAutoRaise(True)
|
||||
self.caseSensitiveSearchButton.setIconSize(QtCore.QSize(20, 20))
|
||||
|
||||
self.matchWholeWordButton.setIcon(
|
||||
self.parent.main_window.QImageFactory.get_image('search-word'))
|
||||
self.matchWholeWordButton.setToolTip(self.match_word_string)
|
||||
self.matchWholeWordButton.setCheckable(True)
|
||||
self.matchWholeWordButton.setAutoRaise(True)
|
||||
self.matchWholeWordButton.setIconSize(QtCore.QSize(20, 20))
|
||||
|
||||
self.searchOptionsLayout.setContentsMargins(0, 3, 0, 0)
|
||||
self.searchOptionsLayout.addWidget(self.searchLineEdit)
|
||||
self.searchOptionsLayout.addWidget(self.searchBookButton)
|
||||
self.searchOptionsLayout.addWidget(self.caseSensitiveSearchButton)
|
||||
self.searchOptionsLayout.addWidget(self.matchWholeWordButton)
|
||||
|
||||
self.searchResultsTreeView.setHeaderHidden(True)
|
||||
self.searchResultsTreeView.setEditTriggers(
|
||||
QtWidgets.QTreeView.NoEditTriggers)
|
||||
self.searchResultsTreeView.clicked.connect(
|
||||
self.navigate_to_search_result)
|
||||
|
||||
self.searchTabLayout.addLayout(self.searchOptionsLayout)
|
||||
self.searchTabLayout.addWidget(self.searchResultsTreeView)
|
||||
self.searchTabLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.searchTabWidget = QtWidgets.QWidget(self.parent)
|
||||
self.searchTabWidget.setLayout(self.searchTabLayout)
|
||||
|
||||
# Add widget to side dock
|
||||
self.parent.sideDockTabWidget.addTab(
|
||||
self.searchTabWidget, self.search_string)
|
||||
|
||||
def set_search_options(self):
|
||||
def generate_title_content_pair(required_chapters):
|
||||
title_content_list = []
|
||||
for i in self.parentTab.metadata['toc']:
|
||||
if i[2] in required_chapters:
|
||||
title_content_list.append(
|
||||
(i[1], self.parentTab.metadata['content'][i[2] - 1], i[2]))
|
||||
return title_content_list
|
||||
|
||||
# Select either the current chapter or all chapters
|
||||
# Function name is descriptive
|
||||
chapter_numbers = (self.parentTab.metadata['position']['current_chapter'],)
|
||||
if self.searchBookButton.isChecked():
|
||||
chapter_numbers = [i + 1 for i in range(len(self.parentTab.metadata['content']))]
|
||||
search_content = generate_title_content_pair(chapter_numbers)
|
||||
|
||||
self.searchThread.set_search_options(
|
||||
search_content,
|
||||
self.searchLineEdit.text(),
|
||||
self.caseSensitiveSearchButton.isChecked(),
|
||||
self.matchWholeWordButton.isChecked())
|
||||
self.searchThread.start()
|
||||
|
||||
def generate_search_result_model(self):
|
||||
self.parent.searchResultsModel.clear()
|
||||
search_results = self.searchThread.search_results
|
||||
for i in search_results:
|
||||
parentItem = QtGui.QStandardItem()
|
||||
parentItem.setData(True, QtCore.Qt.UserRole) # Is parent?
|
||||
parentItem.setData(i, QtCore.Qt.UserRole + 3) # Display text for label
|
||||
|
||||
for j in search_results[i]:
|
||||
childItem = QtGui.QStandardItem(parentItem)
|
||||
childItem.setData(False, QtCore.Qt.UserRole) # Is parent?
|
||||
childItem.setData(j[3], QtCore.Qt.UserRole + 1) # Chapter index
|
||||
childItem.setData(j[0], QtCore.Qt.UserRole + 2) # Cursor Position
|
||||
childItem.setData(j[1], QtCore.Qt.UserRole + 3) # Display text for label
|
||||
childItem.setData(j[2], QtCore.Qt.UserRole + 4) # Search term
|
||||
parentItem.appendRow(childItem)
|
||||
self.parent.searchResultsModel.appendRow(parentItem)
|
||||
|
||||
self.searchResultsTreeView.setModel(self.parent.searchResultsModel)
|
||||
self.searchResultsTreeView.expandToDepth(1)
|
||||
|
||||
# Reset stylesheet in case something is found
|
||||
if search_results:
|
||||
self.searchLineEdit.setStyleSheet(
|
||||
QtWidgets.QLineEdit.styleSheet(self.parent))
|
||||
|
||||
# Or set to Red in case nothing is found
|
||||
if not search_results and len(self.searchLineEdit.text()) > 2:
|
||||
self.searchLineEdit.setStyleSheet("QLineEdit {color: red;}")
|
||||
|
||||
# We'll be putting in labels instead of making a delegate
|
||||
# QLabels can understand RTF, and they also have the somewhat
|
||||
# distinct advantage of being a lot less work than a delegate
|
||||
|
||||
def generate_label(index):
|
||||
label_text = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 3)
|
||||
labelWidget = PliantLabelWidget(index, self.navigate_to_search_result)
|
||||
labelWidget.setText(label_text)
|
||||
self.searchResultsTreeView.setIndexWidget(index, labelWidget)
|
||||
|
||||
for parent_iter in range(self.parent.searchResultsModel.rowCount()):
|
||||
parentItem = self.parent.searchResultsModel.item(parent_iter)
|
||||
parentIndex = self.parent.searchResultsModel.index(parent_iter, 0)
|
||||
generate_label(parentIndex)
|
||||
|
||||
for child_iter in range(parentItem.rowCount()):
|
||||
childIndex = self.parent.searchResultsModel.index(child_iter, 0, parentIndex)
|
||||
generate_label(childIndex)
|
||||
|
||||
def navigate_to_search_result(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
is_parent = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole)
|
||||
if is_parent:
|
||||
return
|
||||
|
||||
chapter_number = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 1)
|
||||
cursor_position = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 2)
|
||||
search_term = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 4)
|
||||
|
||||
self.parentTab.set_content(chapter_number, True, True)
|
||||
if not self.parentTab.are_we_doing_images_only:
|
||||
self.parentTab.set_cursor_position(
|
||||
cursor_position, len(search_term))
|
||||
|
||||
|
||||
class PliantLabelWidget(QtWidgets.QLabel):
|
||||
# This is a hack to get clickable / editable appearance
|
||||
# search results in the tree view.
|
||||
|
||||
def __init__(self, index, navigate_to_search_result):
|
||||
super(PliantLabelWidget, self).__init__()
|
||||
self.index = index
|
||||
self.navigate_to_search_result = navigate_to_search_result
|
||||
|
||||
def mousePressEvent(self, QMouseEvent):
|
||||
self.navigate_to_search_result(self.index)
|
||||
QtWidgets.QLabel.mousePressEvent(self, QMouseEvent)
|
@@ -1,324 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
from urllib.parse import unquote
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class EPUB:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.zip_file = None
|
||||
self.book = {}
|
||||
self.book['split_chapters'] = {}
|
||||
|
||||
def read_epub(self):
|
||||
# This is the function that should error out in
|
||||
# case the module cannot process the file
|
||||
self.load_zip()
|
||||
contents_path = self.get_file_path(
|
||||
None, True)
|
||||
|
||||
if not contents_path:
|
||||
return False # No (valid) opf was found so processing cannot continue
|
||||
|
||||
self.generate_book_metadata(contents_path)
|
||||
self.parse_toc()
|
||||
|
||||
return True
|
||||
|
||||
def load_zip(self):
|
||||
try:
|
||||
self.zip_file = zipfile.ZipFile(
|
||||
self.filename, mode='r', allowZip64=True)
|
||||
except (KeyError, AttributeError, zipfile.BadZipFile):
|
||||
print('Cannot parse ' + self.filename)
|
||||
return
|
||||
|
||||
def parse_xml(self, filename, parser):
|
||||
try:
|
||||
this_xml = self.zip_file.read(filename).decode()
|
||||
except KeyError:
|
||||
short_filename = os.path.basename(self.filename)
|
||||
print(f'{str(filename)} not found in {short_filename}')
|
||||
return
|
||||
|
||||
root = BeautifulSoup(this_xml, parser)
|
||||
return root
|
||||
|
||||
def get_file_path(self, filename, is_content_file=False):
|
||||
# Use this to get the location of the content.opf file
|
||||
# And maybe some other file that has a more well formatted
|
||||
|
||||
# We're going to all this trouble because there really is
|
||||
# no going forward without a toc
|
||||
if is_content_file:
|
||||
container_location = self.get_file_path('container.xml')
|
||||
xml = self.parse_xml(container_location, 'xml')
|
||||
|
||||
if xml:
|
||||
root_item = xml.find('rootfile')
|
||||
try:
|
||||
return root_item.get('full-path')
|
||||
except AttributeError:
|
||||
print(f'ePub module: {self.filename} has a malformed container.xml')
|
||||
return None
|
||||
|
||||
possible_filenames = ('content.opf', 'package.opf')
|
||||
for i in possible_filenames:
|
||||
presumptive_location = self.get_file_path(i)
|
||||
if presumptive_location:
|
||||
return presumptive_location
|
||||
|
||||
for i in self.zip_file.filelist:
|
||||
if os.path.basename(i.filename) == os.path.basename(filename):
|
||||
return i.filename
|
||||
|
||||
return None
|
||||
|
||||
def read_from_zip(self, filename):
|
||||
filename = unquote(filename)
|
||||
try:
|
||||
file_data = self.zip_file.read(filename)
|
||||
return file_data
|
||||
except KeyError:
|
||||
file_path_actual = self.get_file_path(filename)
|
||||
if file_path_actual:
|
||||
return self.zip_file.read(file_path_actual)
|
||||
else:
|
||||
print('ePub module can\'t find ' + filename)
|
||||
|
||||
#______________________________________________________
|
||||
|
||||
def generate_book_metadata(self, contents_path):
|
||||
self.book['title'] = os.path.splitext(
|
||||
os.path.basename(self.filename))[0]
|
||||
self.book['author'] = 'Unknown'
|
||||
self.book['isbn'] = None
|
||||
self.book['tags'] = None
|
||||
self.book['cover'] = None
|
||||
self.book['toc_file'] = 'toc.ncx' # Overwritten if another one exists
|
||||
|
||||
# Parse XML
|
||||
xml = self.parse_xml(contents_path, 'xml')
|
||||
|
||||
# Parse metadata
|
||||
item_dict = {
|
||||
'title': 'title',
|
||||
'author': 'creator',
|
||||
'year': 'date'}
|
||||
|
||||
for i in item_dict.items():
|
||||
item = xml.find(i[1])
|
||||
if item:
|
||||
self.book[i[0]] = item.text
|
||||
|
||||
try:
|
||||
self.book['year'] = int(self.book['year'][:4])
|
||||
except (TypeError, KeyError, IndexError, ValueError):
|
||||
self.book['year'] = 9999
|
||||
|
||||
# Get identifier
|
||||
identifier_items = xml.find_all('identifier')
|
||||
for i in identifier_items:
|
||||
scheme = i.get('scheme')
|
||||
try:
|
||||
if scheme.lower() == 'isbn':
|
||||
self.book['isbn'] = i.text
|
||||
except AttributeError:
|
||||
self.book['isbn'] = None
|
||||
|
||||
# Tags
|
||||
tag_items = xml.find_all('subject')
|
||||
tag_list = [i.text for i in tag_items]
|
||||
self.book['tags'] = tag_list
|
||||
|
||||
# Get items
|
||||
self.book['content_dict'] = {}
|
||||
all_items = xml.find_all('item')
|
||||
for i in all_items:
|
||||
media_type = i.get('media-type')
|
||||
this_id = i.get('id')
|
||||
|
||||
if media_type == 'application/xhtml+xml' or media_type == 'text/html':
|
||||
self.book['content_dict'][this_id] = i.get('href')
|
||||
|
||||
if media_type == 'application/x-dtbncx+xml':
|
||||
self.book['toc_file'] = i.get('href')
|
||||
|
||||
# Cover image
|
||||
if 'cover' in this_id and media_type.split('/')[0] == 'image':
|
||||
cover_href = i.get('href')
|
||||
try:
|
||||
self.book['cover'] = self.zip_file.read(cover_href)
|
||||
except KeyError:
|
||||
# The cover cannot be found according to the
|
||||
# path specified in the content reference
|
||||
self.book['cover'] = self.zip_file.read(
|
||||
self.get_file_path(cover_href))
|
||||
|
||||
if not self.book['cover']:
|
||||
# If no cover is located the conventional way,
|
||||
# we go looking for the largest image in the book
|
||||
biggest_image_size = 0
|
||||
biggest_image = None
|
||||
for j in self.zip_file.filelist:
|
||||
if os.path.splitext(j.filename)[1] in ['.jpg', '.jpeg', '.png', '.gif']:
|
||||
if j.file_size > biggest_image_size:
|
||||
biggest_image = j.filename
|
||||
biggest_image_size = j.file_size
|
||||
|
||||
if biggest_image:
|
||||
self.book['cover'] = self.read_from_zip(biggest_image)
|
||||
else:
|
||||
print('No cover found for: ' + self.filename)
|
||||
|
||||
# Parse spine and arrange chapter paths acquired from the opf
|
||||
# according to the order IN THE SPINE
|
||||
spine_items = xml.find_all('itemref')
|
||||
spine_order = []
|
||||
for i in spine_items:
|
||||
spine_order.append(i.get('idref'))
|
||||
|
||||
self.book['chapters_in_order'] = []
|
||||
for i in spine_order:
|
||||
chapter_path = self.book['content_dict'][i]
|
||||
self.book['chapters_in_order'].append(chapter_path)
|
||||
|
||||
def parse_toc(self):
|
||||
# This has no bearing on the actual order
|
||||
# We're just using this to get chapter names
|
||||
self.book['navpoint_dict'] = {}
|
||||
|
||||
toc_file = self.book['toc_file']
|
||||
if toc_file:
|
||||
toc_file = self.get_file_path(toc_file)
|
||||
|
||||
xml = self.parse_xml(toc_file, 'xml')
|
||||
if not xml:
|
||||
return
|
||||
|
||||
navpoints = xml.find_all('navPoint')
|
||||
|
||||
for i in navpoints:
|
||||
chapter_title = i.find('text').text
|
||||
chapter_source = i.find('content').get('src')
|
||||
chapter_source_file = unquote(chapter_source.split('#')[0])
|
||||
|
||||
if '#' in chapter_source:
|
||||
try:
|
||||
self.book['split_chapters'][chapter_source_file].append(
|
||||
(chapter_source.split('#')[1], chapter_title))
|
||||
except KeyError:
|
||||
self.book['split_chapters'][chapter_source_file] = []
|
||||
self.book['split_chapters'][chapter_source_file].append(
|
||||
(chapter_source.split('#')[1], chapter_title))
|
||||
|
||||
self.book['navpoint_dict'][chapter_source_file] = chapter_title
|
||||
|
||||
def parse_chapters(self, temp_dir=None, split_large_xml=False):
|
||||
no_title_chapter = 0
|
||||
self.book['book_list'] = []
|
||||
|
||||
for i in self.book['chapters_in_order']:
|
||||
chapter_data = self.read_from_zip(i).decode()
|
||||
|
||||
if i in self.book['split_chapters'] and not split_large_xml:
|
||||
split_chapters = get_split_content(
|
||||
chapter_data, self.book['split_chapters'][i])
|
||||
self.book['book_list'].extend(split_chapters)
|
||||
|
||||
elif split_large_xml:
|
||||
# https://stackoverflow.com/questions/14444732/how-to-split-a-html-page-to-multiple-pages-using-python-and-beautiful-soup
|
||||
markup = BeautifulSoup(chapter_data, 'xml')
|
||||
chapters = []
|
||||
pagebreaks = markup.find_all('pagebreak')
|
||||
|
||||
def next_element(elem):
|
||||
while elem is not None:
|
||||
elem = elem.next_sibling
|
||||
if hasattr(elem, 'name'):
|
||||
return elem
|
||||
|
||||
for pbreak in pagebreaks:
|
||||
chapter = [str(pbreak)]
|
||||
elem = next_element(pbreak)
|
||||
while elem and elem.name != 'pagebreak':
|
||||
chapter.append(str(elem))
|
||||
elem = next_element(elem)
|
||||
chapters.append('\n'.join(chapter))
|
||||
|
||||
for this_chapter in chapters:
|
||||
fallback_title = str(no_title_chapter)
|
||||
self.book['book_list'].append(
|
||||
(fallback_title, this_chapter + ('<br/>' * 8)))
|
||||
no_title_chapter += 1
|
||||
else:
|
||||
try:
|
||||
self.book['book_list'].append(
|
||||
(self.book['navpoint_dict'][i], chapter_data + ('<br/>' * 8)))
|
||||
except KeyError:
|
||||
fallback_title = str(no_title_chapter)
|
||||
self.book['book_list'].append(
|
||||
(fallback_title, chapter_data))
|
||||
no_title_chapter += 1
|
||||
|
||||
cover_path = os.path.join(temp_dir, os.path.basename(self.filename)) + '- cover'
|
||||
if self.book['cover']:
|
||||
with open(cover_path, 'wb') as cover_temp:
|
||||
cover_temp.write(self.book['cover'])
|
||||
|
||||
try:
|
||||
self.book['book_list'][0] = (
|
||||
'Cover', f'<center><img src="{cover_path}" alt="Cover"></center>')
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def get_split_content(chapter_data, split_by):
|
||||
split_anchors = [i[0] for i in split_by]
|
||||
chapter_titles = [i[1] for i in split_by]
|
||||
return_list = []
|
||||
|
||||
xml = BeautifulSoup(chapter_data, 'lxml')
|
||||
xml_string = xml.body.prettify()
|
||||
|
||||
for count, i in enumerate(split_anchors):
|
||||
this_split = xml_string.split(i)
|
||||
current_chapter = this_split[0]
|
||||
|
||||
bs_obj = BeautifulSoup(current_chapter, 'lxml')
|
||||
# Since tags correspond to data following them, the first
|
||||
# chunk will be ignored
|
||||
# As will all empty chapters
|
||||
if bs_obj.text == '\n' or bs_obj.text == '' or count == 0:
|
||||
continue
|
||||
bs_obj_string = str(bs_obj).replace('">', '', 1) + ('<br/>' * 8)
|
||||
|
||||
return_list.append(
|
||||
(chapter_titles[count - 1], bs_obj_string))
|
||||
xml_string = this_split[1]
|
||||
|
||||
bs_obj = BeautifulSoup(xml_string, 'lxml')
|
||||
bs_obj_string = str(bs_obj).replace('">', '', 1) + ('<br/>' * 8)
|
||||
return_list.append(
|
||||
(chapter_titles[-1], bs_obj_string))
|
||||
|
||||
return return_list
|
@@ -1,7 +1,5 @@
|
||||
#!usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2018 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,9 +14,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from PyQt5 import QtGui
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from lector import database
|
||||
from lector.settings import Settings
|
||||
from lector.resources import resources
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QImageFactory:
|
||||
def __init__(self, parent):
|
||||
@@ -30,3 +35,316 @@ class QImageFactory:
|
||||
|
||||
this_qicon = QtGui.QIcon(icon_path)
|
||||
return this_qicon
|
||||
|
||||
|
||||
# For nearly all cases below, code remains unchanged from its
|
||||
# state in the __main__ module. References to objects have been
|
||||
# made in the respective __init__ functions of the classes here
|
||||
class CoverLoadingAndCulling:
|
||||
def __init__(self, main_window):
|
||||
self.main_window = main_window
|
||||
self.lib_ref = self.main_window.lib_ref
|
||||
self.listView = self.main_window.listView
|
||||
self.database_path = self.main_window.database_path
|
||||
|
||||
def cull_covers(self, event=None):
|
||||
blank_pixmap = QtGui.QPixmap()
|
||||
blank_pixmap.load(':/images/blank.png') # Keep this. Removing it causes the
|
||||
# listView to go blank on a resize
|
||||
|
||||
all_indexes = set()
|
||||
for i in range(self.lib_ref.itemProxyModel.rowCount()):
|
||||
all_indexes.add(self.lib_ref.itemProxyModel.index(i, 0))
|
||||
|
||||
y_range = list(range(0, self.listView.viewport().height(), 100))
|
||||
y_range.extend((-20, self.listView.viewport().height() + 20))
|
||||
x_range = range(0, self.listView.viewport().width(), 80)
|
||||
|
||||
visible_indexes = set()
|
||||
for i in y_range:
|
||||
for j in x_range:
|
||||
this_index = self.listView.indexAt(QtCore.QPoint(j, i))
|
||||
visible_indexes.add(this_index)
|
||||
|
||||
invisible_indexes = all_indexes - visible_indexes
|
||||
for i in invisible_indexes:
|
||||
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
|
||||
this_item = self.lib_ref.libraryModel.item(model_index.row())
|
||||
|
||||
if this_item:
|
||||
this_item.setIcon(QtGui.QIcon(blank_pixmap))
|
||||
this_item.setData(False, QtCore.Qt.UserRole + 8)
|
||||
|
||||
hash_index_dict = {}
|
||||
hash_list = []
|
||||
for i in visible_indexes:
|
||||
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
|
||||
|
||||
book_hash = self.lib_ref.libraryModel.data(
|
||||
model_index, QtCore.Qt.UserRole + 6)
|
||||
cover_displayed = self.lib_ref.libraryModel.data(
|
||||
model_index, QtCore.Qt.UserRole + 8)
|
||||
|
||||
if book_hash and not cover_displayed:
|
||||
hash_list.append(book_hash)
|
||||
hash_index_dict[book_hash] = model_index
|
||||
|
||||
all_covers = database.DatabaseFunctions(
|
||||
self.database_path).fetch_covers_only(hash_list)
|
||||
|
||||
for i in all_covers:
|
||||
book_hash = i[0]
|
||||
cover = i[1]
|
||||
model_index = hash_index_dict[book_hash]
|
||||
|
||||
book_item = self.lib_ref.libraryModel.item(model_index.row())
|
||||
self.cover_loader(book_item, cover)
|
||||
|
||||
def load_all_covers(self):
|
||||
all_covers_db = database.DatabaseFunctions(
|
||||
self.database_path).fetch_data(
|
||||
('Hash', 'CoverImage',),
|
||||
'books',
|
||||
{'Hash': ''},
|
||||
'LIKE')
|
||||
|
||||
if not all_covers_db:
|
||||
return
|
||||
|
||||
all_covers = {
|
||||
i[0]: i[1] for i in all_covers_db}
|
||||
|
||||
for i in range(self.lib_ref.libraryModel.rowCount()):
|
||||
this_item = self.lib_ref.libraryModel.item(i, 0)
|
||||
|
||||
is_cover_already_displayed = this_item.data(QtCore.Qt.UserRole + 8)
|
||||
if is_cover_already_displayed:
|
||||
continue
|
||||
|
||||
book_hash = this_item.data(QtCore.Qt.UserRole + 6)
|
||||
cover = all_covers[book_hash]
|
||||
self.cover_loader(this_item, cover)
|
||||
|
||||
def cover_loader(self, item, cover):
|
||||
img_pixmap = QtGui.QPixmap()
|
||||
if cover:
|
||||
img_pixmap.loadFromData(cover)
|
||||
else:
|
||||
img_pixmap.load(':/images/NotFound.png')
|
||||
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
|
||||
item.setIcon(QtGui.QIcon(img_pixmap))
|
||||
item.setData(True, QtCore.Qt.UserRole + 8)
|
||||
|
||||
|
||||
class ViewProfileModification:
|
||||
def __init__(self, main_window):
|
||||
self.main_window = main_window
|
||||
|
||||
self.listView = self.main_window.listView
|
||||
self.settings = self.main_window.settings
|
||||
self.bookToolBar = self.main_window.bookToolBar
|
||||
self.comic_profile = self.main_window.comic_profile
|
||||
self.tabWidget = self.main_window.tabWidget
|
||||
self.alignment_dict = self.main_window.alignment_dict
|
||||
|
||||
def get_color(self, signal_sender):
|
||||
def open_color_dialog(current_color):
|
||||
color_dialog = QtWidgets.QColorDialog()
|
||||
new_color = color_dialog.getColor(current_color)
|
||||
if new_color.isValid(): # Returned in case cancel is pressed
|
||||
return new_color
|
||||
else:
|
||||
return current_color
|
||||
|
||||
# Special cases that don't affect (comic)book display
|
||||
if signal_sender == 'libraryBackground':
|
||||
current_color = self.settings['listview_background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.listView.setStyleSheet("QListView {{background-color: {0}}}".format(
|
||||
new_color.name()))
|
||||
self.settings['listview_background'] = new_color
|
||||
return
|
||||
|
||||
if signal_sender == 'dialogBackground':
|
||||
current_color = self.settings['dialog_background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.settings['dialog_background'] = new_color
|
||||
return
|
||||
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
# Retain current values on opening a new dialog
|
||||
if signal_sender == 'fgColor':
|
||||
current_color = current_profile['foreground']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.bookToolBar.colorBoxFG.setStyleSheet(
|
||||
'background-color: %s' % new_color.name())
|
||||
current_profile['foreground'] = new_color
|
||||
|
||||
elif signal_sender == 'bgColor':
|
||||
current_color = current_profile['background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.bookToolBar.colorBoxBG.setStyleSheet(
|
||||
'background-color: %s' % new_color.name())
|
||||
current_profile['background'] = new_color
|
||||
|
||||
elif signal_sender == 'comicBGColor':
|
||||
current_color = self.comic_profile['background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.bookToolBar.comicBGColor.setStyleSheet(
|
||||
'background-color: %s' % new_color.name())
|
||||
self.comic_profile['background'] = new_color
|
||||
|
||||
self.bookToolBar.profileBox.setItemData(
|
||||
profile_index, current_profile, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
||||
def modify_font(self, signal_sender):
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
if signal_sender == 'fontBox':
|
||||
current_profile['font'] = self.bookToolBar.fontBox.currentFont().family()
|
||||
|
||||
if signal_sender == 'fontSizeBox':
|
||||
old_size = current_profile['font_size']
|
||||
new_size = self.bookToolBar.fontSizeBox.itemText(
|
||||
self.bookToolBar.fontSizeBox.currentIndex())
|
||||
if new_size.isdigit():
|
||||
current_profile['font_size'] = new_size
|
||||
else:
|
||||
current_profile['font_size'] = old_size
|
||||
|
||||
if signal_sender == 'lineSpacingUp' and current_profile['line_spacing'] < 200:
|
||||
current_profile['line_spacing'] += 5
|
||||
if signal_sender == 'lineSpacingDown' and current_profile['line_spacing'] > 90:
|
||||
current_profile['line_spacing'] -= 5
|
||||
|
||||
if signal_sender == 'paddingUp':
|
||||
current_profile['padding'] += 5
|
||||
if signal_sender == 'paddingDown':
|
||||
current_profile['padding'] -= 5
|
||||
|
||||
alignment_dict = {
|
||||
'alignLeft': 'left',
|
||||
'alignRight': 'right',
|
||||
'alignCenter': 'center',
|
||||
'alignJustify': 'justify'}
|
||||
if signal_sender in alignment_dict:
|
||||
current_profile['text_alignment'] = alignment_dict[signal_sender]
|
||||
|
||||
self.bookToolBar.profileBox.setItemData(
|
||||
profile_index, current_profile, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
||||
def modify_comic_view(self, signal_sender, key_pressed):
|
||||
comic_profile = self.main_window.comic_profile
|
||||
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
|
||||
|
||||
self.bookToolBar.fitWidth.setChecked(False)
|
||||
self.bookToolBar.bestFit.setChecked(False)
|
||||
self.bookToolBar.originalSize.setChecked(False)
|
||||
|
||||
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
|
||||
comic_profile['zoom_mode'] = 'manualZoom'
|
||||
comic_profile['padding'] += 50
|
||||
|
||||
# This prevents infinite zoom out
|
||||
if comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
|
||||
comic_profile['padding'] -= 50
|
||||
|
||||
if signal_sender == 'zoomIn' or key_pressed in (
|
||||
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
|
||||
comic_profile['zoom_mode'] = 'manualZoom'
|
||||
comic_profile['padding'] -= 50
|
||||
|
||||
# This prevents infinite zoom in
|
||||
if comic_profile['padding'] < 0:
|
||||
comic_profile['padding'] = 0
|
||||
|
||||
if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W:
|
||||
comic_profile['zoom_mode'] = 'fitWidth'
|
||||
comic_profile['padding'] = 0
|
||||
self.bookToolBar.fitWidth.setChecked(True)
|
||||
|
||||
# Padding in the following cases is decided by
|
||||
# the image pixmap loaded by the widget
|
||||
if signal_sender == 'bestFit' or key_pressed == QtCore.Qt.Key_B:
|
||||
comic_profile['zoom_mode'] = 'bestFit'
|
||||
self.bookToolBar.bestFit.setChecked(True)
|
||||
|
||||
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
|
||||
comic_profile['zoom_mode'] = 'originalSize'
|
||||
self.bookToolBar.originalSize.setChecked(True)
|
||||
|
||||
self.format_contentView()
|
||||
|
||||
def format_contentView(self):
|
||||
current_tab = self.tabWidget.currentWidget()
|
||||
|
||||
try:
|
||||
current_metadata = current_tab.metadata
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
if current_metadata['images_only']:
|
||||
background = self.comic_profile['background']
|
||||
zoom_mode = self.comic_profile['zoom_mode']
|
||||
|
||||
if zoom_mode == 'fitWidth':
|
||||
self.bookToolBar.fitWidth.setChecked(True)
|
||||
if zoom_mode == 'bestFit':
|
||||
self.bookToolBar.bestFit.setChecked(True)
|
||||
if zoom_mode == 'originalSize':
|
||||
self.bookToolBar.originalSize.setChecked(True)
|
||||
|
||||
self.bookToolBar.comicBGColor.setStyleSheet(
|
||||
'background-color: %s' % background.name())
|
||||
|
||||
current_tab.format_view(
|
||||
None, None, None, background, None, None, None)
|
||||
|
||||
else:
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
font = current_profile['font']
|
||||
foreground = current_profile['foreground']
|
||||
background = current_profile['background']
|
||||
padding = current_profile['padding']
|
||||
font_size = current_profile['font_size']
|
||||
line_spacing = current_profile['line_spacing']
|
||||
text_alignment = current_profile['text_alignment']
|
||||
|
||||
# Change toolbar widgets to match new settings
|
||||
self.bookToolBar.fontBox.blockSignals(True)
|
||||
self.bookToolBar.fontSizeBox.blockSignals(True)
|
||||
self.bookToolBar.fontBox.setCurrentText(font)
|
||||
current_index = self.bookToolBar.fontSizeBox.findText(
|
||||
str(font_size), QtCore.Qt.MatchExactly)
|
||||
self.bookToolBar.fontSizeBox.setCurrentIndex(current_index)
|
||||
self.bookToolBar.fontBox.blockSignals(False)
|
||||
self.bookToolBar.fontSizeBox.blockSignals(False)
|
||||
|
||||
self.alignment_dict[current_profile['text_alignment']].setChecked(True)
|
||||
|
||||
self.bookToolBar.colorBoxFG.setStyleSheet(
|
||||
'background-color: %s' % foreground.name())
|
||||
self.bookToolBar.colorBoxBG.setStyleSheet(
|
||||
'background-color: %s' % background.name())
|
||||
|
||||
current_tab.format_view(
|
||||
font, font_size, foreground,
|
||||
background, padding, line_spacing,
|
||||
text_alignment)
|
||||
|
||||
def reset_profile(self):
|
||||
current_profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile_default = Settings(self).default_profiles[current_profile_index]
|
||||
self.bookToolBar.profileBox.setItemData(
|
||||
current_profile_index, current_profile_default, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -18,27 +16,32 @@
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from PyQt5 import QtGui, QtCore
|
||||
|
||||
from lector import database
|
||||
from lector.models import TableProxyModel, ItemProxyModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Library:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.view_model = None
|
||||
self.item_proxy_model = None
|
||||
self.table_proxy_model = None
|
||||
self.main_window = parent
|
||||
self.libraryModel = None
|
||||
self.itemProxyModel = None
|
||||
self.tableProxyModel = None
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
def generate_model(self, mode, parsed_books=None, is_database_ready=True):
|
||||
if mode == 'build':
|
||||
self.view_model = QtGui.QStandardItemModel()
|
||||
self.view_model.setColumnCount(10)
|
||||
self.libraryModel = QtGui.QStandardItemModel()
|
||||
self.libraryModel.setColumnCount(10)
|
||||
|
||||
books = database.DatabaseFunctions(
|
||||
self.parent.database_path).fetch_data(
|
||||
self.main_window.database_path).fetch_data(
|
||||
('Title', 'Author', 'Year', 'DateAdded', 'Path',
|
||||
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed',
|
||||
'Addition'),
|
||||
@@ -47,11 +50,11 @@ class Library:
|
||||
'LIKE')
|
||||
|
||||
if not books:
|
||||
print('Database returned nothing')
|
||||
logger.warning('Database returned nothing')
|
||||
return
|
||||
|
||||
elif mode == 'addition':
|
||||
# Assumes self.view_model already exists and may be extended
|
||||
# Assumes self.libraryModel already exists and may be extended
|
||||
# Because any additional books have already been added to the
|
||||
# database using background threads
|
||||
|
||||
@@ -99,18 +102,22 @@ class Library:
|
||||
if position:
|
||||
position = pickle.loads(position)
|
||||
if position['is_read']:
|
||||
position_perc = 100
|
||||
position_perc = 1
|
||||
else:
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_chapter'] * 100 / position['total_chapters'])
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
position['current_block'] / position['total_blocks'])
|
||||
except (KeyError, ZeroDivisionError):
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_chapter'] / position['total_chapters'])
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
|
||||
try:
|
||||
file_exists = os.path.exists(path)
|
||||
except UnicodeEncodeError:
|
||||
print('Error with unicode encoding in the library module')
|
||||
print('Library: Unicode encoding error')
|
||||
|
||||
all_metadata = {
|
||||
'title': title,
|
||||
@@ -145,6 +152,7 @@ class Library:
|
||||
item.setToolTip(tooltip_string)
|
||||
|
||||
# Just keep the following order. It's way too much trouble otherwise
|
||||
# User roles have to be correlated to sorting order below
|
||||
item.setData(title, QtCore.Qt.UserRole)
|
||||
item.setData(author, QtCore.Qt.UserRole + 1)
|
||||
item.setData(year, QtCore.Qt.UserRole + 2)
|
||||
@@ -156,57 +164,60 @@ class Library:
|
||||
item.setData(False, QtCore.Qt.UserRole + 8) # Is the cover being displayed?
|
||||
item.setData(date_added, QtCore.Qt.UserRole + 9)
|
||||
item.setData(last_accessed, QtCore.Qt.UserRole + 12)
|
||||
item.setData(path, QtCore.Qt.UserRole + 13)
|
||||
item.setIcon(QtGui.QIcon(img_pixmap))
|
||||
|
||||
self.view_model.appendRow(item)
|
||||
self.libraryModel.appendRow(item)
|
||||
|
||||
# The is_database_ready boolean is required when a new thread sends
|
||||
# books here for model generation.
|
||||
if not self.parent.settings['perform_culling'] and is_database_ready:
|
||||
self.parent.load_all_covers()
|
||||
if not self.main_window.settings['perform_culling'] and is_database_ready:
|
||||
self.main_window.cover_functions.load_all_covers()
|
||||
|
||||
def generate_proxymodels(self):
|
||||
self.item_proxy_model = ItemProxyModel()
|
||||
self.item_proxy_model.setSourceModel(self.view_model)
|
||||
self.item_proxy_model.setSortCaseSensitivity(False)
|
||||
self.itemProxyModel = ItemProxyModel()
|
||||
self.itemProxyModel.setSourceModel(self.libraryModel)
|
||||
self.itemProxyModel.setSortCaseSensitivity(False)
|
||||
s = QtCore.QSize(160, 250) # Set icon sizing here
|
||||
self.parent.listView.setIconSize(s)
|
||||
self.parent.listView.setModel(self.item_proxy_model)
|
||||
self.main_window.listView.setIconSize(s)
|
||||
self.main_window.listView.setModel(self.itemProxyModel)
|
||||
|
||||
self.table_proxy_model = TableProxyModel(
|
||||
self.parent.temp_dir.path(), self.parent.tableView.horizontalHeader())
|
||||
self.table_proxy_model.setSourceModel(self.view_model)
|
||||
self.table_proxy_model.setSortCaseSensitivity(False)
|
||||
self.parent.tableView.setModel(self.table_proxy_model)
|
||||
self.tableProxyModel = TableProxyModel(
|
||||
self.main_window.temp_dir.path(),
|
||||
self.main_window.tableView.horizontalHeader(),
|
||||
self.main_window.settings['consider_read_at'])
|
||||
self.tableProxyModel.setSourceModel(self.libraryModel)
|
||||
self.tableProxyModel.setSortCaseSensitivity(False)
|
||||
self.main_window.tableView.setModel(self.tableProxyModel)
|
||||
|
||||
self.update_proxymodels()
|
||||
|
||||
def update_proxymodels(self):
|
||||
# Table proxy model
|
||||
self.table_proxy_model.invalidateFilter()
|
||||
self.table_proxy_model.setFilterParams(
|
||||
self.parent.libraryToolBar.searchBar.text(),
|
||||
self.parent.active_library_filters,
|
||||
self.tableProxyModel.invalidateFilter()
|
||||
self.tableProxyModel.setFilterParams(
|
||||
self.main_window.libraryToolBar.searchBar.text(),
|
||||
self.main_window.active_library_filters,
|
||||
0) # This doesn't need to know the sorting box position
|
||||
self.table_proxy_model.setFilterFixedString(
|
||||
self.parent.libraryToolBar.searchBar.text())
|
||||
self.tableProxyModel.setFilterFixedString(
|
||||
self.main_window.libraryToolBar.searchBar.text())
|
||||
# ^^^ This isn't needed, but it forces a model update every time the
|
||||
# text in the line edit changes. So I guess it is needed.
|
||||
self.table_proxy_model.sort_table_columns(
|
||||
self.parent.tableView.horizontalHeader().sortIndicatorSection())
|
||||
self.table_proxy_model.sort_table_columns()
|
||||
self.tableProxyModel.sort_table_columns(
|
||||
self.main_window.tableView.horizontalHeader().sortIndicatorSection())
|
||||
self.tableProxyModel.sort_table_columns()
|
||||
|
||||
# Item proxy model
|
||||
self.item_proxy_model.invalidateFilter()
|
||||
self.item_proxy_model.setFilterParams(
|
||||
self.parent.libraryToolBar.searchBar.text(),
|
||||
self.parent.active_library_filters,
|
||||
self.parent.libraryToolBar.sortingBox.currentIndex())
|
||||
self.item_proxy_model.setFilterFixedString(
|
||||
self.parent.libraryToolBar.searchBar.text())
|
||||
self.itemProxyModel.invalidateFilter()
|
||||
self.itemProxyModel.setFilterParams(
|
||||
self.main_window.libraryToolBar.searchBar.text(),
|
||||
self.main_window.active_library_filters,
|
||||
self.main_window.libraryToolBar.sortingBox.currentIndex())
|
||||
self.itemProxyModel.setFilterFixedString(
|
||||
self.main_window.libraryToolBar.searchBar.text())
|
||||
|
||||
self.parent.statusMessage.setText(
|
||||
str(self.item_proxy_model.rowCount()) +
|
||||
self.main_window.statusMessage.setText(
|
||||
str(self.itemProxyModel.rowCount()) +
|
||||
self._translate('Library', ' books'))
|
||||
|
||||
# TODO
|
||||
@@ -221,23 +232,25 @@ class Library:
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 9,
|
||||
4: 12}
|
||||
4: 12,
|
||||
5: 7}
|
||||
|
||||
# Sorting according to roles and the drop down in the library toolbar
|
||||
self.item_proxy_model.setSortRole(
|
||||
QtCore.Qt.UserRole + sort_roles[self.parent.libraryToolBar.sortingBox.currentIndex()])
|
||||
self.itemProxyModel.setSortRole(
|
||||
QtCore.Qt.UserRole +
|
||||
sort_roles[self.main_window.libraryToolBar.sortingBox.currentIndex()])
|
||||
|
||||
# This can be expanded to other fields by appending to the list
|
||||
sort_order = QtCore.Qt.AscendingOrder
|
||||
if self.parent.libraryToolBar.sortingBox.currentIndex() in [3, 4]:
|
||||
if self.main_window.libraryToolBar.sortingBox.currentIndex() in [3, 4, 5]:
|
||||
sort_order = QtCore.Qt.DescendingOrder
|
||||
|
||||
self.item_proxy_model.sort(0, sort_order)
|
||||
self.parent.start_culling_timer()
|
||||
self.itemProxyModel.sort(0, sort_order)
|
||||
self.main_window.start_culling_timer()
|
||||
|
||||
def generate_library_tags(self):
|
||||
db_library_directories = database.DatabaseFunctions(
|
||||
self.parent.database_path).fetch_data(
|
||||
self.main_window.database_path).fetch_data(
|
||||
('Path', 'Name', 'Tags'),
|
||||
'directories', # This checks the directories table NOT the book one
|
||||
{'Path': ''},
|
||||
@@ -249,7 +262,7 @@ class Library:
|
||||
|
||||
else:
|
||||
db_library_directories = database.DatabaseFunctions(
|
||||
self.parent.database_path).fetch_data(
|
||||
self.main_window.database_path).fetch_data(
|
||||
('Path',),
|
||||
'books', # THIS CHECKS THE BOOKS TABLE
|
||||
{'Path': ''},
|
||||
@@ -285,8 +298,8 @@ class Library:
|
||||
|
||||
# Generate tags for the QStandardItemModel
|
||||
# This isn't triggered for an empty view model
|
||||
for i in range(self.view_model.rowCount()):
|
||||
this_item = self.view_model.item(i, 0)
|
||||
for i in range(self.libraryModel.rowCount()):
|
||||
this_item = self.libraryModel.item(i, 0)
|
||||
all_metadata = this_item.data(QtCore.Qt.UserRole + 3)
|
||||
directory_name, directory_tags = get_tags(all_metadata)
|
||||
|
||||
@@ -301,8 +314,8 @@ class Library:
|
||||
invalid_paths = []
|
||||
deletable_persistent_indexes = []
|
||||
|
||||
for i in range(self.view_model.rowCount()):
|
||||
item = self.view_model.item(i)
|
||||
for i in range(self.libraryModel.rowCount()):
|
||||
item = self.libraryModel.item(i)
|
||||
|
||||
item_metadata = item.data(QtCore.Qt.UserRole + 3)
|
||||
book_path = item_metadata['path']
|
||||
@@ -310,7 +323,7 @@ class Library:
|
||||
addition_mode = item_metadata['addition_mode']
|
||||
except KeyError:
|
||||
addition_mode = 'automatic'
|
||||
print('Libary: Error setting addition mode for prune')
|
||||
logger.error('Libary: Error setting addition mode for prune')
|
||||
|
||||
if (book_path not in valid_paths and
|
||||
(addition_mode != 'manual' or addition_mode is None)):
|
||||
@@ -321,8 +334,8 @@ class Library:
|
||||
|
||||
if deletable_persistent_indexes:
|
||||
for i in deletable_persistent_indexes:
|
||||
self.view_model.removeRow(i.row())
|
||||
self.libraryModel.removeRow(i.row())
|
||||
|
||||
# Remove invalid paths from the database as well
|
||||
database.DatabaseFunctions(
|
||||
self.parent.database_path).delete_from_database('Path', invalid_paths)
|
||||
self.main_window.database_path).delete_from_database('Path', invalid_paths)
|
||||
|
58
lector/logger.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
VERSION = '0.5.0'
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
location_prefix = os.path.join(
|
||||
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation),
|
||||
'Lector')
|
||||
logger_filename = os.path.join(location_prefix, 'Lector.log')
|
||||
|
||||
|
||||
def init_logging(cli_arguments):
|
||||
# This needs a separate 'Lector' in the os.path.join because
|
||||
# application name isn't explicitly set in this module
|
||||
|
||||
os.makedirs(location_prefix, exist_ok=True)
|
||||
|
||||
log_level = 30 # Warning and above
|
||||
# Set log level according to command line arguments
|
||||
try:
|
||||
if cli_arguments[1] == 'debug':
|
||||
log_level = 10 # Debug and above
|
||||
print('Debug logging enabled')
|
||||
try:
|
||||
os.remove(logger_filename) # Remove old log for clarity
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# Create logging object
|
||||
logging.basicConfig(
|
||||
filename=logger_filename,
|
||||
filemode='a',
|
||||
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
|
||||
datefmt='%Y/%m/%d %H:%M:%S',
|
||||
level=log_level)
|
||||
logging.addLevelName(60, 'HAMMERTIME') ## Messages that MUST be logged
|
||||
|
||||
return logging.getLogger('lector.main')
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,12 +14,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from lector import database
|
||||
from lector.widgets import PliantQGraphicsScene
|
||||
from lector.resources import metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
def __init__(self, parent):
|
||||
@@ -38,8 +40,12 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
radius = 15
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
|
||||
try:
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
except TypeError: # Required for older versions of Qt
|
||||
pass
|
||||
|
||||
self.parent = parent
|
||||
self.database_path = self.parent.database_path
|
||||
@@ -86,7 +92,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
self.coverView.setScene(graphics_scene)
|
||||
|
||||
def ok_pressed(self, event=None):
|
||||
book_item = self.parent.lib_ref.view_model.item(self.book_index.row())
|
||||
book_item = self.parent.lib_ref.libraryModel.item(self.book_index.row())
|
||||
|
||||
title = self.titleLine.text()
|
||||
author = self.authorLine.text()
|
||||
@@ -116,7 +122,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
|
||||
if self.cover_for_database:
|
||||
database_dict['CoverImage'] = self.cover_for_database
|
||||
self.parent.cover_loader(
|
||||
self.parent.cover_functions.cover_loader(
|
||||
book_item, self.cover_for_database)
|
||||
|
||||
self.parent.lib_ref.update_proxymodels()
|
||||
@@ -148,7 +154,8 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
background = self.parent.settings['dialog_background']
|
||||
else:
|
||||
self.previous_position = self.pos()
|
||||
background = self.parent.get_color()
|
||||
self.parent.get_color()
|
||||
background = self.parent.settings['dialog_background']
|
||||
|
||||
self.setStyleSheet(
|
||||
"QDialog {{background-color: {0}}}".format(background.name()))
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,33 +14,32 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from lector.resources import pie_chart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BookmarkProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
super(BookmarkProxyModel, self).__init__(parent)
|
||||
self.parent = parent
|
||||
self.parentTab = self.parent.parent
|
||||
self.filter_text = None
|
||||
|
||||
def setFilterParams(self, filter_text):
|
||||
self.filter_text = filter_text
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
# TODO
|
||||
# Connect this to the search bar
|
||||
return True
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if role == QtCore.Qt.EditRole:
|
||||
source_index = self.mapToSource(index)
|
||||
identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2)
|
||||
|
||||
self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole)
|
||||
self.parent.metadata['bookmarks'][identifier]['description'] = value
|
||||
self.parentTab.metadata['bookmarks'][identifier]['description'] = value
|
||||
|
||||
return True
|
||||
|
||||
@@ -65,9 +62,10 @@ class ItemProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
|
||||
class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, temp_dir, tableViewHeader, parent=None):
|
||||
def __init__(self, temp_dir, tableViewHeader, consider_read_at, parent=None):
|
||||
super(TableProxyModel, self).__init__(parent)
|
||||
self.tableViewHeader = tableViewHeader
|
||||
self.consider_read_at = consider_read_at
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
title_string = self._translate('TableProxyModel', 'Title')
|
||||
@@ -100,7 +98,9 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
try:
|
||||
return self.header_data[column]
|
||||
except IndexError:
|
||||
print('Table proxy model: Can\'t find header for column', column)
|
||||
logger.error(
|
||||
'Table proxy model: Can\'t find header for column' + str(column))
|
||||
# The column will be called IndexError. Not a typo.
|
||||
return 'IndexError'
|
||||
|
||||
def flags(self, index):
|
||||
@@ -121,38 +121,22 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
return_pixmap = None
|
||||
|
||||
file_exists = item.data(QtCore.Qt.UserRole + 5)
|
||||
metadata = item.data(QtCore.Qt.UserRole + 3)
|
||||
position = metadata['position']
|
||||
if position:
|
||||
is_read = position['is_read']
|
||||
position_percent = item.data(QtCore.Qt.UserRole + 7)
|
||||
|
||||
if not file_exists:
|
||||
return pie_chart.pixmapper(
|
||||
-1, None, None, QtCore.Qt.SizeHintRole + 10)
|
||||
|
||||
if position:
|
||||
if is_read:
|
||||
current_chapter = total_chapters = 100
|
||||
else:
|
||||
try:
|
||||
current_chapter = position['current_chapter']
|
||||
total_chapters = position['total_chapters']
|
||||
|
||||
# TODO
|
||||
# See if there's any rationale for this
|
||||
if current_chapter == 1:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if position_percent:
|
||||
return_pixmap = pie_chart.pixmapper(
|
||||
current_chapter, total_chapters, self.temp_dir,
|
||||
position_percent, self.temp_dir,
|
||||
self.consider_read_at,
|
||||
QtCore.Qt.SizeHintRole + 10)
|
||||
|
||||
return return_pixmap
|
||||
|
||||
elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
if index.column() in (0, 5): # Cover and Status
|
||||
if index.column() in (0, 5): # Cover and Status
|
||||
return QtCore.QVariant()
|
||||
|
||||
if index.column() == 4:
|
||||
@@ -214,14 +198,20 @@ class ProxyModelsCommonFunctions:
|
||||
title = model.data(this_index, QtCore.Qt.UserRole)
|
||||
author = model.data(this_index, QtCore.Qt.UserRole + 1)
|
||||
tags = model.data(this_index, QtCore.Qt.UserRole + 4)
|
||||
progress = model.data(this_index, QtCore.Qt.UserRole + 7)
|
||||
directory_name = model.data(this_index, QtCore.Qt.UserRole + 10)
|
||||
directory_tags = model.data(this_index, QtCore.Qt.UserRole + 11)
|
||||
last_accessed = model.data(this_index, QtCore.Qt.UserRole + 12)
|
||||
file_path = model.data(this_index, QtCore.Qt.UserRole + 13)
|
||||
|
||||
# Hide untouched files when sorting by last accessed
|
||||
if self.parent_model.sorting_box_position == 4 and not last_accessed:
|
||||
return False
|
||||
|
||||
# Hide untouched files when sorting by progress
|
||||
if self.parent_model.sorting_box_position == 5 and not progress:
|
||||
return False
|
||||
|
||||
if self.parent_model.active_library_filters:
|
||||
if directory_name not in self.parent_model.active_library_filters:
|
||||
return False
|
||||
@@ -233,7 +223,9 @@ class ProxyModelsCommonFunctions:
|
||||
else:
|
||||
valid_data = [
|
||||
i.lower() for i in (
|
||||
title, author, tags, directory_name, directory_tags) if i is not None]
|
||||
title, author, tags, directory_name,
|
||||
directory_tags, file_path)
|
||||
if i is not None]
|
||||
for i in valid_data:
|
||||
if self.parent_model.filter_text.lower() in i:
|
||||
return True
|
||||
@@ -351,32 +343,3 @@ class MostExcellentFileSystemModel(QtWidgets.QFileSystemModel):
|
||||
|
||||
for i in deletable:
|
||||
del self.tag_data[i]
|
||||
|
||||
|
||||
# TODO
|
||||
# Unbork this
|
||||
class FileSystemProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
super(FileSystemProxyModel, self).__init__(parent)
|
||||
|
||||
def filterAcceptsRow(self, row_num, parent):
|
||||
model = self.sourceModel()
|
||||
filter_out = [
|
||||
'boot', 'dev', 'etc', 'lost+found', 'opt', 'pdb',
|
||||
'proc', 'root', 'run', 'srv', 'sys', 'tmp', 'twonky',
|
||||
'usr', 'var', 'bin', 'kdeinit5__0', 'lib', 'lib64', 'sbin']
|
||||
|
||||
name_index = model.index(row_num, 0)
|
||||
valid_data = model.data(name_index)
|
||||
|
||||
print(valid_data)
|
||||
|
||||
return True
|
||||
|
||||
try:
|
||||
if valid_data in filter_out:
|
||||
return False
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-18 BasioMeusPuga
|
||||
# Copyright (C) 2017-19 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -21,9 +19,14 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import zipfile
|
||||
import collections
|
||||
|
||||
from lector.rarfile import rarfile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParseCOMIC:
|
||||
def __init__(self, filename, *args):
|
||||
@@ -33,47 +36,45 @@ class ParseCOMIC:
|
||||
self.book_extension = os.path.splitext(self.filename)
|
||||
|
||||
def read_book(self):
|
||||
try:
|
||||
if self.book_extension[1] == '.cbz':
|
||||
self.book = zipfile.ZipFile(
|
||||
self.filename, mode='r', allowZip64=True)
|
||||
self.image_list = [i.filename for i in self.book.infolist() if not i.is_dir()]
|
||||
if self.book_extension[1] == '.cbz':
|
||||
self.book = zipfile.ZipFile(
|
||||
self.filename, mode='r', allowZip64=True)
|
||||
self.image_list = [
|
||||
i.filename for i in self.book.infolist()
|
||||
if not i.is_dir() and is_image(i.filename)]
|
||||
|
||||
elif self.book_extension[1] == '.cbr':
|
||||
self.book = rarfile.RarFile(self.filename)
|
||||
self.image_list = [i.filename for i in self.book.infolist() if not i.isdir()]
|
||||
elif self.book_extension[1] == '.cbr':
|
||||
self.book = rarfile.RarFile(self.filename)
|
||||
self.image_list = [
|
||||
i.filename for i in self.book.infolist()
|
||||
if not i.isdir() and is_image(i.filename)]
|
||||
|
||||
self.image_list.sort()
|
||||
except: # Specifying no exception here is warranted
|
||||
print('Cannot parse ' + self.filename)
|
||||
return
|
||||
self.image_list.sort()
|
||||
|
||||
def get_title(self):
|
||||
def generate_metadata(self):
|
||||
title = os.path.basename(self.book_extension[0]).strip(' ')
|
||||
return title
|
||||
author = '<Unknown>'
|
||||
isbn = None
|
||||
tags = []
|
||||
cover = self.book.read(self.image_list[0])
|
||||
|
||||
def get_author(self):
|
||||
return None
|
||||
|
||||
def get_year(self):
|
||||
creation_time = time.ctime(os.path.getctime(self.filename))
|
||||
creation_year = creation_time.split()[-1]
|
||||
return creation_year
|
||||
year = creation_time.split()[-1]
|
||||
|
||||
def get_cover_image(self):
|
||||
# The first image in the archive may not be the cover
|
||||
# It is implied, however, that the first image in order
|
||||
# will be the cover
|
||||
return self.book.read(self.image_list[0])
|
||||
Metadata = collections.namedtuple(
|
||||
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
|
||||
return Metadata(title, author, year, isbn, tags, cover)
|
||||
|
||||
def get_isbn(self):
|
||||
return None
|
||||
def generate_content(self):
|
||||
image_number = len(self.image_list)
|
||||
toc = [(1, f'Page {i + 1}', i + 1) for i in range(image_number)]
|
||||
|
||||
def get_tags(self):
|
||||
return None
|
||||
# Return toc, content, images_only
|
||||
return toc, self.image_list, True
|
||||
|
||||
def get_contents(self):
|
||||
file_settings = {'images_only': True}
|
||||
contents = [(f'Page {count + 1}', i) for count, i in enumerate(self.image_list)]
|
||||
|
||||
return contents, file_settings
|
||||
def is_image(filename):
|
||||
valid_image_extensions = ['.png', '.jpg', '.bmp']
|
||||
if os.path.splitext(filename)[1].lower() in valid_image_extensions:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,51 +14,43 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# TODO
|
||||
# Maybe also include book description
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import logging
|
||||
|
||||
from lector.ePub.read_epub import EPUB
|
||||
from lector.readers.read_epub import EPUB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParseEPUB:
|
||||
def __init__(self, filename, temp_dir, file_md5):
|
||||
# TODO
|
||||
# Maybe also include book description
|
||||
self.book_ref = None
|
||||
self.book = None
|
||||
self.filename = filename
|
||||
self.temp_dir = temp_dir
|
||||
self.extract_path = os.path.join(temp_dir, file_md5)
|
||||
|
||||
def read_book(self):
|
||||
self.book_ref = EPUB(self.filename)
|
||||
contents_found = self.book_ref.read_epub()
|
||||
if not contents_found:
|
||||
print('Cannot process: ' + self.filename)
|
||||
return
|
||||
self.book = self.book_ref.book
|
||||
self.book = EPUB(self.filename, self.temp_dir)
|
||||
|
||||
def get_title(self):
|
||||
return self.book['title']
|
||||
def generate_metadata(self):
|
||||
self.book.generate_metadata()
|
||||
return self.book.metadata
|
||||
|
||||
def get_author(self):
|
||||
return self.book['author']
|
||||
|
||||
def get_year(self):
|
||||
return self.book['year']
|
||||
|
||||
def get_cover_image(self):
|
||||
return self.book['cover']
|
||||
|
||||
def get_isbn(self):
|
||||
return self.book['isbn']
|
||||
|
||||
def get_tags(self):
|
||||
return self.book['tags']
|
||||
|
||||
def get_contents(self):
|
||||
def generate_content(self):
|
||||
zipfile.ZipFile(self.filename).extractall(self.extract_path)
|
||||
|
||||
self.book_ref.parse_chapters(temp_dir=self.extract_path)
|
||||
file_settings = {
|
||||
'images_only': False}
|
||||
return self.book['book_list'], file_settings
|
||||
self.book.generate_toc()
|
||||
self.book.generate_content()
|
||||
|
||||
toc = []
|
||||
content = []
|
||||
for count, i in enumerate(self.book.content):
|
||||
toc.append((i[0], i[1], count + 1))
|
||||
content.append(i[2])
|
||||
|
||||
# Return toc, content, images_only
|
||||
return toc, content, False
|
||||
|
52
lector/parsers/fb2.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# TODO
|
||||
# Maybe also include book description
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from lector.readers.read_fb2 import FB2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParseFB2:
|
||||
def __init__(self, filename, temp_dir, file_md5):
|
||||
self.book = None
|
||||
self.filename = filename
|
||||
self.extract_path = os.path.join(temp_dir, file_md5)
|
||||
|
||||
def read_book(self):
|
||||
self.book = FB2(self.filename)
|
||||
|
||||
def generate_metadata(self):
|
||||
self.book.generate_metadata()
|
||||
return self.book.metadata
|
||||
|
||||
def generate_content(self):
|
||||
os.makedirs(self.extract_path, exist_ok=True) # Manual creation is required here
|
||||
self.book.generate_content(temp_dir=self.extract_path)
|
||||
|
||||
toc = []
|
||||
content = []
|
||||
for count, i in enumerate(self.book.content):
|
||||
toc.append((i[0], i[1], count + 1))
|
||||
content.append(i[2])
|
||||
|
||||
# Return toc, content, images_only
|
||||
return toc, content, False
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,78 +14,69 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This module parses Amazon ebooks using KindleUnpack to first create an
|
||||
# epub that is then read the usual way
|
||||
# TODO
|
||||
# See if it's possible to just feed the
|
||||
# unzipped mobi7 file into the EPUB parser module
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import zipfile
|
||||
import logging
|
||||
|
||||
from lector.ePub.read_epub import EPUB
|
||||
from lector.readers.read_epub import EPUB
|
||||
import lector.KindleUnpack.kindleunpack as KindleUnpack
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParseMOBI:
|
||||
# This module parses Amazon ebooks using KindleUnpack to first create an
|
||||
# epub and then read the usual way
|
||||
|
||||
def __init__(self, filename, temp_dir, file_md5):
|
||||
self.book_ref = None
|
||||
self.book = None
|
||||
self.filename = filename
|
||||
self.epub_filepath = None
|
||||
self.split_large_xml = False
|
||||
self.temp_dir = temp_dir
|
||||
self.extract_dir = os.path.join(temp_dir, file_md5)
|
||||
self.extract_path = os.path.join(temp_dir, file_md5)
|
||||
|
||||
def read_book(self):
|
||||
with HidePrinting():
|
||||
KindleUnpack.unpackBook(self.filename, self.extract_dir)
|
||||
KindleUnpack.unpackBook(self.filename, self.extract_path)
|
||||
|
||||
epub_filename = os.path.splitext(
|
||||
os.path.basename(self.filename))[0] + '.epub'
|
||||
|
||||
self.epub_filepath = os.path.join(
|
||||
self.extract_dir, 'mobi8', epub_filename)
|
||||
self.extract_path, 'mobi8', epub_filename)
|
||||
|
||||
if not os.path.exists(self.epub_filepath):
|
||||
zip_dir = os.path.join(self.extract_dir, 'mobi7')
|
||||
zip_dir = os.path.join(self.extract_path, 'mobi7')
|
||||
zip_file = os.path.join(
|
||||
self.extract_dir, epub_filename)
|
||||
self.extract_path, epub_filename)
|
||||
self.epub_filepath = shutil.make_archive(zip_file, 'zip', zip_dir)
|
||||
self.split_large_xml = True
|
||||
|
||||
self.book_ref = EPUB(self.epub_filepath)
|
||||
contents_found = self.book_ref.read_epub()
|
||||
if not contents_found:
|
||||
print('Cannot process: ' + self.filename)
|
||||
return
|
||||
self.book = self.book_ref.book
|
||||
self.book = EPUB(self.epub_filepath, self.temp_dir)
|
||||
|
||||
def get_title(self):
|
||||
return self.book['title']
|
||||
def generate_metadata(self):
|
||||
self.book.generate_metadata()
|
||||
return self.book.metadata
|
||||
|
||||
def get_author(self):
|
||||
return self.book['author']
|
||||
def generate_content(self):
|
||||
zipfile.ZipFile(self.epub_filepath).extractall(self.extract_path)
|
||||
|
||||
def get_year(self):
|
||||
return self.book['year']
|
||||
self.book.generate_toc()
|
||||
self.book.generate_content()
|
||||
|
||||
def get_cover_image(self):
|
||||
return self.book['cover']
|
||||
toc = []
|
||||
content = []
|
||||
for count, i in enumerate(self.book.content):
|
||||
toc.append((1, i[1], count + 1))
|
||||
content.append(i[2])
|
||||
|
||||
def get_isbn(self):
|
||||
return self.book['isbn']
|
||||
# Return toc, content, images_only
|
||||
return toc, content, False
|
||||
|
||||
def get_tags(self):
|
||||
return self.book['tags']
|
||||
|
||||
def get_contents(self):
|
||||
extract_path = os.path.join(self.extract_dir)
|
||||
zipfile.ZipFile(self.epub_filepath).extractall(extract_path)
|
||||
|
||||
self.book_ref.parse_chapters(
|
||||
temp_dir=self.temp_dir, split_large_xml=self.split_large_xml)
|
||||
file_settings = {
|
||||
'images_only': False}
|
||||
return self.book['book_list'], file_settings
|
||||
|
||||
class HidePrinting:
|
||||
def __enter__(self):
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2018 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,90 +14,87 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import io
|
||||
import os
|
||||
from PyQt5 import QtCore
|
||||
from bs4 import BeautifulSoup
|
||||
import collections
|
||||
|
||||
import fitz
|
||||
from PyQt5 import QtGui
|
||||
|
||||
proceed = True
|
||||
try:
|
||||
import popplerqt5
|
||||
except ImportError:
|
||||
print('python-poppler-qt5 is not installed. Pdf files will not work.')
|
||||
proceed = False
|
||||
|
||||
class ParsePDF:
|
||||
def __init__(self, filename, *args):
|
||||
self.filename = filename
|
||||
self.book = None
|
||||
self.metadata = None
|
||||
|
||||
def read_book(self):
|
||||
if not proceed:
|
||||
return
|
||||
self.book = fitz.open(self.filename)
|
||||
|
||||
self.book = popplerqt5.Poppler.Document.load(self.filename)
|
||||
if not self.book:
|
||||
return
|
||||
def generate_metadata(self):
|
||||
title = self.book.metadata['title']
|
||||
if not title:
|
||||
title = os.path.splitext(os.path.basename(self.filename))[0]
|
||||
|
||||
self.metadata = BeautifulSoup(self.book.metadata(), 'xml')
|
||||
author = self.book.metadata['author']
|
||||
if not author:
|
||||
author = 'Unknown'
|
||||
|
||||
def get_title(self):
|
||||
creation_date = self.book.metadata['creationDate']
|
||||
try:
|
||||
title = self.metadata.find('title').text
|
||||
return title.replace('\n', '')
|
||||
except AttributeError:
|
||||
return os.path.splitext(os.path.basename(self.filename))[0]
|
||||
year = creation_date.split(':')[1][:4]
|
||||
except (ValueError, AttributeError):
|
||||
year = 9999
|
||||
|
||||
def get_author(self):
|
||||
try:
|
||||
author = self.metadata.find('creator').text
|
||||
return author.replace('\n', '')
|
||||
except AttributeError:
|
||||
return 'Unknown'
|
||||
isbn = None
|
||||
|
||||
def get_year(self):
|
||||
try:
|
||||
year = self.metadata.find('MetadataDate').text
|
||||
return year.replace('\n', '')
|
||||
except AttributeError:
|
||||
return 9999
|
||||
tags = self.book.metadata['keywords']
|
||||
if not tags:
|
||||
tags = []
|
||||
|
||||
def get_cover_image(self):
|
||||
self.book.setRenderHint(
|
||||
popplerqt5.Poppler.Document.Antialiasing
|
||||
and popplerqt5.Poppler.Document.TextAntialiasing)
|
||||
# This is a little roundabout for the cover
|
||||
# and I'm sure it's taking a performance hit
|
||||
# But it is simple. So there's that.
|
||||
cover_page = self.book.loadPage(0)
|
||||
# Disabling scaling gets the covers much faster
|
||||
cover = render_pdf_page(cover_page, True)
|
||||
|
||||
cover_page = self.book.page(0)
|
||||
cover_image = cover_page.renderToImage(300, 300)
|
||||
return resize_image(cover_image)
|
||||
Metadata = collections.namedtuple(
|
||||
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
|
||||
return Metadata(title, author, year, isbn, tags, cover)
|
||||
|
||||
def get_isbn(self):
|
||||
return None
|
||||
def generate_content(self):
|
||||
content = list(range(self.book.pageCount))
|
||||
toc = self.book.getToC()
|
||||
if not toc:
|
||||
toc = [(1, f'Page {i + 1}', i + 1) for i in range(self.book.pageCount)]
|
||||
|
||||
def get_tags(self):
|
||||
try:
|
||||
tags = self.metadata.find('Keywords').text
|
||||
return tags.replace('\n', '')
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_contents(self):
|
||||
file_settings = {'images_only': True}
|
||||
contents = [(f'Page {i + 1}', i) for i in range(self.book.numPages())]
|
||||
|
||||
return contents, file_settings
|
||||
# Return toc, content, images_only
|
||||
return toc, content, True
|
||||
|
||||
|
||||
def resize_image(cover_image):
|
||||
cover_image = cover_image.scaled(
|
||||
420, 600, QtCore.Qt.IgnoreAspectRatio)
|
||||
def render_pdf_page(page_data, for_cover=False):
|
||||
# Draw page contents on to a pixmap
|
||||
# and then return that pixmap
|
||||
|
||||
byte_array = QtCore.QByteArray()
|
||||
buffer = QtCore.QBuffer(byte_array)
|
||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
||||
cover_image.save(buffer, 'jpg', 75)
|
||||
# Render quality is set by the following
|
||||
zoom_matrix = fitz.Matrix(4, 4)
|
||||
if for_cover:
|
||||
zoom_matrix = fitz.Matrix(1, 1)
|
||||
|
||||
cover_image_final = io.BytesIO(byte_array)
|
||||
cover_image_final.seek(0)
|
||||
return cover_image_final.getvalue()
|
||||
pagePixmap = page_data.getPixmap(
|
||||
matrix=zoom_matrix,
|
||||
alpha=False) # Sets background to White
|
||||
imageFormat = QtGui.QImage.Format_RGB888 # Set to Format_RGB888 if alpha
|
||||
pageQImage = QtGui.QImage(
|
||||
pagePixmap.samples,
|
||||
pagePixmap.width,
|
||||
pagePixmap.height,
|
||||
pagePixmap.stride,
|
||||
imageFormat)
|
||||
|
||||
# The cover page doesn't require conversion into a Pixmap
|
||||
if for_cover:
|
||||
return pageQImage
|
||||
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.convertFromImage(pageQImage)
|
||||
return pixmap
|
||||
|
476
lector/readers/read_epub.py
Normal file
@@ -0,0 +1,476 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# TODO
|
||||
# See if inserting chapters not in the toc.ncx can be avoided
|
||||
# Account for stylesheets... eventually
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import logging
|
||||
import collections
|
||||
from urllib.parse import unquote
|
||||
|
||||
import xmltodict
|
||||
from PyQt5 import QtGui
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EPUB:
|
||||
def __init__(self, book_filename, temp_dir):
|
||||
self.book_filename = book_filename
|
||||
self.temp_dir = temp_dir
|
||||
|
||||
self.zip_file = None
|
||||
self.file_list = None
|
||||
self.opf_dict = None
|
||||
self.cover_image_name = None
|
||||
self.split_chapters = {}
|
||||
|
||||
self.metadata = None
|
||||
self.content = []
|
||||
|
||||
self.generate_references()
|
||||
|
||||
def generate_references(self):
|
||||
self.zip_file = zipfile.ZipFile(
|
||||
self.book_filename, mode='r', allowZip64=True)
|
||||
self.file_list = self.zip_file.namelist()
|
||||
|
||||
# Book structure relies on parsing the .opf file
|
||||
# in the book. Now that might be the usual content.opf
|
||||
# or package.opf or it might be named after your favorite
|
||||
# eldritch abomination. The point is we have to check
|
||||
# the container.xml
|
||||
container = self.find_file('container.xml')
|
||||
if container:
|
||||
container_xml = self.zip_file.read(container)
|
||||
container_dict = xmltodict.parse(container_xml)
|
||||
packagefile = container_dict['container']['rootfiles']['rootfile']['@full-path']
|
||||
else:
|
||||
presumptive_names = ('content.opf', 'package.opf', 'volume.opf')
|
||||
for i in presumptive_names:
|
||||
packagefile = self.find_file(i)
|
||||
if packagefile:
|
||||
logger.info('Using presumptive package file: ' + self.book_filename)
|
||||
break
|
||||
|
||||
packagefile_data = self.zip_file.read(packagefile)
|
||||
self.opf_dict = xmltodict.parse(packagefile_data)
|
||||
|
||||
def find_file(self, filename):
|
||||
# Get rid of special characters
|
||||
filename = unquote(filename)
|
||||
|
||||
# First, look for the file in the root of the book
|
||||
if filename in self.file_list:
|
||||
return filename
|
||||
|
||||
# Then search for it elsewhere
|
||||
else:
|
||||
file_basename = os.path.basename(filename)
|
||||
for i in self.file_list:
|
||||
if os.path.basename(i) == file_basename:
|
||||
return i
|
||||
|
||||
# If the file isn't found
|
||||
logger.error(filename + ' not found in ' + self.book_filename)
|
||||
return False
|
||||
|
||||
def generate_toc(self):
|
||||
def find_alternative_toc():
|
||||
toc_filename = None
|
||||
toc_filename_alternative = None
|
||||
manifest = self.opf_dict['package']['manifest']['item']
|
||||
|
||||
for i in manifest:
|
||||
# Behold the burning hoops we're jumping through
|
||||
if i['@id'] == 'ncx':
|
||||
toc_filename = i['@href']
|
||||
if ('ncx' in i['@id']) or ('toc' in i['@id']):
|
||||
toc_filename_alternative = i['@href']
|
||||
if toc_filename and toc_filename_alternative:
|
||||
break
|
||||
|
||||
if not toc_filename:
|
||||
if not toc_filename_alternative:
|
||||
logger.error('No ToC found for: ' + self.book_filename)
|
||||
else:
|
||||
toc_filename = toc_filename_alternative
|
||||
|
||||
logger.info('Using alternate ToC for: ' + self.book_filename)
|
||||
return toc_filename
|
||||
|
||||
# Find the toc.ncx file from the manifest
|
||||
# EPUBs will name literally anything, anything so try
|
||||
# a less stringent approach if the first one doesn't work
|
||||
# The idea is to prioritize 'toc.ncx' since this should work
|
||||
# for the vast majority of books
|
||||
toc_filename = 'toc.ncx'
|
||||
does_toc_exist = self.find_file(toc_filename)
|
||||
if not does_toc_exist:
|
||||
toc_filename = find_alternative_toc()
|
||||
|
||||
tocfile = self.find_file(toc_filename)
|
||||
tocfile_data = self.zip_file.read(tocfile)
|
||||
toc_dict = xmltodict.parse(tocfile_data)
|
||||
|
||||
def recursor(level, nav_node):
|
||||
if isinstance(nav_node, list):
|
||||
these_contents = [[
|
||||
level + 1,
|
||||
i['navLabel']['text'],
|
||||
i['content']['@src']] for i in nav_node]
|
||||
self.content.extend(these_contents)
|
||||
return
|
||||
|
||||
if 'navPoint' in nav_node.keys():
|
||||
recursor(level, nav_node['navPoint'])
|
||||
|
||||
else:
|
||||
self.content.append([
|
||||
level + 1,
|
||||
nav_node['navLabel']['text'],
|
||||
nav_node['content']['@src']])
|
||||
|
||||
navpoints = toc_dict['ncx']['navMap']['navPoint']
|
||||
for top_level_nav in navpoints:
|
||||
# Just one chapter
|
||||
if isinstance(top_level_nav, str):
|
||||
self.content.append([
|
||||
1,
|
||||
navpoints['navLabel']['text'],
|
||||
navpoints['content']['@src']])
|
||||
break
|
||||
|
||||
# Multiple chapters
|
||||
self.content.append([
|
||||
1,
|
||||
top_level_nav['navLabel']['text'],
|
||||
top_level_nav['content']['@src']])
|
||||
|
||||
if 'navPoint' in top_level_nav.keys():
|
||||
recursor(1, top_level_nav)
|
||||
|
||||
def get_chapter_content(self, chapter_file):
|
||||
this_file = self.find_file(chapter_file)
|
||||
if this_file:
|
||||
chapter_content = self.zip_file.read(this_file).decode()
|
||||
|
||||
# Generate a None return for a blank chapter
|
||||
# These will be removed from the contents later
|
||||
contentDocument = QtGui.QTextDocument(None)
|
||||
contentDocument.setHtml(chapter_content)
|
||||
contentText = contentDocument.toPlainText().replace('\n', '')
|
||||
if contentText == '':
|
||||
chapter_content = None
|
||||
|
||||
return chapter_content
|
||||
else:
|
||||
return 'Possible parse error: ' + chapter_file
|
||||
|
||||
def parse_split_chapters(self, chapters_with_split_content):
|
||||
# For split chapters, get the whole chapter first, then split
|
||||
# between ids using their anchors, then "heal" the resultant text
|
||||
# by creating a BeautifulSoup object. Write its str to the content
|
||||
for i in chapters_with_split_content.items():
|
||||
chapter_file = i[0]
|
||||
self.split_chapters[chapter_file] = {}
|
||||
|
||||
chapter_content = self.get_chapter_content(chapter_file)
|
||||
soup = BeautifulSoup(chapter_content, 'lxml')
|
||||
|
||||
split_anchors = i[1]
|
||||
for this_anchor in reversed(split_anchors):
|
||||
this_tag = soup.find(
|
||||
attrs={"id":lambda x: x == this_anchor})
|
||||
|
||||
markup_split = str(soup).split(str(this_tag))
|
||||
soup = BeautifulSoup(markup_split[0], 'lxml')
|
||||
|
||||
# If the tag is None, it probably means the content is overlapping
|
||||
# Skipping the insert is the way forward
|
||||
if this_tag:
|
||||
this_markup = BeautifulSoup(
|
||||
str(this_tag).strip() + markup_split[1], 'lxml')
|
||||
self.split_chapters[chapter_file][this_anchor] = str(this_markup)
|
||||
|
||||
# Remaining markup is assigned here
|
||||
self.split_chapters[chapter_file]['top_level'] = str(soup)
|
||||
|
||||
def generate_content(self):
|
||||
# Find all the chapters mentioned in the opf spine
|
||||
# These are simply ids that correspond to the actual item
|
||||
# as mentioned in the manifest - which is a comprehensive
|
||||
# list of files
|
||||
try:
|
||||
# Multiple chapters
|
||||
chapters_in_spine = [
|
||||
i['@idref']
|
||||
for i in self.opf_dict['package']['spine']['itemref']]
|
||||
except TypeError:
|
||||
# Single chapter - Large xml
|
||||
chapters_in_spine = [
|
||||
self.opf_dict['package']['spine']['itemref']['@idref']]
|
||||
|
||||
# Next, find items and ids from the manifest
|
||||
# This might error out in case there's only one item in
|
||||
# the manifest. Remember that for later.
|
||||
chapters_from_manifest = {
|
||||
i['@id']: i['@href']
|
||||
for i in self.opf_dict['package']['manifest']['item']}
|
||||
|
||||
# Finally, check which items are supposed to be in the spine
|
||||
# on the basis of the id and change the toc accordingly
|
||||
spine_final = []
|
||||
for i in chapters_in_spine:
|
||||
try:
|
||||
spine_final.append(chapters_from_manifest.pop(i))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
toc_chapters = [
|
||||
unquote(i[2].split('#')[0]) for i in self.content]
|
||||
|
||||
for i in spine_final:
|
||||
if not i in toc_chapters:
|
||||
spine_index = spine_final.index(i)
|
||||
if spine_index == 0: # Or chapter insertion circles back to the end
|
||||
previous_chapter_toc_index = -1
|
||||
else:
|
||||
previous_chapter = spine_final[spine_final.index(i) - 1]
|
||||
previous_chapter_toc_index = toc_chapters.index(previous_chapter)
|
||||
|
||||
toc_chapters.insert(
|
||||
previous_chapter_toc_index + 1, i)
|
||||
self.content.insert(
|
||||
previous_chapter_toc_index + 1, [1, None, i])
|
||||
|
||||
# Parse split chapters as below
|
||||
# They can be picked up during the iteration through the toc
|
||||
chapters_with_split_content = {}
|
||||
for i in self.content:
|
||||
if '#' in i[2]:
|
||||
this_split = i[2].split('#')
|
||||
chapter = this_split[0]
|
||||
anchor = this_split[1]
|
||||
|
||||
try:
|
||||
chapters_with_split_content[chapter].append(anchor)
|
||||
except KeyError:
|
||||
chapters_with_split_content[chapter] = []
|
||||
chapters_with_split_content[chapter].append(anchor)
|
||||
|
||||
self.parse_split_chapters(chapters_with_split_content)
|
||||
|
||||
# Now we iterate over the ToC as presented in the toc.ncx
|
||||
# and add chapters to the content list
|
||||
# In case a split chapter is encountered, get its content
|
||||
# from the split_chapters dictionary
|
||||
# What could possibly go wrong?
|
||||
toc_copy = self.content[:]
|
||||
|
||||
# Put the book into the book
|
||||
for count, i in enumerate(toc_copy):
|
||||
chapter_file = i[2]
|
||||
|
||||
# Get split content according to its corresponding id attribute
|
||||
if '#' in chapter_file:
|
||||
this_split = chapter_file.split('#')
|
||||
chapter_file_proper = this_split[0]
|
||||
this_anchor = this_split[1]
|
||||
|
||||
try:
|
||||
chapter_content = (
|
||||
self.split_chapters[chapter_file_proper][this_anchor])
|
||||
except KeyError:
|
||||
chapter_content = 'Parse Error'
|
||||
error_string = (
|
||||
f'Error parsing {self.book_filename}: {chapter_file_proper}')
|
||||
logger.error(error_string)
|
||||
|
||||
# Get content that remained at the end of the pillaging above
|
||||
elif chapter_file in self.split_chapters.keys():
|
||||
try:
|
||||
chapter_content = self.split_chapters[chapter_file]['top_level']
|
||||
except KeyError:
|
||||
chapter_content = 'Parse Error'
|
||||
error_string = (
|
||||
f'Error parsing {self.book_filename}: {chapter_file}')
|
||||
logger.error(error_string)
|
||||
|
||||
# Vanilla non split chapters
|
||||
else:
|
||||
chapter_content = self.get_chapter_content(chapter_file)
|
||||
|
||||
self.content[count][2] = chapter_content
|
||||
|
||||
# Cleanup content by removing null chapters
|
||||
unnamed_chapter_title = 1
|
||||
content_copy = []
|
||||
for i in self.content:
|
||||
if i[2]:
|
||||
chapter_title = i[1]
|
||||
if not chapter_title:
|
||||
chapter_title = unnamed_chapter_title
|
||||
content_copy.append((
|
||||
i[0], str(chapter_title), i[2]))
|
||||
unnamed_chapter_title += 1
|
||||
self.content = content_copy
|
||||
|
||||
# Get cover image and put it in its place
|
||||
# I imagine this involves saying nasty things to it
|
||||
# There's no point shifting this to the parser
|
||||
# The performance increase is negligible
|
||||
cover_image = self.generate_book_cover()
|
||||
|
||||
if cover_image:
|
||||
cover_path = os.path.join(
|
||||
self.temp_dir, os.path.basename(self.book_filename)) + ' - cover'
|
||||
with open(cover_path, 'wb') as cover_temp:
|
||||
cover_temp.write(cover_image)
|
||||
|
||||
# This is probably stupid, but I can't stand the idea of
|
||||
# having to look at two book covers
|
||||
cover_replacement_conditions = (
|
||||
self.cover_image_name.lower() + '.jpg' in self.content[0][2].lower(),
|
||||
self.cover_image_name.lower() + '.png' in self.content[0][2].lower(),
|
||||
'cover' in self.content[0][1].lower())
|
||||
|
||||
if True in cover_replacement_conditions:
|
||||
logger.info(
|
||||
f'Replacing cover {cover_replacement_conditions}: {self.book_filename}')
|
||||
self.content[0] = (
|
||||
1, 'Cover',
|
||||
f'<center><img src="{cover_path}" alt="Cover"></center>')
|
||||
else:
|
||||
logger.info('Adding cover: ' + self.book_filename)
|
||||
self.content.insert(
|
||||
0,
|
||||
(1, 'Cover',
|
||||
f'<center><img src="{cover_path}" alt="Cover"></center>'))
|
||||
|
||||
def generate_metadata(self):
|
||||
book_metadata = self.opf_dict['package']['metadata']
|
||||
|
||||
def flattener(this_object):
|
||||
if isinstance(this_object, collections.OrderedDict):
|
||||
return this_object['#text']
|
||||
|
||||
if isinstance(this_object, list):
|
||||
if isinstance(this_object[0], collections.OrderedDict):
|
||||
return this_object[0]['#text']
|
||||
else:
|
||||
return this_object[0]
|
||||
|
||||
if isinstance(this_object, str):
|
||||
return this_object
|
||||
|
||||
# There are no exception types specified below
|
||||
# This is on purpose and makes me long for the days
|
||||
# of simpler, happier things.
|
||||
|
||||
# Book title
|
||||
try:
|
||||
title = flattener(book_metadata['dc:title'])
|
||||
except:
|
||||
logger.warning('Title not found: ' + self.book_filename)
|
||||
title = os.path.splitext(
|
||||
os.path.basename(self.book_filename))[0]
|
||||
|
||||
# Book author
|
||||
try:
|
||||
author = flattener(book_metadata['dc:creator'])
|
||||
except:
|
||||
logger.warning('Author not found: ' + self.book_filename)
|
||||
author = 'Unknown'
|
||||
|
||||
# Book year
|
||||
try:
|
||||
year = int(flattener(book_metadata['dc:date'])[:4])
|
||||
except:
|
||||
logger.warning('Year not found: ' + self.book_filename)
|
||||
year = 9999
|
||||
|
||||
# Book isbn
|
||||
# Both one and multiple schema
|
||||
isbn = None
|
||||
try:
|
||||
scheme = book_metadata['dc:identifier']['@opf:scheme'].lower()
|
||||
if scheme.lower() == 'isbn':
|
||||
isbn = book_metadata['dc:identifier']['#text']
|
||||
|
||||
except (TypeError, KeyError):
|
||||
try:
|
||||
for i in book_metadata['dc:identifier']:
|
||||
if i['@opf:scheme'].lower() == 'isbn':
|
||||
isbn = i['#text']
|
||||
break
|
||||
except:
|
||||
logger.warning('ISBN not found: ' + self.book_filename)
|
||||
|
||||
# Book tags
|
||||
try:
|
||||
tags = book_metadata['dc:subject']
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
except:
|
||||
tags = []
|
||||
|
||||
# Book cover
|
||||
cover = self.generate_book_cover()
|
||||
|
||||
# Named tuple? Named tuple.
|
||||
Metadata = collections.namedtuple(
|
||||
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
|
||||
self.metadata = Metadata(title, author, year, isbn, tags, cover)
|
||||
|
||||
def generate_book_cover(self):
|
||||
# This is separate because the book cover needs to
|
||||
# be found and extracted both during addition / reading
|
||||
book_cover = None
|
||||
|
||||
try:
|
||||
cover_image = [
|
||||
i['@href'] for i in self.opf_dict['package']['manifest']['item']
|
||||
if i['@media-type'].split('/')[0] == 'image' and
|
||||
'cover' in i['@id']][0]
|
||||
book_cover = self.zip_file.read(self.find_file(cover_image))
|
||||
self.cover_image_name = os.path.splitext(
|
||||
os.path.basename(cover_image))[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Find book cover the hard way
|
||||
if not book_cover:
|
||||
biggest_image_size = 0
|
||||
biggest_image = None
|
||||
for j in self.zip_file.filelist:
|
||||
if os.path.splitext(j.filename)[1] in ['.jpg', '.jpeg', '.png', '.gif']:
|
||||
if j.file_size > biggest_image_size:
|
||||
biggest_image = j.filename
|
||||
biggest_image_size = j.file_size
|
||||
|
||||
if biggest_image:
|
||||
book_cover = self.zip_file.read(
|
||||
self.find_file(biggest_image))
|
||||
|
||||
if not book_cover:
|
||||
logger.warning('Cover not found: ' + self.book_filename)
|
||||
|
||||
return book_cover
|
176
lector/readers/read_fb2.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import base64
|
||||
import zipfile
|
||||
import logging
|
||||
import collections
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FB2:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.zip_file = None
|
||||
self.xml = None
|
||||
|
||||
self.metadata = None
|
||||
self.content = []
|
||||
|
||||
self.generate_references()
|
||||
|
||||
def generate_references(self):
|
||||
if self.filename.endswith('.fb2.zip'):
|
||||
this_book = zipfile.ZipFile(
|
||||
self.filename, mode='r', allowZip64=True)
|
||||
for i in this_book.filelist:
|
||||
if os.path.splitext(i.filename)[1] == '.fb2':
|
||||
book_text = this_book.read(i.filename)
|
||||
break
|
||||
|
||||
else:
|
||||
with open(self.filename, 'r') as book_file:
|
||||
book_text = book_file.read()
|
||||
|
||||
self.xml = BeautifulSoup(book_text, 'lxml')
|
||||
|
||||
def generate_metadata(self):
|
||||
# All metadata can be parsed in one pass
|
||||
all_tags = self.xml.find('description')
|
||||
|
||||
title = all_tags.find('book-title').text
|
||||
if title == '' or title is None:
|
||||
title = os.path.splitext(
|
||||
os.path.basename(self.filename))[0]
|
||||
|
||||
author = all_tags.find(
|
||||
'author').getText(separator=' ').replace('\n', ' ')
|
||||
if author == '' or author is None:
|
||||
author = '<Unknown>'
|
||||
else:
|
||||
author = author.strip()
|
||||
|
||||
# TODO
|
||||
# Account for other date formats
|
||||
try:
|
||||
year = int(all_tags.find('date').text)
|
||||
except ValueError:
|
||||
year = 9999
|
||||
|
||||
isbn = None
|
||||
tags = None
|
||||
|
||||
cover = self.generate_book_cover()
|
||||
|
||||
Metadata = collections.namedtuple(
|
||||
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
|
||||
self.metadata = Metadata(title, author, year, isbn, tags, cover)
|
||||
|
||||
def generate_content(self, temp_dir):
|
||||
# TODO
|
||||
# Check what's up with recursion levels
|
||||
# Why is the TypeError happening in get_title
|
||||
|
||||
def get_title(element):
|
||||
this_title = '<No title>'
|
||||
title_xml = '<No title xml>'
|
||||
try:
|
||||
for i in element:
|
||||
if i.name == 'title':
|
||||
this_title = i.getText(separator=' ')
|
||||
this_title = this_title.replace('\n', '').strip()
|
||||
title_xml = str(i.unwrap())
|
||||
break
|
||||
except TypeError:
|
||||
return None, None
|
||||
return this_title, title_xml
|
||||
|
||||
def recursor(level, element):
|
||||
children = element.findChildren('section', recursive=False)
|
||||
if not children and level != 1:
|
||||
this_title, title_xml = get_title(element)
|
||||
self.content.append(
|
||||
[level, this_title, title_xml + str(element)])
|
||||
else:
|
||||
for i in children:
|
||||
recursor(level + 1, i)
|
||||
|
||||
first_element = self.xml.find('section') # Recursive find
|
||||
siblings = list(first_element.findNextSiblings('section', recursive=False))
|
||||
siblings.insert(0, first_element)
|
||||
|
||||
for this_element in siblings:
|
||||
this_title, title_xml = get_title(this_element)
|
||||
# Do not add chapter content in case it has sections
|
||||
# inside it. This prevents having large Book sections that
|
||||
# have duplicated content
|
||||
section_children = this_element.findChildren('section')
|
||||
chapter_text = str(this_element)
|
||||
if section_children:
|
||||
chapter_text = this_title
|
||||
|
||||
self.content.append([1, this_title, chapter_text])
|
||||
recursor(1, this_element)
|
||||
|
||||
# Extract all images to the temp_dir
|
||||
for i in self.xml.find_all('binary'):
|
||||
image_name = i.get('id')
|
||||
image_path = os.path.join(temp_dir, image_name)
|
||||
image_string = f'<image l:href="#{image_name}"'
|
||||
replacement_string = f'<p></p><img src=\"{image_path}\"'
|
||||
|
||||
for j in self.content:
|
||||
j[2] = j[2].replace(
|
||||
image_string, replacement_string)
|
||||
try:
|
||||
image_data = base64.decodebytes(i.text.encode())
|
||||
with open(image_path, 'wb') as outimage:
|
||||
outimage.write(image_data)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Insert the book cover at the beginning
|
||||
cover_image = self.generate_book_cover()
|
||||
if cover_image:
|
||||
cover_path = os.path.join(
|
||||
temp_dir, os.path.basename(self.filename)) + ' - cover'
|
||||
with open(cover_path, 'wb') as cover_temp:
|
||||
cover_temp.write(cover_image)
|
||||
|
||||
self.content.insert(
|
||||
0, (1, 'Cover', f'<center><img src="{cover_path}" alt="Cover"></center>'))
|
||||
|
||||
def generate_book_cover(self):
|
||||
cover = None
|
||||
|
||||
try:
|
||||
cover_image_xml = self.xml.find('coverpage')
|
||||
for i in cover_image_xml:
|
||||
cover_image_name = i.get('l:href')
|
||||
|
||||
cover_image_data = self.xml.find_all('binary')
|
||||
for i in cover_image_data:
|
||||
if cover_image_name.endswith(i.get('id')):
|
||||
cover = base64.decodebytes(i.text.encode())
|
||||
except (AttributeError, TypeError):
|
||||
# Catch TypeError in case no images exist in the book
|
||||
logger.warning('Cover not found: ' + self.filename)
|
||||
|
||||
return cover
|
@@ -10,5 +10,7 @@
|
||||
<p>Author: BasioMeusPuga <a href="mailto:disgruntled.mob@gmail.com">disgruntled.mob@gmail.com</a></p>
|
||||
<p>Page: <a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p>
|
||||
<p>License: GPLv3 <a href="https://www.gnu.org/licenses/gpl-3.0.en.html">https://www.gnu.org/licenses/gpl-3.0.en.html</a></p>
|
||||
<p>Donate (Paypal): <a href="https://www.paypal.me/supportlector">https://www.paypal.me/supportlector</p>
|
||||
<p>Donate (Bitcoin): 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro</p>
|
||||
<p> </p></body>
|
||||
</html>
|
||||
|
146
lector/resources/annotationswindow.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'raw/annotations.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.10.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
Dialog.setObjectName("Dialog")
|
||||
Dialog.resize(306, 387)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Dialog)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
self.nameEdit = QtWidgets.QLineEdit(Dialog)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.nameEdit.sizePolicy().hasHeightForWidth())
|
||||
self.nameEdit.setSizePolicy(sizePolicy)
|
||||
self.nameEdit.setObjectName("nameEdit")
|
||||
self.horizontalLayout_2.addWidget(self.nameEdit)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_2)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.typeLabel = QtWidgets.QLabel(Dialog)
|
||||
self.typeLabel.setObjectName("typeLabel")
|
||||
self.horizontalLayout.addWidget(self.typeLabel)
|
||||
self.typeBox = QtWidgets.QComboBox(Dialog)
|
||||
self.typeBox.setObjectName("typeBox")
|
||||
self.horizontalLayout.addWidget(self.typeBox)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.stackedWidget = QtWidgets.QStackedWidget(Dialog)
|
||||
self.stackedWidget.setObjectName("stackedWidget")
|
||||
self.page = QtWidgets.QWidget()
|
||||
self.page.setObjectName("page")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.page)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.verticalLayout_12 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_12.setObjectName("verticalLayout_12")
|
||||
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
|
||||
self.foregroundCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.foregroundCheck.setObjectName("foregroundCheck")
|
||||
self.horizontalLayout_15.addWidget(self.foregroundCheck)
|
||||
self.foregroundColorButton = QtWidgets.QPushButton(self.page)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.foregroundColorButton.sizePolicy().hasHeightForWidth())
|
||||
self.foregroundColorButton.setSizePolicy(sizePolicy)
|
||||
self.foregroundColorButton.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.foregroundColorButton.setMaximumSize(QtCore.QSize(45, 40))
|
||||
self.foregroundColorButton.setText("")
|
||||
self.foregroundColorButton.setObjectName("foregroundColorButton")
|
||||
self.horizontalLayout_15.addWidget(self.foregroundColorButton)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_15)
|
||||
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
|
||||
self.highlightCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.highlightCheck.setObjectName("highlightCheck")
|
||||
self.horizontalLayout_16.addWidget(self.highlightCheck)
|
||||
self.highlightColorButton = QtWidgets.QPushButton(self.page)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.highlightColorButton.sizePolicy().hasHeightForWidth())
|
||||
self.highlightColorButton.setSizePolicy(sizePolicy)
|
||||
self.highlightColorButton.setMinimumSize(QtCore.QSize(30, 24))
|
||||
self.highlightColorButton.setMaximumSize(QtCore.QSize(45, 40))
|
||||
self.highlightColorButton.setText("")
|
||||
self.highlightColorButton.setObjectName("highlightColorButton")
|
||||
self.horizontalLayout_16.addWidget(self.highlightColorButton)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_16)
|
||||
self.horizontalLayout_17 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_17.setObjectName("horizontalLayout_17")
|
||||
self.boldCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.boldCheck.setObjectName("boldCheck")
|
||||
self.horizontalLayout_17.addWidget(self.boldCheck)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_17)
|
||||
self.horizontalLayout_18 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_18.setObjectName("horizontalLayout_18")
|
||||
self.italicCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.italicCheck.setObjectName("italicCheck")
|
||||
self.horizontalLayout_18.addWidget(self.italicCheck)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_18)
|
||||
self.horizontalLayout_19 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_19.setObjectName("horizontalLayout_19")
|
||||
self.underlineCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.underlineCheck.setObjectName("underlineCheck")
|
||||
self.horizontalLayout_19.addWidget(self.underlineCheck)
|
||||
self.underlineType = QtWidgets.QComboBox(self.page)
|
||||
self.underlineType.setObjectName("underlineType")
|
||||
self.horizontalLayout_19.addWidget(self.underlineType)
|
||||
self.underlineColorButton = QtWidgets.QPushButton(self.page)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.underlineColorButton.sizePolicy().hasHeightForWidth())
|
||||
self.underlineColorButton.setSizePolicy(sizePolicy)
|
||||
self.underlineColorButton.setMinimumSize(QtCore.QSize(45, 24))
|
||||
self.underlineColorButton.setMaximumSize(QtCore.QSize(45, 40))
|
||||
self.underlineColorButton.setText("")
|
||||
self.underlineColorButton.setObjectName("underlineColorButton")
|
||||
self.horizontalLayout_19.addWidget(self.underlineColorButton)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_19)
|
||||
self.gridLayout_2.addLayout(self.verticalLayout_12, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.page)
|
||||
self.verticalLayout.addWidget(self.stackedWidget)
|
||||
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
|
||||
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_3.addItem(spacerItem)
|
||||
self.okButton = QtWidgets.QPushButton(Dialog)
|
||||
self.okButton.setObjectName("okButton")
|
||||
self.horizontalLayout_3.addWidget(self.okButton)
|
||||
self.cancelButton = QtWidgets.QPushButton(Dialog)
|
||||
self.cancelButton.setObjectName("cancelButton")
|
||||
self.horizontalLayout_3.addWidget(self.cancelButton)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_3.addItem(spacerItem1)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_3, 1, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
QtCore.QMetaObject.connectSlotsByName(Dialog)
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Dialog.setWindowTitle(_translate("Dialog", "Annotation Editor"))
|
||||
self.nameEdit.setPlaceholderText(_translate("Dialog", "Annotation Name"))
|
||||
self.typeLabel.setText(_translate("Dialog", "Type"))
|
||||
self.foregroundCheck.setText(_translate("Dialog", "Foreground"))
|
||||
self.highlightCheck.setText(_translate("Dialog", "Highlight"))
|
||||
self.boldCheck.setText(_translate("Dialog", "Bold"))
|
||||
self.italicCheck.setText(_translate("Dialog", "Italic"))
|
||||
self.underlineCheck.setText(_translate("Dialog", "Underline"))
|
||||
self.okButton.setText(_translate("Dialog", "OK"))
|
||||
self.cancelButton.setText(_translate("Dialog", "Cancel"))
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'raw/main.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.9.2
|
||||
# Created by: PyQt5 UI code generator 5.10.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -35,20 +35,6 @@ class Ui_MainWindow(object):
|
||||
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_4.setSpacing(0)
|
||||
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||
self.listView = QtWidgets.QListView(self.listPage)
|
||||
self.listView.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
self.listView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.listView.setProperty("showDropIndicator", False)
|
||||
self.listView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.listView.setMovement(QtWidgets.QListView.Static)
|
||||
self.listView.setProperty("isWrapping", True)
|
||||
self.listView.setResizeMode(QtWidgets.QListView.Fixed)
|
||||
self.listView.setLayoutMode(QtWidgets.QListView.SinglePass)
|
||||
self.listView.setViewMode(QtWidgets.QListView.IconMode)
|
||||
self.listView.setUniformItemSizes(True)
|
||||
self.listView.setWordWrap(True)
|
||||
self.listView.setObjectName("listView")
|
||||
self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.listPage)
|
||||
self.tablePage = QtWidgets.QWidget()
|
||||
self.tablePage.setObjectName("tablePage")
|
||||
@@ -56,20 +42,6 @@ class Ui_MainWindow(object):
|
||||
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_3.setSpacing(0)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.tableView = QtWidgets.QTableView(self.tablePage)
|
||||
self.tableView.setFrameShape(QtWidgets.QFrame.Box)
|
||||
self.tableView.setFrameShadow(QtWidgets.QFrame.Plain)
|
||||
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
|
||||
self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
|
||||
self.tableView.setAlternatingRowColors(True)
|
||||
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.tableView.setGridStyle(QtCore.Qt.NoPen)
|
||||
self.tableView.setSortingEnabled(True)
|
||||
self.tableView.setWordWrap(False)
|
||||
self.tableView.setObjectName("tableView")
|
||||
self.tableView.horizontalHeader().setVisible(True)
|
||||
self.tableView.verticalHeader().setVisible(False)
|
||||
self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.tablePage)
|
||||
self.gridLayout_2.addWidget(self.stackedWidget, 0, 0, 1, 1)
|
||||
self.tabWidget.addTab(self.tab, "")
|
||||
|
@@ -94,26 +94,26 @@ def generate_pie(progress_percent, temp_dir=None):
|
||||
return lSvg
|
||||
|
||||
|
||||
def pixmapper(current_chapter, total_chapters, temp_dir, size):
|
||||
def pixmapper(position_percent, temp_dir, consider_read_at, size):
|
||||
# A current_chapter of -1 implies the files does not exist
|
||||
# A chapter number == Total chapters implies the file is unread
|
||||
return_pixmap = None
|
||||
# position_percent and consider_read_at are expected as a <1 decimal value
|
||||
|
||||
if current_chapter == -1:
|
||||
return_pixmap = None
|
||||
consider_read_at = consider_read_at / 100
|
||||
|
||||
if position_percent == -1:
|
||||
return_pixmap = QtGui.QIcon(':/images/error.svg').pixmap(size)
|
||||
return return_pixmap
|
||||
|
||||
if current_chapter == total_chapters:
|
||||
if position_percent >= consider_read_at: # Consider book read @ this progress
|
||||
return_pixmap = QtGui.QIcon(':/images/checkmark.svg').pixmap(size)
|
||||
else:
|
||||
|
||||
# TODO
|
||||
# See if saving the svg to disk can be avoided
|
||||
# Shift to lines to track progress
|
||||
# Maybe make the alignment a little more uniform across emblems
|
||||
|
||||
progress_percent = int(current_chapter * 100 / total_chapters)
|
||||
generate_pie(progress_percent, temp_dir)
|
||||
generate_pie(int(position_percent * 100), temp_dir)
|
||||
svg_path = os.path.join(temp_dir, 'lector_progress.svg')
|
||||
return_pixmap = QtGui.QIcon(svg_path).pixmap(size - 4) ## The -4 looks more proportional
|
||||
|
||||
|
8
lector/resources/raw/DarkIcons/about.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 815 B |
8
lector/resources/raw/DarkIcons/annotate.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 617 B |
8
lector/resources/raw/DarkIcons/arrow-down.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 372 B |
8
lector/resources/raw/DarkIcons/arrow-up.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 373 B |
8
lector/resources/raw/DarkIcons/invert.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 1 L 2 15 L 9 15 L 9 13 A 5 5 0 0 1 4 8 A 5 5 0 0 1 9 3 L 9 1 L 2 1 z M 9 3 L 9 13 C 11.7614 13 14 10.7614 14 8 C 14 5.2386 11.7614 3 9 3 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 487 B |
61
lector/resources/raw/DarkIcons/manga-mode.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
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"
|
||||
width="22"
|
||||
height="22"
|
||||
version="1.1"
|
||||
viewBox="0 0 22 22"
|
||||
id="svg7"
|
||||
sodipodi:docname="manga.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata11">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1586"
|
||||
inkscape:window-height="856"
|
||||
id="namedview9"
|
||||
showgrid="false"
|
||||
inkscape:zoom="10.727273"
|
||||
inkscape:cx="11"
|
||||
inkscape:cy="11"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg7" />
|
||||
<defs
|
||||
id="defs3">
|
||||
<style
|
||||
id="current-color-scheme"
|
||||
type="text/css">
|
||||
.ColorScheme-Text { color:#6e6e6e; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="fill:#5c616c;fill-opacity:1"
|
||||
class="ColorScheme-Text"
|
||||
d="M 19,11 14,6 V 8 H 8 V 6 l -5,5 5,5 v -2 h 6 v 2 z"
|
||||
id="path5" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
8
lector/resources/raw/DarkIcons/page-double.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 1 L 2 15 L 14 15 L 14 1 L 2 1 z M 4 3 L 12 3 L 12 13 L 4 13 L 4 3 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 416 B |
53
lector/resources/raw/DarkIcons/search-case.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
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"
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="search-case.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1480"
|
||||
inkscape:window-height="750"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="14.75"
|
||||
inkscape:cx="-6.5084746"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
style="fill:#5c616c;fill-opacity:1"
|
||||
d="M 4.7890625,2 1,13 h 1.90625 l 0.6796875,-2 h 3.8300781 l 0.6875,2 H 10 L 6.2109375,2 Z M 5.4980469,5.4375 6.6835938,9 H 4.3144531 Z M 11.5,8 v 1 h 3 C 14.715,9 15,9.305 15,9.5 V 10 h -2.5 c -0.46,0 -0.87,0.189375 -1.125,0.484375 C 11.12,10.774375 11,11.14 11,11.5 c 0,0.36 0.135625,0.725625 0.390625,1.015625 C 11.645625,12.805625 12.045,13 12.5,13 H 16 V 9.5 C 16,8.685 15.34,8 14.5,8 Z m 1,3 H 15 v 1 H 12.5 C 12.3,12 12.215625,11.944375 12.140625,11.859375 12.065625,11.774375 12,11.64 12,11.5 12,11.36 12.05,11.225625 12.125,11.140625 12.2,11.060625 12.29,11 12.5,11 Z"
|
||||
id="path2" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
68
lector/resources/raw/DarkIcons/search-word.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
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"
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="search-word.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1043"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:cx="6.5123537"
|
||||
inkscape:cy="6.8917559"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
style="fill:#5c616c;fill-opacity:1"
|
||||
d="M 3,3 C 1.02036,3 -9.9999927e-7,4.1718311 0,6.6074219 V 8.5 A 1.5,1.5 0 0 0 1.5,10 1.5,1.5 0 0 0 3,8.5 1.5,1.5 0 0 0 1.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 2.0660618,4.29323 2.453243,4.0371834 3,4 Z M 7,3 C 5.02036,3 3.999999,4.1718311 4,6.6074219 V 8.5 A 1.5,1.5 0 0 0 5.5,10 1.5,1.5 0 0 0 7,8.5 1.5,1.5 0 0 0 5.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 6.0660608,4.29323 6.453243,4.0371834 7,4 Z M 9.5,5 A 1.5,1.5 0 0 0 8,6.5 1.5,1.5 0 0 0 9.490234,8 l 0.00781,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 8.9339389,10.70677 8.5467573,10.962817 8,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 9.5,5 Z m 4,0 A 1.5,1.5 0 0 0 12,6.5 1.5,1.5 0 0 0 13.490234,8 l 0.0078,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 12.933938,10.70677 12.546757,10.962817 12,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 13.5,5 Z"
|
||||
id="path2" />
|
||||
<path
|
||||
style="fill:#5c616c;stroke:none;stroke-width:0.06779661;fill-opacity:1"
|
||||
d="M 8.040678,11.489133 V 11.01394 l 0.2542373,-0.0411 C 8.6241141,10.91962 9.0049959,10.657098 9.1919904,10.354534 9.3774432,10.054466 9.5267422,9.2371719 9.5298381,8.5050867 9.5321781,7.9511596 9.5305475,7.9457647 9.3605343,7.9457647 8.6870054,7.9457647 8.0454876,7.2386514 8.0421669,6.4925935 8.0367069,5.2649491 9.4833138,4.5924333 10.437555,5.379 c 0.470567,0.3878808 0.518377,0.5947737 0.518377,2.2432227 0,0.9924588 -0.03143,1.6284257 -0.09756,1.9740504 -0.267079,1.3958959 -1.1175811,2.2244799 -2.3939662,2.3322709 l -0.4237288,0.03579 z"
|
||||
id="path4524"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#5c616c;stroke:none;stroke-width:0.00390625;fill-opacity:1"
|
||||
d="m 9.7117131,11.513295 c 0.00204,-0.0018 0.023926,-0.01759 0.048633,-0.03507 0.1798996,-0.127248 0.3444879,-0.28336 0.4858319,-0.460812 0.252616,-0.317149 0.444866,-0.723077 0.559095,-1.1805105 0.06615,-0.2648918 0.09348,-0.4533196 0.116987,-0.8066406 C 10.94809,8.6420545 10.9575,8.1721993 10.95441,7.4247931 10.952,6.840388 10.947836,6.672371 10.92997,6.438465 10.924141,6.3621478 10.9106,6.2310896 10.906461,6.2109259 l -0.0022,-0.010742 h 0.0313 c 0.0366,0 0.0324,-0.00607 0.0433,0.0625 0.01847,0.1161553 0.01782,0.070812 0.01782,1.25 0,1.1705155 -1.65e-4,1.1838055 -0.01779,1.4296875 -0.05607,0.7822203 -0.232739,1.4031726 -0.538111,1.8913516 -0.157745,0.252177 -0.36026,0.471333 -0.5979529,0.647089 l -0.048386,0.03578 h -0.043215 c -0.023798,0 -0.041548,-0.0015 -0.039504,-0.0033 z"
|
||||
id="path4526"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#5c616c;stroke:none;stroke-width:0.00390625;fill-opacity:1"
|
||||
d="m 8.0037065,11.500359 v -0.499846 l 0.038086,-0.0025 c 0.052532,-0.0035 0.1567951,-0.0193 0.2234149,-0.03389 C 8.640889,10.881833 8.9448125,10.678717 9.1476483,10.374368 9.3523256,10.067256 9.4641833,9.5521406 9.492672,8.7855008 L 9.497502,8.655618 H 9.512387 9.527272 L 9.524992,8.705423 C 9.5049185,9.1437939 9.4502274,9.5471986 9.3653315,9.8830268 9.3036893,10.126869 9.2445481,10.277176 9.1635619,10.395823 9.0101537,10.620571 8.748788,10.822299 8.4830209,10.92108 c -0.078653,0.02923 -0.1439508,0.04431 -0.30158,0.06961 l -0.140625,0.02258 -9.922e-4,0.475456 c -9.556e-4,0.457983 -7.301e-4,0.475456 0.00614,0.475456 0.012211,0 0.421374,-0.03493 0.483138,-0.04125 0.3949367,-0.04038 0.7679272,-0.159625 1.078125,-0.344663 0.023633,-0.0141 0.0556,-0.03409 0.071038,-0.04442 l 0.028069,-0.01879 0.04029,0.003 0.04029,0.003 -0.046244,0.03035 c -0.2869093,0.188322 -0.6383031,0.321037 -1.0300398,0.389028 -0.189836,0.03295 -0.3718111,0.04969 -0.6219586,0.0572 l -0.084961,0.0026 z"
|
||||
id="path4528"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.2 KiB |
8
lector/resources/raw/DarkIcons/switches.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 0 C 0.892 0 0 0.892 0 2 L 0 14 C 0 15.108 0.892 16 2 16 L 14 16 C 15.108 16 16 15.108 16 14 L 16 2 C 16 0.892 15.108 0 14 0 L 2 0 z M 3.7148438 2 L 12.285156 2 C 13.235156 2 14 2.7651437 14 3.7148438 L 14 12.285156 C 14 13.235156 13.235156 14 12.285156 14 L 3.7148438 14 C 2.7651438 14 2 13.235156 2 12.285156 L 2 3.7148438 C 2 2.7651438 2.7651437 2 3.7148438 2 z M 6.7402344 3 L 6.6289062 4.3164062 A 3.964 3.9286 0 0 0 5.4707031 4.9804688 L 4.2617188 4.4179688 L 3.0019531 6.5820312 L 4.0976562 7.3378906 A 3.964 3.9286 0 0 0 4.0371094 8 A 3.964 3.9286 0 0 0 4.0957031 8.6660156 L 3.0019531 9.4179688 L 4.2617188 11.582031 L 5.4667969 11.019531 A 3.964 3.9286 0 0 0 6.6289062 11.679688 L 6.7402344 13 L 9.2617188 13 L 9.3730469 11.683594 A 3.964 3.9286 0 0 0 10.53125 11.019531 L 11.740234 11.582031 L 13.001953 9.4179688 L 11.904297 8.6621094 A 3.964 3.9286 0 0 0 11.964844 8 A 3.964 3.9286 0 0 0 11.908203 7.3339844 L 13.001953 6.5820312 L 11.740234 4.4179688 L 10.535156 4.9804688 A 3.964 3.9286 0 0 0 9.3730469 4.3203125 L 9.2617188 3 L 6.7402344 3 z M 8.0019531 6.5722656 A 1.4414 1.4286 0 0 1 9.4433594 8 A 1.4414 1.4286 0 0 1 8.0019531 9.4277344 A 1.4414 1.4286 0 0 1 6.5605469 8 A 1.4414 1.4286 0 0 1 8.0019531 6.5722656 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
8
lector/resources/raw/DarkIcons/web-browser.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 0.99609375 C 4.134 0.99609375 1 4.1300937 1 7.9960938 C 1 11.862094 4.134 14.996094 8 14.996094 C 11.866 14.996094 15 11.862094 15 7.9960938 C 15 4.1300937 11.866 0.99609375 8 0.99609375 z M 7.5 2.9335938 C 7.5669 2.9265937 7.65125 2.9375937 7.71875 2.9335938 C 7.72675 2.9655938 7.67005 3.0794638 7.59375 3.2460938 C 7.10789 4.3074937 7.08033 5.5504437 7.53125 6.2148438 C 7.61285 6.3353038 7.6875 6.4499437 7.6875 6.4648438 C 7.6875 6.4797438 7.5995 6.4960938 7.5 6.4960938 C 7.26642 6.4960938 7.04538 6.3537238 6.59375 5.9960938 C 6.39312 5.8372237 6.1323 5.7037938 6.03125 5.6835938 C 5.87257 5.6518937 5.83028 5.6657938 5.625 5.8710938 C 5.43401 6.0620537 5.375 6.1650237 5.375 6.3398438 C 5.375 7.0027837 6.16208 7.5297437 7.625 7.8398438 C 9.6117 8.2609137 10.10145 8.6389138 10.15625 9.6835938 C 10.22505 10.993594 9.5276 11.981394 8 12.746094 C 7.81767 12.837394 7.7015 12.872844 7.625 12.902344 C 7.5911 12.899344 7.56505 12.905344 7.53125 12.902344 C 7.51825 12.861844 7.5 12.767884 7.5 12.589844 C 7.5 11.894064 7.22575 11.177844 6.8125 10.777344 C 6.70157 10.669824 6.39098 10.441994 6.125 10.277344 C 5.85903 10.112704 5.59105 9.9214438 5.53125 9.8398438 C 5.43215 9.7044337 5.42386 9.6212437 5.5 9.3710938 C 5.63876 8.9142237 5.80392 8.6597637 6.125 8.3710938 C 6.29333 8.2197537 6.46271 8.0928437 6.5 8.0898438 C 6.5373 8.0868438 6.28485 8.0110437 5.90625 7.9335938 C 5.52767 7.8559938 4.97383 7.6934738 4.6875 7.5898438 C 4.16392 7.4003938 3.457 7.0026837 3.1875 6.7148438 C 3.1761 6.7026437 3.16615 6.6943938 3.15625 6.6835938 C 3.54238 5.1454938 4.626 3.8848438 6.0625 3.2773438 C 6.36307 3.1502138 6.67292 3.0629938 7 2.9960938 C 7.16292 2.9627938 7.33178 2.9506937 7.5 2.9335938 z M 12.1875 5.2773438 C 12.30495 5.3499437 12.74841 6.3093438 12.875 6.7773438 C 13.03844 7.3815337 13.02661 8.4271437 12.875 9.0273438 C 12.8173 9.2557838 12.74335 9.4694937 12.71875 9.4960938 C 12.69415 9.5226938 12.60494 9.3695637 12.5 9.1835938 C 12.39505 8.9976538 12.05984 8.6025437 11.78125 8.3085938 C 10.97711 7.4600637 10.85066 7.0170437 11.1875 6.3398438 C 11.35737 5.9983538 12.0966 5.2212438 12.1875 5.2773438 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
BIN
lector/resources/raw/Google.png
Normal file
After Width: | Height: | Size: 40 KiB |
8
lector/resources/raw/LightIcons/about.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 815 B |
8
lector/resources/raw/LightIcons/annotate.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 617 B |
8
lector/resources/raw/LightIcons/arrow-down.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 372 B |
8
lector/resources/raw/LightIcons/arrow-up.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 373 B |
8
lector/resources/raw/LightIcons/invert.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 1 L 2 15 L 9 15 L 9 13 A 5 5 0 0 1 4 8 A 5 5 0 0 1 9 3 L 9 1 L 2 1 z M 9 3 L 9 13 C 11.7614 13 14 10.7614 14 8 C 14 5.2386 11.7614 3 9 3 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 487 B |
8
lector/resources/raw/LightIcons/manga-mode.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" version="1.1" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 19,11 14,6 V 8 H 8 V 6 l -5,5 5,5 v -2 h 6 v 2 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 382 B |
8
lector/resources/raw/LightIcons/page-double.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 1 1 L 1 15 L 15 15 L 15 1 L 1 1 z M 3 3 L 7 3 L 7 13 L 3 13 L 3 3 z M 9 3 L 13 3 L 13 13 L 9 13 L 9 3 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 450 B |
3
lector/resources/raw/LightIcons/search-case.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
|
||||
<path style="fill:#dfdfdf" d="M 4.7890625,2 1,13 h 1.90625 l 0.6796875,-2 h 3.8300781 l 0.6875,2 H 10 L 6.2109375,2 Z M 5.4980469,5.4375 6.6835938,9 H 4.3144531 Z M 11.5,8 v 1 h 3 C 14.715,9 15,9.305 15,9.5 V 10 h -2.5 c -0.46,0 -0.87,0.189375 -1.125,0.484375 C 11.12,10.774375 11,11.14 11,11.5 c 0,0.36 0.135625,0.725625 0.390625,1.015625 C 11.645625,12.805625 12.045,13 12.5,13 H 16 V 9.5 C 16,8.685 15.34,8 14.5,8 Z m 1,3 H 15 v 1 H 12.5 C 12.3,12 12.215625,11.944375 12.140625,11.859375 12.065625,11.774375 12,11.64 12,11.5 12,11.36 12.05,11.225625 12.125,11.140625 12.2,11.060625 12.29,11 12.5,11 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 693 B |
3
lector/resources/raw/LightIcons/search-word.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
|
||||
<path style="fill:#dfdfdf" d="M 3,3 C 1.02036,3 -9.9999927e-7,4.1718311 0,6.6074219 V 8.5 A 1.5,1.5 0 0 0 1.5,10 1.5,1.5 0 0 0 3,8.5 1.5,1.5 0 0 0 1.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 2.0660618,4.29323 2.453243,4.0371834 3,4 Z M 7,3 C 5.02036,3 3.999999,4.1718311 4,6.6074219 V 8.5 A 1.5,1.5 0 0 0 5.5,10 1.5,1.5 0 0 0 7,8.5 1.5,1.5 0 0 0 5.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 6.0660608,4.29323 6.453243,4.0371834 7,4 Z M 9.5,5 A 1.5,1.5 0 0 0 8,6.5 1.5,1.5 0 0 0 9.490234,8 l 0.00781,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 8.9339389,10.70677 8.5467573,10.962817 8,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 9.5,5 Z m 4,0 A 1.5,1.5 0 0 0 12,6.5 1.5,1.5 0 0 0 13.490234,8 l 0.0078,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 12.933938,10.70677 12.546757,10.962817 12,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 13.5,5 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
8
lector/resources/raw/LightIcons/switches.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 0 C 0.892 0 0 0.892 0 2 L 0 14 C 0 15.108 0.892 16 2 16 L 14 16 C 15.108 16 16 15.108 16 14 L 16 2 C 16 0.892 15.108 0 14 0 L 2 0 z M 3.7148438 2 L 12.285156 2 C 13.235156 2 14 2.7651437 14 3.7148438 L 14 12.285156 C 14 13.235156 13.235156 14 12.285156 14 L 3.7148438 14 C 2.7651438 14 2 13.235156 2 12.285156 L 2 3.7148438 C 2 2.7651438 2.7651437 2 3.7148438 2 z M 6.7402344 3 L 6.6289062 4.3164062 A 3.964 3.9286 0 0 0 5.4707031 4.9804688 L 4.2617188 4.4179688 L 3.0019531 6.5820312 L 4.0976562 7.3378906 A 3.964 3.9286 0 0 0 4.0371094 8 A 3.964 3.9286 0 0 0 4.0957031 8.6660156 L 3.0019531 9.4179688 L 4.2617188 11.582031 L 5.4667969 11.019531 A 3.964 3.9286 0 0 0 6.6289062 11.679688 L 6.7402344 13 L 9.2617188 13 L 9.3730469 11.683594 A 3.964 3.9286 0 0 0 10.53125 11.019531 L 11.740234 11.582031 L 13.001953 9.4179688 L 11.904297 8.6621094 A 3.964 3.9286 0 0 0 11.964844 8 A 3.964 3.9286 0 0 0 11.908203 7.3339844 L 13.001953 6.5820312 L 11.740234 4.4179688 L 10.535156 4.9804688 A 3.964 3.9286 0 0 0 9.3730469 4.3203125 L 9.2617188 3 L 6.7402344 3 z M 8.0019531 6.5722656 A 1.4414 1.4286 0 0 1 9.4433594 8 A 1.4414 1.4286 0 0 1 8.0019531 9.4277344 A 1.4414 1.4286 0 0 1 6.5605469 8 A 1.4414 1.4286 0 0 1 8.0019531 6.5722656 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
8
lector/resources/raw/LightIcons/web-browser.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 0.99609375 C 4.134 0.99609375 1 4.1300937 1 7.9960938 C 1 11.862094 4.134 14.996094 8 14.996094 C 11.866 14.996094 15 11.862094 15 7.9960938 C 15 4.1300937 11.866 0.99609375 8 0.99609375 z M 7.5 2.9335938 C 7.5669 2.9265937 7.65125 2.9375937 7.71875 2.9335938 C 7.72675 2.9655938 7.67005 3.0794638 7.59375 3.2460938 C 7.10789 4.3074937 7.08033 5.5504437 7.53125 6.2148438 C 7.61285 6.3353038 7.6875 6.4499437 7.6875 6.4648438 C 7.6875 6.4797438 7.5995 6.4960938 7.5 6.4960938 C 7.26642 6.4960938 7.04538 6.3537238 6.59375 5.9960938 C 6.39312 5.8372237 6.1323 5.7037938 6.03125 5.6835938 C 5.87257 5.6518937 5.83028 5.6657938 5.625 5.8710938 C 5.43401 6.0620537 5.375 6.1650237 5.375 6.3398438 C 5.375 7.0027837 6.16208 7.5297437 7.625 7.8398438 C 9.6117 8.2609137 10.10145 8.6389138 10.15625 9.6835938 C 10.22505 10.993594 9.5276 11.981394 8 12.746094 C 7.81767 12.837394 7.7015 12.872844 7.625 12.902344 C 7.5911 12.899344 7.56505 12.905344 7.53125 12.902344 C 7.51825 12.861844 7.5 12.767884 7.5 12.589844 C 7.5 11.894064 7.22575 11.177844 6.8125 10.777344 C 6.70157 10.669824 6.39098 10.441994 6.125 10.277344 C 5.85903 10.112704 5.59105 9.9214438 5.53125 9.8398438 C 5.43215 9.7044337 5.42386 9.6212437 5.5 9.3710938 C 5.63876 8.9142237 5.80392 8.6597637 6.125 8.3710938 C 6.29333 8.2197537 6.46271 8.0928437 6.5 8.0898438 C 6.5373 8.0868438 6.28485 8.0110437 5.90625 7.9335938 C 5.52767 7.8559938 4.97383 7.6934738 4.6875 7.5898438 C 4.16392 7.4003938 3.457 7.0026837 3.1875 6.7148438 C 3.1761 6.7026437 3.16615 6.6943938 3.15625 6.6835938 C 3.54238 5.1454938 4.626 3.8848438 6.0625 3.2773438 C 6.36307 3.1502138 6.67292 3.0629938 7 2.9960938 C 7.16292 2.9627938 7.33178 2.9506937 7.5 2.9335938 z M 12.1875 5.2773438 C 12.30495 5.3499437 12.74841 6.3093438 12.875 6.7773438 C 13.03844 7.3815337 13.02661 8.4271437 12.875 9.0273438 C 12.8173 9.2557838 12.74335 9.4694937 12.71875 9.4960938 C 12.69415 9.5226938 12.60494 9.3695637 12.5 9.1835938 C 12.39505 8.9976538 12.05984 8.6025437 11.78125 8.3085938 C 10.97711 7.4600637 10.85066 7.0170437 11.1875 6.3398438 C 11.35737 5.9983538 12.0966 5.2212438 12.1875 5.2773438 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
BIN
lector/resources/raw/Wikipedia.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
lector/resources/raw/Youtube.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
245
lector/resources/raw/annotations.ui
Normal file
@@ -0,0 +1,245 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>306</width>
|
||||
<height>387</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Annotation Editor</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="nameEdit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Annotation Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="typeLabel">
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="typeBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stackedWidget">
|
||||
<widget class="QWidget" name="page">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_12">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_15">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="foregroundCheck">
|
||||
<property name="text">
|
||||
<string>Foreground</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="foregroundColorButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_16">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="highlightCheck">
|
||||
<property name="text">
|
||||
<string>Highlight</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="highlightColorButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>24</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_17">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="boldCheck">
|
||||
<property name="text">
|
||||
<string>Bold</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_18">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="italicCheck">
|
||||
<property name="text">
|
||||
<string>Italic</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_19">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="underlineCheck">
|
||||
<property name="text">
|
||||
<string>Underline</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="underlineType"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="underlineColorButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>24</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<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="okButton">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
BIN
lector/resources/raw/logo/icon.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
25
lector/resources/raw/logo/icon.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="178.117px" height="234.088px" viewBox="0 0 178.117 234.088" enable-background="new 0 0 178.117 234.088"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#41CD52" d="M128.275,0H67.067H0v168.689h0.091c37.278,0.518,64.647,9.549,81.347,26.848
|
||||
c2.557,2.652,4.862,5.513,6.894,8.564c2.034-3.052,4.338-5.912,6.893-8.564c16.925-17.534,44.797-26.573,82.843-26.87h0.05V49.842
|
||||
L128.275,0z M162.231,156.781h-0.044c-31.258,0.243-54.156,7.67-68.062,22.081c-2.099,2.173-3.993,4.527-5.667,7.031
|
||||
c-1.668-2.504-3.562-4.858-5.661-7.031c-13.721-14.213-36.207-21.636-66.835-22.058l-0.076-0.004V18.208h55.104l50.289-0.004
|
||||
l40.951,40.951V156.781z"/>
|
||||
<path fill="#41CD52" d="M100.03,200.175c-5.132,5.315-9.028,11.451-11.699,18.353c-2.67-6.901-6.566-13.037-11.696-18.353
|
||||
C61.197,184.181,35.406,175.856,0,175.366v10.383c23.437,0.365,53.315,4.596,69.823,21.7c6.819,7.062,10.964,16.009,12.459,26.639
|
||||
h12.098c1.497-10.63,5.643-19.577,12.461-26.639c16.851-17.459,47.633-21.503,71.275-21.716v-10.39
|
||||
C141.959,175.621,115.676,183.96,100.03,200.175z"/>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="38.626" y="69.435" fill="#41CD52" width="100.864" height="9.664"/>
|
||||
<rect x="38.626" y="97.219" fill="#41CD52" width="100.864" height="9.664"/>
|
||||
<rect x="38.626" y="124.999" fill="#41CD52" width="100.864" height="9.663"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
BIN
lector/resources/raw/logo/logotype_horizontal.png
Normal file
After Width: | Height: | Size: 12 KiB |
51
lector/resources/raw/logo/logotype_horizontal.svg
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="854.721px" height="234.088px" viewBox="0 0 854.721 234.088" enable-background="new 0 0 854.721 234.088"
|
||||
xml:space="preserve">
|
||||
<path fill="#41CD52" d="M128.275,0H67.067H0v168.689h0.091c37.278,0.518,64.647,9.549,81.347,26.848
|
||||
c2.557,2.652,4.862,5.513,6.894,8.564c2.034-3.052,4.339-5.912,6.894-8.564c16.924-17.534,44.796-26.573,82.842-26.87h0.049V49.842
|
||||
L128.275,0z M162.231,156.781h-0.044c-31.257,0.243-54.156,7.67-68.062,22.081c-2.098,2.173-3.993,4.527-5.667,7.031
|
||||
c-1.668-2.504-3.562-4.858-5.661-7.031c-13.721-14.213-36.207-21.636-66.835-22.058l-0.076-0.004V18.208h55.104l50.289-0.004
|
||||
l40.951,40.951V156.781z"/>
|
||||
<path fill="#41CD52" d="M100.03,200.175c-5.132,5.315-9.028,11.451-11.699,18.353c-2.67-6.901-6.566-13.037-11.696-18.353
|
||||
C61.197,184.181,35.406,175.856,0,175.366v10.383c23.437,0.365,53.315,4.596,69.823,21.7c6.819,7.062,10.964,16.009,12.459,26.639
|
||||
h12.098c1.497-10.63,5.642-19.577,12.461-26.639c16.85-17.459,47.632-21.503,71.274-21.716v-10.39
|
||||
C141.959,175.621,115.676,183.96,100.03,200.175z"/>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="38.626" y="69.435" fill="#41CD52" width="100.864" height="9.664"/>
|
||||
<rect x="38.626" y="97.219" fill="#41CD52" width="100.864" height="9.664"/>
|
||||
<rect x="38.626" y="124.999" fill="#41CD52" width="100.864" height="9.663"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" points="238.749,49.355 226.182,49.355 226.182,185.003
|
||||
299.459,185.003 299.459,174.092 238.749,174.092 "/>
|
||||
<polygon fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" points="332.486,118.691 379.688,118.691 379.688,107.78
|
||||
332.486,107.78 332.486,60.27 397.687,60.27 397.687,49.355 319.92,49.355 319.92,185.003 400.541,185.003 400.541,174.092
|
||||
332.486,174.092 "/>
|
||||
<path fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" d="M488.217,167.807c-5.688,5.718-13.057,8.583-22.107,8.583
|
||||
c-12.167,0-21.647-5.246-28.427-15.735c-6.787-10.485-10.181-25.144-10.181-43.979c0-18.893,3.241-33.46,9.721-43.702
|
||||
c6.482-10.238,15.689-15.362,27.604-15.362c8.191,0,15.059,2.507,20.59,7.521c5.539,5.014,10.018,12.779,13.441,23.294l11.649-2.75
|
||||
c-3.676-13.205-9.192-22.99-16.562-29.348c-7.361-6.361-16.827-9.542-28.385-9.542c-16.39,0-29.024,5.98-37.927,17.931
|
||||
c-8.895,11.957-13.342,29.001-13.342,51.135c0,22.865,4.447,40.495,13.342,52.871c8.902,12.384,21.537,18.573,37.927,18.573
|
||||
c12.475,0,22.515-3.557,30.128-10.687c7.616-7.122,12.984-17.957,16.101-32.513l-11.098-2.474
|
||||
C498.06,153.365,493.902,162.093,488.217,167.807z"/>
|
||||
<polygon fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" points="521.967,60.27 562.142,60.27 562.142,185.003
|
||||
574.803,185.003 574.803,60.27 614.97,60.27 614.97,49.355 521.967,49.355 "/>
|
||||
<path fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" d="M681.103,47.156c-15.955,0-28.818,6.373-38.569,19.121
|
||||
c-9.755,12.745-14.628,29.732-14.628,50.949c0,21.218,4.873,38.2,14.628,50.945c9.751,12.753,22.614,19.125,38.569,19.125
|
||||
c15.956,0,28.762-6.372,38.429-19.125c9.663-12.745,14.487-29.728,14.487-50.945c0-21.217-4.824-38.204-14.487-50.949
|
||||
C709.865,53.529,697.059,47.156,681.103,47.156z M710.36,160.563c-7.152,10.546-16.907,15.826-29.257,15.826
|
||||
c-12.353,0-22.107-5.28-29.26-15.826c-7.156-10.546-10.725-24.992-10.725-43.337c0-18.402,3.568-32.867,10.725-43.382
|
||||
c7.152-10.516,16.907-15.773,29.26-15.773c12.35,0,22.104,5.258,29.257,15.773c7.155,10.515,10.729,24.979,10.729,43.382
|
||||
C721.088,135.571,717.515,150.018,710.36,160.563z"/>
|
||||
<path fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" d="M821.244,121.537c8.99-2.568,15.743-6.799,20.267-12.703
|
||||
c4.535-5.901,6.791-13.438,6.791-22.606c0-12.228-4.067-21.431-12.197-27.609c-8.134-6.175-20.239-9.264-36.321-9.264h-37.697
|
||||
v135.648h12.573v-61.632h33.472l31.55,61.632h14.222L821.244,121.537z M805.285,112.733h-30.626V60.27h26.87
|
||||
c11.372,0,19.826,2.142,25.357,6.418c5.539,4.28,8.301,10.857,8.301,19.718c0,8.442-2.587,14.936-7.749,19.49
|
||||
C822.267,110.458,814.89,112.733,805.285,112.733z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
BIN
lector/resources/raw/logo/logotype_vertical.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
49
lector/resources/raw/logo/logotype_vertical.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="267.176px" height="313.485px" viewBox="0 0 267.176 313.485" enable-background="new 0 0 267.176 313.485"
|
||||
xml:space="preserve">
|
||||
<path fill="#41CD52" d="M172.645,0h-61.208H44.37v168.689h0.091c37.278,0.518,64.647,9.549,81.347,26.848
|
||||
c2.557,2.652,4.862,5.513,6.894,8.564c2.034-3.052,4.339-5.912,6.894-8.564c16.924-17.534,44.796-26.573,82.842-26.87h0.049V49.843
|
||||
L172.645,0z M206.601,156.781h-0.044c-31.257,0.243-54.156,7.67-68.062,22.081c-2.098,2.173-3.993,4.527-5.667,7.031
|
||||
c-1.668-2.504-3.562-4.858-5.661-7.031c-13.721-14.213-36.207-21.636-66.835-22.058l-0.076-0.004V18.208h55.104l50.289-0.004
|
||||
l40.951,40.951V156.781z"/>
|
||||
<path fill="#41CD52" d="M144.4,200.175c-5.132,5.315-9.028,11.451-11.699,18.353c-2.67-6.901-6.566-13.037-11.696-18.353
|
||||
c-15.438-15.994-41.229-24.318-76.635-24.809v10.383c23.437,0.365,53.315,4.596,69.823,21.7
|
||||
c6.819,7.062,10.964,16.009,12.459,26.639h12.098c1.497-10.63,5.642-19.577,12.461-26.639c16.85-17.459,47.632-21.503,71.274-21.716
|
||||
v-10.39C186.329,175.621,160.046,183.96,144.4,200.175z"/>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="82.996" y="69.435" fill="#41CD52" width="100.864" height="9.664"/>
|
||||
<rect x="82.996" y="97.219" fill="#41CD52" width="100.864" height="9.663"/>
|
||||
<rect x="82.996" y="124.999" fill="#41CD52" width="100.864" height="9.663"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" points="5.822,254.563 0.5,254.563 0.5,312.014 31.535,312.014
|
||||
31.535,307.393 5.822,307.393 "/>
|
||||
<polygon fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" points="45.522,283.929 65.515,283.929 65.515,279.309
|
||||
45.522,279.309 45.522,259.187 73.137,259.187 73.137,254.563 40.2,254.563 40.2,312.014 74.345,312.014 74.345,307.393
|
||||
45.522,307.393 "/>
|
||||
<path fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" d="M111.479,304.73c-2.408,2.422-5.529,3.636-9.362,3.636
|
||||
c-5.152,0-9.168-2.222-12.039-6.665c-2.875-4.439-4.312-10.648-4.312-18.626c0-8.001,1.372-14.171,4.116-18.509
|
||||
c2.745-4.336,6.645-6.506,11.691-6.506c3.469,0,6.378,1.062,8.721,3.186c2.345,2.123,4.242,5.411,5.692,9.865l4.934-1.166
|
||||
c-1.557-5.592-3.894-9.735-7.014-12.43c-3.118-2.693-7.127-4.04-12.022-4.04c-6.941,0-12.292,2.532-16.063,7.594
|
||||
c-3.767,5.065-5.649,12.284-5.649,21.657c0,9.684,1.883,17.151,5.649,22.394c3.771,5.244,9.122,7.865,16.063,7.865
|
||||
c5.284,0,9.536-1.507,12.76-4.526c3.226-3.016,5.5-7.604,6.819-13.77l-4.7-1.047C115.647,298.614,113.887,302.312,111.479,304.73z"
|
||||
/>
|
||||
<polygon fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" points="125.772,259.187 142.789,259.187 142.789,312.014
|
||||
148.15,312.014 148.15,259.187 165.162,259.187 165.162,254.563 125.772,254.563 "/>
|
||||
<path fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" d="M193.172,253.631c-6.758,0-12.206,2.701-16.335,8.1
|
||||
c-4.133,5.398-6.195,12.593-6.195,21.578c0,8.987,2.063,16.179,6.195,21.576c4.129,5.402,9.577,8.101,16.335,8.101
|
||||
s12.182-2.698,16.275-8.101c4.094-5.397,6.136-12.589,6.136-21.576c0-8.985-2.042-16.18-6.136-21.578
|
||||
S199.93,253.631,193.172,253.631z M205.563,301.663c-3.028,4.466-7.161,6.703-12.391,6.703c-5.232,0-9.363-2.237-12.392-6.703
|
||||
c-3.031-4.466-4.544-10.586-4.544-18.354c0-7.794,1.513-13.92,4.544-18.374c3.028-4.453,7.159-6.68,12.392-6.68
|
||||
c5.229,0,9.362,2.227,12.391,6.68c3.031,4.454,4.544,10.58,4.544,18.374C210.106,291.077,208.594,297.197,205.563,301.663z"/>
|
||||
<path fill="#4D4D4D" stroke="#4D4D4D" stroke-miterlimit="10" d="M252.525,285.135c3.808-1.088,6.667-2.88,8.583-5.381
|
||||
c1.921-2.499,2.876-5.69,2.876-9.573c0-5.18-1.723-9.078-5.165-11.693c-3.445-2.615-8.572-3.924-15.384-3.924H227.47v57.45h5.325
|
||||
V285.91h14.177l13.361,26.104h6.023L252.525,285.135z M245.766,281.406h-12.971v-22.22h11.38c4.816,0,8.397,0.907,10.739,2.717
|
||||
c2.346,1.813,3.516,4.601,3.516,8.354c0,3.574-1.096,6.325-3.281,8.254C252.958,280.443,249.834,281.406,245.766,281.406z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
@@ -55,46 +55,6 @@
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QListView" name="listView">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="movement">
|
||||
<enum>QListView::Static</enum>
|
||||
</property>
|
||||
<property name="isWrapping" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="resizeMode">
|
||||
<enum>QListView::Fixed</enum>
|
||||
</property>
|
||||
<property name="layoutMode">
|
||||
<enum>QListView::SinglePass</enum>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="viewMode">
|
||||
<enum>QListView::IconMode</enum>
|
||||
</property>
|
||||
<property name="uniformItemSizes">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tablePage">
|
||||
@@ -114,43 +74,6 @@
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QTableView" name="tableView">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="gridStyle">
|
||||
<enum>Qt::NoPen</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@@ -1,5 +1,31 @@
|
||||
<RCC>
|
||||
<qresource prefix="images">
|
||||
<file>DarkIcons/invert.svg</file>
|
||||
<file>LightIcons/invert.svg</file>
|
||||
<file>DarkIcons/manga-mode.svg</file>
|
||||
<file>LightIcons/manga-mode.svg</file>
|
||||
<file>DarkIcons/search-word.svg</file>
|
||||
<file>DarkIcons/search-word.svg</file>
|
||||
<file>DarkIcons/search-case.svg</file>
|
||||
<file>LightIcons/search-word.svg</file>
|
||||
<file>LightIcons/search-case.svg</file>
|
||||
<file>DarkIcons/page-double.svg</file>
|
||||
<file>LightIcons/page-double.svg</file>
|
||||
<file>DarkIcons/about.svg</file>
|
||||
<file>DarkIcons/switches.svg</file>
|
||||
<file>LightIcons/about.svg</file>
|
||||
<file>LightIcons/switches.svg</file>
|
||||
<file>LightIcons/annotate.svg</file>
|
||||
<file>DarkIcons/annotate.svg</file>
|
||||
<file>Google.png</file>
|
||||
<file>Wikipedia.png</file>
|
||||
<file>Youtube.png</file>
|
||||
<file>DarkIcons/web-browser.svg</file>
|
||||
<file>LightIcons/web-browser.svg</file>
|
||||
<file>DarkIcons/arrow-down.svg</file>
|
||||
<file>DarkIcons/arrow-up.svg</file>
|
||||
<file>LightIcons/arrow-down.svg</file>
|
||||
<file>LightIcons/arrow-up.svg</file>
|
||||
<file>Lector.png</file>
|
||||
<file>DarkIcons/tableofcontents.svg</file>
|
||||
<file>LightIcons/tableofcontents.svg</file>
|
||||
@@ -78,5 +104,6 @@
|
||||
<file>translations_bin/Lector_es.qm</file>
|
||||
<file>translations_bin/Lector_de.qm</file>
|
||||
<file>translations_bin/Lector_fr.qm</file>
|
||||
<file>translations_bin/Lector_zh.qm</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@@ -6,211 +6,705 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1216</width>
|
||||
<height>658</height>
|
||||
<width>1139</width>
|
||||
<height>612</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Library</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTreeView" name="treeView"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QTextBrowser" name="aboutBox">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Switches</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="refreshLibrary">
|
||||
<property name="text">
|
||||
<string>Startup: Refresh library</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="fileRemember">
|
||||
<property name="text">
|
||||
<string>Remember open files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="performCulling">
|
||||
<property name="toolTip">
|
||||
<string>Enabling reduces startup time and memory usage</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Load covers only when needed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="coverShadows">
|
||||
<property name="text">
|
||||
<string>Cover shadows</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="autoTags">
|
||||
<property name="text">
|
||||
<string>Generate tags from files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cachingEnabled">
|
||||
<property name="toolTip">
|
||||
<string>Greatly reduces page transition time at the cost of more memory</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cache comic / pdf pages</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="languageLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Dictionary:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="languageBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="hideScrollBars">
|
||||
<property name="toolTip">
|
||||
<string>Horizontal scrolling with Alt + Scroll
|
||||
Reopen book to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Hide scrollbars when reading</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="toolTip">
|
||||
<string>Restart application to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Icon theme: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="darkIconsRadio">
|
||||
<property name="toolTip">
|
||||
<string>Restart application to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Dar&k</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="lightIconsRadio">
|
||||
<property name="toolTip">
|
||||
<string>Restart application to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Light</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="SaysHelloWhenClicked" name="listView">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="okButton">
|
||||
<property name="text">
|
||||
<string>Scan Library</string>
|
||||
</property>
|
||||
<widget class="QStackedWidget" name="stackedWidget">
|
||||
<widget class="QWidget" name="treeViewPage">
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTreeView" name="treeView"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="switchPage">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Library</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_13">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_14">
|
||||
<item>
|
||||
<widget class="QLabel" name="readAtLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Consider book read at percent</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="readAtPercent">
|
||||
<property name="minimum">
|
||||
<number>90</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>95</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="toolTip">
|
||||
<string>Restart application to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Icon theme: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="darkIconsRadio">
|
||||
<property name="toolTip">
|
||||
<string>Restart application to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Dark</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="lightIconsRadio">
|
||||
<property name="toolTip">
|
||||
<string>Restart application to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>L&ight</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="refreshLibrary">
|
||||
<property name="text">
|
||||
<string>Startup: Refresh library</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="fileRemember">
|
||||
<property name="text">
|
||||
<string>Remember open files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="coverShadows">
|
||||
<property name="text">
|
||||
<string>Cover shadows</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="performCulling">
|
||||
<property name="toolTip">
|
||||
<string>Enabling reduces startup time and memory usage</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Load covers only when needed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="autoTags">
|
||||
<property name="text">
|
||||
<string>Generate tags from files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="attenuateTitles">
|
||||
<property name="text">
|
||||
<string>Shrink long book titles</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_16">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="autoCover">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Attempt to download missing book covers from Google books - SLOW</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Download missing covers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Reading</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="2" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="hideScrollBars">
|
||||
<property name="toolTip">
|
||||
<string>Horizontal scrolling with Alt + Scroll
|
||||
Reopen book to see changes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Hide scrollbars when reading</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cachingEnabled">
|
||||
<property name="toolTip">
|
||||
<string>Greatly reduces page transition time at the cost of more memory</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cache comic / pdf pages</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_15">
|
||||
<item>
|
||||
<widget class="QLabel" name="smallIncrementLabel">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Small increment</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="smallIncrementBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html></string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="largeIncrementLabel">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Large increment</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="largeIncrementBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html></string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="languageLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Dictionary language</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="languageBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="scrollSpeedLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Scroll speed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="scrollSpeedSlider">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>15</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="annotationsPage">
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="textTab">
|
||||
<attribute name="title">
|
||||
<string>Text</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_8">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<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="QPushButton" name="newAnnotation">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>New</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="deleteAnnotation">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="editAnnotation">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Edit</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="moveUp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Move Up</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="moveDown">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>45</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Move Down</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<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>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="annotationsList">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::IgnoreAction</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="previewView">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>100</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="imageTab">
|
||||
<attribute name="title">
|
||||
<string>Image</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="aboutPage">
|
||||
<layout class="QGridLayout" name="gridLayout_9">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTabWidget" name="aboutTabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="aboutTab">
|
||||
<attribute name="title">
|
||||
<string>About</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_6">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextBrowser" name="aboutBox">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="logTab">
|
||||
<attribute name="title">
|
||||
<string>Log</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_10">
|
||||
<item row="0" column="0">
|
||||
<widget class="QPlainTextEdit" name="logBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="aboutButton">
|
||||
<property name="text">
|
||||
<string>About</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetButton">
|
||||
<property name="text">
|
||||
<string>Reset Application</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="clearLogButton">
|
||||
<property name="text">
|
||||
<string>Clear Log</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="okButton">
|
||||
<property name="text">
|
||||
<string>Scan Library</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SaysHelloWhenClicked</class>
|
||||
<extends>QListView</extends>
|
||||
<header>lector.widgets</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
BIN
lector/resources/raw/translations_bin/Lector_zh.qm
Normal file
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'raw/settings.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.10.1
|
||||
# Created by: PyQt5 UI code generator 5.11.3
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -11,30 +11,74 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
Dialog.setObjectName("Dialog")
|
||||
Dialog.resize(1216, 658)
|
||||
self.gridLayout_3 = QtWidgets.QGridLayout(Dialog)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.groupBox_2 = QtWidgets.QGroupBox(Dialog)
|
||||
self.groupBox_2.setObjectName("groupBox_2")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.treeView = QtWidgets.QTreeView(self.groupBox_2)
|
||||
self.treeView.setObjectName("treeView")
|
||||
self.gridLayout_2.addWidget(self.treeView, 0, 0, 1, 1)
|
||||
self.aboutBox = QtWidgets.QTextBrowser(self.groupBox_2)
|
||||
self.aboutBox.setOpenExternalLinks(True)
|
||||
self.aboutBox.setOpenLinks(False)
|
||||
self.aboutBox.setObjectName("aboutBox")
|
||||
self.gridLayout_2.addWidget(self.aboutBox, 1, 0, 1, 1)
|
||||
self.verticalLayout_2.addWidget(self.groupBox_2)
|
||||
self.groupBox = QtWidgets.QGroupBox(Dialog)
|
||||
self.groupBox.setObjectName("groupBox")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
|
||||
Dialog.resize(1139, 612)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Dialog)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.listView = SaysHelloWhenClicked(Dialog)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth())
|
||||
self.listView.setSizePolicy(sizePolicy)
|
||||
self.listView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.listView.setObjectName("listView")
|
||||
self.gridLayout.addWidget(self.listView, 0, 0, 1, 1)
|
||||
self.verticalLayout_4 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_4.setObjectName("verticalLayout_4")
|
||||
self.stackedWidget = QtWidgets.QStackedWidget(Dialog)
|
||||
self.stackedWidget.setObjectName("stackedWidget")
|
||||
self.treeViewPage = QtWidgets.QWidget()
|
||||
self.treeViewPage.setObjectName("treeViewPage")
|
||||
self.gridLayout_5 = QtWidgets.QGridLayout(self.treeViewPage)
|
||||
self.gridLayout_5.setObjectName("gridLayout_5")
|
||||
self.treeView = QtWidgets.QTreeView(self.treeViewPage)
|
||||
self.treeView.setObjectName("treeView")
|
||||
self.gridLayout_5.addWidget(self.treeView, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.treeViewPage)
|
||||
self.switchPage = QtWidgets.QWidget()
|
||||
self.switchPage.setObjectName("switchPage")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.switchPage)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.groupBox = QtWidgets.QGroupBox(self.switchPage)
|
||||
self.groupBox.setObjectName("groupBox")
|
||||
self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox)
|
||||
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.horizontalLayout_13 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_13.setObjectName("horizontalLayout_13")
|
||||
self.horizontalLayout_14 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_14.setObjectName("horizontalLayout_14")
|
||||
self.readAtLabel = QtWidgets.QLabel(self.groupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.readAtLabel.sizePolicy().hasHeightForWidth())
|
||||
self.readAtLabel.setSizePolicy(sizePolicy)
|
||||
self.readAtLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
|
||||
self.readAtLabel.setObjectName("readAtLabel")
|
||||
self.horizontalLayout_14.addWidget(self.readAtLabel)
|
||||
self.readAtPercent = QtWidgets.QSpinBox(self.groupBox)
|
||||
self.readAtPercent.setMinimum(90)
|
||||
self.readAtPercent.setProperty("value", 95)
|
||||
self.readAtPercent.setObjectName("readAtPercent")
|
||||
self.horizontalLayout_14.addWidget(self.readAtPercent)
|
||||
self.horizontalLayout_13.addLayout(self.horizontalLayout_14)
|
||||
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||
self.label = QtWidgets.QLabel(self.groupBox)
|
||||
self.label.setObjectName("label")
|
||||
self.horizontalLayout_7.addWidget(self.label)
|
||||
self.darkIconsRadio = QtWidgets.QRadioButton(self.groupBox)
|
||||
self.darkIconsRadio.setObjectName("darkIconsRadio")
|
||||
self.horizontalLayout_7.addWidget(self.darkIconsRadio)
|
||||
self.lightIconsRadio = QtWidgets.QRadioButton(self.groupBox)
|
||||
self.lightIconsRadio.setObjectName("lightIconsRadio")
|
||||
self.horizontalLayout_7.addWidget(self.lightIconsRadio)
|
||||
self.horizontalLayout_13.addLayout(self.horizontalLayout_7)
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout_13)
|
||||
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
|
||||
self.refreshLibrary = QtWidgets.QCheckBox(self.groupBox)
|
||||
@@ -43,28 +87,77 @@ class Ui_Dialog(object):
|
||||
self.fileRemember = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.fileRemember.setObjectName("fileRemember")
|
||||
self.horizontalLayout_4.addWidget(self.fileRemember)
|
||||
self.performCulling = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.performCulling.setObjectName("performCulling")
|
||||
self.horizontalLayout_4.addWidget(self.performCulling)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_4)
|
||||
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.coverShadows = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.coverShadows.setObjectName("coverShadows")
|
||||
self.horizontalLayout.addWidget(self.coverShadows)
|
||||
self.autoTags = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.autoTags.setObjectName("autoTags")
|
||||
self.horizontalLayout.addWidget(self.autoTags)
|
||||
self.cachingEnabled = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.cachingEnabled.setObjectName("cachingEnabled")
|
||||
self.horizontalLayout.addWidget(self.cachingEnabled)
|
||||
self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1)
|
||||
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout_4)
|
||||
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||
self.languageLabel = QtWidgets.QLabel(self.groupBox)
|
||||
self.coverShadows = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.coverShadows.setObjectName("coverShadows")
|
||||
self.horizontalLayout_3.addWidget(self.coverShadows)
|
||||
self.performCulling = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.performCulling.setObjectName("performCulling")
|
||||
self.horizontalLayout_3.addWidget(self.performCulling)
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout_3)
|
||||
self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_9.setObjectName("horizontalLayout_9")
|
||||
self.autoTags = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.autoTags.setObjectName("autoTags")
|
||||
self.horizontalLayout_9.addWidget(self.autoTags)
|
||||
self.attenuateTitles = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.attenuateTitles.setObjectName("attenuateTitles")
|
||||
self.horizontalLayout_9.addWidget(self.attenuateTitles)
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout_9)
|
||||
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
|
||||
self.autoCover = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.autoCover.setObjectName("autoCover")
|
||||
self.horizontalLayout_16.addWidget(self.autoCover)
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout_16)
|
||||
self.gridLayout_4.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
|
||||
self.verticalLayout.addWidget(self.groupBox)
|
||||
self.groupBox_2 = QtWidgets.QGroupBox(self.switchPage)
|
||||
self.groupBox_2.setObjectName("groupBox_2")
|
||||
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox_2)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_3.setObjectName("verticalLayout_3")
|
||||
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
|
||||
self.hideScrollBars = QtWidgets.QCheckBox(self.groupBox_2)
|
||||
self.hideScrollBars.setObjectName("hideScrollBars")
|
||||
self.horizontalLayout_6.addWidget(self.hideScrollBars)
|
||||
self.cachingEnabled = QtWidgets.QCheckBox(self.groupBox_2)
|
||||
self.cachingEnabled.setObjectName("cachingEnabled")
|
||||
self.horizontalLayout_6.addWidget(self.cachingEnabled)
|
||||
self.verticalLayout_3.addLayout(self.horizontalLayout_6)
|
||||
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_12.setObjectName("horizontalLayout_12")
|
||||
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
|
||||
self.smallIncrementLabel = QtWidgets.QLabel(self.groupBox_2)
|
||||
self.smallIncrementLabel.setObjectName("smallIncrementLabel")
|
||||
self.horizontalLayout_15.addWidget(self.smallIncrementLabel)
|
||||
self.smallIncrementBox = QtWidgets.QSpinBox(self.groupBox_2)
|
||||
self.smallIncrementBox.setMinimum(4)
|
||||
self.smallIncrementBox.setMaximum(10)
|
||||
self.smallIncrementBox.setProperty("value", 4)
|
||||
self.smallIncrementBox.setObjectName("smallIncrementBox")
|
||||
self.horizontalLayout_15.addWidget(self.smallIncrementBox)
|
||||
self.largeIncrementLabel = QtWidgets.QLabel(self.groupBox_2)
|
||||
self.largeIncrementLabel.setObjectName("largeIncrementLabel")
|
||||
self.horizontalLayout_15.addWidget(self.largeIncrementLabel)
|
||||
self.largeIncrementBox = QtWidgets.QSpinBox(self.groupBox_2)
|
||||
self.largeIncrementBox.setMinimum(1)
|
||||
self.largeIncrementBox.setMaximum(10)
|
||||
self.largeIncrementBox.setProperty("value", 2)
|
||||
self.largeIncrementBox.setObjectName("largeIncrementBox")
|
||||
self.horizontalLayout_15.addWidget(self.largeIncrementBox)
|
||||
self.horizontalLayout_12.addLayout(self.horizontalLayout_15)
|
||||
self.verticalLayout_3.addLayout(self.horizontalLayout_12)
|
||||
self.horizontalLayout_8 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.languageLabel = QtWidgets.QLabel(self.groupBox_2)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
@@ -72,70 +165,240 @@ class Ui_Dialog(object):
|
||||
self.languageLabel.setSizePolicy(sizePolicy)
|
||||
self.languageLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
|
||||
self.languageLabel.setObjectName("languageLabel")
|
||||
self.horizontalLayout_3.addWidget(self.languageLabel)
|
||||
self.languageBox = QtWidgets.QComboBox(self.groupBox)
|
||||
self.horizontalLayout_5.addWidget(self.languageLabel)
|
||||
self.languageBox = QtWidgets.QComboBox(self.groupBox_2)
|
||||
self.languageBox.setObjectName("languageBox")
|
||||
self.horizontalLayout_3.addWidget(self.languageBox)
|
||||
self.horizontalLayout_6.addLayout(self.horizontalLayout_3)
|
||||
self.hideScrollBars = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.hideScrollBars.setObjectName("hideScrollBars")
|
||||
self.horizontalLayout_6.addWidget(self.hideScrollBars)
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.label = QtWidgets.QLabel(self.groupBox)
|
||||
self.label.setObjectName("label")
|
||||
self.horizontalLayout_5.addWidget(self.label)
|
||||
self.darkIconsRadio = QtWidgets.QRadioButton(self.groupBox)
|
||||
self.darkIconsRadio.setObjectName("darkIconsRadio")
|
||||
self.horizontalLayout_5.addWidget(self.darkIconsRadio)
|
||||
self.lightIconsRadio = QtWidgets.QRadioButton(self.groupBox)
|
||||
self.lightIconsRadio.setObjectName("lightIconsRadio")
|
||||
self.horizontalLayout_5.addWidget(self.lightIconsRadio)
|
||||
self.horizontalLayout_6.addLayout(self.horizontalLayout_5)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_6, 2, 0, 1, 1)
|
||||
self.verticalLayout_2.addWidget(self.groupBox)
|
||||
self.gridLayout_3.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
|
||||
self.horizontalLayout_5.addWidget(self.languageBox)
|
||||
self.horizontalLayout_8.addLayout(self.horizontalLayout_5)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.scrollSpeedLabel = QtWidgets.QLabel(self.groupBox_2)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.scrollSpeedLabel.sizePolicy().hasHeightForWidth())
|
||||
self.scrollSpeedLabel.setSizePolicy(sizePolicy)
|
||||
self.scrollSpeedLabel.setObjectName("scrollSpeedLabel")
|
||||
self.horizontalLayout.addWidget(self.scrollSpeedLabel)
|
||||
self.scrollSpeedSlider = QtWidgets.QSlider(self.groupBox_2)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.scrollSpeedSlider.sizePolicy().hasHeightForWidth())
|
||||
self.scrollSpeedSlider.setSizePolicy(sizePolicy)
|
||||
self.scrollSpeedSlider.setMinimum(3)
|
||||
self.scrollSpeedSlider.setMaximum(15)
|
||||
self.scrollSpeedSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.scrollSpeedSlider.setObjectName("scrollSpeedSlider")
|
||||
self.horizontalLayout.addWidget(self.scrollSpeedSlider)
|
||||
self.horizontalLayout_8.addLayout(self.horizontalLayout)
|
||||
self.verticalLayout_3.addLayout(self.horizontalLayout_8)
|
||||
self.gridLayout_3.addLayout(self.verticalLayout_3, 2, 0, 1, 1)
|
||||
self.verticalLayout.addWidget(self.groupBox_2)
|
||||
self.gridLayout_2.addLayout(self.verticalLayout, 0, 0, 1, 1)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem, 1, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.switchPage)
|
||||
self.annotationsPage = QtWidgets.QWidget()
|
||||
self.annotationsPage.setObjectName("annotationsPage")
|
||||
self.gridLayout_7 = QtWidgets.QGridLayout(self.annotationsPage)
|
||||
self.gridLayout_7.setObjectName("gridLayout_7")
|
||||
self.tabWidget = QtWidgets.QTabWidget(self.annotationsPage)
|
||||
self.tabWidget.setObjectName("tabWidget")
|
||||
self.textTab = QtWidgets.QWidget()
|
||||
self.textTab.setObjectName("textTab")
|
||||
self.gridLayout_8 = QtWidgets.QGridLayout(self.textTab)
|
||||
self.gridLayout_8.setObjectName("gridLayout_8")
|
||||
self.verticalLayout_5 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_5.setObjectName("verticalLayout_5")
|
||||
self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_11.setObjectName("horizontalLayout_11")
|
||||
self.verticalLayout_6 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_6.setObjectName("verticalLayout_6")
|
||||
spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_6.addItem(spacerItem1)
|
||||
self.newAnnotation = QtWidgets.QPushButton(self.textTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.newAnnotation.sizePolicy().hasHeightForWidth())
|
||||
self.newAnnotation.setSizePolicy(sizePolicy)
|
||||
self.newAnnotation.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.newAnnotation.setMaximumSize(QtCore.QSize(45, 16777215))
|
||||
self.newAnnotation.setText("")
|
||||
self.newAnnotation.setObjectName("newAnnotation")
|
||||
self.verticalLayout_6.addWidget(self.newAnnotation)
|
||||
self.deleteAnnotation = QtWidgets.QPushButton(self.textTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.deleteAnnotation.sizePolicy().hasHeightForWidth())
|
||||
self.deleteAnnotation.setSizePolicy(sizePolicy)
|
||||
self.deleteAnnotation.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.deleteAnnotation.setMaximumSize(QtCore.QSize(45, 16777215))
|
||||
self.deleteAnnotation.setText("")
|
||||
self.deleteAnnotation.setObjectName("deleteAnnotation")
|
||||
self.verticalLayout_6.addWidget(self.deleteAnnotation)
|
||||
self.editAnnotation = QtWidgets.QPushButton(self.textTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.editAnnotation.sizePolicy().hasHeightForWidth())
|
||||
self.editAnnotation.setSizePolicy(sizePolicy)
|
||||
self.editAnnotation.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.editAnnotation.setMaximumSize(QtCore.QSize(45, 16777215))
|
||||
self.editAnnotation.setText("")
|
||||
self.editAnnotation.setObjectName("editAnnotation")
|
||||
self.verticalLayout_6.addWidget(self.editAnnotation)
|
||||
self.moveUp = QtWidgets.QPushButton(self.textTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.moveUp.sizePolicy().hasHeightForWidth())
|
||||
self.moveUp.setSizePolicy(sizePolicy)
|
||||
self.moveUp.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.moveUp.setMaximumSize(QtCore.QSize(45, 16777215))
|
||||
self.moveUp.setText("")
|
||||
self.moveUp.setObjectName("moveUp")
|
||||
self.verticalLayout_6.addWidget(self.moveUp)
|
||||
self.moveDown = QtWidgets.QPushButton(self.textTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.moveDown.sizePolicy().hasHeightForWidth())
|
||||
self.moveDown.setSizePolicy(sizePolicy)
|
||||
self.moveDown.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.moveDown.setMaximumSize(QtCore.QSize(45, 16777215))
|
||||
self.moveDown.setText("")
|
||||
self.moveDown.setObjectName("moveDown")
|
||||
self.verticalLayout_6.addWidget(self.moveDown)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_6.addItem(spacerItem2)
|
||||
self.horizontalLayout_11.addLayout(self.verticalLayout_6)
|
||||
self.annotationsList = QtWidgets.QListView(self.textTab)
|
||||
self.annotationsList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.annotationsList.setProperty("showDropIndicator", False)
|
||||
self.annotationsList.setDefaultDropAction(QtCore.Qt.IgnoreAction)
|
||||
self.annotationsList.setObjectName("annotationsList")
|
||||
self.horizontalLayout_11.addWidget(self.annotationsList)
|
||||
self.verticalLayout_5.addLayout(self.horizontalLayout_11)
|
||||
self.gridLayout_8.addLayout(self.verticalLayout_5, 0, 0, 1, 1)
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
self.previewView = QtWidgets.QTextBrowser(self.textTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.previewView.sizePolicy().hasHeightForWidth())
|
||||
self.previewView.setSizePolicy(sizePolicy)
|
||||
self.previewView.setMaximumSize(QtCore.QSize(16777215, 100))
|
||||
self.previewView.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.previewView.setObjectName("previewView")
|
||||
self.horizontalLayout_2.addWidget(self.previewView)
|
||||
self.gridLayout_8.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
||||
self.tabWidget.addTab(self.textTab, "")
|
||||
self.imageTab = QtWidgets.QWidget()
|
||||
self.imageTab.setObjectName("imageTab")
|
||||
self.tabWidget.addTab(self.imageTab, "")
|
||||
self.gridLayout_7.addWidget(self.tabWidget, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.annotationsPage)
|
||||
self.aboutPage = QtWidgets.QWidget()
|
||||
self.aboutPage.setObjectName("aboutPage")
|
||||
self.gridLayout_9 = QtWidgets.QGridLayout(self.aboutPage)
|
||||
self.gridLayout_9.setObjectName("gridLayout_9")
|
||||
self.aboutTabWidget = QtWidgets.QTabWidget(self.aboutPage)
|
||||
self.aboutTabWidget.setObjectName("aboutTabWidget")
|
||||
self.aboutTab = QtWidgets.QWidget()
|
||||
self.aboutTab.setObjectName("aboutTab")
|
||||
self.gridLayout_6 = QtWidgets.QGridLayout(self.aboutTab)
|
||||
self.gridLayout_6.setObjectName("gridLayout_6")
|
||||
self.aboutBox = QtWidgets.QTextBrowser(self.aboutTab)
|
||||
self.aboutBox.setOpenExternalLinks(True)
|
||||
self.aboutBox.setOpenLinks(False)
|
||||
self.aboutBox.setObjectName("aboutBox")
|
||||
self.gridLayout_6.addWidget(self.aboutBox, 0, 0, 1, 1)
|
||||
self.aboutTabWidget.addTab(self.aboutTab, "")
|
||||
self.logTab = QtWidgets.QWidget()
|
||||
self.logTab.setObjectName("logTab")
|
||||
self.gridLayout_10 = QtWidgets.QGridLayout(self.logTab)
|
||||
self.gridLayout_10.setObjectName("gridLayout_10")
|
||||
self.logBox = QtWidgets.QPlainTextEdit(self.logTab)
|
||||
self.logBox.setObjectName("logBox")
|
||||
self.gridLayout_10.addWidget(self.logBox, 0, 0, 1, 1)
|
||||
self.aboutTabWidget.addTab(self.logTab, "")
|
||||
self.gridLayout_9.addWidget(self.aboutTabWidget, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.aboutPage)
|
||||
self.verticalLayout_4.addWidget(self.stackedWidget)
|
||||
self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
|
||||
self.resetButton = QtWidgets.QPushButton(Dialog)
|
||||
self.resetButton.setObjectName("resetButton")
|
||||
self.horizontalLayout_10.addWidget(self.resetButton)
|
||||
self.clearLogButton = QtWidgets.QPushButton(Dialog)
|
||||
self.clearLogButton.setObjectName("clearLogButton")
|
||||
self.horizontalLayout_10.addWidget(self.clearLogButton)
|
||||
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_10.addItem(spacerItem3)
|
||||
self.okButton = QtWidgets.QPushButton(Dialog)
|
||||
self.okButton.setObjectName("okButton")
|
||||
self.horizontalLayout_2.addWidget(self.okButton)
|
||||
self.horizontalLayout_10.addWidget(self.okButton)
|
||||
self.cancelButton = QtWidgets.QPushButton(Dialog)
|
||||
self.cancelButton.setObjectName("cancelButton")
|
||||
self.horizontalLayout_2.addWidget(self.cancelButton)
|
||||
self.aboutButton = QtWidgets.QPushButton(Dialog)
|
||||
self.aboutButton.setCheckable(True)
|
||||
self.aboutButton.setObjectName("aboutButton")
|
||||
self.horizontalLayout_2.addWidget(self.aboutButton)
|
||||
self.gridLayout_3.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
||||
self.horizontalLayout_10.addWidget(self.cancelButton)
|
||||
self.verticalLayout_4.addLayout(self.horizontalLayout_10)
|
||||
self.gridLayout.addLayout(self.verticalLayout_4, 0, 1, 1, 1)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
self.aboutTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(Dialog)
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Dialog.setWindowTitle(_translate("Dialog", "Settings"))
|
||||
self.groupBox_2.setTitle(_translate("Dialog", "Library"))
|
||||
self.groupBox.setTitle(_translate("Dialog", "Switches"))
|
||||
self.refreshLibrary.setText(_translate("Dialog", "Startup: Refresh library"))
|
||||
self.fileRemember.setText(_translate("Dialog", "Remember open files"))
|
||||
self.performCulling.setToolTip(_translate("Dialog", "Enabling reduces startup time and memory usage"))
|
||||
self.performCulling.setText(_translate("Dialog", "Load covers only when needed"))
|
||||
self.coverShadows.setText(_translate("Dialog", "Cover shadows"))
|
||||
self.autoTags.setText(_translate("Dialog", "Generate tags from files"))
|
||||
self.cachingEnabled.setToolTip(_translate("Dialog", "Greatly reduces page transition time at the cost of more memory"))
|
||||
self.cachingEnabled.setText(_translate("Dialog", "Cache comic / pdf pages"))
|
||||
self.languageLabel.setText(_translate("Dialog", "Dictionary:"))
|
||||
self.hideScrollBars.setToolTip(_translate("Dialog", "Horizontal scrolling with Alt + Scroll\n"
|
||||
"Reopen book to see changes"))
|
||||
self.hideScrollBars.setText(_translate("Dialog", "Hide scrollbars when reading"))
|
||||
self.groupBox.setTitle(_translate("Dialog", "Library"))
|
||||
self.readAtLabel.setText(_translate("Dialog", "Consider book read at percent"))
|
||||
self.label.setToolTip(_translate("Dialog", "Restart application to see changes"))
|
||||
self.label.setText(_translate("Dialog", "Icon theme: "))
|
||||
self.darkIconsRadio.setToolTip(_translate("Dialog", "Restart application to see changes"))
|
||||
self.darkIconsRadio.setText(_translate("Dialog", "Dar&k"))
|
||||
self.darkIconsRadio.setText(_translate("Dialog", "&Dark"))
|
||||
self.lightIconsRadio.setToolTip(_translate("Dialog", "Restart application to see changes"))
|
||||
self.lightIconsRadio.setText(_translate("Dialog", "&Light"))
|
||||
self.lightIconsRadio.setText(_translate("Dialog", "L&ight"))
|
||||
self.refreshLibrary.setText(_translate("Dialog", "Startup: Refresh library"))
|
||||
self.fileRemember.setText(_translate("Dialog", "Remember open files"))
|
||||
self.coverShadows.setText(_translate("Dialog", "Cover shadows"))
|
||||
self.performCulling.setToolTip(_translate("Dialog", "Enabling reduces startup time and memory usage"))
|
||||
self.performCulling.setText(_translate("Dialog", "Load covers only when needed"))
|
||||
self.autoTags.setText(_translate("Dialog", "Generate tags from files"))
|
||||
self.attenuateTitles.setText(_translate("Dialog", "Shrink long book titles"))
|
||||
self.autoCover.setToolTip(_translate("Dialog", "<html><head/><body><p>Attempt to download missing book covers from Google books - SLOW</p></body></html>"))
|
||||
self.autoCover.setText(_translate("Dialog", "Download missing covers"))
|
||||
self.groupBox_2.setTitle(_translate("Dialog", "Reading"))
|
||||
self.hideScrollBars.setToolTip(_translate("Dialog", "Horizontal scrolling with Alt + Scroll\n"
|
||||
"Reopen book to see changes"))
|
||||
self.hideScrollBars.setText(_translate("Dialog", "Hide scrollbars when reading"))
|
||||
self.cachingEnabled.setToolTip(_translate("Dialog", "Greatly reduces page transition time at the cost of more memory"))
|
||||
self.cachingEnabled.setText(_translate("Dialog", "Cache comic / pdf pages"))
|
||||
self.smallIncrementLabel.setToolTip(_translate("Dialog", "<html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html>"))
|
||||
self.smallIncrementLabel.setText(_translate("Dialog", "Small increment"))
|
||||
self.smallIncrementBox.setToolTip(_translate("Dialog", "<html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html>"))
|
||||
self.largeIncrementLabel.setToolTip(_translate("Dialog", "<html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html>"))
|
||||
self.largeIncrementLabel.setText(_translate("Dialog", "Large increment"))
|
||||
self.largeIncrementBox.setToolTip(_translate("Dialog", "<html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html>"))
|
||||
self.languageLabel.setText(_translate("Dialog", "Dictionary language"))
|
||||
self.scrollSpeedLabel.setText(_translate("Dialog", "Scroll speed"))
|
||||
self.newAnnotation.setToolTip(_translate("Dialog", "New"))
|
||||
self.deleteAnnotation.setToolTip(_translate("Dialog", "Delete"))
|
||||
self.editAnnotation.setToolTip(_translate("Dialog", "Edit"))
|
||||
self.moveUp.setToolTip(_translate("Dialog", "Move Up"))
|
||||
self.moveDown.setToolTip(_translate("Dialog", "Move Down"))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.textTab), _translate("Dialog", "Text"))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.imageTab), _translate("Dialog", "Image"))
|
||||
self.aboutTabWidget.setTabText(self.aboutTabWidget.indexOf(self.aboutTab), _translate("Dialog", "About"))
|
||||
self.aboutTabWidget.setTabText(self.aboutTabWidget.indexOf(self.logTab), _translate("Dialog", "Log"))
|
||||
self.resetButton.setText(_translate("Dialog", "Reset Application"))
|
||||
self.clearLogButton.setText(_translate("Dialog", "Clear Log"))
|
||||
self.okButton.setText(_translate("Dialog", "Scan Library"))
|
||||
self.cancelButton.setText(_translate("Dialog", "Close"))
|
||||
self.aboutButton.setText(_translate("Dialog", "About"))
|
||||
|
||||
from lector.widgets import SaysHelloWhenClicked
|
||||
|
1031
lector/resources/translations/Lector_zh.ts
Normal file
@@ -1,10 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Keep in mind that all integer / boolean settings are returned as strings
|
||||
|
||||
import os
|
||||
import logging
|
||||
from ast import literal_eval
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Settings:
|
||||
def __init__(self, parent):
|
||||
@@ -103,6 +122,20 @@ class Settings:
|
||||
'cachingEnabled', 'True').capitalize())
|
||||
self.parent.settings['hide_scrollbars'] = literal_eval(self.settings.value(
|
||||
'hideScrollBars', 'False').capitalize())
|
||||
self.parent.settings['auto_cover'] = literal_eval(self.settings.value(
|
||||
'autoCover', 'False').capitalize())
|
||||
self.parent.settings['scroll_speed'] = int(self.settings.value('scrollSpeed', 7))
|
||||
self.parent.settings['consider_read_at'] = int(self.settings.value('considerReadAt', 95))
|
||||
self.parent.settings['small_increment'] = int(self.settings.value('smallIncrement', 4))
|
||||
self.parent.settings['large_increment'] = int(self.settings.value('largeIncrement', 2))
|
||||
self.parent.settings['attenuate_titles'] = literal_eval(self.settings.value(
|
||||
'attenuateTitles', 'False').capitalize())
|
||||
self.parent.settings['double_page_mode'] = literal_eval(self.settings.value(
|
||||
'doublePageMode', 'False').capitalize())
|
||||
self.parent.settings['manga_mode'] = literal_eval(self.settings.value(
|
||||
'mangaMode', 'False').capitalize())
|
||||
self.parent.settings['invert_colors'] = literal_eval(self.settings.value(
|
||||
'invertColors', 'False').capitalize())
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('dialogSettings')
|
||||
@@ -110,8 +143,16 @@ class Settings:
|
||||
'dialogBackground', QtGui.QColor().fromRgb(0, 0, 0))
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('annotations')
|
||||
self.parent.settings['annotations'] = self.settings.value(
|
||||
'annotationList', list())
|
||||
if self.parent.settings['annotations'] is None:
|
||||
self.parent.settings['annotations'] = list()
|
||||
self.settings.endGroup()
|
||||
|
||||
logger.info('Settings loaded')
|
||||
|
||||
def save_settings(self):
|
||||
print('Saving settings...')
|
||||
current_settings = self.parent.settings
|
||||
|
||||
self.settings.beginGroup('mainWindow')
|
||||
@@ -165,16 +206,31 @@ class Settings:
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('settingsSwitches')
|
||||
self.settings.setValue('rememberFiles', current_settings['remember_files'])
|
||||
self.settings.setValue('coverShadows', current_settings['cover_shadows'])
|
||||
self.settings.setValue('autoTags', current_settings['auto_tags'])
|
||||
self.settings.setValue('scanLibraryAtStart', current_settings['scan_library'])
|
||||
self.settings.setValue('performCulling', current_settings['perform_culling'])
|
||||
self.settings.setValue('dictionaryLanguage', current_settings['dictionary_language'])
|
||||
self.settings.setValue('cachingEnabled', current_settings['caching_enabled'])
|
||||
self.settings.setValue('hideScrollBars', current_settings['hide_scrollbars'])
|
||||
self.settings.setValue('rememberFiles', str(current_settings['remember_files']))
|
||||
self.settings.setValue('coverShadows', str(current_settings['cover_shadows']))
|
||||
self.settings.setValue('autoTags', str(current_settings['auto_tags']))
|
||||
self.settings.setValue('scanLibraryAtStart', str(current_settings['scan_library']))
|
||||
self.settings.setValue('performCulling', str(current_settings['perform_culling']))
|
||||
self.settings.setValue('dictionaryLanguage', str(current_settings['dictionary_language']))
|
||||
self.settings.setValue('cachingEnabled', str(current_settings['caching_enabled']))
|
||||
self.settings.setValue('hideScrollBars', str(current_settings['hide_scrollbars']))
|
||||
self.settings.setValue('attenuateTitles', str(current_settings['attenuate_titles']))
|
||||
self.settings.setValue('autoCover', str(current_settings['auto_cover']))
|
||||
self.settings.setValue('scrollSpeed', current_settings['scroll_speed'])
|
||||
self.settings.setValue('considerReadAt', current_settings['consider_read_at'])
|
||||
self.settings.setValue('doublePageMode', str(current_settings['double_page_mode']))
|
||||
self.settings.setValue('mangaMode', str(current_settings['manga_mode']))
|
||||
self.settings.setValue('invertColors', str(current_settings['invert_colors']))
|
||||
self.settings.setValue('smallIncrement', current_settings['small_increment'])
|
||||
self.settings.setValue('largeIncrement', current_settings['large_increment'])
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('dialogSettings')
|
||||
self.settings.setValue('dialogBackground', current_settings['dialog_background'])
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('annotations')
|
||||
self.settings.setValue('annotationList', current_settings['annotations'])
|
||||
self.settings.endGroup()
|
||||
|
||||
logger.info('Settings saved')
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -21,37 +19,51 @@
|
||||
|
||||
import os
|
||||
import copy
|
||||
import logging
|
||||
import pathlib
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from lector import database
|
||||
from lector.annotations import AnnotationsUI
|
||||
from lector.models import MostExcellentFileSystemModel
|
||||
from lector.threaded import BackGroundBookSearch, BackGroundBookAddition
|
||||
from lector.resources import settingswindow
|
||||
from lector.settings import Settings
|
||||
from lector.logger import logger_filename, VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
def __init__(self, parent=None):
|
||||
super(SettingsUI, self).__init__()
|
||||
self.setupUi(self)
|
||||
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
self.parent = parent
|
||||
self.database_path = self.parent.database_path
|
||||
self.main_window = parent
|
||||
self.database_path = self.main_window.database_path
|
||||
self.image_factory = self.main_window.QImageFactory
|
||||
|
||||
self.resize(self.parent.settings['settings_dialog_size'])
|
||||
self.move(self.parent.settings['settings_dialog_position'])
|
||||
# The annotation dialog will use the settings dialog as its parent
|
||||
self.annotationsDialog = AnnotationsUI(self)
|
||||
|
||||
self.resize(self.main_window.settings['settings_dialog_size'])
|
||||
self.move(self.main_window.settings['settings_dialog_position'])
|
||||
|
||||
self.aboutBox.setVisible(False)
|
||||
install_dir = os.path.realpath(__file__)
|
||||
install_dir = pathlib.Path(install_dir).parents[1]
|
||||
aboutfile_path = os.path.join(install_dir, 'lector', 'resources', 'about.html')
|
||||
with open(aboutfile_path) as about_html:
|
||||
self.aboutBox.setHtml(about_html.read())
|
||||
html = about_html.readlines()
|
||||
html.insert(
|
||||
8, f'<h3 style="text-align: center;">v{VERSION}</h3>\n')
|
||||
self.aboutBox.setHtml(''.join(html))
|
||||
|
||||
self.paths = None
|
||||
self.thread = None
|
||||
self.filesystem_model = None
|
||||
self.filesystemModel = None
|
||||
self.tag_data_copy = None
|
||||
|
||||
english_string = self._translate('SettingsUI', 'English')
|
||||
@@ -60,7 +72,7 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
languages = [english_string, spanish_string, hindi_string]
|
||||
|
||||
self.languageBox.addItems(languages)
|
||||
current_language = self.parent.settings['dictionary_language']
|
||||
current_language = self.main_window.settings['dictionary_language']
|
||||
if current_language == 'en':
|
||||
self.languageBox.setCurrentIndex(0)
|
||||
elif current_language == 'es':
|
||||
@@ -73,10 +85,9 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self._translate('SettingsUI', 'Save changes and start library scan'))
|
||||
self.okButton.clicked.connect(self.start_library_scan)
|
||||
self.cancelButton.clicked.connect(self.cancel_pressed)
|
||||
self.aboutButton.clicked.connect(self.about_pressed)
|
||||
|
||||
# Radio buttons
|
||||
if self.parent.settings['icon_theme'] == 'DarkIcons':
|
||||
if self.main_window.settings['icon_theme'] == 'DarkIcons':
|
||||
self.darkIconsRadio.setChecked(True)
|
||||
else:
|
||||
self.lightIconsRadio.setChecked(True)
|
||||
@@ -84,13 +95,19 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self.lightIconsRadio.clicked.connect(self.change_icon_theme)
|
||||
|
||||
# Check boxes
|
||||
self.autoTags.setChecked(self.parent.settings['auto_tags'])
|
||||
self.coverShadows.setChecked(self.parent.settings['cover_shadows'])
|
||||
self.refreshLibrary.setChecked(self.parent.settings['scan_library'])
|
||||
self.fileRemember.setChecked(self.parent.settings['remember_files'])
|
||||
self.performCulling.setChecked(self.parent.settings['perform_culling'])
|
||||
self.cachingEnabled.setChecked(self.parent.settings['caching_enabled'])
|
||||
self.hideScrollBars.setChecked(self.parent.settings['hide_scrollbars'])
|
||||
self.autoTags.setChecked(self.main_window.settings['auto_tags'])
|
||||
self.coverShadows.setChecked(self.main_window.settings['cover_shadows'])
|
||||
self.refreshLibrary.setChecked(self.main_window.settings['scan_library'])
|
||||
self.fileRemember.setChecked(self.main_window.settings['remember_files'])
|
||||
self.performCulling.setChecked(self.main_window.settings['perform_culling'])
|
||||
self.cachingEnabled.setChecked(self.main_window.settings['caching_enabled'])
|
||||
self.hideScrollBars.setChecked(self.main_window.settings['hide_scrollbars'])
|
||||
self.attenuateTitles.setChecked(self.main_window.settings['attenuate_titles'])
|
||||
self.autoCover.setChecked(self.main_window.settings['auto_cover'])
|
||||
self.scrollSpeedSlider.setValue(self.main_window.settings['scroll_speed'])
|
||||
self.readAtPercent.setValue(self.main_window.settings['consider_read_at'])
|
||||
self.smallIncrementBox.setValue(self.main_window.settings['small_increment'])
|
||||
self.largeIncrementBox.setValue(self.main_window.settings['large_increment'])
|
||||
|
||||
self.autoTags.clicked.connect(self.manage_checkboxes)
|
||||
self.coverShadows.clicked.connect(self.manage_checkboxes)
|
||||
@@ -99,10 +116,102 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self.performCulling.clicked.connect(self.manage_checkboxes)
|
||||
self.cachingEnabled.clicked.connect(self.manage_checkboxes)
|
||||
self.hideScrollBars.clicked.connect(self.manage_checkboxes)
|
||||
self.attenuateTitles.clicked.connect(self.manage_checkboxes)
|
||||
self.autoCover.clicked.connect(self.manage_checkboxes)
|
||||
self.scrollSpeedSlider.valueChanged.connect(self.change_scroll_speed)
|
||||
self.readAtPercent.valueChanged.connect(self.change_read_at)
|
||||
self.smallIncrementBox.valueChanged.connect(self.change_increment)
|
||||
self.largeIncrementBox.valueChanged.connect(self.change_increment)
|
||||
|
||||
# Generate the QStandardItemModel for the listView
|
||||
self.listModel = QtGui.QStandardItemModel(self.listView)
|
||||
|
||||
library_string = self._translate('SettingsUI', 'Library')
|
||||
switches_string = self._translate('SettingsUI', 'Switches')
|
||||
annotations_string = self._translate('SettingsUI', 'Annotations')
|
||||
about_string = self._translate('SettingsUI', 'About')
|
||||
list_options = [
|
||||
library_string, switches_string, annotations_string, about_string]
|
||||
|
||||
icon_dict = {
|
||||
0: 'view-readermode',
|
||||
1: 'switches',
|
||||
2: 'annotate',
|
||||
3: 'about'}
|
||||
|
||||
for count, i in enumerate(list_options):
|
||||
item = QtGui.QStandardItem()
|
||||
item.setText(i)
|
||||
this_icon = icon_dict[count]
|
||||
item.setIcon(
|
||||
self.main_window.QImageFactory.get_image(this_icon))
|
||||
self.listModel.appendRow(item)
|
||||
self.listView.setModel(self.listModel)
|
||||
|
||||
# Custom signal to account for page changes
|
||||
self.listView.newIndexSignal.connect(self.list_index_changed)
|
||||
|
||||
# Annotation related buttons
|
||||
# Icon names
|
||||
self.newAnnotation.setIcon(self.image_factory.get_image('add'))
|
||||
self.deleteAnnotation.setIcon(self.image_factory.get_image('remove'))
|
||||
self.editAnnotation.setIcon(self.image_factory.get_image('edit-rename'))
|
||||
self.moveUp.setIcon(self.image_factory.get_image('arrow-up'))
|
||||
self.moveDown.setIcon(self.image_factory.get_image('arrow-down'))
|
||||
|
||||
# Icon sizes
|
||||
self.newAnnotation.setIconSize(QtCore.QSize(24, 24))
|
||||
self.deleteAnnotation.setIconSize(QtCore.QSize(24, 24))
|
||||
self.editAnnotation.setIconSize(QtCore.QSize(24, 24))
|
||||
self.moveUp.setIconSize(QtCore.QSize(24, 24))
|
||||
self.moveDown.setIconSize(QtCore.QSize(24, 24))
|
||||
|
||||
self.annotationsList.clicked.connect(self.load_annotation)
|
||||
self.annotationsList.doubleClicked.connect(self.editAnnotation.click)
|
||||
self.newAnnotation.clicked.connect(self.add_annotation)
|
||||
self.deleteAnnotation.clicked.connect(self.delete_annotation)
|
||||
self.editAnnotation.clicked.connect(self.load_annotation)
|
||||
self.moveUp.clicked.connect(self.move_annotation)
|
||||
self.moveDown.clicked.connect(self.move_annotation)
|
||||
|
||||
# Generate annotation settings
|
||||
self.annotationModel = QtGui.QStandardItemModel()
|
||||
self.generate_annotations()
|
||||
|
||||
# Generate the filesystem treeView
|
||||
self.generate_tree()
|
||||
|
||||
# About... About
|
||||
self.aboutTabWidget.setDocumentMode(True)
|
||||
self.aboutTabWidget.setContentsMargins(0, 0, 0, 0)
|
||||
self.logBox.setReadOnly(True)
|
||||
|
||||
# About buttons
|
||||
self.resetButton.clicked.connect(self.delete_database)
|
||||
self.clearLogButton.clicked.connect(self.clear_log)
|
||||
|
||||
# Hide the image annotation tab
|
||||
# TODO
|
||||
# Maybe get off your lazy ass and write something for this
|
||||
self.tabWidget.setContentsMargins(0, 0, 0, 0)
|
||||
self.tabWidget.tabBar().setVisible(False)
|
||||
|
||||
def list_index_changed(self, index):
|
||||
switch_to = index.row()
|
||||
self.stackedWidget.setCurrentIndex(switch_to)
|
||||
|
||||
valid_buttons = {
|
||||
0: (self.okButton,),
|
||||
3: (self.resetButton, self.clearLogButton),}
|
||||
|
||||
for i in valid_buttons:
|
||||
if i == switch_to:
|
||||
for j in valid_buttons[i]:
|
||||
j.setVisible(True)
|
||||
else:
|
||||
for j in valid_buttons[i]:
|
||||
j.setVisible(False)
|
||||
|
||||
def generate_tree(self):
|
||||
# Fetch all directories in the database
|
||||
paths = database.DatabaseFunctions(
|
||||
@@ -112,10 +221,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
{'Path': ''},
|
||||
'LIKE')
|
||||
|
||||
self.parent.generate_library_filter_menu(paths)
|
||||
self.main_window.generate_library_filter_menu(paths)
|
||||
directory_data = {}
|
||||
if not paths:
|
||||
print('Database returned no paths for settings...')
|
||||
logger.warning('No book paths saved')
|
||||
else:
|
||||
# Convert to the dictionary format that is
|
||||
# to be fed into the QFileSystemModel
|
||||
@@ -125,9 +234,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
'tags': i[2],
|
||||
'check_state': i[3]}
|
||||
|
||||
self.filesystem_model = MostExcellentFileSystemModel(directory_data)
|
||||
self.filesystem_model.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
|
||||
self.treeView.setModel(self.filesystem_model)
|
||||
self.filesystemModel = MostExcellentFileSystemModel(directory_data)
|
||||
self.filesystemModel.setFilter(
|
||||
QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
|
||||
self.treeView.setModel(self.filesystemModel)
|
||||
|
||||
# TODO
|
||||
# This here might break on them pestilent non unixy OSes
|
||||
@@ -135,11 +245,12 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
|
||||
root_directory = QtCore.QDir().rootPath()
|
||||
self.treeView.setRootIndex(
|
||||
self.filesystem_model.setRootPath(root_directory))
|
||||
self.filesystemModel.setRootPath(root_directory))
|
||||
|
||||
# Set the treeView and QFileSystemModel to its desired state
|
||||
selected_paths = [
|
||||
i for i in directory_data if directory_data[i]['check_state'] == QtCore.Qt.Checked]
|
||||
i for i in directory_data
|
||||
if directory_data[i]['check_state'] == QtCore.Qt.Checked]
|
||||
expand_paths = set()
|
||||
for i in selected_paths:
|
||||
|
||||
@@ -157,10 +268,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
expand_paths.remove(root_directory)
|
||||
|
||||
for i in expand_paths:
|
||||
this_index = self.filesystem_model.index(i)
|
||||
this_index = self.filesystemModel.index(i)
|
||||
self.treeView.expand(this_index)
|
||||
|
||||
header_sizes = self.parent.settings['settings_dialog_headers']
|
||||
header_sizes = self.main_window.settings['settings_dialog_headers']
|
||||
if header_sizes:
|
||||
for count, i in enumerate((0, 4)):
|
||||
self.treeView.setColumnWidth(i, int(header_sizes[count]))
|
||||
@@ -178,7 +289,7 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self.hide()
|
||||
|
||||
data_pairs = []
|
||||
for i in self.filesystem_model.tag_data.items():
|
||||
for i in self.filesystemModel.tag_data.items():
|
||||
data_pairs.append([
|
||||
i[0], i[1]['name'], i[1]['tags'], i[1]['check_state']
|
||||
])
|
||||
@@ -187,33 +298,37 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self.database_path).set_library_paths(data_pairs)
|
||||
|
||||
if not data_pairs:
|
||||
logger.error('Can\'t scan - No book paths saved')
|
||||
try:
|
||||
if self.sender().objectName() == 'reloadLibrary':
|
||||
self.show()
|
||||
treeViewIndex = self.listModel.index(0, 0)
|
||||
self.listView.setCurrentIndex(treeViewIndex)
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
database.DatabaseFunctions(
|
||||
self.database_path).delete_from_database('*', '*')
|
||||
|
||||
self.parent.lib_ref.generate_model('build')
|
||||
self.parent.lib_ref.generate_proxymodels()
|
||||
self.parent.generate_library_filter_menu()
|
||||
self.main_window.lib_ref.generate_model('build')
|
||||
self.main_window.lib_ref.generate_proxymodels()
|
||||
self.main_window.generate_library_filter_menu()
|
||||
|
||||
return
|
||||
|
||||
# Update the main window library filter menu
|
||||
self.parent.generate_library_filter_menu(data_pairs)
|
||||
self.parent.set_library_filter()
|
||||
self.main_window.generate_library_filter_menu(data_pairs)
|
||||
self.main_window.set_library_filter()
|
||||
|
||||
# Disallow rechecking until the first check completes
|
||||
self.okButton.setEnabled(False)
|
||||
self.parent.reloadLibrary.setEnabled(False)
|
||||
self.main_window.libraryToolBar.reloadLibraryButton.setEnabled(False)
|
||||
self.okButton.setToolTip(
|
||||
self._translate('SettingsUI', 'Library scan in progress...'))
|
||||
|
||||
# Traverse directories looking for files
|
||||
self.parent.statusMessage.setText(
|
||||
self.main_window.statusMessage.setText(
|
||||
self._translate('SettingsUI', 'Checking library folders'))
|
||||
self.thread = BackGroundBookSearch(data_pairs)
|
||||
self.thread.finished.connect(self.finished_iterating)
|
||||
@@ -223,22 +338,24 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
# The books the search thread has found
|
||||
# are now in self.thread.valid_files
|
||||
if not self.thread.valid_files:
|
||||
self.parent.move_on()
|
||||
self.main_window.move_on()
|
||||
return
|
||||
|
||||
# Hey, messaging is important, okay?
|
||||
self.parent.sorterProgress.setVisible(True)
|
||||
self.parent.statusMessage.setText(
|
||||
self.main_window.statusBar.setVisible(True)
|
||||
self.main_window.sorterProgress.setVisible(True)
|
||||
self.main_window.statusMessage.setText(
|
||||
self._translate('SettingsUI', 'Parsing files'))
|
||||
|
||||
# We now create a new thread to put those files into the database
|
||||
self.thread = BackGroundBookAddition(
|
||||
self.thread.valid_files, self.database_path, 'automatic', self.parent)
|
||||
self.thread.finished.connect(self.parent.move_on)
|
||||
self.thread.valid_files, self.database_path, 'automatic', self.main_window)
|
||||
self.thread.finished.connect(
|
||||
lambda: self.main_window.move_on(self.thread.errors))
|
||||
self.thread.start()
|
||||
|
||||
def cancel_pressed(self):
|
||||
self.filesystem_model.tag_data = copy.deepcopy(self.tag_data_copy)
|
||||
self.filesystemModel.tag_data = copy.deepcopy(self.tag_data_copy)
|
||||
self.hide()
|
||||
|
||||
def hideEvent(self, event):
|
||||
@@ -246,35 +363,57 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
event.accept()
|
||||
|
||||
def showEvent(self, event):
|
||||
self.tag_data_copy = copy.deepcopy(self.filesystem_model.tag_data)
|
||||
# Load log into the plainTextEdit
|
||||
with open(logger_filename) as infile:
|
||||
log_text = infile.read()
|
||||
self.logBox.setPlainText(log_text)
|
||||
# Annotation preview
|
||||
self.format_preview()
|
||||
# Make copy of tags in case of a nope.jpg
|
||||
self.tag_data_copy = copy.deepcopy(self.filesystemModel.tag_data)
|
||||
|
||||
event.accept()
|
||||
|
||||
def no_more_settings(self):
|
||||
self.parent.libraryToolBar.settingsButton.setChecked(False)
|
||||
self.aboutBox.hide()
|
||||
self.treeView.show()
|
||||
self.main_window.libraryToolBar.settingsButton.setChecked(False)
|
||||
self.gather_annotations()
|
||||
Settings(self.main_window).save_settings()
|
||||
Settings(self.main_window).read_settings()
|
||||
self.main_window.settings['last_open_tab'] = None # Needed to allow focus change
|
||||
# to newly opened book
|
||||
self.resizeEvent()
|
||||
|
||||
def resizeEvent(self, event=None):
|
||||
self.parent.settings['settings_dialog_size'] = self.size()
|
||||
self.parent.settings['settings_dialog_position'] = self.pos()
|
||||
self.main_window.settings['settings_dialog_size'] = self.size()
|
||||
self.main_window.settings['settings_dialog_position'] = self.pos()
|
||||
table_headers = []
|
||||
for i in [0, 4]:
|
||||
table_headers.append(self.treeView.columnWidth(i))
|
||||
self.parent.settings['settings_dialog_headers'] = table_headers
|
||||
self.main_window.settings['settings_dialog_headers'] = table_headers
|
||||
|
||||
def change_icon_theme(self):
|
||||
if self.sender() == self.darkIconsRadio:
|
||||
self.parent.settings['icon_theme'] = 'DarkIcons'
|
||||
self.main_window.settings['icon_theme'] = 'DarkIcons'
|
||||
else:
|
||||
self.parent.settings['icon_theme'] = 'LightIcons'
|
||||
self.main_window.settings['icon_theme'] = 'LightIcons'
|
||||
|
||||
def change_dictionary_language(self, event):
|
||||
language_dict = {
|
||||
0: 'en',
|
||||
1: 'es',
|
||||
2: 'hi'}
|
||||
self.parent.settings['dictionary_language'] = language_dict[self.languageBox.currentIndex()]
|
||||
self.main_window.settings[
|
||||
'dictionary_language'] = language_dict[self.languageBox.currentIndex()]
|
||||
|
||||
def change_scroll_speed(self, event=None):
|
||||
self.main_window.settings['scroll_speed'] = self.scrollSpeedSlider.value()
|
||||
|
||||
def change_read_at(self, event=None):
|
||||
self.main_window.settings['consider_read_at'] = self.readAtPercent.value()
|
||||
|
||||
def change_increment(self, event=None):
|
||||
self.main_window.settings['small_increment'] = self.smallIncrementBox.value()
|
||||
self.main_window.settings['large_increment'] = self.largeIncrementBox.value()
|
||||
|
||||
def manage_checkboxes(self, event=None):
|
||||
sender = self.sender().objectName()
|
||||
@@ -286,13 +425,136 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
'fileRemember': 'remember_files',
|
||||
'performCulling': 'perform_culling',
|
||||
'cachingEnabled': 'caching_enabled',
|
||||
'hideScrollBars': 'hide_scrollbars'}
|
||||
'hideScrollBars': 'hide_scrollbars',
|
||||
'attenuateTitles': 'attenuate_titles',
|
||||
'autoCover': 'auto_cover'}
|
||||
|
||||
self.parent.settings[sender_dict[sender]] = not self.parent.settings[sender_dict[sender]]
|
||||
self.main_window.settings[
|
||||
sender_dict[sender]] = not self.main_window.settings[sender_dict[sender]]
|
||||
|
||||
if not self.performCulling.isChecked():
|
||||
self.parent.load_all_covers()
|
||||
self.main_window.cover_functions.load_all_covers()
|
||||
|
||||
def about_pressed(self):
|
||||
self.treeView.setVisible(not self.treeView.isVisible())
|
||||
self.aboutBox.setVisible(not self.aboutBox.isVisible())
|
||||
def generate_annotations(self):
|
||||
saved_annotations = self.main_window.settings['annotations']
|
||||
|
||||
for i in saved_annotations:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setText(i['name'])
|
||||
item.setData(i, QtCore.Qt.UserRole)
|
||||
self.annotationModel.appendRow(item)
|
||||
|
||||
self.annotationsList.setModel(self.annotationModel)
|
||||
|
||||
def format_preview(self):
|
||||
# Needed to clear the preview of annotation ickiness
|
||||
cursor = QtGui.QTextCursor()
|
||||
self.previewView.setTextCursor(cursor)
|
||||
|
||||
self.previewView.setText('Vidistine nuper imagines moventes bonas?')
|
||||
profile_index = self.main_window.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.main_window.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
if not current_profile:
|
||||
return
|
||||
|
||||
font = current_profile['font']
|
||||
self.foreground = current_profile['foreground']
|
||||
background = current_profile['background']
|
||||
font_size = current_profile['font_size']
|
||||
|
||||
self.previewView.setStyleSheet(
|
||||
"QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format(
|
||||
font, font_size, self.foreground.name(), background.name()))
|
||||
|
||||
block_format = QtGui.QTextBlockFormat()
|
||||
block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
|
||||
|
||||
cursor = self.previewView.textCursor()
|
||||
while True:
|
||||
old_position = cursor.position()
|
||||
cursor.mergeBlockFormat(block_format)
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1)
|
||||
new_position = cursor.position()
|
||||
if old_position == new_position:
|
||||
break
|
||||
|
||||
def add_annotation(self):
|
||||
self.annotationsDialog.show_dialog('add')
|
||||
|
||||
def delete_annotation(self):
|
||||
selected_index = self.annotationsList.currentIndex()
|
||||
if not selected_index.isValid():
|
||||
return
|
||||
|
||||
self.annotationModel.removeRow(
|
||||
self.annotationsList.currentIndex().row())
|
||||
self.format_preview()
|
||||
self.annotationsList.clearSelection()
|
||||
|
||||
def load_annotation(self):
|
||||
selected_index = self.annotationsList.currentIndex()
|
||||
if not selected_index.isValid():
|
||||
return
|
||||
|
||||
if self.sender() == self.annotationsList:
|
||||
self.annotationsDialog.show_dialog('preview', selected_index)
|
||||
|
||||
elif self.sender() == self.editAnnotation:
|
||||
self.annotationsDialog.show_dialog('edit', selected_index)
|
||||
|
||||
def move_annotation(self):
|
||||
current_row = self.annotationsList.currentIndex().row()
|
||||
|
||||
if self.sender() == self.moveUp:
|
||||
new_row = current_row - 1
|
||||
if new_row < 0:
|
||||
return
|
||||
|
||||
elif self.sender() == self.moveDown:
|
||||
new_row = current_row + 1
|
||||
if new_row == self.annotationModel.rowCount():
|
||||
return
|
||||
|
||||
row_out = self.annotationModel.takeRow(current_row)
|
||||
self.annotationModel.insertRow(new_row, row_out)
|
||||
new_index = self.annotationModel.index(new_row, 0)
|
||||
|
||||
self.annotationsList.setCurrentIndex(new_index)
|
||||
|
||||
def gather_annotations(self):
|
||||
annotations_out = []
|
||||
for i in range(self.annotationModel.rowCount()):
|
||||
annotation_item = self.annotationModel.item(i, 0)
|
||||
annotation_data = annotation_item.data(QtCore.Qt.UserRole)
|
||||
annotations_out.append(annotation_data)
|
||||
|
||||
self.main_window.settings['annotations'] = annotations_out
|
||||
|
||||
def delete_database(self):
|
||||
def ifcontinue(box_button):
|
||||
if box_button.text() != '&Yes':
|
||||
return
|
||||
|
||||
database_filename = os.path.join(
|
||||
self.main_window.database_path, 'Lector.db')
|
||||
os.remove(database_filename)
|
||||
QtWidgets.qApp.exit()
|
||||
|
||||
# Generate a message box to confirm deletion
|
||||
confirm_deletion = QtWidgets.QMessageBox()
|
||||
deletion_prompt = self._translate(
|
||||
'SettingsUI', f'Delete database and exit?')
|
||||
confirm_deletion.setText(deletion_prompt)
|
||||
confirm_deletion.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
confirm_deletion.setWindowTitle(self._translate('SettingsUI', 'Confirm'))
|
||||
confirm_deletion.setStandardButtons(
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
confirm_deletion.buttonClicked.connect(ifcontinue)
|
||||
confirm_deletion.show()
|
||||
confirm_deletion.exec_()
|
||||
|
||||
def clear_log(self):
|
||||
self.logBox.clear()
|
||||
open(logger_filename, 'w').close()
|
||||
|
330
lector/sorter.py
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -17,52 +15,79 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# INSTRUCTIONS
|
||||
# Every parser is supposed to have the following methods, even if they return None:
|
||||
# read_book()
|
||||
# get_title()
|
||||
# get_author()
|
||||
# get_year()
|
||||
# get_cover_image()
|
||||
# get_isbn()
|
||||
# get_tags()
|
||||
# get_contents() - Should return a tuple with 0: TOC 1: special_settings (dict)
|
||||
# Parsers for files containing only images need to return only images_only = True
|
||||
|
||||
# TODO
|
||||
# Maybe shift to insert or replace instead of hash checking
|
||||
# See if you want to include a hash of the book's name and author
|
||||
# Change thread niceness
|
||||
# Every parser is supposed to have the following methods.
|
||||
# Exceptions will be caught - but that's just bad practice
|
||||
# read_book() - Initialize book
|
||||
# generate_metadata() - For addition
|
||||
# generate_content() - For reading
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import pickle
|
||||
import logging
|
||||
import hashlib
|
||||
import threading
|
||||
from multiprocessing import Pool, Manager
|
||||
from PyQt5 import QtCore, QtGui
|
||||
import importlib
|
||||
import urllib.request
|
||||
|
||||
# The multiprocessing module does not work correctly on Windows
|
||||
if sys.platform.startswith('win'):
|
||||
from multiprocessing.dummy import Pool, Manager
|
||||
thread_count = 4 # This is all on one CPU thread anyway
|
||||
else:
|
||||
from multiprocessing import Pool, Manager, cpu_count
|
||||
thread_count = cpu_count()
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from lector import database
|
||||
|
||||
from lector.parsers.pdf import ParsePDF
|
||||
from lector.parsers.epub import ParseEPUB
|
||||
from lector.parsers.mobi import ParseMOBI
|
||||
from lector.parsers.fb2 import ParseFB2
|
||||
from lector.parsers.comicbooks import ParseCOMIC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sorter = {
|
||||
'pdf': ParsePDF,
|
||||
'epub': ParseEPUB,
|
||||
'mobi': ParseMOBI,
|
||||
'azw': ParseMOBI,
|
||||
'azw3': ParseMOBI,
|
||||
'azw4': ParseMOBI,
|
||||
'prc': ParseMOBI,
|
||||
'cbz': ParseCOMIC,
|
||||
'cbr': ParseCOMIC}
|
||||
|
||||
# Check what dependencies are installed
|
||||
# pymupdf - Optional
|
||||
mupdf_check = importlib.util.find_spec('fitz')
|
||||
if mupdf_check:
|
||||
from lector.parsers.pdf import ParsePDF
|
||||
sorter['pdf'] = ParsePDF
|
||||
else:
|
||||
error_string = 'pymupdf is not installed. Will be unable to load PDFs.'
|
||||
print(error_string)
|
||||
logger.error(error_string)
|
||||
|
||||
# python-lxml - Required for everything except comics
|
||||
lxml_check = importlib.util.find_spec('lxml')
|
||||
xmltodict_check = importlib.util.find_spec('xmltodict')
|
||||
if lxml_check:
|
||||
lxml_dependent = {
|
||||
'epub': ParseEPUB,
|
||||
'mobi': ParseMOBI,
|
||||
'azw': ParseMOBI,
|
||||
'azw3': ParseMOBI,
|
||||
'azw4': ParseMOBI,
|
||||
'prc': ParseMOBI,
|
||||
'fb2': ParseFB2,
|
||||
'fb2.zip': ParseFB2}
|
||||
sorter.update(lxml_dependent)
|
||||
else:
|
||||
critical_sting = 'python-lxml / xmltodict is not installed. Only comics will load.'
|
||||
print(critical_sting)
|
||||
logger.critical(critical_sting)
|
||||
|
||||
available_parsers = [i for i in sorter]
|
||||
progressbar = None # This is populated by __main__
|
||||
progress_emitter = None # This is to be made into a global variable
|
||||
_progress_emitter = None # This is to be made into a global variable
|
||||
|
||||
|
||||
class UpdateProgress(QtCore.QObject):
|
||||
@@ -77,7 +102,7 @@ class UpdateProgress(QtCore.QObject):
|
||||
|
||||
|
||||
class BookSorter:
|
||||
def __init__(self, file_list, mode, database_path, auto_tags=True, temp_dir=None):
|
||||
def __init__(self, file_list, mode, database_path, settings, temp_dir=None):
|
||||
# Have the GUI pass a list of files straight to here
|
||||
# Then, on the basis of what is needed, pass the
|
||||
# filenames to the requisite functions
|
||||
@@ -90,13 +115,15 @@ class BookSorter:
|
||||
self.work_mode = mode[0]
|
||||
self.addition_mode = mode[1]
|
||||
self.database_path = database_path
|
||||
self.auto_tags = auto_tags
|
||||
self.auto_tags = settings['auto_tags']
|
||||
self.auto_cover = settings['auto_cover']
|
||||
self.temp_dir = temp_dir
|
||||
if database_path:
|
||||
self.database_hashes()
|
||||
|
||||
self.threading_completed = []
|
||||
self.queue = Manager().Queue()
|
||||
self.errors = Manager().list()
|
||||
self.processed_books = []
|
||||
|
||||
if self.work_mode == 'addition':
|
||||
@@ -111,14 +138,14 @@ class BookSorter:
|
||||
'LIKE')
|
||||
|
||||
if all_hashes_and_paths:
|
||||
# self.hashes = [i[0] for i in all_hashes]
|
||||
self.hashes_and_paths = {
|
||||
i[0]: i[1] for i in all_hashes_and_paths}
|
||||
|
||||
def database_entry_for_book(self, file_hash):
|
||||
database_return = database.DatabaseFunctions(
|
||||
self.database_path).fetch_data(
|
||||
('Title', 'Author', 'Year', 'ISBN', 'Tags', 'Position', 'Bookmarks'),
|
||||
('Title', 'Author', 'Year', 'ISBN', 'Tags',
|
||||
'Position', 'Bookmarks', 'CoverImage', 'Annotations'),
|
||||
'books',
|
||||
{'Hash': file_hash},
|
||||
'EQUALS')[0]
|
||||
@@ -126,7 +153,7 @@ class BookSorter:
|
||||
book_data = []
|
||||
|
||||
for count, i in enumerate(database_return):
|
||||
if count in (5, 6):
|
||||
if count in (5, 6, 8): # Position, Bookmarks, and Annotations are pickled
|
||||
if i:
|
||||
book_data.append(pickle.loads(i))
|
||||
else:
|
||||
@@ -150,7 +177,7 @@ class BookSorter:
|
||||
self.queue.put(filename)
|
||||
|
||||
# This should not get triggered in reading mode
|
||||
# IF the file is NOT being loaded into the reader,
|
||||
# IF the file is NOT being loaded into the reader
|
||||
|
||||
# Do not allow addition in case the file
|
||||
# is already in the database and it remains at its original path
|
||||
@@ -159,91 +186,120 @@ class BookSorter:
|
||||
or os.path.exists(self.hashes_and_paths[file_md5])):
|
||||
|
||||
if not self.hashes_and_paths[file_md5] == filename:
|
||||
print(f'{os.path.basename(filename)} is already in database')
|
||||
warning_string = (
|
||||
f'{os.path.basename(filename)} is already in database')
|
||||
logger.warning(warning_string)
|
||||
return
|
||||
|
||||
file_extension = os.path.splitext(filename)[1][1:]
|
||||
try:
|
||||
# Get the requisite parser from the sorter dict
|
||||
book_ref = sorter[file_extension](filename, self.temp_dir, file_md5)
|
||||
except KeyError:
|
||||
print(filename + ' has an unsupported extension')
|
||||
# This allows for eliminating issues with filenames that have
|
||||
# a dot in them. All hail the roundabout fix.
|
||||
valid_extension = False
|
||||
for i in sorter:
|
||||
if os.path.basename(filename).endswith(i):
|
||||
file_extension = i
|
||||
valid_extension = True
|
||||
break
|
||||
|
||||
if not valid_extension:
|
||||
logger.error('Unsupported extension: ' + filename)
|
||||
return
|
||||
|
||||
# Everything following this is standard
|
||||
# None values are accounted for here
|
||||
book_ref.read_book()
|
||||
if book_ref.book:
|
||||
book_ref = sorter[file_extension](filename, self.temp_dir, file_md5)
|
||||
|
||||
# None of the following have an exception type specified
|
||||
# This will keep everything from crashing, but will make
|
||||
# troubleshooting difficult
|
||||
# TODO
|
||||
# In application notifications
|
||||
|
||||
this_book = {}
|
||||
this_book[file_md5] = {
|
||||
'hash': file_md5,
|
||||
'path': filename}
|
||||
try:
|
||||
book_ref.read_book()
|
||||
except Exception as e:
|
||||
this_error = f'Error initializing: {filename}'
|
||||
self.errors.append(this_error)
|
||||
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
|
||||
return
|
||||
|
||||
# Different modes require different values
|
||||
if self.work_mode == 'addition':
|
||||
# Reduce the size of the incoming image
|
||||
# if one is found
|
||||
title = book_ref.get_title()
|
||||
this_book = {}
|
||||
this_book[file_md5] = {
|
||||
'hash': file_md5,
|
||||
'path': filename}
|
||||
|
||||
author = book_ref.get_author()
|
||||
if not author:
|
||||
author = 'Unknown'
|
||||
# Different modes require different values
|
||||
if self.work_mode == 'addition':
|
||||
try:
|
||||
metadata = book_ref.generate_metadata()
|
||||
except Exception as e:
|
||||
this_error = f'Metadata generation error: {filename}'
|
||||
self.errors.append(this_error)
|
||||
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
|
||||
return
|
||||
|
||||
year = book_ref.get_year()
|
||||
title = metadata.title
|
||||
author = metadata.author
|
||||
year = metadata.year
|
||||
isbn = metadata.isbn
|
||||
|
||||
isbn = book_ref.get_isbn()
|
||||
tags = None
|
||||
if self.auto_tags:
|
||||
tags = metadata.tags
|
||||
|
||||
tags = None
|
||||
if self.auto_tags:
|
||||
tags = book_ref.get_tags()
|
||||
cover_image_raw = metadata.cover
|
||||
if cover_image_raw:
|
||||
cover_image = resize_image(cover_image_raw)
|
||||
else:
|
||||
cover_image = None
|
||||
if self.auto_cover:
|
||||
cover_image = fetch_cover(title, author)
|
||||
|
||||
cover_image_raw = book_ref.get_cover_image()
|
||||
if cover_image_raw:
|
||||
cover_image = resize_image(cover_image_raw)
|
||||
else:
|
||||
cover_image = None
|
||||
this_book[file_md5]['cover_image'] = cover_image
|
||||
this_book[file_md5]['addition_mode'] = self.addition_mode
|
||||
|
||||
this_book[file_md5]['cover_image'] = cover_image
|
||||
this_book[file_md5]['addition_mode'] = self.addition_mode
|
||||
if self.work_mode == 'reading':
|
||||
try:
|
||||
book_breakdown = book_ref.generate_content()
|
||||
except Exception as e:
|
||||
this_error = f'Content generation error: {filename}'
|
||||
self.errors.append(this_error)
|
||||
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
|
||||
return
|
||||
|
||||
if self.work_mode == 'reading':
|
||||
all_content = book_ref.get_contents()
|
||||
|
||||
# get_contents() returns a tuple. Index 1 is a collection of
|
||||
# special settings that depend on the kind of data being parsed.
|
||||
# Currently, this includes:
|
||||
# Only images included images_only BOOL Specify only paths to images
|
||||
# File will not be cached on exit
|
||||
|
||||
content = all_content[0]
|
||||
images_only = all_content[1]['images_only']
|
||||
|
||||
if not content:
|
||||
content = [('Invalid', 'Something went horribly wrong')]
|
||||
toc = book_breakdown[0]
|
||||
content = book_breakdown[1]
|
||||
images_only = book_breakdown[2]
|
||||
|
||||
try:
|
||||
book_data = self.database_entry_for_book(file_md5)
|
||||
title = book_data[0]
|
||||
author = book_data[1]
|
||||
year = book_data[2]
|
||||
isbn = book_data[3]
|
||||
tags = book_data[4]
|
||||
position = book_data[5]
|
||||
bookmarks = book_data[6]
|
||||
except TypeError:
|
||||
logger.error(
|
||||
f'Database error: {filename}. Re-add book to program')
|
||||
return
|
||||
|
||||
this_book[file_md5]['position'] = position
|
||||
this_book[file_md5]['bookmarks'] = bookmarks
|
||||
this_book[file_md5]['content'] = content
|
||||
this_book[file_md5]['images_only'] = images_only
|
||||
title = book_data[0].replace('&', '&&')
|
||||
author = book_data[1]
|
||||
year = book_data[2]
|
||||
isbn = book_data[3]
|
||||
tags = book_data[4]
|
||||
position = book_data[5]
|
||||
bookmarks = book_data[6]
|
||||
cover = book_data[7]
|
||||
annotations = book_data[8]
|
||||
|
||||
this_book[file_md5]['title'] = title
|
||||
this_book[file_md5]['author'] = author
|
||||
this_book[file_md5]['year'] = year
|
||||
this_book[file_md5]['isbn'] = isbn
|
||||
this_book[file_md5]['tags'] = tags
|
||||
this_book[file_md5]['position'] = position
|
||||
this_book[file_md5]['bookmarks'] = bookmarks
|
||||
this_book[file_md5]['toc'] = toc
|
||||
this_book[file_md5]['content'] = content
|
||||
this_book[file_md5]['images_only'] = images_only
|
||||
this_book[file_md5]['cover'] = cover
|
||||
this_book[file_md5]['annotations'] = annotations
|
||||
|
||||
return this_book
|
||||
this_book[file_md5]['title'] = title
|
||||
this_book[file_md5]['author'] = author
|
||||
this_book[file_md5]['year'] = year
|
||||
this_book[file_md5]['isbn'] = isbn
|
||||
this_book[file_md5]['tags'] = tags
|
||||
|
||||
return this_book
|
||||
|
||||
def read_progress(self):
|
||||
while True:
|
||||
@@ -253,8 +309,9 @@ class BookSorter:
|
||||
total_number = len(self.file_list)
|
||||
completed_number = len(self.threading_completed)
|
||||
|
||||
if progress_emitter: # Skip update in reading mode
|
||||
progress_emitter.update_progress(
|
||||
# Just for the record, this slows down book searching by about 20%
|
||||
if _progress_emitter: # Skip update in reading mode
|
||||
_progress_emitter.update_progress(
|
||||
completed_number * 100 // total_number)
|
||||
|
||||
if total_number == completed_number:
|
||||
@@ -265,7 +322,7 @@ class BookSorter:
|
||||
return None
|
||||
|
||||
def pool_creator():
|
||||
_pool = Pool(5)
|
||||
_pool = Pool(thread_count)
|
||||
self.processed_books = _pool.map(
|
||||
self.read_book, self.file_list)
|
||||
|
||||
@@ -290,21 +347,29 @@ class BookSorter:
|
||||
return_books[j] = i[j]
|
||||
|
||||
del self.processed_books
|
||||
print('Finished processing in', time.time() - start_time)
|
||||
return return_books
|
||||
processing_time = str(time.time() - start_time)
|
||||
logger.info('Finished processing in ' + processing_time)
|
||||
|
||||
return return_books, self.errors
|
||||
|
||||
|
||||
def progress_object_generator():
|
||||
# This has to be kept separate from the BookSorter class because
|
||||
# the QtObject inheritance disallows pickling
|
||||
global progress_emitter
|
||||
progress_emitter = UpdateProgress()
|
||||
progress_emitter.connect_to_progressbar()
|
||||
global _progress_emitter
|
||||
_progress_emitter = UpdateProgress()
|
||||
_progress_emitter.connect_to_progressbar()
|
||||
|
||||
|
||||
def resize_image(cover_image_raw):
|
||||
cover_image = QtGui.QImage()
|
||||
cover_image.loadFromData(cover_image_raw)
|
||||
if isinstance(cover_image_raw, QtGui.QImage):
|
||||
cover_image = cover_image_raw
|
||||
else:
|
||||
cover_image = QtGui.QImage()
|
||||
cover_image.loadFromData(cover_image_raw)
|
||||
|
||||
# Resize image to what literally everyone
|
||||
# agrees is an acceptable cover size
|
||||
cover_image = cover_image.scaled(
|
||||
420, 600, QtCore.Qt.IgnoreAspectRatio)
|
||||
|
||||
@@ -316,3 +381,46 @@ def resize_image(cover_image_raw):
|
||||
cover_image_final = io.BytesIO(byte_array)
|
||||
cover_image_final.seek(0)
|
||||
return cover_image_final.getvalue()
|
||||
|
||||
|
||||
def fetch_cover(title, author):
|
||||
# TODO
|
||||
# Start using the author parameter
|
||||
# Generate a cover image in case the Google API finds nothing
|
||||
# Why is that stupid UnicodeEncodeError happening?
|
||||
|
||||
api_url = 'https://www.googleapis.com/books/v1/volumes?q='
|
||||
key = '&key=' + 'AIzaSyDOferpeSS424Dshs4YWY1s-nIBA9884hE'
|
||||
title = title.replace(' ', '+')
|
||||
req = api_url + title + key
|
||||
|
||||
try:
|
||||
response = urllib.request.urlopen(req)
|
||||
if response.getcode() == 200:
|
||||
response_text = response.read().decode('utf-8')
|
||||
response_json = json.loads(response_text)
|
||||
else:
|
||||
return None
|
||||
|
||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||
return None
|
||||
|
||||
except UnicodeEncodeError:
|
||||
logger.error('UnicodeEncodeError fetching cover for ' + title)
|
||||
return None
|
||||
|
||||
try:
|
||||
# Get cover link from json
|
||||
cover_link = response_json['items'][0]['volumeInfo']['imageLinks']['thumbnail']
|
||||
# Get a slightly larger version
|
||||
cover_link = cover_link.replace('zoom=1', 'zoom=2')
|
||||
cover_request = urllib.request.urlopen(cover_link)
|
||||
response = cover_request.read() # Bytes object
|
||||
cover_image = resize_image(response)
|
||||
logger.info('Cover found for ' + title)
|
||||
|
||||
return cover_image
|
||||
|
||||
except:
|
||||
logger.error(f'Couldn\'t find cover for ' + title)
|
||||
return None
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -17,13 +15,23 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import pathlib
|
||||
from multiprocessing.dummy import Pool
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
||||
from lector import sorter
|
||||
from lector import database
|
||||
|
||||
try:
|
||||
from lector.parsers.pdf import render_pdf_page
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackGroundTabUpdate(QtCore.QThread):
|
||||
def __init__(self, database_path, all_metadata, parent=None):
|
||||
@@ -37,7 +45,8 @@ class BackGroundTabUpdate(QtCore.QThread):
|
||||
database_dict = {
|
||||
'Position': i['position'],
|
||||
'LastAccessed': i['last_accessed'],
|
||||
'Bookmarks': i['bookmarks']}
|
||||
'Bookmarks': i['bookmarks'],
|
||||
'Annotations': i['annotations']}
|
||||
|
||||
database.DatabaseFunctions(self.database_path).modify_metadata(
|
||||
database_dict, book_hash)
|
||||
@@ -50,6 +59,7 @@ class BackGroundBookAddition(QtCore.QThread):
|
||||
self.database_path = database_path
|
||||
self.addition_mode = addition_mode
|
||||
self.main_window = main_window
|
||||
self.errors = []
|
||||
|
||||
self.prune_required = True
|
||||
if self.addition_mode == 'manual':
|
||||
@@ -60,10 +70,10 @@ class BackGroundBookAddition(QtCore.QThread):
|
||||
self.file_list,
|
||||
('addition', self.addition_mode),
|
||||
self.database_path,
|
||||
self.main_window.settings['auto_tags'],
|
||||
self.main_window.settings,
|
||||
self.main_window.temp_dir.path())
|
||||
|
||||
parsed_books = books.initiate_threads()
|
||||
parsed_books, self.errors = books.initiate_threads()
|
||||
self.main_window.lib_ref.generate_model('addition', parsed_books, False)
|
||||
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
|
||||
|
||||
@@ -120,15 +130,23 @@ class BackGroundBookSearch(QtCore.QThread):
|
||||
|
||||
if self.valid_directories:
|
||||
initiate_threads()
|
||||
print(len(self.valid_files), 'books found')
|
||||
if self.valid_files:
|
||||
info_string = str(len(self.valid_files)) + ' books found'
|
||||
logger.info(info_string)
|
||||
else:
|
||||
logger.error('No books found on scan')
|
||||
else:
|
||||
print('No valid directories')
|
||||
logger.error('No valid directories')
|
||||
|
||||
|
||||
class BackGroundCacheRefill(QtCore.QThread):
|
||||
def __init__(self, image_cache, remove_value, filetype, book, all_pages, parent=None):
|
||||
super(BackGroundCacheRefill, self).__init__(parent)
|
||||
|
||||
# TODO
|
||||
# Return with only the first image in case of a cache miss
|
||||
# Rebuilding the entire n image cache takes considerably longer
|
||||
|
||||
self.image_cache = image_cache
|
||||
self.remove_value = remove_value
|
||||
self.filetype = filetype
|
||||
@@ -137,16 +155,17 @@ class BackGroundCacheRefill(QtCore.QThread):
|
||||
|
||||
def run(self):
|
||||
def load_page(current_page):
|
||||
image_pixmap = QtGui.QPixmap()
|
||||
pixmap = QtGui.QPixmap()
|
||||
|
||||
if self.filetype in ('cbz', 'cbr'):
|
||||
page_data = self.book.read(current_page)
|
||||
image_pixmap.loadFromData(page_data)
|
||||
pixmap.loadFromData(page_data)
|
||||
|
||||
elif self.filetype == 'pdf':
|
||||
page_data = self.book.page(current_page)
|
||||
page_qimage = page_data.renderToImage(350, 350)
|
||||
image_pixmap.convertFromImage(page_qimage)
|
||||
return image_pixmap
|
||||
page_data = self.book.loadPage(current_page)
|
||||
pixmap = render_pdf_page(page_data)
|
||||
|
||||
return pixmap
|
||||
|
||||
remove_index = self.image_cache.index(self.remove_value)
|
||||
|
||||
@@ -167,3 +186,86 @@ class BackGroundCacheRefill(QtCore.QThread):
|
||||
self.image_cache.append((next_page, refill_pixmap))
|
||||
except (IndexError, TypeError):
|
||||
self.image_cache.append(None)
|
||||
|
||||
|
||||
class BackGroundTextSearch(QtCore.QThread):
|
||||
def __init__(self):
|
||||
super(BackGroundTextSearch, self).__init__(None)
|
||||
self.search_content = None
|
||||
self.search_text = None
|
||||
self.case_sensitive = False
|
||||
self.match_words = False
|
||||
self.search_results = []
|
||||
|
||||
def set_search_options(
|
||||
self, search_content, search_text,
|
||||
case_sensitive, match_words):
|
||||
self.search_content = search_content
|
||||
self.search_text = search_text
|
||||
self.case_sensitive = case_sensitive
|
||||
self.match_words = match_words
|
||||
|
||||
def run(self):
|
||||
if not self.search_text or len(self.search_text) < 3:
|
||||
return
|
||||
|
||||
def get_surrounding_text(textCursor, words_before):
|
||||
textCursor.movePosition(
|
||||
QtGui.QTextCursor.WordLeft,
|
||||
QtGui.QTextCursor.MoveAnchor,
|
||||
words_before)
|
||||
textCursor.movePosition(
|
||||
QtGui.QTextCursor.NextWord,
|
||||
QtGui.QTextCursor.KeepAnchor,
|
||||
words_before * 2)
|
||||
cursor_selection = textCursor.selection().toPlainText()
|
||||
return cursor_selection.replace('\n', '')
|
||||
|
||||
self.search_results = {}
|
||||
|
||||
# Create a new QTextDocument of each chapter and iterate
|
||||
# through it looking for hits
|
||||
|
||||
for i in self.search_content:
|
||||
chapter_title = i[0]
|
||||
chapterDocument = QtGui.QTextDocument()
|
||||
chapterDocument.setHtml(i[1])
|
||||
chapter_number = i[2]
|
||||
|
||||
findFlags = QtGui.QTextDocument.FindFlags(0)
|
||||
if self.match_words:
|
||||
findFlags = findFlags | QtGui.QTextDocument.FindWholeWords
|
||||
if self.case_sensitive:
|
||||
findFlags = findFlags | QtGui.QTextDocument.FindCaseSensitively
|
||||
|
||||
findResultCursor = chapterDocument.find(self.search_text, 0, findFlags)
|
||||
while not findResultCursor.isNull():
|
||||
result_position = findResultCursor.position()
|
||||
|
||||
words_before = 3
|
||||
while True:
|
||||
surroundingTextCursor = QtGui.QTextCursor(chapterDocument)
|
||||
surroundingTextCursor.setPosition(
|
||||
result_position, QtGui.QTextCursor.MoveAnchor)
|
||||
surrounding_text = get_surrounding_text(
|
||||
surroundingTextCursor, words_before)
|
||||
words_before += 1
|
||||
if surrounding_text[:2] not in ('. ', ', '):
|
||||
break
|
||||
|
||||
# Case insensitive replace for find results
|
||||
replace_pattern = re.compile(re.escape(self.search_text), re.IGNORECASE)
|
||||
surrounding_text = replace_pattern.sub(
|
||||
f'<b>{self.search_text}</b>', surrounding_text)
|
||||
|
||||
result_tuple = (
|
||||
result_position, surrounding_text, self.search_text, chapter_number)
|
||||
|
||||
try:
|
||||
self.search_results[chapter_title].append(result_tuple)
|
||||
except KeyError:
|
||||
self.search_results[chapter_title] = [result_tuple]
|
||||
|
||||
new_position = result_position + len(self.search_text)
|
||||
findResultCursor = chapterDocument.find(
|
||||
self.search_text, new_position, findFlags)
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2018 BasioMeusPuga
|
||||
# Copyright (C) 2017-2019 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,8 +14,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BookToolBar(QtWidgets.QToolBar):
|
||||
def __init__(self, parent=None):
|
||||
@@ -29,9 +31,6 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
spacer.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
||||
|
||||
self.setMovable(False)
|
||||
self.setIconSize(QtCore.QSize(22, 22))
|
||||
self.setFloatable(False)
|
||||
@@ -45,9 +44,9 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
image_factory.get_image('gtk-select-font'),
|
||||
self._translate('BookToolBar', 'View settings'),
|
||||
self)
|
||||
self.fullscreenButton = QtWidgets.QAction(
|
||||
image_factory.get_image('view-fullscreen'),
|
||||
self._translate('BookToolBar', 'Fullscreen'),
|
||||
self.annotationButton = QtWidgets.QAction(
|
||||
image_factory.get_image('annotate'),
|
||||
self._translate('BookToolBar', 'Annotations (Ctrl + N)'),
|
||||
self)
|
||||
self.addBookmarkButton = QtWidgets.QAction(
|
||||
image_factory.get_image('bookmark-new'),
|
||||
@@ -55,9 +54,20 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self)
|
||||
self.bookmarkButton = QtWidgets.QAction(
|
||||
image_factory.get_image('bookmarks'),
|
||||
self._translate('BookToolBar', 'Bookmarks'),
|
||||
self._translate('BookToolBar', 'Bookmarks (Ctrl + B)'),
|
||||
self)
|
||||
self.searchButton = QtWidgets.QAction(
|
||||
image_factory.get_image('search'),
|
||||
self._translate('BookToolBar', 'Search (Ctrl + F)'),
|
||||
self)
|
||||
self.distractionFreeButton = QtWidgets.QAction(
|
||||
image_factory.get_image('visibility'),
|
||||
self._translate('Main_BookToolBarUI', 'Toggle distraction free mode (Ctrl + D)'),
|
||||
self)
|
||||
self.fullscreenButton = QtWidgets.QAction(
|
||||
image_factory.get_image('view-fullscreen'),
|
||||
self._translate('BookToolBar', 'Fullscreen (F)'),
|
||||
self)
|
||||
self.bookmarkButton.setObjectName('bookmarkButton')
|
||||
self.resetProfile = QtWidgets.QAction(
|
||||
image_factory.get_image('reload'),
|
||||
self._translate('BookToolBar', 'Reset profile'),
|
||||
@@ -67,11 +77,15 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.addAction(self.fontButton)
|
||||
self.fontButton.setCheckable(True)
|
||||
self.fontButton.triggered.connect(self.toggle_font_settings)
|
||||
self.addSeparator()
|
||||
self.bookSeparator1 = self.addSeparator()
|
||||
self.addAction(self.addBookmarkButton)
|
||||
self.addAction(self.bookmarkButton)
|
||||
self.bookmarkButton.setCheckable(True)
|
||||
self.addSeparator()
|
||||
self.bookSeparator2 = self.addSeparator()
|
||||
self.addAction(self.annotationButton)
|
||||
self.bookSeparator3 = self.addSeparator()
|
||||
self.addAction(self.searchButton)
|
||||
self.bookSeparator4 = self.addSeparator()
|
||||
self.addAction(self.distractionFreeButton)
|
||||
self.addAction(self.fullscreenButton)
|
||||
|
||||
# Font modification
|
||||
@@ -168,7 +182,7 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.fontSeparator4 = self.addSeparator()
|
||||
self.addAction(self.paddingUp)
|
||||
self.addAction(self.paddingDown)
|
||||
self.fontSeparator4 = self.addSeparator()
|
||||
self.fontSeparator5 = self.addSeparator()
|
||||
self.addAction(self.alignLeft)
|
||||
self.addAction(self.alignRight)
|
||||
self.addAction(self.alignCenter)
|
||||
@@ -192,38 +206,60 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.fontSeparator2,
|
||||
self.fontSeparator3,
|
||||
self.fontSeparator4,
|
||||
self.fontSeparator5,
|
||||
self.resetProfile]
|
||||
|
||||
for i in self.fontActions:
|
||||
i.setVisible(False)
|
||||
|
||||
# Comic view modification
|
||||
self.doublePageButton = QtWidgets.QAction(
|
||||
image_factory.get_image('page-double'),
|
||||
self._translate('BookToolBar', 'Double page mode (D)'),
|
||||
self)
|
||||
self.doublePageButton.setObjectName('doublePageButton')
|
||||
self.doublePageButton.setCheckable(True)
|
||||
|
||||
self.mangaModeButton = QtWidgets.QAction(
|
||||
image_factory.get_image('manga-mode'),
|
||||
self._translate('BookToolBar', 'Manga mode (M)'),
|
||||
self)
|
||||
self.mangaModeButton.setObjectName('mangaModeButton')
|
||||
self.mangaModeButton.setCheckable(True)
|
||||
|
||||
self.invertButton = QtWidgets.QAction(
|
||||
image_factory.get_image('invert'),
|
||||
self._translate('BookToolBar', 'Invert page colors'),
|
||||
self)
|
||||
self.invertButton.setObjectName('mangaModeButton')
|
||||
self.invertButton.setCheckable(True)
|
||||
|
||||
self.zoomIn = QtWidgets.QAction(
|
||||
image_factory.get_image('zoom-in'),
|
||||
self._translate('BookToolBar', 'Zoom in'),
|
||||
self._translate('BookToolBar', 'Zoom in (+)'),
|
||||
self)
|
||||
self.zoomIn.setObjectName('zoomIn')
|
||||
self.zoomOut = QtWidgets.QAction(
|
||||
image_factory.get_image('zoom-out'),
|
||||
self._translate('BookToolBar', 'Zoom Out'),
|
||||
self._translate('BookToolBar', 'Zoom Out (-)'),
|
||||
self)
|
||||
self.zoomOut.setObjectName('zoomOut')
|
||||
|
||||
self.fitWidth = QtWidgets.QAction(
|
||||
image_factory.get_image('zoom-fit-width'),
|
||||
self._translate('BookToolBar', 'Fit Width'),
|
||||
self._translate('BookToolBar', 'Fit Width (W)'),
|
||||
self)
|
||||
self.fitWidth.setObjectName('fitWidth')
|
||||
self.fitWidth.setCheckable(True)
|
||||
self.bestFit = QtWidgets.QAction(
|
||||
image_factory.get_image('zoom-fit-best'),
|
||||
self._translate('BookToolBar', 'Best Fit'),
|
||||
self._translate('BookToolBar', 'Best Fit (B)'),
|
||||
self)
|
||||
self.bestFit.setObjectName('bestFit')
|
||||
self.bestFit.setCheckable(True)
|
||||
self.originalSize = QtWidgets.QAction(
|
||||
image_factory.get_image('zoom-original'),
|
||||
self._translate('BookToolBar', 'Original size'),
|
||||
self._translate('BookToolBar', 'Original size (O)'),
|
||||
self)
|
||||
self.originalSize.setObjectName('originalSize')
|
||||
self.originalSize.setCheckable(True)
|
||||
@@ -233,15 +269,22 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.comicBGColor.setObjectName('comicBGColor')
|
||||
|
||||
self.comicSeparator1 = self.addSeparator()
|
||||
self.addAction(self.doublePageButton)
|
||||
self.addAction(self.mangaModeButton)
|
||||
self.addAction(self.invertButton)
|
||||
self.comicSeparator2 = self.addSeparator()
|
||||
self.addAction(self.zoomIn)
|
||||
self.addAction(self.zoomOut)
|
||||
self.addAction(self.fitWidth)
|
||||
self.addAction(self.bestFit)
|
||||
self.addAction(self.originalSize)
|
||||
self.comicSeparator2 = self.addSeparator()
|
||||
self.comicSeparator3 = self.addSeparator()
|
||||
self.comicBGColorAction = self.addWidget(self.comicBGColor)
|
||||
|
||||
self.comicActions = [
|
||||
self.doublePageButton,
|
||||
self.mangaModeButton,
|
||||
self.invertButton,
|
||||
self.comicBGColorAction,
|
||||
self.zoomIn,
|
||||
self.zoomOut,
|
||||
@@ -249,39 +292,40 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.bestFit,
|
||||
self.originalSize,
|
||||
self.comicSeparator1,
|
||||
self.comicSeparator2]
|
||||
self.comicSeparator2,
|
||||
self.comicSeparator3]
|
||||
|
||||
for i in self.comicActions:
|
||||
i.setVisible(False)
|
||||
|
||||
# Other booktoolbar widgets
|
||||
self.searchBar = FixedLineEdit(self)
|
||||
self.searchBar.setPlaceholderText(
|
||||
self._translate('BookToolBar', 'Search...'))
|
||||
self.searchBar.setSizePolicy(sizePolicy)
|
||||
self.searchBar.setContentsMargins(10, 0, 0, 0)
|
||||
self.searchBar.setObjectName('searchBar')
|
||||
|
||||
# Sorter
|
||||
# Table of contents Combo Box
|
||||
# Has to have a QTreeview associated with it
|
||||
self.tocBox = FixedComboBox(self)
|
||||
self.tocBox.setObjectName('sortingBox')
|
||||
self.tocBox.setToolTip(
|
||||
self._translate('BookToolBar', 'Table of Contents'))
|
||||
self.tocTreeView = QtWidgets.QTreeView(self.tocBox)
|
||||
self.tocBox.setView(self.tocTreeView)
|
||||
self.tocTreeView.setItemsExpandable(False)
|
||||
self.tocTreeView.setRootIsDecorated(False)
|
||||
|
||||
# All of these will be put after the spacer
|
||||
# This means that the buttons in the left side of
|
||||
# the toolbar have to split up and added here
|
||||
self.boxSpacer = self.addWidget(spacer)
|
||||
|
||||
self.addWidget(spacer)
|
||||
self.tocBoxAction = self.addWidget(self.tocBox)
|
||||
self.searchBarAction = self.addWidget(self.searchBar)
|
||||
|
||||
self.bookActions = [
|
||||
self.annotationButton,
|
||||
self.addBookmarkButton,
|
||||
self.bookmarkButton,
|
||||
self.searchButton,
|
||||
self.distractionFreeButton,
|
||||
self.fullscreenButton,
|
||||
self.tocBoxAction,
|
||||
self.searchBarAction]
|
||||
self.bookSeparator1,
|
||||
self.bookSeparator2,
|
||||
self.bookSeparator3,
|
||||
self.bookSeparator4]
|
||||
|
||||
for i in self.bookActions:
|
||||
i.setVisible(True)
|
||||
@@ -295,17 +339,16 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.customize_view_off()
|
||||
|
||||
def customize_view_on(self):
|
||||
if self.parent().tabWidget.widget(
|
||||
self.parent().tabWidget.currentIndex()).metadata['images_only']:
|
||||
|
||||
# The following might seem redundant,
|
||||
# but it's necessary for tab switching
|
||||
|
||||
images_only = self.parent().tabWidget.currentWidget().are_we_doing_images_only
|
||||
# The following might seem redundant,
|
||||
# but it's necessary for tab switching
|
||||
if images_only:
|
||||
for i in self.comicActions:
|
||||
i.setVisible(True)
|
||||
|
||||
for i in self.fontActions:
|
||||
i.setVisible(False)
|
||||
|
||||
else:
|
||||
for i in self.fontActions:
|
||||
i.setVisible(True)
|
||||
@@ -317,6 +360,7 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
i.setVisible(False)
|
||||
|
||||
def customize_view_off(self):
|
||||
self.fontButton.setChecked(False)
|
||||
for i in self.fontActions:
|
||||
i.setVisible(False)
|
||||
|
||||
@@ -332,15 +376,10 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
super(LibraryToolBar, self).__init__(parent)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
spacer = QtWidgets.QWidget()
|
||||
spacer.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
|
||||
self.setMovable(False)
|
||||
self.setIconSize(QtCore.QSize(22, 22))
|
||||
self.setFloatable(False)
|
||||
self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
|
||||
self.setObjectName("LibraryToolBar")
|
||||
|
||||
image_factory = self.window().QImageFactory
|
||||
|
||||
@@ -353,16 +392,6 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
image_factory.get_image('remove'),
|
||||
self._translate('LibraryToolBar', 'Delete book'),
|
||||
self)
|
||||
self.colorButton = QtWidgets.QAction(
|
||||
image_factory.get_image('color-picker'),
|
||||
self._translate('LibraryToolBar', 'Library background color'),
|
||||
self)
|
||||
self.colorButton.setObjectName('libraryBackground')
|
||||
self.settingsButton = QtWidgets.QAction(
|
||||
image_factory.get_image('settings'),
|
||||
self._translate('LibraryToolBar', 'Settings'),
|
||||
self)
|
||||
self.settingsButton.setCheckable(True)
|
||||
|
||||
self.coverViewButton = QtWidgets.QAction(
|
||||
image_factory.get_image('view-grid'),
|
||||
@@ -375,13 +404,33 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
self)
|
||||
self.tableViewButton.setCheckable(True)
|
||||
|
||||
self.reloadLibraryButton = QtWidgets.QAction(
|
||||
image_factory.get_image('reload'),
|
||||
self._translate('LibraryToolBar', 'Scan Library'),
|
||||
self)
|
||||
self.reloadLibraryButton.setObjectName('reloadLibrary')
|
||||
|
||||
self.libraryFilterButton = QtWidgets.QToolButton(self)
|
||||
self.libraryFilterButton.setIcon(image_factory.get_image('view-readermode'))
|
||||
self.libraryFilterButton.setText(
|
||||
self._translate('LibraryToolBar', 'Filter library'))
|
||||
self.libraryFilterButton.setToolTip(
|
||||
self._translate('LibraryToolBar', 'Filter library'))
|
||||
|
||||
self.colorButton = QtWidgets.QAction(
|
||||
image_factory.get_image('color-picker'),
|
||||
self._translate('LibraryToolBar', 'Library background color'),
|
||||
self)
|
||||
self.colorButton.setObjectName('libraryBackground')
|
||||
self.settingsButton = QtWidgets.QAction(
|
||||
image_factory.get_image('settings'),
|
||||
self._translate('LibraryToolBar', 'Settings'),
|
||||
self)
|
||||
self.settingsButton.setCheckable(True)
|
||||
|
||||
self.aboutButton = QtWidgets.QAction(
|
||||
image_factory.get_image('about'),
|
||||
self._translate('LibraryToolBar', 'About'),
|
||||
self)
|
||||
|
||||
# Auto unchecks the other QToolButton in case of clicking
|
||||
self.viewButtons = QtWidgets.QActionGroup(self)
|
||||
self.viewButtons.setExclusive(True)
|
||||
@@ -395,10 +444,12 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
self.addAction(self.coverViewButton)
|
||||
self.addAction(self.tableViewButton)
|
||||
self.addSeparator()
|
||||
self.addAction(self.reloadLibraryButton)
|
||||
self.addWidget(self.libraryFilterButton)
|
||||
self.addSeparator()
|
||||
self.addAction(self.colorButton)
|
||||
self.addAction(self.settingsButton)
|
||||
self.addAction(self.aboutButton)
|
||||
|
||||
# Filter
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
@@ -409,29 +460,33 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
self.searchBar.setPlaceholderText(
|
||||
self._translate('LibraryToolBar', 'Search for Title, Author, Tags...'))
|
||||
self.searchBar.setSizePolicy(sizePolicy)
|
||||
self.searchBar.setContentsMargins(10, 0, 0, 0)
|
||||
self.searchBar.setObjectName('searchBar')
|
||||
self.searchBar.setContentsMargins(0, 0, 10, 0)
|
||||
|
||||
# Sorter
|
||||
title_string = self._translate('TableProxyModel', 'Title')
|
||||
author_string = self._translate('TableProxyModel', 'Author')
|
||||
year_string = self._translate('TableProxyModel', 'Year')
|
||||
newest_string = self._translate('TableProxyModel', 'Newest')
|
||||
lastread_string = self._translate('TableProxyModel', 'Last Read')
|
||||
title_string = self._translate('LibraryToolBar', 'Title')
|
||||
author_string = self._translate('LibraryToolBar', 'Author')
|
||||
year_string = self._translate('LibraryToolBar', 'Year')
|
||||
newest_string = self._translate('LibraryToolBar', 'Newest')
|
||||
lastread_string = self._translate('LibraryToolBar', 'Last Read')
|
||||
progress_string = self._translate('LibraryToolBar', 'Progress')
|
||||
sorting_choices = [
|
||||
title_string, author_string, year_string, newest_string, lastread_string]
|
||||
title_string, author_string, year_string,
|
||||
newest_string, lastread_string, progress_string]
|
||||
|
||||
self.sortingBox = FixedComboBox(self)
|
||||
self.sortingBox.addItems(sorting_choices)
|
||||
self.sortingBox.setObjectName('sortingBox')
|
||||
self.sortingBox.setSizePolicy(sizePolicy)
|
||||
self.sortingBox.setMinimumContentsLength(10)
|
||||
self.sortingBox.setToolTip(self._translate('LibraryToolBar', 'Sort by'))
|
||||
|
||||
# Spacer
|
||||
spacer = QtWidgets.QWidget()
|
||||
spacer.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
|
||||
# Add widgets
|
||||
self.addWidget(spacer)
|
||||
self.sortingBoxAction = self.addWidget(self.sortingBox)
|
||||
self.addWidget(self.searchBar)
|
||||
self.sortingBoxAction = self.addWidget(self.sortingBox)
|
||||
|
||||
|
||||
# Sublassing these widgets out prevents them from resizing
|
||||
@@ -439,18 +494,21 @@ class FixedComboBox(QtWidgets.QComboBox):
|
||||
def __init__(self, parent=None):
|
||||
super(FixedComboBox, self).__init__(parent)
|
||||
screen_width = QtWidgets.QDesktopWidget().screenGeometry().width()
|
||||
self.adjusted_size = screen_width // 4.8
|
||||
self.adjusted_size = screen_width // 4.5
|
||||
|
||||
def sizeHint(self):
|
||||
# This and the one below should adjust to screen size
|
||||
return QtCore.QSize(self.adjusted_size, 22)
|
||||
|
||||
def wheelEvent(self, QWheelEvent):
|
||||
# Disable mouse wheel scrolling in the ComboBox
|
||||
return
|
||||
|
||||
class FixedLineEdit(QtWidgets.QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
super(FixedLineEdit, self).__init__(parent)
|
||||
screen_width = QtWidgets.QDesktopWidget().screenGeometry().width()
|
||||
self.adjusted_size = screen_width // 4.8
|
||||
self.adjusted_size = screen_width // 4.5
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(self.adjusted_size, 22)
|
||||
|
1182
lector/widgets.py
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
beautifulsoup4==4.7.1
|
||||
lxml==4.3.1
|
||||
PyMuPDF==1.14.8
|
||||
PyQt5==5.11.3
|
||||
PyQt5-sip==4.19.13
|
||||
soupsieve==1.7.3
|
||||
xmltodict==0.11.0
|
14
setup.py
@@ -1,22 +1,16 @@
|
||||
import codecs
|
||||
from os import path
|
||||
from setuptools import setup, find_packages
|
||||
from lector.logger import VERSION
|
||||
|
||||
HERE = path.abspath(path.dirname(__file__))
|
||||
|
||||
MAJOR_VERSION = '0'
|
||||
MINOR_VERSION = '3'
|
||||
MICRO_VERSION = '0'
|
||||
VERSION = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION)
|
||||
|
||||
# Get the long description from the README file
|
||||
with codecs.open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
|
||||
LONG_DESC = f.read()
|
||||
|
||||
INSTALL_DEPS = ['requests',
|
||||
'beautifulsoup4']
|
||||
TEST_DEPS = ['pytest',
|
||||
'unittest2']
|
||||
INSTALL_DEPS = ['beautifulsoup4']
|
||||
TEST_DEPS = []
|
||||
DEV_DEPS = []
|
||||
|
||||
setup(
|
||||
@@ -75,6 +69,6 @@ setup(
|
||||
extras_require={
|
||||
'dev': DEV_DEPS,
|
||||
'test': TEST_DEPS,
|
||||
'PDF': ['python-poppler-qt5']
|
||||
'PDF': ['pymupdf']
|
||||
},
|
||||
)
|
||||
|