109 Commits
0.3.1 ... 0.5

Author SHA1 Message Date
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
BasioMeusPuga
55bee210c6 Update version for release
Update translations
2018-05-13 18:40:24 -04:00
BasioMeusPuga
d3746c8e98 Merge branch 'master' of https://github.com/basiomeuspuga/lector 2018-05-13 18:28:52 -04:00
BasioMeusPuga
ffcf07414f Implement file drag drop 2018-05-13 18:16:17 -04:00
BasioMeusPuga
ffaace2eaa Uniform tab sizes
Path search
PDF parser exception handling
2018-05-13 15:54:17 -04:00
BasioMeusPuga
32455dd859 Merge pull request #52 from jaccsr/master
Language code
2018-05-05 09:38:19 -04:00
BasioMeusPuga
3e54340694 Account for older versions of Qt 2018-05-03 08:37:22 -04:00
jaccsr
bc6c7d1c36 I forgot the language code 2018-05-02 10:23:41 +08:00
jaccsr
ebd746b7b2 Merge pull request #1 from BasioMeusPuga/master
a
2018-05-02 09:11:23 +08:00
BasioMeusPuga
ebc3ef9f1b Update README.md 2018-05-01 18:33:44 -04:00
BasioMeusPuga
7238605441 Update translations: Chinese (simplified) 2018-05-01 18:29:59 -04:00
BasioMeusPuga
ea86737970 Merge pull request #50 from jaccsr/master
Chinese (simplified) translation
2018-05-01 18:22:19 -04:00
jaccsr
ab4c586c06 add chinese(simp) language 2018-05-02 06:06:23 +08:00
BasioMeusPuga
7977bde410 Multiple fixes 2018-04-29 08:11:46 -04:00
BasioMeusPuga
626472dd04 Comic view drag and drop
Menu icons
Polish for docks
2018-04-20 10:00:12 +05:30
BasioMeusPuga
d9efe2da3c Annotation notes 2018-04-19 20:35:22 +05:30
BasioMeusPuga
ec197f0829 Annotation saving, loading, and deletion 2018-04-19 15:19:20 +05:30
BasioMeusPuga
335479bcfb Small fixes 2018-04-17 11:24:16 +05:30
BasioMeusPuga
cbf01c6d16 Annotation placement 2018-04-16 13:00:49 +05:30
BasioMeusPuga
98ca118a60 Remove unnecessary shebangs
How this isn't a Ricky Martin song, we'll never know
2018-04-12 10:15:20 +05:30
BasioMeusPuga
c7aa0e28ee Web search for selection
Bugfixes
2018-04-11 01:43:21 +05:30
BasioMeusPuga
528c2e387c Improve spacebar navigation
Refactor variables
2018-04-10 12:39:52 +05:30
BasioMeusPuga
bc54d6b686 Complete annotation editor
Annotation saving and loading
2018-04-08 14:41:31 +05:30
BasioMeusPuga
8f298de58e Cleanup optional imports
Disable the multiprocessing module on Windows
Update translations
2018-04-02 19:57:46 +05:30
BasioMeusPuga
366859ebe0 Fix incorrect function argument 2018-04-02 01:06:53 +05:30
BasioMeusPuga
8c51cc047e Split content display widgets into new module 2018-03-31 10:43:13 +05:30
BasioMeusPuga
5081a31f1a Fine tune progress display
Option: Set consider read at percentage
Small fixes
2018-03-31 10:31:57 +05:30
BasioMeusPuga
aff69d95c1 Make progress work with block count
Break database thoroughly
Fix pdf year bug
2018-03-31 03:03:49 +05:30
BasioMeusPuga
0b8427c864 Flatpak manifest: No pdf support but otherwise functional 2018-03-30 21:03:37 +05:30
BasioMeusPuga
43dd6a34d9 Implement basic annotation editor / preview 2018-03-30 20:48:13 +05:30
BasioMeusPuga
2f4adfc183 Small fixes 2018-03-30 11:06:48 +05:30
BasioMeusPuga
0d015ad72e Auto hide Tab-bar and Statusbar 2018-03-29 02:50:01 +05:30
BasioMeusPuga
406ca0485f Position setting should work all the time now
Learn not to swear so much at the screen
Cover icons in the tab bar
Shift Scan Library button from the Library tab to the Library toolbar
2018-03-29 01:45:58 +05:30
BasioMeusPuga
ab6760226e Search position seeking fix for multiple tabs
Space navigation tries its best to not cut lines off
2018-03-28 20:17:00 +05:30
BasioMeusPuga
66c8626d43 Significant improvements to bookmark dock display 2018-03-28 00:46:12 +05:30
BasioMeusPuga
5fa724ae69 Implement scroll speed slider 2018-03-27 21:58:35 +05:30
BasioMeusPuga
d417a94829 Fix fullscreen toggle affecting reading position bug
Bookmark navigation much more reliable
Start annotations UI
2018-03-27 08:23:07 +05:30
BasioMeusPuga
9c85a1075e Fix context menu behavior 2018-03-24 01:34:42 +05:30
BasioMeusPuga
dd4b502861 Move contentView profile modification functions to guifunctions module 2018-03-24 01:15:33 +05:30
BasioMeusPuga
0f963b20f9 Move cover loading and culling to guifunctions module 2018-03-24 00:30:58 +05:30
BasioMeusPuga
f63b6627b2 Update README.md 2018-03-24 00:00:12 +05:30
BasioMeusPuga
00db5d5e0f Update translations 2018-03-23 23:57:49 +05:30
BasioMeusPuga
5e53d40e68 Redesign settings dialog
Remove dependency on requests
2018-03-23 23:56:01 +05:30
BasioMeusPuga
6ffa6934ed Fix splitting for a repeated anchor 2018-03-23 17:48:58 +05:30
86 changed files with 21309 additions and 8822 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,19 +15,23 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SOURCES += lector/__main__.py \
lector/annotations.py \
lector/contentwidgets.py \
lector/definitionsdialog.py \
lector/dockwidgets.py \
lector/library.py \
lector/metadatadialog.py \
lector/models.py \
lector/widgets.py \
lector/library.py \
lector/toolbars.py \
lector/settingsdialog.py \
resources/definitions.py \
resources/settingswindow.py \
resources/metadata.py \
resources/mainwindow.py
lector/toolbars.py \
lector/widgets.py \
lector/resources/definitions.py \
lector/resources/settingswindow.py \
lector/resources/metadata.py \
lector/resources/mainwindow.py
TRANSLATIONS += resources/translations/Lector_es.ts \
resources/translations/Lector_fr.ts \
resources/translations/Lector_de.ts \
resources/translations/SAMPLE.ts
TRANSLATIONS += lector/resources/translations/Lector_es.ts \
lector/resources/translations/Lector_fr.ts \
lector/resources/translations/Lector_de.ts \
lector/resources/translations/Lector_zh.ts \
lector/resources/translations/SAMPLE.ts

View File

@@ -1,27 +1,42 @@
# Lector
<p align="center"><img src="lector/resources/raw/logo/logotype_horizontal.png" alt="Lector" height="90px"></p>
Qt based ebook reader
Currently supports:
* pdf
* epub
* fb2
* mobi
* azw / azw3 / azw4
* cbr / cbz
Support for a bunch of other formats is coming. Please see the TODO for additional information.
## Contribute
[Paypal](https://www.paypal.me/supportlector)
Bitcoin: 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro
## Requirements
### Needed
| Package | Version tested |
| --- | --- |
| Qt5 | 5.10.1 |
| Python | 3.6 |
| PyQt5 | 5.10.1 |
| python-requests | 2.18.4 |
| python-lxml | 4.3.0 |
| python-beautifulsoup4 | 4.6.0 |
| poppler-qt5 | 0.61.1 |
| python-poppler-qt5 | 0.24.2 |
| python-xmltodict | 0.11.0 |
poppler-qt5 and python-poppler-qt5 are optional.
### Optional
| Package | Version tested |
| --- | --- |
| python-pymupdf | 1.14.5 |
## Support
When reporting issues:
* Make sure you're at the latest commit.
* Run with `$EXECUTABLEPATH debug`.
* Include the log `~/.local/share/Lector/Lector.log` AND terminal output.
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
## Installation
### Manual
@@ -34,49 +49,55 @@ poppler-qt5 and python-poppler-qt5 are optional.
3. OR launch with `lector/__main__.py`
### Available packages
* [AUR](https://aur.archlinux.org/packages/lector-git/)
* [AUR - Releases](https://aur.archlinux.org/packages/lector/)
* [AUR - Git](https://aur.archlinux.org/packages/lector-git/)
* [Gentoo (unofficial)](https://bitbucket.org/szymonsz/gen2-overlay/src/master/app-text/lector/)
* [Fedora (unofficial)](https://copr.fedorainfracloud.org/coprs/bugzy/lector/)
* [openSUSE](https://software.opensuse.org/package/lector)
## Translations
1. There is a `SAMPLE.ts` file in `resources/translations`. Open it in `Qt Linguist`.
1. There is a `SAMPLE.ts` file [here](https://github.com/BasioMeusPuga/Lector/tree/master/lector/resources/translations). Open it in `Qt Linguist`.
2. Pick the language you wish to translate to.
3. Translate relevant strings.
4. Try to resist the urge to include profanity.
5. Save the file as `Lector_<language>` and send it to me, preferably as a pull request.
Oh, please keep the translations short. There's only so much space for UI elements.
Please keep the translations short. There's only so much space for UI elements.
## Screenshots
### Main window
![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)

66
TODO
View File

@@ -1,8 +1,10 @@
TODO
General:
✓ Internationalization
Application icon
.desktop file
Application icon
.desktop file
✓ Shift to logging instead of print statements
Flatpak and AppImage support
Options:
✓ Automatic library management
✓ Recursive file addition
@@ -29,9 +31,14 @@ TODO
✓ Information dialog widget
✓ Allow editing of database data through the UI + for Bookmarks
✓ Include (action) icons with the applications
✓ Drag and drop support for the library
✓ Tab reordering
Additional Settings:
✓ Create covers for books without them - VERY SLOW
Set focus to newly added file
Reading:
✓ Drop down for TOC
✓ Treeview navigation for TOC
✓ Override the keypress event of the textedit
✓ Use format* icons for toolbar buttons
✓ Implement book view settings with a(nother) toolbar
@@ -57,43 +64,74 @@ TODO
✓ Paragraph indentation
✓ Comic view keyboard shortcuts
✓ Comic view context menu
✓ Make the bookmark dock float over the reading area
✓ Spacebar should not cut off lines at the top
✓ Track open bookmark windows so they can be closed quickly at exit
✓ Search document using QTextCursor
✓ Double page / column view
✓ For comics
Caching is currently non functional
Annotations
✓ Text
✓ Disable buttons for annotations, search in images
Adjust key navigation according to viewport dimensions
Search document using QTextCursor?
Filetypes:
✓ pdf support
Parse TOC
Parse TOC
✓ epub support
✓ Homegrown solution please
✓ cbz, cbr support
✓ Keep font settings enabled but only for background color
✓ Double page view
✓ Manga mode
✓ mobi, azw support
Limit the extra files produced by KindleUnpack
Have them save to memory
✓ fb2 support
✓ Images need to show up in their placeholders
Other:
✓ Define every widget in code
Bugs:
Slider position change might be acting up
Deselecting all directories in the settings dialog also filters out manually added books
Last line in QTextBrowser should never be cut off
Bookmark name for a page that's not on the TOC and has nothing before
Screen position still keeps jumping when inside a paragraph
Better recursion needed for fb2 toc
Search results should ignore punctuation
Keep text size for annotations
Sort by new is not working
Secondary:
Annotations
Text to speech
Definitions dialog needs to respond to escape
Zoom slider for comics
Tab tooltip
Additional Settings:
Find definitions on Google
Disable progressbar - 20% book addition speed improvement
Disable cover loading when reading - Saves ~2M / book
Special formatting for each chapter's title
Signal end of chapter with some text
Graphical themes
Change focus rectangle dimensions
Tab reordering
Universal Ctrl + Tab
Allow tabs to detach and form their own windows
Goodreads API: Ratings, Read, Recommendations
Get ISBN using python-isbnlib
Pagination
Use embedded fonts + CSS
Scrolling: Smooth / By Line
Spacebar should not cut off lines at the top
Shift to logging instead of print statements
txt, doc, chm, djvu, fb2 support
txt, doc, chm, djvu support
Include icons for filetype emblems
Drag and drop support for the library
Comic view modes
Continuous paging
Double pages
Ignore a / the / numbers for sorting purposes
? Add only one file type if multiple are present
? Create emblem per filetype
In application notifications
Notification in case the filter is filtering out all files with no option in place
Option to fit images to viewport
Need help with:
Double page view for books
Scrolling: Smooth / By Line
Annotation preview in listView
Pagination

View File

@@ -0,0 +1,143 @@
{
"app-id":"com.basiomeuspuga.Lector",
"runtime":"org.kde.Platform",
"runtime-version":"5.12",
"sdk":"org.kde.Sdk",
"command":"lector",
"rename-icon":"Lector",
"rename-desktop-file":"lector.desktop",
"rename-appdata-file":"lector.appdata.xml",
"finish-args":[
"--filesystem=host",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--share=ipc",
"--share=network"
],
"build-options":{
"cflags":"-O2",
"cxxflags":"-O2"
},
"modules":[
{
"name": "PyQt5",
"buildsystem": "simple",
"build-commands": [
"pip3 install --prefix=/app PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
],
"modules":[
{
"name":"PyQt5-sip",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/ae/9c/74fba0b62a0756d214f9aded5b0184130f7866def7532fa68823f34feefa/PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl",
"sha256":"04bd0bb8b6f8fa03c2dfbdfff0c8c9bfb3f46a21dd4cac73983dae93bf949523"
}
],
"buildsystem":"simple",
"build-commands":[
"pip3 install --prefix=/app PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl"
]
}
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/5e/91/9ac8827d0af428e756f461a3aa7bcbc53d6450edfe026e27569f5ff3689e/PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl",
"sha256": "fd5946795b39922f971cf92dec799aadc7544b7fa993a79b9f6059f13d817e6e"
}
]
},
{
"name":"beautifulsoup4",
"buildsystem":"simple",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/1d/5d/3260694a59df0ec52f8b4883f5d23b130bc237602a1411fa670eae12351e/beautifulsoup4-4.7.1-py3-none-any.whl",
"sha256":"034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858"
}
],
"modules":[
{
"name": "soupsieve",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/bf/b3/2473abf05c4950c6a829ed5dcbc40d8b56d4351d15d6939c8ffb7c6b1a14/soupsieve-1.7.3-py2.py3-none-any.whl",
"sha256":"466910df7561796a60748826781ebe9a888f7a1668a636ae86783f44d10aae73"
}
],
"buildsystem":"simple",
"build-commands":[
"pip3 install --prefix=/app soupsieve-1.7.3-py2.py3-none-any.whl"
]
}
],
"build-commands":[
"pip3 install --prefix=/app beautifulsoup4-4.7.1-py3-none-any.whl"
]
},
{
"name":"xmltodict",
"buildsystem":"simple",
"sources":[
{
"type": "file",
"url":"https://files.pythonhosted.org/packages/28/fd/30d5c1d3ac29ce229f6bdc40bbc20b28f716e8b363140c26eff19122d8a5/xmltodict-0.12.0-py2.py3-none-any.whl",
"sha256":"8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"
}
],
"build-commands":[
"pip3 install --prefix=/app xmltodict-0.12.0-py2.py3-none-any.whl"
]
},
{
"name":"PyMuPDF",
"buildsystem":"simple",
"build-commands": [
"pip3 install --prefix=/app PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl"
],
"sources":[
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/3c/df/4bfaee2631b505d502c2ba64aa437799f0a64125edb1d4c4c38044ad1ecc/PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl",
"sha256": "a49798b58cce00e09b8a4431a5f64a400b11a0959f29507187c471208ce040a5"
}
]
},
{
"name":"lxml",
"buildsystem":"simple",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/08/f2/04bf04e42c070f65b64dbde02d2c94851251f19f5e9f803cc8f8bc61ac77/lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl",
"sha256":"c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a"
}
],
"build-commands":[
"pip3 install --prefix=/app lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl"
]
},
{
"name":"lector",
"buildsystem":"simple",
"ensure-writable":[
"/lib/python*/site-packages/easy-install.pth"
],
"sources":[
{
"type":"git",
"url":"https://github.com/BasioMeusPuga/Lector.git"
}
],
"build-commands":[
"python3 setup.py build",
"python3 setup.py install --prefix=/app"
]
}
]
}

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

File diff suppressed because it is too large Load Diff

317
lector/annotations.py Normal file
View File

@@ -0,0 +1,317 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore, QtGui
from lector.resources import annotationswindow
logger = logging.getLogger(__name__)
class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
def __init__(self, parent=None):
super(AnnotationsUI, self).__init__()
self.setupUi(self)
self.parent = parent
self._translate = QtCore.QCoreApplication.translate
# Current annotation
self.modelIndex = None # The index of the annotations list model in the parent dialog
self.current_annotation = {}
# Populate annotation type
textmarkup_string = self._translate('AnnotationsUI', 'Text markup')
all_types = [textmarkup_string]
for i in all_types:
self.typeBox.addItem(i)
# Init defaults
self.default_stylesheet = self.foregroundCheck.styleSheet()
self.foregroundColor = QtGui.QColor.fromRgb(0, 0, 0)
self.underlineColor = QtGui.QColor.fromRgb(255, 0, 0)
self.highlightColor = QtGui.QColor.fromRgb(66, 209, 56)
self.underline_styles = {
'Solid': QtGui.QTextCharFormat.SingleUnderline,
'Dashes': QtGui.QTextCharFormat.DashUnderline,
'Dots': QtGui.QTextCharFormat.DotLine,
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
# Push buttons
self.foregroundColorButton.clicked.connect(self.modify_annotation)
self.highlightColorButton.clicked.connect(self.modify_annotation)
self.underlineColorButton.clicked.connect(self.modify_annotation)
self.okButton.clicked.connect(self.ok_pressed)
self.cancelButton.clicked.connect(self.hide)
# Underline combo box
underline_items = ['Solid', 'Dashes', 'Dots', 'Wavy']
self.underlineType.addItems(underline_items)
self.underlineType.currentIndexChanged.connect(self.modify_annotation)
# Text markup related checkboxes
self.foregroundCheck.clicked.connect(self.modify_annotation)
self.highlightCheck.clicked.connect(self.modify_annotation)
self.boldCheck.clicked.connect(self.modify_annotation)
self.italicCheck.clicked.connect(self.modify_annotation)
self.underlineCheck.clicked.connect(self.modify_annotation)
def show_dialog(self, mode, index=None):
# TODO
# Account for annotation type here
# and point to a relevant set of widgets accordingly
if mode == 'edit' or mode == 'preview':
self.modelIndex = index
this_annotation = self.parent.annotationModel.data(
index, QtCore.Qt.UserRole)
annotation_name = this_annotation['name']
self.nameEdit.setText(annotation_name)
annotation_components = this_annotation['components']
if 'foregroundColor' in annotation_components:
self.foregroundCheck.setChecked(True)
self.foregroundColor = annotation_components['foregroundColor']
self.set_button_background_color(
self.foregroundColorButton, annotation_components['foregroundColor'])
else:
self.foregroundCheck.setChecked(False)
if 'highlightColor' in annotation_components:
self.highlightCheck.setChecked(True)
self.highlightColor = annotation_components['highlightColor']
self.set_button_background_color(
self.highlightColorButton, annotation_components['highlightColor'])
else:
self.highlightCheck.setChecked(False)
if 'bold' in annotation_components:
self.boldCheck.setChecked(True)
else:
self.boldCheck.setChecked(False)
if 'italic' in annotation_components:
self.italicCheck.setChecked(True)
else:
self.italicCheck.setChecked(False)
if 'underline' in annotation_components:
self.underlineCheck.setChecked(True)
underline_params = annotation_components['underline']
self.underlineType.setCurrentText(underline_params[0])
self.set_button_background_color(
self.underlineColorButton, underline_params[1])
else:
self.underlineCheck.setChecked(False)
elif mode == 'add':
new_annotation_string = self._translate('AnnotationsUI', 'New annotation')
self.nameEdit.setText(new_annotation_string)
all_checkboxes = (
self.foregroundCheck, self.highlightCheck,
self.boldCheck, self.italicCheck, self.underlineCheck)
for i in all_checkboxes:
i.setChecked(False)
self.modelIndex = None
self.set_button_background_color(
self.foregroundColorButton, self.foregroundColor)
self.set_button_background_color(
self.highlightColorButton, self.highlightColor)
self.set_button_background_color(
self.underlineColorButton, self.underlineColor)
self.update_preview()
if mode != 'preview':
self.show()
def set_button_background_color(self, button, color):
button.setStyleSheet(
"QPushButton {{background-color: {0}}}".format(color.name()))
def update_preview(self):
cursor = self.parent.previewView.textCursor()
cursor.setPosition(0)
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
# TODO
# Other kinds of text markup
previewCharFormat = QtGui.QTextCharFormat()
if self.foregroundCheck.isChecked():
previewCharFormat.setForeground(self.foregroundColor)
highlight = QtCore.Qt.transparent
if self.highlightCheck.isChecked():
highlight = self.highlightColor
previewCharFormat.setBackground(highlight)
font_weight = QtGui.QFont.Normal
if self.boldCheck.isChecked():
font_weight = QtGui.QFont.Bold
previewCharFormat.setFontWeight(font_weight)
if self.italicCheck.isChecked():
previewCharFormat.setFontItalic(True)
if self.underlineCheck.isChecked():
previewCharFormat.setFontUnderline(True)
previewCharFormat.setUnderlineColor(self.underlineColor)
previewCharFormat.setUnderlineStyle(
self.underline_styles[self.underlineType.currentText()])
previewCharFormat.setFontStyleStrategy(
QtGui.QFont.PreferAntialias)
cursor.setCharFormat(previewCharFormat)
cursor.clearSelection()
self.parent.previewView.setTextCursor(cursor)
def modify_annotation(self):
sender = self.sender()
if isinstance(sender, QtWidgets.QCheckBox):
if not sender.isChecked():
self.update_preview()
return
new_color = None
if sender == self.foregroundColorButton:
new_color = self.get_color(self.foregroundColor)
self.foregroundColor = new_color
if sender == self.highlightColorButton:
new_color = self.get_color(self.highlightColor)
self.highlightColor = new_color
if sender == self.underlineColorButton:
new_color = self.get_color(self.underlineColor)
self.underlineColor = new_color
if new_color:
self.set_button_background_color(sender, new_color)
self.update_preview()
def get_color(self, current_color):
color_dialog = QtWidgets.QColorDialog()
new_color = color_dialog.getColor(current_color)
if new_color.isValid(): # Returned in case cancel is pressed
return new_color
else:
return current_color
def ok_pressed(self):
annotation_name = self.nameEdit.text()
if annotation_name == '':
self.nameEdit.setText('Why do you like bugs? WHY?')
return
annotation_components = {}
if self.foregroundCheck.isChecked():
annotation_components['foregroundColor'] = self.foregroundColor
if self.highlightCheck.isChecked():
annotation_components['highlightColor'] = self.highlightColor
if self.boldCheck.isChecked():
annotation_components['bold'] = True
if self.italicCheck.isChecked():
annotation_components['italic'] = True
if self.underlineCheck.isChecked():
annotation_components['underline'] = (
self.underlineType.currentText(), self.underlineColor)
self.current_annotation = {
'name': annotation_name,
'applicable_to': 'text',
'type': 'text_markup',
'components': annotation_components}
if self.modelIndex:
self.parent.annotationModel.setData(
self.modelIndex, annotation_name, QtCore.Qt.DisplayRole)
self.parent.annotationModel.setData(
self.modelIndex, self.current_annotation, QtCore.Qt.UserRole)
else: # New annotation
new_annotation_item = QtGui.QStandardItem()
new_annotation_item.setText(annotation_name)
new_annotation_item.setData(self.current_annotation, QtCore.Qt.UserRole)
self.parent.annotationModel.appendRow(new_annotation_item)
self.hide()
class AnnotationPlacement:
def __init__(self):
self.annotation_type = None
self.annotation_components = None
self.underline_styles = {
'Solid': QtGui.QTextCharFormat.SingleUnderline,
'Dashes': QtGui.QTextCharFormat.DashUnderline,
'Dots': QtGui.QTextCharFormat.DotLine,
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
def set_current_annotation(self, annotation_type, annotation_components):
# Components expected to be a dictionary
self.annotation_type = annotation_type # This is currently unused
self.annotation_components = annotation_components
def format_text(self, cursor, start_here, end_here):
# This is applicable only to the PliantQTextBrowser
# for the text_markup style of annotation
# The cursor is the textCursor of the QTextEdit
# containing the text that has to be modified
if not self.annotation_components:
return
cursor.setPosition(start_here)
cursor.setPosition(end_here, QtGui.QTextCursor.KeepAnchor)
newCharFormat = QtGui.QTextCharFormat()
if 'foregroundColor' in self.annotation_components:
newCharFormat.setForeground(
self.annotation_components['foregroundColor'])
if 'highlightColor' in self.annotation_components:
newCharFormat.setBackground(
self.annotation_components['highlightColor'])
if 'bold' in self.annotation_components:
newCharFormat.setFontWeight(QtGui.QFont.Bold)
if 'italic' in self.annotation_components:
newCharFormat.setFontItalic(True)
if 'underline' in self.annotation_components:
newCharFormat.setFontUnderline(True)
newCharFormat.setUnderlineStyle(
self.underline_styles[self.annotation_components['underline'][0]])
newCharFormat.setUnderlineColor(
self.annotation_components['underline'][1])
newCharFormat.setFontStyleStrategy(
QtGui.QFont.PreferAntialias)
cursor.setCharFormat(newCharFormat)
cursor.clearSelection()
return cursor

919
lector/contentwidgets.py Normal file
View File

@@ -0,0 +1,919 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import zipfile
import logging
import webbrowser
try:
import fitz
from lector.parsers.pdf import render_pdf_page
except ImportError:
pass
from PyQt5 import QtWidgets, QtGui, QtCore
from lector.rarfile import rarfile
from lector.threaded import BackGroundCacheRefill
from lector.annotations import AnnotationPlacement
logger = logging.getLogger(__name__)
class PliantQGraphicsView(QtWidgets.QGraphicsView):
def __init__(self, filepath, main_window, parent=None):
super(PliantQGraphicsView, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
self.parent = parent
self.main_window = main_window
self.image_pixmap = None
self.image_cache = [None for _ in range(4)]
self.thread = None
self.annotation_dict = self.parent.metadata['annotations']
self.filepath = filepath
self.filetype = os.path.splitext(self.filepath)[1][1:]
if self.filetype == 'cbz':
self.book = zipfile.ZipFile(self.filepath)
elif self.filetype == 'cbr':
self.book = rarfile.RarFile(self.filepath)
elif self.filetype == 'pdf':
self.book = fitz.open(self.filepath)
self.common_functions = PliantWidgetsCommonFunctions(
self, self.main_window)
self.ignore_wheel_event = False
self.ignore_wheel_event_number = 0
self.setMouseTracking(True)
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(
self.generate_graphicsview_context_menu)
def loadImage(self, current_page):
all_pages = self.parent.metadata['content']
current_page_index = all_pages.index(current_page)
double_page_mode = False
if (self.main_window.settings['double_page_mode']
and (current_page_index not in (0, len(all_pages) - 1))):
double_page_mode = True
def load_page(current_page):
def page_loader(page):
pixmap = QtGui.QPixmap()
if self.filetype in ('cbz', 'cbr'):
page_data = self.book.read(page)
pixmap.loadFromData(page_data)
elif self.filetype == 'pdf':
page_data = self.book.loadPage(page)
pixmap = render_pdf_page(page_data)
return pixmap
firstPixmap = page_loader(current_page)
if not double_page_mode:
return firstPixmap
next_page = all_pages[current_page_index + 1]
secondPixmap = page_loader(next_page)
# Pixmap height should be the greater of the 2 images
pixmap_height = firstPixmap.height()
if secondPixmap.height() > pixmap_height:
pixmap_height = secondPixmap.height()
bigPixmap = QtGui.QPixmap(
firstPixmap.width() + secondPixmap.width() + 5,
pixmap_height)
bigPixmap.fill(QtCore.Qt.transparent)
imagePainter = QtGui.QPainter(bigPixmap)
manga_mode = self.main_window.settings['manga_mode']
if manga_mode:
imagePainter.drawPixmap(0, 0, secondPixmap)
imagePainter.drawPixmap(secondPixmap.width() + 4, 0, firstPixmap)
else:
imagePainter.drawPixmap(0, 0, firstPixmap)
imagePainter.drawPixmap(firstPixmap.width() + 4, 0, secondPixmap)
imagePainter.end()
return bigPixmap
def generate_image_cache(current_page):
logger.info('(Re)building image cache')
current_page_index = all_pages.index(current_page)
# Image caching for single and double page views
page_indices = (-1, 0, 1, 2)
index_modifier = 0
if double_page_mode:
index_modifier = 1
for i in page_indices:
try:
this_page = all_pages[current_page_index + i + index_modifier]
this_pixmap = load_page(this_page)
self.image_cache[i + 1] = (this_page, this_pixmap)
except IndexError:
self.image_cache[i + 1] = None
def refill_cache(remove_value):
# Do NOT put a parent in here or the mother of all
# memory leaks will result
self.thread = BackGroundCacheRefill(
self.image_cache, remove_value,
self.filetype, self.book, all_pages)
self.thread.finished.connect(overwrite_cache)
self.thread.start()
def overwrite_cache():
self.image_cache = self.thread.image_cache
def check_cache(current_page):
for i in self.image_cache:
if i:
if i[0] == current_page:
return_pixmap = i[1]
refill_cache(i)
return return_pixmap
# No return happened so the image isn't in the cache
generate_image_cache(current_page)
# TODO
# Get caching working for double page view
if not double_page_mode and self.main_window.settings['caching_enabled']:
return_pixmap = None
while not return_pixmap:
return_pixmap = check_cache(current_page)
else:
return_pixmap = load_page(current_page)
if self.main_window.settings['invert_colors']:
qImg = return_pixmap.toImage()
qImg.invertPixels()
if qImg: # Will return None if conversion doesn't work
return_pixmap = QtGui.QPixmap().fromImage(qImg)
else:
logger.error('Color inversion failed: ' + current_page)
self.image_pixmap = return_pixmap
self.resizeEvent()
def resizeEvent(self, *args):
if not self.image_pixmap:
return
zoom_mode = self.main_window.comic_profile['zoom_mode']
padding = self.main_window.comic_profile['padding']
if zoom_mode == 'fitWidth':
available_width = self.viewport().width()
image_pixmap = self.image_pixmap.scaledToWidth(
available_width, QtCore.Qt.SmoothTransformation)
elif zoom_mode == 'originalSize':
image_pixmap = self.image_pixmap
new_padding = (self.viewport().width() - image_pixmap.width()) // 2
if new_padding < 0: # The image is larger than the viewport
self.main_window.comic_profile['padding'] = 0
else:
self.main_window.comic_profile['padding'] = new_padding
elif zoom_mode == 'bestFit':
available_width = self.viewport().width()
available_height = self.viewport().height()
image_pixmap = self.image_pixmap.scaled(
available_width, available_height,
QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
self.main_window.comic_profile['padding'] = (
self.viewport().width() - image_pixmap.width()) // 2
elif zoom_mode == 'manualZoom':
available_width = self.viewport().width() - 2 * padding
image_pixmap = self.image_pixmap.scaledToWidth(
available_width, QtCore.Qt.SmoothTransformation)
graphicsScene = QtWidgets.QGraphicsScene()
graphicsScene.addPixmap(image_pixmap)
self.setScene(graphicsScene)
self.show()
# This prevents a partial page scroll on first load
self.verticalScrollBar().setValue(0)
def wheelEvent(self, event):
self.common_functions.wheelEvent(event)
def keyPressEvent(self, event):
vertical = self.verticalScrollBar().value()
maximum = self.verticalScrollBar().maximum()
def scroller(increment, move_forward=True):
if move_forward:
if vertical == maximum:
self.common_functions.change_chapter(1, True)
else:
next_val = vertical + increment
if next_val >= .95 * maximum:
next_val = maximum
self.verticalScrollBar().setValue(next_val)
else:
if vertical == 0:
self.common_functions.change_chapter(-1, False)
else:
next_val = vertical - increment
if next_val <= .05 * maximum:
next_val = 0
self.verticalScrollBar().setValue(next_val)
small_increment = maximum //self.main_window.settings['small_increment']
big_increment = maximum // self.main_window.settings['large_increment']
# Scrolling
if event.key() == QtCore.Qt.Key_Up:
scroller(small_increment, False)
if event.key() == QtCore.Qt.Key_Down:
scroller(small_increment)
if event.key() == QtCore.Qt.Key_Space:
scroller(big_increment)
# Double page mode and manga mode
if event.key() in (QtCore.Qt.Key_D, QtCore.Qt.Key_M):
self.main_window.change_page_view(event.key())
# Image fit modes
view_modification_keys = (
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Minus, QtCore.Qt.Key_Equal,
QtCore.Qt.Key_B, QtCore.Qt.Key_W, QtCore.Qt.Key_O)
if event.key() in view_modification_keys:
self.main_window.modify_comic_view(event.key())
def record_position(self):
self.parent.metadata['position']['is_read'] = False
self.common_functions.update_model()
def mouseMoveEvent(self, event):
if QtWidgets.QApplication.mouseButtons() == QtCore.Qt.NoButton:
self.viewport().setCursor(QtCore.Qt.OpenHandCursor)
else:
self.viewport().setCursor(QtCore.Qt.ClosedHandCursor)
self.parent.mouse_hide_timer.start(2000)
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
def generate_graphicsview_context_menu(self, position):
contextMenu = QtWidgets.QMenu()
fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQGraphicsView', 'Exit fullscreen'))
elif not self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQGraphicsView', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
saveAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('filesaveas'),
self._translate('PliantQGraphicsView', 'Save page as...'))
view_submenu_string = self._translate('PliantQGraphicsView', 'View')
viewSubMenu = contextMenu.addMenu(view_submenu_string)
viewSubMenu.setIcon(
self.main_window.QImageFactory.get_image('mail-thread-watch'))
doublePageAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('page-double'),
self._translate('PliantQGraphicsView', 'Double page mode (D)'))
doublePageAction.setCheckable(True)
doublePageAction.setChecked(
self.main_window.bookToolBar.doublePageButton.isChecked())
mangaModeAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('manga-mode'),
self._translate('PliantQGraphicsView', 'Manga mode (M)'))
mangaModeAction.setCheckable(True)
mangaModeAction.setChecked(
self.main_window.bookToolBar.mangaModeButton.isChecked())
viewSubMenu.addSeparator()
zoominAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-in'),
self._translate('PliantQGraphicsView', 'Zoom in (+)'))
zoomoutAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-out'),
self._translate('PliantQGraphicsView', 'Zoom out (-)'))
fitWidthAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-fit-width'),
self._translate('PliantQGraphicsView', 'Fit width (W)'))
bestFitAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-fit-best'),
self._translate('PliantQGraphicsView', 'Best fit (B)'))
originalSizeAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-original'),
self._translate('PliantQGraphicsView', 'Original size (O)'))
bookmarksToggleAction = 'Latin quote 2. Electric Boogaloo.'
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
bookmarksToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('bookmarks'),
self._translate('PliantQGraphicsView', 'Bookmarks'))
self.common_functions.generate_combo_box_action(contextMenu)
action = contextMenu.exec_(self.sender().mapToGlobal(position))
if action == doublePageAction:
self.main_window.bookToolBar.doublePageButton.trigger()
if action == mangaModeAction:
self.main_window.bookToolBar.mangaModeButton.trigger()
if action == saveAction:
dialog_prompt = self._translate('Main_UI', 'Save page as...')
extension_string = self._translate('Main_UI', 'Images')
save_file = QtWidgets.QFileDialog.getSaveFileName(
self, dialog_prompt, self.main_window.settings['last_open_path'],
f'{extension_string} (*.png *.jpg *.bmp)')
if save_file:
self.image_pixmap.save(save_file[0])
if action == bookmarksToggleAction:
self.parent.toggle_side_dock(1)
if action == dfToggleAction:
self.main_window.toggle_distraction_free()
if action == fsToggleAction:
self.parent.exit_fullscreen()
view_action_dict = {
zoominAction: QtCore.Qt.Key_Plus,
zoomoutAction: QtCore.Qt.Key_Minus,
fitWidthAction: QtCore.Qt.Key_W,
bestFitAction: QtCore.Qt.Key_B,
originalSizeAction: QtCore.Qt.Key_O}
if action in view_action_dict:
self.main_window.modify_comic_view(view_action_dict[action])
def closeEvent(self, *args):
# In case the program is closed when a contentView is fullscreened
self.main_window.closeEvent()
def toggle_annotation_mode(self):
# The graphics view doesn't currently have annotation functionality
# Don't delete this because it's still called
pass
class PliantQTextBrowser(QtWidgets.QTextBrowser):
def __init__(self, main_window, parent=None):
super(PliantQTextBrowser, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
self.parent = parent
self.main_window = main_window
self.annotation_mode = False
self.annotator = AnnotationPlacement()
self.current_annotation = None
self.annotation_dict = self.parent.metadata['annotations']
self.common_functions = PliantWidgetsCommonFunctions(
self, self.main_window)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(
self.generate_textbrowser_context_menu)
self.setMouseTracking(True)
self.verticalScrollBar().sliderMoved.connect(
self.record_position)
self.ignore_wheel_event = False
self.ignore_wheel_event_number = 0
self.at_end = False
def wheelEvent(self, event):
self.record_position()
self.common_functions.wheelEvent(event)
def keyPressEvent(self, event):
QtWidgets.QTextEdit.keyPressEvent(self, event)
if event.key() == QtCore.Qt.Key_Space:
if self.verticalScrollBar().value() == self.verticalScrollBar().maximum():
if self.at_end: # This makes sure the last lines of the chapter don't get skipped
self.common_functions.change_chapter(1, True)
self.at_end = True
else:
self.at_end = False
self.set_top_line_cleanly()
self.record_position()
def set_top_line_cleanly(self):
# Find the cursor position of the top line and move to it
find_cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
find_cursor.movePosition(
find_cursor.position(), QtGui.QTextCursor.KeepAnchor)
self.setTextCursor(find_cursor)
self.ensureCursorVisible()
def record_position(self, return_as_bookmark=False):
self.parent.metadata['position']['is_read'] = False
# The y coordinate is set to 10 because 0 tends to make
# cursor position a little finicky
cursor = self.cursorForPosition(QtCore.QPoint(0, 10))
cursor_position = cursor.position()
# Current block for progress measurement
current_block = cursor.block().blockNumber()
current_chapter = self.parent.metadata['position']['current_chapter']
blocks_per_chapter = self.parent.metadata['position']['blocks_per_chapter']
block_sum = sum(blocks_per_chapter[:(current_chapter - 1)])
block_sum += current_block
# This 'current_block' refers to the number of
# blocks in the book upto this one
self.parent.metadata['position']['current_block'] = block_sum
self.common_functions.update_model()
if return_as_bookmark:
return (self.parent.metadata['position']['current_chapter'],
cursor_position)
else:
self.parent.metadata['position']['cursor_position'] = cursor_position
def toggle_annotation_mode(self):
if self.annotation_mode:
self.annotation_mode = False
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
self.parent.sideDock.show()
self.parent.sideDock.setWindowOpacity(.95)
self.current_annotation = None
self.parent.sideDock.annotations.annotationListView.clearSelection()
else:
self.annotation_mode = True
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
self.parent.sideDock.hide()
selected_index = self.parent.sideDock.annotations.annotationListView.currentIndex()
self.current_annotation = self.parent.sideDock.annotationModel.data(
selected_index, QtCore.Qt.UserRole)
logger.info('Selected annotation: ' + self.current_annotation['name'])
def mouseReleaseEvent(self, event):
# This takes care of annotation placement
# and addition to the list that holds all current annotations
if not self.current_annotation:
QtWidgets.QTextBrowser.mouseReleaseEvent(self, event)
return
self.place_annotation(self.current_annotation)
self.toggle_annotation_mode()
def place_annotation(self, annotation):
current_chapter = self.parent.metadata['position']['current_chapter']
cursor = self.textCursor()
cursor_start = cursor.selectionStart()
cursor_end = cursor.selectionEnd()
annotation_type = 'text_markup'
applicable_to = 'text'
annotation_components = annotation['components']
self.annotator.set_current_annotation(
annotation_type, annotation_components)
new_cursor = self.annotator.format_text(
cursor, cursor_start, cursor_end)
self.setTextCursor(new_cursor)
# TODO
# Maybe use annotation name for a consolidated annotation list
this_annotation = {
'name': annotation['name'],
'applicable_to': applicable_to,
'type': annotation_type,
'cursor': (cursor_start, cursor_end),
'components': annotation_components,
'note': None}
try:
self.annotation_dict[current_chapter].append(this_annotation)
except KeyError:
self.annotation_dict[current_chapter] = []
self.annotation_dict[current_chapter].append(this_annotation)
def generate_textbrowser_context_menu(self, position):
selection = self.textCursor().selection()
selection = selection.toPlainText()
current_chapter = self.parent.metadata['position']['current_chapter']
cursor_at_mouse = self.cursorForPosition(position)
annotation_is_present = self.common_functions.annotation_specific(
'check', 'text', current_chapter, cursor_at_mouse.position())
contextMenu = QtWidgets.QMenu()
# The following cannot be None because a click
# outside the menu means that the action variable is None.
defineAction = fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
searchWikipediaAction = searchYoutubeAction = 'Does anyone know something funny in Latin?'
searchAction = searchGoogleAction = bookmarksToggleAction = 'TODO Insert Latin Joke'
deleteAnnotationAction = editAnnotationNoteAction = 'Latin quote 2. Electric Boogaloo.'
annotationActions = []
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQTextBrowser', 'Exit fullscreen'))
elif not self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQTextBrowser', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
if selection and selection != '':
first_selected_word = selection.split()[0]
elided_selection = selection
if len(elided_selection) > 15:
elided_selection = elided_selection[:15] + '...'
define_string = self._translate('PliantQTextBrowser', 'Define')
defineAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-readermode'),
f'{define_string} "{first_selected_word}"')
search_submenu_string = self._translate('PliantQTextBrowser', 'Search for')
searchSubMenu = contextMenu.addMenu(
search_submenu_string + f' "{elided_selection}"')
searchSubMenu.setIcon(self.main_window.QImageFactory.get_image('search'))
searchAction = searchSubMenu.addAction(
self.main_window.QImageFactory.get_image('search'),
self._translate('PliantQTextBrowser', 'In this book'))
searchSubMenu.addSeparator()
searchGoogleAction = searchSubMenu.addAction(
QtGui.QIcon(':/images/Google.png'),
'Google')
searchWikipediaAction = searchSubMenu.addAction(
QtGui.QIcon(':/images/Wikipedia.png'),
'Wikipedia')
searchYoutubeAction = searchSubMenu.addAction(
QtGui.QIcon(':/images/Youtube.png'),
'Youtube')
# Allow adding new annotation from the context menu
if not annotation_is_present:
annotation_string = self._translate('PliantQTextBrowser', 'Annotate')
annotationSubmenu = contextMenu.addMenu(annotation_string)
annotationSubmenu.setIcon(
self.main_window.QImageFactory.get_image('annotate'))
saved_annotations = self.parent.main_window.settings['annotations']
if not saved_annotations:
nope = annotationSubmenu.addAction('<No annotations set>')
nope.setEnabled(False)
for i in saved_annotations:
this_action = QtWidgets.QAction(i['name'])
# Does not require / support a role
this_action.setData(i)
annotationActions.append(this_action)
annotationSubmenu.addAction(this_action)
else:
searchAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('search'),
self._translate('PliantQTextBrowser', 'Search'))
if annotation_is_present:
annotationsubMenu = contextMenu.addMenu('Annotation')
annotationsubMenu.setIcon(self.main_window.QImageFactory.get_image('annotate'))
editAnnotationNoteAction = annotationsubMenu.addAction(
self.main_window.QImageFactory.get_image('edit-rename'),
self._translate('PliantQTextBrowser', 'Edit note'))
deleteAnnotationAction = annotationsubMenu.addAction(
self.main_window.QImageFactory.get_image('remove'),
self._translate('PliantQTextBrowser', 'Delete annotation'))
add_bookmark_string = self._translate('PliantQTextBrowser', 'Add Bookmark')
addBookMarkAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('bookmark-new'),
add_bookmark_string)
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
bookmarksToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('bookmarks'),
self._translate('PliantQTextBrowser', 'Bookmarks'))
self.common_functions.generate_combo_box_action(contextMenu)
action = contextMenu.exec_(self.sender().mapToGlobal(position))
if action == addBookMarkAction:
self.parent.sideDock.bookmarks.add_bookmark(cursor_at_mouse.position())
if action == defineAction:
self.main_window.definitionDialog.find_definition(selection)
if action == searchAction:
if selection and selection != '':
self.parent.sideDock.search.searchLineEdit.setText(selection)
self.parent.toggle_side_dock(2, True)
if action == searchGoogleAction:
webbrowser.open_new_tab(
f'https://www.google.com/search?q={selection}')
if action == searchWikipediaAction:
webbrowser.open_new_tab(
f'https://en.wikipedia.org/wiki/Special:Search?search={selection}')
if action == searchYoutubeAction:
webbrowser.open_new_tab(
f'https://www.youtube.com/results?search_query={selection}')
if action in annotationActions:
self.place_annotation(action.data())
if action == editAnnotationNoteAction:
self.common_functions.annotation_specific(
'note', 'text', current_chapter, cursor_at_mouse.position())
if action == deleteAnnotationAction:
self.common_functions.annotation_specific(
'delete', 'text', current_chapter, cursor_at_mouse.position())
if action == bookmarksToggleAction:
self.parent.toggle_side_dock(0)
if action == fsToggleAction:
self.parent.exit_fullscreen()
if action == dfToggleAction:
self.main_window.toggle_distraction_free()
def closeEvent(self, *args):
self.main_window.closeEvent()
def mouseMoveEvent(self, event):
if self.annotation_mode:
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
else:
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
self.parent.mouse_hide_timer.start(2000)
QtWidgets.QTextBrowser.mouseMoveEvent(self, event)
class PliantWidgetsCommonFunctions:
def __init__(self, parent_widget, main_window):
self.pw = parent_widget
self.main_window = main_window
self.are_we_doing_images_only = self.pw.parent.are_we_doing_images_only
def wheelEvent(self, event):
ignore_events = 20
if self.are_we_doing_images_only:
ignore_events = 10
if self.pw.ignore_wheel_event:
self.pw.ignore_wheel_event_number += 1
if self.pw.ignore_wheel_event_number > ignore_events:
self.pw.ignore_wheel_event = False
self.pw.ignore_wheel_event_number = 0
return
if self.are_we_doing_images_only:
QtWidgets.QGraphicsView.wheelEvent(self.pw, event)
else:
QtWidgets.QTextBrowser.wheelEvent(self.pw, event)
# Since this is a delta on a mouse move event, it cannot ever be 0
vertical_pdelta = event.pixelDelta().y()
if vertical_pdelta > 0:
moving_up = True
elif vertical_pdelta < 0:
moving_up = False
if abs(vertical_pdelta) > 80: # Adjust sensitivity here
# Implies that no scrollbar movement is possible
if self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum() == 0:
if moving_up:
self.change_chapter(-1)
else:
self.change_chapter(1)
# Implies that the scrollbar is at the bottom
elif self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum():
if not moving_up:
self.change_chapter(1)
# Implies scrollbar is at the top
elif self.pw.verticalScrollBar().value() == 0:
if moving_up:
self.change_chapter(-1)
def change_chapter(self, direction, was_button_pressed=None):
current_tab = self.pw.parent
current_position = current_tab.metadata['position']['current_chapter']
final_position = len(current_tab.metadata['content'])
# Prevent scrolling below page 1
if current_position == 1 and direction == -1:
return
# Prevent scrolling beyond last page
if (current_position == final_position) and direction == 1:
return
# Special cases for double page view
# Page limits are taken care of by the set_content method
def get_modifier():
if (not self.main_window.settings['double_page_mode']
or not self.are_we_doing_images_only):
return 0
if (current_position == 0 or current_position % 2 == 0):
return 0
if current_position % 2 == 1:
return direction
current_tab.set_content(
current_position + direction + get_modifier(), True, True)
# Set page position depending on if the chapter number is increasing or decreasing
if direction == 1 or was_button_pressed:
self.pw.verticalScrollBar().setValue(0)
else:
self.pw.verticalScrollBar().setValue(
self.pw.verticalScrollBar().maximum())
if not was_button_pressed:
self.pw.ignore_wheel_event = True
def load_annotations(self, chapter):
try:
chapter_annotations = self.pw.annotation_dict[chapter]
except KeyError:
return
for i in chapter_annotations:
applicable_to = i['applicable_to']
annotation_type = i['type']
annotation_components = i['components']
if not self.are_we_doing_images_only and applicable_to == 'text':
cursor = self.pw.textCursor()
cursor_start = i['cursor'][0]
cursor_end = i['cursor'][1]
self.pw.annotator.set_current_annotation(
annotation_type, annotation_components)
new_cursor = self.pw.annotator.format_text(
cursor, cursor_start, cursor_end)
self.pw.setTextCursor(new_cursor)
def clear_annotations(self):
if not self.are_we_doing_images_only:
cursor = self.pw.textCursor()
cursor.setPosition(0)
cursor.movePosition(
QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
previewCharFormat = QtGui.QTextCharFormat()
previewCharFormat.setFontStyleStrategy(
QtGui.QFont.PreferAntialias)
cursor.setCharFormat(previewCharFormat)
cursor.clearSelection()
self.pw.setTextCursor(cursor)
def annotation_specific(self, mode, annotation_type, chapter, cursor_position):
try:
chapter_annotations = self.pw.annotation_dict[chapter]
except KeyError:
return False
for i in chapter_annotations:
if annotation_type == 'text':
cursor_start = i['cursor'][0]
cursor_end = i['cursor'][1]
if cursor_start <= cursor_position <= cursor_end:
if mode == 'check':
return True
if mode == 'delete':
self.pw.annotation_dict[chapter].remove(i)
if mode == 'note':
note = i['note']
self.pw.parent.annotationNoteDock.set_annotation(i)
self.pw.parent.annotationNoteEdit.setText(note)
self.pw.parent.annotationNoteDock.show()
# Post iteration
if mode == 'check':
return False
if mode == 'delete':
scroll_position = self.pw.verticalScrollBar().value()
self.clear_annotations()
self.load_annotations(chapter)
self.pw.verticalScrollBar().setValue(scroll_position)
def update_model(self):
# We're updating the underlying model to have real-time
# updates on the read status
# Set a baseline model index in case the item gets deleted
# E.g It's open in a tab and deleted from the library
model_index = None
start_index = self.main_window.lib_ref.libraryModel.index(0, 0)
# Find index of the model item that corresponds to the tab
model_index = self.main_window.lib_ref.libraryModel.match(
start_index,
QtCore.Qt.UserRole + 6,
self.pw.parent.metadata['hash'],
1, QtCore.Qt.MatchExactly)
if self.are_we_doing_images_only:
position_percentage = (
self.pw.parent.metadata['position']['current_chapter'] /
self.pw.parent.metadata['position']['total_chapters'])
else:
position_percentage = (
self.pw.parent.metadata['position']['current_block'] /
self.pw.parent.metadata['position']['total_blocks'])
# Update position percentage
if model_index:
self.main_window.lib_ref.libraryModel.setData(
model_index[0], position_percentage, QtCore.Qt.UserRole + 7)
def generate_combo_box_action(self, contextMenu):
contextMenu.addSeparator()
def set_toc_position(tocTree):
currentIndex = tocTree.currentIndex()
required_position = currentIndex.data(QtCore.Qt.UserRole)
self.pw.parent.set_content(required_position, True, True)
# Create the Combobox / Treeview combination
tocComboBox = QtWidgets.QComboBox()
tocTree = QtWidgets.QTreeView()
tocComboBox.setView(tocTree)
tocComboBox.setModel(self.pw.parent.tocModel)
tocTree.setRootIsDecorated(False)
tocTree.setItemsExpandable(False)
tocTree.expandAll()
# Set the position of the QComboBox
self.pw.parent.set_tocBox_index(None, tocComboBox)
# Make clicking do something
tocComboBox.currentIndexChanged.connect(
lambda: set_toc_position(tocTree))
comboboxAction = QtWidgets.QWidgetAction(self.pw)
comboboxAction.setDefaultWidget(tocComboBox)
contextMenu.addAction(comboboxAction)

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
@@ -19,12 +17,15 @@
import os
import pickle
import sqlite3
import logging
from PyQt5 import QtCore
logger = logging.getLogger(__name__)
class DatabaseInit:
def __init__(self, location_prefix):
os.makedirs(location_prefix, exist_ok=True)
self.database_path = os.path.join(location_prefix, 'Lector.db')
self.books_table_columns = {
@@ -41,7 +42,8 @@ class DatabaseInit:
'LastAccessed': 'BLOB',
'Bookmarks': 'BLOB',
'CoverImage': 'BLOB',
'Addition': 'TEXT'}
'Addition': 'TEXT',
'Annotations': 'BLOB'}
self.directories_table_columns = {
'id': 'INTEGER PRIMARY KEY',
@@ -51,7 +53,7 @@ class DatabaseInit:
'CheckState': 'INTEGER'}
if os.path.exists(self.database_path):
self.check_database()
self.check_columns()
else:
self.create_database()
@@ -70,7 +72,7 @@ class DatabaseInit:
self.database.commit()
self.database.close()
def check_database(self):
def check_columns(self):
self.database = sqlite3.connect(self.database_path)
database_return = self.database.execute("PRAGMA table_info(books)").fetchall()
@@ -81,13 +83,13 @@ class DatabaseInit:
for i in self.books_table_columns.items():
if i[0] not in database_columns:
commit_required = True
print(f'Database: Adding column {i[0]}')
info_string = f'Database: Adding column "{i[0]}"'
logger.info(info_string)
sql_command = f"ALTER TABLE books ADD COLUMN {i[0]} {i[1]}"
self.database.execute(sql_command)
if commit_required:
self.database.commit()
self.database.close()
class DatabaseFunctions:
@@ -208,8 +210,9 @@ class DatabaseFunctions:
else:
return None
except (KeyError, sqlite3.OperationalError):
print('SQLite is in wretched rebellion @ data fetching handling')
except Exception as e:
error_string = 'SQLite is in wretched rebellion @ data fetching handling'
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
def fetch_covers_only(self, hash_list):
parameter_marks = ','.join(['?' for i in hash_list])
@@ -220,7 +223,7 @@ class DatabaseFunctions:
def modify_metadata(self, metadata_dict, book_hash):
def generate_binary(column, data):
if column in ('Position', 'LastAccessed', 'Bookmarks'):
if column in ('Position', 'LastAccessed', 'Bookmarks', 'Annotations'):
return sqlite3.Binary(pickle.dumps(data))
elif column == 'CoverImage':
return sqlite3.Binary(data)
@@ -241,8 +244,9 @@ class DatabaseFunctions:
try:
self.database.execute(
sql_command, update_data)
except sqlite3.OperationalError:
print('SQLite is in wretched rebellion @ metadata handling')
except sqlite3.OperationalError as e:
error_string = 'SQLite is in wretched rebellion @ metadata handling'
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
self.database.commit()
self.database.close()

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,8 +14,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia
import json
import logging
import urllib.request
from PyQt5 import QtWidgets, QtCore, QtGui
logger = logging.getLogger(__name__)
try:
from PyQt5 import QtMultimedia
multimedia_available = True
except ImportError:
error_string = 'QtMultimedia not found. Sounds will not play.'
logger.error(error_string)
multimedia_available = False
from lector.resources import definitions
@@ -37,8 +49,12 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
radius = 15
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
try:
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
except TypeError: # Required for older versions of Qt
pass
self.definitionView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
@@ -51,25 +67,35 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
self.pronunciation_mp3 = None
self.okButton.clicked.connect(self.hide)
self.pronounceButton.clicked.connect(self.play_pronunciation)
self.dialogBackground.clicked.connect(self.color_background)
if multimedia_available:
self.pronounceButton.clicked.connect(self.play_pronunciation)
else:
self.pronounceButton.setEnabled(False)
def api_call(self, url, word):
language = self.parent.settings['dictionary_language']
url = url + language + '/' + word.lower()
r = requests.get(
url,
headers={'app_id': self.app_id, 'app_key': self.app_key})
req = urllib.request.Request(url)
req.add_header('app_id', self.app_id)
req.add_header('app_key', self.app_key)
if r.status_code != 200:
print('A firm nope on the dictionary finding thing')
try:
response = urllib.request.urlopen(req)
if response.getcode() == 200:
return_json = json.loads(response.read())
return return_json
except Exception as e:
this_error = f'API access error'
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
self.parent.display_error_notification(None)
return None
return r.json()
def find_definition(self, word):
word_root_json = self.api_call(self.root_url, word)
if not word_root_json:
logger.error('Word root json noped out: ' + word)
self.set_text(word, None, None, True)
return
@@ -78,6 +104,8 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
definition_json = self.api_call(self.define_url, word_root)
if not definition_json:
logger.error('Definition json noped out: ' + word_root)
self.set_text(word, None, None, True)
return
definitions = {}
@@ -95,7 +123,7 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
this_definition = j['definitions'][0].capitalize()
except KeyError:
# The API also reports crossReferenceMarkers here
pass
this_definition = '<Not found>'
try:
definitions[category].add(this_definition)
@@ -137,18 +165,30 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
background = self.parent.settings['dialog_background']
else:
self.previous_position = self.pos()
background = self.parent.get_color()
self.parent.get_color()
background = self.parent.settings['dialog_background']
# Calculate inverse color for the background so that
# the text doesn't look blank
r, g, b, alpha = background.getRgb()
inv_average = 255 - (r + g + b) // 3
if 100 < inv_average < 150:
inv_average = 255
foreground = QtGui.QColor(
inv_average, inv_average, inv_average, alpha)
self.setStyleSheet(
"QDialog {{background-color: {0}}}".format(background.name()))
self.definitionView.setStyleSheet(
"QTextBrowser {{background-color: {0}}}".format(background.name()))
"QTextBrowser {{color:{0}; background-color: {1}}}".format(
foreground.name(), background.name()))
if not set_initial:
self.show()
def play_pronunciation(self):
if not self.pronunciation_mp3:
if not self.pronunciation_mp3 or not multimedia_available:
return
media_content = QtMultimedia.QMediaContent(

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,9 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtGui, QtCore
from lector.resources import pie_chart
logger = logging.getLogger(__name__)
class LibraryDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, temp_dir, parent=None):
@@ -34,11 +37,7 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
option = option.__class__(option)
file_exists = index.data(QtCore.Qt.UserRole + 5)
metadata = index.data(QtCore.Qt.UserRole + 3)
position = metadata['position']
if position:
is_read = position['is_read']
position_percent = index.data(QtCore.Qt.UserRole + 7)
# The shadow pixmap currently is set to 420 x 600
# Only draw the cover shadow in case the setting is enabled
@@ -55,55 +54,20 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
if not file_exists:
painter.setOpacity(.7)
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
read_icon = pie_chart.pixmapper(-1, None, None, 36)
painter.setOpacity(1)
read_icon = pie_chart.pixmapper(
-1, None, self.parent.settings['consider_read_at'], 36)
x_draw = option.rect.bottomRight().x() - 30
y_draw = option.rect.bottomRight().y() - 35
painter.drawPixmap(x_draw, y_draw, read_icon)
painter.setOpacity(1)
return
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
if position:
if is_read:
current_chapter = total_chapters = 100
else:
try:
current_chapter = position['current_chapter']
total_chapters = position['total_chapters']
except KeyError:
return
if position_percent:
read_icon = pie_chart.pixmapper(
current_chapter, total_chapters, self.temp_dir, 36)
position_percent, self.temp_dir, self.parent.settings['consider_read_at'], 36)
x_draw = option.rect.bottomRight().x() - 30
y_draw = option.rect.bottomRight().y() - 35
if current_chapter != 1:
painter.drawPixmap(x_draw, y_draw, read_icon)
class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(BookmarkDelegate, self).__init__(parent)
self.parent = parent
def sizeHint(self, *args):
dockwidget_width = self.parent.width() - 20
return QtCore.QSize(dockwidget_width, 50)
def paint(self, painter, option, index):
# TODO
# Alignment of the painted item
option = option.__class__(option)
chapter_index = index.data(QtCore.Qt.UserRole)
chapter_name = self.parent.window().bookToolBar.tocBox.itemText(chapter_index - 1)
if len(chapter_name) > 25:
chapter_name = chapter_name[:25] + '...'
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
painter.drawText(
option.rect,
QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight | QtCore.Qt.TextWordWrap,
' ' + chapter_name)
painter.drawPixmap(x_draw, y_draw, read_icon)

520
lector/dockwidgets.py Normal file
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,324 +0,0 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import zipfile
from urllib.parse import unquote
from bs4 import BeautifulSoup
class EPUB:
def __init__(self, filename):
self.filename = filename
self.zip_file = None
self.book = {}
self.book['split_chapters'] = {}
def read_epub(self):
# This is the function that should error out in
# case the module cannot process the file
self.load_zip()
contents_path = self.get_file_path(
None, True)
if not contents_path:
return False # No (valid) opf was found so processing cannot continue
self.generate_book_metadata(contents_path)
self.parse_toc()
return True
def load_zip(self):
try:
self.zip_file = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
except (KeyError, AttributeError, zipfile.BadZipFile):
print('Cannot parse ' + self.filename)
return
def parse_xml(self, filename, parser):
try:
this_xml = self.zip_file.read(filename).decode()
except KeyError:
short_filename = os.path.basename(self.filename)
print(f'{str(filename)} not found in {short_filename}')
return
root = BeautifulSoup(this_xml, parser)
return root
def get_file_path(self, filename, is_content_file=False):
# Use this to get the location of the content.opf file
# And maybe some other file that has a more well formatted
# We're going to all this trouble because there really is
# no going forward without a toc
if is_content_file:
container_location = self.get_file_path('container.xml')
xml = self.parse_xml(container_location, 'xml')
if xml:
root_item = xml.find('rootfile')
try:
return root_item.get('full-path')
except AttributeError:
print(f'ePub module: {self.filename} has a malformed container.xml')
return None
possible_filenames = ('content.opf', 'package.opf')
for i in possible_filenames:
presumptive_location = self.get_file_path(i)
if presumptive_location:
return presumptive_location
for i in self.zip_file.filelist:
if os.path.basename(i.filename) == os.path.basename(filename):
return i.filename
return None
def read_from_zip(self, filename):
filename = unquote(filename)
try:
file_data = self.zip_file.read(filename)
return file_data
except KeyError:
file_path_actual = self.get_file_path(filename)
if file_path_actual:
return self.zip_file.read(file_path_actual)
else:
print('ePub module can\'t find ' + filename)
#______________________________________________________
def generate_book_metadata(self, contents_path):
self.book['title'] = os.path.splitext(
os.path.basename(self.filename))[0]
self.book['author'] = 'Unknown'
self.book['isbn'] = None
self.book['tags'] = None
self.book['cover'] = None
self.book['toc_file'] = 'toc.ncx' # Overwritten if another one exists
# Parse XML
xml = self.parse_xml(contents_path, 'xml')
# Parse metadata
item_dict = {
'title': 'title',
'author': 'creator',
'year': 'date'}
for i in item_dict.items():
item = xml.find(i[1])
if item:
self.book[i[0]] = item.text
try:
self.book['year'] = int(self.book['year'][:4])
except (TypeError, KeyError, IndexError, ValueError):
self.book['year'] = 9999
# Get identifier
identifier_items = xml.find_all('identifier')
for i in identifier_items:
scheme = i.get('scheme')
try:
if scheme.lower() == 'isbn':
self.book['isbn'] = i.text
except AttributeError:
self.book['isbn'] = None
# Tags
tag_items = xml.find_all('subject')
tag_list = [i.text for i in tag_items]
self.book['tags'] = tag_list
# Get items
self.book['content_dict'] = {}
all_items = xml.find_all('item')
for i in all_items:
media_type = i.get('media-type')
this_id = i.get('id')
if media_type == 'application/xhtml+xml' or media_type == 'text/html':
self.book['content_dict'][this_id] = i.get('href')
if media_type == 'application/x-dtbncx+xml':
self.book['toc_file'] = i.get('href')
# Cover image
if 'cover' in this_id and media_type.split('/')[0] == 'image':
cover_href = i.get('href')
try:
self.book['cover'] = self.zip_file.read(cover_href)
except KeyError:
# The cover cannot be found according to the
# path specified in the content reference
self.book['cover'] = self.zip_file.read(
self.get_file_path(cover_href))
if not self.book['cover']:
# If no cover is located the conventional way,
# we go looking for the largest image in the book
biggest_image_size = 0
biggest_image = None
for j in self.zip_file.filelist:
if os.path.splitext(j.filename)[1] in ['.jpg', '.jpeg', '.png', '.gif']:
if j.file_size > biggest_image_size:
biggest_image = j.filename
biggest_image_size = j.file_size
if biggest_image:
self.book['cover'] = self.read_from_zip(biggest_image)
else:
print('No cover found for: ' + self.filename)
# Parse spine and arrange chapter paths acquired from the opf
# according to the order IN THE SPINE
spine_items = xml.find_all('itemref')
spine_order = []
for i in spine_items:
spine_order.append(i.get('idref'))
self.book['chapters_in_order'] = []
for i in spine_order:
chapter_path = self.book['content_dict'][i]
self.book['chapters_in_order'].append(chapter_path)
def parse_toc(self):
# This has no bearing on the actual order
# We're just using this to get chapter names
self.book['navpoint_dict'] = {}
toc_file = self.book['toc_file']
if toc_file:
toc_file = self.get_file_path(toc_file)
xml = self.parse_xml(toc_file, 'xml')
if not xml:
return
navpoints = xml.find_all('navPoint')
for i in navpoints:
chapter_title = i.find('text').text
chapter_source = i.find('content').get('src')
chapter_source_file = unquote(chapter_source.split('#')[0])
if '#' in chapter_source:
try:
self.book['split_chapters'][chapter_source_file].append(
(chapter_source.split('#')[1], chapter_title))
except KeyError:
self.book['split_chapters'][chapter_source_file] = []
self.book['split_chapters'][chapter_source_file].append(
(chapter_source.split('#')[1], chapter_title))
self.book['navpoint_dict'][chapter_source_file] = chapter_title
def parse_chapters(self, temp_dir=None, split_large_xml=False):
no_title_chapter = 0
self.book['book_list'] = []
for i in self.book['chapters_in_order']:
chapter_data = self.read_from_zip(i).decode()
if i in self.book['split_chapters'] and not split_large_xml:
split_chapters = get_split_content(
chapter_data, self.book['split_chapters'][i])
self.book['book_list'].extend(split_chapters)
elif split_large_xml:
# https://stackoverflow.com/questions/14444732/how-to-split-a-html-page-to-multiple-pages-using-python-and-beautiful-soup
markup = BeautifulSoup(chapter_data, 'xml')
chapters = []
pagebreaks = markup.find_all('pagebreak')
def next_element(elem):
while elem is not None:
elem = elem.next_sibling
if hasattr(elem, 'name'):
return elem
for pbreak in pagebreaks:
chapter = [str(pbreak)]
elem = next_element(pbreak)
while elem and elem.name != 'pagebreak':
chapter.append(str(elem))
elem = next_element(elem)
chapters.append('\n'.join(chapter))
for this_chapter in chapters:
fallback_title = str(no_title_chapter)
self.book['book_list'].append(
(fallback_title, this_chapter + ('<br/>' * 8)))
no_title_chapter += 1
else:
try:
self.book['book_list'].append(
(self.book['navpoint_dict'][i], chapter_data + ('<br/>' * 8)))
except KeyError:
fallback_title = str(no_title_chapter)
self.book['book_list'].append(
(fallback_title, chapter_data))
no_title_chapter += 1
cover_path = os.path.join(temp_dir, os.path.basename(self.filename)) + '- cover'
if self.book['cover']:
with open(cover_path, 'wb') as cover_temp:
cover_temp.write(self.book['cover'])
try:
self.book['book_list'][0] = (
'Cover', f'<center><img src="{cover_path}" alt="Cover"></center>')
except IndexError:
pass
def get_split_content(chapter_data, split_by):
split_anchors = [i[0] for i in split_by]
chapter_titles = [i[1] for i in split_by]
return_list = []
xml = BeautifulSoup(chapter_data, 'lxml')
xml_string = xml.body.prettify()
for count, i in enumerate(split_anchors):
this_split = xml_string.split(i)
current_chapter = this_split[0]
bs_obj = BeautifulSoup(current_chapter, 'lxml')
# Since tags correspond to data following them, the first
# chunk will be ignored
# As will all empty chapters
if bs_obj.text == '\n' or bs_obj.text == '' or count == 0:
continue
bs_obj_string = str(bs_obj).replace('"&gt;', '', 1) + ('<br/>' * 8)
return_list.append(
(chapter_titles[count - 1], bs_obj_string))
xml_string = this_split[1]
bs_obj = BeautifulSoup(xml_string, 'lxml')
bs_obj_string = str(bs_obj).replace('"&gt;', '', 1) + ('<br/>' * 8)
return_list.append(
(chapter_titles[-1], bs_obj_string))
return return_list

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,9 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt5 import QtGui
import logging
from PyQt5 import QtCore, QtGui, QtWidgets
from lector import database
from lector.settings import Settings
from lector.resources import resources
logger = logging.getLogger(__name__)
class QImageFactory:
def __init__(self, parent):
@@ -30,3 +35,316 @@ class QImageFactory:
this_qicon = QtGui.QIcon(icon_path)
return this_qicon
# For nearly all cases below, code remains unchanged from its
# state in the __main__ module. References to objects have been
# made in the respective __init__ functions of the classes here
class CoverLoadingAndCulling:
def __init__(self, main_window):
self.main_window = main_window
self.lib_ref = self.main_window.lib_ref
self.listView = self.main_window.listView
self.database_path = self.main_window.database_path
def cull_covers(self, event=None):
blank_pixmap = QtGui.QPixmap()
blank_pixmap.load(':/images/blank.png') # Keep this. Removing it causes the
# listView to go blank on a resize
all_indexes = set()
for i in range(self.lib_ref.itemProxyModel.rowCount()):
all_indexes.add(self.lib_ref.itemProxyModel.index(i, 0))
y_range = list(range(0, self.listView.viewport().height(), 100))
y_range.extend((-20, self.listView.viewport().height() + 20))
x_range = range(0, self.listView.viewport().width(), 80)
visible_indexes = set()
for i in y_range:
for j in x_range:
this_index = self.listView.indexAt(QtCore.QPoint(j, i))
visible_indexes.add(this_index)
invisible_indexes = all_indexes - visible_indexes
for i in invisible_indexes:
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
this_item = self.lib_ref.libraryModel.item(model_index.row())
if this_item:
this_item.setIcon(QtGui.QIcon(blank_pixmap))
this_item.setData(False, QtCore.Qt.UserRole + 8)
hash_index_dict = {}
hash_list = []
for i in visible_indexes:
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
book_hash = self.lib_ref.libraryModel.data(
model_index, QtCore.Qt.UserRole + 6)
cover_displayed = self.lib_ref.libraryModel.data(
model_index, QtCore.Qt.UserRole + 8)
if book_hash and not cover_displayed:
hash_list.append(book_hash)
hash_index_dict[book_hash] = model_index
all_covers = database.DatabaseFunctions(
self.database_path).fetch_covers_only(hash_list)
for i in all_covers:
book_hash = i[0]
cover = i[1]
model_index = hash_index_dict[book_hash]
book_item = self.lib_ref.libraryModel.item(model_index.row())
self.cover_loader(book_item, cover)
def load_all_covers(self):
all_covers_db = database.DatabaseFunctions(
self.database_path).fetch_data(
('Hash', 'CoverImage',),
'books',
{'Hash': ''},
'LIKE')
if not all_covers_db:
return
all_covers = {
i[0]: i[1] for i in all_covers_db}
for i in range(self.lib_ref.libraryModel.rowCount()):
this_item = self.lib_ref.libraryModel.item(i, 0)
is_cover_already_displayed = this_item.data(QtCore.Qt.UserRole + 8)
if is_cover_already_displayed:
continue
book_hash = this_item.data(QtCore.Qt.UserRole + 6)
cover = all_covers[book_hash]
self.cover_loader(this_item, cover)
def cover_loader(self, item, cover):
img_pixmap = QtGui.QPixmap()
if cover:
img_pixmap.loadFromData(cover)
else:
img_pixmap.load(':/images/NotFound.png')
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
item.setIcon(QtGui.QIcon(img_pixmap))
item.setData(True, QtCore.Qt.UserRole + 8)
class ViewProfileModification:
def __init__(self, main_window):
self.main_window = main_window
self.listView = self.main_window.listView
self.settings = self.main_window.settings
self.bookToolBar = self.main_window.bookToolBar
self.comic_profile = self.main_window.comic_profile
self.tabWidget = self.main_window.tabWidget
self.alignment_dict = self.main_window.alignment_dict
def get_color(self, signal_sender):
def open_color_dialog(current_color):
color_dialog = QtWidgets.QColorDialog()
new_color = color_dialog.getColor(current_color)
if new_color.isValid(): # Returned in case cancel is pressed
return new_color
else:
return current_color
# Special cases that don't affect (comic)book display
if signal_sender == 'libraryBackground':
current_color = self.settings['listview_background']
new_color = open_color_dialog(current_color)
self.listView.setStyleSheet("QListView {{background-color: {0}}}".format(
new_color.name()))
self.settings['listview_background'] = new_color
return
if signal_sender == 'dialogBackground':
current_color = self.settings['dialog_background']
new_color = open_color_dialog(current_color)
self.settings['dialog_background'] = new_color
return
profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile = self.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
# Retain current values on opening a new dialog
if signal_sender == 'fgColor':
current_color = current_profile['foreground']
new_color = open_color_dialog(current_color)
self.bookToolBar.colorBoxFG.setStyleSheet(
'background-color: %s' % new_color.name())
current_profile['foreground'] = new_color
elif signal_sender == 'bgColor':
current_color = current_profile['background']
new_color = open_color_dialog(current_color)
self.bookToolBar.colorBoxBG.setStyleSheet(
'background-color: %s' % new_color.name())
current_profile['background'] = new_color
elif signal_sender == 'comicBGColor':
current_color = self.comic_profile['background']
new_color = open_color_dialog(current_color)
self.bookToolBar.comicBGColor.setStyleSheet(
'background-color: %s' % new_color.name())
self.comic_profile['background'] = new_color
self.bookToolBar.profileBox.setItemData(
profile_index, current_profile, QtCore.Qt.UserRole)
self.format_contentView()
def modify_font(self, signal_sender):
profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile = self.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
if signal_sender == 'fontBox':
current_profile['font'] = self.bookToolBar.fontBox.currentFont().family()
if signal_sender == 'fontSizeBox':
old_size = current_profile['font_size']
new_size = self.bookToolBar.fontSizeBox.itemText(
self.bookToolBar.fontSizeBox.currentIndex())
if new_size.isdigit():
current_profile['font_size'] = new_size
else:
current_profile['font_size'] = old_size
if signal_sender == 'lineSpacingUp' and current_profile['line_spacing'] < 200:
current_profile['line_spacing'] += 5
if signal_sender == 'lineSpacingDown' and current_profile['line_spacing'] > 90:
current_profile['line_spacing'] -= 5
if signal_sender == 'paddingUp':
current_profile['padding'] += 5
if signal_sender == 'paddingDown':
current_profile['padding'] -= 5
alignment_dict = {
'alignLeft': 'left',
'alignRight': 'right',
'alignCenter': 'center',
'alignJustify': 'justify'}
if signal_sender in alignment_dict:
current_profile['text_alignment'] = alignment_dict[signal_sender]
self.bookToolBar.profileBox.setItemData(
profile_index, current_profile, QtCore.Qt.UserRole)
self.format_contentView()
def modify_comic_view(self, signal_sender, key_pressed):
comic_profile = self.main_window.comic_profile
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
self.bookToolBar.fitWidth.setChecked(False)
self.bookToolBar.bestFit.setChecked(False)
self.bookToolBar.originalSize.setChecked(False)
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
comic_profile['zoom_mode'] = 'manualZoom'
comic_profile['padding'] += 50
# This prevents infinite zoom out
if comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
comic_profile['padding'] -= 50
if signal_sender == 'zoomIn' or key_pressed in (
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
comic_profile['zoom_mode'] = 'manualZoom'
comic_profile['padding'] -= 50
# This prevents infinite zoom in
if comic_profile['padding'] < 0:
comic_profile['padding'] = 0
if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W:
comic_profile['zoom_mode'] = 'fitWidth'
comic_profile['padding'] = 0
self.bookToolBar.fitWidth.setChecked(True)
# Padding in the following cases is decided by
# the image pixmap loaded by the widget
if signal_sender == 'bestFit' or key_pressed == QtCore.Qt.Key_B:
comic_profile['zoom_mode'] = 'bestFit'
self.bookToolBar.bestFit.setChecked(True)
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
comic_profile['zoom_mode'] = 'originalSize'
self.bookToolBar.originalSize.setChecked(True)
self.format_contentView()
def format_contentView(self):
current_tab = self.tabWidget.currentWidget()
try:
current_metadata = current_tab.metadata
except AttributeError:
return
if current_metadata['images_only']:
background = self.comic_profile['background']
zoom_mode = self.comic_profile['zoom_mode']
if zoom_mode == 'fitWidth':
self.bookToolBar.fitWidth.setChecked(True)
if zoom_mode == 'bestFit':
self.bookToolBar.bestFit.setChecked(True)
if zoom_mode == 'originalSize':
self.bookToolBar.originalSize.setChecked(True)
self.bookToolBar.comicBGColor.setStyleSheet(
'background-color: %s' % background.name())
current_tab.format_view(
None, None, None, background, None, None, None)
else:
profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile = self.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
font = current_profile['font']
foreground = current_profile['foreground']
background = current_profile['background']
padding = current_profile['padding']
font_size = current_profile['font_size']
line_spacing = current_profile['line_spacing']
text_alignment = current_profile['text_alignment']
# Change toolbar widgets to match new settings
self.bookToolBar.fontBox.blockSignals(True)
self.bookToolBar.fontSizeBox.blockSignals(True)
self.bookToolBar.fontBox.setCurrentText(font)
current_index = self.bookToolBar.fontSizeBox.findText(
str(font_size), QtCore.Qt.MatchExactly)
self.bookToolBar.fontSizeBox.setCurrentIndex(current_index)
self.bookToolBar.fontBox.blockSignals(False)
self.bookToolBar.fontSizeBox.blockSignals(False)
self.alignment_dict[current_profile['text_alignment']].setChecked(True)
self.bookToolBar.colorBoxFG.setStyleSheet(
'background-color: %s' % foreground.name())
self.bookToolBar.colorBoxBG.setStyleSheet(
'background-color: %s' % background.name())
current_tab.format_view(
font, font_size, foreground,
background, padding, line_spacing,
text_alignment)
def reset_profile(self):
current_profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile_default = Settings(self).default_profiles[current_profile_index]
self.bookToolBar.profileBox.setItemData(
current_profile_index, current_profile_default, QtCore.Qt.UserRole)
self.format_contentView()

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
@@ -18,27 +16,32 @@
import os
import pickle
import logging
import pathlib
from PyQt5 import QtGui, QtCore
from lector import database
from lector.models import TableProxyModel, ItemProxyModel
logger = logging.getLogger(__name__)
class Library:
def __init__(self, parent):
self.parent = parent
self.view_model = None
self.item_proxy_model = None
self.table_proxy_model = None
self.main_window = parent
self.libraryModel = None
self.itemProxyModel = None
self.tableProxyModel = None
self._translate = QtCore.QCoreApplication.translate
def generate_model(self, mode, parsed_books=None, is_database_ready=True):
if mode == 'build':
self.view_model = QtGui.QStandardItemModel()
self.view_model.setColumnCount(10)
self.libraryModel = QtGui.QStandardItemModel()
self.libraryModel.setColumnCount(10)
books = database.DatabaseFunctions(
self.parent.database_path).fetch_data(
self.main_window.database_path).fetch_data(
('Title', 'Author', 'Year', 'DateAdded', 'Path',
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed',
'Addition'),
@@ -47,11 +50,11 @@ class Library:
'LIKE')
if not books:
print('Database returned nothing')
logger.warning('Database returned nothing')
return
elif mode == 'addition':
# Assumes self.view_model already exists and may be extended
# Assumes self.libraryModel already exists and may be extended
# Because any additional books have already been added to the
# database using background threads
@@ -99,18 +102,22 @@ class Library:
if position:
position = pickle.loads(position)
if position['is_read']:
position_perc = 100
position_perc = 1
else:
try:
position_perc = (
position['current_chapter'] * 100 / position['total_chapters'])
except KeyError:
position_perc = None
position['current_block'] / position['total_blocks'])
except (KeyError, ZeroDivisionError):
try:
position_perc = (
position['current_chapter'] / position['total_chapters'])
except KeyError:
position_perc = None
try:
file_exists = os.path.exists(path)
except UnicodeEncodeError:
print('Error with unicode encoding in the library module')
print('Library: Unicode encoding error')
all_metadata = {
'title': title,
@@ -145,6 +152,7 @@ class Library:
item.setToolTip(tooltip_string)
# Just keep the following order. It's way too much trouble otherwise
# User roles have to be correlated to sorting order below
item.setData(title, QtCore.Qt.UserRole)
item.setData(author, QtCore.Qt.UserRole + 1)
item.setData(year, QtCore.Qt.UserRole + 2)
@@ -156,57 +164,60 @@ class Library:
item.setData(False, QtCore.Qt.UserRole + 8) # Is the cover being displayed?
item.setData(date_added, QtCore.Qt.UserRole + 9)
item.setData(last_accessed, QtCore.Qt.UserRole + 12)
item.setData(path, QtCore.Qt.UserRole + 13)
item.setIcon(QtGui.QIcon(img_pixmap))
self.view_model.appendRow(item)
self.libraryModel.appendRow(item)
# The is_database_ready boolean is required when a new thread sends
# books here for model generation.
if not self.parent.settings['perform_culling'] and is_database_ready:
self.parent.load_all_covers()
if not self.main_window.settings['perform_culling'] and is_database_ready:
self.main_window.cover_functions.load_all_covers()
def generate_proxymodels(self):
self.item_proxy_model = ItemProxyModel()
self.item_proxy_model.setSourceModel(self.view_model)
self.item_proxy_model.setSortCaseSensitivity(False)
self.itemProxyModel = ItemProxyModel()
self.itemProxyModel.setSourceModel(self.libraryModel)
self.itemProxyModel.setSortCaseSensitivity(False)
s = QtCore.QSize(160, 250) # Set icon sizing here
self.parent.listView.setIconSize(s)
self.parent.listView.setModel(self.item_proxy_model)
self.main_window.listView.setIconSize(s)
self.main_window.listView.setModel(self.itemProxyModel)
self.table_proxy_model = TableProxyModel(
self.parent.temp_dir.path(), self.parent.tableView.horizontalHeader())
self.table_proxy_model.setSourceModel(self.view_model)
self.table_proxy_model.setSortCaseSensitivity(False)
self.parent.tableView.setModel(self.table_proxy_model)
self.tableProxyModel = TableProxyModel(
self.main_window.temp_dir.path(),
self.main_window.tableView.horizontalHeader(),
self.main_window.settings['consider_read_at'])
self.tableProxyModel.setSourceModel(self.libraryModel)
self.tableProxyModel.setSortCaseSensitivity(False)
self.main_window.tableView.setModel(self.tableProxyModel)
self.update_proxymodels()
def update_proxymodels(self):
# Table proxy model
self.table_proxy_model.invalidateFilter()
self.table_proxy_model.setFilterParams(
self.parent.libraryToolBar.searchBar.text(),
self.parent.active_library_filters,
self.tableProxyModel.invalidateFilter()
self.tableProxyModel.setFilterParams(
self.main_window.libraryToolBar.searchBar.text(),
self.main_window.active_library_filters,
0) # This doesn't need to know the sorting box position
self.table_proxy_model.setFilterFixedString(
self.parent.libraryToolBar.searchBar.text())
self.tableProxyModel.setFilterFixedString(
self.main_window.libraryToolBar.searchBar.text())
# ^^^ This isn't needed, but it forces a model update every time the
# text in the line edit changes. So I guess it is needed.
self.table_proxy_model.sort_table_columns(
self.parent.tableView.horizontalHeader().sortIndicatorSection())
self.table_proxy_model.sort_table_columns()
self.tableProxyModel.sort_table_columns(
self.main_window.tableView.horizontalHeader().sortIndicatorSection())
self.tableProxyModel.sort_table_columns()
# Item proxy model
self.item_proxy_model.invalidateFilter()
self.item_proxy_model.setFilterParams(
self.parent.libraryToolBar.searchBar.text(),
self.parent.active_library_filters,
self.parent.libraryToolBar.sortingBox.currentIndex())
self.item_proxy_model.setFilterFixedString(
self.parent.libraryToolBar.searchBar.text())
self.itemProxyModel.invalidateFilter()
self.itemProxyModel.setFilterParams(
self.main_window.libraryToolBar.searchBar.text(),
self.main_window.active_library_filters,
self.main_window.libraryToolBar.sortingBox.currentIndex())
self.itemProxyModel.setFilterFixedString(
self.main_window.libraryToolBar.searchBar.text())
self.parent.statusMessage.setText(
str(self.item_proxy_model.rowCount()) +
self.main_window.statusMessage.setText(
str(self.itemProxyModel.rowCount()) +
self._translate('Library', ' books'))
# TODO
@@ -221,23 +232,25 @@ class Library:
1: 1,
2: 2,
3: 9,
4: 12}
4: 12,
5: 7}
# Sorting according to roles and the drop down in the library toolbar
self.item_proxy_model.setSortRole(
QtCore.Qt.UserRole + sort_roles[self.parent.libraryToolBar.sortingBox.currentIndex()])
self.itemProxyModel.setSortRole(
QtCore.Qt.UserRole +
sort_roles[self.main_window.libraryToolBar.sortingBox.currentIndex()])
# This can be expanded to other fields by appending to the list
sort_order = QtCore.Qt.AscendingOrder
if self.parent.libraryToolBar.sortingBox.currentIndex() in [3, 4]:
if self.main_window.libraryToolBar.sortingBox.currentIndex() in [3, 4, 5]:
sort_order = QtCore.Qt.DescendingOrder
self.item_proxy_model.sort(0, sort_order)
self.parent.start_culling_timer()
self.itemProxyModel.sort(0, sort_order)
self.main_window.start_culling_timer()
def generate_library_tags(self):
db_library_directories = database.DatabaseFunctions(
self.parent.database_path).fetch_data(
self.main_window.database_path).fetch_data(
('Path', 'Name', 'Tags'),
'directories', # This checks the directories table NOT the book one
{'Path': ''},
@@ -249,7 +262,7 @@ class Library:
else:
db_library_directories = database.DatabaseFunctions(
self.parent.database_path).fetch_data(
self.main_window.database_path).fetch_data(
('Path',),
'books', # THIS CHECKS THE BOOKS TABLE
{'Path': ''},
@@ -285,8 +298,8 @@ class Library:
# Generate tags for the QStandardItemModel
# This isn't triggered for an empty view model
for i in range(self.view_model.rowCount()):
this_item = self.view_model.item(i, 0)
for i in range(self.libraryModel.rowCount()):
this_item = self.libraryModel.item(i, 0)
all_metadata = this_item.data(QtCore.Qt.UserRole + 3)
directory_name, directory_tags = get_tags(all_metadata)
@@ -301,8 +314,8 @@ class Library:
invalid_paths = []
deletable_persistent_indexes = []
for i in range(self.view_model.rowCount()):
item = self.view_model.item(i)
for i in range(self.libraryModel.rowCount()):
item = self.libraryModel.item(i)
item_metadata = item.data(QtCore.Qt.UserRole + 3)
book_path = item_metadata['path']
@@ -310,7 +323,7 @@ class Library:
addition_mode = item_metadata['addition_mode']
except KeyError:
addition_mode = 'automatic'
print('Libary: Error setting addition mode for prune')
logger.error('Libary: Error setting addition mode for prune')
if (book_path not in valid_paths and
(addition_mode != 'manual' or addition_mode is None)):
@@ -321,8 +334,8 @@ class Library:
if deletable_persistent_indexes:
for i in deletable_persistent_indexes:
self.view_model.removeRow(i.row())
self.libraryModel.removeRow(i.row())
# Remove invalid paths from the database as well
database.DatabaseFunctions(
self.parent.database_path).delete_from_database('Path', invalid_paths)
self.main_window.database_path).delete_from_database('Path', invalid_paths)

58
lector/logger.py Normal file
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.0'
import os
import logging
from PyQt5 import QtCore
location_prefix = os.path.join(
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation),
'Lector')
logger_filename = os.path.join(location_prefix, 'Lector.log')
def init_logging(cli_arguments):
# This needs a separate 'Lector' in the os.path.join because
# application name isn't explicitly set in this module
os.makedirs(location_prefix, exist_ok=True)
log_level = 30 # Warning and above
# Set log level according to command line arguments
try:
if cli_arguments[1] == 'debug':
log_level = 10 # Debug and above
print('Debug logging enabled')
try:
os.remove(logger_filename) # Remove old log for clarity
except FileNotFoundError:
pass
except IndexError:
pass
# Create logging object
logging.basicConfig(
filename=logger_filename,
filemode='a',
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
level=log_level)
logging.addLevelName(60, 'HAMMERTIME') ## Messages that MUST be logged
return logging.getLogger('lector.main')

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,12 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore, QtGui
from lector import database
from lector.widgets import PliantQGraphicsScene
from lector.resources import metadata
logger = logging.getLogger(__name__)
class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
def __init__(self, parent):
@@ -38,8 +40,12 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
radius = 15
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
try:
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
except TypeError: # Required for older versions of Qt
pass
self.parent = parent
self.database_path = self.parent.database_path
@@ -86,7 +92,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
self.coverView.setScene(graphics_scene)
def ok_pressed(self, event=None):
book_item = self.parent.lib_ref.view_model.item(self.book_index.row())
book_item = self.parent.lib_ref.libraryModel.item(self.book_index.row())
title = self.titleLine.text()
author = self.authorLine.text()
@@ -116,7 +122,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
if self.cover_for_database:
database_dict['CoverImage'] = self.cover_for_database
self.parent.cover_loader(
self.parent.cover_functions.cover_loader(
book_item, self.cover_for_database)
self.parent.lib_ref.update_proxymodels()
@@ -148,7 +154,8 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
background = self.parent.settings['dialog_background']
else:
self.previous_position = self.pos()
background = self.parent.get_color()
self.parent.get_color()
background = self.parent.settings['dialog_background']
self.setStyleSheet(
"QDialog {{background-color: {0}}}".format(background.name()))

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,33 +14,32 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import pathlib
from PyQt5 import QtCore, QtWidgets
from lector.resources import pie_chart
logger = logging.getLogger(__name__)
class BookmarkProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(BookmarkProxyModel, self).__init__(parent)
self.parent = parent
self.parentTab = self.parent.parent
self.filter_text = None
def setFilterParams(self, filter_text):
self.filter_text = filter_text
def filterAcceptsRow(self, row, parent):
# TODO
# Connect this to the search bar
return True
def setData(self, index, value, role):
if role == QtCore.Qt.EditRole:
source_index = self.mapToSource(index)
identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2)
self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole)
self.parent.metadata['bookmarks'][identifier]['description'] = value
self.parentTab.metadata['bookmarks'][identifier]['description'] = value
return True
@@ -65,9 +62,10 @@ class ItemProxyModel(QtCore.QSortFilterProxyModel):
class TableProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, temp_dir, tableViewHeader, parent=None):
def __init__(self, temp_dir, tableViewHeader, consider_read_at, parent=None):
super(TableProxyModel, self).__init__(parent)
self.tableViewHeader = tableViewHeader
self.consider_read_at = consider_read_at
self._translate = QtCore.QCoreApplication.translate
title_string = self._translate('TableProxyModel', 'Title')
@@ -100,7 +98,9 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
try:
return self.header_data[column]
except IndexError:
print('Table proxy model: Can\'t find header for column', column)
logger.error(
'Table proxy model: Can\'t find header for column' + str(column))
# The column will be called IndexError. Not a typo.
return 'IndexError'
def flags(self, index):
@@ -121,38 +121,22 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
return_pixmap = None
file_exists = item.data(QtCore.Qt.UserRole + 5)
metadata = item.data(QtCore.Qt.UserRole + 3)
position = metadata['position']
if position:
is_read = position['is_read']
position_percent = item.data(QtCore.Qt.UserRole + 7)
if not file_exists:
return pie_chart.pixmapper(
-1, None, None, QtCore.Qt.SizeHintRole + 10)
if position:
if is_read:
current_chapter = total_chapters = 100
else:
try:
current_chapter = position['current_chapter']
total_chapters = position['total_chapters']
# TODO
# See if there's any rationale for this
if current_chapter == 1:
raise KeyError
except KeyError:
return
if position_percent:
return_pixmap = pie_chart.pixmapper(
current_chapter, total_chapters, self.temp_dir,
position_percent, self.temp_dir,
self.consider_read_at,
QtCore.Qt.SizeHintRole + 10)
return return_pixmap
elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if index.column() in (0, 5): # Cover and Status
if index.column() in (0, 5): # Cover and Status
return QtCore.QVariant()
if index.column() == 4:
@@ -214,14 +198,20 @@ class ProxyModelsCommonFunctions:
title = model.data(this_index, QtCore.Qt.UserRole)
author = model.data(this_index, QtCore.Qt.UserRole + 1)
tags = model.data(this_index, QtCore.Qt.UserRole + 4)
progress = model.data(this_index, QtCore.Qt.UserRole + 7)
directory_name = model.data(this_index, QtCore.Qt.UserRole + 10)
directory_tags = model.data(this_index, QtCore.Qt.UserRole + 11)
last_accessed = model.data(this_index, QtCore.Qt.UserRole + 12)
file_path = model.data(this_index, QtCore.Qt.UserRole + 13)
# Hide untouched files when sorting by last accessed
if self.parent_model.sorting_box_position == 4 and not last_accessed:
return False
# Hide untouched files when sorting by progress
if self.parent_model.sorting_box_position == 5 and not progress:
return False
if self.parent_model.active_library_filters:
if directory_name not in self.parent_model.active_library_filters:
return False
@@ -233,7 +223,9 @@ class ProxyModelsCommonFunctions:
else:
valid_data = [
i.lower() for i in (
title, author, tags, directory_name, directory_tags) if i is not None]
title, author, tags, directory_name,
directory_tags, file_path)
if i is not None]
for i in valid_data:
if self.parent_model.filter_text.lower() in i:
return True
@@ -351,32 +343,3 @@ class MostExcellentFileSystemModel(QtWidgets.QFileSystemModel):
for i in deletable:
del self.tag_data[i]
# TODO
# Unbork this
class FileSystemProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(FileSystemProxyModel, self).__init__(parent)
def filterAcceptsRow(self, row_num, parent):
model = self.sourceModel()
filter_out = [
'boot', 'dev', 'etc', 'lost+found', 'opt', 'pdb',
'proc', 'root', 'run', 'srv', 'sys', 'tmp', 'twonky',
'usr', 'var', 'bin', 'kdeinit5__0', 'lib', 'lib64', 'sbin']
name_index = model.index(row_num, 0)
valid_data = model.data(name_index)
print(valid_data)
return True
try:
if valid_data in filter_out:
return False
except AttributeError:
pass
return True

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,9 +19,14 @@
import os
import time
import logging
import zipfile
import collections
from lector.rarfile import rarfile
logger = logging.getLogger(__name__)
class ParseCOMIC:
def __init__(self, filename, *args):
@@ -33,47 +36,45 @@ class ParseCOMIC:
self.book_extension = os.path.splitext(self.filename)
def read_book(self):
try:
if self.book_extension[1] == '.cbz':
self.book = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
self.image_list = [i.filename for i in self.book.infolist() if not i.is_dir()]
if self.book_extension[1] == '.cbz':
self.book = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
self.image_list = [
i.filename for i in self.book.infolist()
if not i.is_dir() and is_image(i.filename)]
elif self.book_extension[1] == '.cbr':
self.book = rarfile.RarFile(self.filename)
self.image_list = [i.filename for i in self.book.infolist() if not i.isdir()]
elif self.book_extension[1] == '.cbr':
self.book = rarfile.RarFile(self.filename)
self.image_list = [
i.filename for i in self.book.infolist()
if not i.isdir() and is_image(i.filename)]
self.image_list.sort()
except: # Specifying no exception here is warranted
print('Cannot parse ' + self.filename)
return
self.image_list.sort()
def get_title(self):
def generate_metadata(self):
title = os.path.basename(self.book_extension[0]).strip(' ')
return title
author = '<Unknown>'
isbn = None
tags = []
cover = self.book.read(self.image_list[0])
def get_author(self):
return None
def get_year(self):
creation_time = time.ctime(os.path.getctime(self.filename))
creation_year = creation_time.split()[-1]
return creation_year
year = creation_time.split()[-1]
def get_cover_image(self):
# The first image in the archive may not be the cover
# It is implied, however, that the first image in order
# will be the cover
return self.book.read(self.image_list[0])
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def get_isbn(self):
return None
def generate_content(self):
image_number = len(self.image_list)
toc = [(1, f'Page {i + 1}', i + 1) for i in range(image_number)]
def get_tags(self):
return None
# Return toc, content, images_only
return toc, self.image_list, True
def get_contents(self):
file_settings = {'images_only': True}
contents = [(f'Page {count + 1}', i) for count, i in enumerate(self.image_list)]
return contents, file_settings
def is_image(filename):
valid_image_extensions = ['.png', '.jpg', '.bmp']
if os.path.splitext(filename)[1].lower() in valid_image_extensions:
return True
else:
return False

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,90 +14,87 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import os
from PyQt5 import QtCore
from bs4 import BeautifulSoup
import collections
import fitz
from PyQt5 import QtGui
proceed = True
try:
import popplerqt5
except ImportError:
print('python-poppler-qt5 is not installed. Pdf files will not work.')
proceed = False
class ParsePDF:
def __init__(self, filename, *args):
self.filename = filename
self.book = None
self.metadata = None
def read_book(self):
if not proceed:
return
self.book = fitz.open(self.filename)
self.book = popplerqt5.Poppler.Document.load(self.filename)
if not self.book:
return
def generate_metadata(self):
title = self.book.metadata['title']
if not title:
title = os.path.splitext(os.path.basename(self.filename))[0]
self.metadata = BeautifulSoup(self.book.metadata(), 'xml')
author = self.book.metadata['author']
if not author:
author = 'Unknown'
def get_title(self):
creation_date = self.book.metadata['creationDate']
try:
title = self.metadata.find('title').text
return title.replace('\n', '')
except AttributeError:
return os.path.splitext(os.path.basename(self.filename))[0]
year = creation_date.split(':')[1][:4]
except (ValueError, AttributeError):
year = 9999
def get_author(self):
try:
author = self.metadata.find('creator').text
return author.replace('\n', '')
except AttributeError:
return 'Unknown'
isbn = None
def get_year(self):
try:
year = self.metadata.find('MetadataDate').text
return year.replace('\n', '')
except AttributeError:
return 9999
tags = self.book.metadata['keywords']
if not tags:
tags = []
def get_cover_image(self):
self.book.setRenderHint(
popplerqt5.Poppler.Document.Antialiasing
and popplerqt5.Poppler.Document.TextAntialiasing)
# This is a little roundabout for the cover
# and I'm sure it's taking a performance hit
# But it is simple. So there's that.
cover_page = self.book.loadPage(0)
# Disabling scaling gets the covers much faster
cover = render_pdf_page(cover_page, True)
cover_page = self.book.page(0)
cover_image = cover_page.renderToImage(300, 300)
return resize_image(cover_image)
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def get_isbn(self):
return None
def generate_content(self):
content = list(range(self.book.pageCount))
toc = self.book.getToC()
if not toc:
toc = [(1, f'Page {i + 1}', i + 1) for i in range(self.book.pageCount)]
def get_tags(self):
try:
tags = self.metadata.find('Keywords').text
return tags.replace('\n', '')
except AttributeError:
return None
def get_contents(self):
file_settings = {'images_only': True}
contents = [(f'Page {i + 1}', i) for i in range(self.book.numPages())]
return contents, file_settings
# Return toc, content, images_only
return toc, content, True
def resize_image(cover_image):
cover_image = cover_image.scaled(
420, 600, QtCore.Qt.IgnoreAspectRatio)
def render_pdf_page(page_data, for_cover=False):
# Draw page contents on to a pixmap
# and then return that pixmap
byte_array = QtCore.QByteArray()
buffer = QtCore.QBuffer(byte_array)
buffer.open(QtCore.QIODevice.WriteOnly)
cover_image.save(buffer, 'jpg', 75)
# Render quality is set by the following
zoom_matrix = fitz.Matrix(4, 4)
if for_cover:
zoom_matrix = fitz.Matrix(1, 1)
cover_image_final = io.BytesIO(byte_array)
cover_image_final.seek(0)
return cover_image_final.getvalue()
pagePixmap = page_data.getPixmap(
matrix=zoom_matrix,
alpha=False) # Sets background to White
imageFormat = QtGui.QImage.Format_RGB888 # Set to Format_RGB888 if alpha
pageQImage = QtGui.QImage(
pagePixmap.samples,
pagePixmap.width,
pagePixmap.height,
pagePixmap.stride,
imageFormat)
# The cover page doesn't require conversion into a Pixmap
if for_cover:
return pageQImage
pixmap = QtGui.QPixmap()
pixmap.convertFromImage(pageQImage)
return pixmap

476
lector/readers/read_epub.py Normal file
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,146 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'raw/annotations.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(306, 387)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.nameEdit = QtWidgets.QLineEdit(Dialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.nameEdit.sizePolicy().hasHeightForWidth())
self.nameEdit.setSizePolicy(sizePolicy)
self.nameEdit.setObjectName("nameEdit")
self.horizontalLayout_2.addWidget(self.nameEdit)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.typeLabel = QtWidgets.QLabel(Dialog)
self.typeLabel.setObjectName("typeLabel")
self.horizontalLayout.addWidget(self.typeLabel)
self.typeBox = QtWidgets.QComboBox(Dialog)
self.typeBox.setObjectName("typeBox")
self.horizontalLayout.addWidget(self.typeBox)
self.verticalLayout.addLayout(self.horizontalLayout)
self.stackedWidget = QtWidgets.QStackedWidget(Dialog)
self.stackedWidget.setObjectName("stackedWidget")
self.page = QtWidgets.QWidget()
self.page.setObjectName("page")
self.gridLayout_2 = QtWidgets.QGridLayout(self.page)
self.gridLayout_2.setObjectName("gridLayout_2")
self.verticalLayout_12 = QtWidgets.QVBoxLayout()
self.verticalLayout_12.setObjectName("verticalLayout_12")
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
self.foregroundCheck = QtWidgets.QCheckBox(self.page)
self.foregroundCheck.setObjectName("foregroundCheck")
self.horizontalLayout_15.addWidget(self.foregroundCheck)
self.foregroundColorButton = QtWidgets.QPushButton(self.page)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.foregroundColorButton.sizePolicy().hasHeightForWidth())
self.foregroundColorButton.setSizePolicy(sizePolicy)
self.foregroundColorButton.setMinimumSize(QtCore.QSize(30, 0))
self.foregroundColorButton.setMaximumSize(QtCore.QSize(45, 40))
self.foregroundColorButton.setText("")
self.foregroundColorButton.setObjectName("foregroundColorButton")
self.horizontalLayout_15.addWidget(self.foregroundColorButton)
self.verticalLayout_12.addLayout(self.horizontalLayout_15)
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
self.highlightCheck = QtWidgets.QCheckBox(self.page)
self.highlightCheck.setObjectName("highlightCheck")
self.horizontalLayout_16.addWidget(self.highlightCheck)
self.highlightColorButton = QtWidgets.QPushButton(self.page)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.highlightColorButton.sizePolicy().hasHeightForWidth())
self.highlightColorButton.setSizePolicy(sizePolicy)
self.highlightColorButton.setMinimumSize(QtCore.QSize(30, 24))
self.highlightColorButton.setMaximumSize(QtCore.QSize(45, 40))
self.highlightColorButton.setText("")
self.highlightColorButton.setObjectName("highlightColorButton")
self.horizontalLayout_16.addWidget(self.highlightColorButton)
self.verticalLayout_12.addLayout(self.horizontalLayout_16)
self.horizontalLayout_17 = QtWidgets.QHBoxLayout()
self.horizontalLayout_17.setObjectName("horizontalLayout_17")
self.boldCheck = QtWidgets.QCheckBox(self.page)
self.boldCheck.setObjectName("boldCheck")
self.horizontalLayout_17.addWidget(self.boldCheck)
self.verticalLayout_12.addLayout(self.horizontalLayout_17)
self.horizontalLayout_18 = QtWidgets.QHBoxLayout()
self.horizontalLayout_18.setObjectName("horizontalLayout_18")
self.italicCheck = QtWidgets.QCheckBox(self.page)
self.italicCheck.setObjectName("italicCheck")
self.horizontalLayout_18.addWidget(self.italicCheck)
self.verticalLayout_12.addLayout(self.horizontalLayout_18)
self.horizontalLayout_19 = QtWidgets.QHBoxLayout()
self.horizontalLayout_19.setObjectName("horizontalLayout_19")
self.underlineCheck = QtWidgets.QCheckBox(self.page)
self.underlineCheck.setObjectName("underlineCheck")
self.horizontalLayout_19.addWidget(self.underlineCheck)
self.underlineType = QtWidgets.QComboBox(self.page)
self.underlineType.setObjectName("underlineType")
self.horizontalLayout_19.addWidget(self.underlineType)
self.underlineColorButton = QtWidgets.QPushButton(self.page)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.underlineColorButton.sizePolicy().hasHeightForWidth())
self.underlineColorButton.setSizePolicy(sizePolicy)
self.underlineColorButton.setMinimumSize(QtCore.QSize(45, 24))
self.underlineColorButton.setMaximumSize(QtCore.QSize(45, 40))
self.underlineColorButton.setText("")
self.underlineColorButton.setObjectName("underlineColorButton")
self.horizontalLayout_19.addWidget(self.underlineColorButton)
self.verticalLayout_12.addLayout(self.horizontalLayout_19)
self.gridLayout_2.addLayout(self.verticalLayout_12, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.page)
self.verticalLayout.addWidget(self.stackedWidget)
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem)
self.okButton = QtWidgets.QPushButton(Dialog)
self.okButton.setObjectName("okButton")
self.horizontalLayout_3.addWidget(self.okButton)
self.cancelButton = QtWidgets.QPushButton(Dialog)
self.cancelButton.setObjectName("cancelButton")
self.horizontalLayout_3.addWidget(self.cancelButton)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem1)
self.gridLayout.addLayout(self.horizontalLayout_3, 1, 0, 1, 1)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Annotation Editor"))
self.nameEdit.setPlaceholderText(_translate("Dialog", "Annotation Name"))
self.typeLabel.setText(_translate("Dialog", "Type"))
self.foregroundCheck.setText(_translate("Dialog", "Foreground"))
self.highlightCheck.setText(_translate("Dialog", "Highlight"))
self.boldCheck.setText(_translate("Dialog", "Bold"))
self.italicCheck.setText(_translate("Dialog", "Italic"))
self.underlineCheck.setText(_translate("Dialog", "Underline"))
self.okButton.setText(_translate("Dialog", "OK"))
self.cancelButton.setText(_translate("Dialog", "Cancel"))

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'raw/main.ui'
#
# Created by: PyQt5 UI code generator 5.9.2
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!
@@ -35,20 +35,6 @@ class Ui_MainWindow(object):
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.gridLayout_4.setSpacing(0)
self.gridLayout_4.setObjectName("gridLayout_4")
self.listView = QtWidgets.QListView(self.listPage)
self.listView.setFrameShape(QtWidgets.QFrame.NoFrame)
self.listView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.listView.setProperty("showDropIndicator", False)
self.listView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.listView.setMovement(QtWidgets.QListView.Static)
self.listView.setProperty("isWrapping", True)
self.listView.setResizeMode(QtWidgets.QListView.Fixed)
self.listView.setLayoutMode(QtWidgets.QListView.SinglePass)
self.listView.setViewMode(QtWidgets.QListView.IconMode)
self.listView.setUniformItemSizes(True)
self.listView.setWordWrap(True)
self.listView.setObjectName("listView")
self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.listPage)
self.tablePage = QtWidgets.QWidget()
self.tablePage.setObjectName("tablePage")
@@ -56,20 +42,6 @@ class Ui_MainWindow(object):
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.gridLayout_3.setSpacing(0)
self.gridLayout_3.setObjectName("gridLayout_3")
self.tableView = QtWidgets.QTableView(self.tablePage)
self.tableView.setFrameShape(QtWidgets.QFrame.Box)
self.tableView.setFrameShadow(QtWidgets.QFrame.Plain)
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.tableView.setGridStyle(QtCore.Qt.NoPen)
self.tableView.setSortingEnabled(True)
self.tableView.setWordWrap(False)
self.tableView.setObjectName("tableView")
self.tableView.horizontalHeader().setVisible(True)
self.tableView.verticalHeader().setVisible(False)
self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.tablePage)
self.gridLayout_2.addWidget(self.stackedWidget, 0, 0, 1, 1)
self.tabWidget.addTab(self.tab, "")

View File

@@ -94,26 +94,26 @@ def generate_pie(progress_percent, temp_dir=None):
return lSvg
def pixmapper(current_chapter, total_chapters, temp_dir, size):
def pixmapper(position_percent, temp_dir, consider_read_at, size):
# A current_chapter of -1 implies the files does not exist
# A chapter number == Total chapters implies the file is unread
return_pixmap = None
# position_percent and consider_read_at are expected as a <1 decimal value
if current_chapter == -1:
return_pixmap = None
consider_read_at = consider_read_at / 100
if position_percent == -1:
return_pixmap = QtGui.QIcon(':/images/error.svg').pixmap(size)
return return_pixmap
if current_chapter == total_chapters:
if position_percent >= consider_read_at: # Consider book read @ this progress
return_pixmap = QtGui.QIcon(':/images/checkmark.svg').pixmap(size)
else:
# TODO
# See if saving the svg to disk can be avoided
# Shift to lines to track progress
# Maybe make the alignment a little more uniform across emblems
progress_percent = int(current_chapter * 100 / total_chapters)
generate_pie(progress_percent, temp_dir)
generate_pie(int(position_percent * 100), temp_dir)
svg_path = os.path.join(temp_dir, 'lector_progress.svg')
return_pixmap = QtGui.QIcon(svg_path).pixmap(size - 4) ## The -4 looks more proportional

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 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

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="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 617 B

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:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

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:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

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="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 0 C 0.892 0 0 0.892 0 2 L 0 14 C 0 15.108 0.892 16 2 16 L 14 16 C 15.108 16 16 15.108 16 14 L 16 2 C 16 0.892 15.108 0 14 0 L 2 0 z M 3.7148438 2 L 12.285156 2 C 13.235156 2 14 2.7651437 14 3.7148438 L 14 12.285156 C 14 13.235156 13.235156 14 12.285156 14 L 3.7148438 14 C 2.7651438 14 2 13.235156 2 12.285156 L 2 3.7148438 C 2 2.7651438 2.7651437 2 3.7148438 2 z M 6.7402344 3 L 6.6289062 4.3164062 A 3.964 3.9286 0 0 0 5.4707031 4.9804688 L 4.2617188 4.4179688 L 3.0019531 6.5820312 L 4.0976562 7.3378906 A 3.964 3.9286 0 0 0 4.0371094 8 A 3.964 3.9286 0 0 0 4.0957031 8.6660156 L 3.0019531 9.4179688 L 4.2617188 11.582031 L 5.4667969 11.019531 A 3.964 3.9286 0 0 0 6.6289062 11.679688 L 6.7402344 13 L 9.2617188 13 L 9.3730469 11.683594 A 3.964 3.9286 0 0 0 10.53125 11.019531 L 11.740234 11.582031 L 13.001953 9.4179688 L 11.904297 8.6621094 A 3.964 3.9286 0 0 0 11.964844 8 A 3.964 3.9286 0 0 0 11.908203 7.3339844 L 13.001953 6.5820312 L 11.740234 4.4179688 L 10.535156 4.9804688 A 3.964 3.9286 0 0 0 9.3730469 4.3203125 L 9.2617188 3 L 6.7402344 3 z M 8.0019531 6.5722656 A 1.4414 1.4286 0 0 1 9.4433594 8 A 1.4414 1.4286 0 0 1 8.0019531 9.4277344 A 1.4414 1.4286 0 0 1 6.5605469 8 A 1.4414 1.4286 0 0 1 8.0019531 6.5722656 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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 8 0.99609375 C 4.134 0.99609375 1 4.1300937 1 7.9960938 C 1 11.862094 4.134 14.996094 8 14.996094 C 11.866 14.996094 15 11.862094 15 7.9960938 C 15 4.1300937 11.866 0.99609375 8 0.99609375 z M 7.5 2.9335938 C 7.5669 2.9265937 7.65125 2.9375937 7.71875 2.9335938 C 7.72675 2.9655938 7.67005 3.0794638 7.59375 3.2460938 C 7.10789 4.3074937 7.08033 5.5504437 7.53125 6.2148438 C 7.61285 6.3353038 7.6875 6.4499437 7.6875 6.4648438 C 7.6875 6.4797438 7.5995 6.4960938 7.5 6.4960938 C 7.26642 6.4960938 7.04538 6.3537238 6.59375 5.9960938 C 6.39312 5.8372237 6.1323 5.7037938 6.03125 5.6835938 C 5.87257 5.6518937 5.83028 5.6657938 5.625 5.8710938 C 5.43401 6.0620537 5.375 6.1650237 5.375 6.3398438 C 5.375 7.0027837 6.16208 7.5297437 7.625 7.8398438 C 9.6117 8.2609137 10.10145 8.6389138 10.15625 9.6835938 C 10.22505 10.993594 9.5276 11.981394 8 12.746094 C 7.81767 12.837394 7.7015 12.872844 7.625 12.902344 C 7.5911 12.899344 7.56505 12.905344 7.53125 12.902344 C 7.51825 12.861844 7.5 12.767884 7.5 12.589844 C 7.5 11.894064 7.22575 11.177844 6.8125 10.777344 C 6.70157 10.669824 6.39098 10.441994 6.125 10.277344 C 5.85903 10.112704 5.59105 9.9214438 5.53125 9.8398438 C 5.43215 9.7044337 5.42386 9.6212437 5.5 9.3710938 C 5.63876 8.9142237 5.80392 8.6597637 6.125 8.3710938 C 6.29333 8.2197537 6.46271 8.0928437 6.5 8.0898438 C 6.5373 8.0868438 6.28485 8.0110437 5.90625 7.9335938 C 5.52767 7.8559938 4.97383 7.6934738 4.6875 7.5898438 C 4.16392 7.4003938 3.457 7.0026837 3.1875 6.7148438 C 3.1761 6.7026437 3.16615 6.6943938 3.15625 6.6835938 C 3.54238 5.1454938 4.626 3.8848438 6.0625 3.2773438 C 6.36307 3.1502138 6.67292 3.0629938 7 2.9960938 C 7.16292 2.9627938 7.33178 2.9506937 7.5 2.9335938 z M 12.1875 5.2773438 C 12.30495 5.3499437 12.74841 6.3093438 12.875 6.7773438 C 13.03844 7.3815337 13.02661 8.4271437 12.875 9.0273438 C 12.8173 9.2557838 12.74335 9.4694937 12.71875 9.4960938 C 12.69415 9.5226938 12.60494 9.3695637 12.5 9.1835938 C 12.39505 8.9976538 12.05984 8.6025437 11.78125 8.3085938 C 10.97711 7.4600637 10.85066 7.0170437 11.1875 6.3398438 C 11.35737 5.9983538 12.0966 5.2212438 12.1875 5.2773438 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 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:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

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="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 617 B

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:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

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:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

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

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 2 0 C 0.892 0 0 0.892 0 2 L 0 14 C 0 15.108 0.892 16 2 16 L 14 16 C 15.108 16 16 15.108 16 14 L 16 2 C 16 0.892 15.108 0 14 0 L 2 0 z M 3.7148438 2 L 12.285156 2 C 13.235156 2 14 2.7651437 14 3.7148438 L 14 12.285156 C 14 13.235156 13.235156 14 12.285156 14 L 3.7148438 14 C 2.7651438 14 2 13.235156 2 12.285156 L 2 3.7148438 C 2 2.7651438 2.7651437 2 3.7148438 2 z M 6.7402344 3 L 6.6289062 4.3164062 A 3.964 3.9286 0 0 0 5.4707031 4.9804688 L 4.2617188 4.4179688 L 3.0019531 6.5820312 L 4.0976562 7.3378906 A 3.964 3.9286 0 0 0 4.0371094 8 A 3.964 3.9286 0 0 0 4.0957031 8.6660156 L 3.0019531 9.4179688 L 4.2617188 11.582031 L 5.4667969 11.019531 A 3.964 3.9286 0 0 0 6.6289062 11.679688 L 6.7402344 13 L 9.2617188 13 L 9.3730469 11.683594 A 3.964 3.9286 0 0 0 10.53125 11.019531 L 11.740234 11.582031 L 13.001953 9.4179688 L 11.904297 8.6621094 A 3.964 3.9286 0 0 0 11.964844 8 A 3.964 3.9286 0 0 0 11.908203 7.3339844 L 13.001953 6.5820312 L 11.740234 4.4179688 L 10.535156 4.9804688 A 3.964 3.9286 0 0 0 9.3730469 4.3203125 L 9.2617188 3 L 6.7402344 3 z M 8.0019531 6.5722656 A 1.4414 1.4286 0 0 1 9.4433594 8 A 1.4414 1.4286 0 0 1 8.0019531 9.4277344 A 1.4414 1.4286 0 0 1 6.5605469 8 A 1.4414 1.4286 0 0 1 8.0019531 6.5722656 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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 8 0.99609375 C 4.134 0.99609375 1 4.1300937 1 7.9960938 C 1 11.862094 4.134 14.996094 8 14.996094 C 11.866 14.996094 15 11.862094 15 7.9960938 C 15 4.1300937 11.866 0.99609375 8 0.99609375 z M 7.5 2.9335938 C 7.5669 2.9265937 7.65125 2.9375937 7.71875 2.9335938 C 7.72675 2.9655938 7.67005 3.0794638 7.59375 3.2460938 C 7.10789 4.3074937 7.08033 5.5504437 7.53125 6.2148438 C 7.61285 6.3353038 7.6875 6.4499437 7.6875 6.4648438 C 7.6875 6.4797438 7.5995 6.4960938 7.5 6.4960938 C 7.26642 6.4960938 7.04538 6.3537238 6.59375 5.9960938 C 6.39312 5.8372237 6.1323 5.7037938 6.03125 5.6835938 C 5.87257 5.6518937 5.83028 5.6657938 5.625 5.8710938 C 5.43401 6.0620537 5.375 6.1650237 5.375 6.3398438 C 5.375 7.0027837 6.16208 7.5297437 7.625 7.8398438 C 9.6117 8.2609137 10.10145 8.6389138 10.15625 9.6835938 C 10.22505 10.993594 9.5276 11.981394 8 12.746094 C 7.81767 12.837394 7.7015 12.872844 7.625 12.902344 C 7.5911 12.899344 7.56505 12.905344 7.53125 12.902344 C 7.51825 12.861844 7.5 12.767884 7.5 12.589844 C 7.5 11.894064 7.22575 11.177844 6.8125 10.777344 C 6.70157 10.669824 6.39098 10.441994 6.125 10.277344 C 5.85903 10.112704 5.59105 9.9214438 5.53125 9.8398438 C 5.43215 9.7044337 5.42386 9.6212437 5.5 9.3710938 C 5.63876 8.9142237 5.80392 8.6597637 6.125 8.3710938 C 6.29333 8.2197537 6.46271 8.0928437 6.5 8.0898438 C 6.5373 8.0868438 6.28485 8.0110437 5.90625 7.9335938 C 5.52767 7.8559938 4.97383 7.6934738 4.6875 7.5898438 C 4.16392 7.4003938 3.457 7.0026837 3.1875 6.7148438 C 3.1761 6.7026437 3.16615 6.6943938 3.15625 6.6835938 C 3.54238 5.1454938 4.626 3.8848438 6.0625 3.2773438 C 6.36307 3.1502138 6.67292 3.0629938 7 2.9960938 C 7.16292 2.9627938 7.33178 2.9506937 7.5 2.9335938 z M 12.1875 5.2773438 C 12.30495 5.3499437 12.74841 6.3093438 12.875 6.7773438 C 13.03844 7.3815337 13.02661 8.4271437 12.875 9.0273438 C 12.8173 9.2557838 12.74335 9.4694937 12.71875 9.4960938 C 12.69415 9.5226938 12.60494 9.3695637 12.5 9.1835938 C 12.39505 8.9976538 12.05984 8.6025437 11.78125 8.3085938 C 10.97711 7.4600637 10.85066 7.0170437 11.1875 6.3398438 C 11.35737 5.9983538 12.0966 5.2212438 12.1875 5.2773438 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>306</width>
<height>387</height>
</rect>
</property>
<property name="windowTitle">
<string>Annotation Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="nameEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="placeholderText">
<string>Annotation Name</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="typeBox"/>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QCheckBox" name="foregroundCheck">
<property name="text">
<string>Foreground</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="foregroundColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>40</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_16">
<item>
<widget class="QCheckBox" name="highlightCheck">
<property name="text">
<string>Highlight</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="highlightColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>40</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_17">
<item>
<widget class="QCheckBox" name="boldCheck">
<property name="text">
<string>Bold</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_18">
<item>
<widget class="QCheckBox" name="italicCheck">
<property name="text">
<string>Italic</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_19">
<item>
<widget class="QCheckBox" name="underlineCheck">
<property name="text">
<string>Underline</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="underlineType"/>
</item>
<item>
<widget class="QPushButton" name="underlineColorButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>45</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>40</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>OK</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

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

@@ -55,46 +55,6 @@
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QListView" name="listView">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="movement">
<enum>QListView::Static</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="resizeMode">
<enum>QListView::Fixed</enum>
</property>
<property name="layoutMode">
<enum>QListView::SinglePass</enum>
</property>
<property name="spacing">
<number>0</number>
</property>
<property name="viewMode">
<enum>QListView::IconMode</enum>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tablePage">
@@ -114,43 +74,6 @@
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QTableView" name="tableView">
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="gridStyle">
<enum>Qt::NoPen</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</widget>

View File

@@ -1,5 +1,31 @@
<RCC>
<qresource prefix="images">
<file>DarkIcons/invert.svg</file>
<file>LightIcons/invert.svg</file>
<file>DarkIcons/manga-mode.svg</file>
<file>LightIcons/manga-mode.svg</file>
<file>DarkIcons/search-word.svg</file>
<file>DarkIcons/search-word.svg</file>
<file>DarkIcons/search-case.svg</file>
<file>LightIcons/search-word.svg</file>
<file>LightIcons/search-case.svg</file>
<file>DarkIcons/page-double.svg</file>
<file>LightIcons/page-double.svg</file>
<file>DarkIcons/about.svg</file>
<file>DarkIcons/switches.svg</file>
<file>LightIcons/about.svg</file>
<file>LightIcons/switches.svg</file>
<file>LightIcons/annotate.svg</file>
<file>DarkIcons/annotate.svg</file>
<file>Google.png</file>
<file>Wikipedia.png</file>
<file>Youtube.png</file>
<file>DarkIcons/web-browser.svg</file>
<file>LightIcons/web-browser.svg</file>
<file>DarkIcons/arrow-down.svg</file>
<file>DarkIcons/arrow-up.svg</file>
<file>LightIcons/arrow-down.svg</file>
<file>LightIcons/arrow-up.svg</file>
<file>Lector.png</file>
<file>DarkIcons/tableofcontents.svg</file>
<file>LightIcons/tableofcontents.svg</file>
@@ -78,5 +104,6 @@
<file>translations_bin/Lector_es.qm</file>
<file>translations_bin/Lector_de.qm</file>
<file>translations_bin/Lector_fr.qm</file>
<file>translations_bin/Lector_zh.qm</file>
</qresource>
</RCC>

View File

@@ -6,211 +6,705 @@
<rect>
<x>0</x>
<y>0</y>
<width>1216</width>
<height>658</height>
<width>1139</width>
<height>612</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Library</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTreeView" name="treeView"/>
</item>
<item row="1" column="0">
<widget class="QTextBrowser" name="aboutBox">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Switches</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="refreshLibrary">
<property name="text">
<string>Startup: Refresh library</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="fileRemember">
<property name="text">
<string>Remember open files</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="performCulling">
<property name="toolTip">
<string>Enabling reduces startup time and memory usage</string>
</property>
<property name="text">
<string>Load covers only when needed</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="coverShadows">
<property name="text">
<string>Cover shadows</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoTags">
<property name="text">
<string>Generate tags from files</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cachingEnabled">
<property name="toolTip">
<string>Greatly reduces page transition time at the cost of more memory</string>
</property>
<property name="text">
<string>Cache comic / pdf pages</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="languageLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Dictionary:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="languageBox"/>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="hideScrollBars">
<property name="toolTip">
<string>Horizontal scrolling with Alt + Scroll
Reopen book to see changes</string>
</property>
<property name="text">
<string>Hide scrollbars when reading</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label">
<property name="toolTip">
<string>Restart application to see changes</string>
</property>
<property name="text">
<string>Icon theme: </string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="darkIconsRadio">
<property name="toolTip">
<string>Restart application to see changes</string>
</property>
<property name="text">
<string>Dar&amp;k</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="lightIconsRadio">
<property name="toolTip">
<string>Restart application to see changes</string>
</property>
<property name="text">
<string>&amp;Light</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
<widget class="SaysHelloWhenClicked" name="listView">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>Scan Library</string>
</property>
<widget class="QStackedWidget" name="stackedWidget">
<widget class="QWidget" name="treeViewPage">
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QTreeView" name="treeView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="switchPage">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Library</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
<widget class="QLabel" name="readAtLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Consider book read at percent</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="readAtPercent">
<property name="minimum">
<number>90</number>
</property>
<property name="value">
<number>95</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label">
<property name="toolTip">
<string>Restart application to see changes</string>
</property>
<property name="text">
<string>Icon theme: </string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="darkIconsRadio">
<property name="toolTip">
<string>Restart application to see changes</string>
</property>
<property name="text">
<string>&amp;Dark</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="lightIconsRadio">
<property name="toolTip">
<string>Restart application to see changes</string>
</property>
<property name="text">
<string>L&amp;ight</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="refreshLibrary">
<property name="text">
<string>Startup: Refresh library</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="fileRemember">
<property name="text">
<string>Remember open files</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="coverShadows">
<property name="text">
<string>Cover shadows</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="performCulling">
<property name="toolTip">
<string>Enabling reduces startup time and memory usage</string>
</property>
<property name="text">
<string>Load covers only when needed</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QCheckBox" name="autoTags">
<property name="text">
<string>Generate tags from files</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="attenuateTitles">
<property name="text">
<string>Shrink long book titles</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_16">
<item>
<widget class="QCheckBox" name="autoCover">
<property name="toolTip">
<string>&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>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Reading</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="0">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QCheckBox" name="hideScrollBars">
<property name="toolTip">
<string>Horizontal scrolling with Alt + Scroll
Reopen book to see changes</string>
</property>
<property name="text">
<string>Hide scrollbars when reading</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cachingEnabled">
<property name="toolTip">
<string>Greatly reduces page transition time at the cost of more memory</string>
</property>
<property name="text">
<string>Cache comic / pdf pages</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QLabel" name="smallIncrementLabel">
<property name="toolTip">
<string>&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>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="languageLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Dictionary language</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="languageBox"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="scrollSpeedLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Scroll speed</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="scrollSpeedSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>3</number>
</property>
<property name="maximum">
<number>15</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="annotationsPage">
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="textTab">
<attribute name="title">
<string>Text</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="newAnnotation">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>New</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteAnnotation">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Delete</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editAnnotation">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Edit</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="moveUp">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Move Up</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="moveDown">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>45</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Move Down</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="annotationsList">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="defaultDropAction">
<enum>Qt::IgnoreAction</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QTextBrowser" name="previewView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="imageTab">
<attribute name="title">
<string>Image</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="aboutPage">
<layout class="QGridLayout" name="gridLayout_9">
<item row="0" column="0">
<widget class="QTabWidget" name="aboutTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="aboutTab">
<attribute name="title">
<string>About</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QTextBrowser" name="aboutBox">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="logTab">
<attribute name="title">
<string>Log</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_10">
<item row="0" column="0">
<widget class="QPlainTextEdit" name="logBox"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="aboutButton">
<property name="text">
<string>About</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QPushButton" name="resetButton">
<property name="text">
<string>Reset Application</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearLogButton">
<property name="text">
<string>Clear Log</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>Scan Library</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SaysHelloWhenClicked</class>
<extends>QListView</extends>
<header>lector.widgets</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

Binary file not shown.

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,30 +11,74 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(1216, 658)
self.gridLayout_3 = QtWidgets.QGridLayout(Dialog)
self.gridLayout_3.setObjectName("gridLayout_3")
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.groupBox_2 = QtWidgets.QGroupBox(Dialog)
self.groupBox_2.setObjectName("groupBox_2")
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2)
self.gridLayout_2.setObjectName("gridLayout_2")
self.treeView = QtWidgets.QTreeView(self.groupBox_2)
self.treeView.setObjectName("treeView")
self.gridLayout_2.addWidget(self.treeView, 0, 0, 1, 1)
self.aboutBox = QtWidgets.QTextBrowser(self.groupBox_2)
self.aboutBox.setOpenExternalLinks(True)
self.aboutBox.setOpenLinks(False)
self.aboutBox.setObjectName("aboutBox")
self.gridLayout_2.addWidget(self.aboutBox, 1, 0, 1, 1)
self.verticalLayout_2.addWidget(self.groupBox_2)
self.groupBox = QtWidgets.QGroupBox(Dialog)
self.groupBox.setObjectName("groupBox")
self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
Dialog.resize(1139, 612)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.listView = SaysHelloWhenClicked(Dialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth())
self.listView.setSizePolicy(sizePolicy)
self.listView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.listView.setObjectName("listView")
self.gridLayout.addWidget(self.listView, 0, 0, 1, 1)
self.verticalLayout_4 = QtWidgets.QVBoxLayout()
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.stackedWidget = QtWidgets.QStackedWidget(Dialog)
self.stackedWidget.setObjectName("stackedWidget")
self.treeViewPage = QtWidgets.QWidget()
self.treeViewPage.setObjectName("treeViewPage")
self.gridLayout_5 = QtWidgets.QGridLayout(self.treeViewPage)
self.gridLayout_5.setObjectName("gridLayout_5")
self.treeView = QtWidgets.QTreeView(self.treeViewPage)
self.treeView.setObjectName("treeView")
self.gridLayout_5.addWidget(self.treeView, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.treeViewPage)
self.switchPage = QtWidgets.QWidget()
self.switchPage.setObjectName("switchPage")
self.gridLayout_2 = QtWidgets.QGridLayout(self.switchPage)
self.gridLayout_2.setObjectName("gridLayout_2")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.groupBox = QtWidgets.QGroupBox(self.switchPage)
self.groupBox.setObjectName("groupBox")
self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox)
self.gridLayout_4.setObjectName("gridLayout_4")
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout_13 = QtWidgets.QHBoxLayout()
self.horizontalLayout_13.setObjectName("horizontalLayout_13")
self.horizontalLayout_14 = QtWidgets.QHBoxLayout()
self.horizontalLayout_14.setObjectName("horizontalLayout_14")
self.readAtLabel = QtWidgets.QLabel(self.groupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.readAtLabel.sizePolicy().hasHeightForWidth())
self.readAtLabel.setSizePolicy(sizePolicy)
self.readAtLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.readAtLabel.setObjectName("readAtLabel")
self.horizontalLayout_14.addWidget(self.readAtLabel)
self.readAtPercent = QtWidgets.QSpinBox(self.groupBox)
self.readAtPercent.setMinimum(90)
self.readAtPercent.setProperty("value", 95)
self.readAtPercent.setObjectName("readAtPercent")
self.horizontalLayout_14.addWidget(self.readAtPercent)
self.horizontalLayout_13.addLayout(self.horizontalLayout_14)
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.label = QtWidgets.QLabel(self.groupBox)
self.label.setObjectName("label")
self.horizontalLayout_7.addWidget(self.label)
self.darkIconsRadio = QtWidgets.QRadioButton(self.groupBox)
self.darkIconsRadio.setObjectName("darkIconsRadio")
self.horizontalLayout_7.addWidget(self.darkIconsRadio)
self.lightIconsRadio = QtWidgets.QRadioButton(self.groupBox)
self.lightIconsRadio.setObjectName("lightIconsRadio")
self.horizontalLayout_7.addWidget(self.lightIconsRadio)
self.horizontalLayout_13.addLayout(self.horizontalLayout_7)
self.verticalLayout_2.addLayout(self.horizontalLayout_13)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.refreshLibrary = QtWidgets.QCheckBox(self.groupBox)
@@ -43,28 +87,77 @@ class Ui_Dialog(object):
self.fileRemember = QtWidgets.QCheckBox(self.groupBox)
self.fileRemember.setObjectName("fileRemember")
self.horizontalLayout_4.addWidget(self.fileRemember)
self.performCulling = QtWidgets.QCheckBox(self.groupBox)
self.performCulling.setObjectName("performCulling")
self.horizontalLayout_4.addWidget(self.performCulling)
self.verticalLayout.addLayout(self.horizontalLayout_4)
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.coverShadows = QtWidgets.QCheckBox(self.groupBox)
self.coverShadows.setObjectName("coverShadows")
self.horizontalLayout.addWidget(self.coverShadows)
self.autoTags = QtWidgets.QCheckBox(self.groupBox)
self.autoTags.setObjectName("autoTags")
self.horizontalLayout.addWidget(self.autoTags)
self.cachingEnabled = QtWidgets.QCheckBox(self.groupBox)
self.cachingEnabled.setObjectName("cachingEnabled")
self.horizontalLayout.addWidget(self.cachingEnabled)
self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1)
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.verticalLayout_2.addLayout(self.horizontalLayout_4)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.languageLabel = QtWidgets.QLabel(self.groupBox)
self.coverShadows = QtWidgets.QCheckBox(self.groupBox)
self.coverShadows.setObjectName("coverShadows")
self.horizontalLayout_3.addWidget(self.coverShadows)
self.performCulling = QtWidgets.QCheckBox(self.groupBox)
self.performCulling.setObjectName("performCulling")
self.horizontalLayout_3.addWidget(self.performCulling)
self.verticalLayout_2.addLayout(self.horizontalLayout_3)
self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
self.horizontalLayout_9.setObjectName("horizontalLayout_9")
self.autoTags = QtWidgets.QCheckBox(self.groupBox)
self.autoTags.setObjectName("autoTags")
self.horizontalLayout_9.addWidget(self.autoTags)
self.attenuateTitles = QtWidgets.QCheckBox(self.groupBox)
self.attenuateTitles.setObjectName("attenuateTitles")
self.horizontalLayout_9.addWidget(self.attenuateTitles)
self.verticalLayout_2.addLayout(self.horizontalLayout_9)
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
self.autoCover = QtWidgets.QCheckBox(self.groupBox)
self.autoCover.setObjectName("autoCover")
self.horizontalLayout_16.addWidget(self.autoCover)
self.verticalLayout_2.addLayout(self.horizontalLayout_16)
self.gridLayout_4.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
self.verticalLayout.addWidget(self.groupBox)
self.groupBox_2 = QtWidgets.QGroupBox(self.switchPage)
self.groupBox_2.setObjectName("groupBox_2")
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox_2)
self.gridLayout_3.setObjectName("gridLayout_3")
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.hideScrollBars = QtWidgets.QCheckBox(self.groupBox_2)
self.hideScrollBars.setObjectName("hideScrollBars")
self.horizontalLayout_6.addWidget(self.hideScrollBars)
self.cachingEnabled = QtWidgets.QCheckBox(self.groupBox_2)
self.cachingEnabled.setObjectName("cachingEnabled")
self.horizontalLayout_6.addWidget(self.cachingEnabled)
self.verticalLayout_3.addLayout(self.horizontalLayout_6)
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
self.horizontalLayout_12.setObjectName("horizontalLayout_12")
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
self.smallIncrementLabel = QtWidgets.QLabel(self.groupBox_2)
self.smallIncrementLabel.setObjectName("smallIncrementLabel")
self.horizontalLayout_15.addWidget(self.smallIncrementLabel)
self.smallIncrementBox = QtWidgets.QSpinBox(self.groupBox_2)
self.smallIncrementBox.setMinimum(4)
self.smallIncrementBox.setMaximum(10)
self.smallIncrementBox.setProperty("value", 4)
self.smallIncrementBox.setObjectName("smallIncrementBox")
self.horizontalLayout_15.addWidget(self.smallIncrementBox)
self.largeIncrementLabel = QtWidgets.QLabel(self.groupBox_2)
self.largeIncrementLabel.setObjectName("largeIncrementLabel")
self.horizontalLayout_15.addWidget(self.largeIncrementLabel)
self.largeIncrementBox = QtWidgets.QSpinBox(self.groupBox_2)
self.largeIncrementBox.setMinimum(1)
self.largeIncrementBox.setMaximum(10)
self.largeIncrementBox.setProperty("value", 2)
self.largeIncrementBox.setObjectName("largeIncrementBox")
self.horizontalLayout_15.addWidget(self.largeIncrementBox)
self.horizontalLayout_12.addLayout(self.horizontalLayout_15)
self.verticalLayout_3.addLayout(self.horizontalLayout_12)
self.horizontalLayout_8 = QtWidgets.QHBoxLayout()
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.languageLabel = QtWidgets.QLabel(self.groupBox_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -72,70 +165,240 @@ class Ui_Dialog(object):
self.languageLabel.setSizePolicy(sizePolicy)
self.languageLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.languageLabel.setObjectName("languageLabel")
self.horizontalLayout_3.addWidget(self.languageLabel)
self.languageBox = QtWidgets.QComboBox(self.groupBox)
self.horizontalLayout_5.addWidget(self.languageLabel)
self.languageBox = QtWidgets.QComboBox(self.groupBox_2)
self.languageBox.setObjectName("languageBox")
self.horizontalLayout_3.addWidget(self.languageBox)
self.horizontalLayout_6.addLayout(self.horizontalLayout_3)
self.hideScrollBars = QtWidgets.QCheckBox(self.groupBox)
self.hideScrollBars.setObjectName("hideScrollBars")
self.horizontalLayout_6.addWidget(self.hideScrollBars)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.label = QtWidgets.QLabel(self.groupBox)
self.label.setObjectName("label")
self.horizontalLayout_5.addWidget(self.label)
self.darkIconsRadio = QtWidgets.QRadioButton(self.groupBox)
self.darkIconsRadio.setObjectName("darkIconsRadio")
self.horizontalLayout_5.addWidget(self.darkIconsRadio)
self.lightIconsRadio = QtWidgets.QRadioButton(self.groupBox)
self.lightIconsRadio.setObjectName("lightIconsRadio")
self.horizontalLayout_5.addWidget(self.lightIconsRadio)
self.horizontalLayout_6.addLayout(self.horizontalLayout_5)
self.gridLayout.addLayout(self.horizontalLayout_6, 2, 0, 1, 1)
self.verticalLayout_2.addWidget(self.groupBox)
self.gridLayout_3.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
self.horizontalLayout_5.addWidget(self.languageBox)
self.horizontalLayout_8.addLayout(self.horizontalLayout_5)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.scrollSpeedLabel = QtWidgets.QLabel(self.groupBox_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.scrollSpeedLabel.sizePolicy().hasHeightForWidth())
self.scrollSpeedLabel.setSizePolicy(sizePolicy)
self.scrollSpeedLabel.setObjectName("scrollSpeedLabel")
self.horizontalLayout.addWidget(self.scrollSpeedLabel)
self.scrollSpeedSlider = QtWidgets.QSlider(self.groupBox_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.scrollSpeedSlider.sizePolicy().hasHeightForWidth())
self.scrollSpeedSlider.setSizePolicy(sizePolicy)
self.scrollSpeedSlider.setMinimum(3)
self.scrollSpeedSlider.setMaximum(15)
self.scrollSpeedSlider.setOrientation(QtCore.Qt.Horizontal)
self.scrollSpeedSlider.setObjectName("scrollSpeedSlider")
self.horizontalLayout.addWidget(self.scrollSpeedSlider)
self.horizontalLayout_8.addLayout(self.horizontalLayout)
self.verticalLayout_3.addLayout(self.horizontalLayout_8)
self.gridLayout_3.addLayout(self.verticalLayout_3, 2, 0, 1, 1)
self.verticalLayout.addWidget(self.groupBox_2)
self.gridLayout_2.addLayout(self.verticalLayout, 0, 0, 1, 1)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem, 1, 0, 1, 1)
self.stackedWidget.addWidget(self.switchPage)
self.annotationsPage = QtWidgets.QWidget()
self.annotationsPage.setObjectName("annotationsPage")
self.gridLayout_7 = QtWidgets.QGridLayout(self.annotationsPage)
self.gridLayout_7.setObjectName("gridLayout_7")
self.tabWidget = QtWidgets.QTabWidget(self.annotationsPage)
self.tabWidget.setObjectName("tabWidget")
self.textTab = QtWidgets.QWidget()
self.textTab.setObjectName("textTab")
self.gridLayout_8 = QtWidgets.QGridLayout(self.textTab)
self.gridLayout_8.setObjectName("gridLayout_8")
self.verticalLayout_5 = QtWidgets.QVBoxLayout()
self.verticalLayout_5.setObjectName("verticalLayout_5")
self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
self.horizontalLayout_11.setObjectName("horizontalLayout_11")
self.verticalLayout_6 = QtWidgets.QVBoxLayout()
self.verticalLayout_6.setObjectName("verticalLayout_6")
spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_6.addItem(spacerItem1)
self.newAnnotation = QtWidgets.QPushButton(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.newAnnotation.sizePolicy().hasHeightForWidth())
self.newAnnotation.setSizePolicy(sizePolicy)
self.newAnnotation.setMinimumSize(QtCore.QSize(30, 0))
self.newAnnotation.setMaximumSize(QtCore.QSize(45, 16777215))
self.newAnnotation.setText("")
self.newAnnotation.setObjectName("newAnnotation")
self.verticalLayout_6.addWidget(self.newAnnotation)
self.deleteAnnotation = QtWidgets.QPushButton(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.deleteAnnotation.sizePolicy().hasHeightForWidth())
self.deleteAnnotation.setSizePolicy(sizePolicy)
self.deleteAnnotation.setMinimumSize(QtCore.QSize(30, 0))
self.deleteAnnotation.setMaximumSize(QtCore.QSize(45, 16777215))
self.deleteAnnotation.setText("")
self.deleteAnnotation.setObjectName("deleteAnnotation")
self.verticalLayout_6.addWidget(self.deleteAnnotation)
self.editAnnotation = QtWidgets.QPushButton(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.editAnnotation.sizePolicy().hasHeightForWidth())
self.editAnnotation.setSizePolicy(sizePolicy)
self.editAnnotation.setMinimumSize(QtCore.QSize(30, 0))
self.editAnnotation.setMaximumSize(QtCore.QSize(45, 16777215))
self.editAnnotation.setText("")
self.editAnnotation.setObjectName("editAnnotation")
self.verticalLayout_6.addWidget(self.editAnnotation)
self.moveUp = QtWidgets.QPushButton(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.moveUp.sizePolicy().hasHeightForWidth())
self.moveUp.setSizePolicy(sizePolicy)
self.moveUp.setMinimumSize(QtCore.QSize(30, 0))
self.moveUp.setMaximumSize(QtCore.QSize(45, 16777215))
self.moveUp.setText("")
self.moveUp.setObjectName("moveUp")
self.verticalLayout_6.addWidget(self.moveUp)
self.moveDown = QtWidgets.QPushButton(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.moveDown.sizePolicy().hasHeightForWidth())
self.moveDown.setSizePolicy(sizePolicy)
self.moveDown.setMinimumSize(QtCore.QSize(30, 0))
self.moveDown.setMaximumSize(QtCore.QSize(45, 16777215))
self.moveDown.setText("")
self.moveDown.setObjectName("moveDown")
self.verticalLayout_6.addWidget(self.moveDown)
spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_6.addItem(spacerItem2)
self.horizontalLayout_11.addLayout(self.verticalLayout_6)
self.annotationsList = QtWidgets.QListView(self.textTab)
self.annotationsList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.annotationsList.setProperty("showDropIndicator", False)
self.annotationsList.setDefaultDropAction(QtCore.Qt.IgnoreAction)
self.annotationsList.setObjectName("annotationsList")
self.horizontalLayout_11.addWidget(self.annotationsList)
self.verticalLayout_5.addLayout(self.horizontalLayout_11)
self.gridLayout_8.addLayout(self.verticalLayout_5, 0, 0, 1, 1)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.previewView = QtWidgets.QTextBrowser(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.previewView.sizePolicy().hasHeightForWidth())
self.previewView.setSizePolicy(sizePolicy)
self.previewView.setMaximumSize(QtCore.QSize(16777215, 100))
self.previewView.setFocusPolicy(QtCore.Qt.NoFocus)
self.previewView.setObjectName("previewView")
self.horizontalLayout_2.addWidget(self.previewView)
self.gridLayout_8.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
self.tabWidget.addTab(self.textTab, "")
self.imageTab = QtWidgets.QWidget()
self.imageTab.setObjectName("imageTab")
self.tabWidget.addTab(self.imageTab, "")
self.gridLayout_7.addWidget(self.tabWidget, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.annotationsPage)
self.aboutPage = QtWidgets.QWidget()
self.aboutPage.setObjectName("aboutPage")
self.gridLayout_9 = QtWidgets.QGridLayout(self.aboutPage)
self.gridLayout_9.setObjectName("gridLayout_9")
self.aboutTabWidget = QtWidgets.QTabWidget(self.aboutPage)
self.aboutTabWidget.setObjectName("aboutTabWidget")
self.aboutTab = QtWidgets.QWidget()
self.aboutTab.setObjectName("aboutTab")
self.gridLayout_6 = QtWidgets.QGridLayout(self.aboutTab)
self.gridLayout_6.setObjectName("gridLayout_6")
self.aboutBox = QtWidgets.QTextBrowser(self.aboutTab)
self.aboutBox.setOpenExternalLinks(True)
self.aboutBox.setOpenLinks(False)
self.aboutBox.setObjectName("aboutBox")
self.gridLayout_6.addWidget(self.aboutBox, 0, 0, 1, 1)
self.aboutTabWidget.addTab(self.aboutTab, "")
self.logTab = QtWidgets.QWidget()
self.logTab.setObjectName("logTab")
self.gridLayout_10 = QtWidgets.QGridLayout(self.logTab)
self.gridLayout_10.setObjectName("gridLayout_10")
self.logBox = QtWidgets.QPlainTextEdit(self.logTab)
self.logBox.setObjectName("logBox")
self.gridLayout_10.addWidget(self.logBox, 0, 0, 1, 1)
self.aboutTabWidget.addTab(self.logTab, "")
self.gridLayout_9.addWidget(self.aboutTabWidget, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.aboutPage)
self.verticalLayout_4.addWidget(self.stackedWidget)
self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
self.resetButton = QtWidgets.QPushButton(Dialog)
self.resetButton.setObjectName("resetButton")
self.horizontalLayout_10.addWidget(self.resetButton)
self.clearLogButton = QtWidgets.QPushButton(Dialog)
self.clearLogButton.setObjectName("clearLogButton")
self.horizontalLayout_10.addWidget(self.clearLogButton)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_10.addItem(spacerItem3)
self.okButton = QtWidgets.QPushButton(Dialog)
self.okButton.setObjectName("okButton")
self.horizontalLayout_2.addWidget(self.okButton)
self.horizontalLayout_10.addWidget(self.okButton)
self.cancelButton = QtWidgets.QPushButton(Dialog)
self.cancelButton.setObjectName("cancelButton")
self.horizontalLayout_2.addWidget(self.cancelButton)
self.aboutButton = QtWidgets.QPushButton(Dialog)
self.aboutButton.setCheckable(True)
self.aboutButton.setObjectName("aboutButton")
self.horizontalLayout_2.addWidget(self.aboutButton)
self.gridLayout_3.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
self.horizontalLayout_10.addWidget(self.cancelButton)
self.verticalLayout_4.addLayout(self.horizontalLayout_10)
self.gridLayout.addLayout(self.verticalLayout_4, 0, 1, 1, 1)
self.retranslateUi(Dialog)
self.tabWidget.setCurrentIndex(0)
self.aboutTabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Settings"))
self.groupBox_2.setTitle(_translate("Dialog", "Library"))
self.groupBox.setTitle(_translate("Dialog", "Switches"))
self.refreshLibrary.setText(_translate("Dialog", "Startup: Refresh library"))
self.fileRemember.setText(_translate("Dialog", "Remember open files"))
self.performCulling.setToolTip(_translate("Dialog", "Enabling reduces startup time and memory usage"))
self.performCulling.setText(_translate("Dialog", "Load covers only when needed"))
self.coverShadows.setText(_translate("Dialog", "Cover shadows"))
self.autoTags.setText(_translate("Dialog", "Generate tags from files"))
self.cachingEnabled.setToolTip(_translate("Dialog", "Greatly reduces page transition time at the cost of more memory"))
self.cachingEnabled.setText(_translate("Dialog", "Cache comic / pdf pages"))
self.languageLabel.setText(_translate("Dialog", "Dictionary:"))
self.hideScrollBars.setToolTip(_translate("Dialog", "Horizontal scrolling with Alt + Scroll\n"
"Reopen book to see changes"))
self.hideScrollBars.setText(_translate("Dialog", "Hide scrollbars when reading"))
self.groupBox.setTitle(_translate("Dialog", "Library"))
self.readAtLabel.setText(_translate("Dialog", "Consider book read at percent"))
self.label.setToolTip(_translate("Dialog", "Restart application to see changes"))
self.label.setText(_translate("Dialog", "Icon theme: "))
self.darkIconsRadio.setToolTip(_translate("Dialog", "Restart application to see changes"))
self.darkIconsRadio.setText(_translate("Dialog", "Dar&k"))
self.darkIconsRadio.setText(_translate("Dialog", "&Dark"))
self.lightIconsRadio.setToolTip(_translate("Dialog", "Restart application to see changes"))
self.lightIconsRadio.setText(_translate("Dialog", "&Light"))
self.lightIconsRadio.setText(_translate("Dialog", "L&ight"))
self.refreshLibrary.setText(_translate("Dialog", "Startup: Refresh library"))
self.fileRemember.setText(_translate("Dialog", "Remember open files"))
self.coverShadows.setText(_translate("Dialog", "Cover shadows"))
self.performCulling.setToolTip(_translate("Dialog", "Enabling reduces startup time and memory usage"))
self.performCulling.setText(_translate("Dialog", "Load covers only when needed"))
self.autoTags.setText(_translate("Dialog", "Generate tags from files"))
self.attenuateTitles.setText(_translate("Dialog", "Shrink long book titles"))
self.autoCover.setToolTip(_translate("Dialog", "<html><head/><body><p>Attempt to download missing book covers from Google books - SLOW</p></body></html>"))
self.autoCover.setText(_translate("Dialog", "Download missing covers"))
self.groupBox_2.setTitle(_translate("Dialog", "Reading"))
self.hideScrollBars.setToolTip(_translate("Dialog", "Horizontal scrolling with Alt + Scroll\n"
"Reopen book to see changes"))
self.hideScrollBars.setText(_translate("Dialog", "Hide scrollbars when reading"))
self.cachingEnabled.setToolTip(_translate("Dialog", "Greatly reduces page transition time at the cost of more memory"))
self.cachingEnabled.setText(_translate("Dialog", "Cache comic / pdf pages"))
self.smallIncrementLabel.setToolTip(_translate("Dialog", "<html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html>"))
self.smallIncrementLabel.setText(_translate("Dialog", "Small increment"))
self.smallIncrementBox.setToolTip(_translate("Dialog", "<html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html>"))
self.largeIncrementLabel.setToolTip(_translate("Dialog", "<html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html>"))
self.largeIncrementLabel.setText(_translate("Dialog", "Large increment"))
self.largeIncrementBox.setToolTip(_translate("Dialog", "<html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html>"))
self.languageLabel.setText(_translate("Dialog", "Dictionary language"))
self.scrollSpeedLabel.setText(_translate("Dialog", "Scroll speed"))
self.newAnnotation.setToolTip(_translate("Dialog", "New"))
self.deleteAnnotation.setToolTip(_translate("Dialog", "Delete"))
self.editAnnotation.setToolTip(_translate("Dialog", "Edit"))
self.moveUp.setToolTip(_translate("Dialog", "Move Up"))
self.moveDown.setToolTip(_translate("Dialog", "Move Down"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.textTab), _translate("Dialog", "Text"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.imageTab), _translate("Dialog", "Image"))
self.aboutTabWidget.setTabText(self.aboutTabWidget.indexOf(self.aboutTab), _translate("Dialog", "About"))
self.aboutTabWidget.setTabText(self.aboutTabWidget.indexOf(self.logTab), _translate("Dialog", "Log"))
self.resetButton.setText(_translate("Dialog", "Reset Application"))
self.clearLogButton.setText(_translate("Dialog", "Clear Log"))
self.okButton.setText(_translate("Dialog", "Scan Library"))
self.cancelButton.setText(_translate("Dialog", "Close"))
self.aboutButton.setText(_translate("Dialog", "About"))
from lector.widgets import SaysHelloWhenClicked

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,10 +1,29 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Keep in mind that all integer / boolean settings are returned as strings
import os
import logging
from ast import literal_eval
from PyQt5 import QtCore, QtGui
logger = logging.getLogger(__name__)
class Settings:
def __init__(self, parent):
@@ -103,6 +122,20 @@ class Settings:
'cachingEnabled', 'True').capitalize())
self.parent.settings['hide_scrollbars'] = literal_eval(self.settings.value(
'hideScrollBars', 'False').capitalize())
self.parent.settings['auto_cover'] = literal_eval(self.settings.value(
'autoCover', 'False').capitalize())
self.parent.settings['scroll_speed'] = int(self.settings.value('scrollSpeed', 7))
self.parent.settings['consider_read_at'] = int(self.settings.value('considerReadAt', 95))
self.parent.settings['small_increment'] = int(self.settings.value('smallIncrement', 4))
self.parent.settings['large_increment'] = int(self.settings.value('largeIncrement', 2))
self.parent.settings['attenuate_titles'] = literal_eval(self.settings.value(
'attenuateTitles', 'False').capitalize())
self.parent.settings['double_page_mode'] = literal_eval(self.settings.value(
'doublePageMode', 'False').capitalize())
self.parent.settings['manga_mode'] = literal_eval(self.settings.value(
'mangaMode', 'False').capitalize())
self.parent.settings['invert_colors'] = literal_eval(self.settings.value(
'invertColors', 'False').capitalize())
self.settings.endGroup()
self.settings.beginGroup('dialogSettings')
@@ -110,8 +143,16 @@ class Settings:
'dialogBackground', QtGui.QColor().fromRgb(0, 0, 0))
self.settings.endGroup()
self.settings.beginGroup('annotations')
self.parent.settings['annotations'] = self.settings.value(
'annotationList', list())
if self.parent.settings['annotations'] is None:
self.parent.settings['annotations'] = list()
self.settings.endGroup()
logger.info('Settings loaded')
def save_settings(self):
print('Saving settings...')
current_settings = self.parent.settings
self.settings.beginGroup('mainWindow')
@@ -165,16 +206,31 @@ class Settings:
self.settings.endGroup()
self.settings.beginGroup('settingsSwitches')
self.settings.setValue('rememberFiles', current_settings['remember_files'])
self.settings.setValue('coverShadows', current_settings['cover_shadows'])
self.settings.setValue('autoTags', current_settings['auto_tags'])
self.settings.setValue('scanLibraryAtStart', current_settings['scan_library'])
self.settings.setValue('performCulling', current_settings['perform_culling'])
self.settings.setValue('dictionaryLanguage', current_settings['dictionary_language'])
self.settings.setValue('cachingEnabled', current_settings['caching_enabled'])
self.settings.setValue('hideScrollBars', current_settings['hide_scrollbars'])
self.settings.setValue('rememberFiles', str(current_settings['remember_files']))
self.settings.setValue('coverShadows', str(current_settings['cover_shadows']))
self.settings.setValue('autoTags', str(current_settings['auto_tags']))
self.settings.setValue('scanLibraryAtStart', str(current_settings['scan_library']))
self.settings.setValue('performCulling', str(current_settings['perform_culling']))
self.settings.setValue('dictionaryLanguage', str(current_settings['dictionary_language']))
self.settings.setValue('cachingEnabled', str(current_settings['caching_enabled']))
self.settings.setValue('hideScrollBars', str(current_settings['hide_scrollbars']))
self.settings.setValue('attenuateTitles', str(current_settings['attenuate_titles']))
self.settings.setValue('autoCover', str(current_settings['auto_cover']))
self.settings.setValue('scrollSpeed', current_settings['scroll_speed'])
self.settings.setValue('considerReadAt', current_settings['consider_read_at'])
self.settings.setValue('doublePageMode', str(current_settings['double_page_mode']))
self.settings.setValue('mangaMode', str(current_settings['manga_mode']))
self.settings.setValue('invertColors', str(current_settings['invert_colors']))
self.settings.setValue('smallIncrement', current_settings['small_increment'])
self.settings.setValue('largeIncrement', current_settings['large_increment'])
self.settings.endGroup()
self.settings.beginGroup('dialogSettings')
self.settings.setValue('dialogBackground', current_settings['dialog_background'])
self.settings.endGroup()
self.settings.beginGroup('annotations')
self.settings.setValue('annotationList', current_settings['annotations'])
self.settings.endGroup()
logger.info('Settings saved')

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
@@ -21,37 +19,51 @@
import os
import copy
import logging
import pathlib
from PyQt5 import QtWidgets, QtCore
from PyQt5 import QtWidgets, QtCore, QtGui
from lector import database
from lector.annotations import AnnotationsUI
from lector.models import MostExcellentFileSystemModel
from lector.threaded import BackGroundBookSearch, BackGroundBookAddition
from lector.resources import settingswindow
from lector.settings import Settings
from lector.logger import logger_filename, VERSION
logger = logging.getLogger(__name__)
class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
def __init__(self, parent=None):
super(SettingsUI, self).__init__()
self.setupUi(self)
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
self._translate = QtCore.QCoreApplication.translate
self.parent = parent
self.database_path = self.parent.database_path
self.main_window = parent
self.database_path = self.main_window.database_path
self.image_factory = self.main_window.QImageFactory
self.resize(self.parent.settings['settings_dialog_size'])
self.move(self.parent.settings['settings_dialog_position'])
# The annotation dialog will use the settings dialog as its parent
self.annotationsDialog = AnnotationsUI(self)
self.resize(self.main_window.settings['settings_dialog_size'])
self.move(self.main_window.settings['settings_dialog_position'])
self.aboutBox.setVisible(False)
install_dir = os.path.realpath(__file__)
install_dir = pathlib.Path(install_dir).parents[1]
aboutfile_path = os.path.join(install_dir, 'lector', 'resources', 'about.html')
with open(aboutfile_path) as about_html:
self.aboutBox.setHtml(about_html.read())
html = about_html.readlines()
html.insert(
8, f'<h3 style="text-align: center;">v{VERSION}</h3>\n')
self.aboutBox.setHtml(''.join(html))
self.paths = None
self.thread = None
self.filesystem_model = None
self.filesystemModel = None
self.tag_data_copy = None
english_string = self._translate('SettingsUI', 'English')
@@ -60,7 +72,7 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
languages = [english_string, spanish_string, hindi_string]
self.languageBox.addItems(languages)
current_language = self.parent.settings['dictionary_language']
current_language = self.main_window.settings['dictionary_language']
if current_language == 'en':
self.languageBox.setCurrentIndex(0)
elif current_language == 'es':
@@ -73,10 +85,9 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self._translate('SettingsUI', 'Save changes and start library scan'))
self.okButton.clicked.connect(self.start_library_scan)
self.cancelButton.clicked.connect(self.cancel_pressed)
self.aboutButton.clicked.connect(self.about_pressed)
# Radio buttons
if self.parent.settings['icon_theme'] == 'DarkIcons':
if self.main_window.settings['icon_theme'] == 'DarkIcons':
self.darkIconsRadio.setChecked(True)
else:
self.lightIconsRadio.setChecked(True)
@@ -84,13 +95,19 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.lightIconsRadio.clicked.connect(self.change_icon_theme)
# Check boxes
self.autoTags.setChecked(self.parent.settings['auto_tags'])
self.coverShadows.setChecked(self.parent.settings['cover_shadows'])
self.refreshLibrary.setChecked(self.parent.settings['scan_library'])
self.fileRemember.setChecked(self.parent.settings['remember_files'])
self.performCulling.setChecked(self.parent.settings['perform_culling'])
self.cachingEnabled.setChecked(self.parent.settings['caching_enabled'])
self.hideScrollBars.setChecked(self.parent.settings['hide_scrollbars'])
self.autoTags.setChecked(self.main_window.settings['auto_tags'])
self.coverShadows.setChecked(self.main_window.settings['cover_shadows'])
self.refreshLibrary.setChecked(self.main_window.settings['scan_library'])
self.fileRemember.setChecked(self.main_window.settings['remember_files'])
self.performCulling.setChecked(self.main_window.settings['perform_culling'])
self.cachingEnabled.setChecked(self.main_window.settings['caching_enabled'])
self.hideScrollBars.setChecked(self.main_window.settings['hide_scrollbars'])
self.attenuateTitles.setChecked(self.main_window.settings['attenuate_titles'])
self.autoCover.setChecked(self.main_window.settings['auto_cover'])
self.scrollSpeedSlider.setValue(self.main_window.settings['scroll_speed'])
self.readAtPercent.setValue(self.main_window.settings['consider_read_at'])
self.smallIncrementBox.setValue(self.main_window.settings['small_increment'])
self.largeIncrementBox.setValue(self.main_window.settings['large_increment'])
self.autoTags.clicked.connect(self.manage_checkboxes)
self.coverShadows.clicked.connect(self.manage_checkboxes)
@@ -99,10 +116,102 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.performCulling.clicked.connect(self.manage_checkboxes)
self.cachingEnabled.clicked.connect(self.manage_checkboxes)
self.hideScrollBars.clicked.connect(self.manage_checkboxes)
self.attenuateTitles.clicked.connect(self.manage_checkboxes)
self.autoCover.clicked.connect(self.manage_checkboxes)
self.scrollSpeedSlider.valueChanged.connect(self.change_scroll_speed)
self.readAtPercent.valueChanged.connect(self.change_read_at)
self.smallIncrementBox.valueChanged.connect(self.change_increment)
self.largeIncrementBox.valueChanged.connect(self.change_increment)
# Generate the QStandardItemModel for the listView
self.listModel = QtGui.QStandardItemModel(self.listView)
library_string = self._translate('SettingsUI', 'Library')
switches_string = self._translate('SettingsUI', 'Switches')
annotations_string = self._translate('SettingsUI', 'Annotations')
about_string = self._translate('SettingsUI', 'About')
list_options = [
library_string, switches_string, annotations_string, about_string]
icon_dict = {
0: 'view-readermode',
1: 'switches',
2: 'annotate',
3: 'about'}
for count, i in enumerate(list_options):
item = QtGui.QStandardItem()
item.setText(i)
this_icon = icon_dict[count]
item.setIcon(
self.main_window.QImageFactory.get_image(this_icon))
self.listModel.appendRow(item)
self.listView.setModel(self.listModel)
# Custom signal to account for page changes
self.listView.newIndexSignal.connect(self.list_index_changed)
# Annotation related buttons
# Icon names
self.newAnnotation.setIcon(self.image_factory.get_image('add'))
self.deleteAnnotation.setIcon(self.image_factory.get_image('remove'))
self.editAnnotation.setIcon(self.image_factory.get_image('edit-rename'))
self.moveUp.setIcon(self.image_factory.get_image('arrow-up'))
self.moveDown.setIcon(self.image_factory.get_image('arrow-down'))
# Icon sizes
self.newAnnotation.setIconSize(QtCore.QSize(24, 24))
self.deleteAnnotation.setIconSize(QtCore.QSize(24, 24))
self.editAnnotation.setIconSize(QtCore.QSize(24, 24))
self.moveUp.setIconSize(QtCore.QSize(24, 24))
self.moveDown.setIconSize(QtCore.QSize(24, 24))
self.annotationsList.clicked.connect(self.load_annotation)
self.annotationsList.doubleClicked.connect(self.editAnnotation.click)
self.newAnnotation.clicked.connect(self.add_annotation)
self.deleteAnnotation.clicked.connect(self.delete_annotation)
self.editAnnotation.clicked.connect(self.load_annotation)
self.moveUp.clicked.connect(self.move_annotation)
self.moveDown.clicked.connect(self.move_annotation)
# Generate annotation settings
self.annotationModel = QtGui.QStandardItemModel()
self.generate_annotations()
# Generate the filesystem treeView
self.generate_tree()
# About... About
self.aboutTabWidget.setDocumentMode(True)
self.aboutTabWidget.setContentsMargins(0, 0, 0, 0)
self.logBox.setReadOnly(True)
# About buttons
self.resetButton.clicked.connect(self.delete_database)
self.clearLogButton.clicked.connect(self.clear_log)
# Hide the image annotation tab
# TODO
# Maybe get off your lazy ass and write something for this
self.tabWidget.setContentsMargins(0, 0, 0, 0)
self.tabWidget.tabBar().setVisible(False)
def list_index_changed(self, index):
switch_to = index.row()
self.stackedWidget.setCurrentIndex(switch_to)
valid_buttons = {
0: (self.okButton,),
3: (self.resetButton, self.clearLogButton),}
for i in valid_buttons:
if i == switch_to:
for j in valid_buttons[i]:
j.setVisible(True)
else:
for j in valid_buttons[i]:
j.setVisible(False)
def generate_tree(self):
# Fetch all directories in the database
paths = database.DatabaseFunctions(
@@ -112,10 +221,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
{'Path': ''},
'LIKE')
self.parent.generate_library_filter_menu(paths)
self.main_window.generate_library_filter_menu(paths)
directory_data = {}
if not paths:
print('Database returned no paths for settings...')
logger.warning('No book paths saved')
else:
# Convert to the dictionary format that is
# to be fed into the QFileSystemModel
@@ -125,9 +234,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
'tags': i[2],
'check_state': i[3]}
self.filesystem_model = MostExcellentFileSystemModel(directory_data)
self.filesystem_model.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
self.treeView.setModel(self.filesystem_model)
self.filesystemModel = MostExcellentFileSystemModel(directory_data)
self.filesystemModel.setFilter(
QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
self.treeView.setModel(self.filesystemModel)
# TODO
# This here might break on them pestilent non unixy OSes
@@ -135,11 +245,12 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
root_directory = QtCore.QDir().rootPath()
self.treeView.setRootIndex(
self.filesystem_model.setRootPath(root_directory))
self.filesystemModel.setRootPath(root_directory))
# Set the treeView and QFileSystemModel to its desired state
selected_paths = [
i for i in directory_data if directory_data[i]['check_state'] == QtCore.Qt.Checked]
i for i in directory_data
if directory_data[i]['check_state'] == QtCore.Qt.Checked]
expand_paths = set()
for i in selected_paths:
@@ -157,10 +268,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
expand_paths.remove(root_directory)
for i in expand_paths:
this_index = self.filesystem_model.index(i)
this_index = self.filesystemModel.index(i)
self.treeView.expand(this_index)
header_sizes = self.parent.settings['settings_dialog_headers']
header_sizes = self.main_window.settings['settings_dialog_headers']
if header_sizes:
for count, i in enumerate((0, 4)):
self.treeView.setColumnWidth(i, int(header_sizes[count]))
@@ -178,7 +289,7 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.hide()
data_pairs = []
for i in self.filesystem_model.tag_data.items():
for i in self.filesystemModel.tag_data.items():
data_pairs.append([
i[0], i[1]['name'], i[1]['tags'], i[1]['check_state']
])
@@ -187,33 +298,37 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.database_path).set_library_paths(data_pairs)
if not data_pairs:
logger.error('Can\'t scan - No book paths saved')
try:
if self.sender().objectName() == 'reloadLibrary':
self.show()
treeViewIndex = self.listModel.index(0, 0)
self.listView.setCurrentIndex(treeViewIndex)
return
except AttributeError:
pass
database.DatabaseFunctions(
self.database_path).delete_from_database('*', '*')
self.parent.lib_ref.generate_model('build')
self.parent.lib_ref.generate_proxymodels()
self.parent.generate_library_filter_menu()
self.main_window.lib_ref.generate_model('build')
self.main_window.lib_ref.generate_proxymodels()
self.main_window.generate_library_filter_menu()
return
# Update the main window library filter menu
self.parent.generate_library_filter_menu(data_pairs)
self.parent.set_library_filter()
self.main_window.generate_library_filter_menu(data_pairs)
self.main_window.set_library_filter()
# Disallow rechecking until the first check completes
self.okButton.setEnabled(False)
self.parent.reloadLibrary.setEnabled(False)
self.main_window.libraryToolBar.reloadLibraryButton.setEnabled(False)
self.okButton.setToolTip(
self._translate('SettingsUI', 'Library scan in progress...'))
# Traverse directories looking for files
self.parent.statusMessage.setText(
self.main_window.statusMessage.setText(
self._translate('SettingsUI', 'Checking library folders'))
self.thread = BackGroundBookSearch(data_pairs)
self.thread.finished.connect(self.finished_iterating)
@@ -223,22 +338,24 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
# The books the search thread has found
# are now in self.thread.valid_files
if not self.thread.valid_files:
self.parent.move_on()
self.main_window.move_on()
return
# Hey, messaging is important, okay?
self.parent.sorterProgress.setVisible(True)
self.parent.statusMessage.setText(
self.main_window.statusBar.setVisible(True)
self.main_window.sorterProgress.setVisible(True)
self.main_window.statusMessage.setText(
self._translate('SettingsUI', 'Parsing files'))
# We now create a new thread to put those files into the database
self.thread = BackGroundBookAddition(
self.thread.valid_files, self.database_path, 'automatic', self.parent)
self.thread.finished.connect(self.parent.move_on)
self.thread.valid_files, self.database_path, 'automatic', self.main_window)
self.thread.finished.connect(
lambda: self.main_window.move_on(self.thread.errors))
self.thread.start()
def cancel_pressed(self):
self.filesystem_model.tag_data = copy.deepcopy(self.tag_data_copy)
self.filesystemModel.tag_data = copy.deepcopy(self.tag_data_copy)
self.hide()
def hideEvent(self, event):
@@ -246,35 +363,57 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
event.accept()
def showEvent(self, event):
self.tag_data_copy = copy.deepcopy(self.filesystem_model.tag_data)
# Load log into the plainTextEdit
with open(logger_filename) as infile:
log_text = infile.read()
self.logBox.setPlainText(log_text)
# Annotation preview
self.format_preview()
# Make copy of tags in case of a nope.jpg
self.tag_data_copy = copy.deepcopy(self.filesystemModel.tag_data)
event.accept()
def no_more_settings(self):
self.parent.libraryToolBar.settingsButton.setChecked(False)
self.aboutBox.hide()
self.treeView.show()
self.main_window.libraryToolBar.settingsButton.setChecked(False)
self.gather_annotations()
Settings(self.main_window).save_settings()
Settings(self.main_window).read_settings()
self.main_window.settings['last_open_tab'] = None # Needed to allow focus change
# to newly opened book
self.resizeEvent()
def resizeEvent(self, event=None):
self.parent.settings['settings_dialog_size'] = self.size()
self.parent.settings['settings_dialog_position'] = self.pos()
self.main_window.settings['settings_dialog_size'] = self.size()
self.main_window.settings['settings_dialog_position'] = self.pos()
table_headers = []
for i in [0, 4]:
table_headers.append(self.treeView.columnWidth(i))
self.parent.settings['settings_dialog_headers'] = table_headers
self.main_window.settings['settings_dialog_headers'] = table_headers
def change_icon_theme(self):
if self.sender() == self.darkIconsRadio:
self.parent.settings['icon_theme'] = 'DarkIcons'
self.main_window.settings['icon_theme'] = 'DarkIcons'
else:
self.parent.settings['icon_theme'] = 'LightIcons'
self.main_window.settings['icon_theme'] = 'LightIcons'
def change_dictionary_language(self, event):
language_dict = {
0: 'en',
1: 'es',
2: 'hi'}
self.parent.settings['dictionary_language'] = language_dict[self.languageBox.currentIndex()]
self.main_window.settings[
'dictionary_language'] = language_dict[self.languageBox.currentIndex()]
def change_scroll_speed(self, event=None):
self.main_window.settings['scroll_speed'] = self.scrollSpeedSlider.value()
def change_read_at(self, event=None):
self.main_window.settings['consider_read_at'] = self.readAtPercent.value()
def change_increment(self, event=None):
self.main_window.settings['small_increment'] = self.smallIncrementBox.value()
self.main_window.settings['large_increment'] = self.largeIncrementBox.value()
def manage_checkboxes(self, event=None):
sender = self.sender().objectName()
@@ -286,13 +425,136 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
'fileRemember': 'remember_files',
'performCulling': 'perform_culling',
'cachingEnabled': 'caching_enabled',
'hideScrollBars': 'hide_scrollbars'}
'hideScrollBars': 'hide_scrollbars',
'attenuateTitles': 'attenuate_titles',
'autoCover': 'auto_cover'}
self.parent.settings[sender_dict[sender]] = not self.parent.settings[sender_dict[sender]]
self.main_window.settings[
sender_dict[sender]] = not self.main_window.settings[sender_dict[sender]]
if not self.performCulling.isChecked():
self.parent.load_all_covers()
self.main_window.cover_functions.load_all_covers()
def about_pressed(self):
self.treeView.setVisible(not self.treeView.isVisible())
self.aboutBox.setVisible(not self.aboutBox.isVisible())
def generate_annotations(self):
saved_annotations = self.main_window.settings['annotations']
for i in saved_annotations:
item = QtGui.QStandardItem()
item.setText(i['name'])
item.setData(i, QtCore.Qt.UserRole)
self.annotationModel.appendRow(item)
self.annotationsList.setModel(self.annotationModel)
def format_preview(self):
# Needed to clear the preview of annotation ickiness
cursor = QtGui.QTextCursor()
self.previewView.setTextCursor(cursor)
self.previewView.setText('Vidistine nuper imagines moventes bonas?')
profile_index = self.main_window.bookToolBar.profileBox.currentIndex()
current_profile = self.main_window.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
if not current_profile:
return
font = current_profile['font']
self.foreground = current_profile['foreground']
background = current_profile['background']
font_size = current_profile['font_size']
self.previewView.setStyleSheet(
"QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format(
font, font_size, self.foreground.name(), background.name()))
block_format = QtGui.QTextBlockFormat()
block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
cursor = self.previewView.textCursor()
while True:
old_position = cursor.position()
cursor.mergeBlockFormat(block_format)
cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1)
new_position = cursor.position()
if old_position == new_position:
break
def add_annotation(self):
self.annotationsDialog.show_dialog('add')
def delete_annotation(self):
selected_index = self.annotationsList.currentIndex()
if not selected_index.isValid():
return
self.annotationModel.removeRow(
self.annotationsList.currentIndex().row())
self.format_preview()
self.annotationsList.clearSelection()
def load_annotation(self):
selected_index = self.annotationsList.currentIndex()
if not selected_index.isValid():
return
if self.sender() == self.annotationsList:
self.annotationsDialog.show_dialog('preview', selected_index)
elif self.sender() == self.editAnnotation:
self.annotationsDialog.show_dialog('edit', selected_index)
def move_annotation(self):
current_row = self.annotationsList.currentIndex().row()
if self.sender() == self.moveUp:
new_row = current_row - 1
if new_row < 0:
return
elif self.sender() == self.moveDown:
new_row = current_row + 1
if new_row == self.annotationModel.rowCount():
return
row_out = self.annotationModel.takeRow(current_row)
self.annotationModel.insertRow(new_row, row_out)
new_index = self.annotationModel.index(new_row, 0)
self.annotationsList.setCurrentIndex(new_index)
def gather_annotations(self):
annotations_out = []
for i in range(self.annotationModel.rowCount()):
annotation_item = self.annotationModel.item(i, 0)
annotation_data = annotation_item.data(QtCore.Qt.UserRole)
annotations_out.append(annotation_data)
self.main_window.settings['annotations'] = annotations_out
def delete_database(self):
def ifcontinue(box_button):
if box_button.text() != '&Yes':
return
database_filename = os.path.join(
self.main_window.database_path, 'Lector.db')
os.remove(database_filename)
QtWidgets.qApp.exit()
# Generate a message box to confirm deletion
confirm_deletion = QtWidgets.QMessageBox()
deletion_prompt = self._translate(
'SettingsUI', f'Delete database and exit?')
confirm_deletion.setText(deletion_prompt)
confirm_deletion.setIcon(QtWidgets.QMessageBox.Critical)
confirm_deletion.setWindowTitle(self._translate('SettingsUI', 'Confirm'))
confirm_deletion.setStandardButtons(
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
confirm_deletion.buttonClicked.connect(ifcontinue)
confirm_deletion.show()
confirm_deletion.exec_()
def clear_log(self):
self.logBox.clear()
open(logger_filename, 'w').close()

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
@@ -17,52 +15,79 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# INSTRUCTIONS
# Every parser is supposed to have the following methods, even if they return None:
# read_book()
# get_title()
# get_author()
# get_year()
# get_cover_image()
# get_isbn()
# get_tags()
# get_contents() - Should return a tuple with 0: TOC 1: special_settings (dict)
# Parsers for files containing only images need to return only images_only = True
# TODO
# Maybe shift to insert or replace instead of hash checking
# See if you want to include a hash of the book's name and author
# Change thread niceness
# Every parser is supposed to have the following methods.
# Exceptions will be caught - but that's just bad practice
# read_book() - Initialize book
# generate_metadata() - For addition
# generate_content() - For reading
import io
import os
import sys
import json
import time
import pickle
import logging
import hashlib
import threading
from multiprocessing import Pool, Manager
from PyQt5 import QtCore, QtGui
import importlib
import urllib.request
# The multiprocessing module does not work correctly on Windows
if sys.platform.startswith('win'):
from multiprocessing.dummy import Pool, Manager
thread_count = 4 # This is all on one CPU thread anyway
else:
from multiprocessing import Pool, Manager, cpu_count
thread_count = cpu_count()
from PyQt5 import QtCore, QtGui
from lector import database
from lector.parsers.pdf import ParsePDF
from lector.parsers.epub import ParseEPUB
from lector.parsers.mobi import ParseMOBI
from lector.parsers.fb2 import ParseFB2
from lector.parsers.comicbooks import ParseCOMIC
logger = logging.getLogger(__name__)
sorter = {
'pdf': ParsePDF,
'epub': ParseEPUB,
'mobi': ParseMOBI,
'azw': ParseMOBI,
'azw3': ParseMOBI,
'azw4': ParseMOBI,
'prc': ParseMOBI,
'cbz': ParseCOMIC,
'cbr': ParseCOMIC}
# Check what dependencies are installed
# pymupdf - Optional
mupdf_check = importlib.util.find_spec('fitz')
if mupdf_check:
from lector.parsers.pdf import ParsePDF
sorter['pdf'] = ParsePDF
else:
error_string = 'pymupdf is not installed. Will be unable to load PDFs.'
print(error_string)
logger.error(error_string)
# python-lxml - Required for everything except comics
lxml_check = importlib.util.find_spec('lxml')
xmltodict_check = importlib.util.find_spec('xmltodict')
if lxml_check:
lxml_dependent = {
'epub': ParseEPUB,
'mobi': ParseMOBI,
'azw': ParseMOBI,
'azw3': ParseMOBI,
'azw4': ParseMOBI,
'prc': ParseMOBI,
'fb2': ParseFB2,
'fb2.zip': ParseFB2}
sorter.update(lxml_dependent)
else:
critical_sting = 'python-lxml / xmltodict is not installed. Only comics will load.'
print(critical_sting)
logger.critical(critical_sting)
available_parsers = [i for i in sorter]
progressbar = None # This is populated by __main__
progress_emitter = None # This is to be made into a global variable
_progress_emitter = None # This is to be made into a global variable
class UpdateProgress(QtCore.QObject):
@@ -77,7 +102,7 @@ class UpdateProgress(QtCore.QObject):
class BookSorter:
def __init__(self, file_list, mode, database_path, auto_tags=True, temp_dir=None):
def __init__(self, file_list, mode, database_path, settings, temp_dir=None):
# Have the GUI pass a list of files straight to here
# Then, on the basis of what is needed, pass the
# filenames to the requisite functions
@@ -90,13 +115,15 @@ class BookSorter:
self.work_mode = mode[0]
self.addition_mode = mode[1]
self.database_path = database_path
self.auto_tags = auto_tags
self.auto_tags = settings['auto_tags']
self.auto_cover = settings['auto_cover']
self.temp_dir = temp_dir
if database_path:
self.database_hashes()
self.threading_completed = []
self.queue = Manager().Queue()
self.errors = Manager().list()
self.processed_books = []
if self.work_mode == 'addition':
@@ -111,14 +138,14 @@ class BookSorter:
'LIKE')
if all_hashes_and_paths:
# self.hashes = [i[0] for i in all_hashes]
self.hashes_and_paths = {
i[0]: i[1] for i in all_hashes_and_paths}
def database_entry_for_book(self, file_hash):
database_return = database.DatabaseFunctions(
self.database_path).fetch_data(
('Title', 'Author', 'Year', 'ISBN', 'Tags', 'Position', 'Bookmarks'),
('Title', 'Author', 'Year', 'ISBN', 'Tags',
'Position', 'Bookmarks', 'CoverImage', 'Annotations'),
'books',
{'Hash': file_hash},
'EQUALS')[0]
@@ -126,7 +153,7 @@ class BookSorter:
book_data = []
for count, i in enumerate(database_return):
if count in (5, 6):
if count in (5, 6, 8): # Position, Bookmarks, and Annotations are pickled
if i:
book_data.append(pickle.loads(i))
else:
@@ -150,7 +177,7 @@ class BookSorter:
self.queue.put(filename)
# This should not get triggered in reading mode
# IF the file is NOT being loaded into the reader,
# IF the file is NOT being loaded into the reader
# Do not allow addition in case the file
# is already in the database and it remains at its original path
@@ -159,91 +186,120 @@ class BookSorter:
or os.path.exists(self.hashes_and_paths[file_md5])):
if not self.hashes_and_paths[file_md5] == filename:
print(f'{os.path.basename(filename)} is already in database')
warning_string = (
f'{os.path.basename(filename)} is already in database')
logger.warning(warning_string)
return
file_extension = os.path.splitext(filename)[1][1:]
try:
# Get the requisite parser from the sorter dict
book_ref = sorter[file_extension](filename, self.temp_dir, file_md5)
except KeyError:
print(filename + ' has an unsupported extension')
# This allows for eliminating issues with filenames that have
# a dot in them. All hail the roundabout fix.
valid_extension = False
for i in sorter:
if os.path.basename(filename).endswith(i):
file_extension = i
valid_extension = True
break
if not valid_extension:
logger.error('Unsupported extension: ' + filename)
return
# Everything following this is standard
# None values are accounted for here
book_ref.read_book()
if book_ref.book:
book_ref = sorter[file_extension](filename, self.temp_dir, file_md5)
# None of the following have an exception type specified
# This will keep everything from crashing, but will make
# troubleshooting difficult
# TODO
# In application notifications
this_book = {}
this_book[file_md5] = {
'hash': file_md5,
'path': filename}
try:
book_ref.read_book()
except Exception as e:
this_error = f'Error initializing: {filename}'
self.errors.append(this_error)
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
return
# Different modes require different values
if self.work_mode == 'addition':
# Reduce the size of the incoming image
# if one is found
title = book_ref.get_title()
this_book = {}
this_book[file_md5] = {
'hash': file_md5,
'path': filename}
author = book_ref.get_author()
if not author:
author = 'Unknown'
# Different modes require different values
if self.work_mode == 'addition':
try:
metadata = book_ref.generate_metadata()
except Exception as e:
this_error = f'Metadata generation error: {filename}'
self.errors.append(this_error)
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
return
year = book_ref.get_year()
title = metadata.title
author = metadata.author
year = metadata.year
isbn = metadata.isbn
isbn = book_ref.get_isbn()
tags = None
if self.auto_tags:
tags = metadata.tags
tags = None
if self.auto_tags:
tags = book_ref.get_tags()
cover_image_raw = metadata.cover
if cover_image_raw:
cover_image = resize_image(cover_image_raw)
else:
cover_image = None
if self.auto_cover:
cover_image = fetch_cover(title, author)
cover_image_raw = book_ref.get_cover_image()
if cover_image_raw:
cover_image = resize_image(cover_image_raw)
else:
cover_image = None
this_book[file_md5]['cover_image'] = cover_image
this_book[file_md5]['addition_mode'] = self.addition_mode
this_book[file_md5]['cover_image'] = cover_image
this_book[file_md5]['addition_mode'] = self.addition_mode
if self.work_mode == 'reading':
try:
book_breakdown = book_ref.generate_content()
except Exception as e:
this_error = f'Content generation error: {filename}'
self.errors.append(this_error)
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
return
if self.work_mode == 'reading':
all_content = book_ref.get_contents()
# get_contents() returns a tuple. Index 1 is a collection of
# special settings that depend on the kind of data being parsed.
# Currently, this includes:
# Only images included images_only BOOL Specify only paths to images
# File will not be cached on exit
content = all_content[0]
images_only = all_content[1]['images_only']
if not content:
content = [('Invalid', 'Something went horribly wrong')]
toc = book_breakdown[0]
content = book_breakdown[1]
images_only = book_breakdown[2]
try:
book_data = self.database_entry_for_book(file_md5)
title = book_data[0]
author = book_data[1]
year = book_data[2]
isbn = book_data[3]
tags = book_data[4]
position = book_data[5]
bookmarks = book_data[6]
except TypeError:
logger.error(
f'Database error: {filename}. Re-add book to program')
return
this_book[file_md5]['position'] = position
this_book[file_md5]['bookmarks'] = bookmarks
this_book[file_md5]['content'] = content
this_book[file_md5]['images_only'] = images_only
title = book_data[0].replace('&', '&&')
author = book_data[1]
year = book_data[2]
isbn = book_data[3]
tags = book_data[4]
position = book_data[5]
bookmarks = book_data[6]
cover = book_data[7]
annotations = book_data[8]
this_book[file_md5]['title'] = title
this_book[file_md5]['author'] = author
this_book[file_md5]['year'] = year
this_book[file_md5]['isbn'] = isbn
this_book[file_md5]['tags'] = tags
this_book[file_md5]['position'] = position
this_book[file_md5]['bookmarks'] = bookmarks
this_book[file_md5]['toc'] = toc
this_book[file_md5]['content'] = content
this_book[file_md5]['images_only'] = images_only
this_book[file_md5]['cover'] = cover
this_book[file_md5]['annotations'] = annotations
return this_book
this_book[file_md5]['title'] = title
this_book[file_md5]['author'] = author
this_book[file_md5]['year'] = year
this_book[file_md5]['isbn'] = isbn
this_book[file_md5]['tags'] = tags
return this_book
def read_progress(self):
while True:
@@ -253,8 +309,9 @@ class BookSorter:
total_number = len(self.file_list)
completed_number = len(self.threading_completed)
if progress_emitter: # Skip update in reading mode
progress_emitter.update_progress(
# Just for the record, this slows down book searching by about 20%
if _progress_emitter: # Skip update in reading mode
_progress_emitter.update_progress(
completed_number * 100 // total_number)
if total_number == completed_number:
@@ -265,7 +322,7 @@ class BookSorter:
return None
def pool_creator():
_pool = Pool(5)
_pool = Pool(thread_count)
self.processed_books = _pool.map(
self.read_book, self.file_list)
@@ -290,21 +347,29 @@ class BookSorter:
return_books[j] = i[j]
del self.processed_books
print('Finished processing in', time.time() - start_time)
return return_books
processing_time = str(time.time() - start_time)
logger.info('Finished processing in ' + processing_time)
return return_books, self.errors
def progress_object_generator():
# This has to be kept separate from the BookSorter class because
# the QtObject inheritance disallows pickling
global progress_emitter
progress_emitter = UpdateProgress()
progress_emitter.connect_to_progressbar()
global _progress_emitter
_progress_emitter = UpdateProgress()
_progress_emitter.connect_to_progressbar()
def resize_image(cover_image_raw):
cover_image = QtGui.QImage()
cover_image.loadFromData(cover_image_raw)
if isinstance(cover_image_raw, QtGui.QImage):
cover_image = cover_image_raw
else:
cover_image = QtGui.QImage()
cover_image.loadFromData(cover_image_raw)
# Resize image to what literally everyone
# agrees is an acceptable cover size
cover_image = cover_image.scaled(
420, 600, QtCore.Qt.IgnoreAspectRatio)
@@ -316,3 +381,46 @@ def resize_image(cover_image_raw):
cover_image_final = io.BytesIO(byte_array)
cover_image_final.seek(0)
return cover_image_final.getvalue()
def fetch_cover(title, author):
# TODO
# Start using the author parameter
# Generate a cover image in case the Google API finds nothing
# Why is that stupid UnicodeEncodeError happening?
api_url = 'https://www.googleapis.com/books/v1/volumes?q='
key = '&key=' + 'AIzaSyDOferpeSS424Dshs4YWY1s-nIBA9884hE'
title = title.replace(' ', '+')
req = api_url + title + key
try:
response = urllib.request.urlopen(req)
if response.getcode() == 200:
response_text = response.read().decode('utf-8')
response_json = json.loads(response_text)
else:
return None
except (urllib.error.HTTPError, urllib.error.URLError):
return None
except UnicodeEncodeError:
logger.error('UnicodeEncodeError fetching cover for ' + title)
return None
try:
# Get cover link from json
cover_link = response_json['items'][0]['volumeInfo']['imageLinks']['thumbnail']
# Get a slightly larger version
cover_link = cover_link.replace('zoom=1', 'zoom=2')
cover_request = urllib.request.urlopen(cover_link)
response = cover_request.read() # Bytes object
cover_image = resize_image(response)
logger.info('Cover found for ' + title)
return cover_image
except:
logger.error(f'Couldn\'t find cover for ' + title)
return None

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
@@ -17,13 +15,23 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import logging
import pathlib
from multiprocessing.dummy import Pool
from PyQt5 import QtCore, QtGui
from lector import sorter
from lector import database
try:
from lector.parsers.pdf import render_pdf_page
except ImportError:
pass
logger = logging.getLogger(__name__)
class BackGroundTabUpdate(QtCore.QThread):
def __init__(self, database_path, all_metadata, parent=None):
@@ -37,7 +45,8 @@ class BackGroundTabUpdate(QtCore.QThread):
database_dict = {
'Position': i['position'],
'LastAccessed': i['last_accessed'],
'Bookmarks': i['bookmarks']}
'Bookmarks': i['bookmarks'],
'Annotations': i['annotations']}
database.DatabaseFunctions(self.database_path).modify_metadata(
database_dict, book_hash)
@@ -50,6 +59,7 @@ class BackGroundBookAddition(QtCore.QThread):
self.database_path = database_path
self.addition_mode = addition_mode
self.main_window = main_window
self.errors = []
self.prune_required = True
if self.addition_mode == 'manual':
@@ -60,10 +70,10 @@ class BackGroundBookAddition(QtCore.QThread):
self.file_list,
('addition', self.addition_mode),
self.database_path,
self.main_window.settings['auto_tags'],
self.main_window.settings,
self.main_window.temp_dir.path())
parsed_books = books.initiate_threads()
parsed_books, self.errors = books.initiate_threads()
self.main_window.lib_ref.generate_model('addition', parsed_books, False)
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
@@ -120,15 +130,23 @@ class BackGroundBookSearch(QtCore.QThread):
if self.valid_directories:
initiate_threads()
print(len(self.valid_files), 'books found')
if self.valid_files:
info_string = str(len(self.valid_files)) + ' books found'
logger.info(info_string)
else:
logger.error('No books found on scan')
else:
print('No valid directories')
logger.error('No valid directories')
class BackGroundCacheRefill(QtCore.QThread):
def __init__(self, image_cache, remove_value, filetype, book, all_pages, parent=None):
super(BackGroundCacheRefill, self).__init__(parent)
# TODO
# Return with only the first image in case of a cache miss
# Rebuilding the entire n image cache takes considerably longer
self.image_cache = image_cache
self.remove_value = remove_value
self.filetype = filetype
@@ -137,16 +155,17 @@ class BackGroundCacheRefill(QtCore.QThread):
def run(self):
def load_page(current_page):
image_pixmap = QtGui.QPixmap()
pixmap = QtGui.QPixmap()
if self.filetype in ('cbz', 'cbr'):
page_data = self.book.read(current_page)
image_pixmap.loadFromData(page_data)
pixmap.loadFromData(page_data)
elif self.filetype == 'pdf':
page_data = self.book.page(current_page)
page_qimage = page_data.renderToImage(350, 350)
image_pixmap.convertFromImage(page_qimage)
return image_pixmap
page_data = self.book.loadPage(current_page)
pixmap = render_pdf_page(page_data)
return pixmap
remove_index = self.image_cache.index(self.remove_value)
@@ -167,3 +186,86 @@ class BackGroundCacheRefill(QtCore.QThread):
self.image_cache.append((next_page, refill_pixmap))
except (IndexError, TypeError):
self.image_cache.append(None)
class BackGroundTextSearch(QtCore.QThread):
def __init__(self):
super(BackGroundTextSearch, self).__init__(None)
self.search_content = None
self.search_text = None
self.case_sensitive = False
self.match_words = False
self.search_results = []
def set_search_options(
self, search_content, search_text,
case_sensitive, match_words):
self.search_content = search_content
self.search_text = search_text
self.case_sensitive = case_sensitive
self.match_words = match_words
def run(self):
if not self.search_text or len(self.search_text) < 3:
return
def get_surrounding_text(textCursor, words_before):
textCursor.movePosition(
QtGui.QTextCursor.WordLeft,
QtGui.QTextCursor.MoveAnchor,
words_before)
textCursor.movePosition(
QtGui.QTextCursor.NextWord,
QtGui.QTextCursor.KeepAnchor,
words_before * 2)
cursor_selection = textCursor.selection().toPlainText()
return cursor_selection.replace('\n', '')
self.search_results = {}
# Create a new QTextDocument of each chapter and iterate
# through it looking for hits
for i in self.search_content:
chapter_title = i[0]
chapterDocument = QtGui.QTextDocument()
chapterDocument.setHtml(i[1])
chapter_number = i[2]
findFlags = QtGui.QTextDocument.FindFlags(0)
if self.match_words:
findFlags = findFlags | QtGui.QTextDocument.FindWholeWords
if self.case_sensitive:
findFlags = findFlags | QtGui.QTextDocument.FindCaseSensitively
findResultCursor = chapterDocument.find(self.search_text, 0, findFlags)
while not findResultCursor.isNull():
result_position = findResultCursor.position()
words_before = 3
while True:
surroundingTextCursor = QtGui.QTextCursor(chapterDocument)
surroundingTextCursor.setPosition(
result_position, QtGui.QTextCursor.MoveAnchor)
surrounding_text = get_surrounding_text(
surroundingTextCursor, words_before)
words_before += 1
if surrounding_text[:2] not in ('. ', ', '):
break
# Case insensitive replace for find results
replace_pattern = re.compile(re.escape(self.search_text), re.IGNORECASE)
surrounding_text = replace_pattern.sub(
f'<b>{self.search_text}</b>', surrounding_text)
result_tuple = (
result_position, surrounding_text, self.search_text, chapter_number)
try:
self.search_results[chapter_title].append(result_tuple)
except KeyError:
self.search_results[chapter_title] = [result_tuple]
new_position = result_position + len(self.search_text)
findResultCursor = chapterDocument.find(
self.search_text, new_position, findFlags)

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,8 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore
logger = logging.getLogger(__name__)
class BookToolBar(QtWidgets.QToolBar):
def __init__(self, parent=None):
@@ -29,9 +31,6 @@ class BookToolBar(QtWidgets.QToolBar):
spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
self.setMovable(False)
self.setIconSize(QtCore.QSize(22, 22))
self.setFloatable(False)
@@ -45,9 +44,9 @@ class BookToolBar(QtWidgets.QToolBar):
image_factory.get_image('gtk-select-font'),
self._translate('BookToolBar', 'View settings'),
self)
self.fullscreenButton = QtWidgets.QAction(
image_factory.get_image('view-fullscreen'),
self._translate('BookToolBar', 'Fullscreen'),
self.annotationButton = QtWidgets.QAction(
image_factory.get_image('annotate'),
self._translate('BookToolBar', 'Annotations (Ctrl + N)'),
self)
self.addBookmarkButton = QtWidgets.QAction(
image_factory.get_image('bookmark-new'),
@@ -55,9 +54,20 @@ class BookToolBar(QtWidgets.QToolBar):
self)
self.bookmarkButton = QtWidgets.QAction(
image_factory.get_image('bookmarks'),
self._translate('BookToolBar', 'Bookmarks'),
self._translate('BookToolBar', 'Bookmarks (Ctrl + B)'),
self)
self.searchButton = QtWidgets.QAction(
image_factory.get_image('search'),
self._translate('BookToolBar', 'Search (Ctrl + F)'),
self)
self.distractionFreeButton = QtWidgets.QAction(
image_factory.get_image('visibility'),
self._translate('Main_BookToolBarUI', 'Toggle distraction free mode (Ctrl + D)'),
self)
self.fullscreenButton = QtWidgets.QAction(
image_factory.get_image('view-fullscreen'),
self._translate('BookToolBar', 'Fullscreen (F)'),
self)
self.bookmarkButton.setObjectName('bookmarkButton')
self.resetProfile = QtWidgets.QAction(
image_factory.get_image('reload'),
self._translate('BookToolBar', 'Reset profile'),
@@ -67,11 +77,15 @@ class BookToolBar(QtWidgets.QToolBar):
self.addAction(self.fontButton)
self.fontButton.setCheckable(True)
self.fontButton.triggered.connect(self.toggle_font_settings)
self.addSeparator()
self.bookSeparator1 = self.addSeparator()
self.addAction(self.addBookmarkButton)
self.addAction(self.bookmarkButton)
self.bookmarkButton.setCheckable(True)
self.addSeparator()
self.bookSeparator2 = self.addSeparator()
self.addAction(self.annotationButton)
self.bookSeparator3 = self.addSeparator()
self.addAction(self.searchButton)
self.bookSeparator4 = self.addSeparator()
self.addAction(self.distractionFreeButton)
self.addAction(self.fullscreenButton)
# Font modification
@@ -168,7 +182,7 @@ class BookToolBar(QtWidgets.QToolBar):
self.fontSeparator4 = self.addSeparator()
self.addAction(self.paddingUp)
self.addAction(self.paddingDown)
self.fontSeparator4 = self.addSeparator()
self.fontSeparator5 = self.addSeparator()
self.addAction(self.alignLeft)
self.addAction(self.alignRight)
self.addAction(self.alignCenter)
@@ -192,38 +206,60 @@ class BookToolBar(QtWidgets.QToolBar):
self.fontSeparator2,
self.fontSeparator3,
self.fontSeparator4,
self.fontSeparator5,
self.resetProfile]
for i in self.fontActions:
i.setVisible(False)
# Comic view modification
self.doublePageButton = QtWidgets.QAction(
image_factory.get_image('page-double'),
self._translate('BookToolBar', 'Double page mode (D)'),
self)
self.doublePageButton.setObjectName('doublePageButton')
self.doublePageButton.setCheckable(True)
self.mangaModeButton = QtWidgets.QAction(
image_factory.get_image('manga-mode'),
self._translate('BookToolBar', 'Manga mode (M)'),
self)
self.mangaModeButton.setObjectName('mangaModeButton')
self.mangaModeButton.setCheckable(True)
self.invertButton = QtWidgets.QAction(
image_factory.get_image('invert'),
self._translate('BookToolBar', 'Invert page colors'),
self)
self.invertButton.setObjectName('mangaModeButton')
self.invertButton.setCheckable(True)
self.zoomIn = QtWidgets.QAction(
image_factory.get_image('zoom-in'),
self._translate('BookToolBar', 'Zoom in'),
self._translate('BookToolBar', 'Zoom in (+)'),
self)
self.zoomIn.setObjectName('zoomIn')
self.zoomOut = QtWidgets.QAction(
image_factory.get_image('zoom-out'),
self._translate('BookToolBar', 'Zoom Out'),
self._translate('BookToolBar', 'Zoom Out (-)'),
self)
self.zoomOut.setObjectName('zoomOut')
self.fitWidth = QtWidgets.QAction(
image_factory.get_image('zoom-fit-width'),
self._translate('BookToolBar', 'Fit Width'),
self._translate('BookToolBar', 'Fit Width (W)'),
self)
self.fitWidth.setObjectName('fitWidth')
self.fitWidth.setCheckable(True)
self.bestFit = QtWidgets.QAction(
image_factory.get_image('zoom-fit-best'),
self._translate('BookToolBar', 'Best Fit'),
self._translate('BookToolBar', 'Best Fit (B)'),
self)
self.bestFit.setObjectName('bestFit')
self.bestFit.setCheckable(True)
self.originalSize = QtWidgets.QAction(
image_factory.get_image('zoom-original'),
self._translate('BookToolBar', 'Original size'),
self._translate('BookToolBar', 'Original size (O)'),
self)
self.originalSize.setObjectName('originalSize')
self.originalSize.setCheckable(True)
@@ -233,15 +269,22 @@ class BookToolBar(QtWidgets.QToolBar):
self.comicBGColor.setObjectName('comicBGColor')
self.comicSeparator1 = self.addSeparator()
self.addAction(self.doublePageButton)
self.addAction(self.mangaModeButton)
self.addAction(self.invertButton)
self.comicSeparator2 = self.addSeparator()
self.addAction(self.zoomIn)
self.addAction(self.zoomOut)
self.addAction(self.fitWidth)
self.addAction(self.bestFit)
self.addAction(self.originalSize)
self.comicSeparator2 = self.addSeparator()
self.comicSeparator3 = self.addSeparator()
self.comicBGColorAction = self.addWidget(self.comicBGColor)
self.comicActions = [
self.doublePageButton,
self.mangaModeButton,
self.invertButton,
self.comicBGColorAction,
self.zoomIn,
self.zoomOut,
@@ -249,39 +292,40 @@ class BookToolBar(QtWidgets.QToolBar):
self.bestFit,
self.originalSize,
self.comicSeparator1,
self.comicSeparator2]
self.comicSeparator2,
self.comicSeparator3]
for i in self.comicActions:
i.setVisible(False)
# Other booktoolbar widgets
self.searchBar = FixedLineEdit(self)
self.searchBar.setPlaceholderText(
self._translate('BookToolBar', 'Search...'))
self.searchBar.setSizePolicy(sizePolicy)
self.searchBar.setContentsMargins(10, 0, 0, 0)
self.searchBar.setObjectName('searchBar')
# Sorter
# Table of contents Combo Box
# Has to have a QTreeview associated with it
self.tocBox = FixedComboBox(self)
self.tocBox.setObjectName('sortingBox')
self.tocBox.setToolTip(
self._translate('BookToolBar', 'Table of Contents'))
self.tocTreeView = QtWidgets.QTreeView(self.tocBox)
self.tocBox.setView(self.tocTreeView)
self.tocTreeView.setItemsExpandable(False)
self.tocTreeView.setRootIsDecorated(False)
# All of these will be put after the spacer
# This means that the buttons in the left side of
# the toolbar have to split up and added here
self.boxSpacer = self.addWidget(spacer)
self.addWidget(spacer)
self.tocBoxAction = self.addWidget(self.tocBox)
self.searchBarAction = self.addWidget(self.searchBar)
self.bookActions = [
self.annotationButton,
self.addBookmarkButton,
self.bookmarkButton,
self.searchButton,
self.distractionFreeButton,
self.fullscreenButton,
self.tocBoxAction,
self.searchBarAction]
self.bookSeparator1,
self.bookSeparator2,
self.bookSeparator3,
self.bookSeparator4]
for i in self.bookActions:
i.setVisible(True)
@@ -295,17 +339,16 @@ class BookToolBar(QtWidgets.QToolBar):
self.customize_view_off()
def customize_view_on(self):
if self.parent().tabWidget.widget(
self.parent().tabWidget.currentIndex()).metadata['images_only']:
# The following might seem redundant,
# but it's necessary for tab switching
images_only = self.parent().tabWidget.currentWidget().are_we_doing_images_only
# The following might seem redundant,
# but it's necessary for tab switching
if images_only:
for i in self.comicActions:
i.setVisible(True)
for i in self.fontActions:
i.setVisible(False)
else:
for i in self.fontActions:
i.setVisible(True)
@@ -317,6 +360,7 @@ class BookToolBar(QtWidgets.QToolBar):
i.setVisible(False)
def customize_view_off(self):
self.fontButton.setChecked(False)
for i in self.fontActions:
i.setVisible(False)
@@ -332,15 +376,10 @@ class LibraryToolBar(QtWidgets.QToolBar):
super(LibraryToolBar, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
spacer = QtWidgets.QWidget()
spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.setMovable(False)
self.setIconSize(QtCore.QSize(22, 22))
self.setFloatable(False)
self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
self.setObjectName("LibraryToolBar")
image_factory = self.window().QImageFactory
@@ -353,16 +392,6 @@ class LibraryToolBar(QtWidgets.QToolBar):
image_factory.get_image('remove'),
self._translate('LibraryToolBar', 'Delete book'),
self)
self.colorButton = QtWidgets.QAction(
image_factory.get_image('color-picker'),
self._translate('LibraryToolBar', 'Library background color'),
self)
self.colorButton.setObjectName('libraryBackground')
self.settingsButton = QtWidgets.QAction(
image_factory.get_image('settings'),
self._translate('LibraryToolBar', 'Settings'),
self)
self.settingsButton.setCheckable(True)
self.coverViewButton = QtWidgets.QAction(
image_factory.get_image('view-grid'),
@@ -375,13 +404,33 @@ class LibraryToolBar(QtWidgets.QToolBar):
self)
self.tableViewButton.setCheckable(True)
self.reloadLibraryButton = QtWidgets.QAction(
image_factory.get_image('reload'),
self._translate('LibraryToolBar', 'Scan Library'),
self)
self.reloadLibraryButton.setObjectName('reloadLibrary')
self.libraryFilterButton = QtWidgets.QToolButton(self)
self.libraryFilterButton.setIcon(image_factory.get_image('view-readermode'))
self.libraryFilterButton.setText(
self._translate('LibraryToolBar', 'Filter library'))
self.libraryFilterButton.setToolTip(
self._translate('LibraryToolBar', 'Filter library'))
self.colorButton = QtWidgets.QAction(
image_factory.get_image('color-picker'),
self._translate('LibraryToolBar', 'Library background color'),
self)
self.colorButton.setObjectName('libraryBackground')
self.settingsButton = QtWidgets.QAction(
image_factory.get_image('settings'),
self._translate('LibraryToolBar', 'Settings'),
self)
self.settingsButton.setCheckable(True)
self.aboutButton = QtWidgets.QAction(
image_factory.get_image('about'),
self._translate('LibraryToolBar', 'About'),
self)
# Auto unchecks the other QToolButton in case of clicking
self.viewButtons = QtWidgets.QActionGroup(self)
self.viewButtons.setExclusive(True)
@@ -395,10 +444,12 @@ class LibraryToolBar(QtWidgets.QToolBar):
self.addAction(self.coverViewButton)
self.addAction(self.tableViewButton)
self.addSeparator()
self.addAction(self.reloadLibraryButton)
self.addWidget(self.libraryFilterButton)
self.addSeparator()
self.addAction(self.colorButton)
self.addAction(self.settingsButton)
self.addAction(self.aboutButton)
# Filter
sizePolicy = QtWidgets.QSizePolicy(
@@ -409,29 +460,33 @@ class LibraryToolBar(QtWidgets.QToolBar):
self.searchBar.setPlaceholderText(
self._translate('LibraryToolBar', 'Search for Title, Author, Tags...'))
self.searchBar.setSizePolicy(sizePolicy)
self.searchBar.setContentsMargins(10, 0, 0, 0)
self.searchBar.setObjectName('searchBar')
self.searchBar.setContentsMargins(0, 0, 10, 0)
# Sorter
title_string = self._translate('TableProxyModel', 'Title')
author_string = self._translate('TableProxyModel', 'Author')
year_string = self._translate('TableProxyModel', 'Year')
newest_string = self._translate('TableProxyModel', 'Newest')
lastread_string = self._translate('TableProxyModel', 'Last Read')
title_string = self._translate('LibraryToolBar', 'Title')
author_string = self._translate('LibraryToolBar', 'Author')
year_string = self._translate('LibraryToolBar', 'Year')
newest_string = self._translate('LibraryToolBar', 'Newest')
lastread_string = self._translate('LibraryToolBar', 'Last Read')
progress_string = self._translate('LibraryToolBar', 'Progress')
sorting_choices = [
title_string, author_string, year_string, newest_string, lastread_string]
title_string, author_string, year_string,
newest_string, lastread_string, progress_string]
self.sortingBox = FixedComboBox(self)
self.sortingBox.addItems(sorting_choices)
self.sortingBox.setObjectName('sortingBox')
self.sortingBox.setSizePolicy(sizePolicy)
self.sortingBox.setMinimumContentsLength(10)
self.sortingBox.setToolTip(self._translate('LibraryToolBar', 'Sort by'))
# Spacer
spacer = QtWidgets.QWidget()
spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# Add widgets
self.addWidget(spacer)
self.sortingBoxAction = self.addWidget(self.sortingBox)
self.addWidget(self.searchBar)
self.sortingBoxAction = self.addWidget(self.sortingBox)
# Sublassing these widgets out prevents them from resizing
@@ -439,18 +494,21 @@ class FixedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(FixedComboBox, self).__init__(parent)
screen_width = QtWidgets.QDesktopWidget().screenGeometry().width()
self.adjusted_size = screen_width // 4.8
self.adjusted_size = screen_width // 4.5
def sizeHint(self):
# This and the one below should adjust to screen size
return QtCore.QSize(self.adjusted_size, 22)
def wheelEvent(self, QWheelEvent):
# Disable mouse wheel scrolling in the ComboBox
return
class FixedLineEdit(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super(FixedLineEdit, self).__init__(parent)
screen_width = QtWidgets.QDesktopWidget().screenGeometry().width()
self.adjusted_size = screen_width // 4.8
self.adjusted_size = screen_width // 4.5
def sizeHint(self):
return QtCore.QSize(self.adjusted_size, 22)

File diff suppressed because it is too large Load Diff

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,22 +1,16 @@
import codecs
from os import path
from setuptools import setup, find_packages
from lector.logger import VERSION
HERE = path.abspath(path.dirname(__file__))
MAJOR_VERSION = '0'
MINOR_VERSION = '3'
MICRO_VERSION = '0'
VERSION = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION)
# Get the long description from the README file
with codecs.open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
LONG_DESC = f.read()
INSTALL_DEPS = ['requests',
'beautifulsoup4']
TEST_DEPS = ['pytest',
'unittest2']
INSTALL_DEPS = ['beautifulsoup4']
TEST_DEPS = []
DEV_DEPS = []
setup(
@@ -75,6 +69,6 @@ setup(
extras_require={
'dev': DEV_DEPS,
'test': TEST_DEPS,
'PDF': ['python-poppler-qt5']
'PDF': ['pymupdf']
},
)