70 Commits
0.4 ... 0.5.1

Author SHA1 Message Date
BasioMeusPuga
c71985f621 Minor fixes 2019-03-09 10:11:12 -05:00
BasioMeusPuga
c8fe0ba8b6 Make dependency checks less... crashy 2019-03-03 07:56:05 -05:00
BasioMeusPuga
d6df28c503 Merge pull request #92 from guoyunhe/hidpi-icons
Make icons sharp in HiDPI screen
2019-03-02 19:47:50 -05:00
Guo Yunhe
75ace25c57 Make icons sharp in HiDPI screen 2019-03-02 13:37:04 +01:00
BasioMeusPuga
d2d7dc2c8f Update for release 2019-03-01 23:17:22 -05:00
BasioMeusPuga
f622b0c23e Implement image color inversion 2019-02-19 00:01:07 +05:30
BasioMeusPuga
f312714a2c Multiple fixes
MuPDF import error
Definition text color
Database logging
2019-02-15 00:17:47 +05:30
BasioMeusPuga
c3f26ca225 Whoops 2019-02-13 00:27:48 +05:30
BasioMeusPuga
f6c7307647 Update README.md 2019-02-13 00:12:01 +05:30
BasioMeusPuga
b1714b9674 Error notifications
In application log viewer / database reset
Cleanup settings navigation
2019-02-13 00:09:37 +05:30
BasioMeusPuga
fa030e3060 Update flatpak manifest
Implement automated missing cover downloading
2019-02-12 11:51:16 +05:30
BasioMeusPuga
564db06179 Multiple fixes
Images are now center aligned
Better logging
2019-02-11 11:54:05 +05:30
BasioMeusPuga
3cd75807f9 Fix MOBI parser
Update Kindleunpack
Discover new and exciting bugs
2019-02-10 17:58:35 +05:30
BasioMeusPuga
f6f9d01060 Cleanup parsers 2019-02-10 09:03:12 +05:30
BasioMeusPuga
c6e30b67ad Improve EPUB parser compatibility and speed
Completely break MOBI parser
2019-02-10 06:47:51 +05:30
BasioMeusPuga
e4be239bf0 Overhaul EPUB parsing and ToC generation 2019-02-09 04:21:22 +05:30
BasioMeusPuga
1e004774c9 Search Google books for missing covers
Small fixes
2019-02-05 23:12:22 +05:30
BasioMeusPuga
91ca1e2190 Improve fb2 parsing
Miscellaneous fixes to navigation
2019-02-05 02:50:47 +05:30
BasioMeusPuga
d1662b47d9 Small fixes 2019-02-02 13:04:38 +05:30
BasioMeusPuga
dfe0fceea9 Small fixes
Compulsive refactor
2019-01-31 13:00:21 +05:30
BasioMeusPuga
268014cc3a Refactor sideDock 2019-01-31 01:51:47 +05:30
BasioMeusPuga
d1b1d7c59c Cleanup 2019-01-29 07:32:52 +05:30
BasioMeusPuga
470fc1078f Multiple fixes
Update translations
2019-01-28 02:28:43 +05:30
BasioMeusPuga
96f4d9193a Update requirements.txt 2019-01-26 19:53:40 +05:30
BasioMeusPuga
7aa42603bd Debulk widgets module 2019-01-26 19:44:58 +05:30
BasioMeusPuga
9a6392d1e6 Update README.md 2019-01-26 19:42:33 +05:30
BasioMeusPuga
739b84e9f4 Overhaul TOC generation and navigation 2019-01-26 19:03:30 +05:30
BasioMeusPuga
66746b4eaa Improve cover creation for PDFs 2019-01-22 23:32:25 +05:30
BasioMeusPuga
164450a888 Shift to MuPDF backend for pdf rendering 2019-01-22 22:36:32 +05:30
BasioMeusPuga
191ea7ef3a Update README.md 2019-01-19 22:34:21 +05:30
BasioMeusPuga
ca8ddd38a2 Improve logging
requirements.txt
Small UI fixes
2019-01-19 22:29:56 +05:30
BasioMeusPuga
a45e183914 Implement logging 2019-01-19 20:31:19 +05:30
BasioMeusPuga
506c458544 Update readme
Begin logging
Account for fb2 books without covers
2019-01-19 01:19:58 +05:30
BasioMeusPuga
5e3987dc04 Improve comic view 2019-01-17 23:03:28 +05:30
BasioMeusPuga
5b8bc1d707 Comic page increment setting 2019-01-17 22:42:11 +05:30
BasioMeusPuga
b3e4060661 UI cleanup 2019-01-17 22:17:22 +05:30
BasioMeusPuga
2185e9fcf7 Tab reordering 2019-01-17 21:53:04 +05:30
BasioMeusPuga
5d35319164 Improve mouse pointer hiding
Improve search result formatting
2019-01-16 12:58:40 +05:30
BasioMeusPuga
c6d24fd970 Multiple fixes 2019-01-16 12:25:59 +05:30
BasioMeusPuga
17f39c557b Manga mode
Comics are parsed for images only
Miscellaneous fixes
2019-01-14 15:54:29 +05:30
BasioMeusPuga
f997bc9c9a Search result highlighting
Disable UI elements when irrelevant
2019-01-09 13:50:08 +05:30
BasioMeusPuga
930a97a8fa Implement search 2019-01-09 05:58:47 +05:30
BasioMeusPuga
026fff3d7a Implement search UI
Discovered severe inadequacies. Some of them were in the program.
2019-01-08 05:47:50 +05:30
BasioMeusPuga
f9eec130dd Update translations 2019-01-03 04:05:52 +05:30
BasioMeusPuga
d75689ea97 Consolidate docks
Update copyright
Pondered the nature of protein powder
2019-01-03 03:28:31 +05:30
BasioMeusPuga
6ea5635d28 Cleanup 2018-12-01 18:59:49 +05:30
BasioMeusPuga
4b9221128c Implement single/double page modes for comics/pdfs 2018-10-31 10:39:21 +05:30
BasioMeusPuga
b5349315be UI elements for page modes 2018-10-29 23:08:34 +05:30
BasioMeusPuga
ee18f157f1 Account for absence of Qt Multimedia 2018-10-20 03:49:45 +05:30
BasioMeusPuga
ae325736d5 Update README.md 2018-07-11 13:09:16 -04:00
BasioMeusPuga
826da72d4b Adjust image paths 2018-07-11 13:07:58 -04:00
BasioMeusPuga
b5231cd383 Merge pull request #71 from psikoz/master
logo add
2018-07-11 13:02:24 -04:00
BasioMeusPuga
5ac843e48c Merge pull request #72 from guoyunhe/patch-1
Update README: openSUSE package is now official
2018-07-11 12:55:22 -04:00
Guo Yunhe
74417319be Update README: openSUSE package is now official
Lector is now included in openSUSE Tumbleweed and will be included in future Leap releases.
2018-07-11 13:15:37 +03:00
psikoz
f8555a6ed5 Update README.md 2018-07-11 12:33:03 +03:00
psikoz
4346c27adc Add files via upload 2018-07-11 12:21:09 +03:00
BasioMeusPuga
16adf57dae Add option to include TOC with bookmarks
Shift dock positions
2018-07-08 20:20:57 -04:00
BasioMeusPuga
8534088f4a Update README.md 2018-07-06 16:19:24 -04:00
BasioMeusPuga
a81ed537a6 Improve bookmark addition and deletion
Fix toolbar button checking
2018-07-06 15:38:56 -04:00
BasioMeusPuga
ed5bc0b2b9 Shift to tree view for bookmarks
Cleanup
2018-07-06 09:01:40 -04:00
BasioMeusPuga
8cb8904e58 Exception handling for improperly formatted fb2 books 2018-06-17 10:40:37 -04:00
BasioMeusPuga
aa093b8cc2 Image display in fb2 parser 2018-06-17 10:29:55 -04:00
BasioMeusPuga
42b4d0317d Update README.md 2018-06-14 16:14:11 -04:00
BasioMeusPuga
a0e463bc58 Speed up file addition
Improve fb2 parser
Fix extension checking
2018-06-14 16:10:27 -04:00
BasioMeusPuga
4a2da61b51 Merge branch 'master' of https://github.com/basiomeuspuga/lector 2018-06-13 16:33:57 -04:00
BasioMeusPuga
5c481ccafe Begin fb2 support
Fix Chinese translation
2018-06-13 16:33:30 -04:00
BasioMeusPuga
30760b879e Merge pull request #67 from guoyunhe/guoyunhe-patch-1
Add openSUSE package
2018-06-07 18:17:41 -04:00
Guo Yunhe
af7868f62a Add openSUSE package 2018-06-07 19:03:45 +03:00
BasioMeusPuga
62c44730d8 Start search functionality
Multiple fixes
2018-05-24 15:14:56 -04:00
BasioMeusPuga
045d8a3e52 Update README.md 2018-05-15 00:45:40 -04:00
66 changed files with 14359 additions and 10440 deletions

View File

@@ -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 \

View File

@@ -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
![alt tag](https://i.imgur.com/yrv2c0a.png)
![alt tag](https://i.imgur.com/516hRkS.png)
### Table view
![alt tag](https://i.imgur.com/b1XdXqP.png)
![alt tag](https://i.imgur.com/o9An7AR.png)
### Book reading view
![alt tag](https://i.imgur.com/Tei6TqF.png)
![alt tag](https://i.imgur.com/ITG63Fc.png)
### Distraction free view
![alt tag](https://i.imgur.com/g8Ltupy.png)
### Annotation support
![alt tag](https://i.imgur.com/gLK29F4.png)
### Comic reading view
![alt tag](https://i.imgur.com/U5JR35g.png)
![alt tag](https://i.imgur.com/rvvTQCM.png)
### Bookmark support
![alt tag](https://i.imgur.com/RZkmCzG.png)
![alt tag](https://i.imgur.com/Y7qoU8m.png)
### View profiles
![alt tag](https://i.imgur.com/gkJ88pi.png)
![alt tag](https://i.imgur.com/awE2q2K.png)
### Metadata editor
![alt tag](https://i.imgur.com/AqQREBf.png)
![alt tag](https://i.imgur.com/0CDpNO8.png)
### In program dictionary
![alt tag](https://i.imgur.com/Vh9xQUC.png)
![alt tag](https://i.imgur.com/RF72m2h.png)
## 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
![alt tag](https://i.imgur.com/l6zJXaH.png)
## Attributions
* [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack)

54
TODO
View File

@@ -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

View File

@@ -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",

View File

@@ -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.")

View File

@@ -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]

View File

@@ -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())

View File

@@ -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()

View File

@@ -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:

View File

@@ -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)

View File

@@ -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()

View File

@@ -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(

View File

@@ -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
View 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)

View File

@@ -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('"&gt;', '', 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('"&gt;', '', 1) + ('<br/>' * 8)
return_list.append(
(chapter_titles[-1], bs_obj_string))
return return_list

View File

@@ -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()

View File

@@ -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
View 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')

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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):

View File

@@ -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
View 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
View 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

View File

@@ -10,5 +10,7 @@
<p>Author: BasioMeusPuga <a href="mailto:disgruntled.mob@gmail.com">disgruntled.mob@gmail.com</a></p>
<p>Page:&nbsp;<a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p>
<p>License: GPLv3&nbsp;<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>&nbsp;</p></body>
</html>

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View 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

View File

@@ -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>

View 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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Attempt to download missing book covers from Google books - SLOW&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;UP/DOWN ARROW - Steps to take before turning comicbook page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Small increment</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="smallIncrementBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;UP/DOWN ARROW - Steps to take before turning comicbook page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;SPACEBAR - Steps to take before turning comicbook page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Large increment</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="largeIncrementBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;SPACEBAR - Steps to take before turning comicbook page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'raw/settings.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
@@ -11,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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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')

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
View 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

View File

@@ -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']
},
)