Compare commits
70 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c71985f621 | ||
|
c8fe0ba8b6 | ||
|
d6df28c503 | ||
|
75ace25c57 | ||
|
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 |
11
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,13 +15,16 @@
|
||||
# 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 \
|
||||
lector/toolbars.py \
|
||||
lector/widgets.py \
|
||||
lector/resources/definitions.py \
|
||||
lector/resources/settingswindow.py \
|
||||
lector/resources/metadata.py \
|
||||
|
62
README.md
@@ -1,26 +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-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
|
||||
@@ -33,8 +49,11 @@ 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 [here](https://github.com/BasioMeusPuga/Lector/tree/master/lector/resources/translations). Open it in `Qt Linguist`.
|
||||
@@ -48,34 +67,37 @@ 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)
|
||||
|
54
TODO
@@ -3,6 +3,8 @@ TODO
|
||||
✓ Internationalization
|
||||
✓ Application icon
|
||||
✓ .desktop file
|
||||
✓ Shift to logging instead of print statements
|
||||
Flatpak and AppImage support
|
||||
Options:
|
||||
✓ Automatic library management
|
||||
✓ Recursive file addition
|
||||
@@ -30,9 +32,13 @@ TODO
|
||||
✓ 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
|
||||
@@ -61,47 +67,71 @@ TODO
|
||||
✓ 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
|
||||
Annotation preview in listView
|
||||
Image
|
||||
✓ 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:
|
||||
Deselecting all directories in the settings dialog also filters out manually added books
|
||||
Clean up 'switch' page layout
|
||||
Colors aren't loaded properly for annotation previews
|
||||
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:
|
||||
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
|
||||
Shift to logging instead of print statements
|
||||
txt, doc, chm, djvu, fb2 support
|
||||
txt, doc, chm, djvu support
|
||||
Include icons for filetype emblems
|
||||
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
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"app-id":"com.basiomeuspuga.Lector",
|
||||
"runtime":"org.kde.Platform",
|
||||
"runtime-version":"5.10",
|
||||
"runtime-version":"5.12",
|
||||
"sdk":"org.kde.Sdk",
|
||||
"command":"lector",
|
||||
"rename-icon":"Lector",
|
||||
@@ -21,74 +21,107 @@
|
||||
},
|
||||
"modules":[
|
||||
{
|
||||
"name": "python",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tar.xz",
|
||||
"sha256": "159b932bf56aeaa76fd66e7420522d8c8853d486b8567c459b84fe2ed13bcaba"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pyqt5",
|
||||
"name": "PyQt5",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --prefix=/app PyQt5-5.10.1-5.10.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
|
||||
"pip3 install --prefix=/app PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
|
||||
],
|
||||
"modules":[
|
||||
{
|
||||
"name":"sip",
|
||||
"name":"PyQt5-sip",
|
||||
"sources":[
|
||||
{
|
||||
"type":"file",
|
||||
"url":"https://pypi.python.org/packages/8a/ea/d317ce5696dda4df7c156cd60447cda22833b38106c98250eae1451f03ec/sip-4.19.8-cp36-cp36m-manylinux1_x86_64.whl",
|
||||
"sha256":"cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85"
|
||||
"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 sip-4.19.8-cp36-cp36m-manylinux1_x86_64.whl"
|
||||
"pip3 install --prefix=/app PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://pypi.python.org/packages/e4/15/4e2e49f64884edbab6f833c6fd3add24d7938f2429aec1f2883e645d4d8f/PyQt5-5.10.1-5.10.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl",
|
||||
"sha256": "1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac"
|
||||
"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":"beautifulsoup",
|
||||
"name":"beautifulsoup4",
|
||||
"buildsystem":"simple",
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://pypi.python.org/packages/fa/8d/1d14391fdaed5abada4e0f63543fef49b8331a34ca60c88bd521bcf7f782/beautifulsoup4-4.6.0.tar.gz",
|
||||
"sha256":"808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89"
|
||||
"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":[
|
||||
"python3 setup.py build",
|
||||
"python3 setup.py install --prefix=/app"
|
||||
"pip3 install --prefix=/app beautifulsoup4-4.7.1-py3-none-any.whl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lxml",
|
||||
"buildsystem": "simple",
|
||||
{
|
||||
"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 lxml-4.2.1-cp36-cp36m-manylinux1_x86_64.whl"
|
||||
"pip3 install --prefix=/app PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl"
|
||||
],
|
||||
"sources": [
|
||||
"sources":[
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://pypi.python.org/packages/a7/b9/ccf46cea0f698b40bca2a9c1a44039c336fe1988b82de4f7353be7a8396a/lxml-4.2.1-cp36-cp36m-manylinux1_x86_64.whl",
|
||||
"sha256": "0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b"
|
||||
"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",
|
||||
|
@@ -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())
|
@@ -1,7 +1,7 @@
|
||||
#!/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
|
||||
@@ -31,6 +31,13 @@ sys.path.append(str(install_dir))
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
# Init logging
|
||||
# Must be done first and at the module level
|
||||
# or it won't work properly in case of the imports below
|
||||
from lector.logger import init_logging, VERSION
|
||||
logger = init_logging(sys.argv)
|
||||
logger.log(60, f'Lector {VERSION} - Application started')
|
||||
|
||||
from lector import database
|
||||
from lector import sorter
|
||||
from lector.toolbars import LibraryToolBar, BookToolBar
|
||||
@@ -78,7 +85,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.comic_profile = {}
|
||||
self.database_path = None
|
||||
self.active_library_filters = []
|
||||
self.active_bookmark_docks = []
|
||||
self.active_docks = []
|
||||
|
||||
# Initialize application
|
||||
Settings(self).read_settings() # This should populate all variables that need
|
||||
@@ -120,6 +127,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# Statusbar widgets
|
||||
self.statusMessage.setObjectName('statusMessage')
|
||||
self.statusBar.addPermanentWidget(self.statusMessage)
|
||||
self.errorButton = QtWidgets.QPushButton(self.statusBar)
|
||||
self.errorButton.setIcon(QtGui.QIcon(':/images/error.svg'))
|
||||
self.errorButton.setFlat(True)
|
||||
self.errorButton.setVisible(False)
|
||||
self.errorButton.setToolTip('What hast thou done?')
|
||||
self.errorButton.clicked.connect(self.show_errors)
|
||||
self.statusBar.addPermanentWidget(self.errorButton)
|
||||
self.sorterProgress = QtWidgets.QProgressBar()
|
||||
self.sorterProgress.setMaximumWidth(300)
|
||||
self.sorterProgress.setObjectName('sorterProgress')
|
||||
@@ -158,7 +172,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.libraryToolBar.reloadLibraryButton.triggered.connect(
|
||||
self.settingsDialog.start_library_scan)
|
||||
self.libraryToolBar.colorButton.triggered.connect(self.get_color)
|
||||
self.libraryToolBar.settingsButton.triggered.connect(self.show_settings)
|
||||
self.libraryToolBar.settingsButton.triggered.connect(
|
||||
lambda: self.show_settings(0))
|
||||
self.libraryToolBar.aboutButton.triggered.connect(
|
||||
lambda: self.show_settings(3))
|
||||
self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodels)
|
||||
self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodels)
|
||||
self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup)
|
||||
@@ -171,11 +188,28 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.libraryToolBar.tableViewButton.trigger()
|
||||
|
||||
# Book toolbar
|
||||
self.bookToolBar.annotationButton.triggered.connect(self.toggle_dock_widgets)
|
||||
self.bookToolBar.addBookmarkButton.triggered.connect(self.add_bookmark)
|
||||
self.bookToolBar.bookmarkButton.triggered.connect(self.toggle_dock_widgets)
|
||||
self.bookToolBar.distractionFreeButton.triggered.connect(self.toggle_distraction_free)
|
||||
self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)
|
||||
self.bookToolBar.addBookmarkButton.triggered.connect(
|
||||
lambda: self.tabWidget.currentWidget().sideDock.bookmarks.add_bookmark())
|
||||
self.bookToolBar.bookmarkButton.triggered.connect(
|
||||
lambda: self.tabWidget.currentWidget().toggle_side_dock(0))
|
||||
self.bookToolBar.annotationButton.triggered.connect(
|
||||
lambda: self.tabWidget.currentWidget().toggle_side_dock(1))
|
||||
self.bookToolBar.searchButton.triggered.connect(
|
||||
lambda: self.tabWidget.currentWidget().toggle_side_dock(2))
|
||||
self.bookToolBar.distractionFreeButton.triggered.connect(
|
||||
self.toggle_distraction_free)
|
||||
self.bookToolBar.fullscreenButton.triggered.connect(
|
||||
lambda: self.tabWidget.currentWidget().go_fullscreen())
|
||||
|
||||
self.bookToolBar.doublePageButton.triggered.connect(self.change_page_view)
|
||||
self.bookToolBar.mangaModeButton.triggered.connect(self.change_page_view)
|
||||
self.bookToolBar.invertButton.triggered.connect(self.change_page_view)
|
||||
if self.settings['double_page_mode']:
|
||||
self.bookToolBar.doublePageButton.setChecked(True)
|
||||
if self.settings['manga_mode']:
|
||||
self.bookToolBar.mangaModeButton.setChecked(True)
|
||||
if self.settings['invert_colors']:
|
||||
self.bookToolBar.invertButton.setChecked(True)
|
||||
|
||||
for count, i in enumerate(self.display_profiles):
|
||||
self.bookToolBar.profileBox.setItemData(count, i, QtCore.Qt.UserRole)
|
||||
@@ -199,12 +233,18 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
i[1].triggered.connect(self.modify_font)
|
||||
self.alignment_dict[current_profile['text_alignment']].setChecked(True)
|
||||
|
||||
self.bookToolBar.zoomIn.triggered.connect(self.modify_comic_view)
|
||||
self.bookToolBar.zoomOut.triggered.connect(self.modify_comic_view)
|
||||
self.bookToolBar.fitWidth.triggered.connect(self.modify_comic_view)
|
||||
self.bookToolBar.bestFit.triggered.connect(self.modify_comic_view)
|
||||
self.bookToolBar.originalSize.triggered.connect(self.modify_comic_view)
|
||||
self.bookToolBar.comicBGColor.clicked.connect(self.get_color)
|
||||
self.bookToolBar.zoomIn.triggered.connect(
|
||||
self.modify_comic_view)
|
||||
self.bookToolBar.zoomOut.triggered.connect(
|
||||
self.modify_comic_view)
|
||||
self.bookToolBar.fitWidth.triggered.connect(
|
||||
lambda: self.modify_comic_view(False))
|
||||
self.bookToolBar.bestFit.triggered.connect(
|
||||
lambda: self.modify_comic_view(False))
|
||||
self.bookToolBar.originalSize.triggered.connect(
|
||||
lambda: self.modify_comic_view(False))
|
||||
self.bookToolBar.comicBGColor.clicked.connect(
|
||||
self.get_color)
|
||||
|
||||
self.bookToolBar.colorBoxFG.clicked.connect(self.get_color)
|
||||
self.bookToolBar.colorBoxBG.clicked.connect(self.get_color)
|
||||
@@ -216,16 +256,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.tab_switch()
|
||||
self.tabWidget.currentChanged.connect(self.tab_switch)
|
||||
|
||||
# Tab closing
|
||||
# Tab Widget formatting
|
||||
self.tabWidget.setTabsClosable(True)
|
||||
self.tabWidget.setDocumentMode(True)
|
||||
self.tabWidget.tabBarClicked.connect(self.tab_disallow_library_movement)
|
||||
|
||||
# Get list of available parsers
|
||||
self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers)
|
||||
print('Available parsers: ' + self.available_parsers)
|
||||
logger.info('Available parsers: ' + self.available_parsers)
|
||||
|
||||
# The Library tab gets no button
|
||||
self.tabWidget.tabBar().setTabButton(
|
||||
0, QtWidgets.QTabBar.RightSide, None)
|
||||
self.tabWidget.widget(0).is_library = True
|
||||
self.tabWidget.tabCloseRequested.connect(self.tab_close)
|
||||
self.tabWidget.setTabBarAutoHide(True)
|
||||
|
||||
@@ -272,9 +315,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
for count, i in enumerate(self.settings['main_window_headers']):
|
||||
self.tableView.horizontalHeader().resizeSection(count, int(i))
|
||||
self.tableView.horizontalHeader().resizeSection(5, 30)
|
||||
self.tableView.horizontalHeader().setStretchLastSection(False)
|
||||
self.tableView.horizontalHeader().setStretchLastSection(True)
|
||||
self.tableView.horizontalHeader().sectionClicked.connect(
|
||||
self.lib_ref.tableProxyModel.sort_table_columns)
|
||||
self.lib_ref.tableProxyModel.sort_table_columns(2)
|
||||
self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.tableView.customContextMenuRequested.connect(
|
||||
self.generate_library_context_menu)
|
||||
@@ -330,15 +374,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
def process_post_hoc_files(self, file_list, open_files_after_processing):
|
||||
# Takes care of both dragged and dropped files
|
||||
# As well as files sent as command line arguments
|
||||
file_list = [i for i in file_list if os.path.exists(i)]
|
||||
if not file_list:
|
||||
return
|
||||
|
||||
books = sorter.BookSorter(
|
||||
file_list,
|
||||
('addition', 'manual'),
|
||||
self.database_path,
|
||||
self.settings['auto_tags'],
|
||||
self.settings,
|
||||
self.temp_dir.path())
|
||||
|
||||
parsed_books = books.initiate_threads()
|
||||
if not parsed_books:
|
||||
parsed_books, errors = books.initiate_threads()
|
||||
if not parsed_books and not open_files_after_processing:
|
||||
return
|
||||
|
||||
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
|
||||
@@ -348,14 +396,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if open_files_after_processing:
|
||||
self.open_files(file_dict)
|
||||
|
||||
self.move_on()
|
||||
self.move_on(errors)
|
||||
|
||||
def open_files(self, path_hash_dictionary):
|
||||
# file_paths is expected to be a dictionary
|
||||
# This allows for threading file opening
|
||||
# Which should speed up multiple file opening
|
||||
# especially @ application start
|
||||
|
||||
file_paths = [i for i in path_hash_dictionary]
|
||||
|
||||
for filename in path_hash_dictionary.items():
|
||||
@@ -382,30 +429,32 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if not file_paths:
|
||||
return
|
||||
|
||||
def finishing_touches():
|
||||
self.profile_functions.format_contentView()
|
||||
self.start_culling_timer()
|
||||
logger.info(
|
||||
'Attempting to open: ' + ', '.join(file_paths))
|
||||
|
||||
print('Attempting to open: ' + ', '.join(file_paths))
|
||||
|
||||
contents = sorter.BookSorter(
|
||||
contents, errors = sorter.BookSorter(
|
||||
file_paths,
|
||||
('reading', None),
|
||||
self.database_path,
|
||||
True,
|
||||
self.settings,
|
||||
self.temp_dir.path()).initiate_threads()
|
||||
|
||||
# TODO
|
||||
# Notification feedback in case all books return nothing
|
||||
if errors:
|
||||
self.display_error_notification(errors)
|
||||
|
||||
if not contents:
|
||||
logger.error('No parseable files found')
|
||||
return
|
||||
|
||||
successfully_opened = []
|
||||
for i in contents:
|
||||
# New tabs are created here
|
||||
# Initial position adjustment is carried out by the tab itself
|
||||
file_data = contents[i]
|
||||
Tab(file_data, self)
|
||||
successfully_opened.append(file_data['path'])
|
||||
logger.info(
|
||||
'Successfully opened: ' + ', '.join(file_paths))
|
||||
|
||||
if self.settings['last_open_tab'] == 'library':
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
@@ -418,61 +467,16 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if self.settings['last_open_tab'] == this_path:
|
||||
self.tabWidget.setCurrentIndex(i)
|
||||
self.settings['last_open_tab'] = None
|
||||
finishing_touches()
|
||||
return
|
||||
|
||||
self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)
|
||||
finishing_touches()
|
||||
|
||||
def start_culling_timer(self):
|
||||
if self.settings['perform_culling']:
|
||||
self.culling_timer.start(30)
|
||||
|
||||
def add_bookmark(self):
|
||||
if self.tabWidget.currentIndex() != 0:
|
||||
self.tabWidget.widget(self.tabWidget.currentIndex()).add_bookmark()
|
||||
|
||||
def resizeEvent(self, event=None):
|
||||
if event:
|
||||
# This implies a vertical resize event only
|
||||
# We ain't about that lifestyle
|
||||
if event.oldSize().width() == event.size().width():
|
||||
return
|
||||
|
||||
# The hackiness of this hack is just...
|
||||
default_size = 170 # This is size of the QIcon (160 by default) +
|
||||
# minimum margin is needed between thumbnails
|
||||
|
||||
# for n icons, the n + 1th icon will appear at > n +1.11875
|
||||
# First, calculate the number of images per row
|
||||
i = self.listView.viewport().width() / default_size
|
||||
rem = i - int(i)
|
||||
if rem >= .21875 and rem <= .9999:
|
||||
num_images = int(i)
|
||||
else:
|
||||
num_images = int(i) - 1
|
||||
|
||||
# The rest is illustrated using informative variable names
|
||||
space_occupied = num_images * default_size
|
||||
# 12 is the scrollbar width
|
||||
# Larger numbers keep reduce flickering but also increase
|
||||
# the distance from the scrollbar
|
||||
space_left = (
|
||||
self.listView.viewport().width() - space_occupied - 19)
|
||||
try:
|
||||
layout_extra_space_per_image = space_left // num_images
|
||||
self.listView.setGridSize(
|
||||
QtCore.QSize(default_size + layout_extra_space_per_image, 250))
|
||||
self.start_culling_timer()
|
||||
except ZeroDivisionError: # Initial resize is ignored
|
||||
return
|
||||
|
||||
def add_books(self):
|
||||
dialog_prompt = self._translate('Main_UI', 'Add books to database')
|
||||
ebooks_string = self._translate('Main_UI', 'eBooks')
|
||||
opened_files = QtWidgets.QFileDialog.getOpenFileNames(
|
||||
self, dialog_prompt, self.settings['last_open_path'],
|
||||
f'{ebooks_string} ({self.available_parsers})')
|
||||
f'{ebooks_string}({self.available_parsers})')
|
||||
|
||||
if not opened_files[0]:
|
||||
return
|
||||
@@ -486,18 +490,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.statusMessage.setText(self._translate('Main_UI', 'Adding books...'))
|
||||
self.thread = BackGroundBookAddition(
|
||||
opened_files[0], self.database_path, 'manual', self)
|
||||
self.thread.finished.connect(self.move_on)
|
||||
self.thread.finished.connect(
|
||||
lambda: self.move_on(self.thread.errors))
|
||||
self.thread.start()
|
||||
|
||||
def get_selection(self, library_widget):
|
||||
def get_selection(self):
|
||||
selected_indexes = None
|
||||
|
||||
if library_widget == self.listView:
|
||||
if self.listView.isVisible():
|
||||
selected_books = self.lib_ref.itemProxyModel.mapSelectionToSource(
|
||||
self.listView.selectionModel().selection())
|
||||
selected_indexes = [i.indexes()[0] for i in selected_books]
|
||||
|
||||
elif library_widget == self.tableView:
|
||||
elif self.tableView.isVisible():
|
||||
selected_books = self.tableView.selectionModel().selectedRows()
|
||||
selected_indexes = [
|
||||
self.lib_ref.tableProxyModel.mapToSource(i) for i in selected_books]
|
||||
@@ -505,16 +510,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
return selected_indexes
|
||||
|
||||
def delete_books(self, selected_indexes=None):
|
||||
if not selected_indexes:
|
||||
# Get a list of QItemSelection objects
|
||||
# What we're interested in is the indexes()[0] in each of them
|
||||
# That gives a list of indexes from the view model
|
||||
if self.listView.isVisible():
|
||||
selected_indexes = self.get_selection(self.listView)
|
||||
|
||||
elif self.tableView.isVisible():
|
||||
selected_indexes = self.get_selection(self.tableView)
|
||||
|
||||
# Get a list of QItemSelection objects
|
||||
# What we're interested in is the indexes()[0] in each of them
|
||||
# That gives a list of indexes from the view model
|
||||
selected_indexes = self.get_selection()
|
||||
if not selected_indexes:
|
||||
return
|
||||
|
||||
@@ -528,7 +527,8 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
delete_hashes = [
|
||||
self.lib_ref.libraryModel.data(
|
||||
i, QtCore.Qt.UserRole + 6) for i in selected_indexes]
|
||||
persistent_indexes = [QtCore.QPersistentModelIndex(i) for i in selected_indexes]
|
||||
persistent_indexes = [
|
||||
QtCore.QPersistentModelIndex(i) for i in selected_indexes]
|
||||
|
||||
for i in persistent_indexes:
|
||||
self.lib_ref.libraryModel.removeRow(i.row())
|
||||
@@ -540,10 +540,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.thread.start()
|
||||
|
||||
# Generate a message box to confirm deletion
|
||||
selected_number = len(selected_indexes)
|
||||
confirm_deletion = QtWidgets.QMessageBox()
|
||||
deletion_prompt = self._translate(
|
||||
'Main_UI', f'Delete {selected_number} book(s)?')
|
||||
'Main_UI', f'Delete book(s)?')
|
||||
confirm_deletion.setText(deletion_prompt)
|
||||
confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
|
||||
confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion'))
|
||||
@@ -557,7 +556,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if self.tabWidget.currentIndex() == 0:
|
||||
self.delete_books()
|
||||
|
||||
def move_on(self):
|
||||
def move_on(self, errors=None):
|
||||
self.settingsDialog.okButton.setEnabled(True)
|
||||
self.settingsDialog.okButton.setToolTip(
|
||||
self._translate('Main_UI', 'Save changes and start library scan'))
|
||||
@@ -566,8 +565,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.sorterProgress.setVisible(False)
|
||||
self.sorterProgress.setValue(0)
|
||||
|
||||
if self.libraryToolBar.searchBar.text() == '':
|
||||
self.statusBar.setVisible(False)
|
||||
# The errors argument is a list and will only be present
|
||||
# in case of addition and reading
|
||||
if errors:
|
||||
self.display_error_notification(errors)
|
||||
else:
|
||||
if self.libraryToolBar.searchBar.text() == '':
|
||||
self.statusBar.setVisible(False)
|
||||
|
||||
self.lib_ref.update_proxymodels()
|
||||
self.lib_ref.generate_library_tags()
|
||||
@@ -591,6 +595,12 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
def tab_switch(self):
|
||||
try:
|
||||
# Disallow library tab movement
|
||||
# Does not need to be looped since the library
|
||||
# tab can only ever go to position 1
|
||||
if not self.tabWidget.widget(0).is_library:
|
||||
self.tabWidget.tabBar().moveTab(1, 0)
|
||||
|
||||
if self.current_tab != 0:
|
||||
self.tabWidget.widget(
|
||||
self.current_tab).update_last_accessed_time()
|
||||
@@ -599,15 +609,15 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
self.current_tab = self.tabWidget.currentIndex()
|
||||
|
||||
# Hide bookmark and annotation widgets
|
||||
# Hide all side docks whenever a tab is switched
|
||||
for i in range(1, self.tabWidget.count()):
|
||||
self.tabWidget.widget(i).bookmarkDock.setVisible(False)
|
||||
self.tabWidget.widget(i).annotationDock.setVisible(False)
|
||||
self.tabWidget.widget(i).sideDock.setVisible(False)
|
||||
|
||||
# If library
|
||||
if self.tabWidget.currentIndex() == 0:
|
||||
|
||||
self.resizeEvent()
|
||||
self.start_culling_timer()
|
||||
|
||||
if self.settings['show_bars']:
|
||||
self.bookToolBar.hide()
|
||||
self.libraryToolBar.show()
|
||||
@@ -618,38 +628,39 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.statusMessage.setText(
|
||||
str(self.lib_ref.itemProxyModel.rowCount()) +
|
||||
self._translate('Main_UI', ' Books'))
|
||||
else:
|
||||
|
||||
if self.libraryToolBar.searchBar.text() != '':
|
||||
self.statusBar.setVisible(True)
|
||||
|
||||
else:
|
||||
if self.settings['show_bars']:
|
||||
self.bookToolBar.show()
|
||||
self.libraryToolBar.hide()
|
||||
|
||||
current_tab = self.tabWidget.widget(
|
||||
self.tabWidget.currentIndex())
|
||||
current_metadata = current_tab.metadata
|
||||
current_tab = self.tabWidget.currentWidget()
|
||||
self.bookToolBar.tocBox.setModel(current_tab.tocModel)
|
||||
self.bookToolBar.tocTreeView.expandAll()
|
||||
current_tab.set_tocBox_index(None, None)
|
||||
|
||||
# Needed to set the contentView widget background
|
||||
# on first run. Subsequent runs might be redundant,
|
||||
# but it doesn't seem to visibly affect performance
|
||||
self.profile_functions.format_contentView()
|
||||
self.statusBar.setVisible(False)
|
||||
|
||||
if self.bookToolBar.fontButton.isChecked():
|
||||
self.bookToolBar.customize_view_on()
|
||||
|
||||
current_title = current_metadata['title']
|
||||
current_author = current_metadata['author']
|
||||
current_position = current_metadata['position']
|
||||
current_toc = [i[0] for i in current_metadata['content']]
|
||||
|
||||
self.bookToolBar.tocBox.blockSignals(True)
|
||||
self.bookToolBar.tocBox.clear()
|
||||
self.bookToolBar.tocBox.addItems(current_toc)
|
||||
if current_position:
|
||||
self.bookToolBar.tocBox.setCurrentIndex(
|
||||
current_position['current_chapter'] - 1)
|
||||
if not current_metadata['images_only']:
|
||||
current_tab.hiddenButton.animateClick(25)
|
||||
self.bookToolBar.tocBox.blockSignals(False)
|
||||
|
||||
self.profile_functions.format_contentView()
|
||||
|
||||
self.statusMessage.setText(
|
||||
current_author + ' - ' + current_title)
|
||||
else:
|
||||
if current_tab.are_we_doing_images_only:
|
||||
self.bookToolBar.searchButton.setVisible(False)
|
||||
self.bookToolBar.annotationButton.setVisible(False)
|
||||
self.bookToolBar.bookSeparator2.setVisible(False)
|
||||
self.bookToolBar.bookSeparator3.setVisible(False)
|
||||
else:
|
||||
self.bookToolBar.searchButton.setVisible(True)
|
||||
self.bookToolBar.annotationButton.setVisible(True)
|
||||
self.bookToolBar.bookSeparator2.setVisible(True)
|
||||
self.bookToolBar.bookSeparator3.setVisible(True)
|
||||
|
||||
def tab_close(self, tab_index=None):
|
||||
if not tab_index:
|
||||
@@ -669,35 +680,23 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.tabWidget.widget(tab_index).setParent(None)
|
||||
gc.collect()
|
||||
|
||||
def tab_disallow_library_movement(self, tab_index):
|
||||
# Makes the library tab immovable
|
||||
if tab_index == 0:
|
||||
self.tabWidget.setMovable(False)
|
||||
else:
|
||||
self.tabWidget.setMovable(True)
|
||||
|
||||
def set_toc_position(self, event=None):
|
||||
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
|
||||
currentIndex = self.bookToolBar.tocTreeView.currentIndex()
|
||||
required_position = currentIndex.data(QtCore.Qt.UserRole)
|
||||
if not required_position:
|
||||
return # Initial startup might return a None
|
||||
|
||||
current_tab.metadata[
|
||||
'position']['current_chapter'] = event + 1
|
||||
current_tab.metadata[
|
||||
'position']['is_read'] = False
|
||||
|
||||
# Go on to change the value of the Table of Contents box
|
||||
current_tab.change_chapter_tocBox()
|
||||
current_tab.contentView.record_position()
|
||||
|
||||
self.profile_functions.format_contentView()
|
||||
|
||||
def set_fullscreen(self):
|
||||
current_tab = self.tabWidget.currentIndex()
|
||||
current_tab_widget = self.tabWidget.widget(current_tab)
|
||||
current_tab_widget.go_fullscreen()
|
||||
|
||||
def toggle_dock_widgets(self):
|
||||
sender = self.sender()
|
||||
current_tab = self.tabWidget.currentIndex()
|
||||
current_tab_widget = self.tabWidget.widget(current_tab)
|
||||
|
||||
if sender == self.bookToolBar.bookmarkButton:
|
||||
current_tab_widget.toggle_bookmarks()
|
||||
|
||||
if sender == self.bookToolBar.annotationButton:
|
||||
current_tab_widget.toggle_annotations()
|
||||
# The set_content method is universal
|
||||
# It's going to do position tracking
|
||||
current_tab = self.tabWidget.currentWidget()
|
||||
current_tab.set_content(required_position, False, True)
|
||||
|
||||
def library_doubleclick(self, index):
|
||||
sender = self.sender().objectName()
|
||||
@@ -713,6 +712,20 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
self.open_files(path)
|
||||
|
||||
def display_error_notification(self, errors):
|
||||
self.statusBar.setVisible(True)
|
||||
self.errorButton.setVisible(True)
|
||||
|
||||
def show_errors(self):
|
||||
# TODO
|
||||
# Create a separate viewing area for errors
|
||||
# before showing the log
|
||||
|
||||
self.show_settings(3)
|
||||
self.settingsDialog.aboutTabWidget.setCurrentIndex(1)
|
||||
self.errorButton.setVisible(False)
|
||||
self.statusBar.setVisible(False)
|
||||
|
||||
def statusbar_visibility(self):
|
||||
if self.sender() == self.libraryToolBar.searchBar:
|
||||
if self.libraryToolBar.searchBar.text() == '':
|
||||
@@ -720,13 +733,16 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
else:
|
||||
self.statusBar.setVisible(True)
|
||||
|
||||
def show_settings(self):
|
||||
def show_settings(self, stacked_widget_index):
|
||||
if not self.settingsDialog.isVisible():
|
||||
self.settingsDialog.show()
|
||||
index = self.settingsDialog.listModel.index(
|
||||
stacked_widget_index, 0)
|
||||
self.settingsDialog.listView.setCurrentIndex(index)
|
||||
else:
|
||||
self.settingsDialog.hide()
|
||||
|
||||
#____________________________________________
|
||||
#==================================================================
|
||||
# The contentView modification functions are in the guifunctions
|
||||
# module. self.profile_functions is the reference here.
|
||||
|
||||
@@ -743,10 +759,37 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
signal_sender = None
|
||||
else:
|
||||
signal_sender = self.sender().objectName()
|
||||
|
||||
self.profile_functions.modify_comic_view(
|
||||
signal_sender, key_pressed)
|
||||
|
||||
#____________________________________________
|
||||
#=================================================================
|
||||
|
||||
def change_page_view(self, key_pressed=False):
|
||||
# Set zoom mode to best fit to
|
||||
# make the transition less jarring
|
||||
# if the sender isn't the invert colors button
|
||||
if self.sender() != self.bookToolBar.invertButton:
|
||||
self.comic_profile['zoom_mode'] = 'bestFit'
|
||||
|
||||
# Toggle Double page mode / manga mode on keypress
|
||||
if key_pressed == QtCore.Qt.Key_D:
|
||||
self.bookToolBar.doublePageButton.setChecked(
|
||||
not self.bookToolBar.doublePageButton.isChecked())
|
||||
if key_pressed == QtCore.Qt.Key_M:
|
||||
self.bookToolBar.mangaModeButton.setChecked(
|
||||
not self.bookToolBar.mangaModeButton.isChecked())
|
||||
|
||||
# Change settings according to the
|
||||
# current state of each of the toolbar buttons
|
||||
self.settings['double_page_mode'] = self.bookToolBar.doublePageButton.isChecked()
|
||||
self.settings['manga_mode'] = self.bookToolBar.mangaModeButton.isChecked()
|
||||
self.settings['invert_colors'] = self.bookToolBar.invertButton.isChecked()
|
||||
|
||||
# Switch page to whatever index is selected in the tocBox
|
||||
current_tab = self.tabWidget.currentWidget()
|
||||
chapter_number = current_tab.metadata['position']['current_chapter']
|
||||
current_tab.set_content(chapter_number, False)
|
||||
|
||||
def generate_library_context_menu(self, position):
|
||||
index = self.sender().indexAt(position)
|
||||
@@ -755,7 +798,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
# It's worth remembering that these are indexes of the libraryModel
|
||||
# and NOT of the proxy models
|
||||
selected_indexes = self.get_selection(self.sender())
|
||||
selected_indexes = self.get_selection()
|
||||
|
||||
context_menu = QtWidgets.QMenu()
|
||||
|
||||
@@ -791,8 +834,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
if action == editAction:
|
||||
edit_book = selected_indexes[0]
|
||||
metadata = self.lib_ref.libraryModel.data(
|
||||
edit_book, QtCore.Qt.UserRole + 3)
|
||||
is_cover_loaded = self.lib_ref.libraryModel.data(
|
||||
edit_book, QtCore.Qt.UserRole + 8)
|
||||
|
||||
@@ -805,15 +846,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.database_path).fetch_covers_only([book_hash])[0][1]
|
||||
self.cover_functions.cover_loader(book_item, book_cover)
|
||||
|
||||
cover = self.lib_ref.libraryModel.item(edit_book.row()).icon()
|
||||
title = metadata['title']
|
||||
author = metadata['author']
|
||||
year = str(metadata['year'])
|
||||
tags = metadata['tags']
|
||||
cover = self.lib_ref.libraryModel.item(
|
||||
edit_book.row()).icon()
|
||||
title = self.lib_ref.libraryModel.data(
|
||||
edit_book, QtCore.Qt.UserRole)
|
||||
author = self.lib_ref.libraryModel.data(
|
||||
edit_book, QtCore.Qt.UserRole + 1)
|
||||
year = str(self.lib_ref.libraryModel.data(
|
||||
edit_book, QtCore.Qt.UserRole + 2)) # Text cannot be int
|
||||
tags = self.lib_ref.libraryModel.data(
|
||||
edit_book, QtCore.Qt.UserRole + 4)
|
||||
|
||||
self.metadataDialog.load_book(
|
||||
cover, title, author, year, tags, edit_book)
|
||||
|
||||
self.metadataDialog.show()
|
||||
|
||||
if action == deleteAction:
|
||||
@@ -929,6 +974,45 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
self.start_culling_timer()
|
||||
|
||||
def start_culling_timer(self):
|
||||
if self.settings['perform_culling']:
|
||||
self.culling_timer.start(30)
|
||||
|
||||
def resizeEvent(self, event=None):
|
||||
if event:
|
||||
# This implies a vertical resize event only
|
||||
# We ain't about that lifestyle
|
||||
if event.oldSize().width() == event.size().width():
|
||||
return
|
||||
|
||||
# The hackiness of this hack is just...
|
||||
default_size = 170 # This is size of the QIcon (160 by default) +
|
||||
# minimum margin needed between thumbnails
|
||||
|
||||
# for n icons, the n + 1th icon will appear at > n +1.11875
|
||||
# First, calculate the number of images per row
|
||||
i = self.listView.viewport().width() / default_size
|
||||
rem = i - int(i)
|
||||
if rem >= .21875 and rem <= .9999:
|
||||
num_images = int(i)
|
||||
else:
|
||||
num_images = int(i) - 1
|
||||
|
||||
# The rest is illustrated using informative variable names
|
||||
space_occupied = num_images * default_size
|
||||
# 12 is the scrollbar width
|
||||
# Larger numbers keep reduce flickering but also increase
|
||||
# the distance from the scrollbar
|
||||
space_left = (
|
||||
self.listView.viewport().width() - space_occupied - 19)
|
||||
try:
|
||||
layout_extra_space_per_image = space_left // num_images
|
||||
self.listView.setGridSize(
|
||||
QtCore.QSize(default_size + layout_extra_space_per_image, 250))
|
||||
self.start_culling_timer()
|
||||
except ZeroDivisionError: # Initial resize is ignored
|
||||
return
|
||||
|
||||
def closeEvent(self, event=None):
|
||||
if event:
|
||||
event.ignore()
|
||||
@@ -938,7 +1022,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.settingsDialog.hide()
|
||||
self.definitionDialog.hide()
|
||||
self.temp_dir.remove()
|
||||
for this_dock in self.active_bookmark_docks:
|
||||
for this_dock in self.active_docks:
|
||||
try:
|
||||
this_dock.setVisible(False)
|
||||
except RuntimeError:
|
||||
@@ -975,6 +1059,8 @@ def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName('Lector') # This is needed for QStandardPaths
|
||||
# and my own hubris
|
||||
# Make icons sharp in HiDPI screen
|
||||
app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
|
||||
|
||||
# Internationalization support
|
||||
translator = QtCore.QTranslator()
|
||||
@@ -982,10 +1068,10 @@ def main():
|
||||
QtCore.QLocale.system(), ':/translations/translations_bin/Lector_')
|
||||
app.installTranslator(translator)
|
||||
|
||||
translations_out_string = '(Translations found)'
|
||||
translations_out_string = ' (Translations found)'
|
||||
if not translations_found:
|
||||
translations_out_string = '(No translations found)'
|
||||
print(f'Locale: {QtCore.QLocale.system().name()}', translations_out_string)
|
||||
translations_out_string = ' (No translations found)'
|
||||
print(f'Locale: {QtCore.QLocale.system().name()}' + translations_out_string)
|
||||
|
||||
form = MainUI()
|
||||
form.show()
|
||||
|
@@ -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
|
||||
@@ -14,10 +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, QtCore, QtGui
|
||||
|
||||
from lector.resources import annotationswindow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
|
||||
def __init__(self, parent=None):
|
||||
@@ -85,6 +89,7 @@ class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
|
||||
|
||||
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:
|
||||
@@ -92,6 +97,7 @@ class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
|
||||
|
||||
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:
|
||||
|
@@ -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
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import logging
|
||||
import webbrowser
|
||||
|
||||
try:
|
||||
import popplerqt5
|
||||
import fitz
|
||||
from lector.parsers.pdf import render_pdf_page
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -29,6 +31,8 @@ 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):
|
||||
@@ -37,7 +41,6 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
self.parent = parent
|
||||
self.main_window = main_window
|
||||
|
||||
self.qimage = None # Will be needed to resize pdf
|
||||
self.image_pixmap = None
|
||||
self.image_cache = [None for _ in range(4)]
|
||||
|
||||
@@ -55,10 +58,7 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
self.book = rarfile.RarFile(self.filepath)
|
||||
|
||||
elif self.filetype == 'pdf':
|
||||
self.book = popplerqt5.Poppler.Document.load(self.filepath)
|
||||
self.book.setRenderHint(
|
||||
popplerqt5.Poppler.Document.Antialiasing
|
||||
and popplerqt5.Poppler.Document.TextAntialiasing)
|
||||
self.book = fitz.open(self.filepath)
|
||||
|
||||
self.common_functions = PliantWidgetsCommonFunctions(
|
||||
self, self.main_window)
|
||||
@@ -73,29 +73,71 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
self.generate_graphicsview_context_menu)
|
||||
|
||||
def loadImage(self, current_page):
|
||||
# TODO
|
||||
# For double page view: 1 before, 1 after
|
||||
all_pages = [i[1] for i in self.parent.metadata['content']]
|
||||
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):
|
||||
image_pixmap = QtGui.QPixmap()
|
||||
def page_loader(page):
|
||||
pixmap = QtGui.QPixmap()
|
||||
|
||||
if self.filetype in ('cbz', 'cbr'):
|
||||
page_data = self.book.read(current_page)
|
||||
image_pixmap.loadFromData(page_data)
|
||||
elif self.filetype == 'pdf':
|
||||
page_data = self.book.page(current_page)
|
||||
page_qimage = page_data.renderToImage(400, 400) # TODO Maybe this needs a setting?
|
||||
image_pixmap.convertFromImage(page_qimage)
|
||||
return image_pixmap
|
||||
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):
|
||||
print('Building image cache')
|
||||
logger.info('(Re)building image cache')
|
||||
current_page_index = all_pages.index(current_page)
|
||||
|
||||
for i in (-1, 0, 1, 2):
|
||||
# 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]
|
||||
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:
|
||||
@@ -124,13 +166,23 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
# No return happened so the image isn't in the cache
|
||||
generate_image_cache(current_page)
|
||||
|
||||
if self.main_window.settings['caching_enabled']:
|
||||
# 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()
|
||||
|
||||
@@ -171,12 +223,15 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
image_pixmap = self.image_pixmap.scaledToWidth(
|
||||
available_width, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
graphics_scene = QtWidgets.QGraphicsScene()
|
||||
graphics_scene.addPixmap(image_pixmap)
|
||||
graphicsScene = QtWidgets.QGraphicsScene()
|
||||
graphicsScene.addPixmap(image_pixmap)
|
||||
|
||||
self.setScene(graphics_scene)
|
||||
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)
|
||||
|
||||
@@ -202,9 +257,10 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
next_val = 0
|
||||
self.verticalScrollBar().setValue(next_val)
|
||||
|
||||
small_increment = maximum // 4
|
||||
big_increment = maximum // 2
|
||||
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:
|
||||
@@ -212,6 +268,11 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
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)
|
||||
@@ -227,39 +288,49 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
self.viewport().setCursor(QtCore.Qt.OpenHandCursor)
|
||||
else:
|
||||
self.viewport().setCursor(QtCore.Qt.ClosedHandCursor)
|
||||
self.parent.mouse_hide_timer.start(3000)
|
||||
self.parent.mouse_hide_timer.start(2000)
|
||||
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
|
||||
|
||||
def generate_graphicsview_context_menu(self, position):
|
||||
contextMenu = QtWidgets.QMenu()
|
||||
|
||||
saveAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('filesaveas'),
|
||||
self._translate('PliantQGraphicsView', 'Save page as...'))
|
||||
|
||||
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'))
|
||||
else:
|
||||
if self.main_window.settings['show_bars']:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQGraphicsView', 'Distraction Free mode')
|
||||
else:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQGraphicsView', 'Exit Distraction Free mode')
|
||||
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 (+)'))
|
||||
@@ -290,6 +361,11 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
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')
|
||||
@@ -301,7 +377,7 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
self.image_pixmap.save(save_file[0])
|
||||
|
||||
if action == bookmarksToggleAction:
|
||||
self.parent.toggle_bookmarks()
|
||||
self.parent.toggle_side_dock(1)
|
||||
if action == dfToggleAction:
|
||||
self.main_window.toggle_distraction_free()
|
||||
if action == fsToggleAction:
|
||||
@@ -322,6 +398,8 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
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
|
||||
|
||||
|
||||
@@ -380,7 +458,9 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
def record_position(self, return_as_bookmark=False):
|
||||
self.parent.metadata['position']['is_read'] = False
|
||||
|
||||
cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
|
||||
# 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
|
||||
@@ -406,21 +486,21 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
if self.annotation_mode:
|
||||
self.annotation_mode = False
|
||||
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.parent.annotationDock.show()
|
||||
self.parent.annotationDock.setWindowOpacity(.95)
|
||||
self.parent.sideDock.show()
|
||||
self.parent.sideDock.setWindowOpacity(.95)
|
||||
|
||||
self.current_annotation = None
|
||||
self.parent.annotationListView.clearSelection()
|
||||
self.parent.sideDock.annotations.annotationListView.clearSelection()
|
||||
|
||||
else:
|
||||
self.annotation_mode = True
|
||||
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
||||
self.parent.annotationDock.hide()
|
||||
self.parent.sideDock.hide()
|
||||
|
||||
selected_index = self.parent.annotationListView.currentIndex()
|
||||
self.current_annotation = self.parent.annotationModel.data(
|
||||
selected_index = self.parent.sideDock.annotations.annotationListView.currentIndex()
|
||||
self.current_annotation = self.parent.sideDock.annotationModel.data(
|
||||
selected_index, QtCore.Qt.UserRole)
|
||||
print('Current annotation: ' + self.current_annotation['name'])
|
||||
logger.info('Selected annotation: ' + self.current_annotation['name'])
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# This takes care of annotation placement
|
||||
@@ -429,13 +509,17 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
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 = self.current_annotation['components']
|
||||
annotation_components = annotation['components']
|
||||
|
||||
self.annotator.set_current_annotation(
|
||||
annotation_type, annotation_components)
|
||||
@@ -448,7 +532,7 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
# Maybe use annotation name for a consolidated annotation list
|
||||
|
||||
this_annotation = {
|
||||
'name': self.current_annotation['name'],
|
||||
'name': annotation['name'],
|
||||
'applicable_to': applicable_to,
|
||||
'type': annotation_type,
|
||||
'cursor': (cursor_start, cursor_end),
|
||||
@@ -461,8 +545,6 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
self.annotation_dict[current_chapter] = []
|
||||
self.annotation_dict[current_chapter].append(this_annotation)
|
||||
|
||||
self.toggle_annotation_mode()
|
||||
|
||||
def generate_textbrowser_context_menu(self, position):
|
||||
selection = self.textCursor().selection()
|
||||
selection = selection.toPlainText()
|
||||
@@ -480,16 +562,33 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
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' "{selection}"')
|
||||
searchSubMenu = contextMenu.addMenu(
|
||||
search_submenu_string + f' "{elided_selection}"')
|
||||
searchSubMenu.setIcon(self.main_window.QImageFactory.get_image('search'))
|
||||
|
||||
searchAction = searchSubMenu.addAction(
|
||||
@@ -506,6 +605,30 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
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'))
|
||||
@@ -517,21 +640,10 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
self.main_window.QImageFactory.get_image('remove'),
|
||||
self._translate('PliantQTextBrowser', 'Delete annotation'))
|
||||
|
||||
if self.parent.is_fullscreen:
|
||||
fsToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('view-fullscreen'),
|
||||
self._translate('PliantQTextBrowser', 'Exit fullscreen'))
|
||||
else:
|
||||
if self.main_window.settings['show_bars']:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQTextBrowser', 'Distraction Free mode')
|
||||
else:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQTextBrowser', 'Exit Distraction Free mode')
|
||||
|
||||
dfToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('visibility'),
|
||||
distraction_free_prompt)
|
||||
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(
|
||||
@@ -542,12 +654,17 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
|
||||
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:
|
||||
self.main_window.bookToolBar.searchBar.setText(selection)
|
||||
self.main_window.bookToolBar.searchBar.setFocus()
|
||||
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}')
|
||||
@@ -558,16 +675,18 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
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_bookmarks()
|
||||
self.parent.toggle_side_dock(0)
|
||||
|
||||
if action == fsToggleAction:
|
||||
self.parent.exit_fullscreen()
|
||||
@@ -582,7 +701,7 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
||||
else:
|
||||
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.parent.mouse_hide_timer.start(3000)
|
||||
self.parent.mouse_hide_timer.start(2000)
|
||||
QtWidgets.QTextBrowser.mouseMoveEvent(self, event)
|
||||
|
||||
|
||||
@@ -635,23 +754,43 @@ class PliantWidgetsCommonFunctions:
|
||||
self.change_chapter(-1)
|
||||
|
||||
def change_chapter(self, direction, was_button_pressed=None):
|
||||
current_toc_index = self.main_window.bookToolBar.tocBox.currentIndex()
|
||||
max_toc_index = self.main_window.bookToolBar.tocBox.count() - 1
|
||||
current_tab = self.pw.parent
|
||||
current_position = current_tab.metadata['position']['current_chapter']
|
||||
final_position = len(current_tab.metadata['content'])
|
||||
|
||||
if (current_toc_index < max_toc_index and direction == 1) or (
|
||||
current_toc_index > 0 and direction == -1):
|
||||
self.main_window.bookToolBar.tocBox.setCurrentIndex(
|
||||
current_toc_index + direction)
|
||||
# Prevent scrolling below page 1
|
||||
if current_position == 1 and direction == -1:
|
||||
return
|
||||
|
||||
# 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())
|
||||
# Prevent scrolling beyond last page
|
||||
if (current_position == final_position) and direction == 1:
|
||||
return
|
||||
|
||||
if not was_button_pressed:
|
||||
self.pw.ignore_wheel_event = True
|
||||
# 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:
|
||||
@@ -680,7 +819,8 @@ class PliantWidgetsCommonFunctions:
|
||||
if not self.are_we_doing_images_only:
|
||||
cursor = self.pw.textCursor()
|
||||
cursor.setPosition(0)
|
||||
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
|
||||
cursor.movePosition(
|
||||
QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
previewCharFormat = QtGui.QTextCharFormat()
|
||||
previewCharFormat.setFontStyleStrategy(
|
||||
@@ -737,11 +877,13 @@ class PliantWidgetsCommonFunctions:
|
||||
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'])
|
||||
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'])
|
||||
position_percentage = (
|
||||
self.pw.parent.metadata['position']['current_block'] /
|
||||
self.pw.parent.metadata['position']['total_blocks'])
|
||||
|
||||
# Update position percentage
|
||||
if model_index:
|
||||
@@ -751,14 +893,27 @@ class PliantWidgetsCommonFunctions:
|
||||
def generate_combo_box_action(self, contextMenu):
|
||||
contextMenu.addSeparator()
|
||||
|
||||
toc_combobox = QtWidgets.QComboBox()
|
||||
toc_data = [i[0] for i in self.pw.parent.metadata['content']]
|
||||
toc_combobox.addItems(toc_data)
|
||||
toc_combobox.setCurrentIndex(
|
||||
self.pw.main_window.bookToolBar.tocBox.currentIndex())
|
||||
toc_combobox.currentIndexChanged.connect(
|
||||
self.pw.main_window.bookToolBar.tocBox.setCurrentIndex)
|
||||
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(toc_combobox)
|
||||
comboboxAction.setDefaultWidget(tocComboBox)
|
||||
contextMenu.addAction(comboboxAction)
|
||||
|
@@ -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
|
||||
@@ -17,13 +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 = {
|
||||
@@ -81,7 +83,8 @@ 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)
|
||||
|
||||
@@ -207,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])
|
||||
@@ -240,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,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,9 +15,20 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.request
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia
|
||||
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
|
||||
|
||||
@@ -56,8 +67,11 @@ 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']
|
||||
@@ -72,12 +86,16 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
if response.getcode() == 200:
|
||||
return_json = json.loads(response.read())
|
||||
return return_json
|
||||
except urllib.error.HTTPError:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -86,6 +104,7 @@ 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
|
||||
|
||||
@@ -104,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)
|
||||
@@ -149,16 +168,27 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
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,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
|
||||
@@ -14,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):
|
||||
@@ -66,31 +71,3 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
x_draw = option.rect.bottomRight().x() - 30
|
||||
y_draw = option.rect.bottomRight().y() - 35
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
|
||||
|
||||
class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, main_window, parent=None):
|
||||
super(BookmarkDelegate, self).__init__()
|
||||
self.main_window = main_window
|
||||
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.main_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)
|
||||
|
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,325 +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 = ''.join(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,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
|
||||
@@ -14,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 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):
|
||||
@@ -237,6 +241,7 @@ class ViewProfileModification:
|
||||
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)
|
||||
@@ -244,41 +249,41 @@ class ViewProfileModification:
|
||||
self.bookToolBar.originalSize.setChecked(False)
|
||||
|
||||
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
|
||||
self.comic_profile['zoom_mode'] = 'manualZoom'
|
||||
self.comic_profile['padding'] += 50
|
||||
comic_profile['zoom_mode'] = 'manualZoom'
|
||||
comic_profile['padding'] += 50
|
||||
|
||||
# This prevents infinite zoom out
|
||||
if self.comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
|
||||
self.comic_profile['padding'] -= 50
|
||||
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):
|
||||
self.comic_profile['zoom_mode'] = 'manualZoom'
|
||||
self.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 self.comic_profile['padding'] < 0:
|
||||
self.comic_profile['padding'] = 0
|
||||
if comic_profile['padding'] < 0:
|
||||
comic_profile['padding'] = 0
|
||||
|
||||
if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W:
|
||||
self.comic_profile['zoom_mode'] = 'fitWidth'
|
||||
self.comic_profile['padding'] = 0
|
||||
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:
|
||||
self.comic_profile['zoom_mode'] = 'bestFit'
|
||||
comic_profile['zoom_mode'] = 'bestFit'
|
||||
self.bookToolBar.bestFit.setChecked(True)
|
||||
|
||||
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
|
||||
self.comic_profile['zoom_mode'] = 'originalSize'
|
||||
comic_profile['zoom_mode'] = 'originalSize'
|
||||
self.bookToolBar.originalSize.setChecked(True)
|
||||
|
||||
self.format_contentView()
|
||||
|
||||
def format_contentView(self):
|
||||
current_tab = self.tabWidget.widget(
|
||||
self.tabWidget.currentIndex())
|
||||
current_tab = self.tabWidget.currentWidget()
|
||||
|
||||
try:
|
||||
current_metadata = current_tab.metadata
|
||||
@@ -287,7 +292,6 @@ class ViewProfileModification:
|
||||
|
||||
if current_metadata['images_only']:
|
||||
background = self.comic_profile['background']
|
||||
padding = self.comic_profile['padding']
|
||||
zoom_mode = self.comic_profile['zoom_mode']
|
||||
|
||||
if zoom_mode == 'fitWidth':
|
||||
@@ -301,7 +305,7 @@ class ViewProfileModification:
|
||||
'background-color: %s' % background.name())
|
||||
|
||||
current_tab.format_view(
|
||||
None, None, None, background, padding, None, None)
|
||||
None, None, None, background, None, None, None)
|
||||
|
||||
else:
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
|
@@ -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
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from PyQt5 import QtGui, QtCore
|
||||
@@ -23,6 +24,8 @@ 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):
|
||||
@@ -47,7 +50,7 @@ class Library:
|
||||
'LIKE')
|
||||
|
||||
if not books:
|
||||
print('Database returned nothing')
|
||||
logger.warning('Database returned nothing')
|
||||
return
|
||||
|
||||
elif mode == 'addition':
|
||||
@@ -98,18 +101,7 @@ class Library:
|
||||
position = i[5]
|
||||
if position:
|
||||
position = pickle.loads(position)
|
||||
if position['is_read']:
|
||||
position_perc = 1
|
||||
else:
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_block'] / position['total_blocks'])
|
||||
except (KeyError, ZeroDivisionError):
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_chapter'] / position['total_chapters'])
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
position_perc = generate_position_percentage(position)
|
||||
|
||||
try:
|
||||
file_exists = os.path.exists(path)
|
||||
@@ -149,6 +141,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)
|
||||
@@ -319,7 +312,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)):
|
||||
@@ -335,3 +328,23 @@ class Library:
|
||||
# Remove invalid paths from the database as well
|
||||
database.DatabaseFunctions(
|
||||
self.main_window.database_path).delete_from_database('Path', invalid_paths)
|
||||
|
||||
|
||||
def generate_position_percentage(position):
|
||||
if not position:
|
||||
return None
|
||||
|
||||
if position['is_read']:
|
||||
position_perc = 1
|
||||
else:
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_block'] / position['total_blocks'])
|
||||
except (KeyError, ZeroDivisionError):
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_chapter'] / position['total_chapters'])
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
|
||||
return position_perc
|
||||
|
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.1'
|
||||
|
||||
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,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
|
||||
@@ -14,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):
|
||||
|
@@ -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
|
||||
@@ -14,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
|
||||
|
||||
@@ -99,7 +98,8 @@ 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'
|
||||
|
||||
@@ -343,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,10 +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):
|
||||
@@ -34,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 'Unknown'
|
||||
|
||||
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,87 +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
|
||||
import collections
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import popplerqt5
|
||||
import fitz
|
||||
from PyQt5 import QtGui
|
||||
|
||||
|
||||
class ParsePDF:
|
||||
def __init__(self, filename, *args):
|
||||
self.filename = filename
|
||||
self.book = None
|
||||
self.metadata = None
|
||||
|
||||
def read_book(self):
|
||||
self.book = popplerqt5.Poppler.Document.load(self.filename)
|
||||
if not self.book:
|
||||
return
|
||||
self.book = fitz.open(self.filename)
|
||||
|
||||
self.metadata = BeautifulSoup(self.book.metadata(), 'xml')
|
||||
def generate_metadata(self):
|
||||
title = self.book.metadata['title']
|
||||
if not title:
|
||||
title = os.path.splitext(os.path.basename(self.filename))[0]
|
||||
|
||||
def get_title(self):
|
||||
author = self.book.metadata['author']
|
||||
if not author:
|
||||
author = 'Unknown'
|
||||
|
||||
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 int(year.replace('\n', '')[:4])
|
||||
except (AttributeError, ValueError):
|
||||
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)
|
||||
|
||||
try:
|
||||
cover_page = self.book.page(0)
|
||||
cover_image = cover_page.renderToImage(300, 300)
|
||||
return resize_image(cover_image)
|
||||
except AttributeError:
|
||||
return None
|
||||
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>
|
||||
|
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/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 |
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 |
@@ -1,5 +1,16 @@
|
||||
<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>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1119</width>
|
||||
<width>1139</width>
|
||||
<height>612</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -15,7 +15,7 @@
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QListView" name="listView">
|
||||
<widget class="SaysHelloWhenClicked" name="listView">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@@ -175,6 +175,20 @@
|
||||
</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>
|
||||
@@ -213,6 +227,66 @@ Reopen book to see changes</string>
|
||||
</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>
|
||||
@@ -536,15 +610,39 @@ Reopen book to see changes</string>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="aboutPage">
|
||||
<layout class="QGridLayout" name="gridLayout_6">
|
||||
<layout class="QGridLayout" name="gridLayout_9">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextBrowser" name="aboutBox">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
<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>
|
||||
@@ -553,6 +651,20 @@ Reopen book to see changes</string>
|
||||
</item>
|
||||
<item>
|
||||
<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">
|
||||
@@ -586,6 +698,13 @@ Reopen book to see changes</string>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SaysHelloWhenClicked</class>
|
||||
<extends>QListView</extends>
|
||||
<header>lector.widgets</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@@ -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,10 +11,10 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
Dialog.setObjectName("Dialog")
|
||||
Dialog.resize(1119, 612)
|
||||
Dialog.resize(1139, 612)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Dialog)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.listView = QtWidgets.QListView(Dialog)
|
||||
self.listView = SaysHelloWhenClicked(Dialog)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
@@ -106,6 +106,12 @@ class Ui_Dialog(object):
|
||||
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)
|
||||
@@ -123,6 +129,30 @@ class Ui_Dialog(object):
|
||||
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()
|
||||
@@ -274,17 +304,39 @@ class Ui_Dialog(object):
|
||||
self.stackedWidget.addWidget(self.annotationsPage)
|
||||
self.aboutPage = QtWidgets.QWidget()
|
||||
self.aboutPage.setObjectName("aboutPage")
|
||||
self.gridLayout_6 = QtWidgets.QGridLayout(self.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.aboutPage)
|
||||
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)
|
||||
@@ -298,6 +350,7 @@ class Ui_Dialog(object):
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
self.aboutTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(Dialog)
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
@@ -318,12 +371,20 @@ class Ui_Dialog(object):
|
||||
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"))
|
||||
@@ -333,6 +394,11 @@ class Ui_Dialog(object):
|
||||
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"))
|
||||
|
||||
from lector.widgets import SaysHelloWhenClicked
|
||||
|
@@ -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
|
||||
@@ -17,10 +17,13 @@
|
||||
# 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):
|
||||
@@ -119,10 +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')
|
||||
@@ -137,8 +150,9 @@ class Settings:
|
||||
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')
|
||||
@@ -201,8 +215,14 @@ class Settings:
|
||||
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')
|
||||
@@ -212,3 +232,5 @@ class Settings:
|
||||
self.settings.beginGroup('annotations')
|
||||
self.settings.setValue('annotationList', current_settings['annotations'])
|
||||
self.settings.endGroup()
|
||||
|
||||
logger.info('Settings saved')
|
||||
|
@@ -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
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import os
|
||||
import copy
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
@@ -29,6 +30,9 @@ 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):
|
||||
@@ -52,7 +56,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
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
|
||||
@@ -96,8 +103,11 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
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)
|
||||
@@ -107,11 +117,14 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
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.listModel = QtGui.QStandardItemModel(self.listView)
|
||||
|
||||
library_string = self._translate('SettingsUI', 'Library')
|
||||
switches_string = self._translate('SettingsUI', 'Switches')
|
||||
@@ -134,7 +147,9 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self.main_window.QImageFactory.get_image(this_icon))
|
||||
self.listModel.appendRow(item)
|
||||
self.listView.setModel(self.listModel)
|
||||
self.listView.clicked.connect(self.page_switch)
|
||||
|
||||
# Custom signal to account for page changes
|
||||
self.listView.newIndexSignal.connect(self.list_index_changed)
|
||||
|
||||
# Annotation related buttons
|
||||
# Icon names
|
||||
@@ -166,11 +181,37 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
# 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(
|
||||
@@ -183,7 +224,7 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
self.main_window.generate_library_filter_menu(paths)
|
||||
directory_data = {}
|
||||
if not paths:
|
||||
print('Database: No paths for settings...')
|
||||
logger.warning('No book paths saved')
|
||||
else:
|
||||
# Convert to the dictionary format that is
|
||||
# to be fed into the QFileSystemModel
|
||||
@@ -194,7 +235,8 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
'check_state': i[3]}
|
||||
|
||||
self.filesystemModel = MostExcellentFileSystemModel(directory_data)
|
||||
self.filesystemModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
|
||||
self.filesystemModel.setFilter(
|
||||
QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
|
||||
self.treeView.setModel(self.filesystemModel)
|
||||
|
||||
# TODO
|
||||
@@ -207,7 +249,8 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -255,9 +298,13 @@ 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
|
||||
|
||||
@@ -303,16 +350,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
# 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.main_window)
|
||||
self.thread.finished.connect(self.main_window.move_on)
|
||||
self.thread.finished.connect(
|
||||
lambda: self.main_window.move_on(self.thread.errors))
|
||||
self.thread.start()
|
||||
|
||||
def page_switch(self, index):
|
||||
self.stackedWidget.setCurrentIndex(index.row())
|
||||
if index.row() == 0:
|
||||
self.okButton.setVisible(True)
|
||||
else:
|
||||
self.okButton.setVisible(False)
|
||||
|
||||
def cancel_pressed(self):
|
||||
self.filesystemModel.tag_data = copy.deepcopy(self.tag_data_copy)
|
||||
self.hide()
|
||||
@@ -322,8 +363,15 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
event.accept()
|
||||
|
||||
def showEvent(self, event):
|
||||
# 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):
|
||||
@@ -363,6 +411,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
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()
|
||||
|
||||
@@ -374,7 +426,8 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
'performCulling': 'perform_culling',
|
||||
'cachingEnabled': 'caching_enabled',
|
||||
'hideScrollBars': 'hide_scrollbars',
|
||||
'attenuateTitles': 'attenuate_titles'}
|
||||
'attenuateTitles': 'attenuate_titles',
|
||||
'autoCover': 'auto_cover'}
|
||||
|
||||
self.main_window.settings[
|
||||
sender_dict[sender]] = not self.main_window.settings[sender_dict[sender]]
|
||||
@@ -478,3 +531,30 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
|
||||
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()
|
||||
|
321
lector/sorter.py
@@ -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,63 +15,79 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# INSTRUCTIONS
|
||||
# Every parser is supposed to have the following methods. None returns are not allowed.
|
||||
# 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
|
||||
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
|
||||
from multiprocessing import Pool, Manager, cpu_count
|
||||
thread_count = cpu_count()
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from lector import database
|
||||
|
||||
from lector.parsers.epub import ParseEPUB
|
||||
from lector.parsers.mobi import ParseMOBI
|
||||
from lector.parsers.comicbooks import ParseCOMIC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sorter = {
|
||||
'epub': ParseEPUB,
|
||||
'mobi': ParseMOBI,
|
||||
'azw': ParseMOBI,
|
||||
'azw3': ParseMOBI,
|
||||
'azw4': ParseMOBI,
|
||||
'prc': ParseMOBI,
|
||||
'cbz': ParseCOMIC,
|
||||
'cbr': ParseCOMIC}
|
||||
|
||||
# The following imports are for optional dependencies
|
||||
try:
|
||||
# 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
|
||||
except ImportError:
|
||||
print('python-poppler-qt5 is not installed. Pdf files will not work.')
|
||||
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 and xmltodict_check:
|
||||
from lector.parsers.epub import ParseEPUB
|
||||
from lector.parsers.mobi import ParseMOBI
|
||||
from lector.parsers.fb2 import ParseFB2
|
||||
|
||||
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 = '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):
|
||||
@@ -86,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
|
||||
@@ -99,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':
|
||||
@@ -120,7 +138,6 @@ 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}
|
||||
|
||||
@@ -160,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
|
||||
@@ -169,88 +186,122 @@ 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:
|
||||
this_error = 'Unsupported extension: ' + filename
|
||||
self.errors.append(this_error)
|
||||
logger.error(this_error)
|
||||
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)
|
||||
|
||||
this_book = {}
|
||||
this_book[file_md5] = {
|
||||
'hash': file_md5,
|
||||
'path': filename}
|
||||
# None of the following have an exception type specified
|
||||
# This will keep everything from crashing, but will make
|
||||
# troubleshooting difficult
|
||||
# TODO
|
||||
# In application notifications
|
||||
|
||||
# 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()
|
||||
author = book_ref.get_author()
|
||||
year = book_ref.get_year()
|
||||
isbn = book_ref.get_isbn()
|
||||
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
|
||||
|
||||
tags = None
|
||||
if self.auto_tags:
|
||||
tags = book_ref.get_tags()
|
||||
this_book = {}
|
||||
this_book[file_md5] = {
|
||||
'hash': file_md5,
|
||||
'path': filename}
|
||||
|
||||
cover_image_raw = book_ref.get_cover_image()
|
||||
if cover_image_raw:
|
||||
cover_image = resize_image(cover_image_raw)
|
||||
else:
|
||||
cover_image = None
|
||||
# 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
|
||||
|
||||
this_book[file_md5]['cover_image'] = cover_image
|
||||
this_book[file_md5]['addition_mode'] = self.addition_mode
|
||||
title = metadata.title
|
||||
author = metadata.author
|
||||
year = metadata.year
|
||||
isbn = metadata.isbn
|
||||
|
||||
if self.work_mode == 'reading':
|
||||
all_content = book_ref.get_contents()
|
||||
tags = None
|
||||
if self.auto_tags:
|
||||
tags = metadata.tags
|
||||
|
||||
# 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 Book contains only images
|
||||
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)
|
||||
|
||||
content = all_content[0]
|
||||
images_only = all_content[1]['images_only']
|
||||
this_book[file_md5]['cover_image'] = cover_image
|
||||
this_book[file_md5]['addition_mode'] = self.addition_mode
|
||||
|
||||
if not content:
|
||||
content = [('Invalid', 'Something went horribly wrong')]
|
||||
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
|
||||
|
||||
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]
|
||||
cover = book_data[7]
|
||||
annotations = book_data[8]
|
||||
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
|
||||
this_book[file_md5]['cover'] = cover
|
||||
this_book[file_md5]['annotations'] = annotations
|
||||
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:
|
||||
@@ -260,8 +311,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:
|
||||
@@ -272,7 +324,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)
|
||||
|
||||
@@ -297,21 +349,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)
|
||||
|
||||
@@ -323,3 +383,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,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,14 +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):
|
||||
@@ -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,9 +130,13 @@ 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):
|
||||
@@ -141,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)
|
||||
|
||||
@@ -171,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,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
|
||||
@@ -14,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):
|
||||
@@ -27,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,7 +46,7 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self)
|
||||
self.annotationButton = QtWidgets.QAction(
|
||||
image_factory.get_image('annotate'),
|
||||
self._translate('BookToolBar', 'Annotations'),
|
||||
self._translate('BookToolBar', 'Annotations (Ctrl + N)'),
|
||||
self)
|
||||
self.addBookmarkButton = QtWidgets.QAction(
|
||||
image_factory.get_image('bookmark-new'),
|
||||
@@ -55,13 +56,17 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
image_factory.get_image('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 (F11)'),
|
||||
self._translate('BookToolBar', 'Fullscreen (F)'),
|
||||
self)
|
||||
self.resetProfile = QtWidgets.QAction(
|
||||
image_factory.get_image('reload'),
|
||||
@@ -72,14 +77,14 @@ class BookToolBar(QtWidgets.QToolBar):
|
||||
self.addAction(self.fontButton)
|
||||
self.fontButton.setCheckable(True)
|
||||
self.fontButton.triggered.connect(self.toggle_font_settings)
|
||||
self.addSeparator()
|
||||
self.addAction(self.annotationButton)
|
||||
self.annotationButton.setCheckable(True)
|
||||
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)
|
||||
|
||||
@@ -177,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)
|
||||
@@ -201,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)
|
||||
@@ -242,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,
|
||||
@@ -258,41 +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)
|
||||
@@ -306,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)
|
||||
@@ -344,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
|
||||
|
||||
@@ -365,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'),
|
||||
@@ -391,14 +408,29 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
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)
|
||||
@@ -417,6 +449,7 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
self.addSeparator()
|
||||
self.addAction(self.colorButton)
|
||||
self.addAction(self.settingsButton)
|
||||
self.addAction(self.aboutButton)
|
||||
|
||||
# Filter
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
@@ -427,8 +460,7 @@ 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('LibraryToolBar', 'Title')
|
||||
@@ -443,15 +475,18 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
||||
|
||||
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
|
||||
@@ -459,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)
|
||||
|
@@ -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
|
||||
@@ -17,18 +17,18 @@
|
||||
# TODO
|
||||
# Reading modes
|
||||
# Double page, Continuous etc
|
||||
# Especially for comics
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
from lector.models import BookmarkProxyModel
|
||||
from lector.delegates import BookmarkDelegate
|
||||
from lector.sorter import resize_image
|
||||
from lector.dockwidgets import PliantDockWidget
|
||||
from lector.contentwidgets import PliantQGraphicsView, PliantQTextBrowser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tab(QtWidgets.QWidget):
|
||||
def __init__(self, metadata, main_window, parent=None):
|
||||
@@ -37,18 +37,31 @@ class Tab(QtWidgets.QWidget):
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
self.first_run = True
|
||||
self.main_window = main_window
|
||||
self.metadata = metadata # Save progress data into this dictionary
|
||||
self.are_we_doing_images_only = self.metadata['images_only']
|
||||
self.is_fullscreen = False
|
||||
self.is_library = False
|
||||
|
||||
self.masterLayout = QtWidgets.QHBoxLayout(self)
|
||||
self.masterLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime()
|
||||
|
||||
# Create relevant containers
|
||||
if not self.metadata['annotations']:
|
||||
self.metadata['annotations'] = {}
|
||||
if not self.metadata['bookmarks']:
|
||||
self.metadata['bookmarks'] = {}
|
||||
|
||||
# Generate toc Model
|
||||
self.tocModel = QtGui.QStandardItemModel()
|
||||
self.tocModel.setHorizontalHeaderLabels(('Table of Contents',))
|
||||
self.generate_toc_model()
|
||||
|
||||
# Get the current position of the book
|
||||
if self.metadata['position']:
|
||||
# A book might have been marked read without being opened
|
||||
if self.metadata['position']['is_read']:
|
||||
self.generate_position(True)
|
||||
current_chapter = self.metadata['position']['current_chapter']
|
||||
@@ -56,53 +69,49 @@ class Tab(QtWidgets.QWidget):
|
||||
self.generate_position()
|
||||
current_chapter = 1
|
||||
|
||||
chapter_content = self.metadata['content'][current_chapter - 1][1]
|
||||
|
||||
# Create relevant containers
|
||||
if not self.metadata['annotations']:
|
||||
self.metadata['annotations'] = {}
|
||||
|
||||
# See bookmark availability
|
||||
if not self.metadata['bookmarks']:
|
||||
self.metadata['bookmarks'] = {}
|
||||
|
||||
# The content display widget is, by default a QTextBrowser.
|
||||
# In case the incoming data is only images
|
||||
# such as in the case of comic book files,
|
||||
# we want a QGraphicsView widget doing all the heavy lifting
|
||||
# instead of a QTextBrowser
|
||||
if self.are_we_doing_images_only: # Boolean
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
self.contentView = PliantQGraphicsView(
|
||||
self.metadata['path'], self.main_window, self)
|
||||
self.contentView.loadImage(chapter_content)
|
||||
else:
|
||||
self.contentView = PliantQTextBrowser(self.main_window, self)
|
||||
|
||||
else:
|
||||
self.contentView = PliantQTextBrowser(
|
||||
self.main_window, self)
|
||||
self.contentView.setReadOnly(True)
|
||||
|
||||
# TODO
|
||||
# Change this when HTML navigation works
|
||||
self.contentView.setOpenLinks(False)
|
||||
|
||||
# TODO
|
||||
# Rename the .css files to something else here and keep
|
||||
# a record of them .Currently, I'm just removing them
|
||||
# for the sake of simplicity
|
||||
relative_path_root = os.path.join(
|
||||
self.main_window.temp_dir.path(), self.metadata['hash'])
|
||||
relative_paths = []
|
||||
for i in os.walk(relative_path_root):
|
||||
|
||||
# TODO
|
||||
# Rename the .css files to something else here and keep
|
||||
# a record of them
|
||||
# Currently, I'm just removing them for the sake of simplicity
|
||||
for j in i[2]:
|
||||
file_extension = os.path.splitext(j)[1]
|
||||
if file_extension == '.css':
|
||||
file_path = os.path.join(i[0], j)
|
||||
os.remove(file_path)
|
||||
|
||||
relative_paths.append(os.path.join(relative_path_root, i[0]))
|
||||
self.contentView.setSearchPaths(relative_paths)
|
||||
|
||||
self.contentView.setOpenLinks(False) # TODO Change this when HTML navigation works
|
||||
self.contentView.setHtml(chapter_content)
|
||||
self.contentView.setReadOnly(True)
|
||||
|
||||
self.hiddenButton = QtWidgets.QToolButton(self)
|
||||
self.hiddenButton.setVisible(False)
|
||||
self.hiddenButton.clicked.connect(self.set_cursor_position)
|
||||
|
||||
# All content must be set through this function
|
||||
self.set_content(current_chapter, True, False)
|
||||
if not self.are_we_doing_images_only:
|
||||
# Setting this later breaks cursor positioning for search results
|
||||
self.hiddenButton.animateClick(50)
|
||||
|
||||
# Load annotations for current content
|
||||
@@ -116,76 +125,47 @@ class Tab(QtWidgets.QWidget):
|
||||
self.main_window.settings['scroll_speed'])
|
||||
|
||||
if self.main_window.settings['hide_scrollbars']:
|
||||
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.contentView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.contentView.setHorizontalScrollBarPolicy(
|
||||
QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.contentView.setVerticalScrollBarPolicy(
|
||||
QtCore.Qt.ScrollBarAlwaysOff)
|
||||
else:
|
||||
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.contentView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.contentView.setHorizontalScrollBarPolicy(
|
||||
QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.contentView.setVerticalScrollBarPolicy(
|
||||
QtCore.Qt.ScrollBarAsNeeded)
|
||||
|
||||
# Create the annotations dock
|
||||
self.annotationDock = PliantDockWidget(self.main_window, 'annotations', self.contentView)
|
||||
self.annotationDock.setWindowTitle(self._translate('Tab', 'Annotations'))
|
||||
self.annotationDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.annotationDock.hide()
|
||||
|
||||
self.annotationListView = QtWidgets.QListView(self.annotationDock)
|
||||
self.annotationListView.setResizeMode(QtWidgets.QListWidget.Adjust)
|
||||
self.annotationListView.setMaximumWidth(350)
|
||||
self.annotationListView.doubleClicked.connect(self.contentView.toggle_annotation_mode)
|
||||
self.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
|
||||
self.annotationDock.setWidget(self.annotationListView)
|
||||
|
||||
self.annotationModel = QtGui.QStandardItemModel(self)
|
||||
self.generate_annotation_model()
|
||||
# Create a common dock for bookmarks, annotations, and search
|
||||
self.sideDock = PliantDockWidget(
|
||||
self.main_window, False, self.contentView, self)
|
||||
self.sideDock.populate()
|
||||
|
||||
# Create the annotation notes dock
|
||||
self.annotationNoteDock = PliantDockWidget(self.main_window, 'notes', self.contentView)
|
||||
self.annotationNoteDock = PliantDockWidget(
|
||||
self.main_window, True, self.contentView, self)
|
||||
self.annotationNoteDock.setWindowTitle(self._translate('Tab', 'Note'))
|
||||
self.annotationNoteDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.annotationNoteDock.hide()
|
||||
|
||||
self.annotationNoteEdit = QtWidgets.QTextEdit(self.annotationDock)
|
||||
self.annotationNoteEdit = QtWidgets.QTextEdit(self.annotationNoteDock)
|
||||
self.annotationNoteEdit.setMaximumSize(QtCore.QSize(250, 250))
|
||||
self.annotationNoteEdit.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.annotationNoteDock.setWidget(self.annotationNoteEdit)
|
||||
|
||||
# Create the dock widget for context specific display
|
||||
self.bookmarkDock = PliantDockWidget(self.main_window, 'bookmarks', self.contentView)
|
||||
self.bookmarkDock.setWindowTitle(self._translate('Tab', 'Bookmarks'))
|
||||
self.bookmarkDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.bookmarkDock.hide()
|
||||
|
||||
self.bookmarkListView = QtWidgets.QListView(self.bookmarkDock)
|
||||
self.bookmarkListView.setResizeMode(QtWidgets.QListWidget.Adjust)
|
||||
self.bookmarkListView.setMaximumWidth(350)
|
||||
self.bookmarkListView.setItemDelegate(
|
||||
BookmarkDelegate(self.main_window, self.bookmarkListView))
|
||||
self.bookmarkListView.setUniformItemSizes(True)
|
||||
self.bookmarkListView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.bookmarkListView.customContextMenuRequested.connect(
|
||||
self.generate_bookmark_context_menu)
|
||||
self.bookmarkListView.clicked.connect(self.navigate_to_bookmark)
|
||||
self.bookmarkDock.setWidget(self.bookmarkListView)
|
||||
|
||||
self.bookmarkModel = QtGui.QStandardItemModel(self)
|
||||
self.bookmarkProxyModel = BookmarkProxyModel(self)
|
||||
self.generate_bookmark_model()
|
||||
|
||||
self.generate_keyboard_shortcuts()
|
||||
|
||||
self.masterLayout.addWidget(self.contentView)
|
||||
self.masterLayout.addWidget(self.annotationDock)
|
||||
self.masterLayout.addWidget(self.sideDock)
|
||||
self.masterLayout.addWidget(self.annotationNoteDock)
|
||||
self.masterLayout.addWidget(self.bookmarkDock)
|
||||
|
||||
# The following has to be after the docks are added to the layout
|
||||
self.annotationDock.setFloating(True)
|
||||
self.annotationDock.setWindowOpacity(.95)
|
||||
self.sideDock.setFloating(True)
|
||||
self.sideDock.setWindowOpacity(.95)
|
||||
self.annotationNoteDock.setFloating(True)
|
||||
self.annotationNoteDock.setWindowOpacity(.95)
|
||||
self.bookmarkDock.setFloating(True)
|
||||
self.bookmarkDock.setWindowOpacity(.95)
|
||||
self.sideDock.hide()
|
||||
|
||||
# Create tab in the central tab widget
|
||||
title = self.metadata['title']
|
||||
if self.main_window.settings['attenuate_titles'] and len(title) > 30:
|
||||
title = title[:30] + '...'
|
||||
@@ -208,6 +188,20 @@ class Tab(QtWidgets.QWidget):
|
||||
|
||||
self.contentView.setFocus()
|
||||
|
||||
def toggle_side_dock(self, tab_required, override_hide=False):
|
||||
if (self.sideDock.isVisible()
|
||||
and self.sideDock.sideDockTabWidget.currentIndex() == tab_required
|
||||
and not override_hide):
|
||||
self.sideDock.hide()
|
||||
elif not self.sideDock.isVisible():
|
||||
self.sideDock.show()
|
||||
if tab_required == 2:
|
||||
self.sideDock.activateWindow()
|
||||
self.sideDock.search.searchLineEdit.setFocus()
|
||||
self.sideDock.search.searchLineEdit.selectAll()
|
||||
|
||||
self.sideDock.sideDockTabWidget.setCurrentIndex(tab_required)
|
||||
|
||||
def update_last_accessed_time(self):
|
||||
self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime()
|
||||
|
||||
@@ -220,15 +214,16 @@ class Tab(QtWidgets.QWidget):
|
||||
|
||||
try:
|
||||
self.main_window.lib_ref.libraryModel.setData(
|
||||
matching_item[0], self.metadata['last_accessed'], QtCore.Qt.UserRole + 12)
|
||||
matching_item[0],
|
||||
self.metadata['last_accessed'], QtCore.Qt.UserRole + 12)
|
||||
except IndexError: # The file has been deleted
|
||||
pass
|
||||
|
||||
def set_cursor_position(self, cursor_position=None):
|
||||
def set_cursor_position(self, cursor_position=None, select_chars=0):
|
||||
try:
|
||||
required_position = self.metadata['position']['cursor_position']
|
||||
except KeyError:
|
||||
print(f'Database: Cursor position error. Recommend retry.')
|
||||
logging.error('Database: Cursor position error. Recommend retry.')
|
||||
return
|
||||
|
||||
if cursor_position:
|
||||
@@ -242,10 +237,21 @@ class Tab(QtWidgets.QWidget):
|
||||
# textCursor() RETURNS a copy of the textcursor
|
||||
cursor = self.contentView.textCursor()
|
||||
cursor.setPosition(
|
||||
required_position, QtGui.QTextCursor.MoveAnchor)
|
||||
required_position - select_chars,
|
||||
QtGui.QTextCursor.MoveAnchor)
|
||||
if select_chars > 0: # Select search results
|
||||
cursor.movePosition(
|
||||
QtGui.QTextCursor.NextCharacter,
|
||||
QtGui.QTextCursor.KeepAnchor,
|
||||
select_chars)
|
||||
self.contentView.setTextCursor(cursor)
|
||||
self.contentView.ensureCursorVisible()
|
||||
|
||||
# Finally, to make sure the cover image isn't
|
||||
# scrolled halfway through on first open,
|
||||
if self.metadata['position']['current_chapter'] == 1:
|
||||
self.contentView.verticalScrollBar().setValue(0)
|
||||
|
||||
def generate_position(self, is_read=False):
|
||||
total_chapters = len(self.metadata['content'])
|
||||
|
||||
@@ -260,10 +266,8 @@ class Tab(QtWidgets.QWidget):
|
||||
|
||||
if not self.are_we_doing_images_only:
|
||||
for i in self.metadata['content']:
|
||||
chapter_html = i[1]
|
||||
|
||||
textDocument = QtGui.QTextDocument(None)
|
||||
textDocument.setHtml(chapter_html)
|
||||
textDocument.setHtml(i)
|
||||
block_count = textDocument.blockCount()
|
||||
|
||||
blocks_per_chapter.append(block_count)
|
||||
@@ -279,30 +283,88 @@ class Tab(QtWidgets.QWidget):
|
||||
'cursor_position': 0}
|
||||
|
||||
def generate_keyboard_shortcuts(self):
|
||||
self.ksNextChapter = QtWidgets.QShortcut(
|
||||
ksNextChapter = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('Right'), self.contentView)
|
||||
self.ksNextChapter.setObjectName('nextChapter')
|
||||
self.ksNextChapter.activated.connect(self.sneaky_change)
|
||||
ksNextChapter.setObjectName('nextChapter')
|
||||
ksNextChapter.activated.connect(self.sneaky_change)
|
||||
|
||||
self.ksPrevChapter = QtWidgets.QShortcut(
|
||||
ksPrevChapter = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('Left'), self.contentView)
|
||||
self.ksPrevChapter.setObjectName('prevChapter')
|
||||
self.ksPrevChapter.activated.connect(self.sneaky_change)
|
||||
ksPrevChapter.setObjectName('prevChapter')
|
||||
ksPrevChapter.activated.connect(self.sneaky_change)
|
||||
|
||||
self.ksGoFullscreen = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('F11'), self.contentView)
|
||||
self.ksGoFullscreen.activated.connect(self.go_fullscreen)
|
||||
ksGoFullscreen = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('F'), self.contentView)
|
||||
ksGoFullscreen.activated.connect(self.go_fullscreen)
|
||||
|
||||
self.ksExitFullscreen = QtWidgets.QShortcut(
|
||||
ksExitFullscreen = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('Escape'), self.contentView)
|
||||
self.ksExitFullscreen.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksExitFullscreen.activated.connect(self.exit_fullscreen)
|
||||
ksExitFullscreen.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
ksExitFullscreen.activated.connect(self.exit_fullscreen)
|
||||
|
||||
self.ksToggleBookMarks = QtWidgets.QShortcut(
|
||||
ksToggleBookmarks = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('Ctrl+B'), self.contentView)
|
||||
self.ksToggleBookMarks.activated.connect(self.toggle_bookmarks)
|
||||
ksToggleBookmarks.activated.connect(
|
||||
lambda: self.toggle_side_dock(0))
|
||||
|
||||
# Shortcuts not required for comic view functionality
|
||||
if not self.are_we_doing_images_only:
|
||||
ksToggleAnnotations = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('Ctrl+N'), self.contentView)
|
||||
ksToggleAnnotations.activated.connect(
|
||||
lambda: self.toggle_side_dock(1))
|
||||
|
||||
ksToggleSearch = QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence('Ctrl+F'), self.contentView)
|
||||
ksToggleSearch.activated.connect(
|
||||
lambda: self.toggle_side_dock(2))
|
||||
|
||||
def generate_toc_model(self):
|
||||
# The toc list is:
|
||||
# 0: Level
|
||||
# 1: Title
|
||||
# 2: Chapter content / page number
|
||||
# pprint it out to get a better idea of structure
|
||||
|
||||
toc = self.metadata['toc']
|
||||
parent_list = []
|
||||
for i in toc:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setText(i[1])
|
||||
item.setData(i[2], QtCore.Qt.UserRole)
|
||||
item.setData(i[1], QtCore.Qt.UserRole + 1)
|
||||
|
||||
current_level = i[0]
|
||||
if current_level == 1:
|
||||
self.tocModel.appendRow(item)
|
||||
parent_list.clear()
|
||||
parent_list.append(item)
|
||||
else:
|
||||
parent_list[current_level - 2].appendRow(item)
|
||||
try:
|
||||
next_level = toc[toc.index(i) + 1][0]
|
||||
if next_level > current_level:
|
||||
parent_list.append(item)
|
||||
|
||||
if next_level < current_level:
|
||||
level_difference = current_level - next_level
|
||||
parent_list = parent_list[:-level_difference]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# This is needed to be able to have the toc Combobox
|
||||
# jump to the correct position in the book when it is
|
||||
# first opened
|
||||
self.main_window.bookToolBar.tocBox.setModel(self.tocModel)
|
||||
self.main_window.bookToolBar.tocTreeView.expandAll()
|
||||
|
||||
def go_fullscreen(self):
|
||||
# To allow toggles to function
|
||||
# properly after the fullscreening
|
||||
|
||||
self.sideDock.hide()
|
||||
self.annotationNoteDock.hide()
|
||||
|
||||
if self.contentView.windowState() == QtCore.Qt.WindowFullScreen:
|
||||
self.exit_fullscreen()
|
||||
return
|
||||
@@ -316,16 +378,22 @@ class Tab(QtWidgets.QWidget):
|
||||
self.main_window.hide()
|
||||
|
||||
if not self.are_we_doing_images_only:
|
||||
self.hiddenButton.animateClick(100)
|
||||
self.hiddenButton.animateClick(50)
|
||||
|
||||
self.mouse_hide_timer.start(2000)
|
||||
self.is_fullscreen = True
|
||||
|
||||
def exit_fullscreen(self):
|
||||
for i in (self.bookmarkDock, self.annotationDock, self.annotationNoteDock):
|
||||
# Intercept escape presses
|
||||
for i in (self.annotationNoteDock, self.sideDock):
|
||||
if i.isVisible():
|
||||
i.setVisible(False)
|
||||
return
|
||||
|
||||
# Prevents cursor position change on escape presses
|
||||
if self.main_window.isVisible():
|
||||
return
|
||||
|
||||
if not self.are_we_doing_images_only:
|
||||
self.contentView.record_position()
|
||||
|
||||
@@ -345,11 +413,27 @@ class Tab(QtWidgets.QWidget):
|
||||
if not self.main_window.settings['show_bars']:
|
||||
self.main_window.toggle_distraction_free()
|
||||
|
||||
self.mouse_hide_timer.start(2000)
|
||||
self.contentView.setFocus()
|
||||
|
||||
def change_chapter_tocBox(self):
|
||||
chapter_number = self.main_window.bookToolBar.tocBox.currentIndex()
|
||||
required_content = self.metadata['content'][chapter_number][1]
|
||||
def set_content(self, required_position, tocBox_readjust=False, record_position=False):
|
||||
# All content changes must come through here
|
||||
# This function will decide how to relate
|
||||
# entries in the toc to the actual content
|
||||
|
||||
# Set the required page to the corresponding index
|
||||
# For images this is simply a page number
|
||||
# For text based books, this is the entire text of the chapter
|
||||
try:
|
||||
required_content = self.metadata['content'][required_position - 1]
|
||||
except IndexError:
|
||||
return # Do not allow cycling beyond last page
|
||||
|
||||
# Update the metadata dictionary to save position
|
||||
self.metadata['position']['current_chapter'] = required_position
|
||||
self.metadata['position']['is_read'] = False
|
||||
if record_position:
|
||||
self.contentView.record_position()
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
self.contentView.loadImage(required_content)
|
||||
@@ -357,11 +441,57 @@ class Tab(QtWidgets.QWidget):
|
||||
self.contentView.clear()
|
||||
self.contentView.setHtml(required_content)
|
||||
|
||||
self.contentView.common_functions.load_annotations(chapter_number + 1)
|
||||
# Set the contentview to look the way God intended
|
||||
self.main_window.profile_functions.format_contentView()
|
||||
self.contentView.common_functions.load_annotations(required_position)
|
||||
|
||||
def format_view(self, font, font_size, foreground,
|
||||
background, padding, line_spacing,
|
||||
text_alignment):
|
||||
# Change the index of the tocBox. This is manual and each function
|
||||
# that calls set_position must specify if it needs this adjustment
|
||||
if tocBox_readjust:
|
||||
self.set_tocBox_index(required_position, None)
|
||||
|
||||
self.contentView.setFocus()
|
||||
|
||||
def set_tocBox_index(self, current_position=None, tocBox=None):
|
||||
# Get current position from the metadata dictionary
|
||||
# in case it isn't specified
|
||||
if not current_position:
|
||||
current_position = self.metadata['position']['current_chapter']
|
||||
|
||||
position_reference = 1
|
||||
for i in reversed(self.metadata['toc']):
|
||||
if i[2] <= current_position:
|
||||
position_reference = i[2]
|
||||
break
|
||||
|
||||
# Match the position reference to the corresponding
|
||||
# index in the QTreeView / QCombobox
|
||||
try:
|
||||
matchingIndex = self.tocModel.match(
|
||||
self.tocModel.index(0, 0),
|
||||
QtCore.Qt.UserRole,
|
||||
position_reference,
|
||||
2, QtCore.Qt.MatchRecursive)[0]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
# A tocBox name is specified for the context menu
|
||||
if not tocBox:
|
||||
tocBox = self.main_window.bookToolBar.tocBox
|
||||
|
||||
# The following sets the QCombobox index according
|
||||
# to the index found above.
|
||||
tocBox.blockSignals(True)
|
||||
currentRootModelIndex = tocBox.rootModelIndex()
|
||||
tocBox.setRootModelIndex(matchingIndex.parent())
|
||||
tocBox.setCurrentIndex(matchingIndex.row())
|
||||
tocBox.setRootModelIndex(currentRootModelIndex)
|
||||
tocBox.blockSignals(False)
|
||||
|
||||
def format_view(
|
||||
self, font, font_size, foreground,
|
||||
background, padding, line_spacing,
|
||||
text_alignment):
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
# Tab color does not need to be set separately in case
|
||||
@@ -393,167 +523,41 @@ class Tab(QtWidgets.QWidget):
|
||||
'center': QtCore.Qt.AlignCenter,
|
||||
'justify': QtCore.Qt.AlignJustify}
|
||||
|
||||
current_index = self.main_window.bookToolBar.tocBox.currentIndex()
|
||||
if current_index == 0:
|
||||
block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
|
||||
else:
|
||||
block_format.setAlignment(alignment_dict[text_alignment])
|
||||
|
||||
# Also for padding
|
||||
# Using setViewPortMargins for this disables scrolling in the margins
|
||||
block_format.setLeftMargin(padding)
|
||||
block_format.setRightMargin(padding)
|
||||
|
||||
this_cursor = self.contentView.textCursor()
|
||||
this_cursor.movePosition(QtGui.QTextCursor.Start, 0, 1)
|
||||
this_cursor.setPosition(QtGui.QTextCursor.Start)
|
||||
|
||||
# Iterate over the entire document block by block
|
||||
# The document ends when the cursor position can no longer be incremented
|
||||
while True:
|
||||
# So this fixes the stupid repetitive iteration
|
||||
# I was doing over the entire thing
|
||||
# It also allows for all images to be center aligned.
|
||||
# Magic. *jazz hands*
|
||||
block_text = this_cursor.block().text().strip()
|
||||
try:
|
||||
# Object replacement char - Seems to work with images
|
||||
if ord(block_text) == 65532:
|
||||
block_format.setAlignment(
|
||||
QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
|
||||
else:
|
||||
raise TypeError
|
||||
except TypeError:
|
||||
block_format.setAlignment(alignment_dict[text_alignment])
|
||||
|
||||
# Iterate over the entire document block by block
|
||||
# The document ends when the cursor position can no longer be incremented
|
||||
old_position = this_cursor.position()
|
||||
this_cursor.mergeBlockFormat(block_format)
|
||||
this_cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1)
|
||||
this_cursor.movePosition(
|
||||
QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.MoveAnchor)
|
||||
|
||||
new_position = this_cursor.position()
|
||||
if old_position == new_position:
|
||||
break
|
||||
|
||||
def toggle_annotations(self):
|
||||
if self.annotationDock.isVisible():
|
||||
self.annotationDock.hide()
|
||||
else:
|
||||
self.annotationDock.show()
|
||||
|
||||
def generate_annotation_model(self):
|
||||
saved_annotations = self.main_window.settings['annotations']
|
||||
|
||||
if not saved_annotations:
|
||||
return
|
||||
|
||||
def add_to_model(annotation):
|
||||
item = QtGui.QStandardItem()
|
||||
item.setText(annotation['name'])
|
||||
item.setData(annotation, QtCore.Qt.UserRole)
|
||||
self.annotationModel.appendRow(item)
|
||||
|
||||
# Prevent annotation mixup
|
||||
for i in saved_annotations:
|
||||
if self.are_we_doing_images_only and i['applicable_to'] == 'images':
|
||||
add_to_model(i)
|
||||
elif not self.are_we_doing_images_only and i['applicable_to'] == 'text':
|
||||
add_to_model(i)
|
||||
|
||||
self.annotationListView.setModel(self.annotationModel)
|
||||
|
||||
def toggle_bookmarks(self):
|
||||
if self.bookmarkDock.isVisible():
|
||||
self.bookmarkDock.hide()
|
||||
else:
|
||||
self.bookmarkDock.show()
|
||||
|
||||
def add_bookmark(self):
|
||||
# TODO
|
||||
# Start dockListView.edit(index) when something new is added
|
||||
|
||||
identifier = uuid.uuid4().hex[:10]
|
||||
description = self._translate('Tab', 'New bookmark')
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
chapter = self.metadata['position']['current_chapter']
|
||||
cursor_position = 0
|
||||
else:
|
||||
chapter, cursor_position = self.contentView.record_position(True)
|
||||
|
||||
self.metadata['bookmarks'][identifier] = {
|
||||
'chapter': chapter,
|
||||
'cursor_position': cursor_position,
|
||||
'description': description}
|
||||
|
||||
self.add_bookmark_to_model(
|
||||
description, chapter, cursor_position, identifier)
|
||||
self.bookmarkDock.setVisible(True)
|
||||
|
||||
def add_bookmark_to_model(self, description, chapter, cursor_position, identifier):
|
||||
bookmark = QtGui.QStandardItem()
|
||||
bookmark.setData(description, QtCore.Qt.DisplayRole)
|
||||
|
||||
bookmark.setData(chapter, QtCore.Qt.UserRole)
|
||||
bookmark.setData(cursor_position, QtCore.Qt.UserRole + 1)
|
||||
bookmark.setData(identifier, QtCore.Qt.UserRole + 2)
|
||||
|
||||
self.bookmarkModel.appendRow(bookmark)
|
||||
self.update_bookmark_proxy_model()
|
||||
|
||||
def navigate_to_bookmark(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
chapter = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole)
|
||||
cursor_position = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole + 1)
|
||||
|
||||
self.main_window.bookToolBar.tocBox.setCurrentIndex(chapter - 1)
|
||||
if not self.are_we_doing_images_only:
|
||||
self.set_cursor_position(cursor_position)
|
||||
|
||||
def generate_bookmark_model(self):
|
||||
# TODO
|
||||
# Sorting is not working correctly
|
||||
|
||||
try:
|
||||
for i in self.metadata['bookmarks'].items():
|
||||
self.add_bookmark_to_model(
|
||||
i[1]['description'],
|
||||
i[1]['chapter'],
|
||||
i[1]['cursor_position'],
|
||||
i[0])
|
||||
except KeyError:
|
||||
title = self.metadata['title']
|
||||
|
||||
# TODO
|
||||
# Delete the bookmarks entry for this file
|
||||
print(f'Database: Bookmark error for {title}. Recommend delete entry.')
|
||||
return
|
||||
|
||||
self.generate_bookmark_proxy_model()
|
||||
|
||||
def generate_bookmark_proxy_model(self):
|
||||
self.bookmarkProxyModel.setSourceModel(self.bookmarkModel)
|
||||
self.bookmarkProxyModel.setSortCaseSensitivity(False)
|
||||
self.bookmarkProxyModel.setSortRole(QtCore.Qt.UserRole)
|
||||
self.bookmarkListView.setModel(self.bookmarkProxyModel)
|
||||
|
||||
def update_bookmark_proxy_model(self):
|
||||
self.bookmarkProxyModel.invalidateFilter()
|
||||
self.bookmarkProxyModel.setFilterParams(
|
||||
self.main_window.bookToolBar.searchBar.text())
|
||||
self.bookmarkProxyModel.setFilterFixedString(
|
||||
self.main_window.bookToolBar.searchBar.text())
|
||||
|
||||
def generate_bookmark_context_menu(self, position):
|
||||
index = self.bookmarkListView.indexAt(position)
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
bookmarkMenu = QtWidgets.QMenu()
|
||||
editAction = bookmarkMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('edit-rename'),
|
||||
self._translate('Tab', 'Edit'))
|
||||
deleteAction = bookmarkMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('trash-empty'),
|
||||
self._translate('Tab', 'Delete'))
|
||||
|
||||
action = bookmarkMenu.exec_(
|
||||
self.bookmarkListView.mapToGlobal(position))
|
||||
|
||||
if action == editAction:
|
||||
self.bookmarkListView.edit(index)
|
||||
|
||||
if action == deleteAction:
|
||||
row = index.row()
|
||||
delete_uuid = self.bookmarkModel.item(row).data(QtCore.Qt.UserRole + 2)
|
||||
|
||||
self.metadata['bookmarks'].pop(delete_uuid)
|
||||
self.bookmarkModel.removeRow(index.row())
|
||||
|
||||
def hide_mouse(self):
|
||||
self.contentView.viewport().setCursor(QtCore.Qt.BlankCursor)
|
||||
|
||||
@@ -570,70 +574,6 @@ class Tab(QtWidgets.QWidget):
|
||||
self.main_window.closeEvent()
|
||||
|
||||
|
||||
class PliantDockWidget(QtWidgets.QDockWidget):
|
||||
def __init__(self, main_window, intended_for, contentView, parent=None):
|
||||
super(PliantDockWidget, self).__init__()
|
||||
self.main_window = main_window
|
||||
self.intended_for = intended_for
|
||||
self.contentView = contentView
|
||||
self.current_annotation = None
|
||||
|
||||
def showEvent(self, event):
|
||||
viewport_height = self.contentView.viewport().size().height()
|
||||
viewport_topRight = self.contentView.mapToGlobal(
|
||||
self.contentView.viewport().rect().topRight())
|
||||
viewport_topLeft = self.contentView.mapToGlobal(
|
||||
self.contentView.viewport().rect().topLeft())
|
||||
|
||||
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
|
||||
dock_y = viewport_topRight.y() + (viewport_height * .10)
|
||||
dock_height = viewport_height * .80
|
||||
|
||||
if self.intended_for == 'bookmarks':
|
||||
dock_width = desktop_size.width() // 5.5
|
||||
dock_x = viewport_topRight.x() - dock_width + 1
|
||||
self.main_window.bookToolBar.bookmarkButton.setChecked(True)
|
||||
|
||||
elif self.intended_for == 'annotations':
|
||||
dock_width = desktop_size.width() // 5.5
|
||||
dock_x = viewport_topLeft.x()
|
||||
self.main_window.bookToolBar.annotationButton.setChecked(True)
|
||||
|
||||
elif self.intended_for == 'notes':
|
||||
dock_width = dock_height = desktop_size.width() // 5.5
|
||||
dock_x = QtGui.QCursor.pos().x()
|
||||
dock_y = QtGui.QCursor.pos().y()
|
||||
|
||||
self.main_window.active_bookmark_docks.append(self)
|
||||
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
|
||||
self.setFocus() # TODO This doesn't work
|
||||
|
||||
def hideEvent(self, event=None):
|
||||
if self.intended_for == 'bookmarks':
|
||||
self.main_window.bookToolBar.bookmarkButton.setChecked(False)
|
||||
elif self.intended_for == 'annotations':
|
||||
self.main_window.bookToolBar.annotationButton.setChecked(False)
|
||||
elif self.intended_for == 'notes':
|
||||
annotationNoteEdit = self.findChild(QtWidgets.QTextEdit)
|
||||
if self.current_annotation:
|
||||
self.current_annotation['note'] = annotationNoteEdit.toPlainText()
|
||||
|
||||
try:
|
||||
self.main_window.active_bookmark_docks.remove(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def set_annotation(self, annotation):
|
||||
self.current_annotation = annotation
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.main_window.bookToolBar.annotationButton.setChecked(False)
|
||||
self.hide()
|
||||
|
||||
# Ignoring this event prevents application closure when everything is fullscreened
|
||||
event.ignore()
|
||||
|
||||
|
||||
class PliantQGraphicsScene(QtWidgets.QGraphicsScene):
|
||||
def __init__(self, parent=None):
|
||||
super(PliantQGraphicsScene, self).__init__(parent)
|
||||
@@ -714,7 +654,8 @@ class DragDropTableView(QtWidgets.QTableView):
|
||||
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
|
||||
self.setFrameShape(QtWidgets.QFrame.Box)
|
||||
self.setFrameShadow(QtWidgets.QFrame.Plain)
|
||||
self.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
|
||||
self.setSizeAdjustPolicy(
|
||||
QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
|
||||
self.setEditTriggers(
|
||||
QtWidgets.QAbstractItemView.DoubleClicked |
|
||||
QtWidgets.QAbstractItemView.EditKeyPressed |
|
||||
@@ -744,3 +685,19 @@ class DragDropTableView(QtWidgets.QTableView):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
super(DragDropTableView, self).dropEvent(event)
|
||||
|
||||
|
||||
class SaysHelloWhenClicked(QtWidgets.QListView):
|
||||
# Signal declarations must be outside the constructor
|
||||
# The argument is the type of the data emitted
|
||||
newIndexSignal = QtCore.pyqtSignal(QtCore.QModelIndex)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(SaysHelloWhenClicked, self).__init__(parent)
|
||||
self.parent = parent
|
||||
|
||||
def currentChanged(self, index, previous_index):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
self.newIndexSignal.emit(index)
|
||||
|
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
|
8
setup.py
@@ -1,14 +1,10 @@
|
||||
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 = '4'
|
||||
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()
|
||||
@@ -73,6 +69,6 @@ setup(
|
||||
extras_require={
|
||||
'dev': DEV_DEPS,
|
||||
'test': TEST_DEPS,
|
||||
'PDF': ['python-poppler-qt5']
|
||||
'PDF': ['pymupdf']
|
||||
},
|
||||
)
|
||||
|