Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
55bee210c6 | ||
|
d3746c8e98 | ||
|
ffcf07414f | ||
|
ffaace2eaa | ||
|
32455dd859 | ||
|
3e54340694 | ||
|
bc6c7d1c36 | ||
|
ebd746b7b2 | ||
|
ebc3ef9f1b | ||
|
7238605441 | ||
|
ea86737970 | ||
|
ab4c586c06 | ||
|
7977bde410 | ||
|
626472dd04 | ||
|
d9efe2da3c | ||
|
ec197f0829 | ||
|
335479bcfb | ||
|
cbf01c6d16 | ||
|
98ca118a60 | ||
|
c7aa0e28ee | ||
|
528c2e387c | ||
|
bc54d6b686 | ||
|
8f298de58e | ||
|
366859ebe0 | ||
|
8c51cc047e | ||
|
5081a31f1a | ||
|
aff69d95c1 | ||
|
0b8427c864 | ||
|
43dd6a34d9 | ||
|
2f4adfc183 | ||
|
0d015ad72e | ||
|
406ca0485f | ||
|
ab6760226e | ||
|
66c8626d43 | ||
|
5fa724ae69 | ||
|
d417a94829 | ||
|
9c85a1075e | ||
|
dd4b502861 | ||
|
0f963b20f9 | ||
|
f63b6627b2 | ||
|
00db5d5e0f | ||
|
5e53d40e68 | ||
|
6ffa6934ed | ||
|
34dcf9f1b4 | ||
|
7931f92335 | ||
|
42b655862c | ||
|
e6eb056ec6 | ||
|
7f5b6fc349 | ||
|
9af175b11f | ||
|
c783e44444 | ||
|
a1dba753e8 | ||
|
a55a0e7205 | ||
|
bb8de60efe | ||
|
0d8c2b6648 | ||
|
64a96d816d | ||
|
5a4af54118 | ||
|
4cf18e008d | ||
|
50cc52b116 | ||
|
35f38b9f68 | ||
|
c883ba0175 | ||
|
39cf03a70e | ||
|
44d88d99bb | ||
|
ca67071e91 | ||
|
b5acce6449 | ||
|
7bdf01a67e | ||
|
aca08827fb | ||
|
d4aaa4dc74 | ||
|
98daa40bfd | ||
|
a7df896468 | ||
|
fd149dcafa | ||
|
0bb2e9329f | ||
|
89a32bfeda | ||
|
50089cb57a |
34
Lector.pro
Normal file
@@ -0,0 +1,34 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2018 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/>.
|
||||
|
||||
SOURCES += lector/__main__.py \
|
||||
lector/definitionsdialog.py \
|
||||
lector/metadatadialog.py \
|
||||
lector/models.py \
|
||||
lector/widgets.py \
|
||||
lector/library.py \
|
||||
lector/toolbars.py \
|
||||
lector/settingsdialog.py \
|
||||
lector/resources/definitions.py \
|
||||
lector/resources/settingswindow.py \
|
||||
lector/resources/metadata.py \
|
||||
lector/resources/mainwindow.py
|
||||
|
||||
TRANSLATIONS += lector/resources/translations/Lector_es.ts \
|
||||
lector/resources/translations/Lector_fr.ts \
|
||||
lector/resources/translations/Lector_de.ts \
|
||||
lector/resources/translations/Lector_zh.ts \
|
||||
lector/resources/translations/SAMPLE.ts
|
19
README.md
@@ -16,7 +16,6 @@ Support for a bunch of other formats is coming. Please see the TODO for addition
|
||||
| Qt5 | 5.10.1 |
|
||||
| Python | 3.6 |
|
||||
| PyQt5 | 5.10.1 |
|
||||
| python-requests | 2.18.4 |
|
||||
| python-beautifulsoup4 | 4.6.0 |
|
||||
| poppler-qt5 | 0.61.1 |
|
||||
| python-poppler-qt5 | 0.24.2 |
|
||||
@@ -35,12 +34,16 @@ poppler-qt5 and python-poppler-qt5 are optional.
|
||||
|
||||
### Available packages
|
||||
* [AUR](https://aur.archlinux.org/packages/lector-git/)
|
||||
* [Gentoo (unofficial)](https://bitbucket.org/szymonsz/gen2-overlay/src/master/app-text/lector/)
|
||||
|
||||
## Reporting issues
|
||||
When reporting issues:
|
||||
## Translations
|
||||
1. There is a `SAMPLE.ts` file [here](https://github.com/BasioMeusPuga/Lector/tree/master/lector/resources/translations). Open it in `Qt Linguist`.
|
||||
2. Pick the language you wish to translate to.
|
||||
3. Translate relevant strings.
|
||||
4. Try to resist the urge to include profanity.
|
||||
5. Save the file as `Lector_<language>` and send it to me, preferably as a pull request.
|
||||
|
||||
* 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.
|
||||
Please keep the translations short. There's only so much space for UI elements.
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -68,6 +71,12 @@ When reporting issues:
|
||||
### In program dictionary
|
||||

|
||||
|
||||
## Reporting issues
|
||||
When reporting issues:
|
||||
|
||||
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
|
||||
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
|
||||
|
||||
## Attributions
|
||||
* [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack)
|
||||
* [rarfile](https://github.com/markokr/rarfile)
|
||||
|
33
TODO
@@ -1,4 +1,8 @@
|
||||
TODO
|
||||
General:
|
||||
✓ Internationalization
|
||||
✓ Application icon
|
||||
✓ .desktop file
|
||||
Options:
|
||||
✓ Automatic library management
|
||||
✓ Recursive file addition
|
||||
@@ -25,6 +29,7 @@ TODO
|
||||
✓ Information dialog widget
|
||||
✓ Allow editing of database data through the UI + for Bookmarks
|
||||
✓ Include (action) icons with the applications
|
||||
✓ Drag and drop support for the library
|
||||
Set focus to newly added file
|
||||
Reading:
|
||||
✓ Drop down for TOC
|
||||
@@ -51,8 +56,17 @@ TODO
|
||||
✓ Cache next and previous images
|
||||
✓ Set context menu for definitions and the like
|
||||
✓ Paragraph indentation
|
||||
Search document using QTextCursor?
|
||||
Comic view keyboard shortcuts
|
||||
✓ Comic view keyboard shortcuts
|
||||
✓ Comic view context menu
|
||||
✓ Make the bookmark dock float over the reading area
|
||||
✓ Spacebar should not cut off lines at the top
|
||||
✓ Track open bookmark windows so they can be closed quickly at exit
|
||||
Annotations
|
||||
✓ Text
|
||||
Annotation preview in listView
|
||||
Image
|
||||
Adjust key navigation according to viewport dimensions
|
||||
Search document using QTextCursor
|
||||
Filetypes:
|
||||
✓ pdf support
|
||||
Parse TOC
|
||||
@@ -66,33 +80,28 @@ TODO
|
||||
Other:
|
||||
✓ Define every widget in code
|
||||
Bugs:
|
||||
If there are files open and the database is deleted, TypeErrors result
|
||||
Cover culling does not occur if some other tab has initial focus
|
||||
Slider position change might be acting up too
|
||||
Take metadata from the database when opening the file
|
||||
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
|
||||
|
||||
Secondary:
|
||||
Annotations
|
||||
Graphical themes
|
||||
Change focus rectangle dimensions
|
||||
Tab reordering
|
||||
Universal Ctrl + Tab
|
||||
Allow tabs to detach and form their own windows
|
||||
Goodreads API: Ratings, Read, Recommendations
|
||||
Get ISBN using python-isbnlib
|
||||
Pagination
|
||||
Use embedded fonts + CSS
|
||||
Scrolling: Smooth / By Line
|
||||
Spacebar should not cut off lines at the top
|
||||
Shift to logging instead of print statements
|
||||
txt, doc, chm, djvu, fb2 support
|
||||
Include icons for filetype emblems
|
||||
Drag and drop support for the library
|
||||
Comic view modes
|
||||
Continuous paging
|
||||
Double pages
|
||||
Leave comic images on disk in case tab isn't closed and files are remembered
|
||||
Give the comic view a 'Save image as...' option
|
||||
Ignore a / the / numbers for sorting purposes
|
||||
? Add only one file type if multiple are present
|
||||
? Plugin system for parsers
|
||||
? Create emblem per filetype
|
||||
In application notifications
|
||||
|
110
com.basiomeuspuga.Lector.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"app-id":"com.basiomeuspuga.Lector",
|
||||
"runtime":"org.kde.Platform",
|
||||
"runtime-version":"5.10",
|
||||
"sdk":"org.kde.Sdk",
|
||||
"command":"lector",
|
||||
"rename-icon":"Lector",
|
||||
"rename-desktop-file":"lector.desktop",
|
||||
"rename-appdata-file":"lector.appdata.xml",
|
||||
"finish-args":[
|
||||
"--filesystem=host",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
"--share=ipc",
|
||||
"--share=network"
|
||||
],
|
||||
"build-options":{
|
||||
"cflags":"-O2",
|
||||
"cxxflags":"-O2"
|
||||
},
|
||||
"modules":[
|
||||
{
|
||||
"name": "python",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tar.xz",
|
||||
"sha256": "159b932bf56aeaa76fd66e7420522d8c8853d486b8567c459b84fe2ed13bcaba"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"modules":[
|
||||
{
|
||||
"name":"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"
|
||||
}
|
||||
],
|
||||
"buildsystem":"simple",
|
||||
"build-commands":[
|
||||
"pip3 install --prefix=/app sip-4.19.8-cp36-cp36m-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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"beautifulsoup",
|
||||
"buildsystem":"simple",
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://pypi.python.org/packages/fa/8d/1d14391fdaed5abada4e0f63543fef49b8331a34ca60c88bd521bcf7f782/beautifulsoup4-4.6.0.tar.gz",
|
||||
"sha256":"808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89"
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"python3 setup.py build",
|
||||
"python3 setup.py install --prefix=/app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lxml",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --prefix=/app lxml-4.2.1-cp36-cp36m-manylinux1_x86_64.whl"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://pypi.python.org/packages/a7/b9/ccf46cea0f698b40bca2a9c1a44039c336fe1988b82de4f7353be7a8396a/lxml-4.2.1-cp36-cp36m-manylinux1_x86_64.whl",
|
||||
"sha256": "0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"lector",
|
||||
"buildsystem":"simple",
|
||||
"ensure-writable":[
|
||||
"/lib/python*/site-packages/easy-install.pth"
|
||||
],
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"https://github.com/BasioMeusPuga/Lector.git"
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"python3 setup.py build",
|
||||
"python3 setup.py install --prefix=/app"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
1149
lector/__main__.py
311
lector/annotations.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2018 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/>.
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from lector.resources import annotationswindow
|
||||
|
||||
|
||||
class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
|
||||
def __init__(self, parent=None):
|
||||
super(AnnotationsUI, self).__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self.parent = parent
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
# Current annotation
|
||||
self.modelIndex = None # The index of the annotations list model in the parent dialog
|
||||
self.current_annotation = {}
|
||||
|
||||
# Populate annotation type
|
||||
textmarkup_string = self._translate('AnnotationsUI', 'Text markup')
|
||||
all_types = [textmarkup_string]
|
||||
for i in all_types:
|
||||
self.typeBox.addItem(i)
|
||||
|
||||
# Init defaults
|
||||
self.default_stylesheet = self.foregroundCheck.styleSheet()
|
||||
self.foregroundColor = QtGui.QColor.fromRgb(0, 0, 0)
|
||||
self.underlineColor = QtGui.QColor.fromRgb(255, 0, 0)
|
||||
self.highlightColor = QtGui.QColor.fromRgb(66, 209, 56)
|
||||
self.underline_styles = {
|
||||
'Solid': QtGui.QTextCharFormat.SingleUnderline,
|
||||
'Dashes': QtGui.QTextCharFormat.DashUnderline,
|
||||
'Dots': QtGui.QTextCharFormat.DotLine,
|
||||
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
|
||||
|
||||
# Push buttons
|
||||
self.foregroundColorButton.clicked.connect(self.modify_annotation)
|
||||
self.highlightColorButton.clicked.connect(self.modify_annotation)
|
||||
self.underlineColorButton.clicked.connect(self.modify_annotation)
|
||||
|
||||
self.okButton.clicked.connect(self.ok_pressed)
|
||||
self.cancelButton.clicked.connect(self.hide)
|
||||
|
||||
# Underline combo box
|
||||
underline_items = ['Solid', 'Dashes', 'Dots', 'Wavy']
|
||||
self.underlineType.addItems(underline_items)
|
||||
self.underlineType.currentIndexChanged.connect(self.modify_annotation)
|
||||
|
||||
# Text markup related checkboxes
|
||||
self.foregroundCheck.clicked.connect(self.modify_annotation)
|
||||
self.highlightCheck.clicked.connect(self.modify_annotation)
|
||||
self.boldCheck.clicked.connect(self.modify_annotation)
|
||||
self.italicCheck.clicked.connect(self.modify_annotation)
|
||||
self.underlineCheck.clicked.connect(self.modify_annotation)
|
||||
|
||||
def show_dialog(self, mode, index=None):
|
||||
# TODO
|
||||
# Account for annotation type here
|
||||
# and point to a relevant set of widgets accordingly
|
||||
|
||||
if mode == 'edit' or mode == 'preview':
|
||||
self.modelIndex = index
|
||||
this_annotation = self.parent.annotationModel.data(
|
||||
index, QtCore.Qt.UserRole)
|
||||
|
||||
annotation_name = this_annotation['name']
|
||||
self.nameEdit.setText(annotation_name)
|
||||
|
||||
annotation_components = this_annotation['components']
|
||||
|
||||
if 'foregroundColor' in annotation_components:
|
||||
self.foregroundCheck.setChecked(True)
|
||||
self.set_button_background_color(
|
||||
self.foregroundColorButton, annotation_components['foregroundColor'])
|
||||
else:
|
||||
self.foregroundCheck.setChecked(False)
|
||||
|
||||
if 'highlightColor' in annotation_components:
|
||||
self.highlightCheck.setChecked(True)
|
||||
self.set_button_background_color(
|
||||
self.highlightColorButton, annotation_components['highlightColor'])
|
||||
else:
|
||||
self.highlightCheck.setChecked(False)
|
||||
|
||||
if 'bold' in annotation_components:
|
||||
self.boldCheck.setChecked(True)
|
||||
else:
|
||||
self.boldCheck.setChecked(False)
|
||||
|
||||
if 'italic' in annotation_components:
|
||||
self.italicCheck.setChecked(True)
|
||||
else:
|
||||
self.italicCheck.setChecked(False)
|
||||
|
||||
if 'underline' in annotation_components:
|
||||
self.underlineCheck.setChecked(True)
|
||||
underline_params = annotation_components['underline']
|
||||
self.underlineType.setCurrentText(underline_params[0])
|
||||
self.set_button_background_color(
|
||||
self.underlineColorButton, underline_params[1])
|
||||
else:
|
||||
self.underlineCheck.setChecked(False)
|
||||
|
||||
elif mode == 'add':
|
||||
new_annotation_string = self._translate('AnnotationsUI', 'New annotation')
|
||||
self.nameEdit.setText(new_annotation_string)
|
||||
|
||||
all_checkboxes = (
|
||||
self.foregroundCheck, self.highlightCheck,
|
||||
self.boldCheck, self.italicCheck, self.underlineCheck)
|
||||
for i in all_checkboxes:
|
||||
i.setChecked(False)
|
||||
|
||||
self.modelIndex = None
|
||||
self.set_button_background_color(
|
||||
self.foregroundColorButton, self.foregroundColor)
|
||||
self.set_button_background_color(
|
||||
self.highlightColorButton, self.highlightColor)
|
||||
self.set_button_background_color(
|
||||
self.underlineColorButton, self.underlineColor)
|
||||
|
||||
self.update_preview()
|
||||
if mode != 'preview':
|
||||
self.show()
|
||||
|
||||
def set_button_background_color(self, button, color):
|
||||
button.setStyleSheet(
|
||||
"QPushButton {{background-color: {0}}}".format(color.name()))
|
||||
|
||||
def update_preview(self):
|
||||
cursor = self.parent.previewView.textCursor()
|
||||
cursor.setPosition(0)
|
||||
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
# TODO
|
||||
# Other kinds of text markup
|
||||
previewCharFormat = QtGui.QTextCharFormat()
|
||||
|
||||
if self.foregroundCheck.isChecked():
|
||||
previewCharFormat.setForeground(self.foregroundColor)
|
||||
|
||||
highlight = QtCore.Qt.transparent
|
||||
if self.highlightCheck.isChecked():
|
||||
highlight = self.highlightColor
|
||||
previewCharFormat.setBackground(highlight)
|
||||
|
||||
font_weight = QtGui.QFont.Normal
|
||||
if self.boldCheck.isChecked():
|
||||
font_weight = QtGui.QFont.Bold
|
||||
previewCharFormat.setFontWeight(font_weight)
|
||||
|
||||
if self.italicCheck.isChecked():
|
||||
previewCharFormat.setFontItalic(True)
|
||||
|
||||
if self.underlineCheck.isChecked():
|
||||
previewCharFormat.setFontUnderline(True)
|
||||
previewCharFormat.setUnderlineColor(self.underlineColor)
|
||||
previewCharFormat.setUnderlineStyle(
|
||||
self.underline_styles[self.underlineType.currentText()])
|
||||
|
||||
previewCharFormat.setFontStyleStrategy(
|
||||
QtGui.QFont.PreferAntialias)
|
||||
|
||||
cursor.setCharFormat(previewCharFormat)
|
||||
cursor.clearSelection()
|
||||
self.parent.previewView.setTextCursor(cursor)
|
||||
|
||||
def modify_annotation(self):
|
||||
sender = self.sender()
|
||||
if isinstance(sender, QtWidgets.QCheckBox):
|
||||
if not sender.isChecked():
|
||||
self.update_preview()
|
||||
return
|
||||
|
||||
new_color = None
|
||||
|
||||
if sender == self.foregroundColorButton:
|
||||
new_color = self.get_color(self.foregroundColor)
|
||||
self.foregroundColor = new_color
|
||||
|
||||
if sender == self.highlightColorButton:
|
||||
new_color = self.get_color(self.highlightColor)
|
||||
self.highlightColor = new_color
|
||||
|
||||
if sender == self.underlineColorButton:
|
||||
new_color = self.get_color(self.underlineColor)
|
||||
self.underlineColor = new_color
|
||||
|
||||
if new_color:
|
||||
self.set_button_background_color(sender, new_color)
|
||||
self.update_preview()
|
||||
|
||||
def get_color(self, current_color):
|
||||
color_dialog = QtWidgets.QColorDialog()
|
||||
new_color = color_dialog.getColor(current_color)
|
||||
if new_color.isValid(): # Returned in case cancel is pressed
|
||||
return new_color
|
||||
else:
|
||||
return current_color
|
||||
|
||||
def ok_pressed(self):
|
||||
annotation_name = self.nameEdit.text()
|
||||
if annotation_name == '':
|
||||
self.nameEdit.setText('Why do you like bugs? WHY?')
|
||||
return
|
||||
|
||||
annotation_components = {}
|
||||
if self.foregroundCheck.isChecked():
|
||||
annotation_components['foregroundColor'] = self.foregroundColor
|
||||
if self.highlightCheck.isChecked():
|
||||
annotation_components['highlightColor'] = self.highlightColor
|
||||
if self.boldCheck.isChecked():
|
||||
annotation_components['bold'] = True
|
||||
if self.italicCheck.isChecked():
|
||||
annotation_components['italic'] = True
|
||||
if self.underlineCheck.isChecked():
|
||||
annotation_components['underline'] = (
|
||||
self.underlineType.currentText(), self.underlineColor)
|
||||
|
||||
self.current_annotation = {
|
||||
'name': annotation_name,
|
||||
'applicable_to': 'text',
|
||||
'type': 'text_markup',
|
||||
'components': annotation_components}
|
||||
|
||||
if self.modelIndex:
|
||||
self.parent.annotationModel.setData(
|
||||
self.modelIndex, annotation_name, QtCore.Qt.DisplayRole)
|
||||
self.parent.annotationModel.setData(
|
||||
self.modelIndex, self.current_annotation, QtCore.Qt.UserRole)
|
||||
else: # New annotation
|
||||
new_annotation_item = QtGui.QStandardItem()
|
||||
new_annotation_item.setText(annotation_name)
|
||||
new_annotation_item.setData(self.current_annotation, QtCore.Qt.UserRole)
|
||||
self.parent.annotationModel.appendRow(new_annotation_item)
|
||||
|
||||
self.hide()
|
||||
|
||||
|
||||
class AnnotationPlacement:
|
||||
def __init__(self):
|
||||
self.annotation_type = None
|
||||
self.annotation_components = None
|
||||
self.underline_styles = {
|
||||
'Solid': QtGui.QTextCharFormat.SingleUnderline,
|
||||
'Dashes': QtGui.QTextCharFormat.DashUnderline,
|
||||
'Dots': QtGui.QTextCharFormat.DotLine,
|
||||
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
|
||||
|
||||
def set_current_annotation(self, annotation_type, annotation_components):
|
||||
# Components expected to be a dictionary
|
||||
self.annotation_type = annotation_type # This is currently unused
|
||||
self.annotation_components = annotation_components
|
||||
|
||||
def format_text(self, cursor, start_here, end_here):
|
||||
# This is applicable only to the PliantQTextBrowser
|
||||
# for the text_markup style of annotation
|
||||
|
||||
# The cursor is the textCursor of the QTextEdit
|
||||
# containing the text that has to be modified
|
||||
|
||||
if not self.annotation_components:
|
||||
return
|
||||
|
||||
cursor.setPosition(start_here)
|
||||
cursor.setPosition(end_here, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
newCharFormat = QtGui.QTextCharFormat()
|
||||
|
||||
if 'foregroundColor' in self.annotation_components:
|
||||
newCharFormat.setForeground(
|
||||
self.annotation_components['foregroundColor'])
|
||||
|
||||
if 'highlightColor' in self.annotation_components:
|
||||
newCharFormat.setBackground(
|
||||
self.annotation_components['highlightColor'])
|
||||
|
||||
if 'bold' in self.annotation_components:
|
||||
newCharFormat.setFontWeight(QtGui.QFont.Bold)
|
||||
|
||||
if 'italic' in self.annotation_components:
|
||||
newCharFormat.setFontItalic(True)
|
||||
|
||||
if 'underline' in self.annotation_components:
|
||||
newCharFormat.setFontUnderline(True)
|
||||
newCharFormat.setUnderlineStyle(
|
||||
self.underline_styles[self.annotation_components['underline'][0]])
|
||||
newCharFormat.setUnderlineColor(
|
||||
self.annotation_components['underline'][1])
|
||||
|
||||
newCharFormat.setFontStyleStrategy(
|
||||
QtGui.QFont.PreferAntialias)
|
||||
|
||||
cursor.setCharFormat(newCharFormat)
|
||||
cursor.clearSelection()
|
||||
return cursor
|
764
lector/contentwidgets.py
Normal file
@@ -0,0 +1,764 @@
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017-2018 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import webbrowser
|
||||
|
||||
try:
|
||||
import popplerqt5
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
from lector.rarfile import rarfile
|
||||
from lector.threaded import BackGroundCacheRefill
|
||||
from lector.annotations import AnnotationPlacement
|
||||
|
||||
|
||||
class PliantQGraphicsView(QtWidgets.QGraphicsView):
|
||||
def __init__(self, filepath, main_window, parent=None):
|
||||
super(PliantQGraphicsView, self).__init__(parent)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
self.parent = parent
|
||||
self.main_window = main_window
|
||||
|
||||
self.qimage = None # Will be needed to resize pdf
|
||||
self.image_pixmap = None
|
||||
self.image_cache = [None for _ in range(4)]
|
||||
|
||||
self.thread = None
|
||||
|
||||
self.annotation_dict = self.parent.metadata['annotations']
|
||||
|
||||
self.filepath = filepath
|
||||
self.filetype = os.path.splitext(self.filepath)[1][1:]
|
||||
|
||||
if self.filetype == 'cbz':
|
||||
self.book = zipfile.ZipFile(self.filepath)
|
||||
|
||||
elif self.filetype == 'cbr':
|
||||
self.book = rarfile.RarFile(self.filepath)
|
||||
|
||||
elif self.filetype == 'pdf':
|
||||
self.book = popplerqt5.Poppler.Document.load(self.filepath)
|
||||
self.book.setRenderHint(
|
||||
popplerqt5.Poppler.Document.Antialiasing
|
||||
and popplerqt5.Poppler.Document.TextAntialiasing)
|
||||
|
||||
self.common_functions = PliantWidgetsCommonFunctions(
|
||||
self, self.main_window)
|
||||
|
||||
self.ignore_wheel_event = False
|
||||
self.ignore_wheel_event_number = 0
|
||||
self.setMouseTracking(True)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(
|
||||
self.generate_graphicsview_context_menu)
|
||||
|
||||
def loadImage(self, current_page):
|
||||
# TODO
|
||||
# For double page view: 1 before, 1 after
|
||||
all_pages = [i[1] for i in self.parent.metadata['content']]
|
||||
|
||||
def load_page(current_page):
|
||||
image_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
|
||||
|
||||
def generate_image_cache(current_page):
|
||||
print('Building image cache')
|
||||
current_page_index = all_pages.index(current_page)
|
||||
|
||||
for i in (-1, 0, 1, 2):
|
||||
try:
|
||||
this_page = all_pages[current_page_index + i]
|
||||
this_pixmap = load_page(this_page)
|
||||
self.image_cache[i + 1] = (this_page, this_pixmap)
|
||||
except IndexError:
|
||||
self.image_cache[i + 1] = None
|
||||
|
||||
def refill_cache(remove_value):
|
||||
# Do NOT put a parent in here or the mother of all
|
||||
# memory leaks will result
|
||||
self.thread = BackGroundCacheRefill(
|
||||
self.image_cache, remove_value,
|
||||
self.filetype, self.book, all_pages)
|
||||
self.thread.finished.connect(overwrite_cache)
|
||||
self.thread.start()
|
||||
|
||||
def overwrite_cache():
|
||||
self.image_cache = self.thread.image_cache
|
||||
|
||||
def check_cache(current_page):
|
||||
for i in self.image_cache:
|
||||
if i:
|
||||
if i[0] == current_page:
|
||||
return_pixmap = i[1]
|
||||
refill_cache(i)
|
||||
return return_pixmap
|
||||
|
||||
# No return happened so the image isn't in the cache
|
||||
generate_image_cache(current_page)
|
||||
|
||||
if self.main_window.settings['caching_enabled']:
|
||||
return_pixmap = None
|
||||
while not return_pixmap:
|
||||
return_pixmap = check_cache(current_page)
|
||||
else:
|
||||
return_pixmap = load_page(current_page)
|
||||
|
||||
self.image_pixmap = return_pixmap
|
||||
self.resizeEvent()
|
||||
|
||||
def resizeEvent(self, *args):
|
||||
if not self.image_pixmap:
|
||||
return
|
||||
|
||||
zoom_mode = self.main_window.comic_profile['zoom_mode']
|
||||
padding = self.main_window.comic_profile['padding']
|
||||
|
||||
if zoom_mode == 'fitWidth':
|
||||
available_width = self.viewport().width()
|
||||
image_pixmap = self.image_pixmap.scaledToWidth(
|
||||
available_width, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
elif zoom_mode == 'originalSize':
|
||||
image_pixmap = self.image_pixmap
|
||||
|
||||
new_padding = (self.viewport().width() - image_pixmap.width()) // 2
|
||||
if new_padding < 0: # The image is larger than the viewport
|
||||
self.main_window.comic_profile['padding'] = 0
|
||||
else:
|
||||
self.main_window.comic_profile['padding'] = new_padding
|
||||
|
||||
elif zoom_mode == 'bestFit':
|
||||
available_width = self.viewport().width()
|
||||
available_height = self.viewport().height()
|
||||
|
||||
image_pixmap = self.image_pixmap.scaled(
|
||||
available_width, available_height,
|
||||
QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
self.main_window.comic_profile['padding'] = (
|
||||
self.viewport().width() - image_pixmap.width()) // 2
|
||||
|
||||
elif zoom_mode == 'manualZoom':
|
||||
available_width = self.viewport().width() - 2 * padding
|
||||
image_pixmap = self.image_pixmap.scaledToWidth(
|
||||
available_width, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
graphics_scene = QtWidgets.QGraphicsScene()
|
||||
graphics_scene.addPixmap(image_pixmap)
|
||||
|
||||
self.setScene(graphics_scene)
|
||||
self.show()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
self.common_functions.wheelEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
vertical = self.verticalScrollBar().value()
|
||||
maximum = self.verticalScrollBar().maximum()
|
||||
|
||||
def scroller(increment, move_forward=True):
|
||||
if move_forward:
|
||||
if vertical == maximum:
|
||||
self.common_functions.change_chapter(1, True)
|
||||
else:
|
||||
next_val = vertical + increment
|
||||
if next_val >= .95 * maximum:
|
||||
next_val = maximum
|
||||
self.verticalScrollBar().setValue(next_val)
|
||||
else:
|
||||
if vertical == 0:
|
||||
self.common_functions.change_chapter(-1, False)
|
||||
else:
|
||||
next_val = vertical - increment
|
||||
if next_val <= .05 * maximum:
|
||||
next_val = 0
|
||||
self.verticalScrollBar().setValue(next_val)
|
||||
|
||||
small_increment = maximum // 4
|
||||
big_increment = maximum // 2
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Up:
|
||||
scroller(small_increment, False)
|
||||
if event.key() == QtCore.Qt.Key_Down:
|
||||
scroller(small_increment)
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
scroller(big_increment)
|
||||
|
||||
view_modification_keys = (
|
||||
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Minus, QtCore.Qt.Key_Equal,
|
||||
QtCore.Qt.Key_B, QtCore.Qt.Key_W, QtCore.Qt.Key_O)
|
||||
if event.key() in view_modification_keys:
|
||||
self.main_window.modify_comic_view(event.key())
|
||||
|
||||
def record_position(self):
|
||||
self.parent.metadata['position']['is_read'] = False
|
||||
self.common_functions.update_model()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if QtWidgets.QApplication.mouseButtons() == QtCore.Qt.NoButton:
|
||||
self.viewport().setCursor(QtCore.Qt.OpenHandCursor)
|
||||
else:
|
||||
self.viewport().setCursor(QtCore.Qt.ClosedHandCursor)
|
||||
self.parent.mouse_hide_timer.start(3000)
|
||||
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
|
||||
|
||||
def generate_graphicsview_context_menu(self, position):
|
||||
contextMenu = QtWidgets.QMenu()
|
||||
|
||||
saveAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('filesaveas'),
|
||||
self._translate('PliantQGraphicsView', 'Save page as...'))
|
||||
|
||||
fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
|
||||
|
||||
if self.parent.is_fullscreen:
|
||||
fsToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('view-fullscreen'),
|
||||
self._translate('PliantQGraphicsView', 'Exit fullscreen'))
|
||||
else:
|
||||
if self.main_window.settings['show_bars']:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQGraphicsView', 'Distraction Free mode')
|
||||
else:
|
||||
distraction_free_prompt = self._translate(
|
||||
'PliantQGraphicsView', 'Exit Distraction Free mode')
|
||||
|
||||
dfToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('visibility'),
|
||||
distraction_free_prompt)
|
||||
|
||||
view_submenu_string = self._translate('PliantQGraphicsView', 'View')
|
||||
viewSubMenu = contextMenu.addMenu(view_submenu_string)
|
||||
viewSubMenu.setIcon(
|
||||
self.main_window.QImageFactory.get_image('mail-thread-watch'))
|
||||
|
||||
zoominAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-in'),
|
||||
self._translate('PliantQGraphicsView', 'Zoom in (+)'))
|
||||
|
||||
zoomoutAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-out'),
|
||||
self._translate('PliantQGraphicsView', 'Zoom out (-)'))
|
||||
|
||||
fitWidthAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-fit-width'),
|
||||
self._translate('PliantQGraphicsView', 'Fit width (W)'))
|
||||
|
||||
bestFitAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-fit-best'),
|
||||
self._translate('PliantQGraphicsView', 'Best fit (B)'))
|
||||
|
||||
originalSizeAction = viewSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('zoom-original'),
|
||||
self._translate('PliantQGraphicsView', 'Original size (O)'))
|
||||
|
||||
bookmarksToggleAction = 'Latin quote 2. Electric Boogaloo.'
|
||||
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
|
||||
bookmarksToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('bookmarks'),
|
||||
self._translate('PliantQGraphicsView', 'Bookmarks'))
|
||||
|
||||
self.common_functions.generate_combo_box_action(contextMenu)
|
||||
|
||||
action = contextMenu.exec_(self.sender().mapToGlobal(position))
|
||||
|
||||
if action == saveAction:
|
||||
dialog_prompt = self._translate('Main_UI', 'Save page as...')
|
||||
extension_string = self._translate('Main_UI', 'Images')
|
||||
save_file = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self, dialog_prompt, self.main_window.settings['last_open_path'],
|
||||
f'{extension_string} (*.png *.jpg *.bmp)')
|
||||
|
||||
if save_file:
|
||||
self.image_pixmap.save(save_file[0])
|
||||
|
||||
if action == bookmarksToggleAction:
|
||||
self.parent.toggle_bookmarks()
|
||||
if action == dfToggleAction:
|
||||
self.main_window.toggle_distraction_free()
|
||||
if action == fsToggleAction:
|
||||
self.parent.exit_fullscreen()
|
||||
|
||||
view_action_dict = {
|
||||
zoominAction: QtCore.Qt.Key_Plus,
|
||||
zoomoutAction: QtCore.Qt.Key_Minus,
|
||||
fitWidthAction: QtCore.Qt.Key_W,
|
||||
bestFitAction: QtCore.Qt.Key_B,
|
||||
originalSizeAction: QtCore.Qt.Key_O}
|
||||
|
||||
if action in view_action_dict:
|
||||
self.main_window.modify_comic_view(view_action_dict[action])
|
||||
|
||||
def closeEvent(self, *args):
|
||||
# In case the program is closed when a contentView is fullscreened
|
||||
self.main_window.closeEvent()
|
||||
|
||||
def toggle_annotation_mode(self):
|
||||
pass
|
||||
|
||||
|
||||
class PliantQTextBrowser(QtWidgets.QTextBrowser):
|
||||
def __init__(self, main_window, parent=None):
|
||||
super(PliantQTextBrowser, self).__init__(parent)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
self.parent = parent
|
||||
self.main_window = main_window
|
||||
|
||||
self.annotation_mode = False
|
||||
self.annotator = AnnotationPlacement()
|
||||
self.current_annotation = None
|
||||
self.annotation_dict = self.parent.metadata['annotations']
|
||||
|
||||
self.common_functions = PliantWidgetsCommonFunctions(
|
||||
self, self.main_window)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(
|
||||
self.generate_textbrowser_context_menu)
|
||||
|
||||
self.setMouseTracking(True)
|
||||
self.verticalScrollBar().sliderMoved.connect(
|
||||
self.record_position)
|
||||
self.ignore_wheel_event = False
|
||||
self.ignore_wheel_event_number = 0
|
||||
|
||||
self.at_end = False
|
||||
|
||||
def wheelEvent(self, event):
|
||||
self.record_position()
|
||||
self.common_functions.wheelEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
QtWidgets.QTextEdit.keyPressEvent(self, event)
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
if self.verticalScrollBar().value() == self.verticalScrollBar().maximum():
|
||||
if self.at_end: # This makes sure the last lines of the chapter don't get skipped
|
||||
self.common_functions.change_chapter(1, True)
|
||||
self.at_end = True
|
||||
else:
|
||||
self.at_end = False
|
||||
self.set_top_line_cleanly()
|
||||
self.record_position()
|
||||
|
||||
def set_top_line_cleanly(self):
|
||||
# Find the cursor position of the top line and move to it
|
||||
find_cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
|
||||
find_cursor.movePosition(
|
||||
find_cursor.position(), QtGui.QTextCursor.KeepAnchor)
|
||||
self.setTextCursor(find_cursor)
|
||||
self.ensureCursorVisible()
|
||||
|
||||
def record_position(self, return_as_bookmark=False):
|
||||
self.parent.metadata['position']['is_read'] = False
|
||||
|
||||
cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
|
||||
cursor_position = cursor.position()
|
||||
|
||||
# Current block for progress measurement
|
||||
current_block = cursor.block().blockNumber()
|
||||
current_chapter = self.parent.metadata['position']['current_chapter']
|
||||
|
||||
blocks_per_chapter = self.parent.metadata['position']['blocks_per_chapter']
|
||||
block_sum = sum(blocks_per_chapter[:(current_chapter - 1)])
|
||||
block_sum += current_block
|
||||
|
||||
# This 'current_block' refers to the number of
|
||||
# blocks in the book upto this one
|
||||
self.parent.metadata['position']['current_block'] = block_sum
|
||||
self.common_functions.update_model()
|
||||
|
||||
if return_as_bookmark:
|
||||
return (self.parent.metadata['position']['current_chapter'],
|
||||
cursor_position)
|
||||
else:
|
||||
self.parent.metadata['position']['cursor_position'] = cursor_position
|
||||
|
||||
def toggle_annotation_mode(self):
|
||||
if self.annotation_mode:
|
||||
self.annotation_mode = False
|
||||
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.parent.annotationDock.show()
|
||||
self.parent.annotationDock.setWindowOpacity(.95)
|
||||
|
||||
self.current_annotation = None
|
||||
self.parent.annotationListView.clearSelection()
|
||||
|
||||
else:
|
||||
self.annotation_mode = True
|
||||
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
||||
self.parent.annotationDock.hide()
|
||||
|
||||
selected_index = self.parent.annotationListView.currentIndex()
|
||||
self.current_annotation = self.parent.annotationModel.data(
|
||||
selected_index, QtCore.Qt.UserRole)
|
||||
print('Current annotation: ' + self.current_annotation['name'])
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# This takes care of annotation placement
|
||||
# and addition to the list that holds all current annotations
|
||||
if not self.current_annotation:
|
||||
QtWidgets.QTextBrowser.mouseReleaseEvent(self, event)
|
||||
return
|
||||
|
||||
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']
|
||||
|
||||
self.annotator.set_current_annotation(
|
||||
annotation_type, annotation_components)
|
||||
|
||||
new_cursor = self.annotator.format_text(
|
||||
cursor, cursor_start, cursor_end)
|
||||
self.setTextCursor(new_cursor)
|
||||
|
||||
# TODO
|
||||
# Maybe use annotation name for a consolidated annotation list
|
||||
|
||||
this_annotation = {
|
||||
'name': self.current_annotation['name'],
|
||||
'applicable_to': applicable_to,
|
||||
'type': annotation_type,
|
||||
'cursor': (cursor_start, cursor_end),
|
||||
'components': annotation_components,
|
||||
'note': None}
|
||||
|
||||
try:
|
||||
self.annotation_dict[current_chapter].append(this_annotation)
|
||||
except KeyError:
|
||||
self.annotation_dict[current_chapter] = []
|
||||
self.annotation_dict[current_chapter].append(this_annotation)
|
||||
|
||||
self.toggle_annotation_mode()
|
||||
|
||||
def generate_textbrowser_context_menu(self, position):
|
||||
selection = self.textCursor().selection()
|
||||
selection = selection.toPlainText()
|
||||
|
||||
current_chapter = self.parent.metadata['position']['current_chapter']
|
||||
cursor_at_mouse = self.cursorForPosition(position)
|
||||
annotation_is_present = self.common_functions.annotation_specific(
|
||||
'check', 'text', current_chapter, cursor_at_mouse.position())
|
||||
|
||||
contextMenu = QtWidgets.QMenu()
|
||||
|
||||
# The following cannot be None because a click
|
||||
# outside the menu means that the action variable is None.
|
||||
defineAction = fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
|
||||
searchWikipediaAction = searchYoutubeAction = 'Does anyone know something funny in Latin?'
|
||||
searchAction = searchGoogleAction = bookmarksToggleAction = 'TODO Insert Latin Joke'
|
||||
deleteAnnotationAction = editAnnotationNoteAction = 'Latin quote 2. Electric Boogaloo.'
|
||||
|
||||
if selection and selection != '':
|
||||
first_selected_word = selection.split()[0]
|
||||
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.setIcon(self.main_window.QImageFactory.get_image('search'))
|
||||
|
||||
searchAction = searchSubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('search'),
|
||||
self._translate('PliantQTextBrowser', 'In this book'))
|
||||
searchSubMenu.addSeparator()
|
||||
searchGoogleAction = searchSubMenu.addAction(
|
||||
QtGui.QIcon(':/images/Google.png'),
|
||||
'Google')
|
||||
searchWikipediaAction = searchSubMenu.addAction(
|
||||
QtGui.QIcon(':/images/Wikipedia.png'),
|
||||
'Wikipedia')
|
||||
searchYoutubeAction = searchSubMenu.addAction(
|
||||
QtGui.QIcon(':/images/Youtube.png'),
|
||||
'Youtube')
|
||||
|
||||
if annotation_is_present:
|
||||
annotationsubMenu = contextMenu.addMenu('Annotation')
|
||||
annotationsubMenu.setIcon(self.main_window.QImageFactory.get_image('annotate'))
|
||||
|
||||
editAnnotationNoteAction = annotationsubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('edit-rename'),
|
||||
self._translate('PliantQTextBrowser', 'Edit note'))
|
||||
deleteAnnotationAction = annotationsubMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('remove'),
|
||||
self._translate('PliantQTextBrowser', 'Delete annotation'))
|
||||
|
||||
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)
|
||||
|
||||
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
|
||||
bookmarksToggleAction = contextMenu.addAction(
|
||||
self.main_window.QImageFactory.get_image('bookmarks'),
|
||||
self._translate('PliantQTextBrowser', 'Bookmarks'))
|
||||
|
||||
self.common_functions.generate_combo_box_action(contextMenu)
|
||||
|
||||
action = contextMenu.exec_(self.sender().mapToGlobal(position))
|
||||
|
||||
if action == 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 action == searchGoogleAction:
|
||||
webbrowser.open_new_tab(
|
||||
f'https://www.google.com/search?q={selection}')
|
||||
if action == searchWikipediaAction:
|
||||
webbrowser.open_new_tab(
|
||||
f'https://en.wikipedia.org/wiki/Special:Search?search={selection}')
|
||||
if action == searchYoutubeAction:
|
||||
webbrowser.open_new_tab(
|
||||
f'https://www.youtube.com/results?search_query={selection}')
|
||||
|
||||
if action == 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()
|
||||
|
||||
if action == fsToggleAction:
|
||||
self.parent.exit_fullscreen()
|
||||
if action == dfToggleAction:
|
||||
self.main_window.toggle_distraction_free()
|
||||
|
||||
def closeEvent(self, *args):
|
||||
self.main_window.closeEvent()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.annotation_mode:
|
||||
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
|
||||
else:
|
||||
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.parent.mouse_hide_timer.start(3000)
|
||||
QtWidgets.QTextBrowser.mouseMoveEvent(self, event)
|
||||
|
||||
|
||||
class PliantWidgetsCommonFunctions:
|
||||
def __init__(self, parent_widget, main_window):
|
||||
self.pw = parent_widget
|
||||
self.main_window = main_window
|
||||
self.are_we_doing_images_only = self.pw.parent.are_we_doing_images_only
|
||||
|
||||
def wheelEvent(self, event):
|
||||
ignore_events = 20
|
||||
if self.are_we_doing_images_only:
|
||||
ignore_events = 10
|
||||
|
||||
if self.pw.ignore_wheel_event:
|
||||
self.pw.ignore_wheel_event_number += 1
|
||||
if self.pw.ignore_wheel_event_number > ignore_events:
|
||||
self.pw.ignore_wheel_event = False
|
||||
self.pw.ignore_wheel_event_number = 0
|
||||
return
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
QtWidgets.QGraphicsView.wheelEvent(self.pw, event)
|
||||
else:
|
||||
QtWidgets.QTextBrowser.wheelEvent(self.pw, event)
|
||||
|
||||
# Since this is a delta on a mouse move event, it cannot ever be 0
|
||||
vertical_pdelta = event.pixelDelta().y()
|
||||
if vertical_pdelta > 0:
|
||||
moving_up = True
|
||||
elif vertical_pdelta < 0:
|
||||
moving_up = False
|
||||
|
||||
if abs(vertical_pdelta) > 80: # Adjust sensitivity here
|
||||
# Implies that no scrollbar movement is possible
|
||||
if self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum() == 0:
|
||||
if moving_up:
|
||||
self.change_chapter(-1)
|
||||
else:
|
||||
self.change_chapter(1)
|
||||
|
||||
# Implies that the scrollbar is at the bottom
|
||||
elif self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum():
|
||||
if not moving_up:
|
||||
self.change_chapter(1)
|
||||
|
||||
# Implies scrollbar is at the top
|
||||
elif self.pw.verticalScrollBar().value() == 0:
|
||||
if moving_up:
|
||||
self.change_chapter(-1)
|
||||
|
||||
def change_chapter(self, direction, was_button_pressed=None):
|
||||
current_toc_index = self.main_window.bookToolBar.tocBox.currentIndex()
|
||||
max_toc_index = self.main_window.bookToolBar.tocBox.count() - 1
|
||||
|
||||
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)
|
||||
|
||||
# Set page position depending on if the chapter number is increasing or decreasing
|
||||
if direction == 1 or was_button_pressed:
|
||||
self.pw.verticalScrollBar().setValue(0)
|
||||
else:
|
||||
self.pw.verticalScrollBar().setValue(
|
||||
self.pw.verticalScrollBar().maximum())
|
||||
|
||||
if not was_button_pressed:
|
||||
self.pw.ignore_wheel_event = True
|
||||
|
||||
def load_annotations(self, chapter):
|
||||
try:
|
||||
chapter_annotations = self.pw.annotation_dict[chapter]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
for i in chapter_annotations:
|
||||
applicable_to = i['applicable_to']
|
||||
annotation_type = i['type']
|
||||
annotation_components = i['components']
|
||||
|
||||
if not self.are_we_doing_images_only and applicable_to == 'text':
|
||||
cursor = self.pw.textCursor()
|
||||
cursor_start = i['cursor'][0]
|
||||
cursor_end = i['cursor'][1]
|
||||
|
||||
self.pw.annotator.set_current_annotation(
|
||||
annotation_type, annotation_components)
|
||||
|
||||
new_cursor = self.pw.annotator.format_text(
|
||||
cursor, cursor_start, cursor_end)
|
||||
self.pw.setTextCursor(new_cursor)
|
||||
|
||||
def clear_annotations(self):
|
||||
if not self.are_we_doing_images_only:
|
||||
cursor = self.pw.textCursor()
|
||||
cursor.setPosition(0)
|
||||
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
|
||||
|
||||
previewCharFormat = QtGui.QTextCharFormat()
|
||||
previewCharFormat.setFontStyleStrategy(
|
||||
QtGui.QFont.PreferAntialias)
|
||||
cursor.setCharFormat(previewCharFormat)
|
||||
cursor.clearSelection()
|
||||
self.pw.setTextCursor(cursor)
|
||||
|
||||
def annotation_specific(self, mode, annotation_type, chapter, cursor_position):
|
||||
try:
|
||||
chapter_annotations = self.pw.annotation_dict[chapter]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
for i in chapter_annotations:
|
||||
if annotation_type == 'text':
|
||||
cursor_start = i['cursor'][0]
|
||||
cursor_end = i['cursor'][1]
|
||||
|
||||
if cursor_start <= cursor_position <= cursor_end:
|
||||
if mode == 'check':
|
||||
return True
|
||||
if mode == 'delete':
|
||||
self.pw.annotation_dict[chapter].remove(i)
|
||||
if mode == 'note':
|
||||
note = i['note']
|
||||
self.pw.parent.annotationNoteDock.set_annotation(i)
|
||||
self.pw.parent.annotationNoteEdit.setText(note)
|
||||
self.pw.parent.annotationNoteDock.show()
|
||||
|
||||
# Post iteration
|
||||
if mode == 'check':
|
||||
return False
|
||||
if mode == 'delete':
|
||||
scroll_position = self.pw.verticalScrollBar().value()
|
||||
self.clear_annotations()
|
||||
self.load_annotations(chapter)
|
||||
self.pw.verticalScrollBar().setValue(scroll_position)
|
||||
|
||||
def update_model(self):
|
||||
# We're updating the underlying model to have real-time
|
||||
# updates on the read status
|
||||
|
||||
# Set a baseline model index in case the item gets deleted
|
||||
# E.g It's open in a tab and deleted from the library
|
||||
model_index = None
|
||||
start_index = self.main_window.lib_ref.libraryModel.index(0, 0)
|
||||
|
||||
# Find index of the model item that corresponds to the tab
|
||||
model_index = self.main_window.lib_ref.libraryModel.match(
|
||||
start_index,
|
||||
QtCore.Qt.UserRole + 6,
|
||||
self.pw.parent.metadata['hash'],
|
||||
1, QtCore.Qt.MatchExactly)
|
||||
|
||||
if self.are_we_doing_images_only:
|
||||
position_percentage = (self.pw.parent.metadata['position']['current_chapter'] /
|
||||
self.pw.parent.metadata['position']['total_chapters'])
|
||||
else:
|
||||
position_percentage = (self.pw.parent.metadata['position']['current_block'] /
|
||||
self.pw.parent.metadata['position']['total_blocks'])
|
||||
|
||||
# Update position percentage
|
||||
if model_index:
|
||||
self.main_window.lib_ref.libraryModel.setData(
|
||||
model_index[0], position_percentage, QtCore.Qt.UserRole + 7)
|
||||
|
||||
def generate_combo_box_action(self, contextMenu):
|
||||
contextMenu.addSeparator()
|
||||
|
||||
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)
|
||||
|
||||
comboboxAction = QtWidgets.QWidgetAction(self.pw)
|
||||
comboboxAction.setDefaultWidget(toc_combobox)
|
||||
contextMenu.addAction(comboboxAction)
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2018 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,35 +17,77 @@
|
||||
import os
|
||||
import pickle
|
||||
import sqlite3
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
|
||||
class DatabaseInit:
|
||||
def __init__(self, location_prefix):
|
||||
os.makedirs(location_prefix, exist_ok=True)
|
||||
database_path = os.path.join(location_prefix, 'Lector.db')
|
||||
self.database_path = os.path.join(location_prefix, 'Lector.db')
|
||||
|
||||
if not os.path.exists(database_path):
|
||||
self.database = sqlite3.connect(database_path)
|
||||
self.books_table_columns = {
|
||||
'id': 'INTEGER PRIMARY KEY',
|
||||
'Title': 'TEXT',
|
||||
'Author': 'TEXT',
|
||||
'Year': 'INTEGER',
|
||||
'DateAdded': 'BLOB',
|
||||
'Path': 'TEXT',
|
||||
'Position': 'BLOB',
|
||||
'ISBN': 'TEXT',
|
||||
'Tags': 'TEXT',
|
||||
'Hash': 'TEXT',
|
||||
'LastAccessed': 'BLOB',
|
||||
'Bookmarks': 'BLOB',
|
||||
'CoverImage': 'BLOB',
|
||||
'Addition': 'TEXT',
|
||||
'Annotations': 'BLOB'}
|
||||
|
||||
self.directories_table_columns = {
|
||||
'id': 'INTEGER PRIMARY KEY',
|
||||
'Path': 'TEXT',
|
||||
'Name': 'TEXT',
|
||||
'Tags': 'TEXT',
|
||||
'CheckState': 'INTEGER'}
|
||||
|
||||
if os.path.exists(self.database_path):
|
||||
self.check_columns()
|
||||
else:
|
||||
self.create_database()
|
||||
|
||||
def create_database(self):
|
||||
# TODO
|
||||
# Add separate columns for:
|
||||
# addition mode
|
||||
self.database.execute(
|
||||
"CREATE TABLE books \
|
||||
(id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, DateAdded BLOB, \
|
||||
Path TEXT, Position BLOB, ISBN TEXT, Tags TEXT, Hash TEXT, LastAccessed BLOB,\
|
||||
Bookmarks BLOB, CoverImage BLOB)")
|
||||
self.database = sqlite3.connect(self.database_path)
|
||||
|
||||
column_string = ', '.join(
|
||||
[i[0] + ' ' + i[1] for i in self.books_table_columns.items()])
|
||||
self.database.execute(f"CREATE TABLE books ({column_string})")
|
||||
|
||||
# CheckState is the standard QtCore.Qt.Checked / Unchecked
|
||||
self.database.execute(
|
||||
"CREATE TABLE directories (id INTEGER PRIMARY KEY, Path TEXT, \
|
||||
Name TEXT, Tags TEXT, CheckState INTEGER)")
|
||||
column_string = ', '.join(
|
||||
[i[0] + ' ' + i[1] for i in self.directories_table_columns.items()])
|
||||
self.database.execute(f"CREATE TABLE directories ({column_string})")
|
||||
|
||||
self.database.commit()
|
||||
self.database.close()
|
||||
|
||||
def check_columns(self):
|
||||
self.database = sqlite3.connect(self.database_path)
|
||||
|
||||
database_return = self.database.execute("PRAGMA table_info(books)").fetchall()
|
||||
database_columns = [i[1] for i in database_return]
|
||||
|
||||
# This allows for addition of a column without having to reform the database
|
||||
commit_required = False
|
||||
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]}"')
|
||||
sql_command = f"ALTER TABLE books ADD COLUMN {i[0]} {i[1]}"
|
||||
self.database.execute(sql_command)
|
||||
|
||||
if commit_required:
|
||||
self.database.commit()
|
||||
|
||||
|
||||
class DatabaseFunctions:
|
||||
def __init__(self, location_prefix):
|
||||
@@ -55,10 +95,6 @@ class DatabaseFunctions:
|
||||
self.database = sqlite3.connect(database_path)
|
||||
|
||||
def set_library_paths(self, data_iterable):
|
||||
# TODO
|
||||
# INSERT OR REPLACE is not working
|
||||
# So this is the old fashion kitchen sink approach
|
||||
|
||||
self.database.execute("DELETE FROM directories")
|
||||
|
||||
for i in data_iterable:
|
||||
@@ -67,10 +103,13 @@ class DatabaseFunctions:
|
||||
tags = i[2]
|
||||
is_checked = i[3]
|
||||
|
||||
if not os.path.exists(path):
|
||||
continue # Remove invalid paths from the database
|
||||
|
||||
sql_command = (
|
||||
"INSERT OR REPLACE INTO directories (ID, Path, Name, Tags, CheckState)\
|
||||
VALUES ((SELECT ID FROM directories WHERE Path = ?), ?, ?, ?, ?)")
|
||||
self.database.execute(sql_command, [path, path, name, tags, is_checked])
|
||||
"INSERT INTO directories (Path, Name, Tags, CheckState)\
|
||||
VALUES (?, ?, ?, ?)")
|
||||
self.database.execute(sql_command, [path, name, tags, is_checked])
|
||||
|
||||
self.database.commit()
|
||||
self.database.close()
|
||||
@@ -95,6 +134,7 @@ class DatabaseFunctions:
|
||||
path = i[1]['path']
|
||||
cover = i[1]['cover_image']
|
||||
isbn = i[1]['isbn']
|
||||
addition_mode = i[1]['addition_mode']
|
||||
tags = i[1]['tags']
|
||||
if tags:
|
||||
# Is a list. Needs to be a string
|
||||
@@ -105,8 +145,9 @@ class DatabaseFunctions:
|
||||
|
||||
sql_command_add = (
|
||||
"INSERT OR REPLACE INTO \
|
||||
books (Title, Author, Year, DateAdded, Path, ISBN, Tags, Hash, CoverImage) \
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
books (Title, Author, Year, DateAdded, Path, \
|
||||
ISBN, Tags, Hash, CoverImage, Addition) \
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
|
||||
cover_insert = None
|
||||
if cover:
|
||||
@@ -115,7 +156,8 @@ class DatabaseFunctions:
|
||||
self.database.execute(
|
||||
sql_command_add,
|
||||
[title, author, year, current_datetime_bin,
|
||||
path, isbn, tags, book_hash, cover_insert])
|
||||
path, isbn, tags, book_hash, cover_insert,
|
||||
addition_mode])
|
||||
|
||||
self.database.commit()
|
||||
self.database.close()
|
||||
@@ -177,7 +219,7 @@ class DatabaseFunctions:
|
||||
|
||||
def modify_metadata(self, metadata_dict, book_hash):
|
||||
def generate_binary(column, data):
|
||||
if column in ('Position', 'LastAccessed', 'Bookmarks'):
|
||||
if column in ('Position', 'LastAccessed', 'Bookmarks', 'Annotations'):
|
||||
return sqlite3.Binary(pickle.dumps(data))
|
||||
elif column == 'CoverImage':
|
||||
return sqlite3.Binary(data)
|
||||
@@ -208,9 +250,10 @@ class DatabaseFunctions:
|
||||
# target_data is an iterable
|
||||
|
||||
if column_name == '*':
|
||||
self.database.execute('DELETE FROM books')
|
||||
self.database.execute(
|
||||
"DELETE FROM books WHERE NOT Addition = 'manual'")
|
||||
else:
|
||||
sql_command = f'DELETE FROM books WHERE {column_name} = ?'
|
||||
sql_command = f"DELETE FROM books WHERE {column_name} = ?"
|
||||
for i in target_data:
|
||||
self.database.execute(sql_command, (i,))
|
||||
|
||||
|
@@ -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-2018 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,18 +14,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import requests
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia
|
||||
|
||||
from resources import definitions
|
||||
from lector.resources import definitions
|
||||
|
||||
|
||||
class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
def __init__(self, parent):
|
||||
super(DefinitionsUI, self).__init__()
|
||||
self.setupUi(self)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
self.parent = parent
|
||||
self.previous_position = None
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Popup |
|
||||
@@ -36,8 +38,14 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
radius = 15
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
|
||||
try:
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
except TypeError: # Required for older versions of Qt
|
||||
pass
|
||||
|
||||
self.definitionView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
self.app_id = 'bb7a91f9'
|
||||
self.app_key = 'fefacdf6775c347b52e9efa2efe642ef'
|
||||
@@ -49,21 +57,24 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
|
||||
self.okButton.clicked.connect(self.hide)
|
||||
self.pronounceButton.clicked.connect(self.play_pronunciation)
|
||||
self.dialogBackground.clicked.connect(self.color_background)
|
||||
|
||||
def api_call(self, url, word):
|
||||
language = self.parent.settings['dictionary_language']
|
||||
url = url + language + '/' + word.lower()
|
||||
|
||||
r = requests.get(
|
||||
url,
|
||||
headers={'app_id': self.app_id, 'app_key': self.app_key})
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header('app_id', self.app_id)
|
||||
req.add_header('app_key', self.app_key)
|
||||
|
||||
if r.status_code != 200:
|
||||
print('A firm nope on the dictionary finding thing')
|
||||
try:
|
||||
response = urllib.request.urlopen(req)
|
||||
if response.getcode() == 200:
|
||||
return_json = json.loads(response.read())
|
||||
return return_json
|
||||
except urllib.error.HTTPError:
|
||||
return None
|
||||
|
||||
return r.json()
|
||||
|
||||
def find_definition(self, word):
|
||||
word_root_json = self.api_call(self.root_url, word)
|
||||
if not word_root_json:
|
||||
@@ -75,6 +86,7 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
|
||||
definition_json = self.api_call(self.define_url, word_root)
|
||||
if not definition_json:
|
||||
self.set_text(word, None, None, True)
|
||||
return
|
||||
|
||||
definitions = {}
|
||||
@@ -109,8 +121,9 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
html_string += f'<h2><em><strong>{word}</strong></em></h2>\n'
|
||||
|
||||
if nothing_found:
|
||||
nope_string = self._translate('DefinitionsUI', 'No definitions found in')
|
||||
language = self.parent.settings['dictionary_language'].upper()
|
||||
html_string += f'<p><em>No definitions found in {language}<em></p>\n'
|
||||
html_string += f'<p><em>{nope_string} {language}<em></p>\n'
|
||||
else:
|
||||
# Word root
|
||||
html_string += f'<p><em>Word root: <em>{word_root}</p>\n'
|
||||
@@ -133,7 +146,8 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
background = self.parent.settings['dialog_background']
|
||||
else:
|
||||
self.previous_position = self.pos()
|
||||
background = self.parent.get_color()
|
||||
self.parent.get_color()
|
||||
background = self.parent.settings['dialog_background']
|
||||
|
||||
self.setStyleSheet(
|
||||
"QDialog {{background-color: {0}}}".format(background.name()))
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2018 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,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
from resources import pie_chart
|
||||
from lector.resources import pie_chart
|
||||
|
||||
|
||||
class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
@@ -34,11 +32,7 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
|
||||
option = option.__class__(option)
|
||||
file_exists = index.data(QtCore.Qt.UserRole + 5)
|
||||
metadata = index.data(QtCore.Qt.UserRole + 3)
|
||||
|
||||
position = metadata['position']
|
||||
if position:
|
||||
is_read = position['is_read']
|
||||
position_percent = index.data(QtCore.Qt.UserRole + 7)
|
||||
|
||||
# The shadow pixmap currently is set to 420 x 600
|
||||
# Only draw the cover shadow in case the setting is enabled
|
||||
@@ -55,36 +49,29 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||
if not file_exists:
|
||||
painter.setOpacity(.7)
|
||||
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
||||
read_icon = pie_chart.pixmapper(-1, None, None, 36)
|
||||
painter.setOpacity(1)
|
||||
read_icon = pie_chart.pixmapper(
|
||||
-1, None, self.parent.settings['consider_read_at'], 36)
|
||||
x_draw = option.rect.bottomRight().x() - 30
|
||||
y_draw = option.rect.bottomRight().y() - 35
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
painter.setOpacity(1)
|
||||
return
|
||||
|
||||
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
||||
if position:
|
||||
if is_read:
|
||||
current_chapter = total_chapters = 100
|
||||
else:
|
||||
try:
|
||||
current_chapter = position['current_chapter']
|
||||
total_chapters = position['total_chapters']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if position_percent:
|
||||
read_icon = pie_chart.pixmapper(
|
||||
current_chapter, total_chapters, self.temp_dir, 36)
|
||||
position_percent, self.temp_dir, self.parent.settings['consider_read_at'], 36)
|
||||
|
||||
x_draw = option.rect.bottomRight().x() - 30
|
||||
y_draw = option.rect.bottomRight().y() - 35
|
||||
if current_chapter != 1:
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
painter.drawPixmap(x_draw, y_draw, read_icon)
|
||||
|
||||
|
||||
class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, parent=None):
|
||||
super(BookmarkDelegate, self).__init__(parent)
|
||||
def __init__(self, main_window, parent=None):
|
||||
super(BookmarkDelegate, self).__init__()
|
||||
self.main_window = main_window
|
||||
self.parent = parent
|
||||
|
||||
def sizeHint(self, *args):
|
||||
@@ -98,7 +85,7 @@ class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
|
||||
option = option.__class__(option)
|
||||
|
||||
chapter_index = index.data(QtCore.Qt.UserRole)
|
||||
chapter_name = self.parent.window().bookToolBar.tocBox.itemText(chapter_index - 1)
|
||||
chapter_name = self.main_window.bookToolBar.tocBox.itemText(chapter_index - 1)
|
||||
if len(chapter_name) > 25:
|
||||
chapter_name = chapter_name[:25] + '...'
|
||||
|
||||
|
@@ -38,7 +38,7 @@ class EPUB:
|
||||
None, True)
|
||||
|
||||
if not contents_path:
|
||||
return False # No opf was found so processing cannot continue
|
||||
return False # No (valid) opf was found so processing cannot continue
|
||||
|
||||
self.generate_book_metadata(contents_path)
|
||||
self.parse_toc()
|
||||
@@ -76,13 +76,17 @@ class EPUB:
|
||||
|
||||
if xml:
|
||||
root_item = xml.find('rootfile')
|
||||
return root_item.get('full-path')
|
||||
else:
|
||||
possible_filenames = ('content.opf', 'package.opf')
|
||||
for i in possible_filenames:
|
||||
presumptive_location = self.get_file_path(i)
|
||||
if presumptive_location:
|
||||
return presumptive_location
|
||||
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):
|
||||
@@ -105,7 +109,8 @@ class EPUB:
|
||||
#______________________________________________________
|
||||
|
||||
def generate_book_metadata(self, contents_path):
|
||||
self.book['title'] = 'Unknown'
|
||||
self.book['title'] = os.path.splitext(
|
||||
os.path.basename(self.filename))[0]
|
||||
self.book['author'] = 'Unknown'
|
||||
self.book['isbn'] = None
|
||||
self.book['tags'] = None
|
||||
@@ -281,9 +286,11 @@ class EPUB:
|
||||
with open(cover_path, 'wb') as cover_temp:
|
||||
cover_temp.write(self.book['cover'])
|
||||
|
||||
self.book['book_list'][0] = (
|
||||
'Cover', f'<center><img src="{cover_path}" alt="Cover"></center>')
|
||||
|
||||
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]
|
||||
@@ -307,7 +314,8 @@ def get_split_content(chapter_data, split_by):
|
||||
|
||||
return_list.append(
|
||||
(chapter_titles[count - 1], bs_obj_string))
|
||||
xml_string = this_split[1]
|
||||
|
||||
xml_string = ''.join(this_split[1:])
|
||||
|
||||
bs_obj = BeautifulSoup(xml_string, 'lxml')
|
||||
bs_obj_string = str(bs_obj).replace('">', '', 1) + ('<br/>' * 8)
|
@@ -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-2018 BasioMeusPuga
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -16,8 +14,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from PyQt5 import QtGui
|
||||
from resources import resources
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from lector import database
|
||||
from lector.settings import Settings
|
||||
from lector.resources import resources
|
||||
|
||||
|
||||
class QImageFactory:
|
||||
@@ -30,3 +31,316 @@ class QImageFactory:
|
||||
|
||||
this_qicon = QtGui.QIcon(icon_path)
|
||||
return this_qicon
|
||||
|
||||
|
||||
# For nearly all cases below, code remains unchanged from its
|
||||
# state in the __main__ module. References to objects have been
|
||||
# made in the respective __init__ functions of the classes here
|
||||
class CoverLoadingAndCulling:
|
||||
def __init__(self, main_window):
|
||||
self.main_window = main_window
|
||||
self.lib_ref = self.main_window.lib_ref
|
||||
self.listView = self.main_window.listView
|
||||
self.database_path = self.main_window.database_path
|
||||
|
||||
def cull_covers(self, event=None):
|
||||
blank_pixmap = QtGui.QPixmap()
|
||||
blank_pixmap.load(':/images/blank.png') # Keep this. Removing it causes the
|
||||
# listView to go blank on a resize
|
||||
|
||||
all_indexes = set()
|
||||
for i in range(self.lib_ref.itemProxyModel.rowCount()):
|
||||
all_indexes.add(self.lib_ref.itemProxyModel.index(i, 0))
|
||||
|
||||
y_range = list(range(0, self.listView.viewport().height(), 100))
|
||||
y_range.extend((-20, self.listView.viewport().height() + 20))
|
||||
x_range = range(0, self.listView.viewport().width(), 80)
|
||||
|
||||
visible_indexes = set()
|
||||
for i in y_range:
|
||||
for j in x_range:
|
||||
this_index = self.listView.indexAt(QtCore.QPoint(j, i))
|
||||
visible_indexes.add(this_index)
|
||||
|
||||
invisible_indexes = all_indexes - visible_indexes
|
||||
for i in invisible_indexes:
|
||||
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
|
||||
this_item = self.lib_ref.libraryModel.item(model_index.row())
|
||||
|
||||
if this_item:
|
||||
this_item.setIcon(QtGui.QIcon(blank_pixmap))
|
||||
this_item.setData(False, QtCore.Qt.UserRole + 8)
|
||||
|
||||
hash_index_dict = {}
|
||||
hash_list = []
|
||||
for i in visible_indexes:
|
||||
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
|
||||
|
||||
book_hash = self.lib_ref.libraryModel.data(
|
||||
model_index, QtCore.Qt.UserRole + 6)
|
||||
cover_displayed = self.lib_ref.libraryModel.data(
|
||||
model_index, QtCore.Qt.UserRole + 8)
|
||||
|
||||
if book_hash and not cover_displayed:
|
||||
hash_list.append(book_hash)
|
||||
hash_index_dict[book_hash] = model_index
|
||||
|
||||
all_covers = database.DatabaseFunctions(
|
||||
self.database_path).fetch_covers_only(hash_list)
|
||||
|
||||
for i in all_covers:
|
||||
book_hash = i[0]
|
||||
cover = i[1]
|
||||
model_index = hash_index_dict[book_hash]
|
||||
|
||||
book_item = self.lib_ref.libraryModel.item(model_index.row())
|
||||
self.cover_loader(book_item, cover)
|
||||
|
||||
def load_all_covers(self):
|
||||
all_covers_db = database.DatabaseFunctions(
|
||||
self.database_path).fetch_data(
|
||||
('Hash', 'CoverImage',),
|
||||
'books',
|
||||
{'Hash': ''},
|
||||
'LIKE')
|
||||
|
||||
if not all_covers_db:
|
||||
return
|
||||
|
||||
all_covers = {
|
||||
i[0]: i[1] for i in all_covers_db}
|
||||
|
||||
for i in range(self.lib_ref.libraryModel.rowCount()):
|
||||
this_item = self.lib_ref.libraryModel.item(i, 0)
|
||||
|
||||
is_cover_already_displayed = this_item.data(QtCore.Qt.UserRole + 8)
|
||||
if is_cover_already_displayed:
|
||||
continue
|
||||
|
||||
book_hash = this_item.data(QtCore.Qt.UserRole + 6)
|
||||
cover = all_covers[book_hash]
|
||||
self.cover_loader(this_item, cover)
|
||||
|
||||
def cover_loader(self, item, cover):
|
||||
img_pixmap = QtGui.QPixmap()
|
||||
if cover:
|
||||
img_pixmap.loadFromData(cover)
|
||||
else:
|
||||
img_pixmap.load(':/images/NotFound.png')
|
||||
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
|
||||
item.setIcon(QtGui.QIcon(img_pixmap))
|
||||
item.setData(True, QtCore.Qt.UserRole + 8)
|
||||
|
||||
|
||||
class ViewProfileModification:
|
||||
def __init__(self, main_window):
|
||||
self.main_window = main_window
|
||||
|
||||
self.listView = self.main_window.listView
|
||||
self.settings = self.main_window.settings
|
||||
self.bookToolBar = self.main_window.bookToolBar
|
||||
self.comic_profile = self.main_window.comic_profile
|
||||
self.tabWidget = self.main_window.tabWidget
|
||||
self.alignment_dict = self.main_window.alignment_dict
|
||||
|
||||
def get_color(self, signal_sender):
|
||||
def open_color_dialog(current_color):
|
||||
color_dialog = QtWidgets.QColorDialog()
|
||||
new_color = color_dialog.getColor(current_color)
|
||||
if new_color.isValid(): # Returned in case cancel is pressed
|
||||
return new_color
|
||||
else:
|
||||
return current_color
|
||||
|
||||
# Special cases that don't affect (comic)book display
|
||||
if signal_sender == 'libraryBackground':
|
||||
current_color = self.settings['listview_background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.listView.setStyleSheet("QListView {{background-color: {0}}}".format(
|
||||
new_color.name()))
|
||||
self.settings['listview_background'] = new_color
|
||||
return
|
||||
|
||||
if signal_sender == 'dialogBackground':
|
||||
current_color = self.settings['dialog_background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.settings['dialog_background'] = new_color
|
||||
return
|
||||
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
# Retain current values on opening a new dialog
|
||||
if signal_sender == 'fgColor':
|
||||
current_color = current_profile['foreground']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.bookToolBar.colorBoxFG.setStyleSheet(
|
||||
'background-color: %s' % new_color.name())
|
||||
current_profile['foreground'] = new_color
|
||||
|
||||
elif signal_sender == 'bgColor':
|
||||
current_color = current_profile['background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.bookToolBar.colorBoxBG.setStyleSheet(
|
||||
'background-color: %s' % new_color.name())
|
||||
current_profile['background'] = new_color
|
||||
|
||||
elif signal_sender == 'comicBGColor':
|
||||
current_color = self.comic_profile['background']
|
||||
new_color = open_color_dialog(current_color)
|
||||
self.bookToolBar.comicBGColor.setStyleSheet(
|
||||
'background-color: %s' % new_color.name())
|
||||
self.comic_profile['background'] = new_color
|
||||
|
||||
self.bookToolBar.profileBox.setItemData(
|
||||
profile_index, current_profile, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
||||
def modify_font(self, signal_sender):
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
if signal_sender == 'fontBox':
|
||||
current_profile['font'] = self.bookToolBar.fontBox.currentFont().family()
|
||||
|
||||
if signal_sender == 'fontSizeBox':
|
||||
old_size = current_profile['font_size']
|
||||
new_size = self.bookToolBar.fontSizeBox.itemText(
|
||||
self.bookToolBar.fontSizeBox.currentIndex())
|
||||
if new_size.isdigit():
|
||||
current_profile['font_size'] = new_size
|
||||
else:
|
||||
current_profile['font_size'] = old_size
|
||||
|
||||
if signal_sender == 'lineSpacingUp' and current_profile['line_spacing'] < 200:
|
||||
current_profile['line_spacing'] += 5
|
||||
if signal_sender == 'lineSpacingDown' and current_profile['line_spacing'] > 90:
|
||||
current_profile['line_spacing'] -= 5
|
||||
|
||||
if signal_sender == 'paddingUp':
|
||||
current_profile['padding'] += 5
|
||||
if signal_sender == 'paddingDown':
|
||||
current_profile['padding'] -= 5
|
||||
|
||||
alignment_dict = {
|
||||
'alignLeft': 'left',
|
||||
'alignRight': 'right',
|
||||
'alignCenter': 'center',
|
||||
'alignJustify': 'justify'}
|
||||
if signal_sender in alignment_dict:
|
||||
current_profile['text_alignment'] = alignment_dict[signal_sender]
|
||||
|
||||
self.bookToolBar.profileBox.setItemData(
|
||||
profile_index, current_profile, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
||||
def modify_comic_view(self, signal_sender, key_pressed):
|
||||
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
|
||||
|
||||
self.bookToolBar.fitWidth.setChecked(False)
|
||||
self.bookToolBar.bestFit.setChecked(False)
|
||||
self.bookToolBar.originalSize.setChecked(False)
|
||||
|
||||
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
|
||||
self.comic_profile['zoom_mode'] = 'manualZoom'
|
||||
self.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 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
|
||||
|
||||
# This prevents infinite zoom in
|
||||
if self.comic_profile['padding'] < 0:
|
||||
self.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
|
||||
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'
|
||||
self.bookToolBar.bestFit.setChecked(True)
|
||||
|
||||
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
|
||||
self.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())
|
||||
|
||||
try:
|
||||
current_metadata = current_tab.metadata
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
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':
|
||||
self.bookToolBar.fitWidth.setChecked(True)
|
||||
if zoom_mode == 'bestFit':
|
||||
self.bookToolBar.bestFit.setChecked(True)
|
||||
if zoom_mode == 'originalSize':
|
||||
self.bookToolBar.originalSize.setChecked(True)
|
||||
|
||||
self.bookToolBar.comicBGColor.setStyleSheet(
|
||||
'background-color: %s' % background.name())
|
||||
|
||||
current_tab.format_view(
|
||||
None, None, None, background, padding, None, None)
|
||||
|
||||
else:
|
||||
profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile = self.bookToolBar.profileBox.itemData(
|
||||
profile_index, QtCore.Qt.UserRole)
|
||||
|
||||
font = current_profile['font']
|
||||
foreground = current_profile['foreground']
|
||||
background = current_profile['background']
|
||||
padding = current_profile['padding']
|
||||
font_size = current_profile['font_size']
|
||||
line_spacing = current_profile['line_spacing']
|
||||
text_alignment = current_profile['text_alignment']
|
||||
|
||||
# Change toolbar widgets to match new settings
|
||||
self.bookToolBar.fontBox.blockSignals(True)
|
||||
self.bookToolBar.fontSizeBox.blockSignals(True)
|
||||
self.bookToolBar.fontBox.setCurrentText(font)
|
||||
current_index = self.bookToolBar.fontSizeBox.findText(
|
||||
str(font_size), QtCore.Qt.MatchExactly)
|
||||
self.bookToolBar.fontSizeBox.setCurrentIndex(current_index)
|
||||
self.bookToolBar.fontBox.blockSignals(False)
|
||||
self.bookToolBar.fontSizeBox.blockSignals(False)
|
||||
|
||||
self.alignment_dict[current_profile['text_alignment']].setChecked(True)
|
||||
|
||||
self.bookToolBar.colorBoxFG.setStyleSheet(
|
||||
'background-color: %s' % foreground.name())
|
||||
self.bookToolBar.colorBoxBG.setStyleSheet(
|
||||
'background-color: %s' % background.name())
|
||||
|
||||
current_tab.format_view(
|
||||
font, font_size, foreground,
|
||||
background, padding, line_spacing,
|
||||
text_alignment)
|
||||
|
||||
def reset_profile(self):
|
||||
current_profile_index = self.bookToolBar.profileBox.currentIndex()
|
||||
current_profile_default = Settings(self).default_profiles[current_profile_index]
|
||||
self.bookToolBar.profileBox.setItemData(
|
||||
current_profile_index, current_profile_default, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2018 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 +17,7 @@
|
||||
import os
|
||||
import pickle
|
||||
import pathlib
|
||||
|
||||
from PyQt5 import QtGui, QtCore
|
||||
|
||||
from lector import database
|
||||
@@ -27,20 +26,22 @@ from lector.models import TableProxyModel, ItemProxyModel
|
||||
|
||||
class Library:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.view_model = None
|
||||
self.item_proxy_model = None
|
||||
self.table_proxy_model = None
|
||||
self.main_window = parent
|
||||
self.libraryModel = None
|
||||
self.itemProxyModel = None
|
||||
self.tableProxyModel = None
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
def generate_model(self, mode, parsed_books=None, is_database_ready=True):
|
||||
if mode == 'build':
|
||||
self.view_model = QtGui.QStandardItemModel()
|
||||
self.view_model.setColumnCount(10)
|
||||
self.libraryModel = QtGui.QStandardItemModel()
|
||||
self.libraryModel.setColumnCount(10)
|
||||
|
||||
books = database.DatabaseFunctions(
|
||||
self.parent.database_path).fetch_data(
|
||||
self.main_window.database_path).fetch_data(
|
||||
('Title', 'Author', 'Year', 'DateAdded', 'Path',
|
||||
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed'),
|
||||
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed',
|
||||
'Addition'),
|
||||
'books',
|
||||
{'Title': ''},
|
||||
'LIKE')
|
||||
@@ -50,7 +51,7 @@ class Library:
|
||||
return
|
||||
|
||||
elif mode == 'addition':
|
||||
# Assumes self.view_model already exists and may be extended
|
||||
# Assumes self.libraryModel already exists and may be extended
|
||||
# Because any additional books have already been added to the
|
||||
# database using background threads
|
||||
|
||||
@@ -63,7 +64,7 @@ class Library:
|
||||
|
||||
books.append([
|
||||
i[1]['title'], i[1]['author'], i[1]['year'], current_qdatetime,
|
||||
i[1]['path'], None, i[1]['isbn'], _tags, i[0], None])
|
||||
i[1]['path'], None, i[1]['isbn'], _tags, i[0], None, i[1]['addition_mode']])
|
||||
|
||||
else:
|
||||
return
|
||||
@@ -75,7 +76,11 @@ class Library:
|
||||
author = i[1]
|
||||
year = i[2]
|
||||
path = i[4]
|
||||
addition_mode = i[10]
|
||||
|
||||
last_accessed = i[9]
|
||||
if last_accessed and not isinstance(last_accessed, QtCore.QDateTime):
|
||||
last_accessed = pickle.loads(last_accessed)
|
||||
|
||||
tags = i[7]
|
||||
if isinstance(tags, list): # When files are added for the first time
|
||||
@@ -94,15 +99,22 @@ class Library:
|
||||
if position:
|
||||
position = pickle.loads(position)
|
||||
if position['is_read']:
|
||||
position_perc = 100
|
||||
position_perc = 1
|
||||
else:
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_chapter'] * 100 / position['total_chapters'])
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
position['current_block'] / position['total_blocks'])
|
||||
except (KeyError, ZeroDivisionError):
|
||||
try:
|
||||
position_perc = (
|
||||
position['current_chapter'] / position['total_chapters'])
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
|
||||
file_exists = os.path.exists(path)
|
||||
try:
|
||||
file_exists = os.path.exists(path)
|
||||
except UnicodeEncodeError:
|
||||
print('Library: Unicode encoding error')
|
||||
|
||||
all_metadata = {
|
||||
'title': title,
|
||||
@@ -115,9 +127,12 @@ class Library:
|
||||
'tags': tags,
|
||||
'hash': i[8],
|
||||
'last_accessed': last_accessed,
|
||||
'addition_mode': addition_mode,
|
||||
'file_exists': file_exists}
|
||||
|
||||
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year)
|
||||
author_string = self._translate('Library', 'Author')
|
||||
year_string = self._translate('Library', 'Year')
|
||||
tooltip_string = f'{title} \n{author_string}: {author} \n{year_string}: {str(year)}'
|
||||
|
||||
# Additional data can be set using an incrementing
|
||||
# QtCore.Qt.UserRole
|
||||
@@ -145,53 +160,61 @@ class Library:
|
||||
item.setData(False, QtCore.Qt.UserRole + 8) # Is the cover being displayed?
|
||||
item.setData(date_added, QtCore.Qt.UserRole + 9)
|
||||
item.setData(last_accessed, QtCore.Qt.UserRole + 12)
|
||||
item.setData(path, QtCore.Qt.UserRole + 13)
|
||||
item.setIcon(QtGui.QIcon(img_pixmap))
|
||||
|
||||
self.view_model.appendRow(item)
|
||||
self.libraryModel.appendRow(item)
|
||||
|
||||
# The is_database_ready boolean is required when a new thread sends
|
||||
# books here for model generation.
|
||||
if not self.parent.settings['perform_culling'] and is_database_ready:
|
||||
self.parent.load_all_covers()
|
||||
if not self.main_window.settings['perform_culling'] and is_database_ready:
|
||||
self.main_window.cover_functions.load_all_covers()
|
||||
|
||||
def generate_proxymodels(self):
|
||||
self.item_proxy_model = ItemProxyModel()
|
||||
self.item_proxy_model.setSourceModel(self.view_model)
|
||||
self.item_proxy_model.setSortCaseSensitivity(False)
|
||||
self.itemProxyModel = ItemProxyModel()
|
||||
self.itemProxyModel.setSourceModel(self.libraryModel)
|
||||
self.itemProxyModel.setSortCaseSensitivity(False)
|
||||
s = QtCore.QSize(160, 250) # Set icon sizing here
|
||||
self.parent.listView.setIconSize(s)
|
||||
self.parent.listView.setModel(self.item_proxy_model)
|
||||
self.main_window.listView.setIconSize(s)
|
||||
self.main_window.listView.setModel(self.itemProxyModel)
|
||||
|
||||
self.table_proxy_model = TableProxyModel(self.parent.temp_dir.path())
|
||||
self.table_proxy_model.setSourceModel(self.view_model)
|
||||
self.table_proxy_model.setSortCaseSensitivity(False)
|
||||
self.parent.tableView.setModel(self.table_proxy_model)
|
||||
self.tableProxyModel = TableProxyModel(
|
||||
self.main_window.temp_dir.path(),
|
||||
self.main_window.tableView.horizontalHeader(),
|
||||
self.main_window.settings['consider_read_at'])
|
||||
self.tableProxyModel.setSourceModel(self.libraryModel)
|
||||
self.tableProxyModel.setSortCaseSensitivity(False)
|
||||
self.main_window.tableView.setModel(self.tableProxyModel)
|
||||
|
||||
self.update_proxymodels()
|
||||
|
||||
def update_proxymodels(self):
|
||||
# Table proxy model
|
||||
self.table_proxy_model.invalidateFilter()
|
||||
self.table_proxy_model.setFilterParams(
|
||||
self.parent.libraryToolBar.searchBar.text(),
|
||||
self.parent.active_library_filters,
|
||||
self.tableProxyModel.invalidateFilter()
|
||||
self.tableProxyModel.setFilterParams(
|
||||
self.main_window.libraryToolBar.searchBar.text(),
|
||||
self.main_window.active_library_filters,
|
||||
0) # This doesn't need to know the sorting box position
|
||||
self.table_proxy_model.setFilterFixedString(
|
||||
self.parent.libraryToolBar.searchBar.text())
|
||||
self.tableProxyModel.setFilterFixedString(
|
||||
self.main_window.libraryToolBar.searchBar.text())
|
||||
# ^^^ This isn't needed, but it forces a model update every time the
|
||||
# text in the line edit changes. So I guess it is needed.
|
||||
self.tableProxyModel.sort_table_columns(
|
||||
self.main_window.tableView.horizontalHeader().sortIndicatorSection())
|
||||
self.tableProxyModel.sort_table_columns()
|
||||
|
||||
# Item proxy model
|
||||
self.item_proxy_model.invalidateFilter()
|
||||
self.item_proxy_model.setFilterParams(
|
||||
self.parent.libraryToolBar.searchBar.text(),
|
||||
self.parent.active_library_filters,
|
||||
self.parent.libraryToolBar.sortingBox.currentIndex())
|
||||
self.item_proxy_model.setFilterFixedString(
|
||||
self.parent.libraryToolBar.searchBar.text())
|
||||
self.itemProxyModel.invalidateFilter()
|
||||
self.itemProxyModel.setFilterParams(
|
||||
self.main_window.libraryToolBar.searchBar.text(),
|
||||
self.main_window.active_library_filters,
|
||||
self.main_window.libraryToolBar.sortingBox.currentIndex())
|
||||
self.itemProxyModel.setFilterFixedString(
|
||||
self.main_window.libraryToolBar.searchBar.text())
|
||||
|
||||
self.parent.statusMessage.setText(
|
||||
str(self.item_proxy_model.rowCount()) + ' books')
|
||||
self.main_window.statusMessage.setText(
|
||||
str(self.itemProxyModel.rowCount()) +
|
||||
self._translate('Library', ' books'))
|
||||
|
||||
# TODO
|
||||
# Allow sorting by type
|
||||
@@ -205,33 +228,46 @@ class Library:
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 9,
|
||||
4: 12}
|
||||
4: 12,
|
||||
5: 7}
|
||||
|
||||
# Sorting according to roles and the drop down in the library toolbar
|
||||
self.item_proxy_model.setSortRole(
|
||||
QtCore.Qt.UserRole + sort_roles[self.parent.libraryToolBar.sortingBox.currentIndex()])
|
||||
self.itemProxyModel.setSortRole(
|
||||
QtCore.Qt.UserRole +
|
||||
sort_roles[self.main_window.libraryToolBar.sortingBox.currentIndex()])
|
||||
|
||||
# This can be expanded to other fields by appending to the list
|
||||
sort_order = QtCore.Qt.AscendingOrder
|
||||
if self.parent.libraryToolBar.sortingBox.currentIndex() in [3, 4]:
|
||||
if self.main_window.libraryToolBar.sortingBox.currentIndex() in [3, 4, 5]:
|
||||
sort_order = QtCore.Qt.DescendingOrder
|
||||
|
||||
self.item_proxy_model.sort(0, sort_order)
|
||||
self.parent.start_culling_timer()
|
||||
self.itemProxyModel.sort(0, sort_order)
|
||||
self.main_window.start_culling_timer()
|
||||
|
||||
def generate_library_tags(self):
|
||||
db_library_directories = database.DatabaseFunctions(
|
||||
self.parent.database_path).fetch_data(
|
||||
self.main_window.database_path).fetch_data(
|
||||
('Path', 'Name', 'Tags'),
|
||||
'directories', # This checks the directories table NOT the book one
|
||||
{'Path': ''},
|
||||
'LIKE')
|
||||
|
||||
if not db_library_directories: # Empty database / table
|
||||
return
|
||||
if db_library_directories: # Empty database / table
|
||||
library_directories = {
|
||||
i[0]: (i[1], i[2]) for i in db_library_directories}
|
||||
|
||||
library_directories = {
|
||||
i[0]: (i[1], i[2]) for i in db_library_directories}
|
||||
else:
|
||||
db_library_directories = database.DatabaseFunctions(
|
||||
self.main_window.database_path).fetch_data(
|
||||
('Path',),
|
||||
'books', # THIS CHECKS THE BOOKS TABLE
|
||||
{'Path': ''},
|
||||
'LIKE')
|
||||
|
||||
library_directories = None
|
||||
if db_library_directories:
|
||||
library_directories = {
|
||||
i[0]: (None, None) for i in db_library_directories}
|
||||
|
||||
def get_tags(all_metadata):
|
||||
path = os.path.dirname(all_metadata['path'])
|
||||
@@ -243,7 +279,7 @@ class Library:
|
||||
if directory_name:
|
||||
directory_name = directory_name.lower()
|
||||
else:
|
||||
directory_name = path.rsplit('/')[-1].lower()
|
||||
directory_name = i.rsplit(os.sep)[-1].lower()
|
||||
|
||||
directory_tags = library_directories[i][1]
|
||||
if directory_tags:
|
||||
@@ -251,11 +287,15 @@ class Library:
|
||||
|
||||
return directory_name, directory_tags
|
||||
|
||||
return 'manually added', None
|
||||
# A file is assigned a 'manually added' tag in case it isn't
|
||||
# in any designated library directory
|
||||
added_string = self._translate('Library', 'manually added')
|
||||
return added_string.lower(), None
|
||||
|
||||
# Generate tags for the QStandardItemModel
|
||||
for i in range(self.view_model.rowCount()):
|
||||
this_item = self.view_model.item(i, 0)
|
||||
# This isn't triggered for an empty view model
|
||||
for i in range(self.libraryModel.rowCount()):
|
||||
this_item = self.libraryModel.item(i, 0)
|
||||
all_metadata = this_item.data(QtCore.Qt.UserRole + 3)
|
||||
directory_name, directory_tags = get_tags(all_metadata)
|
||||
|
||||
@@ -267,30 +307,31 @@ class Library:
|
||||
# All files in unselected directories will have to be removed
|
||||
# from both of the models
|
||||
# They will also have to be deleted from the library
|
||||
valid_paths = set(valid_paths)
|
||||
invalid_paths = []
|
||||
deletable_persistent_indexes = []
|
||||
|
||||
for i in range(self.libraryModel.rowCount()):
|
||||
item = self.libraryModel.item(i)
|
||||
|
||||
# Get all paths
|
||||
all_paths = set()
|
||||
for i in range(self.view_model.rowCount()):
|
||||
item = self.view_model.item(i, 0)
|
||||
item_metadata = item.data(QtCore.Qt.UserRole + 3)
|
||||
book_path = item_metadata['path']
|
||||
all_paths.add(book_path)
|
||||
try:
|
||||
addition_mode = item_metadata['addition_mode']
|
||||
except KeyError:
|
||||
addition_mode = 'automatic'
|
||||
print('Libary: Error setting addition mode for prune')
|
||||
|
||||
invalid_paths = all_paths - valid_paths
|
||||
if (book_path not in valid_paths and
|
||||
(addition_mode != 'manual' or addition_mode is None)):
|
||||
|
||||
deletable_persistent_indexes = []
|
||||
for i in range(self.view_model.rowCount()):
|
||||
item = self.view_model.item(i)
|
||||
path = item.data(QtCore.Qt.UserRole + 3)['path']
|
||||
if path in invalid_paths:
|
||||
invalid_paths.append(book_path)
|
||||
deletable_persistent_indexes.append(
|
||||
QtCore.QPersistentModelIndex(item.index()))
|
||||
|
||||
if deletable_persistent_indexes:
|
||||
for i in deletable_persistent_indexes:
|
||||
self.view_model.removeRow(i.row())
|
||||
self.libraryModel.removeRow(i.row())
|
||||
|
||||
# Remove invalid paths from the database as well
|
||||
database.DatabaseFunctions(
|
||||
self.parent.database_path).delete_from_database('Path', invalid_paths)
|
||||
self.main_window.database_path).delete_from_database('Path', invalid_paths)
|
||||
|
@@ -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-2018 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,15 +17,15 @@
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from lector import database
|
||||
|
||||
from resources import metadata
|
||||
from lector.widgets import PliantQGraphicsScene
|
||||
from lector.resources import metadata
|
||||
|
||||
|
||||
class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
def __init__(self, parent):
|
||||
super(MetadataUI, self).__init__()
|
||||
self.setupUi(self)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Popup |
|
||||
@@ -38,8 +36,12 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
radius = 15
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
|
||||
try:
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
except TypeError: # Required for older versions of Qt
|
||||
pass
|
||||
|
||||
self.parent = parent
|
||||
self.database_path = self.parent.database_path
|
||||
@@ -85,8 +87,8 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
graphics_scene.addPixmap(image_pixmap)
|
||||
self.coverView.setScene(graphics_scene)
|
||||
|
||||
def ok_pressed(self, event):
|
||||
book_item = self.parent.lib_ref.view_model.item(self.book_index.row())
|
||||
def ok_pressed(self, event=None):
|
||||
book_item = self.parent.lib_ref.libraryModel.item(self.book_index.row())
|
||||
|
||||
title = self.titleLine.text()
|
||||
author = self.authorLine.text()
|
||||
@@ -97,7 +99,9 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
except ValueError:
|
||||
year = self.book_year
|
||||
|
||||
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year)
|
||||
author_string = self._translate('MetadataUI', 'Author')
|
||||
year_string = self._translate('MetadataUI', 'Year')
|
||||
tooltip_string = f'{title} \n{author_string}: {author} \n{year_string}: {str(year)}'
|
||||
|
||||
book_item.setData(title, QtCore.Qt.UserRole)
|
||||
book_item.setData(author, QtCore.Qt.UserRole + 1)
|
||||
@@ -114,7 +118,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
|
||||
if self.cover_for_database:
|
||||
database_dict['CoverImage'] = self.cover_for_database
|
||||
self.parent.cover_loader(
|
||||
self.parent.cover_functions.cover_loader(
|
||||
book_item, self.cover_for_database)
|
||||
|
||||
self.parent.lib_ref.update_proxymodels()
|
||||
@@ -123,7 +127,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
database.DatabaseFunctions(self.database_path).modify_metadata(
|
||||
database_dict, book_hash)
|
||||
|
||||
def cancel_pressed(self, event):
|
||||
def cancel_pressed(self, event=None):
|
||||
self.hide()
|
||||
|
||||
def generate_display_position(self, mouse_cursor_position):
|
||||
@@ -146,7 +150,8 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
background = self.parent.settings['dialog_background']
|
||||
else:
|
||||
self.previous_position = self.pos()
|
||||
background = self.parent.get_color()
|
||||
self.parent.get_color()
|
||||
background = self.parent.settings['dialog_background']
|
||||
|
||||
self.setStyleSheet(
|
||||
"QDialog {{background-color: {0}}}".format(background.name()))
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is a part of Lector, a Qt based ebook reader
|
||||
# Copyright (C) 2017 BasioMeusPuga
|
||||
# Copyright (C) 2017-2018 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,18 +14,17 @@
|
||||
# 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 pickle
|
||||
import pathlib
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from resources import pie_chart
|
||||
from lector.resources import pie_chart
|
||||
|
||||
|
||||
class BookmarkProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
super(BookmarkProxyModel, self).__init__(parent)
|
||||
self.parent = parent
|
||||
self.filter_string = None
|
||||
self.filter_text = None
|
||||
|
||||
def setFilterParams(self, filter_text):
|
||||
self.filter_text = filter_text
|
||||
@@ -66,10 +63,21 @@ class ItemProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
|
||||
class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, temp_dir, parent=None):
|
||||
def __init__(self, temp_dir, tableViewHeader, consider_read_at, parent=None):
|
||||
super(TableProxyModel, self).__init__(parent)
|
||||
self.tableViewHeader = tableViewHeader
|
||||
self.consider_read_at = consider_read_at
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
title_string = self._translate('TableProxyModel', 'Title')
|
||||
author_string = self._translate('TableProxyModel', 'Author')
|
||||
year_string = self._translate('TableProxyModel', 'Year')
|
||||
lastread_string = self._translate('TableProxyModel', 'Last Read')
|
||||
tags_string = self._translate('TableProxyModel', 'Tags')
|
||||
self.header_data = [
|
||||
None, 'Title', 'Author', 'Year', 'Last Read', '%', 'Tags']
|
||||
None, title_string, author_string,
|
||||
year_string, lastread_string, '%', tags_string]
|
||||
|
||||
self.temp_dir = temp_dir
|
||||
self.filter_text = None
|
||||
self.active_library_filters = None
|
||||
@@ -88,7 +96,12 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
def headerData(self, column, orientation, role):
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
return self.header_data[column]
|
||||
try:
|
||||
return self.header_data[column]
|
||||
except IndexError:
|
||||
print('Table proxy model: Can\'t find header for column', column)
|
||||
# The column will be called IndexError. Not a typo.
|
||||
return 'IndexError'
|
||||
|
||||
def flags(self, index):
|
||||
# Tag editing will take place by way of a right click menu
|
||||
@@ -108,46 +121,27 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
return_pixmap = None
|
||||
|
||||
file_exists = item.data(QtCore.Qt.UserRole + 5)
|
||||
metadata = item.data(QtCore.Qt.UserRole + 3)
|
||||
position = metadata['position']
|
||||
if position:
|
||||
is_read = position['is_read']
|
||||
position_percent = item.data(QtCore.Qt.UserRole + 7)
|
||||
|
||||
if not file_exists:
|
||||
return pie_chart.pixmapper(
|
||||
-1, None, None, QtCore.Qt.SizeHintRole + 10)
|
||||
|
||||
if position:
|
||||
if is_read:
|
||||
current_chapter = total_chapters = 100
|
||||
else:
|
||||
try:
|
||||
current_chapter = position['current_chapter']
|
||||
total_chapters = position['total_chapters']
|
||||
|
||||
# TODO
|
||||
# See if there's any rationale for this
|
||||
if current_chapter == 1:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if position_percent:
|
||||
return_pixmap = pie_chart.pixmapper(
|
||||
current_chapter, total_chapters, self.temp_dir,
|
||||
position_percent, self.temp_dir,
|
||||
self.consider_read_at,
|
||||
QtCore.Qt.SizeHintRole + 10)
|
||||
|
||||
return return_pixmap
|
||||
|
||||
elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
if index.column() in (0, 5): # Cover and Status
|
||||
if index.column() in (0, 5): # Cover and Status
|
||||
return QtCore.QVariant()
|
||||
|
||||
if index.column() == 4:
|
||||
last_accessed_time = item.data(self.role_dictionary[index.column()])
|
||||
if last_accessed_time:
|
||||
last_accessed = last_accessed_time
|
||||
if not isinstance(last_accessed_time, QtCore.QDateTime):
|
||||
last_accessed = pickle.loads(last_accessed_time)
|
||||
last_accessed = item.data(self.role_dictionary[index.column()])
|
||||
if last_accessed:
|
||||
right_now = QtCore.QDateTime().currentDateTime()
|
||||
time_diff = last_accessed.msecsTo(right_now)
|
||||
return self.time_convert(time_diff // 1000)
|
||||
@@ -164,10 +158,13 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
output = self.common_functions.filterAcceptsRow(row, parent)
|
||||
return output
|
||||
|
||||
def sort_table_columns(self, column):
|
||||
sorting_order = self.sender().sortIndicatorOrder()
|
||||
def sort_table_columns(self, column=None):
|
||||
column = self.tableViewHeader.sortIndicatorSection()
|
||||
sorting_order = self.tableViewHeader.sortIndicatorOrder()
|
||||
|
||||
self.sort(0, sorting_order)
|
||||
self.setSortRole(self.role_dictionary[column])
|
||||
if column != 0:
|
||||
self.setSortRole(self.role_dictionary[column])
|
||||
|
||||
def time_convert(self, seconds):
|
||||
seconds = int(seconds)
|
||||
@@ -201,14 +198,20 @@ class ProxyModelsCommonFunctions:
|
||||
title = model.data(this_index, QtCore.Qt.UserRole)
|
||||
author = model.data(this_index, QtCore.Qt.UserRole + 1)
|
||||
tags = model.data(this_index, QtCore.Qt.UserRole + 4)
|
||||
progress = model.data(this_index, QtCore.Qt.UserRole + 7)
|
||||
directory_name = model.data(this_index, QtCore.Qt.UserRole + 10)
|
||||
directory_tags = model.data(this_index, QtCore.Qt.UserRole + 11)
|
||||
last_accessed = model.data(this_index, QtCore.Qt.UserRole + 12)
|
||||
file_path = model.data(this_index, QtCore.Qt.UserRole + 13)
|
||||
|
||||
# Hide untouched files when sorting by last accessed
|
||||
if self.parent_model.sorting_box_position == 4 and not last_accessed:
|
||||
return False
|
||||
|
||||
# Hide untouched files when sorting by progress
|
||||
if self.parent_model.sorting_box_position == 5 and not progress:
|
||||
return False
|
||||
|
||||
if self.parent_model.active_library_filters:
|
||||
if directory_name not in self.parent_model.active_library_filters:
|
||||
return False
|
||||
@@ -220,7 +223,9 @@ class ProxyModelsCommonFunctions:
|
||||
else:
|
||||
valid_data = [
|
||||
i.lower() for i in (
|
||||
title, author, tags, directory_name, directory_tags) if i is not None]
|
||||
title, author, tags, directory_name,
|
||||
directory_tags, file_path)
|
||||
if i is not None]
|
||||
for i in valid_data:
|
||||
if self.parent_model.filter_text.lower() in i:
|
||||
return True
|
||||
|
@@ -22,7 +22,8 @@
|
||||
import os
|
||||
import time
|
||||
import zipfile
|
||||
from rarfile import rarfile
|
||||
|
||||
from lector.rarfile import rarfile
|
||||
|
||||
|
||||
class ParseCOMIC:
|
||||
@@ -49,10 +50,11 @@ class ParseCOMIC:
|
||||
return
|
||||
|
||||
def get_title(self):
|
||||
return self.book_extension[0]
|
||||
title = os.path.basename(self.book_extension[0]).strip(' ')
|
||||
return title
|
||||
|
||||
def get_author(self):
|
||||
return None
|
||||
return 'Unknown'
|
||||
|
||||
def get_year(self):
|
||||
creation_time = time.ctime(os.path.getctime(self.filename))
|
@@ -19,7 +19,7 @@
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
from ePub.read_epub import EPUB
|
||||
from lector.ePub.read_epub import EPUB
|
||||
|
||||
|
||||
class ParseEPUB:
|
@@ -24,8 +24,8 @@ import sys
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
from ePub.read_epub import EPUB
|
||||
import KindleUnpack.kindleunpack as KindleUnpack
|
||||
from lector.ePub.read_epub import EPUB
|
||||
import lector.KindleUnpack.kindleunpack as KindleUnpack
|
||||
|
||||
|
||||
class ParseMOBI:
|
@@ -17,15 +17,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import io
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
proceed = True
|
||||
try:
|
||||
import popplerqt5
|
||||
except ImportError:
|
||||
print('python-poppler-qt5 is not installed. Pdf files will not work.')
|
||||
proceed = False
|
||||
import popplerqt5
|
||||
|
||||
|
||||
class ParsePDF:
|
||||
def __init__(self, filename, *args):
|
||||
@@ -34,9 +32,6 @@ class ParsePDF:
|
||||
self.metadata = None
|
||||
|
||||
def read_book(self):
|
||||
if not proceed:
|
||||
return
|
||||
|
||||
self.book = popplerqt5.Poppler.Document.load(self.filename)
|
||||
if not self.book:
|
||||
return
|
||||
@@ -48,7 +43,7 @@ class ParsePDF:
|
||||
title = self.metadata.find('title').text
|
||||
return title.replace('\n', '')
|
||||
except AttributeError:
|
||||
return 'Unknown'
|
||||
return os.path.splitext(os.path.basename(self.filename))[0]
|
||||
|
||||
def get_author(self):
|
||||
try:
|
||||
@@ -60,8 +55,8 @@ class ParsePDF:
|
||||
def get_year(self):
|
||||
try:
|
||||
year = self.metadata.find('MetadataDate').text
|
||||
return year.replace('\n', '')
|
||||
except AttributeError:
|
||||
return int(year.replace('\n', '')[:4])
|
||||
except (AttributeError, ValueError):
|
||||
return 9999
|
||||
|
||||
def get_cover_image(self):
|
||||
@@ -69,9 +64,12 @@ class ParsePDF:
|
||||
popplerqt5.Poppler.Document.Antialiasing
|
||||
and popplerqt5.Poppler.Document.TextAntialiasing)
|
||||
|
||||
cover_page = self.book.page(0)
|
||||
cover_image = cover_page.renderToImage(300, 300)
|
||||
return resize_image(cover_image)
|
||||
try:
|
||||
cover_page = self.book.page(0)
|
||||
cover_image = cover_page.renderToImage(300, 300)
|
||||
return resize_image(cover_image)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_isbn(self):
|
||||
return None
|
146
lector/resources/annotationswindow.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'raw/annotations.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.10.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
Dialog.setObjectName("Dialog")
|
||||
Dialog.resize(306, 387)
|
||||
self.gridLayout = QtWidgets.QGridLayout(Dialog)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
self.nameEdit = QtWidgets.QLineEdit(Dialog)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.nameEdit.sizePolicy().hasHeightForWidth())
|
||||
self.nameEdit.setSizePolicy(sizePolicy)
|
||||
self.nameEdit.setObjectName("nameEdit")
|
||||
self.horizontalLayout_2.addWidget(self.nameEdit)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_2)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.typeLabel = QtWidgets.QLabel(Dialog)
|
||||
self.typeLabel.setObjectName("typeLabel")
|
||||
self.horizontalLayout.addWidget(self.typeLabel)
|
||||
self.typeBox = QtWidgets.QComboBox(Dialog)
|
||||
self.typeBox.setObjectName("typeBox")
|
||||
self.horizontalLayout.addWidget(self.typeBox)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.stackedWidget = QtWidgets.QStackedWidget(Dialog)
|
||||
self.stackedWidget.setObjectName("stackedWidget")
|
||||
self.page = QtWidgets.QWidget()
|
||||
self.page.setObjectName("page")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.page)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.verticalLayout_12 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_12.setObjectName("verticalLayout_12")
|
||||
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
|
||||
self.foregroundCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.foregroundCheck.setObjectName("foregroundCheck")
|
||||
self.horizontalLayout_15.addWidget(self.foregroundCheck)
|
||||
self.foregroundColorButton = QtWidgets.QPushButton(self.page)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.foregroundColorButton.sizePolicy().hasHeightForWidth())
|
||||
self.foregroundColorButton.setSizePolicy(sizePolicy)
|
||||
self.foregroundColorButton.setMinimumSize(QtCore.QSize(30, 0))
|
||||
self.foregroundColorButton.setMaximumSize(QtCore.QSize(45, 40))
|
||||
self.foregroundColorButton.setText("")
|
||||
self.foregroundColorButton.setObjectName("foregroundColorButton")
|
||||
self.horizontalLayout_15.addWidget(self.foregroundColorButton)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_15)
|
||||
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
|
||||
self.highlightCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.highlightCheck.setObjectName("highlightCheck")
|
||||
self.horizontalLayout_16.addWidget(self.highlightCheck)
|
||||
self.highlightColorButton = QtWidgets.QPushButton(self.page)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.highlightColorButton.sizePolicy().hasHeightForWidth())
|
||||
self.highlightColorButton.setSizePolicy(sizePolicy)
|
||||
self.highlightColorButton.setMinimumSize(QtCore.QSize(30, 24))
|
||||
self.highlightColorButton.setMaximumSize(QtCore.QSize(45, 40))
|
||||
self.highlightColorButton.setText("")
|
||||
self.highlightColorButton.setObjectName("highlightColorButton")
|
||||
self.horizontalLayout_16.addWidget(self.highlightColorButton)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_16)
|
||||
self.horizontalLayout_17 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_17.setObjectName("horizontalLayout_17")
|
||||
self.boldCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.boldCheck.setObjectName("boldCheck")
|
||||
self.horizontalLayout_17.addWidget(self.boldCheck)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_17)
|
||||
self.horizontalLayout_18 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_18.setObjectName("horizontalLayout_18")
|
||||
self.italicCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.italicCheck.setObjectName("italicCheck")
|
||||
self.horizontalLayout_18.addWidget(self.italicCheck)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_18)
|
||||
self.horizontalLayout_19 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_19.setObjectName("horizontalLayout_19")
|
||||
self.underlineCheck = QtWidgets.QCheckBox(self.page)
|
||||
self.underlineCheck.setObjectName("underlineCheck")
|
||||
self.horizontalLayout_19.addWidget(self.underlineCheck)
|
||||
self.underlineType = QtWidgets.QComboBox(self.page)
|
||||
self.underlineType.setObjectName("underlineType")
|
||||
self.horizontalLayout_19.addWidget(self.underlineType)
|
||||
self.underlineColorButton = QtWidgets.QPushButton(self.page)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.underlineColorButton.sizePolicy().hasHeightForWidth())
|
||||
self.underlineColorButton.setSizePolicy(sizePolicy)
|
||||
self.underlineColorButton.setMinimumSize(QtCore.QSize(45, 24))
|
||||
self.underlineColorButton.setMaximumSize(QtCore.QSize(45, 40))
|
||||
self.underlineColorButton.setText("")
|
||||
self.underlineColorButton.setObjectName("underlineColorButton")
|
||||
self.horizontalLayout_19.addWidget(self.underlineColorButton)
|
||||
self.verticalLayout_12.addLayout(self.horizontalLayout_19)
|
||||
self.gridLayout_2.addLayout(self.verticalLayout_12, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.page)
|
||||
self.verticalLayout.addWidget(self.stackedWidget)
|
||||
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
|
||||
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_3.addItem(spacerItem)
|
||||
self.okButton = QtWidgets.QPushButton(Dialog)
|
||||
self.okButton.setObjectName("okButton")
|
||||
self.horizontalLayout_3.addWidget(self.okButton)
|
||||
self.cancelButton = QtWidgets.QPushButton(Dialog)
|
||||
self.cancelButton.setObjectName("cancelButton")
|
||||
self.horizontalLayout_3.addWidget(self.cancelButton)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_3.addItem(spacerItem1)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_3, 1, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
QtCore.QMetaObject.connectSlotsByName(Dialog)
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Dialog.setWindowTitle(_translate("Dialog", "Annotation Editor"))
|
||||
self.nameEdit.setPlaceholderText(_translate("Dialog", "Annotation Name"))
|
||||
self.typeLabel.setText(_translate("Dialog", "Type"))
|
||||
self.foregroundCheck.setText(_translate("Dialog", "Foreground"))
|
||||
self.highlightCheck.setText(_translate("Dialog", "Highlight"))
|
||||
self.boldCheck.setText(_translate("Dialog", "Bold"))
|
||||
self.italicCheck.setText(_translate("Dialog", "Italic"))
|
||||
self.underlineCheck.setText(_translate("Dialog", "Underline"))
|
||||
self.okButton.setText(_translate("Dialog", "OK"))
|
||||
self.cancelButton.setText(_translate("Dialog", "Cancel"))
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'raw/main.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.9.2
|
||||
# Created by: PyQt5 UI code generator 5.10.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -35,20 +35,6 @@ class Ui_MainWindow(object):
|
||||
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_4.setSpacing(0)
|
||||
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||
self.listView = QtWidgets.QListView(self.listPage)
|
||||
self.listView.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
self.listView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.listView.setProperty("showDropIndicator", False)
|
||||
self.listView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.listView.setMovement(QtWidgets.QListView.Static)
|
||||
self.listView.setProperty("isWrapping", True)
|
||||
self.listView.setResizeMode(QtWidgets.QListView.Fixed)
|
||||
self.listView.setLayoutMode(QtWidgets.QListView.SinglePass)
|
||||
self.listView.setViewMode(QtWidgets.QListView.IconMode)
|
||||
self.listView.setUniformItemSizes(True)
|
||||
self.listView.setWordWrap(True)
|
||||
self.listView.setObjectName("listView")
|
||||
self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.listPage)
|
||||
self.tablePage = QtWidgets.QWidget()
|
||||
self.tablePage.setObjectName("tablePage")
|
||||
@@ -56,20 +42,6 @@ class Ui_MainWindow(object):
|
||||
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_3.setSpacing(0)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.tableView = QtWidgets.QTableView(self.tablePage)
|
||||
self.tableView.setFrameShape(QtWidgets.QFrame.Box)
|
||||
self.tableView.setFrameShadow(QtWidgets.QFrame.Plain)
|
||||
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
|
||||
self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
|
||||
self.tableView.setAlternatingRowColors(True)
|
||||
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.tableView.setGridStyle(QtCore.Qt.NoPen)
|
||||
self.tableView.setSortingEnabled(True)
|
||||
self.tableView.setWordWrap(False)
|
||||
self.tableView.setObjectName("tableView")
|
||||
self.tableView.horizontalHeader().setVisible(True)
|
||||
self.tableView.verticalHeader().setVisible(False)
|
||||
self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1)
|
||||
self.stackedWidget.addWidget(self.tablePage)
|
||||
self.gridLayout_2.addWidget(self.stackedWidget, 0, 0, 1, 1)
|
||||
self.tabWidget.addTab(self.tab, "")
|
@@ -94,26 +94,26 @@ def generate_pie(progress_percent, temp_dir=None):
|
||||
return lSvg
|
||||
|
||||
|
||||
def pixmapper(current_chapter, total_chapters, temp_dir, size):
|
||||
def pixmapper(position_percent, temp_dir, consider_read_at, size):
|
||||
# A current_chapter of -1 implies the files does not exist
|
||||
# A chapter number == Total chapters implies the file is unread
|
||||
return_pixmap = None
|
||||
# position_percent and consider_read_at are expected as a <1 decimal value
|
||||
|
||||
if current_chapter == -1:
|
||||
return_pixmap = None
|
||||
consider_read_at = consider_read_at / 100
|
||||
|
||||
if position_percent == -1:
|
||||
return_pixmap = QtGui.QIcon(':/images/error.svg').pixmap(size)
|
||||
return return_pixmap
|
||||
|
||||
if current_chapter == total_chapters:
|
||||
if position_percent >= consider_read_at: # Consider book read @ this progress
|
||||
return_pixmap = QtGui.QIcon(':/images/checkmark.svg').pixmap(size)
|
||||
else:
|
||||
|
||||
# TODO
|
||||
# See if saving the svg to disk can be avoided
|
||||
# Shift to lines to track progress
|
||||
# Maybe make the alignment a little more uniform across emblems
|
||||
|
||||
progress_percent = int(current_chapter * 100 / total_chapters)
|
||||
generate_pie(progress_percent, temp_dir)
|
||||
generate_pie(int(position_percent * 100), temp_dir)
|
||||
svg_path = os.path.join(temp_dir, 'lector_progress.svg')
|
||||
return_pixmap = QtGui.QIcon(svg_path).pixmap(size - 4) ## The -4 looks more proportional
|
||||
|
8
lector/resources/raw/DarkIcons/about.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 815 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
8
lector/resources/raw/DarkIcons/annotate.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 617 B |
8
lector/resources/raw/DarkIcons/arrow-down.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 372 B |
8
lector/resources/raw/DarkIcons/arrow-up.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 373 B |
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 703 B |
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 546 B |
Before Width: | Height: | Size: 891 B After Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 729 B After Width: | Height: | Size: 729 B |
8
lector/resources/raw/DarkIcons/filesaveas.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 5.9980469 1.0195312 L 5.9980469 7.0195312 L 3.6582031 7.0195312 L 7.9902344 13.324219 L 12.371094 7.0195312 L 9.9980469 7.0195312 L 9.9980469 1.0488281 L 5.9980469 1.0195312 z M 1 14.03125 L 1 16 L 15.005859 16 L 15 14.03125 L 1 14.03125 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 561 B |
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 601 B |
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 561 B |
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 484 B |
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 484 B |
Before Width: | Height: | Size: 694 B After Width: | Height: | Size: 694 B |
Before Width: | Height: | Size: 642 B After Width: | Height: | Size: 642 B |
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 859 B |
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 378 B |
8
lector/resources/raw/DarkIcons/search.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 6.4902344 0.99609375 C 3.4613344 0.99609375 0.99023438 3.4706937 0.99023438 6.4960938 C 0.99023438 9.5214938 3.4613344 11.996094 6.4902344 11.996094 C 7.6422344 11.996094 8.7279444 11.638254 9.6152344 11.027344 L 13.302734 14.714844 A 1.0055 1.0055 0 1 0 14.708984 13.277344 L 11.021484 9.5898438 C 11.632274 8.7038438 12.021484 7.6459938 12.021484 6.4960938 C 12.021484 3.4706937 9.5190344 0.99609375 6.4902344 0.99609375 z M 6.4902344 2.9960938 C 8.4376344 2.9960938 9.9902344 4.5508938 9.9902344 6.4960938 C 9.9902344 8.4411937 8.4376344 9.9960938 6.4902344 9.9960938 C 4.5428344 9.9960938 2.9902344 8.4411937 2.9902344 6.4960938 C 2.9902344 4.5508938 4.5428344 2.9960938 6.4902344 2.9960938 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
8
lector/resources/raw/DarkIcons/switches.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 0 C 0.892 0 0 0.892 0 2 L 0 14 C 0 15.108 0.892 16 2 16 L 14 16 C 15.108 16 16 15.108 16 14 L 16 2 C 16 0.892 15.108 0 14 0 L 2 0 z M 3.7148438 2 L 12.285156 2 C 13.235156 2 14 2.7651437 14 3.7148438 L 14 12.285156 C 14 13.235156 13.235156 14 12.285156 14 L 3.7148438 14 C 2.7651438 14 2 13.235156 2 12.285156 L 2 3.7148438 C 2 2.7651438 2.7651437 2 3.7148438 2 z M 6.7402344 3 L 6.6289062 4.3164062 A 3.964 3.9286 0 0 0 5.4707031 4.9804688 L 4.2617188 4.4179688 L 3.0019531 6.5820312 L 4.0976562 7.3378906 A 3.964 3.9286 0 0 0 4.0371094 8 A 3.964 3.9286 0 0 0 4.0957031 8.6660156 L 3.0019531 9.4179688 L 4.2617188 11.582031 L 5.4667969 11.019531 A 3.964 3.9286 0 0 0 6.6289062 11.679688 L 6.7402344 13 L 9.2617188 13 L 9.3730469 11.683594 A 3.964 3.9286 0 0 0 10.53125 11.019531 L 11.740234 11.582031 L 13.001953 9.4179688 L 11.904297 8.6621094 A 3.964 3.9286 0 0 0 11.964844 8 A 3.964 3.9286 0 0 0 11.908203 7.3339844 L 13.001953 6.5820312 L 11.740234 4.4179688 L 10.535156 4.9804688 A 3.964 3.9286 0 0 0 9.3730469 4.3203125 L 9.2617188 3 L 6.7402344 3 z M 8.0019531 6.5722656 A 1.4414 1.4286 0 0 1 9.4433594 8 A 1.4414 1.4286 0 0 1 8.0019531 9.4277344 A 1.4414 1.4286 0 0 1 6.5605469 8 A 1.4414 1.4286 0 0 1 8.0019531 6.5722656 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 501 B After Width: | Height: | Size: 501 B |
8
lector/resources/raw/DarkIcons/tableofcontents.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 1 3.0039062 L 1 5.0039062 L 3 5.0039062 L 3 3.0039062 L 1 3.0039062 z M 5 3.0039062 L 5 5.0039062 L 15 5.0039062 L 15 3.0039062 L 5 3.0039062 z M 1 7.0039062 L 1 9.0039062 L 3 9.0039062 L 3 7.0039062 L 1 7.0039062 z M 5 7.0039062 L 5 9.0039062 L 15 9.0039062 L 15 7.0039062 L 5 7.0039062 z M 1 11.003906 L 1 13.003906 L 3 13.003906 L 3 11.003906 L 1 11.003906 z M 5 11.003906 L 5 13.003906 L 15 13.003906 L 15 11.003906 L 5 11.003906 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 782 B |
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 781 B |
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 916 B |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
8
lector/resources/raw/DarkIcons/web-browser.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 0.99609375 C 4.134 0.99609375 1 4.1300937 1 7.9960938 C 1 11.862094 4.134 14.996094 8 14.996094 C 11.866 14.996094 15 11.862094 15 7.9960938 C 15 4.1300937 11.866 0.99609375 8 0.99609375 z M 7.5 2.9335938 C 7.5669 2.9265937 7.65125 2.9375937 7.71875 2.9335938 C 7.72675 2.9655938 7.67005 3.0794638 7.59375 3.2460938 C 7.10789 4.3074937 7.08033 5.5504437 7.53125 6.2148438 C 7.61285 6.3353038 7.6875 6.4499437 7.6875 6.4648438 C 7.6875 6.4797438 7.5995 6.4960938 7.5 6.4960938 C 7.26642 6.4960938 7.04538 6.3537238 6.59375 5.9960938 C 6.39312 5.8372237 6.1323 5.7037938 6.03125 5.6835938 C 5.87257 5.6518937 5.83028 5.6657938 5.625 5.8710938 C 5.43401 6.0620537 5.375 6.1650237 5.375 6.3398438 C 5.375 7.0027837 6.16208 7.5297437 7.625 7.8398438 C 9.6117 8.2609137 10.10145 8.6389138 10.15625 9.6835938 C 10.22505 10.993594 9.5276 11.981394 8 12.746094 C 7.81767 12.837394 7.7015 12.872844 7.625 12.902344 C 7.5911 12.899344 7.56505 12.905344 7.53125 12.902344 C 7.51825 12.861844 7.5 12.767884 7.5 12.589844 C 7.5 11.894064 7.22575 11.177844 6.8125 10.777344 C 6.70157 10.669824 6.39098 10.441994 6.125 10.277344 C 5.85903 10.112704 5.59105 9.9214438 5.53125 9.8398438 C 5.43215 9.7044337 5.42386 9.6212437 5.5 9.3710938 C 5.63876 8.9142237 5.80392 8.6597637 6.125 8.3710938 C 6.29333 8.2197537 6.46271 8.0928437 6.5 8.0898438 C 6.5373 8.0868438 6.28485 8.0110437 5.90625 7.9335938 C 5.52767 7.8559938 4.97383 7.6934738 4.6875 7.5898438 C 4.16392 7.4003938 3.457 7.0026837 3.1875 6.7148438 C 3.1761 6.7026437 3.16615 6.6943938 3.15625 6.6835938 C 3.54238 5.1454938 4.626 3.8848438 6.0625 3.2773438 C 6.36307 3.1502138 6.67292 3.0629938 7 2.9960938 C 7.16292 2.9627938 7.33178 2.9506937 7.5 2.9335938 z M 12.1875 5.2773438 C 12.30495 5.3499437 12.74841 6.3093438 12.875 6.7773438 C 13.03844 7.3815337 13.02661 8.4271437 12.875 9.0273438 C 12.8173 9.2557838 12.74335 9.4694937 12.71875 9.4960938 C 12.69415 9.5226938 12.60494 9.3695637 12.5 9.1835938 C 12.39505 8.9976538 12.05984 8.6025437 11.78125 8.3085938 C 10.97711 7.4600637 10.85066 7.0170437 11.1875 6.3398438 C 11.35737 5.9983538 12.0966 5.2212438 12.1875 5.2773438 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 790 B After Width: | Height: | Size: 790 B |
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 564 B |
Before Width: | Height: | Size: 540 B After Width: | Height: | Size: 540 B |
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
BIN
lector/resources/raw/Google.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
lector/resources/raw/Lector.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
8
lector/resources/raw/LightIcons/about.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 815 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
8
lector/resources/raw/LightIcons/annotate.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 617 B |
8
lector/resources/raw/LightIcons/arrow-down.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 372 B |
8
lector/resources/raw/LightIcons/arrow-up.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color:#d3dae3; } .ColorScheme-Highlight { color:#5294e2; }
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 373 B |
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 703 B |
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 546 B |