98 Commits
0.4 ... master

Author SHA1 Message Date
BasioMeusPuga
4389a0f5aa Merge pull request #120 from terrycloth/packaging
AppStream metadata, for giving Lector a proper page in Linux app stores
2020-01-15 00:08:44 +05:30
Andrew Toskin
b54ff37828 Replace empty dummy text with actual release notes, at least for now
We don't necessarily *need* to maintain release notes in the
.metainfo.xml file, but for my local test package builds, it would be
better to have actual information than to leave in the dummy text. (The
duplicate empty versions cause it to fail validation.)
2020-01-13 11:02:32 -08:00
Andrew Toskin
e7dd10fa3a Preferred naming scheme for .desktop files is also "reverse-DNS" 2019-12-30 20:40:40 -08:00
Andrew Toskin
3ede5b78fa Add AppStream .metainfo.xml file for Linux app stores
I had to make a number of assumptions here, like what the ID and
metainfo.xml license should be, whether to include the releases, etc. I
think it will be easier to talk about them in the pull request, though,
as then I'll be able to highlight the relevant lines for each point.
2019-12-30 20:37:19 -08:00
BasioMeusPuga
418d9e0c1c Merge pull request #116 from timgates42/bugfix/typo_specific
Fix simple typo: specifc -> specific
2019-12-02 22:33:28 -08:00
Tim Gates
e977826ea1 Fix simple typo: specifc -> specific 2019-12-03 15:05:57 +11:00
BasioMeusPuga
c661ed54de Update translations: Portuguese 2019-10-31 09:10:17 -07:00
BasioMeusPuga
dd3aa8a49c Merge pull request #112 from elisamalzoni/master
Portuguese support
2019-10-31 08:59:18 -07:00
elisamalzoni
65ad48c442 Portuguese support 2019-10-29 09:44:32 -03:00
BasioMeusPuga
fd433d6432 Tag generation stopgap 2019-08-26 17:42:56 -07:00
BasioMeusPuga
2bc73450fe Update translations: Czech 2019-07-06 21:47:45 -07:00
BasioMeusPuga
8b6800e14f Merge pull request #104 from VirtualThief/txt-support
Support for TXT files
2019-07-06 04:42:29 -07:00
Dmitrii Petukhov
d0cdd531a9 Check for textile installation 2019-07-06 07:37:32 +01:00
Dmitrii Petukhov
5e74b6f261 Fix for bookmarks in books without cover 2019-07-06 07:30:40 +01:00
Dmitrii Petukhov
24e45ac2b7 Support for opening txt files 2019-07-06 00:09:50 +01:00
BasioMeusPuga
916bdb5b14 Update translations: Japanese 2019-05-26 07:43:12 -04:00
BasioMeusPuga
ca108da948 Merge pull request #100 from sorairolake/japanese-translation
Add Japanese translation
2019-05-26 07:36:29 -04:00
Shun Sakai
6762f2cfce Add Japanese translation 2019-05-24 22:02:10 +09:00
BasioMeusPuga
0aea9ec33b Implement text pagination
Start double page mode
2019-04-01 22:25:09 -04:00
BasioMeusPuga
af1b988d93 Minor fixes 2019-03-22 21:35:09 -04:00
BasioMeusPuga
8fd6a0d432 Improve navigation bar 2019-03-16 21:53:19 -04:00
BasioMeusPuga
56f15528c2 Markdown support 2019-03-16 12:02:30 -04:00
BasioMeusPuga
f358ad169c Preliminary Navigation Bar 2019-03-16 10:27:07 -04:00
BasioMeusPuga
4cf0a9e78c Implement image rotation 2019-03-16 00:06:53 -04:00
BasioMeusPuga
38de0dcd13 Improve DjVu support 2019-03-15 19:08:06 -04:00
BasioMeusPuga
eb49ca92a4 Update README.md 2019-03-14 23:07:37 -04:00
BasioMeusPuga
bf93c7beab Preliminary DjVu support
SideDock fade in animation
2019-03-14 23:00:42 -04:00
BasioMeusPuga
ca57983739 Fix cover image name assignment 2019-03-11 07:06:54 -04:00
BasioMeusPuga
c71985f621 Minor fixes 2019-03-09 10:11:12 -05:00
BasioMeusPuga
c8fe0ba8b6 Make dependency checks less... crashy 2019-03-03 07:56:05 -05:00
BasioMeusPuga
d6df28c503 Merge pull request #92 from guoyunhe/hidpi-icons
Make icons sharp in HiDPI screen
2019-03-02 19:47:50 -05:00
Guo Yunhe
75ace25c57 Make icons sharp in HiDPI screen 2019-03-02 13:37:04 +01:00
BasioMeusPuga
d2d7dc2c8f Update for release 2019-03-01 23:17:22 -05:00
BasioMeusPuga
f622b0c23e Implement image color inversion 2019-02-19 00:01:07 +05:30
BasioMeusPuga
f312714a2c Multiple fixes
MuPDF import error
Definition text color
Database logging
2019-02-15 00:17:47 +05:30
BasioMeusPuga
c3f26ca225 Whoops 2019-02-13 00:27:48 +05:30
BasioMeusPuga
f6c7307647 Update README.md 2019-02-13 00:12:01 +05:30
BasioMeusPuga
b1714b9674 Error notifications
In application log viewer / database reset
Cleanup settings navigation
2019-02-13 00:09:37 +05:30
BasioMeusPuga
fa030e3060 Update flatpak manifest
Implement automated missing cover downloading
2019-02-12 11:51:16 +05:30
BasioMeusPuga
564db06179 Multiple fixes
Images are now center aligned
Better logging
2019-02-11 11:54:05 +05:30
BasioMeusPuga
3cd75807f9 Fix MOBI parser
Update Kindleunpack
Discover new and exciting bugs
2019-02-10 17:58:35 +05:30
BasioMeusPuga
f6f9d01060 Cleanup parsers 2019-02-10 09:03:12 +05:30
BasioMeusPuga
c6e30b67ad Improve EPUB parser compatibility and speed
Completely break MOBI parser
2019-02-10 06:47:51 +05:30
BasioMeusPuga
e4be239bf0 Overhaul EPUB parsing and ToC generation 2019-02-09 04:21:22 +05:30
BasioMeusPuga
1e004774c9 Search Google books for missing covers
Small fixes
2019-02-05 23:12:22 +05:30
BasioMeusPuga
91ca1e2190 Improve fb2 parsing
Miscellaneous fixes to navigation
2019-02-05 02:50:47 +05:30
BasioMeusPuga
d1662b47d9 Small fixes 2019-02-02 13:04:38 +05:30
BasioMeusPuga
dfe0fceea9 Small fixes
Compulsive refactor
2019-01-31 13:00:21 +05:30
BasioMeusPuga
268014cc3a Refactor sideDock 2019-01-31 01:51:47 +05:30
BasioMeusPuga
d1b1d7c59c Cleanup 2019-01-29 07:32:52 +05:30
BasioMeusPuga
470fc1078f Multiple fixes
Update translations
2019-01-28 02:28:43 +05:30
BasioMeusPuga
96f4d9193a Update requirements.txt 2019-01-26 19:53:40 +05:30
BasioMeusPuga
7aa42603bd Debulk widgets module 2019-01-26 19:44:58 +05:30
BasioMeusPuga
9a6392d1e6 Update README.md 2019-01-26 19:42:33 +05:30
BasioMeusPuga
739b84e9f4 Overhaul TOC generation and navigation 2019-01-26 19:03:30 +05:30
BasioMeusPuga
66746b4eaa Improve cover creation for PDFs 2019-01-22 23:32:25 +05:30
BasioMeusPuga
164450a888 Shift to MuPDF backend for pdf rendering 2019-01-22 22:36:32 +05:30
BasioMeusPuga
191ea7ef3a Update README.md 2019-01-19 22:34:21 +05:30
BasioMeusPuga
ca8ddd38a2 Improve logging
requirements.txt
Small UI fixes
2019-01-19 22:29:56 +05:30
BasioMeusPuga
a45e183914 Implement logging 2019-01-19 20:31:19 +05:30
BasioMeusPuga
506c458544 Update readme
Begin logging
Account for fb2 books without covers
2019-01-19 01:19:58 +05:30
BasioMeusPuga
5e3987dc04 Improve comic view 2019-01-17 23:03:28 +05:30
BasioMeusPuga
5b8bc1d707 Comic page increment setting 2019-01-17 22:42:11 +05:30
BasioMeusPuga
b3e4060661 UI cleanup 2019-01-17 22:17:22 +05:30
BasioMeusPuga
2185e9fcf7 Tab reordering 2019-01-17 21:53:04 +05:30
BasioMeusPuga
5d35319164 Improve mouse pointer hiding
Improve search result formatting
2019-01-16 12:58:40 +05:30
BasioMeusPuga
c6d24fd970 Multiple fixes 2019-01-16 12:25:59 +05:30
BasioMeusPuga
17f39c557b Manga mode
Comics are parsed for images only
Miscellaneous fixes
2019-01-14 15:54:29 +05:30
BasioMeusPuga
f997bc9c9a Search result highlighting
Disable UI elements when irrelevant
2019-01-09 13:50:08 +05:30
BasioMeusPuga
930a97a8fa Implement search 2019-01-09 05:58:47 +05:30
BasioMeusPuga
026fff3d7a Implement search UI
Discovered severe inadequacies. Some of them were in the program.
2019-01-08 05:47:50 +05:30
BasioMeusPuga
f9eec130dd Update translations 2019-01-03 04:05:52 +05:30
BasioMeusPuga
d75689ea97 Consolidate docks
Update copyright
Pondered the nature of protein powder
2019-01-03 03:28:31 +05:30
BasioMeusPuga
6ea5635d28 Cleanup 2018-12-01 18:59:49 +05:30
BasioMeusPuga
4b9221128c Implement single/double page modes for comics/pdfs 2018-10-31 10:39:21 +05:30
BasioMeusPuga
b5349315be UI elements for page modes 2018-10-29 23:08:34 +05:30
BasioMeusPuga
ee18f157f1 Account for absence of Qt Multimedia 2018-10-20 03:49:45 +05:30
BasioMeusPuga
ae325736d5 Update README.md 2018-07-11 13:09:16 -04:00
BasioMeusPuga
826da72d4b Adjust image paths 2018-07-11 13:07:58 -04:00
BasioMeusPuga
b5231cd383 Merge pull request #71 from psikoz/master
logo add
2018-07-11 13:02:24 -04:00
BasioMeusPuga
5ac843e48c Merge pull request #72 from guoyunhe/patch-1
Update README: openSUSE package is now official
2018-07-11 12:55:22 -04:00
Guo Yunhe
74417319be Update README: openSUSE package is now official
Lector is now included in openSUSE Tumbleweed and will be included in future Leap releases.
2018-07-11 13:15:37 +03:00
psikoz
f8555a6ed5 Update README.md 2018-07-11 12:33:03 +03:00
psikoz
4346c27adc Add files via upload 2018-07-11 12:21:09 +03:00
BasioMeusPuga
16adf57dae Add option to include TOC with bookmarks
Shift dock positions
2018-07-08 20:20:57 -04:00
BasioMeusPuga
8534088f4a Update README.md 2018-07-06 16:19:24 -04:00
BasioMeusPuga
a81ed537a6 Improve bookmark addition and deletion
Fix toolbar button checking
2018-07-06 15:38:56 -04:00
BasioMeusPuga
ed5bc0b2b9 Shift to tree view for bookmarks
Cleanup
2018-07-06 09:01:40 -04:00
BasioMeusPuga
8cb8904e58 Exception handling for improperly formatted fb2 books 2018-06-17 10:40:37 -04:00
BasioMeusPuga
aa093b8cc2 Image display in fb2 parser 2018-06-17 10:29:55 -04:00
BasioMeusPuga
42b4d0317d Update README.md 2018-06-14 16:14:11 -04:00
BasioMeusPuga
a0e463bc58 Speed up file addition
Improve fb2 parser
Fix extension checking
2018-06-14 16:10:27 -04:00
BasioMeusPuga
4a2da61b51 Merge branch 'master' of https://github.com/basiomeuspuga/lector 2018-06-13 16:33:57 -04:00
BasioMeusPuga
5c481ccafe Begin fb2 support
Fix Chinese translation
2018-06-13 16:33:30 -04:00
BasioMeusPuga
30760b879e Merge pull request #67 from guoyunhe/guoyunhe-patch-1
Add openSUSE package
2018-06-07 18:17:41 -04:00
Guo Yunhe
af7868f62a Add openSUSE package 2018-06-07 19:03:45 +03:00
BasioMeusPuga
62c44730d8 Start search functionality
Multiple fixes
2018-05-24 15:14:56 -04:00
BasioMeusPuga
045d8a3e52 Update README.md 2018-05-15 00:45:40 -04:00
88 changed files with 24341 additions and 11670 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,20 +15,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SOURCES += lector/__main__.py \
lector/annotations.py \
lector/contentwidgets.py \
lector/definitionsdialog.py \
lector/dockwidgets.py \
lector/library.py \
lector/metadatadialog.py \
lector/models.py \
lector/widgets.py \
lector/library.py \
lector/toolbars.py \
lector/settingsdialog.py \
lector/toolbars.py \
lector/widgets.py \
lector/resources/definitions.py \
lector/resources/settingswindow.py \
lector/resources/metadata.py \
lector/resources/mainwindow.py
TRANSLATIONS += lector/resources/translations/Lector_es.ts \
TRANSLATIONS += lector/resources/translations/Lector_cs.ts \
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/Lector_ja.ts \
lector/resources/translations/Lector_pt.ts \
lector/resources/translations/SAMPLE.ts

View File

@@ -1,26 +1,47 @@
# 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
* djvu
* fb2
* mobi
* azw / azw3 / azw4
* cbr / cbz
* md
Support for a bunch of other formats is coming. Please see the TODO for additional information.
## Contribute
[Paypal](https://www.paypal.me/supportlector)
Bitcoin: 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro
## Requirements
### Needed
| Package | Version tested |
| --- | --- |
| Qt5 | 5.10.1 |
| Python | 3.6 |
| PyQt5 | 5.10.1 |
| python-lxml | 4.3.0 |
| python-beautifulsoup4 | 4.6.0 |
| poppler-qt5 | 0.61.1 |
| python-poppler-qt5 | 0.24.2 |
| python-xmltodict | 0.11.0 |
poppler-qt5 and python-poppler-qt5 are optional.
### Optional
| Package | Version tested | Required for |
| --- | --- | --- |
| python-pymupdf | 1.14.5 | PDF support |
| python-djvulibre | 0.8.4 | DjVu support |
| python-markdown | 3.0.1 | Markdown support |
| textile | 3.0.4 | TXT support |
## Support
When reporting issues:
* Make sure you're at the latest commit.
* Run with `$EXECUTABLEPATH debug`.
* Include the log `~/.local/share/Lector/Lector.log` AND terminal output.
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
## Installation
### Manual
@@ -33,8 +54,11 @@ poppler-qt5 and python-poppler-qt5 are optional.
3. OR launch with `lector/__main__.py`
### Available packages
* [AUR](https://aur.archlinux.org/packages/lector-git/)
* [AUR - Releases](https://aur.archlinux.org/packages/lector/)
* [AUR - Git](https://aur.archlinux.org/packages/lector-git/)
* [Gentoo (unofficial)](https://bitbucket.org/szymonsz/gen2-overlay/src/master/app-text/lector/)
* [Fedora (unofficial)](https://copr.fedorainfracloud.org/coprs/bugzy/lector/)
* [openSUSE](https://software.opensuse.org/package/lector)
## Translations
1. There is a `SAMPLE.ts` file [here](https://github.com/BasioMeusPuga/Lector/tree/master/lector/resources/translations). Open it in `Qt Linguist`.
@@ -48,34 +72,37 @@ Please keep the translations short. There's only so much space for UI elements.
## Screenshots
### Main window
![alt tag](https://i.imgur.com/yrv2c0a.png)
![alt tag](https://i.imgur.com/516hRkS.png)
### Table view
![alt tag](https://i.imgur.com/b1XdXqP.png)
![alt tag](https://i.imgur.com/o9An7AR.png)
### Book reading view
![alt tag](https://i.imgur.com/Tei6TqF.png)
![alt tag](https://i.imgur.com/ITG63Fc.png)
### Distraction free view
![alt tag](https://i.imgur.com/g8Ltupy.png)
### Annotation support
![alt tag](https://i.imgur.com/gLK29F4.png)
### Comic reading view
![alt tag](https://i.imgur.com/U5JR35g.png)
![alt tag](https://i.imgur.com/rvvTQCM.png)
### Bookmark support
![alt tag](https://i.imgur.com/RZkmCzG.png)
![alt tag](https://i.imgur.com/Y7qoU8m.png)
### View profiles
![alt tag](https://i.imgur.com/gkJ88pi.png)
![alt tag](https://i.imgur.com/awE2q2K.png)
### Metadata editor
![alt tag](https://i.imgur.com/AqQREBf.png)
![alt tag](https://i.imgur.com/0CDpNO8.png)
### In program dictionary
![alt tag](https://i.imgur.com/Vh9xQUC.png)
![alt tag](https://i.imgur.com/RF72m2h.png)
## Reporting issues
When reporting issues:
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
### Settings window
![alt tag](https://i.imgur.com/l6zJXaH.png)
## Attributions
* [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack)

61
TODO
View File

@@ -2,7 +2,9 @@ TODO
General:
✓ Internationalization
✓ Application icon
✓ .desktop file
✓ .desktop file
✓ Shift to logging instead of print statements
Flatpak and AppImage support
Options:
✓ Automatic library management
✓ Recursive file addition
@@ -30,9 +32,14 @@ TODO
✓ Allow editing of database data through the UI + for Bookmarks
✓ Include (action) icons with the applications
✓ Drag and drop support for the library
✓ Tab reordering
Additional Settings:
✓ Create covers for books without them - VERY SLOW
Set focus to newly added file
Reading:
✓ Navbar
✓ 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
@@ -58,50 +65,78 @@ TODO
✓ Paragraph indentation
✓ Comic view keyboard shortcuts
✓ Comic view context menu
✓ Image rotation
✓ Make the bookmark dock float over the reading area
✓ Spacebar should not cut off lines at the top
✓ Track open bookmark windows so they can be closed quickly at exit
✓ Search document using QTextCursor
✓ Double page / column view
✓ For comics
Caching is currently non functional
Annotations
✓ Text
Annotation preview in listView
Image
✓ Disable buttons for annotations, search in images
Adjust key navigation according to viewport dimensions
Search document using QTextCursor
Filetypes:
✓ pdf support
Parse TOC
Parse TOC
✓ epub support
✓ Homegrown solution please
✓ cbz, cbr support
✓ Keep font settings enabled but only for background color
✓ Double page view
✓ Manga mode
✓ mobi, azw support
Limit the extra files produced by KindleUnpack
Have them save to memory
✓ fb2 support
✓ Images need to show up in their placeholders
✓ djvu support
✓ markdown support
Other:
✓ Define every widget in code
Bugs:
Deselecting all directories in the settings dialog also filters out manually added books
Clean up 'switch' page layout
Colors aren't loaded properly for annotation previews
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
Drag and drop is acting out
Search and annotation buttons become visible when font settings are hidden in comics
Secondary:
Auto switch between flow and single page mode
Text to speech
Definitions dialog needs to respond to escape
Zoom slider for comics / library
Tab tooltip
Additional Settings:
Find definitions on Google
Disable progressbar - 20% book addition speed improvement
Disable cover loading when reading - Saves ~2M / book
Special formatting for each chapter's title
Signal end of chapter with some text
Graphical themes
Change focus rectangle dimensions
Tab reordering
Universal Ctrl + Tab
Allow tabs to detach and form their own windows
Goodreads API: Ratings, Read, Recommendations
Get ISBN using python-isbnlib
Pagination
Use embedded fonts + CSS
Scrolling: Smooth / By Line
Shift to logging instead of print statements
txt, doc, chm, djvu, fb2 support
txt, doc, chm support
Include icons for filetype emblems
Comic view modes
Continuous paging
Double pages
Ignore a / the / numbers for sorting purposes
? Add only one file type if multiple are present
? Create emblem per filetype
In application notifications
Notification in case the filter is filtering out all files with no option in place
Option to fit images to viewport
Need help with:
Double page view for books
Scrolling: Smooth / By Line
Annotation preview in listView
Pagination

View File

@@ -1,7 +1,7 @@
{
"app-id":"com.basiomeuspuga.Lector",
"runtime":"org.kde.Platform",
"runtime-version":"5.10",
"runtime-version":"5.12",
"sdk":"org.kde.Sdk",
"command":"lector",
"rename-icon":"Lector",
@@ -21,74 +21,107 @@
},
"modules":[
{
"name": "python",
"sources": [
{
"type": "archive",
"url": "https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tar.xz",
"sha256": "159b932bf56aeaa76fd66e7420522d8c8853d486b8567c459b84fe2ed13bcaba"
}
]
},
{
"name": "pyqt5",
"name": "PyQt5",
"buildsystem": "simple",
"build-commands": [
"pip3 install --prefix=/app PyQt5-5.10.1-5.10.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
"pip3 install --prefix=/app PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
],
"modules":[
{
"name":"sip",
"name":"PyQt5-sip",
"sources":[
{
"type":"file",
"url":"https://pypi.python.org/packages/8a/ea/d317ce5696dda4df7c156cd60447cda22833b38106c98250eae1451f03ec/sip-4.19.8-cp36-cp36m-manylinux1_x86_64.whl",
"sha256":"cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85"
"url":"https://files.pythonhosted.org/packages/ae/9c/74fba0b62a0756d214f9aded5b0184130f7866def7532fa68823f34feefa/PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl",
"sha256":"04bd0bb8b6f8fa03c2dfbdfff0c8c9bfb3f46a21dd4cac73983dae93bf949523"
}
],
"buildsystem":"simple",
"build-commands":[
"pip3 install --prefix=/app sip-4.19.8-cp36-cp36m-manylinux1_x86_64.whl"
"pip3 install --prefix=/app PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl"
]
}
],
"sources": [
{
"type": "file",
"url": "https://pypi.python.org/packages/e4/15/4e2e49f64884edbab6f833c6fd3add24d7938f2429aec1f2883e645d4d8f/PyQt5-5.10.1-5.10.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl",
"sha256": "1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac"
"url": "https://files.pythonhosted.org/packages/5e/91/9ac8827d0af428e756f461a3aa7bcbc53d6450edfe026e27569f5ff3689e/PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl",
"sha256": "fd5946795b39922f971cf92dec799aadc7544b7fa993a79b9f6059f13d817e6e"
}
]
},
{
"name":"beautifulsoup",
"name":"beautifulsoup4",
"buildsystem":"simple",
"sources":[
{
"type":"archive",
"url":"https://pypi.python.org/packages/fa/8d/1d14391fdaed5abada4e0f63543fef49b8331a34ca60c88bd521bcf7f782/beautifulsoup4-4.6.0.tar.gz",
"sha256":"808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89"
"type":"file",
"url":"https://files.pythonhosted.org/packages/1d/5d/3260694a59df0ec52f8b4883f5d23b130bc237602a1411fa670eae12351e/beautifulsoup4-4.7.1-py3-none-any.whl",
"sha256":"034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858"
}
],
"modules":[
{
"name": "soupsieve",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/bf/b3/2473abf05c4950c6a829ed5dcbc40d8b56d4351d15d6939c8ffb7c6b1a14/soupsieve-1.7.3-py2.py3-none-any.whl",
"sha256":"466910df7561796a60748826781ebe9a888f7a1668a636ae86783f44d10aae73"
}
],
"buildsystem":"simple",
"build-commands":[
"pip3 install --prefix=/app soupsieve-1.7.3-py2.py3-none-any.whl"
]
}
],
"build-commands":[
"python3 setup.py build",
"python3 setup.py install --prefix=/app"
"pip3 install --prefix=/app beautifulsoup4-4.7.1-py3-none-any.whl"
]
},
{
"name": "lxml",
"buildsystem": "simple",
{
"name":"xmltodict",
"buildsystem":"simple",
"sources":[
{
"type": "file",
"url":"https://files.pythonhosted.org/packages/28/fd/30d5c1d3ac29ce229f6bdc40bbc20b28f716e8b363140c26eff19122d8a5/xmltodict-0.12.0-py2.py3-none-any.whl",
"sha256":"8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"
}
],
"build-commands":[
"pip3 install --prefix=/app xmltodict-0.12.0-py2.py3-none-any.whl"
]
},
{
"name":"PyMuPDF",
"buildsystem":"simple",
"build-commands": [
"pip3 install --prefix=/app lxml-4.2.1-cp36-cp36m-manylinux1_x86_64.whl"
"pip3 install --prefix=/app PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl"
],
"sources": [
"sources":[
{
"type": "file",
"url": "https://pypi.python.org/packages/a7/b9/ccf46cea0f698b40bca2a9c1a44039c336fe1988b82de4f7353be7a8396a/lxml-4.2.1-cp36-cp36m-manylinux1_x86_64.whl",
"sha256": "0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b"
"url": "https://files.pythonhosted.org/packages/3c/df/4bfaee2631b505d502c2ba64aa437799f0a64125edb1d4c4c38044ad1ecc/PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl",
"sha256": "a49798b58cce00e09b8a4431a5f64a400b11a0959f29507187c471208ce040a5"
}
]
},
{
"name":"lxml",
"buildsystem":"simple",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/08/f2/04bf04e42c070f65b64dbde02d2c94851251f19f5e9f803cc8f8bc61ac77/lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl",
"sha256":"c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a"
}
],
"build-commands":[
"pip3 install --prefix=/app lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl"
]
},
{
"name":"lector",
"buildsystem":"simple",

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi
import os
__path__ = ["lib", os.path.dirname(__file__), "kindleunpack"]
__path__ = ["lib", os.path.dirname(os.path.realpath(__file__)), "kindleunpack"]
import sys
import codecs
@@ -140,6 +140,8 @@ if PY2:
# 0.76 pre-release version only fix name related issues in opf by not using original file name in mobi7
# 0.77 bug fix for unpacking HDImages with included Fonts
# 0.80 converted to work with both python 2.7 and Python 3.3 and later
# 0.81 various fixes
# 0.82 Handle calibre-generated mobis that can have skeletons with no fragments
DUMP = False
""" Set to True to dump all possible information. """
@@ -847,7 +849,7 @@ def process_all_mobi_headers(files, apnxfile, sect, mhlst, K8Boundary, k8only=Fa
return
def unpackBook(infile, outdir, apnxfile=None, epubver='2', use_hd=True, dodump=False, dowriteraw=False, dosplitcombos=False):
def unpackBook(infile, outdir, apnxfile=None, epubver='2', use_hd=False, dodump=False, dowriteraw=False, dosplitcombos=False):
global DUMP
global WRITE_RAW_DATA
global SPLIT_COMBO_MOBIS
@@ -949,7 +951,7 @@ def main(argv=unicode_argv()):
global WRITE_RAW_DATA
global SPLIT_COMBO_MOBIS
print("KindleUnpack v0.80")
print("KindleUnpack v0.82")
print(" Based on initial mobipocket version Copyright © 2009 Charles M. Hannum <root@ihack.net>")
print(" Extensive Extensions and Improvements Copyright © 2009-2014 ")
print(" by: P. Durrant, K. Hendricks, S. Siebert, fandrieu, DiapDealer, nickredding, tkeo.")

View File

@@ -180,9 +180,11 @@ class K8Processor:
fragptr = 0
baseptr = 0
cnt = 0
filename = 'part%04d.xhtml' % cnt
for [skelnum, skelname, fragcnt, skelpos, skellen] in self.skeltbl:
baseptr = skelpos + skellen
skeleton = text[skelpos: baseptr]
aidtext = "0"
for i in range(fragcnt):
[insertpos, idtext, filenum, seqnum, startpos, length] = self.fragtbl[fragptr]
aidtext = idtext[12:-2]

View File

@@ -273,7 +273,7 @@ class OPFProcessor(object):
del metadata['CoverOffset']
handleMetaPairs(data, metadata, 'Codec', 'output encoding')
# handle kindlegen specifc tags
# handle kindlegen specific tags
handleTag(data, metadata, 'DictInLanguage', 'DictionaryInLanguage')
handleTag(data, metadata, 'DictOutLanguage', 'DictionaryOutLanguage')
handleMetaPairs(data, metadata, 'RegionMagnification', 'RegionMagnification')

View File

@@ -1,525 +0,0 @@
#! /usr/bin/python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# this program works in concert with the output from KindleUnpack
'''
Convert from Mobi ML to XHTML
'''
import os
import sys
import re
SPECIAL_HANDLING_TAGS = {
'?xml' : ('xmlheader', -1),
'!--' : ('comment', -3),
'!DOCTYPE' : ('doctype', -1),
}
SPECIAL_HANDLING_TYPES = ['xmlheader', 'doctype', 'comment']
SELF_CLOSING_TAGS = ['br' , 'hr', 'input', 'img', 'image', 'meta', 'spacer', 'link', 'frame', 'base', 'col', 'reference']
class MobiMLConverter(object):
PAGE_BREAK_PAT = re.compile(r'(<[/]{0,1}mbp:pagebreak\s*[/]{0,1}>)+', re.IGNORECASE)
IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex')
def __init__(self, filename):
self.base_css_rules = 'blockquote { margin: 0em 0em 0em 1.25em }\n'
self.base_css_rules += 'p { margin: 0em }\n'
self.base_css_rules += '.bold { font-weight: bold }\n'
self.base_css_rules += '.italic { font-style: italic }\n'
self.base_css_rules += '.mbp_pagebreak { page-break-after: always; margin: 0; display: block }\n'
self.tag_css_rules = {}
self.tag_css_rule_cnt = 0
self.path = []
self.filename = filename
self.wipml = open(self.filename, 'rb').read()
self.pos = 0
self.opfname = self.filename.rsplit('.',1)[0] + '.opf'
self.opos = 0
self.meta = ''
self.cssname = os.path.join(os.path.dirname(self.filename),'styles.css')
self.current_font_size = 3
self.font_history = []
def cleanup_html(self):
self.wipml = re.sub(r'<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.wipml)
self.wipml = self.wipml.replace('\r\n', '\n')
self.wipml = self.wipml.replace('> <', '>\n<')
self.wipml = self.wipml.replace('<mbp: ', '<mbp:')
# self.wipml = re.sub(r'<?xml[^>]*>', '', self.wipml)
self.wipml = self.wipml.replace('<br></br>','<br/>')
def replace_page_breaks(self):
self.wipml = self.PAGE_BREAK_PAT.sub(
'<div class="mbp_pagebreak" />',
self.wipml)
# parse leading text of ml and tag
def parseml(self):
p = self.pos
if p >= len(self.wipml):
return None
if self.wipml[p] != '<':
res = self.wipml.find('<',p)
if res == -1 :
res = len(self.wipml)
self.pos = res
return self.wipml[p:res], None
# handle comment as a special case to deal with multi-line comments
if self.wipml[p:p+4] == '<!--':
te = self.wipml.find('-->',p+1)
if te != -1:
te = te+2
else :
te = self.wipml.find('>',p+1)
ntb = self.wipml.find('<',p+1)
if ntb != -1 and ntb < te:
self.pos = ntb
return self.wipml[p:ntb], None
self.pos = te + 1
return None, self.wipml[p:te+1]
# parses string version of tag to identify its name,
# its type 'begin', 'end' or 'single',
# plus build a hashtable of its attributes
# code is written to handle the possiblity of very poor formating
def parsetag(self, s):
p = 1
# get the tag name
tname = None
ttype = None
tattr = {}
while s[p:p+1] == ' ' :
p += 1
if s[p:p+1] == '/':
ttype = 'end'
p += 1
while s[p:p+1] == ' ' :
p += 1
b = p
while s[p:p+1] not in ('>', '/', ' ', '"', "'", "\r", "\n") :
p += 1
tname=s[b:p].lower()
if tname == '!doctype':
tname = '!DOCTYPE'
# special cases
if tname in SPECIAL_HANDLING_TAGS.keys():
ttype, backstep = SPECIAL_HANDLING_TAGS[tname]
tattr['special'] = s[p:backstep]
if ttype is None:
# parse any attributes
while s.find('=',p) != -1 :
while s[p:p+1] == ' ' :
p += 1
b = p
while s[p:p+1] != '=' :
p += 1
aname = s[b:p].lower()
aname = aname.rstrip(' ')
p += 1
while s[p:p+1] == ' ' :
p += 1
if s[p:p+1] in ('"', "'") :
p = p + 1
b = p
while s[p:p+1] not in ('"', "'") :
p += 1
val = s[b:p]
p += 1
else :
b = p
while s[p:p+1] not in ('>', '/', ' ') :
p += 1
val = s[b:p]
tattr[aname] = val
# label beginning and single tags
if ttype is None:
ttype = 'begin'
if s.find(' /',p) >= 0:
ttype = 'single_ext'
elif s.find('/',p) >= 0:
ttype = 'single'
return ttype, tname, tattr
# main routine to convert from mobi markup language to html
def processml(self):
# are these really needed
html_done = False
head_done = False
body_done = False
skip = False
htmlstr = ''
self.replace_page_breaks()
self.cleanup_html()
# now parse the cleaned up ml into standard xhtml
while True:
r = self.parseml()
if not r:
break
text, tag = r
if text:
if not skip:
htmlstr += text
if tag:
ttype, tname, tattr = self.parsetag(tag)
# If we run into a DTD or xml declarations inside the body ... bail.
if tname in SPECIAL_HANDLING_TAGS.keys() and tname != 'comment' and body_done:
htmlstr += '\n</body></html>'
break
# make sure self-closing tags actually self-close
if ttype == 'begin' and tname in SELF_CLOSING_TAGS:
ttype = 'single'
# make sure any end tags of self-closing tags are discarded
if ttype == 'end' and tname in SELF_CLOSING_TAGS:
continue
# remove embedded guide and refernces from old mobis
if tname in ('guide', 'ncx', 'reference') and ttype in ('begin', 'single', 'single_ext'):
tname = 'removeme:{0}'.format(tname)
tattr = None
if tname in ('guide', 'ncx', 'reference', 'font', 'span') and ttype == 'end':
if self.path[-1] == 'removeme:{0}'.format(tname):
tname = 'removeme:{0}'.format(tname)
tattr = None
# Get rid of font tags that only have a color attribute.
if tname == 'font' and ttype in ('begin', 'single', 'single_ext'):
if 'color' in tattr.keys() and len(tattr.keys()) == 1:
tname = 'removeme:{0}'.format(tname)
tattr = None
# Get rid of empty spans in the markup.
if tname == 'span' and ttype in ('begin', 'single', 'single_ext') and not len(tattr):
tname = 'removeme:{0}'.format(tname)
# need to handle fonts outside of the normal methods
# so fonts tags won't be added to the self.path since we keep track
# of font tags separately with self.font_history
if tname == 'font' and ttype == 'begin':
# check for nested font start tags
if len(self.font_history) > 0 :
# inject a font end tag
taginfo = ('end', 'font', None)
htmlstr += self.processtag(taginfo)
self.font_history.append((ttype, tname, tattr))
# handle the current font start tag
taginfo = (ttype, tname, tattr)
htmlstr += self.processtag(taginfo)
continue
# check for nested font tags and unnest them
if tname == 'font' and ttype == 'end':
self.font_history.pop()
# handle this font end tag
taginfo = ('end', 'font', None)
htmlstr += self.processtag(taginfo)
# check if we were nested
if len(self.font_history) > 0:
# inject a copy of the most recent font start tag from history
taginfo = self.font_history[-1]
htmlstr += self.processtag(taginfo)
continue
# keep track of nesting path
if ttype == 'begin':
self.path.append(tname)
elif ttype == 'end':
if tname != self.path[-1]:
print ('improper nesting: ', self.path, tname, ttype)
if tname not in self.path:
# handle case of end tag with no beginning by injecting empty begin tag
taginfo = ('begin', tname, None)
htmlstr += self.processtag(taginfo)
print(" - fixed by injecting empty start tag ", tname)
self.path.append(tname)
elif len(self.path) > 1 and tname == self.path[-2]:
# handle case of dangling missing end
taginfo = ('end', self.path[-1], None)
htmlstr += self.processtag(taginfo)
print(" - fixed by injecting end tag ", self.path[-1])
self.path.pop()
self.path.pop()
if tname == 'removeme:{0}'.format(tname):
if ttype in ('begin', 'single', 'single_ext'):
skip = True
else:
skip = False
else:
taginfo = (ttype, tname, tattr)
htmlstr += self.processtag(taginfo)
# handle potential issue of multiple html, head, and body sections
if tname == 'html' and ttype == 'begin' and not html_done:
htmlstr += '\n'
html_done = True
if tname == 'head' and ttype == 'begin' and not head_done:
htmlstr += '\n'
# also add in metadata and style link tags
htmlstr += self.meta
htmlstr += '<link href="styles.css" rel="stylesheet" type="text/css" />\n'
head_done = True
if tname == 'body' and ttype == 'begin' and not body_done:
htmlstr += '\n'
body_done = True
# handle issue of possibly missing html, head, and body tags
# I have not seen this but the original did something like this so ...
if not body_done:
htmlstr = '<body>\n' + htmlstr + '</body>\n'
if not head_done:
headstr = '<head>\n'
headstr += self.meta
headstr += '<link href="styles.css" rel="stylesheet" type="text/css" />\n'
headstr += '</head>\n'
htmlstr = headstr + htmlstr
if not html_done:
htmlstr = '<html>\n' + htmlstr + '</html>\n'
# finally add DOCTYPE info
htmlstr = '<?xml version="1.0"?>\n<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n' + htmlstr
css = self.base_css_rules
for cls, rule in self.tag_css_rules.items():
css += '.%s { %s }\n' % (cls, rule)
return (htmlstr, css, self.cssname)
def ensure_unit(self, raw, unit='px'):
if re.search(r'\d+$', raw) is not None:
raw += unit
return raw
# flatten possibly modified tag back to string
def taginfo_tostring(self, taginfo):
(ttype, tname, tattr) = taginfo
if ttype is None or tname is None:
return ''
if ttype == 'end':
return '</%s>' % tname
if ttype in SPECIAL_HANDLING_TYPES and tattr is not None and 'special' in tattr.keys():
info = tattr['special']
if ttype == 'comment':
return '<%s %s-->' % tname, info
else:
return '<%s %s>' % tname, info
res = []
res.append('<%s' % tname)
if tattr is not None:
for key in tattr.keys():
res.append(' %s="%s"' % (key, tattr[key]))
if ttype == 'single':
res.append('/>')
elif ttype == 'single_ext':
res.append(' />')
else :
res.append('>')
return "".join(res)
# routines to convert from mobi ml tags atributes to xhtml attributes and styles
def processtag(self, taginfo):
# Converting mobi font sizes to numerics
size_map = {
'xx-small': '1',
'x-small': '2',
'small': '3',
'medium': '4',
'large': '5',
'x-large': '6',
'xx-large': '7',
}
size_to_em_map = {
'1': '.65em',
'2': '.75em',
'3': '1em',
'4': '1.125em',
'5': '1.25em',
'6': '1.5em',
'7': '2em',
}
# current tag to work on
(ttype, tname, tattr) = taginfo
if not tattr:
tattr = {}
styles = []
if tname is None or tname.startswith('removeme'):
return ''
# have not seen an example of this yet so keep it here to be safe
# until this is better understood
if tname in ('country-region', 'place', 'placetype', 'placename',
'state', 'city', 'street', 'address', 'content'):
tname = 'div' if tname == 'content' else 'span'
for key in tattr.keys():
tattr.pop(key)
# handle general case of style, height, width, bgcolor in any tag
if 'style' in tattr.keys():
style = tattr.pop('style').strip()
if style:
styles.append(style)
if 'align' in tattr.keys():
align = tattr.pop('align').strip()
if align:
if tname in ('table', 'td', 'tr'):
pass
else:
styles.append('text-align: %s' % align)
if 'height' in tattr.keys():
height = tattr.pop('height').strip()
if height and '<' not in height and '>' not in height and re.search(r'\d+', height):
if tname in ('table', 'td', 'tr'):
pass
elif tname == 'img':
tattr['height'] = height
else:
styles.append('margin-top: %s' % self.ensure_unit(height))
if 'width' in tattr.keys():
width = tattr.pop('width').strip()
if width and re.search(r'\d+', width):
if tname in ('table', 'td', 'tr'):
pass
elif tname == 'img':
tattr['width'] = width
else:
styles.append('text-indent: %s' % self.ensure_unit(width))
if width.startswith('-'):
styles.append('margin-left: %s' % self.ensure_unit(width[1:]))
if 'bgcolor' in tattr.keys():
# no proprietary html allowed
if tname == 'div':
del tattr['bgcolor']
elif tname == 'font':
# Change font tags to span tags
tname = 'span'
if ttype in ('begin', 'single', 'single_ext'):
# move the face attribute to css font-family
if 'face' in tattr.keys():
face = tattr.pop('face').strip()
styles.append('font-family: "%s"' % face)
# Monitor the constantly changing font sizes, change them to ems and move
# them to css. The following will work for 'flat' font tags, but nested font tags
# will cause things to go wonky. Need to revert to the parent font tag's size
# when a closing tag is encountered.
if 'size' in tattr.keys():
sz = tattr.pop('size').strip().lower()
try:
float(sz)
except ValueError:
if sz in size_map.keys():
sz = size_map[sz]
else:
if sz.startswith('-') or sz.startswith('+'):
sz = self.current_font_size + float(sz)
if sz > 7:
sz = 7
elif sz < 1:
sz = 1
sz = str(int(sz))
styles.append('font-size: %s' % size_to_em_map[sz])
self.current_font_size = int(sz)
elif tname == 'img':
for attr in ('width', 'height'):
if attr in tattr:
val = tattr[attr]
if val.lower().endswith('em'):
try:
nval = float(val[:-2])
nval *= 16 * (168.451/72) # Assume this was set using the Kindle profile
tattr[attr] = "%dpx"%int(nval)
except:
del tattr[attr]
elif val.lower().endswith('%'):
del tattr[attr]
# convert the anchor tags
if 'filepos-id' in tattr:
tattr['id'] = tattr.pop('filepos-id')
if 'name' in tattr and tattr['name'] != tattr['id']:
tattr['name'] = tattr['id']
if 'filepos' in tattr:
filepos = tattr.pop('filepos')
try:
tattr['href'] = "#filepos%d" % int(filepos)
except ValueError:
pass
if styles:
ncls = None
rule = '; '.join(styles)
for sel, srule in self.tag_css_rules.items():
if srule == rule:
ncls = sel
break
if ncls is None:
self.tag_css_rule_cnt += 1
ncls = 'rule_%d' % self.tag_css_rule_cnt
self.tag_css_rules[ncls] = rule
cls = tattr.get('class', '')
cls = cls + (' ' if cls else '') + ncls
tattr['class'] = cls
# convert updated tag back to string representation
if len(tattr) == 0:
tattr = None
taginfo = (ttype, tname, tattr)
return self.taginfo_tostring(taginfo)
''' main only left in for testing outside of plugin '''
def main(argv=sys.argv):
if len(argv) != 2:
return 1
else:
infile = argv[1]
try:
print('Converting Mobi Markup Language to XHTML')
mlc = MobiMLConverter(infile)
print('Processing ...')
htmlstr, css, cssname = mlc.processml()
outname = infile.rsplit('.',1)[0] + '_converted.html'
file(outname, 'wb').write(htmlstr)
file(cssname, 'wb').write(css)
print('Completed')
print('XHTML version of book can be found at: ', outname)
except ValueError as e:
print("Error: %s" % e)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -31,6 +31,13 @@ sys.path.append(str(install_dir))
from PyQt5 import QtWidgets, QtGui, QtCore
# Init logging
# Must be done first and at the module level
# or it won't work properly in case of the imports below
from lector.logger import init_logging, VERSION
logger = init_logging(sys.argv)
logger.log(60, f'Lector {VERSION} - Application started')
from lector import database
from lector import sorter
from lector.toolbars import LibraryToolBar, BookToolBar
@@ -78,7 +85,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.comic_profile = {}
self.database_path = None
self.active_library_filters = []
self.active_bookmark_docks = []
self.active_docks = []
# Initialize application
Settings(self).read_settings() # This should populate all variables that need
@@ -120,6 +127,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
# Statusbar widgets
self.statusMessage.setObjectName('statusMessage')
self.statusBar.addPermanentWidget(self.statusMessage)
self.errorButton = QtWidgets.QPushButton(self.statusBar)
self.errorButton.setIcon(QtGui.QIcon(':/images/error.svg'))
self.errorButton.setFlat(True)
self.errorButton.setVisible(False)
self.errorButton.setToolTip('What hast thou done?')
self.errorButton.clicked.connect(self.show_errors)
self.statusBar.addPermanentWidget(self.errorButton)
self.sorterProgress = QtWidgets.QProgressBar()
self.sorterProgress.setMaximumWidth(300)
self.sorterProgress.setObjectName('sorterProgress')
@@ -158,7 +172,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.libraryToolBar.reloadLibraryButton.triggered.connect(
self.settingsDialog.start_library_scan)
self.libraryToolBar.colorButton.triggered.connect(self.get_color)
self.libraryToolBar.settingsButton.triggered.connect(self.show_settings)
self.libraryToolBar.settingsButton.triggered.connect(
lambda: self.show_settings(0))
self.libraryToolBar.aboutButton.triggered.connect(
lambda: self.show_settings(3))
self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodels)
self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodels)
self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup)
@@ -171,11 +188,30 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.libraryToolBar.tableViewButton.trigger()
# Book toolbar
self.bookToolBar.annotationButton.triggered.connect(self.toggle_dock_widgets)
self.bookToolBar.addBookmarkButton.triggered.connect(self.add_bookmark)
self.bookToolBar.bookmarkButton.triggered.connect(self.toggle_dock_widgets)
self.bookToolBar.distractionFreeButton.triggered.connect(self.toggle_distraction_free)
self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)
self.bookToolBar.addBookmarkButton.triggered.connect(
lambda: self.tabWidget.currentWidget().sideDock.bookmarks.add_bookmark())
self.bookToolBar.bookmarkButton.triggered.connect(
lambda: self.tabWidget.currentWidget().toggle_side_dock(0))
self.bookToolBar.annotationButton.triggered.connect(
lambda: self.tabWidget.currentWidget().toggle_side_dock(1))
self.bookToolBar.searchButton.triggered.connect(
lambda: self.tabWidget.currentWidget().toggle_side_dock(2))
self.bookToolBar.distractionFreeButton.triggered.connect(
self.toggle_distraction_free)
self.bookToolBar.fullscreenButton.triggered.connect(
lambda: self.tabWidget.currentWidget().go_fullscreen())
self.bookToolBar.doublePageButton.triggered.connect(self.change_page_view)
self.bookToolBar.mangaModeButton.triggered.connect(self.change_page_view)
self.bookToolBar.invertButton.triggered.connect(self.change_page_view)
self.bookToolBar.rotateRightButton.triggered.connect(self.change_page_view)
self.bookToolBar.rotateLeftButton.triggered.connect(self.change_page_view)
if self.settings['double_page_mode']:
self.bookToolBar.doublePageButton.setChecked(True)
if self.settings['manga_mode']:
self.bookToolBar.mangaModeButton.setChecked(True)
if self.settings['invert_colors']:
self.bookToolBar.invertButton.setChecked(True)
for count, i in enumerate(self.display_profiles):
self.bookToolBar.profileBox.setItemData(count, i, QtCore.Qt.UserRole)
@@ -199,12 +235,18 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
i[1].triggered.connect(self.modify_font)
self.alignment_dict[current_profile['text_alignment']].setChecked(True)
self.bookToolBar.zoomIn.triggered.connect(self.modify_comic_view)
self.bookToolBar.zoomOut.triggered.connect(self.modify_comic_view)
self.bookToolBar.fitWidth.triggered.connect(self.modify_comic_view)
self.bookToolBar.bestFit.triggered.connect(self.modify_comic_view)
self.bookToolBar.originalSize.triggered.connect(self.modify_comic_view)
self.bookToolBar.comicBGColor.clicked.connect(self.get_color)
self.bookToolBar.zoomIn.triggered.connect(
self.modify_comic_view)
self.bookToolBar.zoomOut.triggered.connect(
self.modify_comic_view)
self.bookToolBar.fitWidth.triggered.connect(
lambda: self.modify_comic_view(False))
self.bookToolBar.bestFit.triggered.connect(
lambda: self.modify_comic_view(False))
self.bookToolBar.originalSize.triggered.connect(
lambda: self.modify_comic_view(False))
self.bookToolBar.comicBGColor.clicked.connect(
self.get_color)
self.bookToolBar.colorBoxFG.clicked.connect(self.get_color)
self.bookToolBar.colorBoxBG.clicked.connect(self.get_color)
@@ -216,16 +258,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.tab_switch()
self.tabWidget.currentChanged.connect(self.tab_switch)
# Tab closing
# Tab Widget formatting
self.tabWidget.setTabsClosable(True)
self.tabWidget.setDocumentMode(True)
self.tabWidget.tabBarClicked.connect(self.tab_disallow_library_movement)
# Get list of available parsers
self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers)
print('Available parsers: ' + self.available_parsers)
logger.log(60, 'Available parsers: ' + self.available_parsers)
# The Library tab gets no button
self.tabWidget.tabBar().setTabButton(
0, QtWidgets.QTabBar.RightSide, None)
self.tabWidget.widget(0).is_library = True
self.tabWidget.tabCloseRequested.connect(self.tab_close)
self.tabWidget.setTabBarAutoHide(True)
@@ -272,9 +317,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
for count, i in enumerate(self.settings['main_window_headers']):
self.tableView.horizontalHeader().resizeSection(count, int(i))
self.tableView.horizontalHeader().resizeSection(5, 30)
self.tableView.horizontalHeader().setStretchLastSection(False)
self.tableView.horizontalHeader().setStretchLastSection(True)
self.tableView.horizontalHeader().sectionClicked.connect(
self.lib_ref.tableProxyModel.sort_table_columns)
self.lib_ref.tableProxyModel.sort_table_columns(2)
self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.tableView.customContextMenuRequested.connect(
self.generate_library_context_menu)
@@ -330,15 +376,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
def process_post_hoc_files(self, file_list, open_files_after_processing):
# Takes care of both dragged and dropped files
# As well as files sent as command line arguments
file_list = [i for i in file_list if os.path.exists(i)]
if not file_list:
return
books = sorter.BookSorter(
file_list,
('addition', 'manual'),
self.database_path,
self.settings['auto_tags'],
self.settings,
self.temp_dir.path())
parsed_books = books.initiate_threads()
if not parsed_books:
parsed_books, errors = books.initiate_threads()
if not parsed_books and not open_files_after_processing:
return
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
@@ -348,14 +398,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if open_files_after_processing:
self.open_files(file_dict)
self.move_on()
self.move_on(errors)
def open_files(self, path_hash_dictionary):
# file_paths is expected to be a dictionary
# This allows for threading file opening
# Which should speed up multiple file opening
# especially @ application start
file_paths = [i for i in path_hash_dictionary]
for filename in path_hash_dictionary.items():
@@ -382,30 +431,32 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if not file_paths:
return
def finishing_touches():
self.profile_functions.format_contentView()
self.start_culling_timer()
logger.info(
'Attempting to open: ' + ', '.join(file_paths))
print('Attempting to open: ' + ', '.join(file_paths))
contents = sorter.BookSorter(
contents, errors = sorter.BookSorter(
file_paths,
('reading', None),
self.database_path,
True,
self.settings,
self.temp_dir.path()).initiate_threads()
# TODO
# Notification feedback in case all books return nothing
if errors:
self.display_error_notification(errors)
if not contents:
logger.error('No parseable files found')
return
successfully_opened = []
for i in contents:
# New tabs are created here
# Initial position adjustment is carried out by the tab itself
file_data = contents[i]
Tab(file_data, self)
successfully_opened.append(file_data['path'])
logger.info(
'Successfully opened: ' + ', '.join(file_paths))
if self.settings['last_open_tab'] == 'library':
self.tabWidget.setCurrentIndex(0)
@@ -418,61 +469,16 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if self.settings['last_open_tab'] == this_path:
self.tabWidget.setCurrentIndex(i)
self.settings['last_open_tab'] = None
finishing_touches()
return
self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)
finishing_touches()
def start_culling_timer(self):
if self.settings['perform_culling']:
self.culling_timer.start(30)
def add_bookmark(self):
if self.tabWidget.currentIndex() != 0:
self.tabWidget.widget(self.tabWidget.currentIndex()).add_bookmark()
def resizeEvent(self, event=None):
if event:
# This implies a vertical resize event only
# We ain't about that lifestyle
if event.oldSize().width() == event.size().width():
return
# The hackiness of this hack is just...
default_size = 170 # This is size of the QIcon (160 by default) +
# minimum margin is needed between thumbnails
# for n icons, the n + 1th icon will appear at > n +1.11875
# First, calculate the number of images per row
i = self.listView.viewport().width() / default_size
rem = i - int(i)
if rem >= .21875 and rem <= .9999:
num_images = int(i)
else:
num_images = int(i) - 1
# The rest is illustrated using informative variable names
space_occupied = num_images * default_size
# 12 is the scrollbar width
# Larger numbers keep reduce flickering but also increase
# the distance from the scrollbar
space_left = (
self.listView.viewport().width() - space_occupied - 19)
try:
layout_extra_space_per_image = space_left // num_images
self.listView.setGridSize(
QtCore.QSize(default_size + layout_extra_space_per_image, 250))
self.start_culling_timer()
except ZeroDivisionError: # Initial resize is ignored
return
def add_books(self):
dialog_prompt = self._translate('Main_UI', 'Add books to database')
ebooks_string = self._translate('Main_UI', 'eBooks')
opened_files = QtWidgets.QFileDialog.getOpenFileNames(
self, dialog_prompt, self.settings['last_open_path'],
f'{ebooks_string} ({self.available_parsers})')
f'{ebooks_string}({self.available_parsers})')
if not opened_files[0]:
return
@@ -486,18 +492,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.statusMessage.setText(self._translate('Main_UI', 'Adding books...'))
self.thread = BackGroundBookAddition(
opened_files[0], self.database_path, 'manual', self)
self.thread.finished.connect(self.move_on)
self.thread.finished.connect(
lambda: self.move_on(self.thread.errors))
self.thread.start()
def get_selection(self, library_widget):
def get_selection(self):
selected_indexes = None
if library_widget == self.listView:
if self.listView.isVisible():
selected_books = self.lib_ref.itemProxyModel.mapSelectionToSource(
self.listView.selectionModel().selection())
selected_indexes = [i.indexes()[0] for i in selected_books]
elif library_widget == self.tableView:
elif self.tableView.isVisible():
selected_books = self.tableView.selectionModel().selectedRows()
selected_indexes = [
self.lib_ref.tableProxyModel.mapToSource(i) for i in selected_books]
@@ -505,16 +512,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
return selected_indexes
def delete_books(self, selected_indexes=None):
if not selected_indexes:
# Get a list of QItemSelection objects
# What we're interested in is the indexes()[0] in each of them
# That gives a list of indexes from the view model
if self.listView.isVisible():
selected_indexes = self.get_selection(self.listView)
elif self.tableView.isVisible():
selected_indexes = self.get_selection(self.tableView)
# Get a list of QItemSelection objects
# What we're interested in is the indexes()[0] in each of them
# That gives a list of indexes from the view model
selected_indexes = self.get_selection()
if not selected_indexes:
return
@@ -528,7 +529,8 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
delete_hashes = [
self.lib_ref.libraryModel.data(
i, QtCore.Qt.UserRole + 6) for i in selected_indexes]
persistent_indexes = [QtCore.QPersistentModelIndex(i) for i in selected_indexes]
persistent_indexes = [
QtCore.QPersistentModelIndex(i) for i in selected_indexes]
for i in persistent_indexes:
self.lib_ref.libraryModel.removeRow(i.row())
@@ -540,10 +542,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.thread.start()
# Generate a message box to confirm deletion
selected_number = len(selected_indexes)
confirm_deletion = QtWidgets.QMessageBox()
deletion_prompt = self._translate(
'Main_UI', f'Delete {selected_number} book(s)?')
'Main_UI', f'Delete book(s)?')
confirm_deletion.setText(deletion_prompt)
confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion'))
@@ -557,7 +558,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if self.tabWidget.currentIndex() == 0:
self.delete_books()
def move_on(self):
def move_on(self, errors=None):
self.settingsDialog.okButton.setEnabled(True)
self.settingsDialog.okButton.setToolTip(
self._translate('Main_UI', 'Save changes and start library scan'))
@@ -566,8 +567,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.sorterProgress.setVisible(False)
self.sorterProgress.setValue(0)
if self.libraryToolBar.searchBar.text() == '':
self.statusBar.setVisible(False)
# The errors argument is a list and will only be present
# in case of addition and reading
if errors:
self.display_error_notification(errors)
else:
if self.libraryToolBar.searchBar.text() == '':
self.statusBar.setVisible(False)
self.lib_ref.update_proxymodels()
self.lib_ref.generate_library_tags()
@@ -591,6 +597,12 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
def tab_switch(self):
try:
# Disallow library tab movement
# Does not need to be looped since the library
# tab can only ever go to position 1
if not self.tabWidget.widget(0).is_library:
self.tabWidget.tabBar().moveTab(1, 0)
if self.current_tab != 0:
self.tabWidget.widget(
self.current_tab).update_last_accessed_time()
@@ -599,15 +611,15 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.current_tab = self.tabWidget.currentIndex()
# Hide bookmark and annotation widgets
# Hide all side docks whenever a tab is switched
for i in range(1, self.tabWidget.count()):
self.tabWidget.widget(i).bookmarkDock.setVisible(False)
self.tabWidget.widget(i).annotationDock.setVisible(False)
self.tabWidget.widget(i).sideDock.setVisible(False)
# If library
if self.tabWidget.currentIndex() == 0:
self.resizeEvent()
self.start_culling_timer()
if self.settings['show_bars']:
self.bookToolBar.hide()
self.libraryToolBar.show()
@@ -618,38 +630,39 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.statusMessage.setText(
str(self.lib_ref.itemProxyModel.rowCount()) +
self._translate('Main_UI', ' Books'))
else:
if self.libraryToolBar.searchBar.text() != '':
self.statusBar.setVisible(True)
else:
if self.settings['show_bars']:
self.bookToolBar.show()
self.libraryToolBar.hide()
current_tab = self.tabWidget.widget(
self.tabWidget.currentIndex())
current_metadata = current_tab.metadata
current_tab = self.tabWidget.currentWidget()
self.bookToolBar.tocBox.setModel(current_tab.tocModel)
self.bookToolBar.tocTreeView.expandAll()
current_tab.set_tocBox_index(None, None)
# Needed to set the contentView widget background
# on first run. Subsequent runs might be redundant,
# but it doesn't seem to visibly affect performance
self.profile_functions.format_contentView()
self.statusBar.setVisible(False)
if self.bookToolBar.fontButton.isChecked():
self.bookToolBar.customize_view_on()
current_title = current_metadata['title']
current_author = current_metadata['author']
current_position = current_metadata['position']
current_toc = [i[0] for i in current_metadata['content']]
self.bookToolBar.tocBox.blockSignals(True)
self.bookToolBar.tocBox.clear()
self.bookToolBar.tocBox.addItems(current_toc)
if current_position:
self.bookToolBar.tocBox.setCurrentIndex(
current_position['current_chapter'] - 1)
if not current_metadata['images_only']:
current_tab.hiddenButton.animateClick(25)
self.bookToolBar.tocBox.blockSignals(False)
self.profile_functions.format_contentView()
self.statusMessage.setText(
current_author + ' - ' + current_title)
else:
if current_tab.are_we_doing_images_only:
self.bookToolBar.searchButton.setVisible(False)
self.bookToolBar.annotationButton.setVisible(False)
self.bookToolBar.bookSeparator2.setVisible(False)
self.bookToolBar.bookSeparator3.setVisible(False)
else:
self.bookToolBar.searchButton.setVisible(True)
self.bookToolBar.annotationButton.setVisible(True)
self.bookToolBar.bookSeparator2.setVisible(True)
self.bookToolBar.bookSeparator3.setVisible(True)
def tab_close(self, tab_index=None):
if not tab_index:
@@ -669,35 +682,23 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.tabWidget.widget(tab_index).setParent(None)
gc.collect()
def tab_disallow_library_movement(self, tab_index):
# Makes the library tab immovable
if tab_index == 0:
self.tabWidget.setMovable(False)
else:
self.tabWidget.setMovable(True)
def set_toc_position(self, event=None):
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
currentIndex = self.bookToolBar.tocTreeView.currentIndex()
required_position = currentIndex.data(QtCore.Qt.UserRole)
if not required_position:
return # Initial startup might return a None
current_tab.metadata[
'position']['current_chapter'] = event + 1
current_tab.metadata[
'position']['is_read'] = False
# Go on to change the value of the Table of Contents box
current_tab.change_chapter_tocBox()
current_tab.contentView.record_position()
self.profile_functions.format_contentView()
def set_fullscreen(self):
current_tab = self.tabWidget.currentIndex()
current_tab_widget = self.tabWidget.widget(current_tab)
current_tab_widget.go_fullscreen()
def toggle_dock_widgets(self):
sender = self.sender()
current_tab = self.tabWidget.currentIndex()
current_tab_widget = self.tabWidget.widget(current_tab)
if sender == self.bookToolBar.bookmarkButton:
current_tab_widget.toggle_bookmarks()
if sender == self.bookToolBar.annotationButton:
current_tab_widget.toggle_annotations()
# The set_content method is universal
# It's going to do position tracking
current_tab = self.tabWidget.currentWidget()
current_tab.set_content(required_position, True, True)
def library_doubleclick(self, index):
sender = self.sender().objectName()
@@ -713,6 +714,20 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.open_files(path)
def display_error_notification(self, errors):
self.statusBar.setVisible(True)
self.errorButton.setVisible(True)
def show_errors(self):
# TODO
# Create a separate viewing area for errors
# before showing the log
self.show_settings(3)
self.settingsDialog.aboutTabWidget.setCurrentIndex(1)
self.errorButton.setVisible(False)
self.statusBar.setVisible(False)
def statusbar_visibility(self):
if self.sender() == self.libraryToolBar.searchBar:
if self.libraryToolBar.searchBar.text() == '':
@@ -720,13 +735,16 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
else:
self.statusBar.setVisible(True)
def show_settings(self):
def show_settings(self, stacked_widget_index):
if not self.settingsDialog.isVisible():
self.settingsDialog.show()
index = self.settingsDialog.listModel.index(
stacked_widget_index, 0)
self.settingsDialog.listView.setCurrentIndex(index)
else:
self.settingsDialog.hide()
#____________________________________________
#==================================================================
# The contentView modification functions are in the guifunctions
# module. self.profile_functions is the reference here.
@@ -743,10 +761,49 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
signal_sender = None
else:
signal_sender = self.sender().objectName()
self.profile_functions.modify_comic_view(
signal_sender, key_pressed)
#____________________________________________
#=================================================================
def change_page_view(self, key_pressed=False):
# Switch page to whatever index is selected in the tocBox
current_tab = self.tabWidget.currentWidget()
chapter_number = current_tab.metadata['position']['current_chapter']
# Set zoom mode to best fit to
# make the transition less jarring
# if the sender isn't the invert colors button
if self.sender() != self.bookToolBar.invertButton:
self.comic_profile['zoom_mode'] = 'bestFit'
# Rotate the image left or right
# The double page mode is incompatible with this
if self.sender() == self.bookToolBar.rotateLeftButton:
current_tab.generate_rotation(-90)
self.bookToolBar.doublePageButton.setChecked(False)
if self.sender() == self.bookToolBar.rotateRightButton:
current_tab.generate_rotation(90)
self.bookToolBar.doublePageButton.setChecked(False)
if self.sender() == self.bookToolBar.doublePageButton:
current_tab.image_rotation = 0
# Toggle Double page mode / manga mode on keypress
if key_pressed == QtCore.Qt.Key_D:
self.bookToolBar.doublePageButton.setChecked(
not self.bookToolBar.doublePageButton.isChecked())
if key_pressed == QtCore.Qt.Key_M:
self.bookToolBar.mangaModeButton.setChecked(
not self.bookToolBar.mangaModeButton.isChecked())
# Change settings according to the
# current state of each of the toolbar buttons
self.settings['double_page_mode'] = self.bookToolBar.doublePageButton.isChecked()
self.settings['manga_mode'] = self.bookToolBar.mangaModeButton.isChecked()
self.settings['invert_colors'] = self.bookToolBar.invertButton.isChecked()
current_tab.set_content(chapter_number, False)
def generate_library_context_menu(self, position):
index = self.sender().indexAt(position)
@@ -755,7 +812,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
# It's worth remembering that these are indexes of the libraryModel
# and NOT of the proxy models
selected_indexes = self.get_selection(self.sender())
selected_indexes = self.get_selection()
context_menu = QtWidgets.QMenu()
@@ -791,8 +848,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if action == editAction:
edit_book = selected_indexes[0]
metadata = self.lib_ref.libraryModel.data(
edit_book, QtCore.Qt.UserRole + 3)
is_cover_loaded = self.lib_ref.libraryModel.data(
edit_book, QtCore.Qt.UserRole + 8)
@@ -805,15 +860,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.database_path).fetch_covers_only([book_hash])[0][1]
self.cover_functions.cover_loader(book_item, book_cover)
cover = self.lib_ref.libraryModel.item(edit_book.row()).icon()
title = metadata['title']
author = metadata['author']
year = str(metadata['year'])
tags = metadata['tags']
cover = self.lib_ref.libraryModel.item(
edit_book.row()).icon()
title = self.lib_ref.libraryModel.data(
edit_book, QtCore.Qt.UserRole)
author = self.lib_ref.libraryModel.data(
edit_book, QtCore.Qt.UserRole + 1)
year = str(self.lib_ref.libraryModel.data(
edit_book, QtCore.Qt.UserRole + 2)) # Text cannot be int
tags = self.lib_ref.libraryModel.data(
edit_book, QtCore.Qt.UserRole + 4)
self.metadataDialog.load_book(
cover, title, author, year, tags, edit_book)
self.metadataDialog.show()
if action == deleteAction:
@@ -846,9 +905,12 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
last_accessed_time = QtCore.QDateTime().currentDateTime()
position_perc = 1
self.lib_ref.libraryModel.setData(i, metadata, QtCore.Qt.UserRole + 3)
self.lib_ref.libraryModel.setData(i, position_perc, QtCore.Qt.UserRole + 7)
self.lib_ref.libraryModel.setData(i, last_accessed_time, QtCore.Qt.UserRole + 12)
self.lib_ref.libraryModel.setData(
i, metadata, QtCore.Qt.UserRole + 3)
self.lib_ref.libraryModel.setData(
i, position_perc, QtCore.Qt.UserRole + 7)
self.lib_ref.libraryModel.setData(
i, last_accessed_time, QtCore.Qt.UserRole + 12)
self.lib_ref.update_proxymodels()
database_dict = {
@@ -929,6 +991,45 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.start_culling_timer()
def start_culling_timer(self):
if self.settings['perform_culling']:
self.culling_timer.start(90)
def resizeEvent(self, event=None):
if event:
# This implies a vertical resize event only
# We ain't about that lifestyle
if event.oldSize().width() == event.size().width():
return
# The hackiness of this hack is just...
default_size = 170 # This is size of the QIcon (160 by default) +
# minimum margin needed between thumbnails
# for n icons, the n + 1th icon will appear at > n +1.11875
# First, calculate the number of images per row
i = self.listView.viewport().width() / default_size
rem = i - int(i)
if rem >= .21875 and rem <= .9999:
num_images = int(i)
else:
num_images = int(i) - 1
# The rest is illustrated using informative variable names
space_occupied = num_images * default_size
# 12 is the scrollbar width
# Larger numbers keep reduce flickering but also increase
# the distance from the scrollbar
space_left = (
self.listView.viewport().width() - space_occupied - 19)
try:
layout_extra_space_per_image = space_left // num_images
self.listView.setGridSize(
QtCore.QSize(default_size + layout_extra_space_per_image, 250))
self.start_culling_timer()
except ZeroDivisionError: # Initial resize is ignored
return
def closeEvent(self, event=None):
if event:
event.ignore()
@@ -938,7 +1039,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.settingsDialog.hide()
self.definitionDialog.hide()
self.temp_dir.remove()
for this_dock in self.active_bookmark_docks:
for this_dock in self.active_docks:
try:
this_dock.setVisible(False)
except RuntimeError:
@@ -975,6 +1076,8 @@ def main():
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName('Lector') # This is needed for QStandardPaths
# and my own hubris
# Make icons sharp in HiDPI screen
app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
# Internationalization support
translator = QtCore.QTranslator()
@@ -982,10 +1085,10 @@ def main():
QtCore.QLocale.system(), ':/translations/translations_bin/Lector_')
app.installTranslator(translator)
translations_out_string = '(Translations found)'
translations_out_string = ' (Translations found)'
if not translations_found:
translations_out_string = '(No translations found)'
print(f'Locale: {QtCore.QLocale.system().name()}', translations_out_string)
translations_out_string = ' (No translations found)'
print(f'Locale: {QtCore.QLocale.system().name()}' + translations_out_string)
form = MainUI()
form.show()

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,10 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore, QtGui
from lector.resources import annotationswindow
logger = logging.getLogger(__name__)
class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
def __init__(self, parent=None):
@@ -85,6 +89,7 @@ class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
if 'foregroundColor' in annotation_components:
self.foregroundCheck.setChecked(True)
self.foregroundColor = annotation_components['foregroundColor']
self.set_button_background_color(
self.foregroundColorButton, annotation_components['foregroundColor'])
else:
@@ -92,6 +97,7 @@ class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
if 'highlightColor' in annotation_components:
self.highlightCheck.setChecked(True)
self.highlightColor = annotation_components['highlightColor']
self.set_button_background_color(
self.highlightColorButton, annotation_components['highlightColor'])
else:

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,10 +16,18 @@
import os
import zipfile
import logging
import webbrowser
try:
import popplerqt5
import fitz
from lector.parsers.pdf import render_pdf_page
except ImportError:
pass
try:
import djvu.decode
from lector.parsers.djvu import render_djvu_page
except ImportError:
pass
@@ -29,6 +37,8 @@ from lector.rarfile import rarfile
from lector.threaded import BackGroundCacheRefill
from lector.annotations import AnnotationPlacement
logger = logging.getLogger(__name__)
class PliantQGraphicsView(QtWidgets.QGraphicsView):
def __init__(self, filepath, main_window, parent=None):
@@ -37,7 +47,6 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
self.parent = parent
self.main_window = main_window
self.qimage = None # Will be needed to resize pdf
self.image_pixmap = None
self.image_cache = [None for _ in range(4)]
@@ -55,16 +64,19 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
self.book = rarfile.RarFile(self.filepath)
elif self.filetype == 'pdf':
self.book = popplerqt5.Poppler.Document.load(self.filepath)
self.book.setRenderHint(
popplerqt5.Poppler.Document.Antialiasing
and popplerqt5.Poppler.Document.TextAntialiasing)
self.book = fitz.open(self.filepath)
elif self.filetype == 'djvu':
self.book = djvu.decode.Context().new_document(
djvu.decode.FileURI(self.filepath))
self.book.decoding_job.wait()
self.common_functions = PliantWidgetsCommonFunctions(
self, self.main_window)
self.ignore_wheel_event = False
self.ignore_wheel_event_number = 0
self.mousePosition = None
self.setMouseTracking(True)
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
@@ -73,29 +85,75 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
self.generate_graphicsview_context_menu)
def loadImage(self, current_page):
# TODO
# For double page view: 1 before, 1 after
all_pages = [i[1] for i in self.parent.metadata['content']]
all_pages = self.parent.metadata['content']
current_page_index = all_pages.index(current_page)
double_page_mode = False
if (self.main_window.settings['double_page_mode']
and (current_page_index not in (0, len(all_pages) - 1))):
double_page_mode = True
def load_page(current_page):
image_pixmap = QtGui.QPixmap()
def page_loader(page):
pixmap = QtGui.QPixmap()
if self.filetype in ('cbz', 'cbr'):
page_data = self.book.read(current_page)
image_pixmap.loadFromData(page_data)
elif self.filetype == 'pdf':
page_data = self.book.page(current_page)
page_qimage = page_data.renderToImage(400, 400) # TODO Maybe this needs a setting?
image_pixmap.convertFromImage(page_qimage)
return image_pixmap
if self.filetype in ('cbz', 'cbr'):
page_data = self.book.read(page)
pixmap.loadFromData(page_data)
elif self.filetype == 'pdf':
page_data = self.book.loadPage(page)
pixmap = render_pdf_page(page_data)
elif self.filetype == 'djvu':
page_data = self.book.pages[page]
pixmap = render_djvu_page(page_data)
return pixmap
firstPixmap = page_loader(current_page)
if not double_page_mode:
return firstPixmap
next_page = all_pages[current_page_index + 1]
secondPixmap = page_loader(next_page)
# Pixmap height should be the greater of the 2 images
pixmap_height = firstPixmap.height()
if secondPixmap.height() > pixmap_height:
pixmap_height = secondPixmap.height()
bigPixmap = QtGui.QPixmap(
firstPixmap.width() + secondPixmap.width() + 5,
pixmap_height)
bigPixmap.fill(QtCore.Qt.transparent)
imagePainter = QtGui.QPainter(bigPixmap)
manga_mode = self.main_window.settings['manga_mode']
if manga_mode:
imagePainter.drawPixmap(0, 0, secondPixmap)
imagePainter.drawPixmap(secondPixmap.width() + 4, 0, firstPixmap)
else:
imagePainter.drawPixmap(0, 0, firstPixmap)
imagePainter.drawPixmap(firstPixmap.width() + 4, 0, secondPixmap)
imagePainter.end()
return bigPixmap
def generate_image_cache(current_page):
print('Building image cache')
logger.info('(Re)building image cache')
current_page_index = all_pages.index(current_page)
for i in (-1, 0, 1, 2):
# Image caching for single and double page views
page_indices = (-1, 0, 1, 2)
index_modifier = 0
if double_page_mode:
index_modifier = 1
for i in page_indices:
try:
this_page = all_pages[current_page_index + i]
this_page = all_pages[current_page_index + i + index_modifier]
this_pixmap = load_page(this_page)
self.image_cache[i + 1] = (this_page, this_pixmap)
except IndexError:
@@ -124,13 +182,37 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
# No return happened so the image isn't in the cache
generate_image_cache(current_page)
if self.main_window.settings['caching_enabled']:
# TODO
# Get caching working for double page view
# All of these must be True
caching_conditions = (
not double_page_mode,
self.main_window.settings['caching_enabled'])
if False not in caching_conditions:
return_pixmap = None
while not return_pixmap:
return_pixmap = check_cache(current_page)
else:
return_pixmap = load_page(current_page)
# Final pixmap transformations may take place here
## Color inversion
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)
## Image rotation
if not self.parent.image_rotation == 0:
transformation = QtGui.QTransform()
transformation.rotate(self.parent.image_rotation)
return_pixmap = return_pixmap.transformed(transformation)
self.image_pixmap = return_pixmap
self.resizeEvent()
@@ -171,12 +253,15 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
image_pixmap = self.image_pixmap.scaledToWidth(
available_width, QtCore.Qt.SmoothTransformation)
graphics_scene = QtWidgets.QGraphicsScene()
graphics_scene.addPixmap(image_pixmap)
graphicsScene = QtWidgets.QGraphicsScene()
graphicsScene.addPixmap(image_pixmap)
self.setScene(graphics_scene)
self.setScene(graphicsScene)
self.show()
# This prevents a partial page scroll on first load
self.verticalScrollBar().setValue(0)
def wheelEvent(self, event):
self.common_functions.wheelEvent(event)
@@ -202,9 +287,10 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
next_val = 0
self.verticalScrollBar().setValue(next_val)
small_increment = maximum // 4
big_increment = maximum // 2
small_increment = maximum //self.main_window.settings['small_increment']
big_increment = maximum // self.main_window.settings['large_increment']
# Scrolling
if event.key() == QtCore.Qt.Key_Up:
scroller(small_increment, False)
if event.key() == QtCore.Qt.Key_Down:
@@ -212,6 +298,11 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
if event.key() == QtCore.Qt.Key_Space:
scroller(big_increment)
# Double page mode and manga mode
if event.key() in (QtCore.Qt.Key_D, QtCore.Qt.Key_M):
self.main_window.change_page_view(event.key())
# Image fit modes
view_modification_keys = (
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Minus, QtCore.Qt.Key_Equal,
QtCore.Qt.Key_B, QtCore.Qt.Key_W, QtCore.Qt.Key_O)
@@ -223,43 +314,75 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
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(3000)
# Compare mouse positions
# This allows to filter out scrolling
# from a normal mouseEvent
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
if not self.mousePosition:
self.mousePosition = event.pos()
return
current_position = event.pos()
if current_position == self.mousePosition or self.parent.sideDock.isVisible():
return
else:
self.mousePosition = event.pos()
self.parent.navBar.show()
if QtWidgets.QApplication.mouseButtons() == QtCore.Qt.NoButton:
self.viewport().setCursor(QtCore.Qt.OpenHandCursor)
else:
self.viewport().setCursor(QtCore.Qt.ClosedHandCursor)
self.parent.mouseHideTimer.start(2000)
def generate_graphicsview_context_menu(self, position):
contextMenu = QtWidgets.QMenu()
saveAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('filesaveas'),
self._translate('PliantQGraphicsView', 'Save page as...'))
fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQGraphicsView', 'Exit fullscreen'))
else:
if self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQGraphicsView', 'Distraction Free mode')
else:
distraction_free_prompt = self._translate(
'PliantQGraphicsView', 'Exit Distraction Free mode')
elif not self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQGraphicsView', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
saveAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('filesaveas'),
self._translate('PliantQGraphicsView', 'Save page as...'))
view_submenu_string = self._translate('PliantQGraphicsView', 'View')
viewSubMenu = contextMenu.addMenu(view_submenu_string)
viewSubMenu.setIcon(
self.main_window.QImageFactory.get_image('mail-thread-watch'))
doublePageAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('page-double'),
self._translate('PliantQGraphicsView', 'Double page mode (D)'))
doublePageAction.setCheckable(True)
doublePageAction.setChecked(
self.main_window.bookToolBar.doublePageButton.isChecked())
mangaModeAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('manga-mode'),
self._translate('PliantQGraphicsView', 'Manga mode (M)'))
mangaModeAction.setCheckable(True)
mangaModeAction.setChecked(
self.main_window.bookToolBar.mangaModeButton.isChecked())
invertColorsAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('invert'),
self._translate('PliantQGraphicsView', 'Invert page colors'))
invertColorsAction.setCheckable(True)
invertColorsAction.setChecked(
self.main_window.bookToolBar.invertButton.isChecked())
viewSubMenu.addSeparator()
zoominAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-in'),
self._translate('PliantQGraphicsView', 'Zoom in (+)'))
@@ -290,6 +413,13 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
action = contextMenu.exec_(self.sender().mapToGlobal(position))
if action == doublePageAction:
self.main_window.bookToolBar.doublePageButton.trigger()
if action == mangaModeAction:
self.main_window.bookToolBar.mangaModeButton.trigger()
if action == invertColorsAction:
self.main_window.bookToolBar.invertButton.trigger()
if action == saveAction:
dialog_prompt = self._translate('Main_UI', 'Save page as...')
extension_string = self._translate('Main_UI', 'Images')
@@ -301,7 +431,7 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
self.image_pixmap.save(save_file[0])
if action == bookmarksToggleAction:
self.parent.toggle_bookmarks()
self.parent.toggle_side_dock(1)
if action == dfToggleAction:
self.main_window.toggle_distraction_free()
if action == fsToggleAction:
@@ -322,6 +452,8 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView):
self.main_window.closeEvent()
def toggle_annotation_mode(self):
# The graphics view doesn't currently have annotation functionality
# Don't delete this because it's still called
pass
@@ -333,6 +465,18 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
self.parent = parent
self.main_window = main_window
# Available modes:
# flow - default
# singlePage
# doublePage
self.text_mode = 'flow'
# New pages will be generated following a resize
# This is timed so as not to drive the processor nuts
self.resizeTimer = QtCore.QTimer()
self.resizeTimer.setSingleShot(True)
self.resizeTimer.timeout.connect(self.create_pages)
self.annotation_mode = False
self.annotator = AnnotationPlacement()
self.current_annotation = None
@@ -348,18 +492,30 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
self.setMouseTracking(True)
self.verticalScrollBar().sliderMoved.connect(
self.record_position)
self.mousePosition = None
self.ignore_wheel_event = False
self.ignore_wheel_event_number = 0
self.at_end = False
self.page_cursors = []
self.page_number = 0
def wheelEvent(self, event):
if self.text_mode in ('singlePage', 'doublePage'):
vertical_pdelta = event.pixelDelta().y()
direction = -1
if vertical_pdelta < 0:
direction = 1
self.turn_page(direction)
return
self.record_position()
self.common_functions.wheelEvent(event)
def keyPressEvent(self, event):
QtWidgets.QTextEdit.keyPressEvent(self, event)
if event.key() == QtCore.Qt.Key_Space:
QtWidgets.QTextEdit.keyPressEvent(self, event)
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)
@@ -367,20 +523,139 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
else:
self.at_end = False
self.set_top_line_cleanly()
self.record_position()
return
if self.text_mode == 'singlePage':
if event.key() == QtCore.Qt.Key_Down:
self.turn_page(1)
if event.key() == QtCore.Qt.Key_Up:
self.turn_page(-1)
self.record_position()
return
QtWidgets.QTextEdit.keyPressEvent(self, event)
def move_to_cursor(self, cursor):
self.setTextCursor(cursor)
self.verticalScrollBar().setValue(
self.verticalScrollBar().maximum())
self.ensureCursorVisible()
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()
cursorTop = self.cursorForPosition(QtCore.QPoint(0, 0))
self.move_to_cursor(cursorTop)
def resizeEvent(self, event=None):
QtWidgets.QTextBrowser.resizeEvent(self, event)
self.resizeTimer.start(100)
def create_pages(self, text_mode=None):
# Return to this value after page calcuation is done
cursorTop = self.cursorForPosition(QtCore.QPoint(0, 0))
# No changes in mode
# if text_mode == self.text_mode:
# return
# Account for resizeEvent
if not text_mode:
if self.text_mode == 'flow':
return
text_mode = self.text_mode
# Single Page mode
page_width = self.viewport().size().width()
page_height = self.viewport().size().height()
# Flow mode
if text_mode == 'flow':
page_height = -1
# TODO
# See what's an appropriate value to pad the text with
# profile_index = self.bookToolBar.profileBox.currentIndex()
# current_profile = self.bookToolBar.profileBox.itemData(
# profile_index, QtCore.Qt.UserRole)
# padding = 20
# Double page mode
if text_mode == 'doublePage':
page_width = page_width // 2 - 10
self.text_mode = text_mode
self.document().setPageSize(
QtCore.QSizeF(page_width, page_height))
self.generate_page_positions()
self.set_page(cursorTop)
def generate_page_positions(self):
self.verticalScrollBar().setValue(0)
cursorEnd = QtGui.QTextCursor(self.document())
cursorEnd.movePosition(QtGui.QTextCursor.End)
self.page_cursors = []
while True:
cursorTopLeft = self.cursorForPosition(
self.viewport().rect().topLeft())
cursorBottomLeft = self.cursorForPosition(
self.viewport().rect().bottomLeft())
cursorBottomRight = self.cursorForPosition(
self.viewport().rect().bottomRight())
self.page_cursors.append(
(cursorTopLeft.position(), cursorBottomRight.position()))
self.move_to_cursor(cursorBottomRight)
# TODO
# See if this requires a failsafe per number of iterations
if cursorEnd.position() == cursorBottomRight.position():
break
def set_page(self, originalCursor):
required_position = originalCursor.position()
if self.text_mode == 'flow':
page_start = required_position
if self.text_mode == 'singlePage':
for count, i in enumerate(self.page_cursors):
if i[0] <= required_position < i[1]:
page_start = i[0]
self.page_number = count
break
cursorGoTo = QtGui.QTextCursor(self.document())
cursorGoTo.setPosition(page_start)
self.move_to_cursor(cursorGoTo)
def turn_page(self, direction):
self.page_number += direction
if self.page_number in (-1, self.document().pageCount()):
self.page_number = 0
self.common_functions.change_chapter(direction)
self.create_pages()
else:
try:
page_start = self.page_cursors[self.page_number][0]
cursorGoTo = QtGui.QTextCursor(self.document())
cursorGoTo.setPosition(page_start)
self.move_to_cursor(cursorGoTo)
self.set_top_line_cleanly()
except IndexError:
pass
def record_position(self, return_as_bookmark=False):
self.parent.metadata['position']['is_read'] = False
cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
# The y coordinate is set to 10 because 0 tends to make
# cursor position a little finicky
cursor = self.cursorForPosition(QtCore.QPoint(0, 10))
cursor_position = cursor.position()
# Current block for progress measurement
@@ -406,21 +681,21 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
if self.annotation_mode:
self.annotation_mode = False
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
self.parent.annotationDock.show()
self.parent.annotationDock.setWindowOpacity(.95)
self.parent.sideDock.show()
self.parent.sideDock.setWindowOpacity(.95)
self.current_annotation = None
self.parent.annotationListView.clearSelection()
self.parent.sideDock.annotations.annotationListView.clearSelection()
else:
self.annotation_mode = True
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
self.parent.annotationDock.hide()
self.parent.sideDock.hide()
selected_index = self.parent.annotationListView.currentIndex()
self.current_annotation = self.parent.annotationModel.data(
selected_index = self.parent.sideDock.annotations.annotationListView.currentIndex()
self.current_annotation = self.parent.sideDock.annotationModel.data(
selected_index, QtCore.Qt.UserRole)
print('Current annotation: ' + self.current_annotation['name'])
logger.info('Selected annotation: ' + self.current_annotation['name'])
def mouseReleaseEvent(self, event):
# This takes care of annotation placement
@@ -429,13 +704,17 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
QtWidgets.QTextBrowser.mouseReleaseEvent(self, event)
return
self.place_annotation(self.current_annotation)
self.toggle_annotation_mode()
def place_annotation(self, annotation):
current_chapter = self.parent.metadata['position']['current_chapter']
cursor = self.textCursor()
cursor_start = cursor.selectionStart()
cursor_end = cursor.selectionEnd()
annotation_type = 'text_markup'
applicable_to = 'text'
annotation_components = self.current_annotation['components']
annotation_components = annotation['components']
self.annotator.set_current_annotation(
annotation_type, annotation_components)
@@ -448,7 +727,7 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
# Maybe use annotation name for a consolidated annotation list
this_annotation = {
'name': self.current_annotation['name'],
'name': annotation['name'],
'applicable_to': applicable_to,
'type': annotation_type,
'cursor': (cursor_start, cursor_end),
@@ -461,8 +740,6 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
self.annotation_dict[current_chapter] = []
self.annotation_dict[current_chapter].append(this_annotation)
self.toggle_annotation_mode()
def generate_textbrowser_context_menu(self, position):
selection = self.textCursor().selection()
selection = selection.toPlainText()
@@ -480,16 +757,51 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
searchWikipediaAction = searchYoutubeAction = 'Does anyone know something funny in Latin?'
searchAction = searchGoogleAction = bookmarksToggleAction = 'TODO Insert Latin Joke'
deleteAnnotationAction = editAnnotationNoteAction = 'Latin quote 2. Electric Boogaloo.'
flowModeAction = singlePageAction = doublePageAction = 'We know the rules, you and I'
annotationActions = []
view_submenu_string = self._translate('PliantQTextBrowser', 'View')
viewSubMenu = contextMenu.addMenu(view_submenu_string)
viewSubMenu.setIcon(
self.main_window.QImageFactory.get_image('mail-thread-watch'))
flowModeAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('page-flow'),
self._translate('PliantQTextBrowser', 'Flow text'))
singlePageAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('page-single'),
self._translate('PliantQTextBrowser', 'Single page'))
doublePageAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('page-double'),
self._translate('PliantQTextBrowser', 'Double page'))
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQTextBrowser', 'Exit fullscreen'))
elif not self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQTextBrowser', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
if selection and selection != '':
first_selected_word = selection.split()[0]
elided_selection = selection
if len(elided_selection) > 15:
elided_selection = elided_selection[:15] + '...'
define_string = self._translate('PliantQTextBrowser', 'Define')
defineAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-readermode'),
f'{define_string} "{first_selected_word}"')
search_submenu_string = self._translate('PliantQTextBrowser', 'Search for')
searchSubMenu = contextMenu.addMenu(search_submenu_string + f' "{selection}"')
searchSubMenu = contextMenu.addMenu(
search_submenu_string + f' "{elided_selection}"')
searchSubMenu.setIcon(self.main_window.QImageFactory.get_image('search'))
searchAction = searchSubMenu.addAction(
@@ -506,6 +818,30 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
QtGui.QIcon(':/images/Youtube.png'),
'Youtube')
# Allow adding new annotation from the context menu
if not annotation_is_present:
annotation_string = self._translate('PliantQTextBrowser', 'Annotate')
annotationSubmenu = contextMenu.addMenu(annotation_string)
annotationSubmenu.setIcon(
self.main_window.QImageFactory.get_image('annotate'))
saved_annotations = self.parent.main_window.settings['annotations']
if not saved_annotations:
nope = annotationSubmenu.addAction('<No annotations set>')
nope.setEnabled(False)
for i in saved_annotations:
this_action = QtWidgets.QAction(i['name'])
# Does not require / support a role
this_action.setData(i)
annotationActions.append(this_action)
annotationSubmenu.addAction(this_action)
else:
searchAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('search'),
self._translate('PliantQTextBrowser', 'Search'))
if annotation_is_present:
annotationsubMenu = contextMenu.addMenu('Annotation')
annotationsubMenu.setIcon(self.main_window.QImageFactory.get_image('annotate'))
@@ -517,21 +853,10 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
self.main_window.QImageFactory.get_image('remove'),
self._translate('PliantQTextBrowser', 'Delete annotation'))
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQTextBrowser', 'Exit fullscreen'))
else:
if self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQTextBrowser', 'Distraction Free mode')
else:
distraction_free_prompt = self._translate(
'PliantQTextBrowser', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
add_bookmark_string = self._translate('PliantQTextBrowser', 'Add Bookmark')
addBookMarkAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('bookmark-new'),
add_bookmark_string)
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
bookmarksToggleAction = contextMenu.addAction(
@@ -542,12 +867,26 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
action = contextMenu.exec_(self.sender().mapToGlobal(position))
if action == flowModeAction:
self.create_pages('flow')
if action == singlePageAction:
self.create_pages('singlePage')
if action == doublePageAction:
self.create_pages('doublePages')
if action == addBookMarkAction:
self.parent.sideDock.bookmarks.add_bookmark(cursor_at_mouse.position())
if action == defineAction:
self.main_window.definitionDialog.find_definition(selection)
if action == searchAction:
self.main_window.bookToolBar.searchBar.setText(selection)
self.main_window.bookToolBar.searchBar.setFocus()
if selection and selection != '':
self.parent.sideDock.search.searchLineEdit.setText(selection)
self.parent.toggle_side_dock(2, True)
if action == searchGoogleAction:
webbrowser.open_new_tab(
f'https://www.google.com/search?q={selection}')
@@ -558,16 +897,18 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
webbrowser.open_new_tab(
f'https://www.youtube.com/results?search_query={selection}')
if action in annotationActions:
self.place_annotation(action.data())
if action == editAnnotationNoteAction:
self.common_functions.annotation_specific(
'note', 'text', current_chapter, cursor_at_mouse.position())
if action == deleteAnnotationAction:
self.common_functions.annotation_specific(
'delete', 'text', current_chapter, cursor_at_mouse.position())
if action == bookmarksToggleAction:
self.parent.toggle_bookmarks()
self.parent.toggle_side_dock(0)
if action == fsToggleAction:
self.parent.exit_fullscreen()
@@ -578,12 +919,27 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser):
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(3000)
# Compare mouse positions
# This allows to filter out scrolling
# from a normal mouseEvent
QtWidgets.QTextBrowser.mouseMoveEvent(self, event)
if not self.mousePosition:
self.mousePosition = event.pos()
return
current_position = event.pos()
if current_position == self.mousePosition or self.parent.sideDock.isVisible():
return
else:
self.mousePosition = event.pos()
self.parent.navBar.show()
if self.annotation_mode:
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
else:
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
self.parent.mouseHideTimer.start(2000)
class PliantWidgetsCommonFunctions:
@@ -604,6 +960,8 @@ class PliantWidgetsCommonFunctions:
self.pw.ignore_wheel_event_number = 0
return
# TODO
# This can probably be cleaned up
if self.are_we_doing_images_only:
QtWidgets.QGraphicsView.wheelEvent(self.pw, event)
else:
@@ -635,23 +993,43 @@ class PliantWidgetsCommonFunctions:
self.change_chapter(-1)
def change_chapter(self, direction, was_button_pressed=None):
current_toc_index = self.main_window.bookToolBar.tocBox.currentIndex()
max_toc_index = self.main_window.bookToolBar.tocBox.count() - 1
current_tab = self.pw.parent
current_position = current_tab.metadata['position']['current_chapter']
final_position = len(current_tab.metadata['content'])
if (current_toc_index < max_toc_index and direction == 1) or (
current_toc_index > 0 and direction == -1):
self.main_window.bookToolBar.tocBox.setCurrentIndex(
current_toc_index + direction)
# Prevent scrolling below page 1
if current_position == 1 and direction == -1:
return
# Set page position depending on if the chapter number is increasing or decreasing
if direction == 1 or was_button_pressed:
self.pw.verticalScrollBar().setValue(0)
else:
self.pw.verticalScrollBar().setValue(
self.pw.verticalScrollBar().maximum())
# Prevent scrolling beyond last page
if (current_position == final_position) and direction == 1:
return
if not was_button_pressed:
self.pw.ignore_wheel_event = True
# Special cases for double page view
# Page limits are taken care of by the set_content method
def get_modifier():
if (not self.main_window.settings['double_page_mode']
or not self.are_we_doing_images_only):
return 0
if (current_position == 0 or current_position % 2 == 0):
return 0
if current_position % 2 == 1:
return direction
current_tab.set_content(
current_position + direction + get_modifier(), True, True)
# Set page position depending on if the chapter number is increasing or decreasing
if direction == 1 or was_button_pressed:
self.pw.verticalScrollBar().setValue(0)
else:
self.pw.verticalScrollBar().setValue(
self.pw.verticalScrollBar().maximum())
if not was_button_pressed:
self.pw.ignore_wheel_event = True
def load_annotations(self, chapter):
try:
@@ -680,7 +1058,8 @@ class PliantWidgetsCommonFunctions:
if not self.are_we_doing_images_only:
cursor = self.pw.textCursor()
cursor.setPosition(0)
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
cursor.movePosition(
QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
previewCharFormat = QtGui.QTextCharFormat()
previewCharFormat.setFontStyleStrategy(
@@ -737,11 +1116,13 @@ class PliantWidgetsCommonFunctions:
1, QtCore.Qt.MatchExactly)
if self.are_we_doing_images_only:
position_percentage = (self.pw.parent.metadata['position']['current_chapter'] /
self.pw.parent.metadata['position']['total_chapters'])
position_percentage = (
self.pw.parent.metadata['position']['current_chapter'] /
self.pw.parent.metadata['position']['total_chapters'])
else:
position_percentage = (self.pw.parent.metadata['position']['current_block'] /
self.pw.parent.metadata['position']['total_blocks'])
position_percentage = (
self.pw.parent.metadata['position']['current_block'] /
self.pw.parent.metadata['position']['total_blocks'])
# Update position percentage
if model_index:
@@ -751,14 +1132,27 @@ class PliantWidgetsCommonFunctions:
def generate_combo_box_action(self, contextMenu):
contextMenu.addSeparator()
toc_combobox = QtWidgets.QComboBox()
toc_data = [i[0] for i in self.pw.parent.metadata['content']]
toc_combobox.addItems(toc_data)
toc_combobox.setCurrentIndex(
self.pw.main_window.bookToolBar.tocBox.currentIndex())
toc_combobox.currentIndexChanged.connect(
self.pw.main_window.bookToolBar.tocBox.setCurrentIndex)
def set_toc_position(tocTree):
currentIndex = tocTree.currentIndex()
required_position = currentIndex.data(QtCore.Qt.UserRole)
self.pw.parent.set_content(required_position, True, True)
# Create the Combobox / Treeview combination
tocComboBox = QtWidgets.QComboBox()
tocTree = QtWidgets.QTreeView()
tocComboBox.setView(tocTree)
tocComboBox.setModel(self.pw.parent.tocModel)
tocTree.setRootIsDecorated(False)
tocTree.setItemsExpandable(False)
tocTree.expandAll()
# Set the position of the QComboBox
self.pw.parent.set_tocBox_index(None, tocComboBox)
# Make clicking do something
tocComboBox.currentIndexChanged.connect(
lambda: set_toc_position(tocTree))
comboboxAction = QtWidgets.QWidgetAction(self.pw)
comboboxAction.setDefaultWidget(toc_combobox)
comboboxAction.setDefaultWidget(tocComboBox)
contextMenu.addAction(comboboxAction)

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,13 +17,15 @@
import os
import pickle
import sqlite3
import logging
from PyQt5 import QtCore
logger = logging.getLogger(__name__)
class DatabaseInit:
def __init__(self, location_prefix):
os.makedirs(location_prefix, exist_ok=True)
self.database_path = os.path.join(location_prefix, 'Lector.db')
self.books_table_columns = {
@@ -81,7 +83,8 @@ class DatabaseInit:
for i in self.books_table_columns.items():
if i[0] not in database_columns:
commit_required = True
print(f'Database: Adding column "{i[0]}"')
info_string = f'Database: Adding column "{i[0]}"'
logger.info(info_string)
sql_command = f"ALTER TABLE books ADD COLUMN {i[0]} {i[1]}"
self.database.execute(sql_command)
@@ -207,8 +210,9 @@ class DatabaseFunctions:
else:
return None
except (KeyError, sqlite3.OperationalError):
print('SQLite is in wretched rebellion @ data fetching handling')
except Exception as e:
error_string = 'SQLite is in wretched rebellion @ data fetching handling'
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
def fetch_covers_only(self, hash_list):
parameter_marks = ','.join(['?' for i in hash_list])
@@ -240,8 +244,9 @@ class DatabaseFunctions:
try:
self.database.execute(
sql_command, update_data)
except sqlite3.OperationalError:
print('SQLite is in wretched rebellion @ metadata handling')
except sqlite3.OperationalError as e:
error_string = 'SQLite is in wretched rebellion @ metadata handling'
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
self.database.commit()
self.database.close()

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,9 +15,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import logging
import urllib.request
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia
from PyQt5 import QtWidgets, QtCore, QtGui
logger = logging.getLogger(__name__)
try:
from PyQt5 import QtMultimedia
multimedia_available = True
except ImportError:
error_string = 'QtMultimedia not found. Sounds will not play.'
logger.error(error_string)
multimedia_available = False
from lector.resources import definitions
@@ -56,8 +67,11 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
self.pronunciation_mp3 = None
self.okButton.clicked.connect(self.hide)
self.pronounceButton.clicked.connect(self.play_pronunciation)
self.dialogBackground.clicked.connect(self.color_background)
if multimedia_available:
self.pronounceButton.clicked.connect(self.play_pronunciation)
else:
self.pronounceButton.setEnabled(False)
def api_call(self, url, word):
language = self.parent.settings['dictionary_language']
@@ -72,12 +86,16 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
if response.getcode() == 200:
return_json = json.loads(response.read())
return return_json
except urllib.error.HTTPError:
except Exception as e:
this_error = f'API access error'
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
self.parent.display_error_notification(None)
return None
def find_definition(self, word):
word_root_json = self.api_call(self.root_url, word)
if not word_root_json:
logger.error('Word root json noped out: ' + word)
self.set_text(word, None, None, True)
return
@@ -86,6 +104,7 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
definition_json = self.api_call(self.define_url, word_root)
if not definition_json:
logger.error('Definition json noped out: ' + word_root)
self.set_text(word, None, None, True)
return
@@ -104,7 +123,7 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
this_definition = j['definitions'][0].capitalize()
except KeyError:
# The API also reports crossReferenceMarkers here
pass
this_definition = '<Not found>'
try:
definitions[category].add(this_definition)
@@ -149,16 +168,27 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
self.parent.get_color()
background = self.parent.settings['dialog_background']
# Calculate inverse color for the background so that
# the text doesn't look blank
r, g, b, alpha = background.getRgb()
inv_average = 255 - (r + g + b) // 3
if 100 < inv_average < 150:
inv_average = 255
foreground = QtGui.QColor(
inv_average, inv_average, inv_average, alpha)
self.setStyleSheet(
"QDialog {{background-color: {0}}}".format(background.name()))
self.definitionView.setStyleSheet(
"QTextBrowser {{background-color: {0}}}".format(background.name()))
"QTextBrowser {{color:{0}; background-color: {1}}}".format(
foreground.name(), background.name()))
if not set_initial:
self.show()
def play_pronunciation(self):
if not self.pronunciation_mp3:
if not self.pronunciation_mp3 or not multimedia_available:
return
media_content = QtMultimedia.QMediaContent(

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,9 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtGui, QtCore
from lector.resources import pie_chart
logger = logging.getLogger(__name__)
class LibraryDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, temp_dir, parent=None):
@@ -66,31 +71,3 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
x_draw = option.rect.bottomRight().x() - 30
y_draw = option.rect.bottomRight().y() - 35
painter.drawPixmap(x_draw, y_draw, read_icon)
class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, main_window, parent=None):
super(BookmarkDelegate, self).__init__()
self.main_window = main_window
self.parent = parent
def sizeHint(self, *args):
dockwidget_width = self.parent.width() - 20
return QtCore.QSize(dockwidget_width, 50)
def paint(self, painter, option, index):
# TODO
# Alignment of the painted item
option = option.__class__(option)
chapter_index = index.data(QtCore.Qt.UserRole)
chapter_name = self.main_window.bookToolBar.tocBox.itemText(chapter_index - 1)
if len(chapter_name) > 25:
chapter_name = chapter_name[:25] + '...'
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
painter.drawText(
option.rect,
QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight | QtCore.Qt.TextWordWrap,
' ' + chapter_name)

661
lector/dockwidgets.py Normal file
View File

@@ -0,0 +1,661 @@
# 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
# Animate appearance
self.animation = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.animation.setStartValue(0)
self.animation.setEndValue(1)
self.animation.setDuration(200)
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.parent.navBar.hide()
self.main_window.active_docks.append(self)
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
self.animation.start()
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)
class PliantNavBarWidget(QtWidgets.QDockWidget):
def __init__(self, main_window, contentView, parent):
super(PliantNavBarWidget, self).__init__(parent)
self.main_window = main_window
self.contentView = contentView
self.parent = parent
self.setWindowTitle('Navigation')
# Animate appearance
self.animation = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.animation.setDuration(200)
self.animation.setStartValue(0)
self.animation.setEndValue(.8)
background = self.main_window.settings['dialog_background']
self.setStyleSheet(
"QDockWidget {{background-color: {0}}}".format(background.name()))
self.backButton = QtWidgets.QPushButton()
self.backButton.setFlat(True)
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/images/previous.png"),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.backButton.setIcon(icon)
self.backButton.setIconSize(QtCore.QSize(24, 24))
self.nextButton = QtWidgets.QPushButton()
self.nextButton.setFlat(True)
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/images/next.png"),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.nextButton.setIcon(icon)
self.nextButton.setIconSize(QtCore.QSize(24, 24))
self.backButton.clicked.connect(lambda: self.button_click(-1))
self.nextButton.clicked.connect(lambda: self.button_click(1))
self.tocComboBox = FixedComboBox(self)
self.populate_combo_box()
self.navLayout = QtWidgets.QHBoxLayout()
self.navLayout.addWidget(self.backButton)
self.navLayout.addWidget(self.tocComboBox)
self.navLayout.addWidget(self.nextButton)
self.navWidget = QtWidgets.QWidget()
self.navWidget.setLayout(self.navLayout)
self.setWidget(self.navWidget)
def showEvent(self, event=None):
# TODO
# See what happens when the size of the viewport is smaller
# than the size of the dock
viewport_bottomRight = self.contentView.mapToGlobal(
self.contentView.viewport().rect().bottomRight())
# Dock dimensions
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
dock_width = desktop_size.width() // 4.5
dock_height = 30
dock_x = viewport_bottomRight.x() - dock_width - 30
dock_y = viewport_bottomRight.y() - 70
self.main_window.active_docks.append(self)
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
# Rounded
radius = 20
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
try:
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
except TypeError: # Required for older versions of Qt
pass
self.animation.start()
def populate_combo_box(self):
def set_toc_position(tocTree):
currentIndex = tocTree.currentIndex()
required_position = currentIndex.data(QtCore.Qt.UserRole)
self.return_focus()
self.parent.set_content(required_position, True, True)
# Create the Combobox / Treeview combination
tocTree = QtWidgets.QTreeView()
self.tocComboBox.setView(tocTree)
self.tocComboBox.setModel(self.parent.tocModel)
tocTree.setRootIsDecorated(False)
tocTree.setItemsExpandable(False)
tocTree.expandAll()
# Set the position of the QComboBox
self.parent.set_tocBox_index(None, self.tocComboBox)
# Make clicking do something
self.tocComboBox.currentIndexChanged.connect(
lambda: set_toc_position(tocTree))
def button_click(self, change):
self.contentView.common_functions.change_chapter(change)
self.return_focus()
def return_focus(self):
# The NavBar needs to be hidden after clicking
self.parent.activateWindow()
self.parent.contentView.setFocus()
self.parent.mouseHideTimer.start()
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 // 6
def sizeHint(self):
return self.minimumSizeHint()
def minimumSizeHint(self):
return QtCore.QSize(self.adjusted_size, 32)
def wheelEvent(self, QWheelEvent):
# Disable mouse wheel scrolling in the ComboBox
return

View File

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

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,12 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtCore, QtGui, QtWidgets
from lector import database
from lector.settings import Settings
from lector.resources import resources
logger = logging.getLogger(__name__)
class QImageFactory:
def __init__(self, parent):
@@ -127,7 +131,8 @@ class CoverLoadingAndCulling:
img_pixmap.loadFromData(cover)
else:
img_pixmap.load(':/images/NotFound.png')
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
img_pixmap = img_pixmap.scaled(
420, 600, QtCore.Qt.IgnoreAspectRatio)
item.setIcon(QtGui.QIcon(img_pixmap))
item.setData(True, QtCore.Qt.UserRole + 8)
@@ -237,6 +242,7 @@ class ViewProfileModification:
self.format_contentView()
def modify_comic_view(self, signal_sender, key_pressed):
comic_profile = self.main_window.comic_profile
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
self.bookToolBar.fitWidth.setChecked(False)
@@ -244,41 +250,41 @@ class ViewProfileModification:
self.bookToolBar.originalSize.setChecked(False)
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
self.comic_profile['zoom_mode'] = 'manualZoom'
self.comic_profile['padding'] += 50
comic_profile['zoom_mode'] = 'manualZoom'
comic_profile['padding'] += 50
# This prevents infinite zoom out
if self.comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
self.comic_profile['padding'] -= 50
if comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
comic_profile['padding'] -= 50
if signal_sender == 'zoomIn' or key_pressed in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
self.comic_profile['zoom_mode'] = 'manualZoom'
self.comic_profile['padding'] -= 50
if signal_sender == 'zoomIn' or key_pressed in (
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
comic_profile['zoom_mode'] = 'manualZoom'
comic_profile['padding'] -= 50
# This prevents infinite zoom in
if self.comic_profile['padding'] < 0:
self.comic_profile['padding'] = 0
if comic_profile['padding'] < 0:
comic_profile['padding'] = 0
if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W:
self.comic_profile['zoom_mode'] = 'fitWidth'
self.comic_profile['padding'] = 0
comic_profile['zoom_mode'] = 'fitWidth'
comic_profile['padding'] = 0
self.bookToolBar.fitWidth.setChecked(True)
# Padding in the following cases is decided by
# the image pixmap loaded by the widget
if signal_sender == 'bestFit' or key_pressed == QtCore.Qt.Key_B:
self.comic_profile['zoom_mode'] = 'bestFit'
comic_profile['zoom_mode'] = 'bestFit'
self.bookToolBar.bestFit.setChecked(True)
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
self.comic_profile['zoom_mode'] = 'originalSize'
comic_profile['zoom_mode'] = 'originalSize'
self.bookToolBar.originalSize.setChecked(True)
self.format_contentView()
def format_contentView(self):
current_tab = self.tabWidget.widget(
self.tabWidget.currentIndex())
current_tab = self.tabWidget.currentWidget()
try:
current_metadata = current_tab.metadata
@@ -287,7 +293,6 @@ class ViewProfileModification:
if current_metadata['images_only']:
background = self.comic_profile['background']
padding = self.comic_profile['padding']
zoom_mode = self.comic_profile['zoom_mode']
if zoom_mode == 'fitWidth':
@@ -301,7 +306,7 @@ class ViewProfileModification:
'background-color: %s' % background.name())
current_tab.format_view(
None, None, None, background, padding, None, None)
None, None, None, background, None, None, None)
else:
profile_index = self.bookToolBar.profileBox.currentIndex()

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,6 +16,7 @@
import os
import pickle
import logging
import pathlib
from PyQt5 import QtGui, QtCore
@@ -23,6 +24,8 @@ from PyQt5 import QtGui, QtCore
from lector import database
from lector.models import TableProxyModel, ItemProxyModel
logger = logging.getLogger(__name__)
class Library:
def __init__(self, parent):
@@ -47,7 +50,7 @@ class Library:
'LIKE')
if not books:
print('Database returned nothing')
logger.warning('Database returned nothing')
return
elif mode == 'addition':
@@ -58,9 +61,13 @@ class Library:
books = []
current_qdatetime = QtCore.QDateTime().currentDateTime()
for i in parsed_books.items():
_tags = i[1]['tags']
if _tags:
_tags = ', '.join([j for j in _tags if j])
try:
_tags = i[1]['tags']
if _tags:
_tags = ', '.join([j for j in _tags if j])
except: # Continuing seems more important than being correct
_tags = []
logger.warning('Tag generation error for: ' + i[1]['path'])
books.append([
i[1]['title'], i[1]['author'], i[1]['year'], current_qdatetime,
@@ -98,18 +105,7 @@ class Library:
position = i[5]
if position:
position = pickle.loads(position)
if position['is_read']:
position_perc = 1
else:
try:
position_perc = (
position['current_block'] / position['total_blocks'])
except (KeyError, ZeroDivisionError):
try:
position_perc = (
position['current_chapter'] / position['total_chapters'])
except KeyError:
position_perc = None
position_perc = generate_position_percentage(position)
try:
file_exists = os.path.exists(path)
@@ -149,6 +145,7 @@ class Library:
item.setToolTip(tooltip_string)
# Just keep the following order. It's way too much trouble otherwise
# User roles have to be correlated to sorting order below
item.setData(title, QtCore.Qt.UserRole)
item.setData(author, QtCore.Qt.UserRole + 1)
item.setData(year, QtCore.Qt.UserRole + 2)
@@ -319,7 +316,7 @@ class Library:
addition_mode = item_metadata['addition_mode']
except KeyError:
addition_mode = 'automatic'
print('Libary: Error setting addition mode for prune')
logger.error('Libary: Error setting addition mode for prune')
if (book_path not in valid_paths and
(addition_mode != 'manual' or addition_mode is None)):
@@ -335,3 +332,23 @@ class Library:
# Remove invalid paths from the database as well
database.DatabaseFunctions(
self.main_window.database_path).delete_from_database('Path', invalid_paths)
def generate_position_percentage(position):
if not position:
return None
if position['is_read']:
position_perc = 1
else:
try:
position_perc = (
position['current_block'] / position['total_blocks'])
except (KeyError, ZeroDivisionError):
try:
position_perc = (
position['current_chapter'] / position['total_chapters'])
except KeyError:
position_perc = None
return position_perc

58
lector/logger.py Normal file
View File

@@ -0,0 +1,58 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
VERSION = '0.5.GittyGittyBangBang'
import os
import logging
from PyQt5 import QtCore
location_prefix = os.path.join(
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation),
'Lector')
logger_filename = os.path.join(location_prefix, 'Lector.log')
def init_logging(cli_arguments):
# This needs a separate 'Lector' in the os.path.join because
# application name isn't explicitly set in this module
os.makedirs(location_prefix, exist_ok=True)
log_level = 30 # Warning and above
# Set log level according to command line arguments
try:
if cli_arguments[1] == 'debug':
log_level = 10 # Debug and above
print('Debug logging enabled')
try:
os.remove(logger_filename) # Remove old log for clarity
except FileNotFoundError:
pass
except IndexError:
pass
# Create logging object
logging.basicConfig(
filename=logger_filename,
filemode='a',
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
level=log_level)
logging.addLevelName(60, 'HAMMERTIME') ## Messages that MUST be logged
return logging.getLogger('lector.main')

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,12 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore, QtGui
from lector import database
from lector.widgets import PliantQGraphicsScene
from lector.resources import metadata
logger = logging.getLogger(__name__)
class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
def __init__(self, parent):

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,33 +14,32 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import pathlib
from PyQt5 import QtCore, QtWidgets
from lector.resources import pie_chart
logger = logging.getLogger(__name__)
class BookmarkProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(BookmarkProxyModel, self).__init__(parent)
self.parent = parent
self.parentTab = self.parent.parent
self.filter_text = None
def setFilterParams(self, filter_text):
self.filter_text = filter_text
def filterAcceptsRow(self, row, parent):
# TODO
# Connect this to the search bar
return True
def setData(self, index, value, role):
if role == QtCore.Qt.EditRole:
source_index = self.mapToSource(index)
identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2)
self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole)
self.parent.metadata['bookmarks'][identifier]['description'] = value
self.parentTab.metadata['bookmarks'][identifier]['description'] = value
return True
@@ -99,7 +98,8 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
try:
return self.header_data[column]
except IndexError:
print('Table proxy model: Can\'t find header for column', column)
logger.error(
'Table proxy model: Can\'t find header for column' + str(column))
# The column will be called IndexError. Not a typo.
return 'IndexError'
@@ -343,32 +343,3 @@ class MostExcellentFileSystemModel(QtWidgets.QFileSystemModel):
for i in deletable:
del self.tag_data[i]
# TODO
# Unbork this
class FileSystemProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(FileSystemProxyModel, self).__init__(parent)
def filterAcceptsRow(self, row_num, parent):
model = self.sourceModel()
filter_out = [
'boot', 'dev', 'etc', 'lost+found', 'opt', 'pdb',
'proc', 'root', 'run', 'srv', 'sys', 'tmp', 'twonky',
'usr', 'var', 'bin', 'kdeinit5__0', 'lib', 'lib64', 'sbin']
name_index = model.index(row_num, 0)
valid_data = model.data(name_index)
print(valid_data)
return True
try:
if valid_data in filter_out:
return False
except AttributeError:
pass
return True

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-18 BasioMeusPuga
# Copyright (C) 2017-19 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,10 +19,14 @@
import os
import time
import logging
import zipfile
import collections
from lector.rarfile import rarfile
logger = logging.getLogger(__name__)
class ParseCOMIC:
def __init__(self, filename, *args):
@@ -34,47 +36,45 @@ class ParseCOMIC:
self.book_extension = os.path.splitext(self.filename)
def read_book(self):
try:
if self.book_extension[1] == '.cbz':
self.book = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
self.image_list = [i.filename for i in self.book.infolist() if not i.is_dir()]
if self.book_extension[1] == '.cbz':
self.book = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
self.image_list = [
i.filename for i in self.book.infolist()
if not i.is_dir() and is_image(i.filename)]
elif self.book_extension[1] == '.cbr':
self.book = rarfile.RarFile(self.filename)
self.image_list = [i.filename for i in self.book.infolist() if not i.isdir()]
elif self.book_extension[1] == '.cbr':
self.book = rarfile.RarFile(self.filename)
self.image_list = [
i.filename for i in self.book.infolist()
if not i.isdir() and is_image(i.filename)]
self.image_list.sort()
except: # Specifying no exception here is warranted
print('Cannot parse ' + self.filename)
return
self.image_list.sort()
def get_title(self):
def generate_metadata(self):
title = os.path.basename(self.book_extension[0]).strip(' ')
return title
author = '<Unknown>'
isbn = None
tags = []
cover = self.book.read(self.image_list[0])
def get_author(self):
return 'Unknown'
def get_year(self):
creation_time = time.ctime(os.path.getctime(self.filename))
creation_year = creation_time.split()[-1]
return creation_year
year = creation_time.split()[-1]
def get_cover_image(self):
# The first image in the archive may not be the cover
# It is implied, however, that the first image in order
# will be the cover
return self.book.read(self.image_list[0])
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def get_isbn(self):
return None
def generate_content(self):
image_number = len(self.image_list)
toc = [(1, f'Page {i + 1}', i + 1) for i in range(image_number)]
def get_tags(self):
return None
# Return toc, content, images_only
return toc, self.image_list, True
def get_contents(self):
file_settings = {'images_only': True}
contents = [(f'Page {count + 1}', i) for count, i in enumerate(self.image_list)]
return contents, file_settings
def is_image(filename):
valid_image_extensions = ['.png', '.jpg', '.bmp']
if os.path.splitext(filename)[1].lower() in valid_image_extensions:
return True
else:
return False

98
lector/parsers/djvu.py Normal file
View File

@@ -0,0 +1,98 @@
# 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 collections
import djvu.decode
from PyQt5 import QtGui
class ParseDJVU:
def __init__(self, filename, *args):
self.book = None
self.filename = filename
def read_book(self):
self.book = djvu.decode.Context().new_document(
djvu.decode.FileURI(self.filename))
self.book.decoding_job.wait()
def generate_metadata(self):
# TODO
# What even is this?
title = os.path.basename(self.filename)
author = 'Unknown'
year = 9999
isbn = None
tags = []
cover_page = self.book.pages[0]
cover = render_djvu_page(cover_page, True)
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
# TODO
# See if it's possible to generate a more involved ToC
content = list(range(len(self.book.pages)))
toc = [(1, f'Page {i + 1}', i + 1) for i in content]
# Return toc, content, images_only
return toc, content, True
def render_djvu_page(page, for_cover=False):
# TODO
# Figure out how to calculate image stride
# and if it impacts row_alignment in the render
# method below
# bytes_per_line = 13200
djvu_pixel_format = djvu.decode.PixelFormatRgbMask(
0xFF0000, 0xFF00, 0xFF, bpp=32)
djvu_pixel_format.rows_top_to_bottom = 1
djvu_pixel_format.y_top_to_bottom = 0
# ¯\_(ツ)_/¯
mode = 0
page_job = page.decode(wait=True)
width, height = page_job.size
rect = (0, 0, width, height)
output = page_job.render(
mode, rect, rect, djvu_pixel_format)
# row_alignment=bytes_per_line)
imageFormat = QtGui.QImage.Format_RGB32
pageQImage = QtGui.QImage(output, width, height, imageFormat)
# Format conversion not only keeps the damn thing from
# segfaulting when converting from QImage to QPixmap,
# but it also allows for the double page mode to keep
# working properly. We like format conversion.
pageQImage = pageQImage.convertToFormat(
QtGui.QImage.Format_ARGB32_Premultiplied)
if for_cover:
return pageQImage
pixmap = QtGui.QPixmap()
pixmap.convertFromImage(pageQImage)
return pixmap

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

@@ -0,0 +1,54 @@
# 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 logging
import collections
import markdown
logger = logging.getLogger(__name__)
class ParseMD:
def __init__(self, filename, *args):
self.book = None
self.filename = filename
def read_book(self):
self.book = None
def generate_metadata(self):
title = os.path.basename(self.filename)
author = 'Unknown'
year = 9999
isbn = None
tags = []
cover = None
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
with open(self.filename, 'r') as book:
text = book.read()
content = [markdown.markdown(text)]
toc = [(1, 'Markdown', 1)]
# Return toc, content, images_only
return toc, content, False

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,78 +14,69 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# This module parses Amazon ebooks using KindleUnpack to first create an
# epub that is then read the usual way
# TODO
# See if it's possible to just feed the
# unzipped mobi7 file into the EPUB parser module
import os
import sys
import shutil
import zipfile
import logging
from lector.ePub.read_epub import EPUB
from lector.readers.read_epub import EPUB
import lector.KindleUnpack.kindleunpack as KindleUnpack
logger = logging.getLogger(__name__)
class ParseMOBI:
# This module parses Amazon ebooks using KindleUnpack to first create an
# epub and then read the usual way
def __init__(self, filename, temp_dir, file_md5):
self.book_ref = None
self.book = None
self.filename = filename
self.epub_filepath = None
self.split_large_xml = False
self.temp_dir = temp_dir
self.extract_dir = os.path.join(temp_dir, file_md5)
self.extract_path = os.path.join(temp_dir, file_md5)
def read_book(self):
with HidePrinting():
KindleUnpack.unpackBook(self.filename, self.extract_dir)
KindleUnpack.unpackBook(self.filename, self.extract_path)
epub_filename = os.path.splitext(
os.path.basename(self.filename))[0] + '.epub'
self.epub_filepath = os.path.join(
self.extract_dir, 'mobi8', epub_filename)
self.extract_path, 'mobi8', epub_filename)
if not os.path.exists(self.epub_filepath):
zip_dir = os.path.join(self.extract_dir, 'mobi7')
zip_dir = os.path.join(self.extract_path, 'mobi7')
zip_file = os.path.join(
self.extract_dir, epub_filename)
self.extract_path, epub_filename)
self.epub_filepath = shutil.make_archive(zip_file, 'zip', zip_dir)
self.split_large_xml = True
self.book_ref = EPUB(self.epub_filepath)
contents_found = self.book_ref.read_epub()
if not contents_found:
print('Cannot process: ' + self.filename)
return
self.book = self.book_ref.book
self.book = EPUB(self.epub_filepath, self.temp_dir)
def get_title(self):
return self.book['title']
def generate_metadata(self):
self.book.generate_metadata()
return self.book.metadata
def get_author(self):
return self.book['author']
def generate_content(self):
zipfile.ZipFile(self.epub_filepath).extractall(self.extract_path)
def get_year(self):
return self.book['year']
self.book.generate_toc()
self.book.generate_content()
def get_cover_image(self):
return self.book['cover']
toc = []
content = []
for count, i in enumerate(self.book.content):
toc.append((1, i[1], count + 1))
content.append(i[2])
def get_isbn(self):
return self.book['isbn']
# Return toc, content, images_only
return toc, content, False
def get_tags(self):
return self.book['tags']
def get_contents(self):
extract_path = os.path.join(self.extract_dir)
zipfile.ZipFile(self.epub_filepath).extractall(extract_path)
self.book_ref.parse_chapters(
temp_dir=self.temp_dir, split_large_xml=self.split_large_xml)
file_settings = {
'images_only': False}
return self.book['book_list'], file_settings
class HidePrinting:
def __enter__(self):

View File

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

55
lector/parsers/txt.py Normal file
View File

@@ -0,0 +1,55 @@
# 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 collections
import os
import textile
class ParseTXT:
"""Parser for TXT files."""
def __init__(self, filename, *args):
"""Initialize new instance of the TXT parser."""
self.filename = filename
def read_book(self):
"""Prepare the parser to read book."""
pass
def generate_metadata(self):
"""Generate metadata for the book."""
title = os.path.basename(self.filename)
author = 'Unknown'
year = 9999
isbn = None
tags = []
cover = None
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
"""Generate content of the book."""
with open(self.filename, 'rt') as txt:
text = txt.read()
content = [textile.textile(text)]
toc = [(1, 'Text', 1)]
return toc, content, False

478
lector/readers/read_epub.py Normal file
View File

@@ -0,0 +1,478 @@
# 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.warning(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.warning('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))
except:
logger.warning('Cover not found in opf: ' + self.book_filename)
# Find book cover the hard way
if not book_cover:
biggest_image_size = 0
cover_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:
cover_image = j.filename
biggest_image_size = j.file_size
if cover_image:
book_cover = self.zip_file.read(
self.find_file(cover_image))
if not book_cover:
self.cover_image_name = ''
logger.warning('Cover not found: ' + self.book_filename)
else:
self.cover_image_name = os.path.splitext(
os.path.basename(cover_image))[0]
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,62 @@
<?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"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="invert.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<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="855"
inkscape:window-height="480"
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:#444444; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
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)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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,62 @@
<?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"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="page-double.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<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="1920"
inkscape:window-height="1043"
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="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
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(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,74 @@
<?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"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="page-flow.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<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" />
<dc:title></dc:title>
</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="1920"
inkscape:window-height="1043"
id="namedview9"
showgrid="false"
inkscape:zoom="15.170655"
inkscape:cx="-1.2825494"
inkscape:cy="5.330307"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
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(3 3)"
id="path5" />
<g
transform="matrix(0.64844409,0,0,0.64844409,5.8379769,5.8670545)"
id="g7"
style="fill:#5c616c;fill-opacity:1">
<path
inkscape:connector-curvature="0"
class="ColorScheme-Text"
d="m 7,2 v 8 L 3.5,6.5 2,8 8,14 14,8 12.5,6.5 9,10 V 2 Z"
style="color:#dfdfdf;fill:#5c616c;fill-opacity:1"
id="path5-3" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.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 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,62 @@
<?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"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="rotate-left.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<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="855"
inkscape:window-height="480"
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:#444444; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 8.0292969 0.001953125 L 4.0292969 3.0019531 L 8.0292969 6.0019531 L 8.0292969 4.0019531 C 10.238397 4.0019531 12.029297 5.7928531 12.029297 8.0019531 C 12.029297 10.211053 10.238397 12.001953 8.0292969 12.001953 C 5.8201969 12.001953 4.0292969 10.211053 4.0292969 8.0019531 A 1 1 0 0 0 3.0292969 7.0019531 A 1 1 0 0 0 2.0292969 8.0019531 A 1 1 0 0 0 2.0351562 8.1015625 C 2.0889563 11.368762 4.7491969 14.001953 8.0292969 14.001953 C 11.342997 14.001953 14.029297 11.315653 14.029297 8.0019531 C 14.029297 4.6882531 11.342997 2.0019531 8.0292969 2.0019531 L 8.0292969 0.001953125 z"
transform="translate(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,62 @@
<?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"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="rotate-right.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<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="855"
inkscape:window-height="480"
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:#444444; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 7.9804688 0.001953125 L 7.9804688 2.0019531 C 4.6667688 2.0019531 1.9804688 4.6882531 1.9804688 8.0019531 C 1.9804688 11.315653 4.6667688 14.001953 7.9804688 14.001953 C 11.260569 14.001953 13.920809 11.368762 13.974609 8.1015625 A 1 1 0 0 0 13.980469 8.0019531 A 1 1 0 0 0 12.980469 7.0019531 A 1 1 0 0 0 11.980469 8.0019531 C 11.980469 10.211053 10.189569 12.001953 7.9804688 12.001953 C 5.7713688 12.001953 3.9804688 10.211053 3.9804688 8.0019531 C 3.9804688 5.7928531 5.7713688 4.0019531 7.9804688 4.0019531 L 7.9804688 6.0019531 L 11.980469 3.0019531 L 7.9804688 0.001953125 z"
transform="translate(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
id="svg4"
sodipodi:docname="search-case.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1480"
inkscape:window-height="750"
id="namedview6"
showgrid="false"
inkscape:zoom="14.75"
inkscape:cx="-6.5084746"
inkscape:cy="8"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
style="fill:#5c616c;fill-opacity:1"
d="M 4.7890625,2 1,13 h 1.90625 l 0.6796875,-2 h 3.8300781 l 0.6875,2 H 10 L 6.2109375,2 Z M 5.4980469,5.4375 6.6835938,9 H 4.3144531 Z M 11.5,8 v 1 h 3 C 14.715,9 15,9.305 15,9.5 V 10 h -2.5 c -0.46,0 -0.87,0.189375 -1.125,0.484375 C 11.12,10.774375 11,11.14 11,11.5 c 0,0.36 0.135625,0.725625 0.390625,1.015625 C 11.645625,12.805625 12.045,13 12.5,13 H 16 V 9.5 C 16,8.685 15.34,8 14.5,8 Z m 1,3 H 15 v 1 H 12.5 C 12.3,12 12.215625,11.944375 12.140625,11.859375 12.065625,11.774375 12,11.64 12,11.5 12,11.36 12.05,11.225625 12.125,11.140625 12.2,11.060625 12.29,11 12.5,11 Z"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
id="svg4"
sodipodi:docname="search-word.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1043"
id="namedview6"
showgrid="false"
inkscape:zoom="45.254834"
inkscape:cx="6.5123537"
inkscape:cy="6.8917559"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
style="fill:#5c616c;fill-opacity:1"
d="M 3,3 C 1.02036,3 -9.9999927e-7,4.1718311 0,6.6074219 V 8.5 A 1.5,1.5 0 0 0 1.5,10 1.5,1.5 0 0 0 3,8.5 1.5,1.5 0 0 0 1.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 2.0660618,4.29323 2.453243,4.0371834 3,4 Z M 7,3 C 5.02036,3 3.999999,4.1718311 4,6.6074219 V 8.5 A 1.5,1.5 0 0 0 5.5,10 1.5,1.5 0 0 0 7,8.5 1.5,1.5 0 0 0 5.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 6.0660608,4.29323 6.453243,4.0371834 7,4 Z M 9.5,5 A 1.5,1.5 0 0 0 8,6.5 1.5,1.5 0 0 0 9.490234,8 l 0.00781,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 8.9339389,10.70677 8.5467573,10.962817 8,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 9.5,5 Z m 4,0 A 1.5,1.5 0 0 0 12,6.5 1.5,1.5 0 0 0 13.490234,8 l 0.0078,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 12.933938,10.70677 12.546757,10.962817 12,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 13.5,5 Z"
id="path2" />
<path
style="fill:#5c616c;stroke:none;stroke-width:0.06779661;fill-opacity:1"
d="M 8.040678,11.489133 V 11.01394 l 0.2542373,-0.0411 C 8.6241141,10.91962 9.0049959,10.657098 9.1919904,10.354534 9.3774432,10.054466 9.5267422,9.2371719 9.5298381,8.5050867 9.5321781,7.9511596 9.5305475,7.9457647 9.3605343,7.9457647 8.6870054,7.9457647 8.0454876,7.2386514 8.0421669,6.4925935 8.0367069,5.2649491 9.4833138,4.5924333 10.437555,5.379 c 0.470567,0.3878808 0.518377,0.5947737 0.518377,2.2432227 0,0.9924588 -0.03143,1.6284257 -0.09756,1.9740504 -0.267079,1.3958959 -1.1175811,2.2244799 -2.3939662,2.3322709 l -0.4237288,0.03579 z"
id="path4524"
inkscape:connector-curvature="0" />
<path
style="fill:#5c616c;stroke:none;stroke-width:0.00390625;fill-opacity:1"
d="m 9.7117131,11.513295 c 0.00204,-0.0018 0.023926,-0.01759 0.048633,-0.03507 0.1798996,-0.127248 0.3444879,-0.28336 0.4858319,-0.460812 0.252616,-0.317149 0.444866,-0.723077 0.559095,-1.1805105 0.06615,-0.2648918 0.09348,-0.4533196 0.116987,-0.8066406 C 10.94809,8.6420545 10.9575,8.1721993 10.95441,7.4247931 10.952,6.840388 10.947836,6.672371 10.92997,6.438465 10.924141,6.3621478 10.9106,6.2310896 10.906461,6.2109259 l -0.0022,-0.010742 h 0.0313 c 0.0366,0 0.0324,-0.00607 0.0433,0.0625 0.01847,0.1161553 0.01782,0.070812 0.01782,1.25 0,1.1705155 -1.65e-4,1.1838055 -0.01779,1.4296875 -0.05607,0.7822203 -0.232739,1.4031726 -0.538111,1.8913516 -0.157745,0.252177 -0.36026,0.471333 -0.5979529,0.647089 l -0.048386,0.03578 h -0.043215 c -0.023798,0 -0.041548,-0.0015 -0.039504,-0.0033 z"
id="path4526"
inkscape:connector-curvature="0" />
<path
style="fill:#5c616c;stroke:none;stroke-width:0.00390625;fill-opacity:1"
d="m 8.0037065,11.500359 v -0.499846 l 0.038086,-0.0025 c 0.052532,-0.0035 0.1567951,-0.0193 0.2234149,-0.03389 C 8.640889,10.881833 8.9448125,10.678717 9.1476483,10.374368 9.3523256,10.067256 9.4641833,9.5521406 9.492672,8.7855008 L 9.497502,8.655618 H 9.512387 9.527272 L 9.524992,8.705423 C 9.5049185,9.1437939 9.4502274,9.5471986 9.3653315,9.8830268 9.3036893,10.126869 9.2445481,10.277176 9.1635619,10.395823 9.0101537,10.620571 8.748788,10.822299 8.4830209,10.92108 c -0.078653,0.02923 -0.1439508,0.04431 -0.30158,0.06961 l -0.140625,0.02258 -9.922e-4,0.475456 c -9.556e-4,0.457983 -7.301e-4,0.475456 0.00614,0.475456 0.012211,0 0.421374,-0.03493 0.483138,-0.04125 0.3949367,-0.04038 0.7679272,-0.159625 1.078125,-0.344663 0.023633,-0.0141 0.0556,-0.03409 0.071038,-0.04442 l 0.028069,-0.01879 0.04029,0.003 0.04029,0.003 -0.046244,0.03035 c -0.2869093,0.188322 -0.6383031,0.321037 -1.0300398,0.389028 -0.189836,0.03295 -0.3718111,0.04969 -0.6219586,0.0572 l -0.084961,0.0026 z"
id="path4528"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 1 L 2 15 L 9 15 L 9 13 A 5 5 0 0 1 4 8 A 5 5 0 0 1 9 3 L 9 1 L 2 1 z M 9 3 L 9 13 C 11.7614 13 14 10.7614 14 8 C 14 5.2386 11.7614 3 9 3 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" version="1.1" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 19,11 14,6 V 8 H 8 V 6 l -5,5 5,5 v -2 h 6 v 2 z"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 1 1 L 1 15 L 15 15 L 15 1 L 1 1 z M 3 3 L 7 3 L 7 13 L 3 13 L 3 3 z M 9 3 L 13 3 L 13 13 L 9 13 L 9 3 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -0,0 +1,73 @@
<?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"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="page-flow.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<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" />
<dc:title></dc:title>
</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="1920"
inkscape:window-height="1043"
id="namedview9"
showgrid="false"
inkscape:zoom="15.170655"
inkscape:cx="-1.2825494"
inkscape:cy="5.330307"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; }
</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(3 3)"
id="path5" />
<g
transform="matrix(0.64844409,0,0,0.64844409,5.8379769,5.8670545)"
id="g7">
<path
inkscape:connector-curvature="0"
class="ColorScheme-Text"
d="m 7,2 v 8 L 3.5,6.5 2,8 8,14 14,8 12.5,6.5 9,10 V 2 Z"
style="color:#dfdfdf;fill:currentColor"
id="path5-3" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; }
</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(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 416 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:#4285f4; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8.0292969 0.001953125 L 4.0292969 3.0019531 L 8.0292969 6.0019531 L 8.0292969 4.0019531 C 10.238397 4.0019531 12.029297 5.7928531 12.029297 8.0019531 C 12.029297 10.211053 10.238397 12.001953 8.0292969 12.001953 C 5.8201969 12.001953 4.0292969 10.211053 4.0292969 8.0019531 A 1 1 0 0 0 3.0292969 7.0019531 A 1 1 0 0 0 2.0292969 8.0019531 A 1 1 0 0 0 2.0351562 8.1015625 C 2.0889563 11.368762 4.7491969 14.001953 8.0292969 14.001953 C 11.342997 14.001953 14.029297 11.315653 14.029297 8.0019531 C 14.029297 4.6882531 11.342997 2.0019531 8.0292969 2.0019531 L 8.0292969 0.001953125 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 928 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:#4285f4; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 7.9804688 0.001953125 L 7.9804688 2.0019531 C 4.6667688 2.0019531 1.9804688 4.6882531 1.9804688 8.0019531 C 1.9804688 11.315653 4.6667688 14.001953 7.9804688 14.001953 C 11.260569 14.001953 13.920809 11.368762 13.974609 8.1015625 A 1 1 0 0 0 13.980469 8.0019531 A 1 1 0 0 0 12.980469 7.0019531 A 1 1 0 0 0 11.980469 8.0019531 C 11.980469 10.211053 10.189569 12.001953 7.9804688 12.001953 C 5.7713688 12.001953 3.9804688 10.211053 3.9804688 8.0019531 C 3.9804688 5.7928531 5.7713688 4.0019531 7.9804688 4.0019531 L 7.9804688 6.0019531 L 11.980469 3.0019531 L 7.9804688 0.001953125 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 928 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,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2019 BasioMeusPuga <https://github.com/BasioMeusPuga> -->
<!-- See <https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html> for an overview of the AppStream metainfo.xml spec. -->
<component type="desktop-application">
<id>io.github.BasioMeusPuga.Lector</id>
<metadata_license>FSFAP</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Lector</name>
<summary>Ebook reader and collection manager</summary>
<description>
<p>
Lector is an ebook reader and collection manager. It offers a
full-screen distraction-free view, document highlighting and
annotations, a built-in dictionary, bookmarks, and multiple profiles for
changing the way the books are presented. Lector can also edit metadata,
so you can correct information about the books, and add keywords to make
them easier to find.
</p>
<p>
It supports the following file formats:
</p>
<ul>
<li>PDF</li>
<li>EPUB</li>
<li>DjVu</li>
<li>FictionBook (.fb2)</li>
<li>Mobipocket (.mobi)</li>
<li>Amazon Kindle (.azw, .azw3, .azw4)</li>
<li>Comic book archives (.cbr, .cbz)</li>
<li>Markdown</li>
</ul>
</description>
<launchable type="desktop-id">io.github.BasioMeusPuga.Lector.desktop</launchable>
<screenshots>
<screenshot type="default">
<caption>Main window</caption>
<image>https://camo.githubusercontent.com/9e2f7346616dfdb67a77c2e6b5db148d9b4669ca/68747470733a2f2f692e696d6775722e636f6d2f35313668526b532e706e67</image>
</screenshot>
<screenshot>
<caption>Table view</caption>
<image>https://camo.githubusercontent.com/66cb4f09a81d164af50180eaa024f8e116c83815/68747470733a2f2f692e696d6775722e636f6d2f6f39416e3741522e706e67</image>
</screenshot>
<screenshot>
<caption>Book reading view, in optional Solarized display profile</caption>
<image>https://camo.githubusercontent.com/1b60a0c27383715e59ccfd9d2585544d303e08d9/68747470733a2f2f692e696d6775722e636f6d2f495447363346632e706e67</image>
</screenshot>
<screenshot>
<caption>Distraction-free view</caption>
<image>https://camo.githubusercontent.com/3289abadb668ad3e53b8ba779b52768b14b25bcf/68747470733a2f2f692e696d6775722e636f6d2f67384c747570792e706e67</image>
</screenshot>
<screenshot>
<caption>Annotation support</caption>
<image>https://camo.githubusercontent.com/80e7423b488e59ed2c05e556b4918c25522edfd1/68747470733a2f2f692e696d6775722e636f6d2f674c4b323946342e706e67</image>
</screenshot>
<screenshot>
<caption>Comic reading view</caption>
<image>https://camo.githubusercontent.com/81aaa963c6e9ed19a1605e2f2b017ce8c83ca656/68747470733a2f2f692e696d6775722e636f6d2f7276765451434d2e706e67</image>
</screenshot>
<screenshot>
<caption>Bookmark support</caption>
<image>https://camo.githubusercontent.com/ebbaffedcbd4f743ba9d67e5eaab2108407832cd/68747470733a2f2f692e696d6775722e636f6d2f5937716f55386d2e706e67</image>
</screenshot>
<screenshot>
<caption>Customizable view profiles</caption>
<image>https://camo.githubusercontent.com/0cea5e37e5c40abd4988544ba32b187db3cbe783/68747470733a2f2f692e696d6775722e636f6d2f6177453271324b2e706e67</image>
</screenshot>
<screenshot>
<caption>Metadata editor</caption>
<image>https://camo.githubusercontent.com/15362fb47e79b2d50eef507133cee8de6c1baa5c/68747470733a2f2f692e696d6775722e636f6d2f304344704e4f382e706e67</image>
</screenshot>
<screenshot>
<caption>In-app dictionary</caption>
<image>https://camo.githubusercontent.com/de3bfc5359db85da8ad94850d12fe563b5eefb87/68747470733a2f2f692e696d6775722e636f6d2f524637326d32682e706e67</image>
</screenshot>
<screenshot>
<caption>Settings window</caption>
<image>https://camo.githubusercontent.com/9ef376164f311cf518b4825684e7d5cc9d1f538c/68747470733a2f2f692e696d6775722e636f6d2f6c367a4a5861482e706e67</image>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/BasioMeusPuga/Lector</url>
<url type="bugtracker">https://github.com/BasioMeusPuga/Lector/issues</url>
<provides>
<binary>lector</binary>
</provides>
<releases>
<release version="0.5.1" date="2019-03-09">
<description>
<p>Sharper icons on hiDPI screens. More reliable dependency checks, and slight tweaks to the GUI.</p>
</description>
</release>
</releases>
</component>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -1,5 +1,24 @@
<RCC>
<qresource prefix="images">
<file>DarkIcons/rotate-left.svg</file>
<file>DarkIcons/rotate-right.svg</file>
<file>LightIcons/rotate-left.svg</file>
<file>LightIcons/rotate-right.svg</file>
<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>DarkIcons/page-single.svg</file>
<file>DarkIcons/page-flow.svg</file>
<file>LightIcons/page-double.svg</file>
<file>LightIcons/page-single.svg</file>
<file>LightIcons/page-flow.svg</file>
<file>DarkIcons/about.svg</file>
<file>DarkIcons/switches.svg</file>
<file>LightIcons/about.svg</file>
@@ -81,6 +100,8 @@
<file>LightIcons/zoom-in.svg</file>
<file>LightIcons/zoom-original.svg</file>
<file>LightIcons/zoom-out.svg</file>
<file>next.png</file>
<file>previous.png</file>
<file>QMPlay2.svg</file>
<file>color.svg</file>
<file>blank.png</file>
@@ -90,9 +111,12 @@
<file>error.svg</file>
</qresource>
<qresource prefix="translations">
<file>translations_bin/Lector_cs.qm</file>
<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_ja.qm</file>
<file>translations_bin/Lector_zh.qm</file>
<file>translations_bin/Lector_pt.qm</file>
</qresource>
</RCC>

View File

@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1119</width>
<width>1139</width>
<height>612</height>
</rect>
</property>
@@ -15,7 +15,7 @@
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QListView" name="listView">
<widget class="SaysHelloWhenClicked" name="listView">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
<horstretch>0</horstretch>
@@ -70,6 +70,22 @@
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSpinBox" name="readAtPercent">
<property name="minimum">
@@ -175,6 +191,27 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_16">
<item>
<widget class="QCheckBox" name="navBarVisible">
<property name="text">
<string>Show navigation bar</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCover">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Attempt to download missing book covers from Google books - SLOW&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Download missing covers</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
@@ -213,6 +250,98 @@ Reopen book to see changes</string>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QLabel" name="smallIncrementLabel">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;UP/DOWN ARROW - Steps to take before turning comicbook page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Small increment</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</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>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</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>
@@ -234,7 +363,30 @@ Reopen book to see changes</string>
</widget>
</item>
<item>
<widget class="QComboBox" name="languageBox"/>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="languageBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
@@ -253,6 +405,22 @@ Reopen book to see changes</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="scrollSpeedSlider">
<property name="sizePolicy">
@@ -536,15 +704,39 @@ Reopen book to see changes</string>
</layout>
</widget>
<widget class="QWidget" name="aboutPage">
<layout class="QGridLayout" name="gridLayout_6">
<layout class="QGridLayout" name="gridLayout_9">
<item row="0" column="0">
<widget class="QTextBrowser" name="aboutBox">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>false</bool>
<widget class="QTabWidget" name="aboutTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="aboutTab">
<attribute name="title">
<string>About</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QTextBrowser" name="aboutBox">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="logTab">
<attribute name="title">
<string>Log</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_10">
<item row="0" column="0">
<widget class="QPlainTextEdit" name="logBox"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@@ -553,6 +745,20 @@ Reopen book to see changes</string>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QPushButton" name="resetButton">
<property name="text">
<string>Reset Application</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearLogButton">
<property name="text">
<string>Clear Log</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
@@ -586,6 +792,13 @@ Reopen book to see changes</string>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SaysHelloWhenClicked</class>
<extends>QListView</extends>
<header>lector.widgets</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2,19 +2,20 @@
# 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.12
#
# 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(1119, 612)
Dialog.resize(1139, 612)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.listView = QtWidgets.QListView(Dialog)
self.listView = SaysHelloWhenClicked(Dialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -60,6 +61,8 @@ class Ui_Dialog(object):
self.readAtLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.readAtLabel.setObjectName("readAtLabel")
self.horizontalLayout_14.addWidget(self.readAtLabel)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_14.addItem(spacerItem)
self.readAtPercent = QtWidgets.QSpinBox(self.groupBox)
self.readAtPercent.setMinimum(90)
self.readAtPercent.setProperty("value", 95)
@@ -106,6 +109,15 @@ class Ui_Dialog(object):
self.attenuateTitles.setObjectName("attenuateTitles")
self.horizontalLayout_9.addWidget(self.attenuateTitles)
self.verticalLayout_2.addLayout(self.horizontalLayout_9)
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
self.navBarVisible = QtWidgets.QCheckBox(self.groupBox)
self.navBarVisible.setObjectName("navBarVisible")
self.horizontalLayout_16.addWidget(self.navBarVisible)
self.autoCover = QtWidgets.QCheckBox(self.groupBox)
self.autoCover.setObjectName("autoCover")
self.horizontalLayout_16.addWidget(self.autoCover)
self.verticalLayout_2.addLayout(self.horizontalLayout_16)
self.gridLayout_4.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
self.verticalLayout.addWidget(self.groupBox)
self.groupBox_2 = QtWidgets.QGroupBox(self.switchPage)
@@ -123,6 +135,34 @@ class Ui_Dialog(object):
self.cachingEnabled.setObjectName("cachingEnabled")
self.horizontalLayout_6.addWidget(self.cachingEnabled)
self.verticalLayout_3.addLayout(self.horizontalLayout_6)
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
self.horizontalLayout_12.setObjectName("horizontalLayout_12")
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
self.smallIncrementLabel = QtWidgets.QLabel(self.groupBox_2)
self.smallIncrementLabel.setObjectName("smallIncrementLabel")
self.horizontalLayout_15.addWidget(self.smallIncrementLabel)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_15.addItem(spacerItem1)
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)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_15.addItem(spacerItem2)
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()
@@ -136,7 +176,14 @@ class Ui_Dialog(object):
self.languageLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.languageLabel.setObjectName("languageLabel")
self.horizontalLayout_5.addWidget(self.languageLabel)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_5.addItem(spacerItem3)
self.languageBox = QtWidgets.QComboBox(self.groupBox_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.languageBox.sizePolicy().hasHeightForWidth())
self.languageBox.setSizePolicy(sizePolicy)
self.languageBox.setObjectName("languageBox")
self.horizontalLayout_5.addWidget(self.languageBox)
self.horizontalLayout_8.addLayout(self.horizontalLayout_5)
@@ -150,6 +197,8 @@ class Ui_Dialog(object):
self.scrollSpeedLabel.setSizePolicy(sizePolicy)
self.scrollSpeedLabel.setObjectName("scrollSpeedLabel")
self.horizontalLayout.addWidget(self.scrollSpeedLabel)
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem4)
self.scrollSpeedSlider = QtWidgets.QSlider(self.groupBox_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -166,8 +215,8 @@ class Ui_Dialog(object):
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)
spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem5, 1, 0, 1, 1)
self.stackedWidget.addWidget(self.switchPage)
self.annotationsPage = QtWidgets.QWidget()
self.annotationsPage.setObjectName("annotationsPage")
@@ -185,8 +234,8 @@ class Ui_Dialog(object):
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)
spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_6.addItem(spacerItem6)
self.newAnnotation = QtWidgets.QPushButton(self.textTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -242,8 +291,8 @@ class Ui_Dialog(object):
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)
spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_6.addItem(spacerItem7)
self.horizontalLayout_11.addLayout(self.verticalLayout_6)
self.annotationsList = QtWidgets.QListView(self.textTab)
self.annotationsList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
@@ -274,19 +323,41 @@ class Ui_Dialog(object):
self.stackedWidget.addWidget(self.annotationsPage)
self.aboutPage = QtWidgets.QWidget()
self.aboutPage.setObjectName("aboutPage")
self.gridLayout_6 = QtWidgets.QGridLayout(self.aboutPage)
self.gridLayout_9 = QtWidgets.QGridLayout(self.aboutPage)
self.gridLayout_9.setObjectName("gridLayout_9")
self.aboutTabWidget = QtWidgets.QTabWidget(self.aboutPage)
self.aboutTabWidget.setObjectName("aboutTabWidget")
self.aboutTab = QtWidgets.QWidget()
self.aboutTab.setObjectName("aboutTab")
self.gridLayout_6 = QtWidgets.QGridLayout(self.aboutTab)
self.gridLayout_6.setObjectName("gridLayout_6")
self.aboutBox = QtWidgets.QTextBrowser(self.aboutPage)
self.aboutBox = QtWidgets.QTextBrowser(self.aboutTab)
self.aboutBox.setOpenExternalLinks(True)
self.aboutBox.setOpenLinks(False)
self.aboutBox.setObjectName("aboutBox")
self.gridLayout_6.addWidget(self.aboutBox, 0, 0, 1, 1)
self.aboutTabWidget.addTab(self.aboutTab, "")
self.logTab = QtWidgets.QWidget()
self.logTab.setObjectName("logTab")
self.gridLayout_10 = QtWidgets.QGridLayout(self.logTab)
self.gridLayout_10.setObjectName("gridLayout_10")
self.logBox = QtWidgets.QPlainTextEdit(self.logTab)
self.logBox.setObjectName("logBox")
self.gridLayout_10.addWidget(self.logBox, 0, 0, 1, 1)
self.aboutTabWidget.addTab(self.logTab, "")
self.gridLayout_9.addWidget(self.aboutTabWidget, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.aboutPage)
self.verticalLayout_4.addWidget(self.stackedWidget)
self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_10.addItem(spacerItem3)
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)
spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_10.addItem(spacerItem8)
self.okButton = QtWidgets.QPushButton(Dialog)
self.okButton.setObjectName("okButton")
self.horizontalLayout_10.addWidget(self.okButton)
@@ -298,6 +369,7 @@ class Ui_Dialog(object):
self.retranslateUi(Dialog)
self.tabWidget.setCurrentIndex(0)
self.aboutTabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
@@ -318,12 +390,21 @@ class Ui_Dialog(object):
self.performCulling.setText(_translate("Dialog", "Load covers only when needed"))
self.autoTags.setText(_translate("Dialog", "Generate tags from files"))
self.attenuateTitles.setText(_translate("Dialog", "Shrink long book titles"))
self.navBarVisible.setText(_translate("Dialog", "Show navigation bar"))
self.autoCover.setToolTip(_translate("Dialog", "<html><head/><body><p>Attempt to download missing book covers from Google books - SLOW</p></body></html>"))
self.autoCover.setText(_translate("Dialog", "Download missing covers"))
self.groupBox_2.setTitle(_translate("Dialog", "Reading"))
self.hideScrollBars.setToolTip(_translate("Dialog", "Horizontal scrolling with Alt + Scroll\n"
"Reopen book to see changes"))
self.hideScrollBars.setText(_translate("Dialog", "Hide scrollbars when reading"))
self.cachingEnabled.setToolTip(_translate("Dialog", "Greatly reduces page transition time at the cost of more memory"))
self.cachingEnabled.setText(_translate("Dialog", "Cache comic / pdf pages"))
self.smallIncrementLabel.setToolTip(_translate("Dialog", "<html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html>"))
self.smallIncrementLabel.setText(_translate("Dialog", "Small increment"))
self.smallIncrementBox.setToolTip(_translate("Dialog", "<html><head/><body><p>UP/DOWN ARROW - Steps to take before turning comicbook page</p></body></html>"))
self.largeIncrementLabel.setToolTip(_translate("Dialog", "<html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html>"))
self.largeIncrementLabel.setText(_translate("Dialog", "Large increment"))
self.largeIncrementBox.setToolTip(_translate("Dialog", "<html><head/><body><p>SPACEBAR - Steps to take before turning comicbook page</p></body></html>"))
self.languageLabel.setText(_translate("Dialog", "Dictionary language"))
self.scrollSpeedLabel.setText(_translate("Dialog", "Scroll speed"))
self.newAnnotation.setToolTip(_translate("Dialog", "New"))
@@ -333,6 +414,12 @@ class Ui_Dialog(object):
self.moveDown.setToolTip(_translate("Dialog", "Move Down"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.textTab), _translate("Dialog", "Text"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.imageTab), _translate("Dialog", "Image"))
self.aboutTabWidget.setTabText(self.aboutTabWidget.indexOf(self.aboutTab), _translate("Dialog", "About"))
self.aboutTabWidget.setTabText(self.aboutTabWidget.indexOf(self.logTab), _translate("Dialog", "Log"))
self.resetButton.setText(_translate("Dialog", "Reset Application"))
self.clearLogButton.setText(_translate("Dialog", "Clear Log"))
self.okButton.setText(_translate("Dialog", "Scan Library"))
self.cancelButton.setText(_translate("Dialog", "Close"))
from lector.widgets import SaysHelloWhenClicked

View File

@@ -0,0 +1,976 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="2.0" language="cs" sourcelanguage="">
<context>
<name>AnnotationsUI</name>
<message>
<location filename="../../annotations.py" line="39"/>
<source>Text markup</source>
<translation>Značení textu</translation>
</message>
<message>
<location filename="../../annotations.py" line="126"/>
<source>New annotation</source>
<translation>Nová vysvětlivka</translation>
</message>
</context>
<context>
<name>BookToolBar</name>
<message>
<location filename="../../toolbars.py" line="43"/>
<source>View settings</source>
<translation>Zobrazit nastavení</translation>
</message>
<message>
<location filename="../../toolbars.py" line="251"/>
<source>Zoom Out (-)</source>
<translation>Oddálit (-)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="269"/>
<source>Original size (O)</source>
<translation>Původní velikost (O)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="59"/>
<source>Search (Ctrl + F)</source>
<translation>Hledat (Ctrl+F)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="122"/>
<source>Left align text</source>
<translation>Zarovnat text vlevo</translation>
</message>
<message>
<location filename="../../toolbars.py" line="136"/>
<source>Center align text</source>
<translation>Zarovnat text na střed</translation>
</message>
<message>
<location filename="../../toolbars.py" line="51"/>
<source>Add bookmark</source>
<translation>Přidat záložku</translation>
</message>
<message>
<location filename="../../toolbars.py" line="319"/>
<source>Table of Contents</source>
<translation>Obsah</translation>
</message>
<message>
<location filename="../../toolbars.py" line="129"/>
<source>Right align text</source>
<translation>Zarovnat text vpravo</translation>
</message>
<message>
<location filename="../../toolbars.py" line="216"/>
<source>Double page mode (D)</source>
<translation>Režim dvou stran (D)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="96"/>
<source>Font size</source>
<translation>Velikost písma</translation>
</message>
<message>
<location filename="../../toolbars.py" line="230"/>
<source>Invert page colors</source>
<translation>Obrátit barvy strany</translation>
</message>
<message>
<location filename="../../toolbars.py" line="246"/>
<source>Zoom in (+)</source>
<translation>Přiblížit (+)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="257"/>
<source>Fit Width (W)</source>
<translation>Přizpůsobit šířku (W)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="277"/>
<source>Background color</source>
<translation>Barva pozadí</translation>
</message>
<message>
<location filename="../../toolbars.py" line="67"/>
<source>Fullscreen (F)</source>
<translation>Celá obrazovka (F)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="111"/>
<source>Increase line spacing</source>
<translation>Zvětšit odstup řádku</translation>
</message>
<message>
<location filename="../../toolbars.py" line="116"/>
<source>Decrease line spacing</source>
<translation>Zmenšit odstup řádku</translation>
</message>
<message>
<location filename="../../toolbars.py" line="143"/>
<source>Justify text</source>
<translation>Zarovnat text do bloku</translation>
</message>
<message>
<location filename="../../toolbars.py" line="263"/>
<source>Best Fit (B)</source>
<translation>Nejlepší umístění (B)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="241"/>
<source>Rotate image anti-clockwise</source>
<translation>Otočit obrázek proti směru hodinových ručiček</translation>
</message>
<message>
<location filename="../../toolbars.py" line="237"/>
<source>Rotate image clockwise</source>
<translation>Otočit obrázek po směru hodinových ručiček</translation>
</message>
<message>
<location filename="../../toolbars.py" line="47"/>
<source>Annotations (Ctrl + N)</source>
<translation>Vysvětlivky (Ctrl+N)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="55"/>
<source>Bookmarks (Ctrl + B)</source>
<translation>Záložky (Ctrl+B)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="223"/>
<source>Manga mode (M)</source>
<translation>Režim manga (M)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="105"/>
<source>Decrease padding</source>
<translation>Zmenšit odstup</translation>
</message>
<message>
<location filename="../../toolbars.py" line="100"/>
<source>Increase padding</source>
<translation>Zvětšit odstup</translation>
</message>
<message>
<location filename="../../toolbars.py" line="71"/>
<source>Reset profile</source>
<translation>Obnovit výchozí profil</translation>
</message>
</context>
<context>
<name>DefinitionsUI</name>
<message>
<location filename="../../definitionsdialog.py" line="143"/>
<source>No definitions found in</source>
<translation>Žádná vymezení nenalezena v</translation>
</message>
</context>
<context>
<name>Dialog</name>
<message>
<location filename="../metadata.py" line="98"/>
<source>OK</source>
<translation>OK</translation>
</message>
<message>
<location filename="../settingswindow.py" line="418"/>
<source>Log</source>
<translation>Záznam</translation>
</message>
<message>
<location filename="../settingswindow.py" line="410"/>
<source>New</source>
<translation>Nový</translation>
</message>
<message>
<location filename="../settingswindow.py" line="412"/>
<source>Edit</source>
<translation>Upravit</translation>
</message>
<message>
<location filename="../metadata.py" line="97"/>
<source>Tags</source>
<translation>Značky</translation>
</message>
<message>
<location filename="../settingswindow.py" line="415"/>
<source>Text</source>
<translation>Text</translation>
</message>
<message>
<location filename="../metadata.py" line="95"/>
<source>Year</source>
<translation>Rok</translation>
</message>
<message>
<location filename="../settingswindow.py" line="389"/>
<source>Enabling reduces startup time and memory usage</source>
<translation>Povolení zmenší čas potřebný ke spuštění a využití paměti</translation>
</message>
<message>
<location filename="../settingswindow.py" line="383"/>
<source>&amp;Dark</source>
<translation>&amp;Tmavý</translation>
</message>
<message>
<location filename="../settingswindow.py" line="417"/>
<source>About</source>
<translation>O programu</translation>
</message>
<message>
<location filename="../settingswindow.py" line="422"/>
<source>Close</source>
<translation>Zavřít</translation>
</message>
<message>
<location filename="../settingswindow.py" line="416"/>
<source>Image</source>
<translation>Obrázek</translation>
</message>
<message>
<location filename="../metadata.py" line="91"/>
<source>Title</source>
<translation>Název</translation>
</message>
<message>
<location filename="../settingswindow.py" line="397"/>
<source>Horizontal scrolling with Alt + Scroll
Reopen book to see changes</source>
<translation>Vodorovné posunování pomocí Alt+posun
Otevřít knihu znovu pro zobrazení změn</translation>
</message>
<message>
<location filename="../settingswindow.py" line="406"/>
<source>Large increment</source>
<translation>Velký přírůstek</translation>
</message>
<message>
<location filename="../settingswindow.py" line="403"/>
<source>Small increment</source>
<translation>Malý přírůstek</translation>
</message>
<message>
<location filename="../settingswindow.py" line="378"/>
<source>Library</source>
<translation>Knihovna</translation>
</message>
<message>
<location filename="../settingswindow.py" line="390"/>
<source>Load covers only when needed</source>
<translation>Nahrát obaly, jen když je to potřeba</translation>
</message>
<message>
<location filename="../settingswindow.py" line="388"/>
<source>Cover shadows</source>
<translation>Stíny obalů</translation>
</message>
<message>
<location filename="../metadata.py" line="89"/>
<source>Cover (click to change)</source>
<translation>Obal knihy (klepnout pro jeho změnu)</translation>
</message>
<message>
<location filename="../settingswindow.py" line="413"/>
<source>Move Up</source>
<translation>Posunout nahoru</translation>
</message>
<message>
<location filename="../metadata.py" line="93"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../metadata.py" line="99"/>
<source>Cancel</source>
<translation>Zrušit</translation>
</message>
<message>
<location filename="../settingswindow.py" line="411"/>
<source>Delete</source>
<translation>Smazat</translation>
</message>
<message>
<location filename="../definitions.py" line="65"/>
<source>Dialog</source>
<translation>Dialog</translation>
</message>
<message>
<location filename="../settingswindow.py" line="387"/>
<source>Remember open files</source>
<translation>Zapamatovat si otevřené soubory</translation>
</message>
<message>
<location filename="../settingswindow.py" line="385"/>
<source>L&amp;ight</source>
<translation>&amp;Světlý</translation>
</message>
<message>
<location filename="../settingswindow.py" line="395"/>
<source>Download missing covers</source>
<translation>Stáhnout chybějící obaly</translation>
</message>
<message>
<location filename="../settingswindow.py" line="386"/>
<source>Startup: Refresh library</source>
<translation>Začátek: Nahrát knihovnu znovu</translation>
</message>
<message>
<location filename="../metadata.py" line="88"/>
<source>Edit metadata</source>
<translation>Upravit popisné údaje</translation>
</message>
<message>
<location filename="../settingswindow.py" line="421"/>
<source>Scan Library</source>
<translation>Prohledat knihovnu</translation>
</message>
<message>
<location filename="../settingswindow.py" line="396"/>
<source>Reading</source>
<translation>Čtení</translation>
</message>
<message>
<location filename="../settingswindow.py" line="404"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;ŠIPKA NAHORU/DOLŮ - Počet kroků před obrácením strany kresleného příběhu&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="399"/>
<source>Hide scrollbars when reading</source>
<translation>Při čtení skrýt posuvníky</translation>
</message>
<message>
<location filename="../settingswindow.py" line="419"/>
<source>Reset Application</source>
<translation>Nastavit výchozí stav programu</translation>
</message>
<message>
<location filename="../settingswindow.py" line="379"/>
<source>Consider book read at percent</source>
<translation>Považovat knihu přečtenu při procentu</translation>
</message>
<message>
<location filename="../settingswindow.py" line="394"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pokus o stažení chybějících obalů z Knih Google - POMALÝ&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="408"/>
<source>Dictionary language</source>
<translation>Jazyk slovníku</translation>
</message>
<message>
<location filename="../settingswindow.py" line="401"/>
<source>Cache comic / pdf pages</source>
<translation>Vyrovnávací paměť pro kreslené příběhy/strany PDF</translation>
</message>
<message>
<location filename="../settingswindow.py" line="392"/>
<source>Shrink long book titles</source>
<translation>Zmenšit dlouhé názvy knih</translation>
</message>
<message>
<location filename="../settingswindow.py" line="420"/>
<source>Clear Log</source>
<translation>Smazat záznam</translation>
</message>
<message>
<location filename="../settingswindow.py" line="409"/>
<source>Scroll speed</source>
<translation>Rychlost posunu</translation>
</message>
<message>
<location filename="../definitions.py" line="67"/>
<source>Play pronunciation of root word</source>
<translation>Přehrát výslovnost kořenového slova</translation>
</message>
<message>
<location filename="../settingswindow.py" line="414"/>
<source>Move Down</source>
<translation>Posunout dolů</translation>
</message>
<message>
<location filename="../settingswindow.py" line="377"/>
<source>Settings</source>
<translation>Nastavení</translation>
</message>
<message>
<location filename="../settingswindow.py" line="400"/>
<source>Greatly reduces page transition time at the cost of more memory</source>
<translation>Ohromně zmenší čas přechodu stránky za cenu více paměti</translation>
</message>
<message>
<location filename="../settingswindow.py" line="391"/>
<source>Generate tags from files</source>
<translation>Vytvořit značky ze souborů</translation>
</message>
<message>
<location filename="../settingswindow.py" line="381"/>
<source>Icon theme: </source>
<translation>Vzhled symbolů:</translation>
</message>
<message>
<location filename="../settingswindow.py" line="407"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;MEZERNÍK - Počet kroků před obrácením strany kresleného příběhu&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../metadata.py" line="96"/>
<source>Tags (comma separated)</source>
<translation>Značky (odděleny čárkou)</translation>
</message>
<message>
<location filename="../settingswindow.py" line="393"/>
<source>Show navigation bar</source>
<translation>Ukázat panel pro pohyb v knize</translation>
</message>
<message>
<location filename="../settingswindow.py" line="384"/>
<source>Restart application to see changes</source>
<translation>Kvůli změnám spustit program znovu</translation>
</message>
<message>
<location filename="../definitions.py" line="66"/>
<source>WERDS</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Library</name>
<message>
<location filename="../../library.py" line="130"/>
<source>Year</source>
<translation>Rok</translation>
</message>
<message>
<location filename="../../library.py" line="212"/>
<source> books</source>
<translation> knihy</translation>
</message>
<message>
<location filename="../../library.py" line="289"/>
<source>manually added</source>
<translation>přidáno ručně</translation>
</message>
<message>
<location filename="../../library.py" line="129"/>
<source>Author</source>
<translation>Autor</translation>
</message>
</context>
<context>
<name>LibraryToolBar</name>
<message>
<location filename="../../toolbars.py" line="483"/>
<source>Year</source>
<translation>Rok</translation>
</message>
<message>
<location filename="../../toolbars.py" line="444"/>
<source>About</source>
<translation>O</translation>
</message>
<message>
<location filename="../../toolbars.py" line="481"/>
<source>Title</source>
<translation>Název</translation>
</message>
<message>
<location filename="../../toolbars.py" line="430"/>
<source>Filter library</source>
<translation>Filtrovat knihovnu</translation>
</message>
<message>
<location filename="../../toolbars.py" line="482"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../../toolbars.py" line="484"/>
<source>Newest</source>
<translation>Nejnovější</translation>
</message>
<message>
<location filename="../../toolbars.py" line="422"/>
<source>Scan Library</source>
<translation>Prohledat knihovnu</translation>
</message>
<message>
<location filename="../../toolbars.py" line="486"/>
<source>Progress</source>
<translation>Postup</translation>
</message>
<message>
<location filename="../../toolbars.py" line="411"/>
<source>View as covers</source>
<translation>Zobrazit jako obaly</translation>
</message>
<message>
<location filename="../../toolbars.py" line="416"/>
<source>View as table</source>
<translation>Zobrazit jako tabulku</translation>
</message>
<message>
<location filename="../../toolbars.py" line="406"/>
<source>Delete book</source>
<translation>Smazat knihu</translation>
</message>
<message>
<location filename="../../toolbars.py" line="485"/>
<source>Last Read</source>
<translation>Naposledy čteno</translation>
</message>
<message>
<location filename="../../toolbars.py" line="402"/>
<source>Add book</source>
<translation>Přidat knihu</translation>
</message>
<message>
<location filename="../../toolbars.py" line="494"/>
<source>Sort by</source>
<translation>Řadit dle</translation>
</message>
<message>
<location filename="../../toolbars.py" line="433"/>
<source>Library background color</source>
<translation>Barva pozadí knihovny</translation>
</message>
<message>
<location filename="../../toolbars.py" line="475"/>
<source>Search for Title, Author, Tags...</source>
<translation>Hledat název, autora, značky...</translation>
</message>
<message>
<location filename="../../toolbars.py" line="438"/>
<source>Settings</source>
<translation>Nastavení</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.py" line="68"/>
<source>Library</source>
<translation>Knihovna</translation>
</message>
<message>
<location filename="../mainwindow.py" line="67"/>
<source>Lector</source>
<translation>Lector</translation>
</message>
</context>
<context>
<name>Main_BookToolBarUI</name>
<message>
<location filename="../../toolbars.py" line="63"/>
<source>Toggle distraction free mode (Ctrl + D)</source>
<translation>Zapnout/Vypnout režim bez rozptylování (Ctrl+D)</translation>
</message>
</context>
<context>
<name>Main_UI</name>
<message>
<location filename="../../__main__.py" line="825"/>
<source>Edit</source>
<translation>Upravit</translation>
</message>
<message>
<location filename="../../__main__.py" line="819"/>
<source>Start reading</source>
<translation>Číst</translation>
</message>
<message>
<location filename="../../__main__.py" line="630"/>
<source> Books</source>
<translation>Knihy</translation>
</message>
<message>
<location filename="../../__main__.py" line="581"/>
<source> books</source>
<translation> knihy</translation>
</message>
<message>
<location filename="../../__main__.py" line="940"/>
<source>Manually Added</source>
<translation>Přidáno ručně</translation>
</message>
<message>
<location filename="../../__main__.py" line="829"/>
<source>Delete</source>
<translation>Smazat</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="425"/>
<source>Images</source>
<translation>Obrázky</translation>
</message>
<message>
<location filename="../../__main__.py" line="563"/>
<source>Save changes and start library scan</source>
<translation>Uložit změny a začít prohledávat knihovnu</translation>
</message>
<message>
<location filename="../../__main__.py" line="478"/>
<source>eBooks</source>
<translation>Elektronické knihy</translation>
</message>
<message>
<location filename="../../__main__.py" line="550"/>
<source>Confirm deletion</source>
<translation>Potvrdit smazání</translation>
</message>
<message>
<location filename="../../__main__.py" line="835"/>
<source>Mark unread</source>
<translation>Označit jako nepřečtené</translation>
</message>
<message>
<location filename="../../__main__.py" line="546"/>
<source>Delete book(s)?</source>
<translation>Smazat knihu(y)?</translation>
</message>
<message>
<location filename="../../__main__.py" line="832"/>
<source>Mark read</source>
<translation>Označit jako přečtené</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="424"/>
<source>Save page as...</source>
<translation>Uložit stranu jako...</translation>
</message>
<message>
<location filename="../../__main__.py" line="477"/>
<source>Add books to database</source>
<translation>Přidat knihy do databáze</translation>
</message>
<message>
<location filename="../../__main__.py" line="492"/>
<source>Adding books...</source>
<translation>Přidávají se knihy...</translation>
</message>
</context>
<context>
<name>MetadataUI</name>
<message>
<location filename="../../metadatadialog.py" line="107"/>
<source>Year</source>
<translation>Rok</translation>
</message>
<message>
<location filename="../../metadatadialog.py" line="106"/>
<source>Author</source>
<translation>Autor</translation>
</message>
</context>
<context>
<name>PliantQGraphicsScene</name>
<message>
<location filename="../../widgets.py" line="621"/>
<source>Select new cover</source>
<translation>Vybrat nový obal</translation>
</message>
<message>
<location filename="../../widgets.py" line="622"/>
<source>Images</source>
<translation>Obrázky</translation>
</message>
</context>
<context>
<name>PliantQGraphicsView</name>
<message>
<location filename="../../contentwidgets.py" line="358"/>
<source>View</source>
<translation>Pohled</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="347"/>
<source>Exit Distraction Free mode</source>
<translation>Ukončit režim bez rozptylování</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="390"/>
<source>Zoom out (-)</source>
<translation>Oddálit (-)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="402"/>
<source>Original size (O)</source>
<translation>Původní velikost (O)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="343"/>
<source>Exit fullscreen</source>
<translation>Ukončit celou obrazovku</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="363"/>
<source>Double page mode (D)</source>
<translation>Režim dvou stran (D)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="377"/>
<source>Invert page colors</source>
<translation>Obrátit barvy strany</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="408"/>
<source>Bookmarks</source>
<translation>Záložky</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="386"/>
<source>Zoom in (+)</source>
<translation>Přiblížit (+)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="394"/>
<source>Fit width (W)</source>
<translation>Přizpůsobit na šířku okna (W)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="398"/>
<source>Best fit (B)</source>
<translation>Nejlepší umístění (B)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="354"/>
<source>Save page as...</source>
<translation>Uložit stranu jako...</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="370"/>
<source>Manga mode (M)</source>
<translation>Režim manga (M)</translation>
</message>
</context>
<context>
<name>PliantQTextBrowser</name>
<message>
<location filename="../../contentwidgets.py" line="763"/>
<source>View</source>
<translation>Pohled</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="849"/>
<source>Edit note</source>
<translation>Upravit poznámku</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="785"/>
<source>Exit Distraction Free mode</source>
<translation>Ukončit režim bez rozptylování</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="772"/>
<source>Single page</source>
<translation>Jedna strana</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="776"/>
<source>Double page</source>
<translation>Dvě strany</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="856"/>
<source>Add Bookmark</source>
<translation>Přidat záložku</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="781"/>
<source>Exit fullscreen</source>
<translation>Ukončit celou obrazovku</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="797"/>
<source>Define</source>
<translation>Vymezit</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="823"/>
<source>Annotate</source>
<translation>Přidat vysvětlivku</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="807"/>
<source>In this book</source>
<translation>V této knize</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="841"/>
<source>Search</source>
<translation>Hledat</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="862"/>
<source>Bookmarks</source>
<translation>Záložky</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="768"/>
<source>Flow text</source>
<translation>Plynulý text</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="802"/>
<source>Search for</source>
<translation>Hledat</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="852"/>
<source>Delete annotation</source>
<translation>Smazat vysvětlivku</translation>
</message>
</context>
<context>
<name>SettingsUI</name>
<message>
<location filename="../../settingsdialog.py" line="134"/>
<source>About</source>
<translation>O</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="71"/>
<source>Hindi</source>
<translation>Hindština</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="550"/>
<source>Delete database and exit?</source>
<translation>Smazat databázi a ukončit?</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="131"/>
<source>Library</source>
<translation>Knihovna</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="84"/>
<source>Save changes and start library scan</source>
<translation>Uložit změny a začít prohledávat knihovnu</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="329"/>
<source>Library scan in progress...</source>
<translation>Probíhá prohledávání knihovny...</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="333"/>
<source>Checking library folders</source>
<translation>Prověřují se složky s knihovnou</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="554"/>
<source>Confirm</source>
<translation>Potvrdit</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="70"/>
<source>Spanish</source>
<translation>Španělština</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="133"/>
<source>Annotations</source>
<translation>Vysvětlivky</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="69"/>
<source>English</source>
<translation>Angličtina</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="132"/>
<source>Switches</source>
<translation>Přepínače</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="349"/>
<source>Parsing files</source>
<translation>Zpracovávají se soubory</translation>
</message>
</context>
<context>
<name>SideDock</name>
<message>
<location filename="../../dockwidgets.py" line="132"/>
<source>New bookmark</source>
<translation>Lesezeichen hinzufügen</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="355"/>
<source>Search</source>
<translation>Hledat</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="131"/>
<source>Bookmarks</source>
<translation>Záložky</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="356"/>
<source>Search entire book</source>
<translation>Hledat v celé knize</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="358"/>
<source>Match word</source>
<translation>Shoda slov</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="357"/>
<source>Match case</source>
<translation>Rozlišovat velká a malá písmena</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="308"/>
<source>Annotations</source>
<translation>Vysvětlivky</translation>
</message>
</context>
<context>
<name>Tab</name>
<message>
<location filename="../../dockwidgets.py" line="272"/>
<source>Edit</source>
<translation>Upravit</translation>
</message>
<message>
<location filename="../../widgets.py" line="160"/>
<source>Note</source>
<translation>Poznámka</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="275"/>
<source>Delete</source>
<translation>Smazat</translation>
</message>
</context>
<context>
<name>TableProxyModel</name>
<message>
<location filename="../../models.py" line="75"/>
<source>Tags</source>
<translation>Značky</translation>
</message>
<message>
<location filename="../../models.py" line="73"/>
<source>Year</source>
<translation>Rok</translation>
</message>
<message>
<location filename="../../models.py" line="71"/>
<source>Title</source>
<translation>Název</translation>
</message>
<message>
<location filename="../../models.py" line="72"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../../models.py" line="74"/>
<source>Last Read</source>
<translation>Naposledy čteno</translation>
</message>
</context>
</TS>

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

@@ -0,0 +1,976 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="2.0" language="ja_JP" sourcelanguage="">
<context>
<name>AnnotationsUI</name>
<message>
<location filename="../../annotations.py" line="39"/>
<source>Text markup</source>
<translation></translation>
</message>
<message>
<location filename="../../annotations.py" line="126"/>
<source>New annotation</source>
<translation></translation>
</message>
</context>
<context>
<name>BookToolBar</name>
<message>
<location filename="../../toolbars.py" line="43"/>
<source>View settings</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="51"/>
<source>Add bookmark</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="71"/>
<source>Reset profile</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="96"/>
<source>Font size</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="100"/>
<source>Increase padding</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="105"/>
<source>Decrease padding</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="111"/>
<source>Increase line spacing</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="116"/>
<source>Decrease line spacing</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="122"/>
<source>Left align text</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="129"/>
<source>Right align text</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="136"/>
<source>Center align text</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="143"/>
<source>Justify text</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="277"/>
<source>Background color</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="319"/>
<source>Table of Contents</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="55"/>
<source>Bookmarks (Ctrl + B)</source>
<translation> (Ctrl + B)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="47"/>
<source>Annotations (Ctrl + N)</source>
<translation> (Ctrl + N)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="59"/>
<source>Search (Ctrl + F)</source>
<translation> (Ctrl + F)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="67"/>
<source>Fullscreen (F)</source>
<translation> (F)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="216"/>
<source>Double page mode (D)</source>
<translation> (D)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="223"/>
<source>Manga mode (M)</source>
<translation> (M)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="246"/>
<source>Zoom in (+)</source>
<translation> (+)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="251"/>
<source>Zoom Out (-)</source>
<translation> (-)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="257"/>
<source>Fit Width (W)</source>
<translation> (W)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="263"/>
<source>Best Fit (B)</source>
<translation> (B)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="269"/>
<source>Original size (O)</source>
<translation> (O)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="230"/>
<source>Invert page colors</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="237"/>
<source>Rotate image clockwise</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../toolbars.py" line="241"/>
<source>Rotate image anti-clockwise</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DefinitionsUI</name>
<message>
<location filename="../../definitionsdialog.py" line="143"/>
<source>No definitions found in</source>
<translation></translation>
</message>
</context>
<context>
<name>Dialog</name>
<message>
<location filename="../definitions.py" line="65"/>
<source>Dialog</source>
<translation></translation>
</message>
<message>
<location filename="../definitions.py" line="66"/>
<source>WERDS</source>
<translation>WERDS</translation>
</message>
<message>
<location filename="../definitions.py" line="67"/>
<source>Play pronunciation of root word</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="377"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="378"/>
<source>Library</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="386"/>
<source>Startup: Refresh library</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="387"/>
<source>Remember open files</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="391"/>
<source>Generate tags from files</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="388"/>
<source>Cover shadows</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="389"/>
<source>Enabling reduces startup time and memory usage</source>
<translation>使</translation>
</message>
<message>
<location filename="../settingswindow.py" line="390"/>
<source>Load covers only when needed</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="400"/>
<source>Greatly reduces page transition time at the cost of more memory</source>
<translation>使</translation>
</message>
<message>
<location filename="../settingswindow.py" line="401"/>
<source>Cache comic / pdf pages</source>
<translation>PDFのページをキャッシュする</translation>
</message>
<message>
<location filename="../settingswindow.py" line="381"/>
<source>Icon theme: </source>
<translation>: </translation>
</message>
<message>
<location filename="../settingswindow.py" line="421"/>
<source>Scan Library</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="422"/>
<source>Close</source>
<translation></translation>
</message>
<message>
<location filename="../metadata.py" line="88"/>
<source>Edit metadata</source>
<translation></translation>
</message>
<message>
<location filename="../metadata.py" line="89"/>
<source>Cover (click to change)</source>
<translation> ()</translation>
</message>
<message>
<location filename="../metadata.py" line="91"/>
<source>Title</source>
<translation></translation>
</message>
<message>
<location filename="../metadata.py" line="93"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../metadata.py" line="95"/>
<source>Year</source>
<translation></translation>
</message>
<message>
<location filename="../metadata.py" line="96"/>
<source>Tags (comma separated)</source>
<translation> ()</translation>
</message>
<message>
<location filename="../metadata.py" line="97"/>
<source>Tags</source>
<translation></translation>
</message>
<message>
<location filename="../metadata.py" line="98"/>
<source>OK</source>
<translation>OK</translation>
</message>
<message>
<location filename="../metadata.py" line="99"/>
<source>Cancel</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="397"/>
<source>Horizontal scrolling with Alt + Scroll
Reopen book to see changes</source>
<translation>Alt + Scroll
</translation>
</message>
<message>
<location filename="../settingswindow.py" line="399"/>
<source>Hide scrollbars when reading</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="384"/>
<source>Restart application to see changes</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="383"/>
<source>&amp;Dark</source>
<translation> (&amp;D)</translation>
</message>
<message>
<location filename="../settingswindow.py" line="385"/>
<source>L&amp;ight</source>
<translation> (&amp;i)</translation>
</message>
<message>
<location filename="../settingswindow.py" line="396"/>
<source>Reading</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="379"/>
<source>Consider book read at percent</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="408"/>
<source>Dictionary language</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="409"/>
<source>Scroll speed</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="415"/>
<source>Text</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="410"/>
<source>New</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="411"/>
<source>Delete</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="412"/>
<source>Edit</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="413"/>
<source>Move Up</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="414"/>
<source>Move Down</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="416"/>
<source>Image</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="392"/>
<source>Shrink long book titles</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="404"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt; - &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="403"/>
<source>Small increment</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="407"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt; - &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="406"/>
<source>Large increment</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="394"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt; Google - &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="395"/>
<source>Download missing covers</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="417"/>
<source>About</source>
<translation>About</translation>
</message>
<message>
<location filename="../settingswindow.py" line="418"/>
<source>Log</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="419"/>
<source>Reset Application</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="420"/>
<source>Clear Log</source>
<translation></translation>
</message>
<message>
<location filename="../settingswindow.py" line="393"/>
<source>Show navigation bar</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Library</name>
<message>
<location filename="../../library.py" line="129"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../../library.py" line="130"/>
<source>Year</source>
<translation></translation>
</message>
<message>
<location filename="../../library.py" line="289"/>
<source>manually added</source>
<translation></translation>
</message>
<message>
<location filename="../../library.py" line="212"/>
<source> books</source>
<translation> </translation>
</message>
</context>
<context>
<name>LibraryToolBar</name>
<message>
<location filename="../../toolbars.py" line="402"/>
<source>Add book</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="406"/>
<source>Delete book</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="433"/>
<source>Library background color</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="438"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="411"/>
<source>View as covers</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="416"/>
<source>View as table</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="430"/>
<source>Filter library</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="475"/>
<source>Search for Title, Author, Tags...</source>
<translation>...</translation>
</message>
<message>
<location filename="../../toolbars.py" line="494"/>
<source>Sort by</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="422"/>
<source>Scan Library</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="481"/>
<source>Title</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="482"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="483"/>
<source>Year</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="484"/>
<source>Newest</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="485"/>
<source>Last Read</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="486"/>
<source>Progress</source>
<translation></translation>
</message>
<message>
<location filename="../../toolbars.py" line="444"/>
<source>About</source>
<translation>Lector </translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.py" line="67"/>
<source>Lector</source>
<translation>Lector</translation>
</message>
<message>
<location filename="../mainwindow.py" line="68"/>
<source>Library</source>
<translation></translation>
</message>
</context>
<context>
<name>Main_BookToolBarUI</name>
<message>
<location filename="../../toolbars.py" line="63"/>
<source>Toggle distraction free mode (Ctrl + D)</source>
<translation> (Ctrl + D)</translation>
</message>
</context>
<context>
<name>Main_UI</name>
<message>
<location filename="../../__main__.py" line="477"/>
<source>Add books to database</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="478"/>
<source>eBooks</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="492"/>
<source>Adding books...</source>
<translation>...</translation>
</message>
<message>
<location filename="../../__main__.py" line="550"/>
<source>Confirm deletion</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="563"/>
<source>Save changes and start library scan</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="630"/>
<source> Books</source>
<translation> </translation>
</message>
<message>
<location filename="../../__main__.py" line="819"/>
<source>Start reading</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="825"/>
<source>Edit</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="829"/>
<source>Delete</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="832"/>
<source>Mark read</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="835"/>
<source>Mark unread</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="940"/>
<source>Manually Added</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="581"/>
<source> books</source>
<translation> </translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="424"/>
<source>Save page as...</source>
<translation>...</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="425"/>
<source>Images</source>
<translation></translation>
</message>
<message>
<location filename="../../__main__.py" line="546"/>
<source>Delete book(s)?</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MetadataUI</name>
<message>
<location filename="../../metadatadialog.py" line="106"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../../metadatadialog.py" line="107"/>
<source>Year</source>
<translation></translation>
</message>
</context>
<context>
<name>PliantQGraphicsScene</name>
<message>
<location filename="../../widgets.py" line="621"/>
<source>Select new cover</source>
<translation></translation>
</message>
<message>
<location filename="../../widgets.py" line="622"/>
<source>Images</source>
<translation></translation>
</message>
</context>
<context>
<name>PliantQGraphicsView</name>
<message>
<location filename="../../contentwidgets.py" line="343"/>
<source>Exit fullscreen</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="347"/>
<source>Exit Distraction Free mode</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="354"/>
<source>Save page as...</source>
<translation>...</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="358"/>
<source>View</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="363"/>
<source>Double page mode (D)</source>
<translation> (D)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="370"/>
<source>Manga mode (M)</source>
<translation> (M)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="386"/>
<source>Zoom in (+)</source>
<translation> (+)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="390"/>
<source>Zoom out (-)</source>
<translation> (-)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="394"/>
<source>Fit width (W)</source>
<translation> (W)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="398"/>
<source>Best fit (B)</source>
<translation> (B)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="402"/>
<source>Original size (O)</source>
<translation> (O)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="408"/>
<source>Bookmarks</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="377"/>
<source>Invert page colors</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PliantQTextBrowser</name>
<message>
<location filename="../../contentwidgets.py" line="781"/>
<source>Exit fullscreen</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="785"/>
<source>Exit Distraction Free mode</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="797"/>
<source>Define</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="802"/>
<source>Search for</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="807"/>
<source>In this book</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="841"/>
<source>Search</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="849"/>
<source>Edit note</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="852"/>
<source>Delete annotation</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="856"/>
<source>Add Bookmark</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="862"/>
<source>Bookmarks</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="823"/>
<source>Annotate</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="763"/>
<source>View</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="768"/>
<source>Flow text</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="772"/>
<source>Single page</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="776"/>
<source>Double page</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsUI</name>
<message>
<location filename="../../settingsdialog.py" line="69"/>
<source>English</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="70"/>
<source>Spanish</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="71"/>
<source>Hindi</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="84"/>
<source>Save changes and start library scan</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="329"/>
<source>Library scan in progress...</source>
<translation>...</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="333"/>
<source>Checking library folders</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="349"/>
<source>Parsing files</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="131"/>
<source>Library</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="132"/>
<source>Switches</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="134"/>
<source>About</source>
<translation>About</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="133"/>
<source>Annotations</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="554"/>
<source>Confirm</source>
<translation></translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="550"/>
<source>Delete database and exit?</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SideDock</name>
<message>
<location filename="../../dockwidgets.py" line="131"/>
<source>Bookmarks</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="132"/>
<source>New bookmark</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="308"/>
<source>Annotations</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="355"/>
<source>Search</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="356"/>
<source>Search entire book</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="357"/>
<source>Match case</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="358"/>
<source>Match word</source>
<translation></translation>
</message>
</context>
<context>
<name>Tab</name>
<message>
<location filename="../../dockwidgets.py" line="272"/>
<source>Edit</source>
<translation></translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="275"/>
<source>Delete</source>
<translation></translation>
</message>
<message>
<location filename="../../widgets.py" line="160"/>
<source>Note</source>
<translation></translation>
</message>
</context>
<context>
<name>TableProxyModel</name>
<message>
<location filename="../../models.py" line="71"/>
<source>Title</source>
<translation></translation>
</message>
<message>
<location filename="../../models.py" line="72"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../../models.py" line="73"/>
<source>Year</source>
<translation></translation>
</message>
<message>
<location filename="../../models.py" line="74"/>
<source>Last Read</source>
<translation></translation>
</message>
<message>
<location filename="../../models.py" line="75"/>
<source>Tags</source>
<translation></translation>
</message>
</context>
</TS>

View File

@@ -0,0 +1,976 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="2.0" language="pt_BR" sourcelanguage="">
<context>
<name>AnnotationsUI</name>
<message>
<location filename="../../annotations.py" line="39"/>
<source>Text markup</source>
<translation>Marcação do texto</translation>
</message>
<message>
<location filename="../../annotations.py" line="126"/>
<source>New annotation</source>
<translation>Nova anotação</translation>
</message>
</context>
<context>
<name>BookToolBar</name>
<message>
<location filename="../../toolbars.py" line="43"/>
<source>View settings</source>
<translation>Ver configurações</translation>
</message>
<message>
<location filename="../../toolbars.py" line="51"/>
<source>Add bookmark</source>
<translation>Adicionar marca páginas</translation>
</message>
<message>
<location filename="../../toolbars.py" line="71"/>
<source>Reset profile</source>
<translation>Reinicializar perfil</translation>
</message>
<message>
<location filename="../../toolbars.py" line="96"/>
<source>Font size</source>
<translation>Tamanho da fonte</translation>
</message>
<message>
<location filename="../../toolbars.py" line="100"/>
<source>Increase padding</source>
<translation>Aumentar padding</translation>
</message>
<message>
<location filename="../../toolbars.py" line="105"/>
<source>Decrease padding</source>
<translation>Diminuir padding</translation>
</message>
<message>
<location filename="../../toolbars.py" line="111"/>
<source>Increase line spacing</source>
<translation>Aumentar espaçamento entre linhas</translation>
</message>
<message>
<location filename="../../toolbars.py" line="116"/>
<source>Decrease line spacing</source>
<translation>Diminuir espaçamento entre linhas</translation>
</message>
<message>
<location filename="../../toolbars.py" line="122"/>
<source>Left align text</source>
<translation>Alinhar texto à esquerda</translation>
</message>
<message>
<location filename="../../toolbars.py" line="129"/>
<source>Right align text</source>
<translation>Alinhar texto à direita</translation>
</message>
<message>
<location filename="../../toolbars.py" line="136"/>
<source>Center align text</source>
<translation>Centralizar texto</translation>
</message>
<message>
<location filename="../../toolbars.py" line="143"/>
<source>Justify text</source>
<translation>Justificar texto</translation>
</message>
<message>
<location filename="../../toolbars.py" line="277"/>
<source>Background color</source>
<translation>Cor do fundo</translation>
</message>
<message>
<location filename="../../toolbars.py" line="319"/>
<source>Table of Contents</source>
<translation>Índice</translation>
</message>
<message>
<location filename="../../toolbars.py" line="55"/>
<source>Bookmarks (Ctrl + B)</source>
<translation>Marcadores de página (Ctrl + B)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="47"/>
<source>Annotations (Ctrl + N)</source>
<translation>Anotações (Ctrl + N)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="59"/>
<source>Search (Ctrl + F)</source>
<translation>Procurar (Ctrl + F)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="67"/>
<source>Fullscreen (F)</source>
<translation>Tela cheia (F)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="216"/>
<source>Double page mode (D)</source>
<translation>Modo de páginas dupla (D)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="223"/>
<source>Manga mode (M)</source>
<translation>Modo mangá (M)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="246"/>
<source>Zoom in (+)</source>
<translation>Aumentar zoom (+)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="251"/>
<source>Zoom Out (-)</source>
<translation>Diminuir zoom (-)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="257"/>
<source>Fit Width (W)</source>
<translation>Ajustar largura (W)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="263"/>
<source>Best Fit (B)</source>
<translation>Melhor ajuste (B)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="269"/>
<source>Original size (O)</source>
<translation>Tamanho original (O)</translation>
</message>
<message>
<location filename="../../toolbars.py" line="230"/>
<source>Invert page colors</source>
<translation>Inverter cores da página</translation>
</message>
<message>
<location filename="../../toolbars.py" line="237"/>
<source>Rotate image clockwise</source>
<translation>Rotacionar imagem no sentido horário</translation>
</message>
<message>
<location filename="../../toolbars.py" line="241"/>
<source>Rotate image anti-clockwise</source>
<translation>Rotacionar imagem no sentido anti-horário</translation>
</message>
</context>
<context>
<name>DefinitionsUI</name>
<message>
<location filename="../../definitionsdialog.py" line="143"/>
<source>No definitions found in</source>
<translation>Nenhuma definição achada em</translation>
</message>
</context>
<context>
<name>Dialog</name>
<message>
<location filename="../definitions.py" line="65"/>
<source>Dialog</source>
<translation>Diálogo</translation>
</message>
<message>
<location filename="../definitions.py" line="66"/>
<source>WERDS</source>
<translation>PALAVRAS</translation>
</message>
<message>
<location filename="../definitions.py" line="67"/>
<source>Play pronunciation of root word</source>
<translation>Reproduzir pronúncia da palavra raiz</translation>
</message>
<message>
<location filename="../settingswindow.py" line="377"/>
<source>Settings</source>
<translation>Configurações</translation>
</message>
<message>
<location filename="../settingswindow.py" line="378"/>
<source>Library</source>
<translation>Biblioteca</translation>
</message>
<message>
<location filename="../settingswindow.py" line="386"/>
<source>Startup: Refresh library</source>
<translation>Inicialização: Atualizar biblioteca</translation>
</message>
<message>
<location filename="../settingswindow.py" line="387"/>
<source>Remember open files</source>
<translation>Lembrar dos arquivos abertos</translation>
</message>
<message>
<location filename="../settingswindow.py" line="391"/>
<source>Generate tags from files</source>
<translation>Gerar palavras-chave a partir de arquivos</translation>
</message>
<message>
<location filename="../settingswindow.py" line="388"/>
<source>Cover shadows</source>
<translation>Sombra nas capas</translation>
</message>
<message>
<location filename="../settingswindow.py" line="389"/>
<source>Enabling reduces startup time and memory usage</source>
<translation>Habilitar diminuir tempo de inicialização e uso de memória</translation>
</message>
<message>
<location filename="../settingswindow.py" line="390"/>
<source>Load covers only when needed</source>
<translation>Carregar capas somente quando necessário</translation>
</message>
<message>
<location filename="../settingswindow.py" line="400"/>
<source>Greatly reduces page transition time at the cost of more memory</source>
<translation>Reduz bastante o tempo de transição de páginas pelo custo de mais memória</translation>
</message>
<message>
<location filename="../settingswindow.py" line="401"/>
<source>Cache comic / pdf pages</source>
<translation>Armazenar comic / páginas pdf em cache</translation>
</message>
<message>
<location filename="../settingswindow.py" line="381"/>
<source>Icon theme: </source>
<translation>Tema do ícone: </translation>
</message>
<message>
<location filename="../settingswindow.py" line="421"/>
<source>Scan Library</source>
<translation>Scanear Biblioteca</translation>
</message>
<message>
<location filename="../settingswindow.py" line="422"/>
<source>Close</source>
<translation>Fechar</translation>
</message>
<message>
<location filename="../metadata.py" line="88"/>
<source>Edit metadata</source>
<translation>Editar metadados</translation>
</message>
<message>
<location filename="../metadata.py" line="89"/>
<source>Cover (click to change)</source>
<translation>Capa (clique para mudar)</translation>
</message>
<message>
<location filename="../metadata.py" line="91"/>
<source>Title</source>
<translation>Título</translation>
</message>
<message>
<location filename="../metadata.py" line="93"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../metadata.py" line="95"/>
<source>Year</source>
<translation>Ano</translation>
</message>
<message>
<location filename="../metadata.py" line="96"/>
<source>Tags (comma separated)</source>
<translation>Palavras-chave (separadas por vígula)</translation>
</message>
<message>
<location filename="../metadata.py" line="97"/>
<source>Tags</source>
<translation>Palavras-chave</translation>
</message>
<message>
<location filename="../metadata.py" line="98"/>
<source>OK</source>
<translation>OK</translation>
</message>
<message>
<location filename="../metadata.py" line="99"/>
<source>Cancel</source>
<translation>Cancelar</translation>
</message>
<message>
<location filename="../settingswindow.py" line="397"/>
<source>Horizontal scrolling with Alt + Scroll
Reopen book to see changes</source>
<translation>Rolagem horizontal com Alt + rolagem
Reabrir livro para ver mudanças</translation>
</message>
<message>
<location filename="../settingswindow.py" line="399"/>
<source>Hide scrollbars when reading</source>
<translation>Esconder barra de rolagem quando estiver lendo</translation>
</message>
<message>
<location filename="../settingswindow.py" line="384"/>
<source>Restart application to see changes</source>
<translation>Reabrir aplicativo para ver as mudanças</translation>
</message>
<message>
<location filename="../settingswindow.py" line="383"/>
<source>&amp;Dark</source>
<translation>&amp;Escuro</translation>
</message>
<message>
<location filename="../settingswindow.py" line="385"/>
<source>L&amp;ight</source>
<translation>C&amp;laro</translation>
</message>
<message>
<location filename="../settingswindow.py" line="396"/>
<source>Reading</source>
<translation>Leitura</translation>
</message>
<message>
<location filename="../settingswindow.py" line="379"/>
<source>Consider book read at percent</source>
<translation>Considerar livro lido na porcentagem</translation>
</message>
<message>
<location filename="../settingswindow.py" line="408"/>
<source>Dictionary language</source>
<translation>Língua do dicionário</translation>
</message>
<message>
<location filename="../settingswindow.py" line="409"/>
<source>Scroll speed</source>
<translation>Velocidade de rolagem</translation>
</message>
<message>
<location filename="../settingswindow.py" line="415"/>
<source>Text</source>
<translation>Texto</translation>
</message>
<message>
<location filename="../settingswindow.py" line="410"/>
<source>New</source>
<translation>Novo</translation>
</message>
<message>
<location filename="../settingswindow.py" line="411"/>
<source>Delete</source>
<translation>Deletar</translation>
</message>
<message>
<location filename="../settingswindow.py" line="412"/>
<source>Edit</source>
<translation>Editar</translation>
</message>
<message>
<location filename="../settingswindow.py" line="413"/>
<source>Move Up</source>
<translation>Mover para cima</translation>
</message>
<message>
<location filename="../settingswindow.py" line="414"/>
<source>Move Down</source>
<translation>Mover para baixo</translation>
</message>
<message>
<location filename="../settingswindow.py" line="416"/>
<source>Image</source>
<translation>Imagem</translation>
</message>
<message>
<location filename="../settingswindow.py" line="392"/>
<source>Shrink long book titles</source>
<translation>Encurtar títulos de livros longos</translation>
</message>
<message>
<location filename="../settingswindow.py" line="404"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;SETAS CIMA/BAIXO - Passos para virar a página de história em quadrinhos&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="403"/>
<source>Small increment</source>
<translation>Pequeno incremento</translation>
</message>
<message>
<location filename="../settingswindow.py" line="407"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BARRA DE ESPAÇO - Passos para virar a página de história em quadrinhos&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="406"/>
<source>Large increment</source>
<translation>Grande incremento</translation>
</message>
<message>
<location filename="../settingswindow.py" line="394"/>
<source>&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;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tentativa de baixar capas ausentes do Google books - DEVAGAR&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../settingswindow.py" line="395"/>
<source>Download missing covers</source>
<translation>Fazer download das capas ausentes</translation>
</message>
<message>
<location filename="../settingswindow.py" line="417"/>
<source>About</source>
<translation>Sobre</translation>
</message>
<message>
<location filename="../settingswindow.py" line="418"/>
<source>Log</source>
<translation>Log</translation>
</message>
<message>
<location filename="../settingswindow.py" line="419"/>
<source>Reset Application</source>
<translation>Resetar aplicativo</translation>
</message>
<message>
<location filename="../settingswindow.py" line="420"/>
<source>Clear Log</source>
<translation>Apagar log</translation>
</message>
<message>
<location filename="../settingswindow.py" line="393"/>
<source>Show navigation bar</source>
<translation>Mostrar barra de navegação</translation>
</message>
</context>
<context>
<name>Library</name>
<message>
<location filename="../../library.py" line="129"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../../library.py" line="130"/>
<source>Year</source>
<translation>Ano</translation>
</message>
<message>
<location filename="../../library.py" line="289"/>
<source>manually added</source>
<translation>adicionado manualmente</translation>
</message>
<message>
<location filename="../../library.py" line="212"/>
<source> books</source>
<translation> livros</translation>
</message>
</context>
<context>
<name>LibraryToolBar</name>
<message>
<location filename="../../toolbars.py" line="402"/>
<source>Add book</source>
<translation>Adicionar livro</translation>
</message>
<message>
<location filename="../../toolbars.py" line="406"/>
<source>Delete book</source>
<translation>Deletar livro</translation>
</message>
<message>
<location filename="../../toolbars.py" line="433"/>
<source>Library background color</source>
<translation>Cor de fundo da biblioteca</translation>
</message>
<message>
<location filename="../../toolbars.py" line="438"/>
<source>Settings</source>
<translation>Configurações</translation>
</message>
<message>
<location filename="../../toolbars.py" line="411"/>
<source>View as covers</source>
<translation>Visualizar como capas</translation>
</message>
<message>
<location filename="../../toolbars.py" line="416"/>
<source>View as table</source>
<translation>Visualizar como tabela</translation>
</message>
<message>
<location filename="../../toolbars.py" line="430"/>
<source>Filter library</source>
<translation>Filtrar biblioteca</translation>
</message>
<message>
<location filename="../../toolbars.py" line="475"/>
<source>Search for Title, Author, Tags...</source>
<translation>Procurar por título, autor, palavras-chave...</translation>
</message>
<message>
<location filename="../../toolbars.py" line="494"/>
<source>Sort by</source>
<translation>Ordenar por</translation>
</message>
<message>
<location filename="../../toolbars.py" line="422"/>
<source>Scan Library</source>
<translation>Scanear biblioteca</translation>
</message>
<message>
<location filename="../../toolbars.py" line="481"/>
<source>Title</source>
<translation>Título</translation>
</message>
<message>
<location filename="../../toolbars.py" line="482"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../../toolbars.py" line="483"/>
<source>Year</source>
<translation>Ano</translation>
</message>
<message>
<location filename="../../toolbars.py" line="484"/>
<source>Newest</source>
<translation>Mais novo</translation>
</message>
<message>
<location filename="../../toolbars.py" line="485"/>
<source>Last Read</source>
<translation>Último lido</translation>
</message>
<message>
<location filename="../../toolbars.py" line="486"/>
<source>Progress</source>
<translation>Progresso</translation>
</message>
<message>
<location filename="../../toolbars.py" line="444"/>
<source>About</source>
<translation>Sobre</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.py" line="67"/>
<source>Lector</source>
<translation>Lector</translation>
</message>
<message>
<location filename="../mainwindow.py" line="68"/>
<source>Library</source>
<translation>Biblioteca</translation>
</message>
</context>
<context>
<name>Main_BookToolBarUI</name>
<message>
<location filename="../../toolbars.py" line="63"/>
<source>Toggle distraction free mode (Ctrl + D)</source>
<translation>Alternar modo sem distrações (Ctrl +D)</translation>
</message>
</context>
<context>
<name>Main_UI</name>
<message>
<location filename="../../__main__.py" line="477"/>
<source>Add books to database</source>
<translation>Adicionar livros na base de dados</translation>
</message>
<message>
<location filename="../../__main__.py" line="478"/>
<source>eBooks</source>
<translation>eBooks</translation>
</message>
<message>
<location filename="../../__main__.py" line="492"/>
<source>Adding books...</source>
<translation>Adicionando livros...</translation>
</message>
<message>
<location filename="../../__main__.py" line="550"/>
<source>Confirm deletion</source>
<translation>Confirmar remoção</translation>
</message>
<message>
<location filename="../../__main__.py" line="563"/>
<source>Save changes and start library scan</source>
<translation>Salvar mudanças e começar scanear bibloteca</translation>
</message>
<message>
<location filename="../../__main__.py" line="630"/>
<source> Books</source>
<translation> Livros</translation>
</message>
<message>
<location filename="../../__main__.py" line="819"/>
<source>Start reading</source>
<translation>Começar leitura</translation>
</message>
<message>
<location filename="../../__main__.py" line="825"/>
<source>Edit</source>
<translation>Editar</translation>
</message>
<message>
<location filename="../../__main__.py" line="829"/>
<source>Delete</source>
<translation>Deletar</translation>
</message>
<message>
<location filename="../../__main__.py" line="832"/>
<source>Mark read</source>
<translation>Marcar como lido</translation>
</message>
<message>
<location filename="../../__main__.py" line="835"/>
<source>Mark unread</source>
<translation>Marcar como não-lido</translation>
</message>
<message>
<location filename="../../__main__.py" line="940"/>
<source>Manually Added</source>
<translation>Adicionado manualmente</translation>
</message>
<message>
<location filename="../../__main__.py" line="581"/>
<source> books</source>
<translation> livros</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="424"/>
<source>Save page as...</source>
<translation>Salvar página como...</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="425"/>
<source>Images</source>
<translation>Imagens</translation>
</message>
<message>
<location filename="../../__main__.py" line="546"/>
<source>Delete book(s)?</source>
<translation>Deletar livro(s)?</translation>
</message>
</context>
<context>
<name>MetadataUI</name>
<message>
<location filename="../../metadatadialog.py" line="106"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../../metadatadialog.py" line="107"/>
<source>Year</source>
<translation>Ano</translation>
</message>
</context>
<context>
<name>PliantQGraphicsScene</name>
<message>
<location filename="../../widgets.py" line="621"/>
<source>Select new cover</source>
<translation>Selecionar nova capa</translation>
</message>
<message>
<location filename="../../widgets.py" line="622"/>
<source>Images</source>
<translation>Imagens</translation>
</message>
</context>
<context>
<name>PliantQGraphicsView</name>
<message>
<location filename="../../contentwidgets.py" line="343"/>
<source>Exit fullscreen</source>
<translation>Sair da tela cheia</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="347"/>
<source>Exit Distraction Free mode</source>
<translation>Sair do modo sem distrações</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="354"/>
<source>Save page as...</source>
<translation>Salvar página como...</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="358"/>
<source>View</source>
<translation>Visualização</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="363"/>
<source>Double page mode (D)</source>
<translation>Modo de páginas dupla (D)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="370"/>
<source>Manga mode (M)</source>
<translation>Modo mangá (M)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="386"/>
<source>Zoom in (+)</source>
<translation>Aumentar zoom (+)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="390"/>
<source>Zoom out (-)</source>
<translation>Diminuir zoom (-)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="394"/>
<source>Fit width (W)</source>
<translation>Ajustar largura (W)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="398"/>
<source>Best fit (B)</source>
<translation>Melhor ajuste (B)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="402"/>
<source>Original size (O)</source>
<translation>Tamanho original (O)</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="408"/>
<source>Bookmarks</source>
<translation>Marca páginas</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="377"/>
<source>Invert page colors</source>
<translation>Inverter cores da página</translation>
</message>
</context>
<context>
<name>PliantQTextBrowser</name>
<message>
<location filename="../../contentwidgets.py" line="781"/>
<source>Exit fullscreen</source>
<translation>Sair da tela cheia</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="785"/>
<source>Exit Distraction Free mode</source>
<translation>Sair do modo sem distrações</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="797"/>
<source>Define</source>
<translation>Definir</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="802"/>
<source>Search for</source>
<translation>Procurar por</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="807"/>
<source>In this book</source>
<translation>Neste livro</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="841"/>
<source>Search</source>
<translation>Procurar</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="849"/>
<source>Edit note</source>
<translation>Editar nota</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="852"/>
<source>Delete annotation</source>
<translation>Deletar anotação</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="856"/>
<source>Add Bookmark</source>
<translation>Adicionar marca páginas</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="862"/>
<source>Bookmarks</source>
<translation>Marca páginas</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="823"/>
<source>Annotate</source>
<translation>Anotar</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="763"/>
<source>View</source>
<translation>Visualização</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="768"/>
<source>Flow text</source>
<translation></translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="772"/>
<source>Single page</source>
<translation>Página simples</translation>
</message>
<message>
<location filename="../../contentwidgets.py" line="776"/>
<source>Double page</source>
<translation>Página dupla</translation>
</message>
</context>
<context>
<name>SettingsUI</name>
<message>
<location filename="../../settingsdialog.py" line="69"/>
<source>English</source>
<translation>Inglês</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="70"/>
<source>Spanish</source>
<translation>Espanhol</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="71"/>
<source>Hindi</source>
<translation>Hindi</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="84"/>
<source>Save changes and start library scan</source>
<translation>Salvar mudanças e começar scanear bibloteca</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="329"/>
<source>Library scan in progress...</source>
<translation>Scan da biblioteca em progresso...</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="333"/>
<source>Checking library folders</source>
<translation>Checando pastas da biblioteca</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="349"/>
<source>Parsing files</source>
<translation>Analisando arquivos</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="131"/>
<source>Library</source>
<translation>Biblioteca</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="132"/>
<source>Switches</source>
<translation>Configurações</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="134"/>
<source>About</source>
<translation>Sobre</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="133"/>
<source>Annotations</source>
<translation>Anotações</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="554"/>
<source>Confirm</source>
<translation>Confirmar</translation>
</message>
<message>
<location filename="../../settingsdialog.py" line="550"/>
<source>Delete database and exit?</source>
<translation>Deletar banco de dados e sair?</translation>
</message>
</context>
<context>
<name>SideDock</name>
<message>
<location filename="../../dockwidgets.py" line="131"/>
<source>Bookmarks</source>
<translation>Marca páginas</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="132"/>
<source>New bookmark</source>
<translation>Novo marca páginas</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="308"/>
<source>Annotations</source>
<translation>Anotações</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="355"/>
<source>Search</source>
<translation>Procurar</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="356"/>
<source>Search entire book</source>
<translation>Procurar no livro todo</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="357"/>
<source>Match case</source>
<translation>Caso correspondente</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="358"/>
<source>Match word</source>
<translation>Palavra correspondente</translation>
</message>
</context>
<context>
<name>Tab</name>
<message>
<location filename="../../dockwidgets.py" line="272"/>
<source>Edit</source>
<translation>Editar</translation>
</message>
<message>
<location filename="../../dockwidgets.py" line="275"/>
<source>Delete</source>
<translation>Deletar</translation>
</message>
<message>
<location filename="../../widgets.py" line="160"/>
<source>Note</source>
<translation>Nota</translation>
</message>
</context>
<context>
<name>TableProxyModel</name>
<message>
<location filename="../../models.py" line="71"/>
<source>Title</source>
<translation>Título</translation>
</message>
<message>
<location filename="../../models.py" line="72"/>
<source>Author</source>
<translation>Autor</translation>
</message>
<message>
<location filename="../../models.py" line="73"/>
<source>Year</source>
<translation>Ano</translation>
</message>
<message>
<location filename="../../models.py" line="74"/>
<source>Last Read</source>
<translation>Último lido</translation>
</message>
<message>
<location filename="../../models.py" line="75"/>
<source>Tags</source>
<translation>Palavras-chave</translation>
</message>
</context>
</TS>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,10 +17,13 @@
# Keep in mind that all integer / boolean settings are returned as strings
import os
import logging
from ast import literal_eval
from PyQt5 import QtCore, QtGui
logger = logging.getLogger(__name__)
class Settings:
def __init__(self, parent):
@@ -119,10 +122,22 @@ 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['nav_bar'] = literal_eval(self.settings.value(
'navBar', 'False').capitalize())
self.parent.settings['scroll_speed'] = int(self.settings.value('scrollSpeed', 7))
self.parent.settings['consider_read_at'] = int(self.settings.value('considerReadAt', 95))
self.parent.settings['small_increment'] = int(self.settings.value('smallIncrement', 4))
self.parent.settings['large_increment'] = int(self.settings.value('largeIncrement', 2))
self.parent.settings['attenuate_titles'] = literal_eval(self.settings.value(
'attenuateTitles', 'False').capitalize())
self.parent.settings['double_page_mode'] = literal_eval(self.settings.value(
'doublePageMode', 'False').capitalize())
self.parent.settings['manga_mode'] = literal_eval(self.settings.value(
'mangaMode', 'False').capitalize())
self.parent.settings['invert_colors'] = literal_eval(self.settings.value(
'invertColors', 'False').capitalize())
self.settings.endGroup()
self.settings.beginGroup('dialogSettings')
@@ -137,8 +152,9 @@ class Settings:
self.parent.settings['annotations'] = list()
self.settings.endGroup()
logger.info('Settings loaded')
def save_settings(self):
print('Saving settings...')
current_settings = self.parent.settings
self.settings.beginGroup('mainWindow')
@@ -201,8 +217,15 @@ class Settings:
self.settings.setValue('cachingEnabled', str(current_settings['caching_enabled']))
self.settings.setValue('hideScrollBars', str(current_settings['hide_scrollbars']))
self.settings.setValue('attenuateTitles', str(current_settings['attenuate_titles']))
self.settings.setValue('navBar', str(current_settings['nav_bar']))
self.settings.setValue('autoCover', str(current_settings['auto_cover']))
self.settings.setValue('scrollSpeed', current_settings['scroll_speed'])
self.settings.setValue('considerReadAt', current_settings['consider_read_at'])
self.settings.setValue('doublePageMode', str(current_settings['double_page_mode']))
self.settings.setValue('mangaMode', str(current_settings['manga_mode']))
self.settings.setValue('invertColors', str(current_settings['invert_colors']))
self.settings.setValue('smallIncrement', current_settings['small_increment'])
self.settings.setValue('largeIncrement', current_settings['large_increment'])
self.settings.endGroup()
self.settings.beginGroup('dialogSettings')
@@ -212,3 +235,5 @@ class Settings:
self.settings.beginGroup('annotations')
self.settings.setValue('annotationList', current_settings['annotations'])
self.settings.endGroup()
logger.info('Settings saved')

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
import os
import copy
import logging
import pathlib
from PyQt5 import QtWidgets, QtCore, QtGui
@@ -29,6 +30,9 @@ from lector.models import MostExcellentFileSystemModel
from lector.threaded import BackGroundBookSearch, BackGroundBookAddition
from lector.resources import settingswindow
from lector.settings import Settings
from lector.logger import logger_filename, VERSION
logger = logging.getLogger(__name__)
class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
@@ -52,7 +56,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
install_dir = pathlib.Path(install_dir).parents[1]
aboutfile_path = os.path.join(install_dir, 'lector', 'resources', 'about.html')
with open(aboutfile_path) as about_html:
self.aboutBox.setHtml(about_html.read())
html = about_html.readlines()
html.insert(
8, f'<h3 style="text-align: center;">v{VERSION}</h3>\n')
self.aboutBox.setHtml(''.join(html))
self.paths = None
self.thread = None
@@ -96,8 +103,12 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.cachingEnabled.setChecked(self.main_window.settings['caching_enabled'])
self.hideScrollBars.setChecked(self.main_window.settings['hide_scrollbars'])
self.attenuateTitles.setChecked(self.main_window.settings['attenuate_titles'])
self.navBarVisible.setChecked(self.main_window.settings['nav_bar'])
self.autoCover.setChecked(self.main_window.settings['auto_cover'])
self.scrollSpeedSlider.setValue(self.main_window.settings['scroll_speed'])
self.readAtPercent.setValue(self.main_window.settings['consider_read_at'])
self.smallIncrementBox.setValue(self.main_window.settings['small_increment'])
self.largeIncrementBox.setValue(self.main_window.settings['large_increment'])
self.autoTags.clicked.connect(self.manage_checkboxes)
self.coverShadows.clicked.connect(self.manage_checkboxes)
@@ -107,11 +118,15 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.cachingEnabled.clicked.connect(self.manage_checkboxes)
self.hideScrollBars.clicked.connect(self.manage_checkboxes)
self.attenuateTitles.clicked.connect(self.manage_checkboxes)
self.navBarVisible.clicked.connect(self.manage_checkboxes)
self.autoCover.clicked.connect(self.manage_checkboxes)
self.scrollSpeedSlider.valueChanged.connect(self.change_scroll_speed)
self.readAtPercent.valueChanged.connect(self.change_read_at)
self.smallIncrementBox.valueChanged.connect(self.change_increment)
self.largeIncrementBox.valueChanged.connect(self.change_increment)
# Generate the QStandardItemModel for the listView
self.listModel = QtGui.QStandardItemModel()
self.listModel = QtGui.QStandardItemModel(self.listView)
library_string = self._translate('SettingsUI', 'Library')
switches_string = self._translate('SettingsUI', 'Switches')
@@ -134,7 +149,9 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.main_window.QImageFactory.get_image(this_icon))
self.listModel.appendRow(item)
self.listView.setModel(self.listModel)
self.listView.clicked.connect(self.page_switch)
# Custom signal to account for page changes
self.listView.newIndexSignal.connect(self.list_index_changed)
# Annotation related buttons
# Icon names
@@ -166,11 +183,37 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
# Generate the filesystem treeView
self.generate_tree()
# About... About
self.aboutTabWidget.setDocumentMode(True)
self.aboutTabWidget.setContentsMargins(0, 0, 0, 0)
self.logBox.setReadOnly(True)
# About buttons
self.resetButton.clicked.connect(self.delete_database)
self.clearLogButton.clicked.connect(self.clear_log)
# Hide the image annotation tab
# TODO
# Maybe get off your lazy ass and write something for this
self.tabWidget.setContentsMargins(0, 0, 0, 0)
self.tabWidget.tabBar().setVisible(False)
def list_index_changed(self, index):
switch_to = index.row()
self.stackedWidget.setCurrentIndex(switch_to)
valid_buttons = {
0: (self.okButton,),
3: (self.resetButton, self.clearLogButton),}
for i in valid_buttons:
if i == switch_to:
for j in valid_buttons[i]:
j.setVisible(True)
else:
for j in valid_buttons[i]:
j.setVisible(False)
def generate_tree(self):
# Fetch all directories in the database
paths = database.DatabaseFunctions(
@@ -183,7 +226,7 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.main_window.generate_library_filter_menu(paths)
directory_data = {}
if not paths:
print('Database: No paths for settings...')
logger.warning('No book paths saved')
else:
# Convert to the dictionary format that is
# to be fed into the QFileSystemModel
@@ -194,7 +237,8 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
'check_state': i[3]}
self.filesystemModel = MostExcellentFileSystemModel(directory_data)
self.filesystemModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
self.filesystemModel.setFilter(
QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
self.treeView.setModel(self.filesystemModel)
# TODO
@@ -207,7 +251,8 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
# Set the treeView and QFileSystemModel to its desired state
selected_paths = [
i for i in directory_data if directory_data[i]['check_state'] == QtCore.Qt.Checked]
i for i in directory_data
if directory_data[i]['check_state'] == QtCore.Qt.Checked]
expand_paths = set()
for i in selected_paths:
@@ -255,9 +300,13 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
self.database_path).set_library_paths(data_pairs)
if not data_pairs:
logger.error('Can\'t scan - No book paths saved')
try:
if self.sender().objectName() == 'reloadLibrary':
self.show()
treeViewIndex = self.listModel.index(0, 0)
self.listView.setCurrentIndex(treeViewIndex)
return
except AttributeError:
pass
@@ -303,16 +352,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
# We now create a new thread to put those files into the database
self.thread = BackGroundBookAddition(
self.thread.valid_files, self.database_path, 'automatic', self.main_window)
self.thread.finished.connect(self.main_window.move_on)
self.thread.finished.connect(
lambda: self.main_window.move_on(self.thread.errors))
self.thread.start()
def page_switch(self, index):
self.stackedWidget.setCurrentIndex(index.row())
if index.row() == 0:
self.okButton.setVisible(True)
else:
self.okButton.setVisible(False)
def cancel_pressed(self):
self.filesystemModel.tag_data = copy.deepcopy(self.tag_data_copy)
self.hide()
@@ -322,8 +365,15 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
event.accept()
def showEvent(self, event):
# Load log into the plainTextEdit
with open(logger_filename) as infile:
log_text = infile.read()
self.logBox.setPlainText(log_text)
# Annotation preview
self.format_preview()
# Make copy of tags in case of a nope.jpg
self.tag_data_copy = copy.deepcopy(self.filesystemModel.tag_data)
event.accept()
def no_more_settings(self):
@@ -363,6 +413,10 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
def change_read_at(self, event=None):
self.main_window.settings['consider_read_at'] = self.readAtPercent.value()
def change_increment(self, event=None):
self.main_window.settings['small_increment'] = self.smallIncrementBox.value()
self.main_window.settings['large_increment'] = self.largeIncrementBox.value()
def manage_checkboxes(self, event=None):
sender = self.sender().objectName()
@@ -374,7 +428,9 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
'performCulling': 'perform_culling',
'cachingEnabled': 'caching_enabled',
'hideScrollBars': 'hide_scrollbars',
'attenuateTitles': 'attenuate_titles'}
'attenuateTitles': 'attenuate_titles',
'navBarVisible': 'nav_bar',
'autoCover': 'auto_cover'}
self.main_window.settings[
sender_dict[sender]] = not self.main_window.settings[sender_dict[sender]]
@@ -478,3 +534,30 @@ class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
annotations_out.append(annotation_data)
self.main_window.settings['annotations'] = annotations_out
def delete_database(self):
def ifcontinue(box_button):
if box_button.text() != '&Yes':
return
database_filename = os.path.join(
self.main_window.database_path, 'Lector.db')
os.remove(database_filename)
QtWidgets.qApp.exit()
# Generate a message box to confirm deletion
confirm_deletion = QtWidgets.QMessageBox()
deletion_prompt = self._translate(
'SettingsUI', f'Delete database and exit?')
confirm_deletion.setText(deletion_prompt)
confirm_deletion.setIcon(QtWidgets.QMessageBox.Critical)
confirm_deletion.setWindowTitle(self._translate('SettingsUI', 'Confirm'))
confirm_deletion.setStandardButtons(
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
confirm_deletion.buttonClicked.connect(ifcontinue)
confirm_deletion.show()
confirm_deletion.exec_()
def clear_log(self):
self.logBox.clear()
open(logger_filename, 'w').close()

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,63 +15,109 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# INSTRUCTIONS
# Every parser is supposed to have the following methods. None returns are not allowed.
# read_book()
# get_title()
# get_author()
# get_year()
# get_cover_image()
# get_isbn()
# get_tags()
# get_contents() - Should return a tuple with 0: TOC 1: special_settings (dict)
# Parsers for files containing only images need to return only images_only = True
# TODO
# Maybe shift to insert or replace instead of hash checking
# See if you want to include a hash of the book's name and author
# Change thread niceness
# Every parser is supposed to have the following methods.
# Exceptions will be caught - but that's just bad practice
# read_book() - Initialize book
# generate_metadata() - For addition
# generate_content() - For reading
import io
import os
import sys
import json
import time
import pickle
import logging
import hashlib
import threading
import importlib
import urllib.request
# The multiprocessing module does not work correctly on Windows
if sys.platform.startswith('win'):
from multiprocessing.dummy import Pool, Manager
thread_count = 4 # This is all on one CPU thread anyway
else:
from multiprocessing import Pool, Manager
from multiprocessing import Pool, Manager, cpu_count
thread_count = cpu_count()
from PyQt5 import QtCore, QtGui
from lector import database
from lector.parsers.epub import ParseEPUB
from lector.parsers.mobi import ParseMOBI
from lector.parsers.comicbooks import ParseCOMIC
logger = logging.getLogger(__name__)
sorter = {
'epub': ParseEPUB,
'mobi': ParseMOBI,
'azw': ParseMOBI,
'azw3': ParseMOBI,
'azw4': ParseMOBI,
'prc': ParseMOBI,
'cbz': ParseCOMIC,
'cbr': ParseCOMIC}
# The following imports are for optional dependencies
try:
# Check what dependencies are installed
# pymupdf - Optional
mupdf_check = importlib.util.find_spec('fitz')
if mupdf_check:
from lector.parsers.pdf import ParsePDF
sorter['pdf'] = ParsePDF
except ImportError:
print('python-poppler-qt5 is not installed. Pdf files will not work.')
else:
error_string = 'pymupdf is not installed. Will be unable to load PDF files.'
print(error_string)
logger.error(error_string)
# djvu - Optional
djvu_check = importlib.util.find_spec('djvu')
if djvu_check:
from lector.parsers.djvu import ParseDJVU
sorter['djvu'] = ParseDJVU
else:
error_string = 'djvulibre is not installed. Will be unable to load Djvu files.'
print(error_string)
logger.error(error_string)
# markdown - Optional
markdown_check = importlib.util.find_spec('markdown')
if markdown_check:
from lector.parsers.markdown import ParseMD
sorter['md'] = ParseMD
else:
error_string = 'markdown is not installed. Will be unable to load Markdown files.'
print(error_string)
logger.error(error_string)
# python-lxml - Required for everything except comics
lxml_check = importlib.util.find_spec('lxml')
xmltodict_check = importlib.util.find_spec('xmltodict')
if lxml_check and xmltodict_check:
from lector.parsers.epub import ParseEPUB
from lector.parsers.mobi import ParseMOBI
from lector.parsers.fb2 import ParseFB2
lxml_dependent = {
'epub': ParseEPUB,
'mobi': ParseMOBI,
'azw': ParseMOBI,
'azw3': ParseMOBI,
'azw4': ParseMOBI,
'prc': ParseMOBI,
'fb2': ParseFB2,
'fb2.zip': ParseFB2}
sorter.update(lxml_dependent)
else:
critical_sting = 'lxml / xmltodict is not installed. Only comics will load.'
print(critical_sting)
logger.critical(critical_sting)
# txt - Optional
textile_check = importlib.util.find_spec('textile')
if textile_check:
from lector.parsers.txt import ParseTXT
sorter['txt'] = ParseTXT
else:
error_string = 'textile is not installed. Will be unable to load TXT files.'
print(error_string)
logger.error(error_string)
available_parsers = [i for i in sorter]
progressbar = None # This is populated by __main__
progress_emitter = None # This is to be made into a global variable
_progress_emitter = None # This is to be made into a global variable
class UpdateProgress(QtCore.QObject):
@@ -86,7 +132,7 @@ class UpdateProgress(QtCore.QObject):
class BookSorter:
def __init__(self, file_list, mode, database_path, auto_tags=True, temp_dir=None):
def __init__(self, file_list, mode, database_path, settings, temp_dir=None):
# Have the GUI pass a list of files straight to here
# Then, on the basis of what is needed, pass the
# filenames to the requisite functions
@@ -99,13 +145,15 @@ class BookSorter:
self.work_mode = mode[0]
self.addition_mode = mode[1]
self.database_path = database_path
self.auto_tags = auto_tags
self.auto_tags = settings['auto_tags']
self.auto_cover = settings['auto_cover']
self.temp_dir = temp_dir
if database_path:
self.database_hashes()
self.threading_completed = []
self.queue = Manager().Queue()
self.errors = Manager().list()
self.processed_books = []
if self.work_mode == 'addition':
@@ -120,7 +168,6 @@ class BookSorter:
'LIKE')
if all_hashes_and_paths:
# self.hashes = [i[0] for i in all_hashes]
self.hashes_and_paths = {
i[0]: i[1] for i in all_hashes_and_paths}
@@ -147,7 +194,7 @@ class BookSorter:
return book_data
def read_book(self, filename):
# filename is expected as a string containg the
# filename is expected as a string containing the
# full path of the ebook file
with open(filename, 'rb') as current_book:
@@ -160,7 +207,7 @@ class BookSorter:
self.queue.put(filename)
# This should not get triggered in reading mode
# IF the file is NOT being loaded into the reader,
# IF the file is NOT being loaded into the reader
# Do not allow addition in case the file
# is already in the database and it remains at its original path
@@ -169,88 +216,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:
this_error = 'Unsupported extension: ' + filename
self.errors.append(this_error)
logger.error(this_error)
return
# Everything following this is standard
# None values are accounted for here
book_ref.read_book()
if book_ref.book:
book_ref = sorter[file_extension](filename, self.temp_dir, file_md5)
this_book = {}
this_book[file_md5] = {
'hash': file_md5,
'path': filename}
# None of the following have an exception type specified
# This will keep everything from crashing, but will make
# troubleshooting difficult
# Different modes require different values
if self.work_mode == 'addition':
# Reduce the size of the incoming image
# if one is found
title = book_ref.get_title()
author = book_ref.get_author()
year = book_ref.get_year()
isbn = book_ref.get_isbn()
try:
book_ref.read_book()
except Exception as e:
this_error = f'Error initializing: {filename}'
self.errors.append(this_error)
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
return
tags = None
if self.auto_tags:
tags = book_ref.get_tags()
this_book = {}
this_book[file_md5] = {
'hash': file_md5,
'path': filename}
cover_image_raw = book_ref.get_cover_image()
if cover_image_raw:
cover_image = resize_image(cover_image_raw)
else:
cover_image = None
# Different modes require different values
if self.work_mode == 'addition':
try:
metadata = book_ref.generate_metadata()
except Exception as e:
this_error = f'Metadata generation error: {filename}'
self.errors.append(this_error)
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
return
this_book[file_md5]['cover_image'] = cover_image
this_book[file_md5]['addition_mode'] = self.addition_mode
title = metadata.title
author = metadata.author
year = metadata.year
isbn = metadata.isbn
if self.work_mode == 'reading':
all_content = book_ref.get_contents()
tags = None
if self.auto_tags:
tags = metadata.tags
# get_contents() returns a tuple. Index 1 is a collection of
# special settings that depend on the kind of data being parsed.
# Currently, this includes:
# Only images included images_only BOOL Book contains only images
cover_image_raw = metadata.cover
if cover_image_raw:
cover_image = resize_image(cover_image_raw)
else:
cover_image = None
if self.auto_cover:
cover_image = fetch_cover(title, author)
content = all_content[0]
images_only = all_content[1]['images_only']
this_book[file_md5]['cover_image'] = cover_image
this_book[file_md5]['addition_mode'] = self.addition_mode
if not content:
content = [('Invalid', 'Something went horribly wrong')]
if self.work_mode == 'reading':
try:
book_breakdown = book_ref.generate_content()
except Exception as e:
this_error = f'Content generation error: {filename}'
self.errors.append(this_error)
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
return
toc = book_breakdown[0]
content = book_breakdown[1]
images_only = book_breakdown[2]
try:
book_data = self.database_entry_for_book(file_md5)
title = book_data[0]
author = book_data[1]
year = book_data[2]
isbn = book_data[3]
tags = book_data[4]
position = book_data[5]
bookmarks = book_data[6]
cover = book_data[7]
annotations = book_data[8]
except TypeError:
logger.error(
f'Database error: {filename}. Re-add book to program')
return
this_book[file_md5]['position'] = position
this_book[file_md5]['bookmarks'] = bookmarks
this_book[file_md5]['content'] = content
this_book[file_md5]['images_only'] = images_only
this_book[file_md5]['cover'] = cover
this_book[file_md5]['annotations'] = annotations
title = book_data[0].replace('&', '&&')
author = book_data[1]
year = book_data[2]
isbn = book_data[3]
tags = book_data[4]
position = book_data[5]
bookmarks = book_data[6]
cover = book_data[7]
annotations = book_data[8]
this_book[file_md5]['title'] = title
this_book[file_md5]['author'] = author
this_book[file_md5]['year'] = year
this_book[file_md5]['isbn'] = isbn
this_book[file_md5]['tags'] = tags
this_book[file_md5]['position'] = position
this_book[file_md5]['bookmarks'] = bookmarks
this_book[file_md5]['toc'] = toc
this_book[file_md5]['content'] = content
this_book[file_md5]['images_only'] = images_only
this_book[file_md5]['cover'] = cover
this_book[file_md5]['annotations'] = annotations
return this_book
this_book[file_md5]['title'] = title
this_book[file_md5]['author'] = author
this_book[file_md5]['year'] = year
this_book[file_md5]['isbn'] = isbn
this_book[file_md5]['tags'] = tags
return this_book
def read_progress(self):
while True:
@@ -260,8 +339,9 @@ class BookSorter:
total_number = len(self.file_list)
completed_number = len(self.threading_completed)
if progress_emitter: # Skip update in reading mode
progress_emitter.update_progress(
# Just for the record, this slows down book searching by about 20%
if _progress_emitter: # Skip update in reading mode
_progress_emitter.update_progress(
completed_number * 100 // total_number)
if total_number == completed_number:
@@ -272,7 +352,7 @@ class BookSorter:
return None
def pool_creator():
_pool = Pool(5)
_pool = Pool(thread_count)
self.processed_books = _pool.map(
self.read_book, self.file_list)
@@ -297,21 +377,29 @@ class BookSorter:
return_books[j] = i[j]
del self.processed_books
print('Finished processing in', time.time() - start_time)
return return_books
processing_time = str(time.time() - start_time)
logger.info('Finished processing in ' + processing_time)
return return_books, self.errors
def progress_object_generator():
# This has to be kept separate from the BookSorter class because
# the QtObject inheritance disallows pickling
global progress_emitter
progress_emitter = UpdateProgress()
progress_emitter.connect_to_progressbar()
global _progress_emitter
_progress_emitter = UpdateProgress()
_progress_emitter.connect_to_progressbar()
def resize_image(cover_image_raw):
cover_image = QtGui.QImage()
cover_image.loadFromData(cover_image_raw)
if isinstance(cover_image_raw, QtGui.QImage):
cover_image = cover_image_raw
else:
cover_image = QtGui.QImage()
cover_image.loadFromData(cover_image_raw)
# Resize image to what literally everyone
# agrees is an acceptable cover size
cover_image = cover_image.scaled(
420, 600, QtCore.Qt.IgnoreAspectRatio)
@@ -323,3 +411,46 @@ def resize_image(cover_image_raw):
cover_image_final = io.BytesIO(byte_array)
cover_image_final.seek(0)
return cover_image_final.getvalue()
def fetch_cover(title, author):
# TODO
# Start using the author parameter
# Generate a cover image in case the Google API finds nothing
# Why is that stupid UnicodeEncodeError happening?
api_url = 'https://www.googleapis.com/books/v1/volumes?q='
key = '&key=' + 'AIzaSyDOferpeSS424Dshs4YWY1s-nIBA9884hE'
title = title.replace(' ', '+')
req = api_url + title + key
try:
response = urllib.request.urlopen(req)
if response.getcode() == 200:
response_text = response.read().decode('utf-8')
response_json = json.loads(response_text)
else:
return None
except (urllib.error.HTTPError, urllib.error.URLError):
return None
except UnicodeEncodeError:
logger.error('UnicodeEncodeError fetching cover for ' + title)
return None
try:
# Get cover link from json
cover_link = response_json['items'][0]['volumeInfo']['imageLinks']['thumbnail']
# Get a slightly larger version
cover_link = cover_link.replace('zoom=1', 'zoom=2')
cover_request = urllib.request.urlopen(cover_link)
response = cover_request.read() # Bytes object
cover_image = resize_image(response)
logger.info('Cover found for ' + title)
return cover_image
except:
logger.error(f'Couldn\'t find cover for ' + title)
return None

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,14 +15,29 @@
# 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
# The following have to be separate
try:
from lector.parsers.pdf import render_pdf_page
except ImportError:
pass
try:
from lector.parsers.djvu import render_djvu_page
except ImportError:
pass
logger = logging.getLogger(__name__)
class BackGroundTabUpdate(QtCore.QThread):
def __init__(self, database_path, all_metadata, parent=None):
@@ -50,6 +65,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 +76,10 @@ class BackGroundBookAddition(QtCore.QThread):
self.file_list,
('addition', self.addition_mode),
self.database_path,
self.main_window.settings['auto_tags'],
self.main_window.settings,
self.main_window.temp_dir.path())
parsed_books = books.initiate_threads()
parsed_books, self.errors = books.initiate_threads()
self.main_window.lib_ref.generate_model('addition', parsed_books, False)
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
@@ -120,9 +136,13 @@ class BackGroundBookSearch(QtCore.QThread):
if self.valid_directories:
initiate_threads()
print(len(self.valid_files), 'books found')
if self.valid_files:
info_string = str(len(self.valid_files)) + ' books found'
logger.info(info_string)
else:
logger.error('No books found on scan')
else:
print('No valid directories')
logger.error('No valid directories')
class BackGroundCacheRefill(QtCore.QThread):
@@ -141,16 +161,21 @@ 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)
elif self.filetype == 'djvu':
page_data = self.book.pages[current_page]
pixmap = render_djvu_page(page_data)
return pixmap
remove_index = self.image_cache.index(self.remove_value)
@@ -171,3 +196,86 @@ class BackGroundCacheRefill(QtCore.QThread):
self.image_cache.append((next_page, refill_pixmap))
except (IndexError, TypeError):
self.image_cache.append(None)
class BackGroundTextSearch(QtCore.QThread):
def __init__(self):
super(BackGroundTextSearch, self).__init__(None)
self.search_content = None
self.search_text = None
self.case_sensitive = False
self.match_words = False
self.search_results = []
def set_search_options(
self, search_content, search_text,
case_sensitive, match_words):
self.search_content = search_content
self.search_text = search_text
self.case_sensitive = case_sensitive
self.match_words = match_words
def run(self):
if not self.search_text or len(self.search_text) < 3:
return
def get_surrounding_text(textCursor, words_before):
textCursor.movePosition(
QtGui.QTextCursor.WordLeft,
QtGui.QTextCursor.MoveAnchor,
words_before)
textCursor.movePosition(
QtGui.QTextCursor.NextWord,
QtGui.QTextCursor.KeepAnchor,
words_before * 2)
cursor_selection = textCursor.selection().toPlainText()
return cursor_selection.replace('\n', '')
self.search_results = {}
# Create a new QTextDocument of each chapter and iterate
# through it looking for hits
for i in self.search_content:
chapter_title = i[0]
chapterDocument = QtGui.QTextDocument()
chapterDocument.setHtml(i[1])
chapter_number = i[2]
findFlags = QtGui.QTextDocument.FindFlags(0)
if self.match_words:
findFlags = findFlags | QtGui.QTextDocument.FindWholeWords
if self.case_sensitive:
findFlags = findFlags | QtGui.QTextDocument.FindCaseSensitively
findResultCursor = chapterDocument.find(self.search_text, 0, findFlags)
while not findResultCursor.isNull():
result_position = findResultCursor.position()
words_before = 3
while True:
surroundingTextCursor = QtGui.QTextCursor(chapterDocument)
surroundingTextCursor.setPosition(
result_position, QtGui.QTextCursor.MoveAnchor)
surrounding_text = get_surrounding_text(
surroundingTextCursor, words_before)
words_before += 1
if surrounding_text[:2] not in ('. ', ', '):
break
# Case insensitive replace for find results
replace_pattern = re.compile(re.escape(self.search_text), re.IGNORECASE)
surrounding_text = replace_pattern.sub(
f'<b>{self.search_text}</b>', surrounding_text)
result_tuple = (
result_position, surrounding_text, self.search_text, chapter_number)
try:
self.search_results[chapter_title].append(result_tuple)
except KeyError:
self.search_results[chapter_title] = [result_tuple]
new_position = result_position + len(self.search_text)
findResultCursor = chapterDocument.find(
self.search_text, new_position, findFlags)

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,8 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore
logger = logging.getLogger(__name__)
class BookToolBar(QtWidgets.QToolBar):
def __init__(self, parent=None):
@@ -27,9 +31,6 @@ class BookToolBar(QtWidgets.QToolBar):
spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
self.setMovable(False)
self.setIconSize(QtCore.QSize(22, 22))
self.setFloatable(False)
@@ -45,7 +46,7 @@ class BookToolBar(QtWidgets.QToolBar):
self)
self.annotationButton = QtWidgets.QAction(
image_factory.get_image('annotate'),
self._translate('BookToolBar', 'Annotations'),
self._translate('BookToolBar', 'Annotations (Ctrl + N)'),
self)
self.addBookmarkButton = QtWidgets.QAction(
image_factory.get_image('bookmark-new'),
@@ -55,13 +56,17 @@ class BookToolBar(QtWidgets.QToolBar):
image_factory.get_image('bookmarks'),
self._translate('BookToolBar', 'Bookmarks (Ctrl + B)'),
self)
self.searchButton = QtWidgets.QAction(
image_factory.get_image('search'),
self._translate('BookToolBar', 'Search (Ctrl + F)'),
self)
self.distractionFreeButton = QtWidgets.QAction(
image_factory.get_image('visibility'),
self._translate('Main_BookToolBarUI', 'Toggle distraction free mode (Ctrl + D)'),
self)
self.fullscreenButton = QtWidgets.QAction(
image_factory.get_image('view-fullscreen'),
self._translate('BookToolBar', 'Fullscreen (F11)'),
self._translate('BookToolBar', 'Fullscreen (F)'),
self)
self.resetProfile = QtWidgets.QAction(
image_factory.get_image('reload'),
@@ -72,14 +77,14 @@ class BookToolBar(QtWidgets.QToolBar):
self.addAction(self.fontButton)
self.fontButton.setCheckable(True)
self.fontButton.triggered.connect(self.toggle_font_settings)
self.addSeparator()
self.addAction(self.annotationButton)
self.annotationButton.setCheckable(True)
self.addSeparator()
self.bookSeparator1 = self.addSeparator()
self.addAction(self.addBookmarkButton)
self.addAction(self.bookmarkButton)
self.bookmarkButton.setCheckable(True)
self.addSeparator()
self.bookSeparator2 = self.addSeparator()
self.addAction(self.annotationButton)
self.bookSeparator3 = self.addSeparator()
self.addAction(self.searchButton)
self.bookSeparator4 = self.addSeparator()
self.addAction(self.distractionFreeButton)
self.addAction(self.fullscreenButton)
@@ -177,7 +182,7 @@ class BookToolBar(QtWidgets.QToolBar):
self.fontSeparator4 = self.addSeparator()
self.addAction(self.paddingUp)
self.addAction(self.paddingDown)
self.fontSeparator4 = self.addSeparator()
self.fontSeparator5 = self.addSeparator()
self.addAction(self.alignLeft)
self.addAction(self.alignRight)
self.addAction(self.alignCenter)
@@ -201,38 +206,69 @@ 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.rotateRightButton = QtWidgets.QAction(
image_factory.get_image('rotate-right'),
self._translate('BookToolBar', 'Rotate image clockwise'),
self)
self.rotateLeftButton = QtWidgets.QAction(
image_factory.get_image('rotate-left'),
self._translate('BookToolBar', 'Rotate image anti-clockwise'),
self)
self.zoomIn = QtWidgets.QAction(
image_factory.get_image('zoom-in'),
self._translate('BookToolBar', 'Zoom in'),
self._translate('BookToolBar', 'Zoom in (+)'),
self)
self.zoomIn.setObjectName('zoomIn')
self.zoomOut = QtWidgets.QAction(
image_factory.get_image('zoom-out'),
self._translate('BookToolBar', 'Zoom Out'),
self._translate('BookToolBar', 'Zoom Out (-)'),
self)
self.zoomOut.setObjectName('zoomOut')
self.fitWidth = QtWidgets.QAction(
image_factory.get_image('zoom-fit-width'),
self._translate('BookToolBar', 'Fit Width'),
self._translate('BookToolBar', 'Fit Width (W)'),
self)
self.fitWidth.setObjectName('fitWidth')
self.fitWidth.setCheckable(True)
self.bestFit = QtWidgets.QAction(
image_factory.get_image('zoom-fit-best'),
self._translate('BookToolBar', 'Best Fit'),
self._translate('BookToolBar', 'Best Fit (B)'),
self)
self.bestFit.setObjectName('bestFit')
self.bestFit.setCheckable(True)
self.originalSize = QtWidgets.QAction(
image_factory.get_image('zoom-original'),
self._translate('BookToolBar', 'Original size'),
self._translate('BookToolBar', 'Original size (O)'),
self)
self.originalSize.setObjectName('originalSize')
self.originalSize.setCheckable(True)
@@ -242,57 +278,69 @@ 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.rotateRightButton)
self.addAction(self.rotateLeftButton)
self.comicSeparator3 = 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.comicSeparator4 = self.addSeparator()
self.comicBGColorAction = self.addWidget(self.comicBGColor)
self.comicActions = [
self.doublePageButton,
self.mangaModeButton,
self.invertButton,
self.comicBGColorAction,
self.rotateLeftButton,
self.rotateRightButton,
self.zoomIn,
self.zoomOut,
self.fitWidth,
self.bestFit,
self.originalSize,
self.comicSeparator1,
self.comicSeparator2]
self.comicSeparator2,
self.comicSeparator3,
self.comicSeparator4]
for i in self.comicActions:
i.setVisible(False)
# Other booktoolbar widgets
self.searchBar = FixedLineEdit(self)
self.searchBar.setPlaceholderText(
self._translate('BookToolBar', 'Search...'))
self.searchBar.setSizePolicy(sizePolicy)
self.searchBar.setContentsMargins(10, 0, 0, 0)
self.searchBar.setObjectName('searchBar')
# Sorter
# Table of contents Combo Box
# Has to have a QTreeview associated with it
self.tocBox = FixedComboBox(self)
self.tocBox.setObjectName('sortingBox')
self.tocBox.setToolTip(
self._translate('BookToolBar', 'Table of Contents'))
self.tocTreeView = QtWidgets.QTreeView(self.tocBox)
self.tocBox.setView(self.tocTreeView)
self.tocTreeView.setItemsExpandable(False)
self.tocTreeView.setRootIsDecorated(False)
# All of these will be put after the spacer
# This means that the buttons in the left side of
# the toolbar have to split up and added here
self.boxSpacer = self.addWidget(spacer)
self.addWidget(spacer)
self.tocBoxAction = self.addWidget(self.tocBox)
self.searchBarAction = self.addWidget(self.searchBar)
self.bookActions = [
self.annotationButton,
self.addBookmarkButton,
self.bookmarkButton,
self.searchButton,
self.distractionFreeButton,
self.fullscreenButton,
self.tocBoxAction,
self.searchBarAction]
self.bookSeparator1,
self.bookSeparator2,
self.bookSeparator3,
self.bookSeparator4]
for i in self.bookActions:
i.setVisible(True)
@@ -306,17 +354,16 @@ class BookToolBar(QtWidgets.QToolBar):
self.customize_view_off()
def customize_view_on(self):
if self.parent().tabWidget.widget(
self.parent().tabWidget.currentIndex()).metadata['images_only']:
# The following might seem redundant,
# but it's necessary for tab switching
images_only = self.parent().tabWidget.currentWidget().are_we_doing_images_only
# The following might seem redundant,
# but it's necessary for tab switching
if images_only:
for i in self.comicActions:
i.setVisible(True)
for i in self.fontActions:
i.setVisible(False)
else:
for i in self.fontActions:
i.setVisible(True)
@@ -344,15 +391,10 @@ class LibraryToolBar(QtWidgets.QToolBar):
super(LibraryToolBar, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
spacer = QtWidgets.QWidget()
spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.setMovable(False)
self.setIconSize(QtCore.QSize(22, 22))
self.setFloatable(False)
self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
self.setObjectName("LibraryToolBar")
image_factory = self.window().QImageFactory
@@ -365,16 +407,6 @@ class LibraryToolBar(QtWidgets.QToolBar):
image_factory.get_image('remove'),
self._translate('LibraryToolBar', 'Delete book'),
self)
self.colorButton = QtWidgets.QAction(
image_factory.get_image('color-picker'),
self._translate('LibraryToolBar', 'Library background color'),
self)
self.colorButton.setObjectName('libraryBackground')
self.settingsButton = QtWidgets.QAction(
image_factory.get_image('settings'),
self._translate('LibraryToolBar', 'Settings'),
self)
self.settingsButton.setCheckable(True)
self.coverViewButton = QtWidgets.QAction(
image_factory.get_image('view-grid'),
@@ -391,14 +423,29 @@ class LibraryToolBar(QtWidgets.QToolBar):
image_factory.get_image('reload'),
self._translate('LibraryToolBar', 'Scan Library'),
self)
self.reloadLibraryButton.setObjectName('reloadLibrary')
self.libraryFilterButton = QtWidgets.QToolButton(self)
self.libraryFilterButton.setIcon(image_factory.get_image('view-readermode'))
self.libraryFilterButton.setText(
self._translate('LibraryToolBar', 'Filter library'))
self.libraryFilterButton.setToolTip(
self._translate('LibraryToolBar', 'Filter library'))
self.colorButton = QtWidgets.QAction(
image_factory.get_image('color-picker'),
self._translate('LibraryToolBar', 'Library background color'),
self)
self.colorButton.setObjectName('libraryBackground')
self.settingsButton = QtWidgets.QAction(
image_factory.get_image('settings'),
self._translate('LibraryToolBar', 'Settings'),
self)
self.settingsButton.setCheckable(True)
self.aboutButton = QtWidgets.QAction(
image_factory.get_image('about'),
self._translate('LibraryToolBar', 'About'),
self)
# Auto unchecks the other QToolButton in case of clicking
self.viewButtons = QtWidgets.QActionGroup(self)
self.viewButtons.setExclusive(True)
@@ -417,6 +464,7 @@ class LibraryToolBar(QtWidgets.QToolBar):
self.addSeparator()
self.addAction(self.colorButton)
self.addAction(self.settingsButton)
self.addAction(self.aboutButton)
# Filter
sizePolicy = QtWidgets.QSizePolicy(
@@ -427,8 +475,7 @@ class LibraryToolBar(QtWidgets.QToolBar):
self.searchBar.setPlaceholderText(
self._translate('LibraryToolBar', 'Search for Title, Author, Tags...'))
self.searchBar.setSizePolicy(sizePolicy)
self.searchBar.setContentsMargins(10, 0, 0, 0)
self.searchBar.setObjectName('searchBar')
self.searchBar.setContentsMargins(0, 0, 10, 0)
# Sorter
title_string = self._translate('LibraryToolBar', 'Title')
@@ -443,15 +490,18 @@ class LibraryToolBar(QtWidgets.QToolBar):
self.sortingBox = FixedComboBox(self)
self.sortingBox.addItems(sorting_choices)
self.sortingBox.setObjectName('sortingBox')
self.sortingBox.setSizePolicy(sizePolicy)
self.sortingBox.setMinimumContentsLength(10)
self.sortingBox.setToolTip(self._translate('LibraryToolBar', 'Sort by'))
# Spacer
spacer = QtWidgets.QWidget()
spacer.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# Add widgets
self.addWidget(spacer)
self.sortingBoxAction = self.addWidget(self.sortingBox)
self.addWidget(self.searchBar)
self.sortingBoxAction = self.addWidget(self.sortingBox)
# Sublassing these widgets out prevents them from resizing
@@ -459,21 +509,30 @@ 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)
return self.minimumSizeHint()
def minimumSizeHint(self):
return QtCore.QSize(self.adjusted_size, 32)
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)
return self.minimumSizeHint()
def minimumSizeHint(self):
return QtCore.QSize(self.adjusted_size, 32)
def keyReleaseEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:

View File

@@ -1,5 +1,5 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2018 BasioMeusPuga
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,18 +17,18 @@
# TODO
# Reading modes
# Double page, Continuous etc
# Especially for comics
import os
import uuid
import logging
from PyQt5 import QtWidgets, QtGui, QtCore
from lector.models import BookmarkProxyModel
from lector.delegates import BookmarkDelegate
from lector.sorter import resize_image
from lector.dockwidgets import PliantDockWidget, PliantNavBarWidget
from lector.contentwidgets import PliantQGraphicsView, PliantQTextBrowser
logger = logging.getLogger(__name__)
class Tab(QtWidgets.QWidget):
def __init__(self, metadata, main_window, parent=None):
@@ -37,18 +37,31 @@ class Tab(QtWidgets.QWidget):
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.first_run = True
self.main_window = main_window
self.metadata = metadata # Save progress data into this dictionary
self.are_we_doing_images_only = self.metadata['images_only']
self.is_fullscreen = False
self.is_library = False
self.masterLayout = QtWidgets.QHBoxLayout(self)
self.masterLayout.setContentsMargins(0, 0, 0, 0)
self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime()
# Create relevant containers
if not self.metadata['annotations']:
self.metadata['annotations'] = {}
if not self.metadata['bookmarks']:
self.metadata['bookmarks'] = {}
# Generate toc Model
self.tocModel = QtGui.QStandardItemModel()
self.tocModel.setHorizontalHeaderLabels(('Table of Contents',))
self.generate_toc_model()
# Get the current position of the book
if self.metadata['position']:
# A book might have been marked read without being opened
if self.metadata['position']['is_read']:
self.generate_position(True)
current_chapter = self.metadata['position']['current_chapter']
@@ -56,53 +69,50 @@ class Tab(QtWidgets.QWidget):
self.generate_position()
current_chapter = 1
chapter_content = self.metadata['content'][current_chapter - 1][1]
# Create relevant containers
if not self.metadata['annotations']:
self.metadata['annotations'] = {}
# See bookmark availability
if not self.metadata['bookmarks']:
self.metadata['bookmarks'] = {}
# The content display widget is, by default a QTextBrowser.
# In case the incoming data is only images
# such as in the case of comic book files,
# we want a QGraphicsView widget doing all the heavy lifting
# instead of a QTextBrowser
if self.are_we_doing_images_only: # Boolean
if self.are_we_doing_images_only:
self.contentView = PliantQGraphicsView(
self.metadata['path'], self.main_window, self)
self.contentView.loadImage(chapter_content)
else:
self.contentView = PliantQTextBrowser(self.main_window, self)
self.image_rotation = 0
else:
self.contentView = PliantQTextBrowser(
self.main_window, self)
self.contentView.setReadOnly(True)
# TODO
# Change this when HTML navigation works
self.contentView.setOpenLinks(False)
# TODO
# Rename the .css files to something else here and keep
# a record of them .Currently, I'm just removing them
# for the sake of simplicity
relative_path_root = os.path.join(
self.main_window.temp_dir.path(), self.metadata['hash'])
relative_paths = []
for i in os.walk(relative_path_root):
# TODO
# Rename the .css files to something else here and keep
# a record of them
# Currently, I'm just removing them for the sake of simplicity
for j in i[2]:
file_extension = os.path.splitext(j)[1]
if file_extension == '.css':
file_path = os.path.join(i[0], j)
os.remove(file_path)
relative_paths.append(os.path.join(relative_path_root, i[0]))
self.contentView.setSearchPaths(relative_paths)
self.contentView.setOpenLinks(False) # TODO Change this when HTML navigation works
self.contentView.setHtml(chapter_content)
self.contentView.setReadOnly(True)
self.hiddenButton = QtWidgets.QToolButton(self)
self.hiddenButton.setVisible(False)
self.hiddenButton.clicked.connect(self.set_cursor_position)
# All content must be set through this function
self.set_content(current_chapter, True, False)
if not self.are_we_doing_images_only:
# Setting this later breaks cursor positioning for search results
self.hiddenButton.animateClick(50)
# Load annotations for current content
@@ -116,76 +126,60 @@ class Tab(QtWidgets.QWidget):
self.main_window.settings['scroll_speed'])
if self.main_window.settings['hide_scrollbars']:
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.contentView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.contentView.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOff)
self.contentView.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOff)
else:
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.contentView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.contentView.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAsNeeded)
self.contentView.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarAsNeeded)
# Create the annotations dock
self.annotationDock = PliantDockWidget(self.main_window, 'annotations', self.contentView)
self.annotationDock.setWindowTitle(self._translate('Tab', 'Annotations'))
self.annotationDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
self.annotationDock.hide()
# Create a NavBar widget
if self.main_window.settings['nav_bar']:
self.navBar = PliantNavBarWidget(
self.main_window, self.contentView, self)
self.navBar.setFloating(True)
self.navBar.setTitleBarWidget(QtWidgets.QWidget(self))
self.navBar.hide()
else:
# This keeps from having to set visibility conditions
# everywhere
self.navBar = QtWidgets.QWidget()
self.navBar.setFixedSize(0, 0)
self.annotationListView = QtWidgets.QListView(self.annotationDock)
self.annotationListView.setResizeMode(QtWidgets.QListWidget.Adjust)
self.annotationListView.setMaximumWidth(350)
self.annotationListView.doubleClicked.connect(self.contentView.toggle_annotation_mode)
self.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
self.annotationDock.setWidget(self.annotationListView)
self.annotationModel = QtGui.QStandardItemModel(self)
self.generate_annotation_model()
# Create a common dock for bookmarks, annotations, and search
self.sideDock = PliantDockWidget(
self.main_window, False, self.contentView, self)
self.sideDock.populate()
# Create the annotation notes dock
self.annotationNoteDock = PliantDockWidget(self.main_window, 'notes', self.contentView)
self.annotationNoteDock = PliantDockWidget(
self.main_window, True, self.contentView, self)
self.annotationNoteDock.setWindowTitle(self._translate('Tab', 'Note'))
self.annotationNoteDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
self.annotationNoteDock.hide()
self.annotationNoteEdit = QtWidgets.QTextEdit(self.annotationDock)
self.annotationNoteEdit = QtWidgets.QTextEdit(self.annotationNoteDock)
self.annotationNoteEdit.setMaximumSize(QtCore.QSize(250, 250))
self.annotationNoteEdit.setFocusPolicy(QtCore.Qt.StrongFocus)
self.annotationNoteDock.setWidget(self.annotationNoteEdit)
# Create the dock widget for context specific display
self.bookmarkDock = PliantDockWidget(self.main_window, 'bookmarks', self.contentView)
self.bookmarkDock.setWindowTitle(self._translate('Tab', 'Bookmarks'))
self.bookmarkDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
self.bookmarkDock.hide()
self.bookmarkListView = QtWidgets.QListView(self.bookmarkDock)
self.bookmarkListView.setResizeMode(QtWidgets.QListWidget.Adjust)
self.bookmarkListView.setMaximumWidth(350)
self.bookmarkListView.setItemDelegate(
BookmarkDelegate(self.main_window, self.bookmarkListView))
self.bookmarkListView.setUniformItemSizes(True)
self.bookmarkListView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.bookmarkListView.customContextMenuRequested.connect(
self.generate_bookmark_context_menu)
self.bookmarkListView.clicked.connect(self.navigate_to_bookmark)
self.bookmarkDock.setWidget(self.bookmarkListView)
self.bookmarkModel = QtGui.QStandardItemModel(self)
self.bookmarkProxyModel = BookmarkProxyModel(self)
self.generate_bookmark_model()
self.generate_keyboard_shortcuts()
self.masterLayout.addWidget(self.contentView)
self.masterLayout.addWidget(self.annotationDock)
self.masterLayout.addWidget(self.sideDock)
self.masterLayout.addWidget(self.annotationNoteDock)
self.masterLayout.addWidget(self.bookmarkDock)
# The following has to be after the docks are added to the layout
self.annotationDock.setFloating(True)
self.annotationDock.setWindowOpacity(.95)
self.sideDock.setFloating(True)
self.sideDock.setWindowOpacity(.95)
self.annotationNoteDock.setFloating(True)
self.annotationNoteDock.setWindowOpacity(.95)
self.bookmarkDock.setFloating(True)
self.bookmarkDock.setWindowOpacity(.95)
self.sideDock.hide()
# Create tab in the central tab widget
title = self.metadata['title']
if self.main_window.settings['attenuate_titles'] and len(title) > 30:
title = title[:30] + '...'
@@ -198,9 +192,9 @@ class Tab(QtWidgets.QWidget):
this_tab_index, QtGui.QIcon(cover_icon))
# Hide mouse cursor timer
self.mouse_hide_timer = QtCore.QTimer()
self.mouse_hide_timer.setSingleShot(True)
self.mouse_hide_timer.timeout.connect(self.hide_mouse)
self.mouseHideTimer = QtCore.QTimer()
self.mouseHideTimer.setSingleShot(True)
self.mouseHideTimer.timeout.connect(self.hide_mouse)
# Hide the tab bar in case distraction free mode is active
if not self.main_window.settings['show_bars']:
@@ -208,6 +202,29 @@ class Tab(QtWidgets.QWidget):
self.contentView.setFocus()
def toggle_side_dock(self, tab_required, override_hide=False):
if (self.sideDock.isVisible()
and self.sideDock.sideDockTabWidget.currentIndex() == tab_required
and not override_hide):
self.sideDock.hide()
elif not self.sideDock.isVisible():
self.sideDock.show()
if tab_required == 2:
self.sideDock.activateWindow()
self.sideDock.search.searchLineEdit.setFocus()
self.sideDock.search.searchLineEdit.selectAll()
self.sideDock.sideDockTabWidget.setCurrentIndex(tab_required)
def generate_rotation(self, delta_angle):
self.image_rotation += delta_angle
# Set bounds for rotation angle
if self.image_rotation == 360:
self.image_rotation = 0
if self.image_rotation == -90:
self.image_rotation = 270
def update_last_accessed_time(self):
self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime()
@@ -220,15 +237,16 @@ class Tab(QtWidgets.QWidget):
try:
self.main_window.lib_ref.libraryModel.setData(
matching_item[0], self.metadata['last_accessed'], QtCore.Qt.UserRole + 12)
matching_item[0],
self.metadata['last_accessed'], QtCore.Qt.UserRole + 12)
except IndexError: # The file has been deleted
pass
def set_cursor_position(self, cursor_position=None):
def set_cursor_position(self, cursor_position=None, select_chars=0):
try:
required_position = self.metadata['position']['cursor_position']
except KeyError:
print(f'Database: Cursor position error. Recommend retry.')
logging.error('Database: Cursor position error. Recommend retry.')
return
if cursor_position:
@@ -242,10 +260,21 @@ class Tab(QtWidgets.QWidget):
# textCursor() RETURNS a copy of the textcursor
cursor = self.contentView.textCursor()
cursor.setPosition(
required_position, QtGui.QTextCursor.MoveAnchor)
required_position - select_chars,
QtGui.QTextCursor.MoveAnchor)
if select_chars > 0: # Select search results
cursor.movePosition(
QtGui.QTextCursor.NextCharacter,
QtGui.QTextCursor.KeepAnchor,
select_chars)
self.contentView.setTextCursor(cursor)
self.contentView.ensureCursorVisible()
# Finally, to make sure the cover image isn't
# scrolled halfway through on first open,
if self.metadata['cover'] and self.metadata['position']['current_chapter'] == 1:
self.contentView.verticalScrollBar().setValue(0)
def generate_position(self, is_read=False):
total_chapters = len(self.metadata['content'])
@@ -260,10 +289,8 @@ class Tab(QtWidgets.QWidget):
if not self.are_we_doing_images_only:
for i in self.metadata['content']:
chapter_html = i[1]
textDocument = QtGui.QTextDocument(None)
textDocument.setHtml(chapter_html)
textDocument.setHtml(i)
block_count = textDocument.blockCount()
blocks_per_chapter.append(block_count)
@@ -279,30 +306,88 @@ class Tab(QtWidgets.QWidget):
'cursor_position': 0}
def generate_keyboard_shortcuts(self):
self.ksNextChapter = QtWidgets.QShortcut(
ksNextChapter = QtWidgets.QShortcut(
QtGui.QKeySequence('Right'), self.contentView)
self.ksNextChapter.setObjectName('nextChapter')
self.ksNextChapter.activated.connect(self.sneaky_change)
ksNextChapter.setObjectName('nextChapter')
ksNextChapter.activated.connect(self.sneaky_change)
self.ksPrevChapter = QtWidgets.QShortcut(
ksPrevChapter = QtWidgets.QShortcut(
QtGui.QKeySequence('Left'), self.contentView)
self.ksPrevChapter.setObjectName('prevChapter')
self.ksPrevChapter.activated.connect(self.sneaky_change)
ksPrevChapter.setObjectName('prevChapter')
ksPrevChapter.activated.connect(self.sneaky_change)
self.ksGoFullscreen = QtWidgets.QShortcut(
QtGui.QKeySequence('F11'), self.contentView)
self.ksGoFullscreen.activated.connect(self.go_fullscreen)
ksGoFullscreen = QtWidgets.QShortcut(
QtGui.QKeySequence('F'), self.contentView)
ksGoFullscreen.activated.connect(self.go_fullscreen)
self.ksExitFullscreen = QtWidgets.QShortcut(
ksExitFullscreen = QtWidgets.QShortcut(
QtGui.QKeySequence('Escape'), self.contentView)
self.ksExitFullscreen.setContext(QtCore.Qt.ApplicationShortcut)
self.ksExitFullscreen.activated.connect(self.exit_fullscreen)
ksExitFullscreen.setContext(QtCore.Qt.ApplicationShortcut)
ksExitFullscreen.activated.connect(self.exit_fullscreen)
self.ksToggleBookMarks = QtWidgets.QShortcut(
ksToggleBookmarks = QtWidgets.QShortcut(
QtGui.QKeySequence('Ctrl+B'), self.contentView)
self.ksToggleBookMarks.activated.connect(self.toggle_bookmarks)
ksToggleBookmarks.activated.connect(
lambda: self.toggle_side_dock(0))
# Shortcuts not required for comic view functionality
if not self.are_we_doing_images_only:
ksToggleAnnotations = QtWidgets.QShortcut(
QtGui.QKeySequence('Ctrl+N'), self.contentView)
ksToggleAnnotations.activated.connect(
lambda: self.toggle_side_dock(1))
ksToggleSearch = QtWidgets.QShortcut(
QtGui.QKeySequence('Ctrl+F'), self.contentView)
ksToggleSearch.activated.connect(
lambda: self.toggle_side_dock(2))
def generate_toc_model(self):
# The toc list is:
# 0: Level
# 1: Title
# 2: Chapter content / page number
# pprint it out to get a better idea of structure
toc = self.metadata['toc']
parent_list = []
for i in toc:
item = QtGui.QStandardItem()
item.setText(i[1])
item.setData(i[2], QtCore.Qt.UserRole)
item.setData(i[1], QtCore.Qt.UserRole + 1)
current_level = i[0]
if current_level == 1:
self.tocModel.appendRow(item)
parent_list.clear()
parent_list.append(item)
else:
parent_list[current_level - 2].appendRow(item)
try:
next_level = toc[toc.index(i) + 1][0]
if next_level > current_level:
parent_list.append(item)
if next_level < current_level:
level_difference = current_level - next_level
parent_list = parent_list[:-level_difference]
except IndexError:
pass
# This is needed to be able to have the toc Combobox
# jump to the correct position in the book when it is
# first opened
self.main_window.bookToolBar.tocBox.setModel(self.tocModel)
self.main_window.bookToolBar.tocTreeView.expandAll()
def go_fullscreen(self):
# To allow toggles to function
# properly after the fullscreening
self.sideDock.hide()
self.annotationNoteDock.hide()
if self.contentView.windowState() == QtCore.Qt.WindowFullScreen:
self.exit_fullscreen()
return
@@ -316,16 +401,22 @@ class Tab(QtWidgets.QWidget):
self.main_window.hide()
if not self.are_we_doing_images_only:
self.hiddenButton.animateClick(100)
self.hiddenButton.animateClick(50)
self.mouseHideTimer.start(2000)
self.is_fullscreen = True
def exit_fullscreen(self):
for i in (self.bookmarkDock, self.annotationDock, self.annotationNoteDock):
# Intercept escape presses
for i in (self.annotationNoteDock, self.sideDock):
if i.isVisible():
i.setVisible(False)
return
# Prevents cursor position change on escape presses
if self.main_window.isVisible():
return
if not self.are_we_doing_images_only:
self.contentView.record_position()
@@ -345,11 +436,28 @@ class Tab(QtWidgets.QWidget):
if not self.main_window.settings['show_bars']:
self.main_window.toggle_distraction_free()
self.navBar.hide()
self.mouseHideTimer.start(2000)
self.contentView.setFocus()
def change_chapter_tocBox(self):
chapter_number = self.main_window.bookToolBar.tocBox.currentIndex()
required_content = self.metadata['content'][chapter_number][1]
def set_content(self, required_position, tocBox_readjust=False, record_position=False):
# All content changes must come through here
# This function will decide how to relate
# entries in the toc to the actual content
# Set the required page to the corresponding index
# For images this is simply a page number
# For text based books, this is the entire text of the chapter
try:
required_content = self.metadata['content'][required_position - 1]
except IndexError:
return # Do not allow cycling beyond last page
# Update the metadata dictionary to save position
self.metadata['position']['current_chapter'] = required_position
self.metadata['position']['is_read'] = False
if record_position:
self.contentView.record_position()
if self.are_we_doing_images_only:
self.contentView.loadImage(required_content)
@@ -357,11 +465,64 @@ class Tab(QtWidgets.QWidget):
self.contentView.clear()
self.contentView.setHtml(required_content)
self.contentView.common_functions.load_annotations(chapter_number + 1)
# Set the contentview to look the way God intended
self.main_window.profile_functions.format_contentView()
self.contentView.common_functions.load_annotations(required_position)
def format_view(self, font, font_size, foreground,
background, padding, line_spacing,
text_alignment):
# Change the index of the tocBox. This is manual and each function
# that calls set_position must specify if it needs this adjustment
if tocBox_readjust:
self.set_tocBox_index(required_position, None)
# The NavBar doesn't get declared until later
try:
self.set_tocBox_index(
required_position, self.navBar.tocComboBox)
except AttributeError:
pass
self.contentView.setFocus()
def set_tocBox_index(self, current_position=None, tocBox=None):
# Get current position from the metadata dictionary
# in case it isn't specified
if not current_position:
current_position = self.metadata['position']['current_chapter']
position_reference = 1
for i in reversed(self.metadata['toc']):
if i[2] <= current_position:
position_reference = i[2]
break
# Match the position reference to the corresponding
# index in the QTreeView / QCombobox
try:
matchingIndex = self.tocModel.match(
self.tocModel.index(0, 0),
QtCore.Qt.UserRole,
position_reference,
2, QtCore.Qt.MatchRecursive)[0]
except IndexError:
return
# A tocBox name is specified for the context menu
if not tocBox:
tocBox = self.main_window.bookToolBar.tocBox
# The following sets the QCombobox index according
# to the index found above.
tocBox.blockSignals(True)
currentRootModelIndex = tocBox.rootModelIndex()
tocBox.setRootModelIndex(matchingIndex.parent())
tocBox.setCurrentIndex(matchingIndex.row())
tocBox.setRootModelIndex(currentRootModelIndex)
tocBox.blockSignals(False)
def format_view(
self, font, font_size, foreground,
background, padding, line_spacing,
text_alignment):
if self.are_we_doing_images_only:
# Tab color does not need to be set separately in case
@@ -393,170 +554,47 @@ class Tab(QtWidgets.QWidget):
'center': QtCore.Qt.AlignCenter,
'justify': QtCore.Qt.AlignJustify}
current_index = self.main_window.bookToolBar.tocBox.currentIndex()
if current_index == 0:
block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
else:
block_format.setAlignment(alignment_dict[text_alignment])
# Also for padding
# Using setViewPortMargins for this disables scrolling in the margins
block_format.setLeftMargin(padding)
block_format.setRightMargin(padding)
this_cursor = self.contentView.textCursor()
this_cursor.movePosition(QtGui.QTextCursor.Start, 0, 1)
this_cursor.setPosition(QtGui.QTextCursor.Start)
# Iterate over the entire document block by block
# The document ends when the cursor position can no longer be incremented
while True:
# So this fixes the stupid repetitive iteration
# I was doing over the entire thing
# It also allows for all images to be center aligned.
# Magic. *jazz hands*
block_text = this_cursor.block().text().strip()
try:
# Object replacement char - Seems to work with images
if ord(block_text) == 65532:
block_format.setAlignment(
QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
else:
raise TypeError
except TypeError:
block_format.setAlignment(alignment_dict[text_alignment])
# Iterate over the entire document block by block
# The document ends when the cursor position can no longer be incremented
old_position = this_cursor.position()
this_cursor.mergeBlockFormat(block_format)
this_cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1)
this_cursor.movePosition(
QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.MoveAnchor)
new_position = this_cursor.position()
if old_position == new_position:
break
def toggle_annotations(self):
if self.annotationDock.isVisible():
self.annotationDock.hide()
else:
self.annotationDock.show()
def generate_annotation_model(self):
saved_annotations = self.main_window.settings['annotations']
if not saved_annotations:
return
def add_to_model(annotation):
item = QtGui.QStandardItem()
item.setText(annotation['name'])
item.setData(annotation, QtCore.Qt.UserRole)
self.annotationModel.appendRow(item)
# Prevent annotation mixup
for i in saved_annotations:
if self.are_we_doing_images_only and i['applicable_to'] == 'images':
add_to_model(i)
elif not self.are_we_doing_images_only and i['applicable_to'] == 'text':
add_to_model(i)
self.annotationListView.setModel(self.annotationModel)
def toggle_bookmarks(self):
if self.bookmarkDock.isVisible():
self.bookmarkDock.hide()
else:
self.bookmarkDock.show()
def add_bookmark(self):
# TODO
# Start dockListView.edit(index) when something new is added
identifier = uuid.uuid4().hex[:10]
description = self._translate('Tab', 'New bookmark')
if self.are_we_doing_images_only:
chapter = self.metadata['position']['current_chapter']
cursor_position = 0
else:
chapter, cursor_position = self.contentView.record_position(True)
self.metadata['bookmarks'][identifier] = {
'chapter': chapter,
'cursor_position': cursor_position,
'description': description}
self.add_bookmark_to_model(
description, chapter, cursor_position, identifier)
self.bookmarkDock.setVisible(True)
def add_bookmark_to_model(self, description, chapter, cursor_position, identifier):
bookmark = QtGui.QStandardItem()
bookmark.setData(description, QtCore.Qt.DisplayRole)
bookmark.setData(chapter, QtCore.Qt.UserRole)
bookmark.setData(cursor_position, QtCore.Qt.UserRole + 1)
bookmark.setData(identifier, QtCore.Qt.UserRole + 2)
self.bookmarkModel.appendRow(bookmark)
self.update_bookmark_proxy_model()
def navigate_to_bookmark(self, index):
if not index.isValid():
return
chapter = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole)
cursor_position = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole + 1)
self.main_window.bookToolBar.tocBox.setCurrentIndex(chapter - 1)
if not self.are_we_doing_images_only:
self.set_cursor_position(cursor_position)
def generate_bookmark_model(self):
# TODO
# Sorting is not working correctly
try:
for i in self.metadata['bookmarks'].items():
self.add_bookmark_to_model(
i[1]['description'],
i[1]['chapter'],
i[1]['cursor_position'],
i[0])
except KeyError:
title = self.metadata['title']
# TODO
# Delete the bookmarks entry for this file
print(f'Database: Bookmark error for {title}. Recommend delete entry.')
return
self.generate_bookmark_proxy_model()
def generate_bookmark_proxy_model(self):
self.bookmarkProxyModel.setSourceModel(self.bookmarkModel)
self.bookmarkProxyModel.setSortCaseSensitivity(False)
self.bookmarkProxyModel.setSortRole(QtCore.Qt.UserRole)
self.bookmarkListView.setModel(self.bookmarkProxyModel)
def update_bookmark_proxy_model(self):
self.bookmarkProxyModel.invalidateFilter()
self.bookmarkProxyModel.setFilterParams(
self.main_window.bookToolBar.searchBar.text())
self.bookmarkProxyModel.setFilterFixedString(
self.main_window.bookToolBar.searchBar.text())
def generate_bookmark_context_menu(self, position):
index = self.bookmarkListView.indexAt(position)
if not index.isValid():
return
bookmarkMenu = QtWidgets.QMenu()
editAction = bookmarkMenu.addAction(
self.main_window.QImageFactory.get_image('edit-rename'),
self._translate('Tab', 'Edit'))
deleteAction = bookmarkMenu.addAction(
self.main_window.QImageFactory.get_image('trash-empty'),
self._translate('Tab', 'Delete'))
action = bookmarkMenu.exec_(
self.bookmarkListView.mapToGlobal(position))
if action == editAction:
self.bookmarkListView.edit(index)
if action == deleteAction:
row = index.row()
delete_uuid = self.bookmarkModel.item(row).data(QtCore.Qt.UserRole + 2)
self.metadata['bookmarks'].pop(delete_uuid)
self.bookmarkModel.removeRow(index.row())
def hide_mouse(self):
self.contentView.viewport().setCursor(QtCore.Qt.BlankCursor)
if self.contentView.hasFocus():
self.navBar.hide()
def sneaky_change(self):
direction = -1
if self.sender().objectName() == 'nextChapter':
@@ -570,70 +608,6 @@ class Tab(QtWidgets.QWidget):
self.main_window.closeEvent()
class PliantDockWidget(QtWidgets.QDockWidget):
def __init__(self, main_window, intended_for, contentView, parent=None):
super(PliantDockWidget, self).__init__()
self.main_window = main_window
self.intended_for = intended_for
self.contentView = contentView
self.current_annotation = None
def showEvent(self, event):
viewport_height = self.contentView.viewport().size().height()
viewport_topRight = self.contentView.mapToGlobal(
self.contentView.viewport().rect().topRight())
viewport_topLeft = self.contentView.mapToGlobal(
self.contentView.viewport().rect().topLeft())
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
dock_y = viewport_topRight.y() + (viewport_height * .10)
dock_height = viewport_height * .80
if self.intended_for == 'bookmarks':
dock_width = desktop_size.width() // 5.5
dock_x = viewport_topRight.x() - dock_width + 1
self.main_window.bookToolBar.bookmarkButton.setChecked(True)
elif self.intended_for == 'annotations':
dock_width = desktop_size.width() // 5.5
dock_x = viewport_topLeft.x()
self.main_window.bookToolBar.annotationButton.setChecked(True)
elif self.intended_for == 'notes':
dock_width = dock_height = desktop_size.width() // 5.5
dock_x = QtGui.QCursor.pos().x()
dock_y = QtGui.QCursor.pos().y()
self.main_window.active_bookmark_docks.append(self)
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
self.setFocus() # TODO This doesn't work
def hideEvent(self, event=None):
if self.intended_for == 'bookmarks':
self.main_window.bookToolBar.bookmarkButton.setChecked(False)
elif self.intended_for == 'annotations':
self.main_window.bookToolBar.annotationButton.setChecked(False)
elif self.intended_for == 'notes':
annotationNoteEdit = self.findChild(QtWidgets.QTextEdit)
if self.current_annotation:
self.current_annotation['note'] = annotationNoteEdit.toPlainText()
try:
self.main_window.active_bookmark_docks.remove(self)
except ValueError:
pass
def set_annotation(self, annotation):
self.current_annotation = annotation
def closeEvent(self, event):
self.main_window.bookToolBar.annotationButton.setChecked(False)
self.hide()
# Ignoring this event prevents application closure when everything is fullscreened
event.ignore()
class PliantQGraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(PliantQGraphicsScene, self).__init__(parent)
@@ -714,7 +688,8 @@ class DragDropTableView(QtWidgets.QTableView):
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.setFrameShape(QtWidgets.QFrame.Box)
self.setFrameShadow(QtWidgets.QFrame.Plain)
self.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.setSizeAdjustPolicy(
QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.setEditTriggers(
QtWidgets.QAbstractItemView.DoubleClicked |
QtWidgets.QAbstractItemView.EditKeyPressed |
@@ -744,3 +719,19 @@ class DragDropTableView(QtWidgets.QTableView):
event.acceptProposedAction()
else:
super(DragDropTableView, self).dropEvent(event)
class SaysHelloWhenClicked(QtWidgets.QListView):
# Signal declarations must be outside the constructor
# The argument is the type of the data emitted
newIndexSignal = QtCore.pyqtSignal(QtCore.QModelIndex)
def __init__(self, parent):
super(SaysHelloWhenClicked, self).__init__(parent)
self.parent = parent
def currentChanged(self, index, previous_index):
if not index.isValid():
return
self.newIndexSignal.emit(index)

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
beautifulsoup4==4.7.1
lxml==4.3.1
PyMuPDF==1.14.8
PyQt5==5.11.3
PyQt5-sip==4.19.13
soupsieve==1.7.3
xmltodict==0.11.0

View File

@@ -1,14 +1,10 @@
import codecs
from os import path
from setuptools import setup, find_packages
from lector.logger import VERSION
HERE = path.abspath(path.dirname(__file__))
MAJOR_VERSION = '0'
MINOR_VERSION = '4'
MICRO_VERSION = '0'
VERSION = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION)
# Get the long description from the README file
with codecs.open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
LONG_DESC = f.read()
@@ -73,6 +69,6 @@ setup(
extras_require={
'dev': DEV_DEPS,
'test': TEST_DEPS,
'PDF': ['python-poppler-qt5']
'PDF': ['pymupdf']
},
)