Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
33
Lector.pro
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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 \
|
||||
resources/definitions.py \
|
||||
resources/settingswindow.py \
|
||||
resources/metadata.py \
|
||||
resources/mainwindow.py
|
||||
|
||||
TRANSLATIONS += resources/translations/Lector_es.ts \
|
||||
resources/translations/Lector_fr.ts \
|
||||
resources/translations/Lector_de.ts \
|
||||
resources/translations/SAMPLE.ts
|
18
README.md
@@ -35,12 +35,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 in `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.
|
||||
Oh, please keep the translations short. There's only so much space for UI elements.
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -68,6 +72,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)
|
||||
|
17
TODO
@@ -1,4 +1,8 @@
|
||||
TODO
|
||||
General:
|
||||
✓ Internationalization
|
||||
Application icon
|
||||
.desktop file
|
||||
Options:
|
||||
✓ Automatic library management
|
||||
✓ Recursive file addition
|
||||
@@ -51,8 +55,10 @@ TODO
|
||||
✓ Cache next and previous images
|
||||
✓ Set context menu for definitions and the like
|
||||
✓ Paragraph indentation
|
||||
✓ Comic view keyboard shortcuts
|
||||
✓ Comic view context menu
|
||||
Adjust key navigation according to viewport dimensions
|
||||
Search document using QTextCursor?
|
||||
Comic view keyboard shortcuts
|
||||
Filetypes:
|
||||
✓ pdf support
|
||||
Parse TOC
|
||||
@@ -66,10 +72,8 @@ 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
|
||||
Slider position change might be acting up
|
||||
Deselecting all directories in the settings dialog also filters out manually added books
|
||||
|
||||
Secondary:
|
||||
Annotations
|
||||
@@ -90,9 +94,6 @@ TODO
|
||||
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
|
||||
|
@@ -17,6 +17,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import gc
|
||||
import sys
|
||||
import hashlib
|
||||
import pathlib
|
||||
@@ -42,7 +43,7 @@ from lector.settingsdialog import SettingsUI
|
||||
from lector.metadatadialog import MetadataUI
|
||||
from lector.definitionsdialog import DefinitionsUI
|
||||
|
||||
from resources import mainwindow, resources
|
||||
from lector.resources import mainwindow, resources
|
||||
|
||||
|
||||
class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
@@ -50,6 +51,17 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
super(MainUI, self).__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# Set window icon
|
||||
self.setWindowIcon(
|
||||
QtGui.QIcon(':/images/Lector.png'))
|
||||
|
||||
# Central Widget - Make borders disappear
|
||||
self.centralWidget().layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Initialize translation function
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
# Empty variables that will be infested soon
|
||||
self.settings = {}
|
||||
self.thread = None # Background Thread
|
||||
@@ -74,8 +86,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# Widget declarations
|
||||
self.libraryFilterMenu = QtWidgets.QMenu()
|
||||
self.statusMessage = QtWidgets.QLabel()
|
||||
self.toolbarToggle = QtWidgets.QToolButton()
|
||||
self.reloadLibrary = QtWidgets.QToolButton()
|
||||
self.distractionFreeToggle = QtWidgets.QToolButton()
|
||||
# self.reloadLibrary = QtWidgets.QToolButton()
|
||||
self.reloadLibrary = QtWidgets.QPushButton()
|
||||
|
||||
# Create the database in case it doesn't exist
|
||||
database.DatabaseInit(self.database_path)
|
||||
@@ -100,13 +113,14 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.statusBar.addWidget(self.sorterProgress)
|
||||
self.sorterProgress.setVisible(False)
|
||||
|
||||
# Statusbar - Toolbar Visibility
|
||||
self.toolbarToggle.setIcon(self.QImageFactory.get_image('visibility'))
|
||||
self.toolbarToggle.setObjectName('toolbarToggle')
|
||||
self.toolbarToggle.setToolTip('Toggle toolbar')
|
||||
self.toolbarToggle.setAutoRaise(True)
|
||||
self.toolbarToggle.clicked.connect(self.toggle_toolbars)
|
||||
self.statusBar.addPermanentWidget(self.toolbarToggle)
|
||||
# Statusbar + Toolbar Visibility
|
||||
self.distractionFreeToggle.setIcon(self.QImageFactory.get_image('visibility'))
|
||||
self.distractionFreeToggle.setObjectName('distractionFreeToggle')
|
||||
self.distractionFreeToggle.setToolTip(
|
||||
self._translate('Main_UI', 'Toggle distraction free mode (Ctrl + D)'))
|
||||
self.distractionFreeToggle.setAutoRaise(True)
|
||||
self.distractionFreeToggle.clicked.connect(self.toggle_distraction_free)
|
||||
self.statusBar.addPermanentWidget(self.distractionFreeToggle)
|
||||
|
||||
# Application wide temporary directory
|
||||
self.temp_dir = QtCore.QTemporaryDir()
|
||||
@@ -121,7 +135,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
# Toolbar display
|
||||
# Maybe make this a persistent option
|
||||
self.settings['show_toolbars'] = True
|
||||
self.settings['show_bars'] = True
|
||||
|
||||
# Library toolbar
|
||||
self.libraryToolBar.addButton.triggered.connect(self.add_books)
|
||||
@@ -196,10 +210,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
print('Available parsers: ' + self.available_parsers)
|
||||
|
||||
# The library refresh button on the Library tab
|
||||
self.reloadLibrary.setFlat(True)
|
||||
self.reloadLibrary.setIcon(self.QImageFactory.get_image('reload'))
|
||||
self.reloadLibrary.setObjectName('reloadLibrary')
|
||||
self.reloadLibrary.setToolTip('Scan library')
|
||||
self.reloadLibrary.setAutoRaise(True)
|
||||
self.reloadLibrary.setToolTip(self._translate('Main_UI', 'Scan library'))
|
||||
self.reloadLibrary.clicked.connect(self.settingsDialog.start_library_scan)
|
||||
|
||||
self.tabWidget.tabBar().setTabButton(
|
||||
@@ -251,17 +265,30 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.tableView.horizontalHeader().sectionClicked.connect(
|
||||
self.lib_ref.table_proxy_model.sort_table_columns)
|
||||
self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.tableView.customContextMenuRequested.connect(self.generate_library_context_menu)
|
||||
self.tableView.customContextMenuRequested.connect(
|
||||
self.generate_library_context_menu)
|
||||
|
||||
# Keyboard shortcuts
|
||||
self.ksCloseTab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self)
|
||||
self.ksCloseTab.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksCloseTab.activated.connect(self.tab_close)
|
||||
self.ksDistractionFree = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+D'), self)
|
||||
self.ksDistractionFree.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksDistractionFree.activated.connect(self.toggle_distraction_free)
|
||||
|
||||
self.ksOpenFile = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+O'), self)
|
||||
self.ksOpenFile.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksOpenFile.activated.connect(self.add_books)
|
||||
|
||||
self.ksExitAll = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'), self)
|
||||
self.ksExitAll.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksExitAll.activated.connect(self.closeEvent)
|
||||
|
||||
self.ksCloseTab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self)
|
||||
self.ksCloseTab.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksCloseTab.activated.connect(self.tab_close)
|
||||
|
||||
self.ksDeletePressed = QtWidgets.QShortcut(QtGui.QKeySequence('Delete'), self)
|
||||
self.ksDeletePressed.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
self.ksDeletePressed.activated.connect(self.delete_pressed)
|
||||
|
||||
self.listView.setFocus()
|
||||
self.open_books_at_startup()
|
||||
|
||||
@@ -289,7 +316,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
file_list = [QtCore.QFileInfo(i).absoluteFilePath() for i in my_args]
|
||||
books = sorter.BookSorter(
|
||||
file_list,
|
||||
'addition',
|
||||
('addition', 'manual'),
|
||||
self.database_path,
|
||||
self.settings['auto_tags'],
|
||||
self.temp_dir.path())
|
||||
@@ -402,9 +429,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if self.tabWidget.currentIndex() != 0:
|
||||
self.tabWidget.widget(self.tabWidget.currentIndex()).add_bookmark()
|
||||
|
||||
def test_function(self):
|
||||
print('Caesar si viveret, ad remum dareris')
|
||||
|
||||
def resizeEvent(self, event=None):
|
||||
if event:
|
||||
# This implies a vertical resize event only
|
||||
@@ -420,7 +444,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# First, calculate the number of images per row
|
||||
i = self.listView.viewport().width() / default_size
|
||||
rem = i - int(i)
|
||||
if rem >= .11875 and rem <= .9999:
|
||||
if rem >= .21875 and rem <= .9999:
|
||||
num_images = int(i)
|
||||
else:
|
||||
num_images = int(i) - 1
|
||||
@@ -441,14 +465,11 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
return
|
||||
|
||||
def add_books(self):
|
||||
# TODO
|
||||
# Remember file addition modality
|
||||
# If a file is added from here, it should not be removed
|
||||
# from the libary in case of a database refresh
|
||||
|
||||
dialog_prompt = self._translate('Main_UI', 'Add books to database')
|
||||
ebooks_string = self._translate('Main_UI', 'eBooks')
|
||||
opened_files = QtWidgets.QFileDialog.getOpenFileNames(
|
||||
self, 'Open file', self.settings['last_open_path'],
|
||||
f'eBooks ({self.available_parsers})')
|
||||
self, dialog_prompt, self.settings['last_open_path'],
|
||||
f'{ebooks_string} ({self.available_parsers})')
|
||||
|
||||
if not opened_files[0]:
|
||||
return
|
||||
@@ -458,9 +479,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
self.settings['last_open_path'] = os.path.dirname(opened_files[0][0])
|
||||
self.sorterProgress.setVisible(True)
|
||||
self.statusMessage.setText('Adding books...')
|
||||
self.statusMessage.setText(self._translate('Main_UI', 'Adding books...'))
|
||||
self.thread = BackGroundBookAddition(
|
||||
opened_files[0], self.database_path, False, self)
|
||||
opened_files[0], self.database_path, 'manual', self)
|
||||
self.thread.finished.connect(self.move_on)
|
||||
self.thread.start()
|
||||
|
||||
@@ -480,11 +501,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
return selected_indexes
|
||||
|
||||
def delete_books(self, selected_indexes=None):
|
||||
# TODO
|
||||
# ? Mirror selection
|
||||
# Ask if library files are to be excluded from further scans
|
||||
# Make a checkbox for this
|
||||
|
||||
if not selected_indexes:
|
||||
# Get a list of QItemSelection objects
|
||||
# What we're interested in is the indexes()[0] in each of them
|
||||
@@ -515,26 +531,32 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
# Update the database in the background
|
||||
self.thread = BackGroundBookDeletion(
|
||||
delete_hashes, self.database_path, self)
|
||||
delete_hashes, self.database_path)
|
||||
self.thread.finished.connect(self.move_on)
|
||||
self.thread.start()
|
||||
|
||||
# Generate a message box to confirm deletion
|
||||
selected_number = len(selected_indexes)
|
||||
confirm_deletion = QtWidgets.QMessageBox()
|
||||
confirm_deletion.setText('Delete %d book(s)?' % selected_number)
|
||||
deletion_prompt = self._translate(
|
||||
'Main_UI', f'Delete {selected_number} book(s)?')
|
||||
confirm_deletion.setText(deletion_prompt)
|
||||
confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
|
||||
confirm_deletion.setWindowTitle('Confirm deletion')
|
||||
confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion'))
|
||||
confirm_deletion.setStandardButtons(
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
confirm_deletion.buttonClicked.connect(ifcontinue)
|
||||
confirm_deletion.show()
|
||||
confirm_deletion.exec_()
|
||||
|
||||
def delete_pressed(self):
|
||||
if self.tabWidget.currentIndex() == 0:
|
||||
self.delete_books()
|
||||
|
||||
def move_on(self):
|
||||
self.settingsDialog.okButton.setEnabled(True)
|
||||
self.settingsDialog.okButton.setToolTip(
|
||||
'Save changes and start library scan')
|
||||
self._translate('Main_UI', 'Save changes and start library scan'))
|
||||
self.reloadLibrary.setEnabled(True)
|
||||
|
||||
self.sorterProgress.setVisible(False)
|
||||
@@ -543,6 +565,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.lib_ref.update_proxymodels()
|
||||
self.lib_ref.generate_library_tags()
|
||||
|
||||
self.statusMessage.setText(
|
||||
str(self.lib_ref.item_proxy_model.rowCount()) +
|
||||
self._translate('Main_UI', ' books'))
|
||||
|
||||
if not self.settings['perform_culling']:
|
||||
self.load_all_covers()
|
||||
|
||||
@@ -570,7 +596,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
self.resizeEvent()
|
||||
self.start_culling_timer()
|
||||
if self.settings['show_toolbars']:
|
||||
if self.settings['show_bars']:
|
||||
self.bookToolBar.hide()
|
||||
self.libraryToolBar.show()
|
||||
|
||||
@@ -578,10 +604,11 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# Making the proxy model available doesn't affect
|
||||
# memory utilization at all. Bleh.
|
||||
self.statusMessage.setText(
|
||||
str(self.lib_ref.item_proxy_model.rowCount()) + ' Books')
|
||||
str(self.lib_ref.item_proxy_model.rowCount()) +
|
||||
self._translate('Main_UI', ' Books'))
|
||||
else:
|
||||
|
||||
if self.settings['show_toolbars']:
|
||||
if self.settings['show_bars']:
|
||||
self.bookToolBar.show()
|
||||
self.libraryToolBar.hide()
|
||||
|
||||
@@ -625,7 +652,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.thread.start()
|
||||
|
||||
self.tabWidget.widget(tab_index).update_last_accessed_time()
|
||||
self.tabWidget.removeTab(tab_index)
|
||||
|
||||
self.tabWidget.widget(tab_index).deleteLater()
|
||||
self.tabWidget.widget(tab_index).setParent(None)
|
||||
gc.collect()
|
||||
|
||||
def set_toc_position(self, event=None):
|
||||
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
|
||||
@@ -742,7 +772,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
contents = sorter.BookSorter(
|
||||
file_paths,
|
||||
'reading',
|
||||
('reading', None),
|
||||
self.database_path,
|
||||
True,
|
||||
self.temp_dir.path()).initiate_threads()
|
||||
@@ -751,7 +781,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# New tabs are created here
|
||||
# Initial position adjustment is carried out by the tab itself
|
||||
file_data = contents[i]
|
||||
Tab(file_data, self.tabWidget)
|
||||
Tab(file_data, self)
|
||||
|
||||
if self.settings['last_open_tab'] == 'library':
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
@@ -869,15 +899,19 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
profile_index, current_profile, QtCore.Qt.UserRole)
|
||||
self.format_contentView()
|
||||
|
||||
def modify_comic_view(self):
|
||||
signal_sender = self.sender().objectName()
|
||||
def modify_comic_view(self, key_pressed=None):
|
||||
if key_pressed:
|
||||
signal_sender = None
|
||||
else:
|
||||
signal_sender = self.sender().objectName()
|
||||
|
||||
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':
|
||||
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
|
||||
self.comic_profile['zoom_mode'] = 'manualZoom'
|
||||
self.comic_profile['padding'] += 50
|
||||
|
||||
@@ -885,7 +919,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if self.comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
|
||||
self.comic_profile['padding'] -= 50
|
||||
|
||||
if signal_sender == 'zoomIn':
|
||||
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
|
||||
|
||||
@@ -893,18 +927,18 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
if self.comic_profile['padding'] < 0:
|
||||
self.comic_profile['padding'] = 0
|
||||
|
||||
if signal_sender == 'fitWidth':
|
||||
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':
|
||||
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':
|
||||
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
|
||||
self.comic_profile['zoom_mode'] = 'originalSize'
|
||||
self.bookToolBar.originalSize.setChecked(True)
|
||||
|
||||
@@ -914,7 +948,8 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# TODO
|
||||
# See what happens if a font isn't installed
|
||||
|
||||
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
|
||||
current_tab = self.tabWidget.widget(
|
||||
self.tabWidget.currentIndex())
|
||||
|
||||
try:
|
||||
current_metadata = current_tab.metadata
|
||||
@@ -999,19 +1034,24 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
context_menu = QtWidgets.QMenu()
|
||||
|
||||
openAction = context_menu.addAction(
|
||||
self.QImageFactory.get_image('view-readermode'), 'Start reading')
|
||||
self.QImageFactory.get_image('view-readermode'),
|
||||
self._translate('Main_UI', 'Start reading'))
|
||||
|
||||
editAction = None
|
||||
if len(selected_indexes) == 1:
|
||||
editAction = context_menu.addAction(
|
||||
self.QImageFactory.get_image('edit-rename'), 'Edit')
|
||||
self.QImageFactory.get_image('edit-rename'),
|
||||
self._translate('Main_UI', 'Edit'))
|
||||
|
||||
deleteAction = context_menu.addAction(
|
||||
self.QImageFactory.get_image('trash-empty'), 'Delete')
|
||||
self.QImageFactory.get_image('trash-empty'),
|
||||
self._translate('Main_UI', 'Delete'))
|
||||
readAction = context_menu.addAction(
|
||||
QtGui.QIcon(':/images/checkmark.svg'), 'Mark read')
|
||||
QtGui.QIcon(':/images/checkmark.svg'),
|
||||
self._translate('Main_UI', 'Mark read'))
|
||||
unreadAction = context_menu.addAction(
|
||||
QtGui.QIcon(':/images/xmark.svg'), 'Mark unread')
|
||||
QtGui.QIcon(':/images/xmark.svg'),
|
||||
self._translate('Main_UI', 'Mark unread'))
|
||||
|
||||
action = context_menu.exec_(self.sender().mapToGlobal(position))
|
||||
|
||||
@@ -1108,11 +1148,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
checked = [i for i in directory_list if i[3] == QtCore.Qt.Checked]
|
||||
filter_list = list(map(generate_name, checked))
|
||||
filter_list.sort()
|
||||
filter_list.append('Manually Added')
|
||||
filter_actions = [QtWidgets.QAction(i, self.libraryFilterMenu) for i in filter_list]
|
||||
|
||||
filter_list.append(self._translate('Main_UI', 'Manually Added'))
|
||||
filter_actions = [QtWidgets.QAction(i, self.libraryFilterMenu) for i in filter_list]
|
||||
|
||||
filter_all = QtWidgets.QAction('All', self.libraryFilterMenu)
|
||||
filter_actions.append(filter_all)
|
||||
|
||||
for i in filter_actions:
|
||||
i.setCheckable(True)
|
||||
i.setChecked(True)
|
||||
@@ -1144,8 +1186,13 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
|
||||
self.lib_ref.update_proxymodels()
|
||||
|
||||
def toggle_toolbars(self):
|
||||
self.settings['show_toolbars'] = not self.settings['show_toolbars']
|
||||
def toggle_distraction_free(self):
|
||||
self.settings['show_bars'] = not self.settings['show_bars']
|
||||
|
||||
self.statusBar.setVisible(
|
||||
not self.statusBar.isVisible())
|
||||
self.tabWidget.tabBar().setVisible(
|
||||
not self.tabWidget.tabBar().isVisible())
|
||||
|
||||
current_tab = self.tabWidget.currentIndex()
|
||||
if current_tab == 0:
|
||||
@@ -1155,6 +1202,8 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.bookToolBar.setVisible(
|
||||
not self.bookToolBar.isVisible())
|
||||
|
||||
self.start_culling_timer()
|
||||
|
||||
def closeEvent(self, event=None):
|
||||
if event:
|
||||
event.ignore()
|
||||
@@ -1196,6 +1245,18 @@ def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName('Lector') # This is needed for QStandardPaths
|
||||
# and my own hubris
|
||||
|
||||
# Internationalization support
|
||||
translator = QtCore.QTranslator()
|
||||
translations_found = translator.load(
|
||||
QtCore.QLocale.system(), ':/translations/translations_bin/Lector_')
|
||||
app.installTranslator(translator)
|
||||
|
||||
translations_out_string = '(Translations found)'
|
||||
if not translations_found:
|
||||
translations_out_string = '(No translations found)'
|
||||
print(f'Locale: {QtCore.QLocale.system().name()}', translations_out_string)
|
||||
|
||||
form = MainUI()
|
||||
form.show()
|
||||
form.resizeEvent()
|
||||
|
@@ -25,29 +25,70 @@ 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'}
|
||||
|
||||
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_database()
|
||||
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_database(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()
|
||||
self.database.close()
|
||||
|
||||
|
||||
class DatabaseFunctions:
|
||||
def __init__(self, location_prefix):
|
||||
@@ -55,10 +96,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 +104,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 +135,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 +146,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 +157,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()
|
||||
@@ -208,9 +251,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,))
|
||||
|
||||
|
@@ -18,16 +18,17 @@
|
||||
|
||||
import requests
|
||||
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 |
|
||||
@@ -39,6 +40,8 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
|
||||
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
|
||||
self.setMask(mask)
|
||||
|
||||
self.definitionView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
self.app_id = 'bb7a91f9'
|
||||
self.app_key = 'fefacdf6775c347b52e9efa2efe642ef'
|
||||
|
||||
@@ -109,8 +112,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'
|
||||
|
@@ -17,7 +17,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):
|
||||
|
@@ -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]
|
@@ -17,7 +17,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from PyQt5 import QtGui
|
||||
from resources import resources
|
||||
from lector.resources import resources
|
||||
|
||||
|
||||
class QImageFactory:
|
||||
|
@@ -20,7 +20,6 @@ import os
|
||||
import pickle
|
||||
import pathlib
|
||||
from PyQt5 import QtGui, QtCore
|
||||
|
||||
from lector import database
|
||||
from lector.models import TableProxyModel, ItemProxyModel
|
||||
|
||||
@@ -31,6 +30,7 @@ class Library:
|
||||
self.view_model = None
|
||||
self.item_proxy_model = None
|
||||
self.table_proxy_model = None
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
def generate_model(self, mode, parsed_books=None, is_database_ready=True):
|
||||
if mode == 'build':
|
||||
@@ -40,7 +40,8 @@ class Library:
|
||||
books = database.DatabaseFunctions(
|
||||
self.parent.database_path).fetch_data(
|
||||
('Title', 'Author', 'Year', 'DateAdded', 'Path',
|
||||
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed'),
|
||||
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed',
|
||||
'Addition'),
|
||||
'books',
|
||||
{'Title': ''},
|
||||
'LIKE')
|
||||
@@ -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
|
||||
@@ -102,7 +107,10 @@ class Library:
|
||||
except KeyError:
|
||||
position_perc = None
|
||||
|
||||
file_exists = os.path.exists(path)
|
||||
try:
|
||||
file_exists = os.path.exists(path)
|
||||
except UnicodeEncodeError:
|
||||
print('Error with unicode encoding in the library module')
|
||||
|
||||
all_metadata = {
|
||||
'title': title,
|
||||
@@ -115,9 +123,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
|
||||
@@ -162,7 +173,8 @@ class Library:
|
||||
self.parent.listView.setIconSize(s)
|
||||
self.parent.listView.setModel(self.item_proxy_model)
|
||||
|
||||
self.table_proxy_model = TableProxyModel(self.parent.temp_dir.path())
|
||||
self.table_proxy_model = TableProxyModel(
|
||||
self.parent.temp_dir.path(), self.parent.tableView.horizontalHeader())
|
||||
self.table_proxy_model.setSourceModel(self.view_model)
|
||||
self.table_proxy_model.setSortCaseSensitivity(False)
|
||||
self.parent.tableView.setModel(self.table_proxy_model)
|
||||
@@ -180,6 +192,9 @@ class Library:
|
||||
self.parent.libraryToolBar.searchBar.text())
|
||||
# ^^^ This isn't needed, but it forces a model update every time the
|
||||
# text in the line edit changes. So I guess it is needed.
|
||||
self.table_proxy_model.sort_table_columns(
|
||||
self.parent.tableView.horizontalHeader().sortIndicatorSection())
|
||||
self.table_proxy_model.sort_table_columns()
|
||||
|
||||
# Item proxy model
|
||||
self.item_proxy_model.invalidateFilter()
|
||||
@@ -191,7 +206,8 @@ class Library:
|
||||
self.parent.libraryToolBar.searchBar.text())
|
||||
|
||||
self.parent.statusMessage.setText(
|
||||
str(self.item_proxy_model.rowCount()) + ' books')
|
||||
str(self.item_proxy_model.rowCount()) +
|
||||
self._translate('Library', ' books'))
|
||||
|
||||
# TODO
|
||||
# Allow sorting by type
|
||||
@@ -227,11 +243,22 @@ class Library:
|
||||
{'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.parent.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 +270,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,9 +278,13 @@ 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
|
||||
# This isn't triggered for an empty view model
|
||||
for i in range(self.view_model.rowCount()):
|
||||
this_item = self.view_model.item(i, 0)
|
||||
all_metadata = this_item.data(QtCore.Qt.UserRole + 3)
|
||||
@@ -267,23 +298,24 @@ 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)
|
||||
|
||||
# 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)
|
||||
|
||||
invalid_paths = all_paths - valid_paths
|
||||
|
||||
invalid_paths = []
|
||||
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:
|
||||
|
||||
item_metadata = item.data(QtCore.Qt.UserRole + 3)
|
||||
book_path = item_metadata['path']
|
||||
try:
|
||||
addition_mode = item_metadata['addition_mode']
|
||||
except KeyError:
|
||||
addition_mode = 'automatic'
|
||||
print('Libary: Error setting addition mode for prune')
|
||||
|
||||
if (book_path not in valid_paths and
|
||||
(addition_mode != 'manual' or addition_mode is None)):
|
||||
|
||||
invalid_paths.append(book_path)
|
||||
deletable_persistent_indexes.append(
|
||||
QtCore.QPersistentModelIndex(item.index()))
|
||||
|
||||
|
@@ -19,15 +19,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 |
|
||||
@@ -85,7 +85,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
|
||||
graphics_scene.addPixmap(image_pixmap)
|
||||
self.coverView.setScene(graphics_scene)
|
||||
|
||||
def ok_pressed(self, event):
|
||||
def ok_pressed(self, event=None):
|
||||
book_item = self.parent.lib_ref.view_model.item(self.book_index.row())
|
||||
|
||||
title = self.titleLine.text()
|
||||
@@ -97,7 +97,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)
|
||||
@@ -123,7 +125,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):
|
||||
|
@@ -16,18 +16,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 +65,20 @@ class ItemProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
|
||||
class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, temp_dir, parent=None):
|
||||
def __init__(self, temp_dir, tableViewHeader, parent=None):
|
||||
super(TableProxyModel, self).__init__(parent)
|
||||
self.tableViewHeader = tableViewHeader
|
||||
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 +97,11 @@ 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)
|
||||
return 'IndexError'
|
||||
|
||||
def flags(self, index):
|
||||
# Tag editing will take place by way of a right click menu
|
||||
@@ -143,11 +156,8 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
||||
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 +174,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)
|
||||
|
@@ -22,7 +22,7 @@
|
||||
import os
|
||||
import time
|
||||
import zipfile
|
||||
from rarfile import rarfile
|
||||
from lector.rarfile import rarfile
|
||||
|
||||
|
||||
class ParseCOMIC:
|
||||
@@ -49,7 +49,8 @@ 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
|
@@ -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,6 +17,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import io
|
||||
import os
|
||||
from PyQt5 import QtCore
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
@@ -48,7 +49,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:
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 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 |
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 |
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/Lector.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 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/LightIcons/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:#d3dae3; } .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 |