Fairly substantial rewrite.

This commit is contained in:
BasioMeusPuga
2017-12-28 18:27:42 +05:30
parent 69392c5d4f
commit 01d1be9ddc
23 changed files with 1360 additions and 611 deletions

1
AUTHORS Normal file
View File

@@ -0,0 +1 @@
BasioMeusPuga <disgruntled.mob@gmail.com>

13
TODO
View File

@@ -1,13 +1,12 @@
TODO TODO
Options: Options:
Automatic library management Automatic library management
✓ Recursive file addition
Auto deletion Auto deletion
Recursive file addition
Add only one file type if multiple are present Add only one file type if multiple are present
Remember files Remember files
Check files (hashes) upon restart Check files (hashes) upon restart
Show what on startup ✓ Draw shadows
Draw shadows
Library: Library:
✓ sqlite3 for cover images cache ✓ sqlite3 for cover images cache
✓ sqlite3 for storing metadata ✓ sqlite3 for storing metadata
@@ -19,6 +18,7 @@ TODO
✓ Tie file deletion and tab closing to model updates ✓ Tie file deletion and tab closing to model updates
✓ Create separate thread for parser - Show progress in main window ✓ Create separate thread for parser - Show progress in main window
? Create emblem per filetype ? Create emblem per filetype
Memory management
Table view Table view
Ignore a / the / numbers for sorting purposes Ignore a / the / numbers for sorting purposes
Put the path in the scope of the search Put the path in the scope of the search
@@ -27,6 +27,7 @@ TODO
Information dialog widget Information dialog widget
Context menu: Cache, Read, Edit database, delete, Mark read/unread Context menu: Cache, Read, Edit database, delete, Mark read/unread
Set focus to newly added file Set focus to newly added file
Add capability to sort by new
Reading: Reading:
✓ Drop down for TOC ✓ Drop down for TOC
✓ Override the keypress event of the textedit ✓ Override the keypress event of the textedit
@@ -45,6 +46,7 @@ TODO
Record progress Record progress
Pagination Pagination
Set context menu for definitions and the like Set context menu for definitions and the like
Hide progressbar
Filetypes: Filetypes:
✓ cbz, cbr support ✓ cbz, cbr support
✓ Keep font settings enabled but only for background color ✓ Keep font settings enabled but only for background color
@@ -60,3 +62,4 @@ TODO
Other: Other:
✓ Define every widget in code ✓ Define every widget in code
✓ Include icons for emblems ✓ Include icons for emblems
Shift to logging instead of print statements

View File

@@ -1,5 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Consider using sender().text() instead of sender().objectName()
import os import os
import sys import sys
@@ -8,9 +27,9 @@ from PyQt5 import QtWidgets, QtGui, QtCore
import sorter import sorter
import database import database
from resources import mainwindow from resources import mainwindow, resources
from widgets import LibraryToolBar, BookToolBar, Tab, LibraryDelegate from widgets import LibraryToolBar, BookToolBar, Tab, LibraryDelegate
from threaded import BackGroundTabUpdate, BackGroundBookAddition from threaded import BackGroundTabUpdate, BackGroundBookAddition, BackGroundBookDeletion
from library import Library from library import Library
from settings import Settings from settings import Settings
@@ -23,8 +42,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.setupUi(self) self.setupUi(self)
# Empty variables that will be infested soon # Empty variables that will be infested soon
self.current_view = None self.settings = {}
self.last_open_books = None
self.last_open_tab = None self.last_open_tab = None
self.last_open_path = None self.last_open_path = None
self.thread = None # Background Thread self.thread = None # Background Thread
@@ -32,8 +50,17 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.display_profiles = None self.display_profiles = None
self.current_profile_index = None self.current_profile_index = None
self.database_path = None self.database_path = None
self.table_header_sizes = None self.library_filter_menu = None
self.settings_dialog_settings = None
# Initialize toolbars
self.libraryToolBar = LibraryToolBar(self)
self.bookToolBar = BookToolBar(self)
# Widget declarations
self.library_filter_menu = QtWidgets.QMenu()
self.statusMessage = QtWidgets.QLabel()
self.toolbarToggle = QtWidgets.QToolButton()
self.reloadLibrary = QtWidgets.QToolButton()
# Initialize application # Initialize application
Settings(self).read_settings() # This should populate all variables that need Settings(self).read_settings() # This should populate all variables that need
@@ -45,25 +72,44 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
# Initialize settings dialog # Initialize settings dialog
self.settings_dialog = SettingsUI(self) self.settings_dialog = SettingsUI(self)
# Create and right align the statusbar label widget # Statusbar widgets
self.statusMessage = QtWidgets.QLabel()
self.statusMessage.setObjectName('statusMessage') self.statusMessage.setObjectName('statusMessage')
self.statusBar.addPermanentWidget(self.statusMessage) self.statusBar.addPermanentWidget(self.statusMessage)
self.sorterProgress = QtWidgets.QProgressBar() self.sorterProgress = QtWidgets.QProgressBar()
self.sorterProgress.setMaximumWidth(300)
self.sorterProgress.setObjectName('sorterProgress') self.sorterProgress.setObjectName('sorterProgress')
sorter.progressbar = self.sorterProgress # This is so that updates can be sorter.progressbar = self.sorterProgress # This is so that updates can be
# connected to setValue # connected to setValue
self.statusBar.addWidget(self.sorterProgress) self.statusBar.addWidget(self.sorterProgress)
self.sorterProgress.setVisible(False) self.sorterProgress.setVisible(False)
self.toolbarToggle.setIcon(QtGui.QIcon.fromTheme('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)
# THIS IS TEMPORARY
self.guiTest = QtWidgets.QToolButton()
self.guiTest.setIcon(QtGui.QIcon.fromTheme('mail-thread-watch'))
self.guiTest.setObjectName('guiTest')
self.guiTest.setToolTip('Test Function')
self.guiTest.setAutoRaise(True)
self.guiTest.clicked.connect(self.test_function)
self.statusBar.addPermanentWidget(self.guiTest)
# Application wide temporary directory # Application wide temporary directory
self.temp_dir = QtCore.QTemporaryDir() self.temp_dir = QtCore.QTemporaryDir()
# Init the Library # Init the Library
self.lib_ref = Library(self) self.lib_ref = Library(self)
# Toolbar display
# Maybe make this a persistent option
self.settings['show_toolbars'] = True
# Library toolbar # Library toolbar
self.libraryToolBar = LibraryToolBar(self)
self.libraryToolBar.addButton.triggered.connect(self.add_books) self.libraryToolBar.addButton.triggered.connect(self.add_books)
self.libraryToolBar.deleteButton.triggered.connect(self.delete_books) self.libraryToolBar.deleteButton.triggered.connect(self.delete_books)
self.libraryToolBar.coverViewButton.triggered.connect(self.switch_library_view) self.libraryToolBar.coverViewButton.triggered.connect(self.switch_library_view)
@@ -72,16 +118,16 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodel) self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodel)
self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_table_proxy_model) self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_table_proxy_model)
self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodel) self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodel)
self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup)
if self.current_view == 0:
self.libraryToolBar.coverViewButton.trigger()
elif self.current_view == 1:
self.libraryToolBar.tableViewButton.trigger()
self.addToolBar(self.libraryToolBar) self.addToolBar(self.libraryToolBar)
if self.settings['current_view'] == 0:
self.libraryToolBar.coverViewButton.trigger()
else:
self.libraryToolBar.tableViewButton.trigger()
# Book toolbar # Book toolbar
self.bookToolBar = BookToolBar(self) self.bookToolBar.bookmarkButton.triggered.connect(self.toggle_dock_widget)
self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen) self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)
for count, i in enumerate(self.display_profiles): for count, i in enumerate(self.display_profiles):
@@ -120,11 +166,12 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers) self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers)
print('Available parsers: ' + self.available_parsers) print('Available parsers: ' + self.available_parsers)
self.reloadLibrary = QtWidgets.QToolButton() # The library refresh button on the Library tab
self.reloadLibrary.setIcon(QtGui.QIcon.fromTheme('reload')) self.reloadLibrary.setIcon(QtGui.QIcon.fromTheme('reload'))
self.reloadLibrary.setObjectName('reloadLibrary')
self.reloadLibrary.setAutoRaise(True) self.reloadLibrary.setAutoRaise(True)
self.reloadLibrary.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.reloadLibrary.clicked.connect(self.settings_dialog.start_library_scan)
self.reloadLibrary.triggered.connect(self.switch_library_view) # self.reloadLibrary.clicked.connect(self.cull_covers) # TODO
self.tabWidget.tabBar().setTabButton( self.tabWidget.tabBar().setTabButton(
0, QtWidgets.QTabBar.RightSide, self.reloadLibrary) 0, QtWidgets.QTabBar.RightSide, self.reloadLibrary)
@@ -140,7 +187,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.listView.setMouseTracking(True) self.listView.setMouseTracking(True)
self.listView.verticalScrollBar().setSingleStep(9) self.listView.verticalScrollBar().setSingleStep(9)
self.listView.doubleClicked.connect(self.library_doubleclick) self.listView.doubleClicked.connect(self.library_doubleclick)
self.listView.setItemDelegate(LibraryDelegate(self.temp_dir.path())) self.listView.setItemDelegate(LibraryDelegate(self.temp_dir.path(), self))
# TableView # TableView
self.tableView.doubleClicked.connect(self.library_doubleclick) self.tableView.doubleClicked.connect(self.library_doubleclick)
@@ -148,11 +195,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
QtWidgets.QHeaderView.Interactive) QtWidgets.QHeaderView.Interactive)
self.tableView.horizontalHeader().setSortIndicator(0, QtCore.Qt.AscendingOrder) self.tableView.horizontalHeader().setSortIndicator(0, QtCore.Qt.AscendingOrder)
self.tableView.horizontalHeader().setHighlightSections(False) self.tableView.horizontalHeader().setHighlightSections(False)
if self.table_header_sizes: if self.settings['main_window_headers']:
for count, i in enumerate(self.table_header_sizes): for count, i in enumerate(self.settings['main_window_headers']):
self.tableView.horizontalHeader().resizeSection(count, int(i)) self.tableView.horizontalHeader().resizeSection(count, int(i))
self.tableView.horizontalHeader().setStretchLastSection(True) self.tableView.horizontalHeader().setStretchLastSection(True)
self.tableView.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)
# Keyboard shortcuts # Keyboard shortcuts
self.ks_close_tab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self) self.ks_close_tab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self)
@@ -167,8 +213,34 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
# Open last... open books. # Open last... open books.
# Then set the value to None for the next run # Then set the value to None for the next run
self.open_files(self.last_open_books) self.open_files(self.settings['last_open_books'])
self.last_open_books = None
# Scan the library @ startup
if self.settings['scan_library']:
self.settings_dialog.start_library_scan()
def test_function(self, event=None):
top_index = self.listView.indexAt(QtCore.QPoint(20, 20))
model_index = self.lib_ref.proxy_model.mapToSource(top_index)
top_item = self.lib_ref.view_model.item(model_index.row())
if top_item:
img_pixmap = QtGui.QPixmap()
img_pixmap.load(':/images/blank.png')
top_item.setIcon(QtGui.QIcon(img_pixmap))
else:
print('Invalid index')
def cull_covers(self):
# TODO
# Use this to reduce memory utilization
img_pixmap = QtGui.QPixmap()
img_pixmap.load(':/images/blank.png')
for i in range(self.lib_ref.view_model.rowCount()):
item = self.lib_ref.view_model.item(i)
item.setIcon(QtGui.QIcon(img_pixmap))
def resizeEvent(self, event=None): def resizeEvent(self, event=None):
if event: if event:
@@ -206,59 +278,96 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
def add_books(self): def add_books(self):
# TODO # TODO
# Maybe expand this to traverse directories recursively # 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
opened_files = QtWidgets.QFileDialog.getOpenFileNames( opened_files = QtWidgets.QFileDialog.getOpenFileNames(
self, 'Open file', self.last_open_path, self, 'Open file', self.last_open_path,
f'eBooks ({self.available_parsers})') f'eBooks ({self.available_parsers})')
if opened_files[0]: if not opened_files[0]:
return
self.settings_dialog.okButton.setEnabled(False)
self.reloadLibrary.setEnabled(False)
self.last_open_path = os.path.dirname(opened_files[0][0]) self.last_open_path = os.path.dirname(opened_files[0][0])
self.sorterProgress.setVisible(True) self.sorterProgress.setVisible(True)
self.statusMessage.setText('Adding books...') self.statusMessage.setText('Adding books...')
self.thread = BackGroundBookAddition(self, opened_files[0], self.database_path) self.thread = BackGroundBookAddition(
opened_files[0], self.database_path, False, self)
self.thread.finished.connect(self.move_on) self.thread.finished.connect(self.move_on)
self.thread.start() self.thread.start()
def move_on(self):
self.sorterProgress.setVisible(False)
self.lib_ref.create_table_model()
self.lib_ref.create_proxymodel()
def delete_books(self): def delete_books(self):
# TODO # TODO
# Use maptosource() here to get the view_model
# indices selected in the listView
# Implement this for the tableview # Implement this for the tableview
# The same process can be used to mirror selection # The same process can be used to mirror selection
# Ask if library files are to be excluded from further scans
# Make a checkbox for this
selected_books = self.listView.selectedIndexes() # Get a list of QItemSelection objects
if selected_books: # What we're interested in is the indexes()[0] in each of them
# That gives a list of indexes from the view model
selected_books = self.lib_ref.proxy_model.mapSelectionToSource(
self.listView.selectionModel().selection())
if not selected_books:
return
# Deal with message box selection
def ifcontinue(box_button): def ifcontinue(box_button):
if box_button.text() == '&Yes': if box_button.text() != '&Yes':
selected_hashes = [] return
for i in selected_books:
data = i.data(QtCore.Qt.UserRole + 3)
selected_hashes.append(data['hash'])
database.DatabaseFunctions( # Generate list of selected indexes and deletable hashes
self.database_path).delete_from_database(selected_hashes) selected_indexes = [i.indexes() for i in selected_books]
delete_hashes = [
self.lib_ref.view_model.data(
i[0], QtCore.Qt.UserRole + 6) for i in selected_indexes]
# Delete the entries from the table model by way of filtering by hash
self.lib_ref.table_rows = [
i for i in self.lib_ref.table_rows if i[6] not in delete_hashes]
# Persistent model indexes are required beause deletion mutates the model
# Gnerate and delete by persistent index
persistent_indexes = [
QtCore.QPersistentModelIndex(i[0]) for i in selected_indexes]
for i in persistent_indexes:
self.lib_ref.view_model.removeRow(i.row())
# Update the database in the background
self.thread = BackGroundBookDeletion(
delete_hashes, self.database_path, self)
self.thread.finished.connect(self.move_on)
self.thread.start()
# Generate a message box to confirm deletion
selected_number = len(selected_books)
confirm_deletion = QtWidgets.QMessageBox()
confirm_deletion.setText('Delete %d book(s)?' % selected_number)
confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
confirm_deletion.setWindowTitle('Confirm deletion')
confirm_deletion.setStandardButtons(
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
confirm_deletion.buttonClicked.connect(ifcontinue)
confirm_deletion.show()
confirm_deletion.exec_()
def move_on(self):
self.settings_dialog.okButton.setEnabled(True)
self.settings_dialog.okButton.setToolTip(
'Save changes and start library scan')
self.reloadLibrary.setEnabled(True)
self.sorterProgress.setVisible(False)
self.sorterProgress.setValue(0)
self.lib_ref.generate_model('build')
self.lib_ref.create_table_model() self.lib_ref.create_table_model()
self.lib_ref.create_proxymodel() self.lib_ref.create_proxymodel()
selected_number = len(selected_books)
msg_box = QtWidgets.QMessageBox()
msg_box.setText('Delete %d book(s)?' % selected_number)
msg_box.setIcon(QtWidgets.QMessageBox.Question)
msg_box.setWindowTitle('Confirm deletion')
msg_box.setStandardButtons(
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
msg_box.buttonClicked.connect(ifcontinue)
msg_box.show()
msg_box.exec_()
def switch_library_view(self): def switch_library_view(self):
if self.libraryToolBar.coverViewButton.isChecked(): if self.libraryToolBar.coverViewButton.isChecked():
self.stackedWidget.setCurrentIndex(0) self.stackedWidget.setCurrentIndex(0)
@@ -273,6 +382,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if self.tabWidget.currentIndex() == 0: if self.tabWidget.currentIndex() == 0:
self.resizeEvent() self.resizeEvent()
if self.settings['show_toolbars']:
self.bookToolBar.hide() self.bookToolBar.hide()
self.libraryToolBar.show() self.libraryToolBar.show()
@@ -282,6 +392,8 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.statusMessage.setText( self.statusMessage.setText(
str(self.lib_ref.proxy_model.rowCount()) + ' Books') str(self.lib_ref.proxy_model.rowCount()) + ' Books')
else: else:
if self.settings['show_toolbars']:
self.bookToolBar.show() self.bookToolBar.show()
self.libraryToolBar.hide() self.libraryToolBar.hide()
@@ -375,14 +487,24 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
current_tab_widget = self.tabWidget.widget(current_tab) current_tab_widget = self.tabWidget.widget(current_tab)
current_tab_widget.go_fullscreen() current_tab_widget.go_fullscreen()
def library_doubleclick(self, myindex): def toggle_dock_widget(self):
sender = self.sender().objectName()
current_tab = self.tabWidget.currentIndex()
current_tab_widget = self.tabWidget.widget(current_tab)
# TODO
# Extend this to other context related functions
# Make this fullscreenable
if sender == 'bookmarkButton':
current_tab_widget.toggle_bookmarks()
def library_doubleclick(self, index):
sender = self.sender().objectName() sender = self.sender().objectName()
if sender == 'listView': if sender == 'listView':
index = self.lib_ref.proxy_model.index(myindex.row(), 0)
metadata = self.lib_ref.proxy_model.data(index, QtCore.Qt.UserRole + 3) metadata = self.lib_ref.proxy_model.data(index, QtCore.Qt.UserRole + 3)
elif sender == 'tableView': elif sender == 'tableView':
index = self.lib_ref.table_proxy_model.index(myindex.row(), 0)
metadata = self.lib_ref.table_proxy_model.data(index, QtCore.Qt.UserRole) metadata = self.lib_ref.table_proxy_model.data(index, QtCore.Qt.UserRole)
# Shift focus to the tab that has the book open (if there is one) # Shift focus to the tab that has the book open (if there is one)
@@ -409,6 +531,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
file_paths, file_paths,
'reading', 'reading',
self.database_path, self.database_path,
True,
self.temp_dir.path()).initiate_threads() self.temp_dir.path()).initiate_threads()
found_a_focusable_tab = False found_a_focusable_tab = False
@@ -609,19 +732,69 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
else: else:
self.settings_dialog.hide() self.settings_dialog.hide()
def generate_library_filter_menu(self, directory_list=None):
# TODO
# Connect this to filtering @ the level of the library
# Remember state of the checkboxes on library update and application restart
# Behavior for clicking on All
# Don't show anything for less than 2 library folders
self.library_filter_menu.clear()
def generate_name(path_data):
this_filter = path_data[1]
if not this_filter:
this_filter = os.path.basename(
path_data[0]).title()
return this_filter
filter_actions = []
filter_list = []
if directory_list:
checked = [i for i in directory_list if i[3] == QtCore.Qt.Checked]
filter_list = list(map(generate_name, checked))
filter_list.sort()
filter_actions = [QtWidgets.QAction(i, self.library_filter_menu) for i in filter_list]
filter_all = QtWidgets.QAction('All', self.library_filter_menu)
filter_actions.append(filter_all)
for i in filter_actions:
i.setCheckable(True)
i.setChecked(True)
i.triggered.connect(self.set_library_filter)
self.library_filter_menu.addActions(filter_actions)
self.library_filter_menu.insertSeparator(filter_all)
self.libraryToolBar.libraryFilterButton.setMenu(self.library_filter_menu)
def set_library_filter(self, event=None):
print(event)
print(self.sender().text())
def toggle_toolbars(self):
self.settings['show_toolbars'] = not self.settings['show_toolbars']
current_tab = self.tabWidget.currentIndex()
if current_tab == 0:
self.libraryToolBar.setVisible(
not self.libraryToolBar.isVisible())
else:
self.bookToolBar.setVisible(
not self.bookToolBar.isVisible())
def closeEvent(self, event=None): def closeEvent(self, event=None):
# All tabs must be iterated upon here # All tabs must be iterated upon here
self.hide() self.hide()
self.settings_dialog.hide() self.settings_dialog.hide()
self.temp_dir.remove() self.temp_dir.remove()
self.last_open_books = [] self.settings['last_open_books'] = []
if self.tabWidget.count() > 1: if self.tabWidget.count() > 1 and self.settings['remember_files']:
all_metadata = [] all_metadata = []
for i in range(1, self.tabWidget.count()): for i in range(1, self.tabWidget.count()):
tab_metadata = self.tabWidget.widget(i).metadata tab_metadata = self.tabWidget.widget(i).metadata
self.last_open_books.append(tab_metadata['path']) self.settings['last_open_books'].append(tab_metadata['path'])
all_metadata.append(tab_metadata) all_metadata.append(tab_metadata)
Settings(self).save_settings() Settings(self).save_settings()

View File

@@ -1,5 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
import pickle import pickle
import sqlite3 import sqlite3
@@ -16,13 +32,20 @@ class DatabaseInit:
def create_database(self): def create_database(self):
# TODO # TODO
# Add a separate column for directory tags # Add separate columns for:
# directory tags
# bookmarks
# date added
# addition mode
self.database.execute( self.database.execute(
"CREATE TABLE books \ "CREATE TABLE books \
(id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, \ (id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, \
Path TEXT, Position BLOB, ISBN TEXT, Tags TEXT, Hash TEXT, CoverImage BLOB)") Path TEXT, Position BLOB, ISBN TEXT, Tags TEXT, Hash TEXT, CoverImage BLOB)")
# CheckState is the standard QtCore.Qt.Checked / Unchecked
self.database.execute( self.database.execute(
"CREATE TABLE directories (id INTEGER PRIMARY KEY, Path TEXT, Name TEXT, Tags TEXT)") "CREATE TABLE directories (id INTEGER PRIMARY KEY, Path TEXT, \
Name TEXT, Tags TEXT, CheckState INTEGER)")
self.database.commit() self.database.commit()
self.database.close() self.database.close()
@@ -33,15 +56,22 @@ class DatabaseFunctions:
self.database = sqlite3.connect(database_path) self.database = sqlite3.connect(database_path)
def set_library_paths(self, data_iterable): 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: for i in data_iterable:
path = i[0] path = i[0]
name = i[1] name = i[1]
tags = i[2] tags = i[2]
is_checked = i[3]
sql_command = ( sql_command = (
"INSERT OR REPLACE INTO directories (ID, Path, Name, Tags)\ "INSERT OR REPLACE INTO directories (ID, Path, Name, Tags, CheckState)\
VALUES ((SELECT ID FROM directories WHERE Path = ?), ?, ?, ?)") VALUES ((SELECT ID FROM directories WHERE Path = ?), ?, ?, ?, ?)")
self.database.execute(sql_command, [path, path, name, tags]) self.database.execute(sql_command, [path, path, name, tags, is_checked])
self.database.commit() self.database.commit()
self.close_database() self.close_database()
@@ -61,9 +91,15 @@ class DatabaseFunctions:
path = i[1]['path'] path = i[1]['path']
cover = i[1]['cover_image'] cover = i[1]['cover_image']
isbn = i[1]['isbn'] isbn = i[1]['isbn']
tags = i[1]['tags']
if tags:
# Is a tuple. Needs to be a string
tags = ', '.join([j for j in tags if j])
sql_command_add = ( sql_command_add = (
"INSERT INTO books (Title,Author,Year,Path,ISBN,Hash,CoverImage) VALUES(?, ?, ?, ?, ?, ?, ?)") "INSERT INTO \
books (Title, Author, Year, Path, ISBN, Tags, Hash, CoverImage) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
cover_insert = None cover_insert = None
if cover: if cover:
@@ -72,7 +108,7 @@ class DatabaseFunctions:
self.database.execute( self.database.execute(
sql_command_add, sql_command_add,
[title, author, year, [title, author, year,
path, isbn, book_hash, cover_insert]) path, isbn, tags, book_hash, cover_insert])
self.database.commit() self.database.commit()
self.close_database() self.close_database()
@@ -121,8 +157,7 @@ class DatabaseFunctions:
else: else:
return None return None
# except sqlite3.OperationalError: except (KeyError, sqlite3.OperationalError):
except KeyError:
print('SQLite is in rebellion, Commander') print('SQLite is in rebellion, Commander')
self.close_database() self.close_database()
@@ -136,7 +171,9 @@ class DatabaseFunctions:
sql_command = "UPDATE books SET Position = ? WHERE Hash = ?" sql_command = "UPDATE books SET Position = ? WHERE Hash = ?"
try: try:
self.database.execute(sql_command, [sqlite3.Binary(pickled_position), file_hash]) self.database.execute(
sql_command,
[sqlite3.Binary(pickled_position), file_hash])
except sqlite3.OperationalError: except sqlite3.OperationalError:
print('SQLite is in rebellion, Commander') print('SQLite is in rebellion, Commander')
return return
@@ -144,18 +181,15 @@ class DatabaseFunctions:
self.database.commit() self.database.commit()
self.close_database() self.close_database()
def delete_from_database(self, file_hashes): def delete_from_database(self, column_name, target_data):
# file_hashes is expected as a list that will be iterated upon # target_data is an iterable
# This should enable multiple deletion
first = file_hashes[0] if column_name == '*':
sql_command = f"DELETE FROM books WHERE Hash = '{first}'" self.database.execute('DELETE FROM books')
else:
if len(file_hashes) > 1: sql_command = f'DELETE FROM books WHERE {column_name} = ?'
for i in file_hashes[1:]: for i in target_data:
sql_command += f" OR Hash = '{i}'" self.database.execute(sql_command, (i,))
self.database.execute(sql_command)
self.database.commit() self.database.commit()
self.close_database() self.close_database()

View File

@@ -1,16 +1,36 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Implement filterAcceptsRow for the view_model
import os import os
import pickle import pickle
import database import database
from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5 import QtGui, QtCore
from models import LibraryItemModel, MostExcellentTableModel, TableProxyModel from models import MostExcellentTableModel, TableProxyModel
class Library: class Library:
def __init__(self, parent): def __init__(self, parent):
self.parent_window = parent self.parent = parent
self.view_model = None self.view_model = None
self.proxy_model = None self.proxy_model = None
self.table_model = None self.table_model = None
@@ -18,16 +38,12 @@ class Library:
self.table_rows = [] self.table_rows = []
def generate_model(self, mode, parsed_books=None): def generate_model(self, mode, parsed_books=None):
# The QlistView widget needs to be populated
# with a model that inherits from QAbstractItemModel
# because I kinda sorta NEED the match() method
if mode == 'build': if mode == 'build':
self.table_rows = [] self.table_rows = []
self.view_model = LibraryItemModel() self.view_model = QtGui.QStandardItemModel()
books = database.DatabaseFunctions( books = database.DatabaseFunctions(
self.parent_window.database_path).fetch_data( self.parent.database_path).fetch_data(
('*',), ('*',),
'books', 'books',
{'Title': ''}, {'Title': ''},
@@ -43,20 +59,19 @@ class Library:
# database using background threads # database using background threads
books = [] books = []
for i in parsed_books: for i in parsed_books.items():
parsed_title = parsed_books[i]['title'] # Scheme
parsed_author = parsed_books[i]['author'] # 1: Title, 2: Author, 3: Year, 4: Path
parsed_year = parsed_books[i]['year'] # 5: Position, 6: isbn, 7: Tags, 8: Hash
parsed_path = parsed_books[i]['path'] # 9: CoverImage
parsed_position = None
parsed_isbn = parsed_books[i]['isbn'] _tags = i[1]['tags']
parsed_tags = None if _tags:
parsed_hash = i _tags = ', '.join([j for j in _tags if j])
parsed_cover = parsed_books[i]['cover_image']
books.append([ books.append([
None, parsed_title, parsed_author, parsed_year, parsed_path, None, i[1]['title'], i[1]['author'], i[1]['year'], i[1]['path'],
parsed_position, parsed_isbn, parsed_tags, parsed_hash, parsed_cover]) None, i[1]['isbn'], _tags, i[0], i[1]['cover_image']])
else: else:
return return
@@ -134,7 +149,7 @@ class Library:
def create_table_model(self): def create_table_model(self):
table_header = ['Title', 'Author', 'Status', 'Year', 'Tags'] table_header = ['Title', 'Author', 'Status', 'Year', 'Tags']
self.table_model = MostExcellentTableModel( self.table_model = MostExcellentTableModel(
table_header, self.table_rows, self.parent_window.temp_dir.path()) table_header, self.table_rows, self.parent.temp_dir.path())
self.create_table_proxy_model() self.create_table_proxy_model()
def create_table_proxy_model(self): def create_table_proxy_model(self):
@@ -142,38 +157,73 @@ class Library:
self.table_proxy_model.setSourceModel(self.table_model) self.table_proxy_model.setSourceModel(self.table_model)
self.table_proxy_model.setSortCaseSensitivity(False) self.table_proxy_model.setSortCaseSensitivity(False)
self.table_proxy_model.sort(0, QtCore.Qt.AscendingOrder) self.table_proxy_model.sort(0, QtCore.Qt.AscendingOrder)
self.parent_window.tableView.setModel(self.table_proxy_model) self.parent.tableView.setModel(self.table_proxy_model)
self.parent_window.tableView.horizontalHeader().setSortIndicator( self.parent.tableView.horizontalHeader().setSortIndicator(
0, QtCore.Qt.AscendingOrder) 0, QtCore.Qt.AscendingOrder)
self.update_table_proxy_model()
def update_table_proxy_model(self): def update_table_proxy_model(self):
self.table_proxy_model.invalidateFilter() self.table_proxy_model.invalidateFilter()
self.table_proxy_model.setFilterParams( self.table_proxy_model.setFilterParams(
self.parent_window.libraryToolBar.searchBar.text(), [0, 1, 4]) self.parent.libraryToolBar.searchBar.text(), [0, 1, 4])
# This isn't needed, but it forces a model update every time the # 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. # text in the line edit changes. So I guess it is needed.
self.table_proxy_model.setFilterFixedString( self.table_proxy_model.setFilterFixedString(
self.parent_window.libraryToolBar.searchBar.text()) self.parent.libraryToolBar.searchBar.text())
def create_proxymodel(self): def create_proxymodel(self):
self.proxy_model = QtCore.QSortFilterProxyModel() self.proxy_model = QtCore.QSortFilterProxyModel()
self.proxy_model.setSourceModel(self.view_model) self.proxy_model.setSourceModel(self.view_model)
self.proxy_model.setSortCaseSensitivity(False) self.proxy_model.setSortCaseSensitivity(False)
s = QtCore.QSize(160, 250) # Set icon sizing here s = QtCore.QSize(160, 250) # Set icon sizing here
self.parent_window.listView.setIconSize(s) self.parent.listView.setIconSize(s)
self.parent_window.listView.setModel(self.proxy_model) self.parent.listView.setModel(self.proxy_model)
self.update_proxymodel() self.update_proxymodel()
def update_proxymodel(self): def update_proxymodel(self):
self.proxy_model.setFilterRole(QtCore.Qt.UserRole + 4) self.proxy_model.setFilterRole(QtCore.Qt.UserRole + 4)
self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.proxy_model.setFilterWildcard( self.proxy_model.setFilterWildcard(
self.parent_window.libraryToolBar.searchBar.text()) self.parent.libraryToolBar.searchBar.text())
self.parent_window.statusMessage.setText( self.parent.statusMessage.setText(
str(self.proxy_model.rowCount()) + ' books') str(self.proxy_model.rowCount()) + ' books')
# Sorting according to roles and the drop down in the library # Sorting according to roles and the drop down in the library
self.proxy_model.setSortRole( self.proxy_model.setSortRole(
QtCore.Qt.UserRole + self.parent_window.libraryToolBar.sortingBox.currentIndex()) QtCore.Qt.UserRole + self.parent.libraryToolBar.sortingBox.currentIndex())
self.proxy_model.sort(0) self.proxy_model.sort(0)
def prune_models(self, valid_paths):
# To be executed when the library is updated by folder
# 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 in the dictionary from either of the models
# self.table_rows has all file metadata in position 5
all_paths = [i[5]['path'] for i in self.table_rows]
all_paths = set(all_paths)
invalid_paths = all_paths - valid_paths
# Remove invalid paths from both of the models
self.table_rows = [
i for i in self.table_rows if i[5]['path'] not in 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:
deletable_persistent_indexes.append(
QtCore.QPersistentModelIndex(item.index()))
if deletable_persistent_indexes:
for i in deletable_persistent_indexes:
self.view_model.removeRow(i.row())
# Remove invalid paths from the database as well
database.DatabaseFunctions(
self.parent.database_path).delete_from_database('Path', invalid_paths)

180
models.py
View File

@@ -1,15 +1,33 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
from PyQt5 import QtCore, QtGui # 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 pathlib
from PyQt5 import QtCore, QtWidgets
from resources import pie_chart from resources import pie_chart
class LibraryItemModel(QtGui.QStandardItemModel, QtCore.QAbstractItemModel): class ItemProxyModel(QtCore.QSortFilterProxyModel):
# TODO
# Implement filterAcceptsRow
def __init__(self, parent=None): def __init__(self, parent=None):
# We're using this to be able to access the match() method super(ItemProxyModel, self).__init__(parent)
super(LibraryItemModel, self).__init__(parent)
class MostExcellentTableModel(QtCore.QAbstractTableModel): class MostExcellentTableModel(QtCore.QAbstractTableModel):
@@ -39,7 +57,7 @@ class MostExcellentTableModel(QtCore.QAbstractTableModel):
if not index.isValid(): if not index.isValid():
return None return None
# This block specializaes this function for the library # This block specializes this function for the library
# Not having a self.temp_dir allows for its reuse elsewhere # Not having a self.temp_dir allows for its reuse elsewhere
if self.temp_dir: if self.temp_dir:
if role == QtCore.Qt.DecorationRole and index.column() == 2: if role == QtCore.Qt.DecorationRole and index.column() == 2:
@@ -71,7 +89,8 @@ class MostExcellentTableModel(QtCore.QAbstractTableModel):
return value return value
#_________________________________ #_________________________________
if role == QtCore.Qt.DisplayRole: # The EditRole is so that editing a cell doesn't clear its contents
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
value = self.display_data[index.row()][index.column()] value = self.display_data[index.row()][index.column()]
return value return value
@@ -84,8 +103,8 @@ class MostExcellentTableModel(QtCore.QAbstractTableModel):
return None return None
def flags(self, index): def flags(self, index):
# In case of the settings model, model column index 1+ are editable # This means only the Tags column is editable
if not self.temp_dir and index.column() != 0: if self.temp_dir and index.column() == 4:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
else: else:
# These are standard select but don't edit values # These are standard select but don't edit values
@@ -124,10 +143,151 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
model = self.sourceModel() model = self.sourceModel()
valid_indices = [model.index(row_num, i) for i in self.filter_columns] valid_indices = [model.index(row_num, i) for i in self.filter_columns]
valid_data = [model.data(i, QtCore.Qt.DisplayRole).lower() for i in valid_indices if model.data(i, QtCore.Qt.DisplayRole) is not None] valid_data = [
model.data(i, QtCore.Qt.DisplayRole).lower() for i in valid_indices if model.data(
i, QtCore.Qt.DisplayRole) is not None]
for i in valid_data: for i in valid_data:
if self.filter_string in i: if self.filter_string in i:
return True return True
return False return False
class MostExcellentFileSystemModel(QtWidgets.QFileSystemModel):
# Directories are tracked on the basis of their paths
# Poll the tag_data dictionary to get User selection
def __init__(self, tag_data, parent=None):
super(MostExcellentFileSystemModel, self).__init__(parent)
self.tag_data = tag_data
self.field_dict = {
0: 'check_state',
4: 'name',
5: 'tags'}
def columnCount(self, parent):
# The QFileSystemModel returns 4 columns by default
# Columns 1, 2, 3 will be present but hidden
return 6
def headerData(self, col, orientation, role):
# Columns not mentioned here will be hidden
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
column_dict = {
0: 'Path',
4: 'Name',
5: 'Tags'}
return column_dict[col]
def data(self, index, role):
if (index.column() in (4, 5)
and (role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole)):
read_field = self.field_dict[index.column()]
try:
return self.tag_data[self.filePath(index)][read_field]
except KeyError:
return QtCore.QVariant()
if role == QtCore.Qt.CheckStateRole and index.column() == 0:
return self.checkState(index)
return QtWidgets.QFileSystemModel.data(self, index, role)
def flags(self, index):
if index.column() in (4, 5):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
else:
return QtWidgets.QFileSystemModel.flags(self, index) | QtCore.Qt.ItemIsUserCheckable
def checkState(self, index):
while index.isValid():
index_path = self.filePath(index)
if index_path in self.tag_data:
return self.tag_data[index_path]['check_state']
index = index.parent()
return QtCore.Qt.Unchecked
def setData(self, index, value, role):
if (role == QtCore.Qt.EditRole or role == QtCore.Qt.CheckStateRole) and index.isValid():
write_field = self.field_dict[index.column()]
self.layoutAboutToBeChanged.emit()
this_path = self.filePath(index)
if this_path not in self.tag_data:
self.populate_dictionary(this_path)
self.tag_data[this_path][write_field] = value
self.depopulate_dictionary()
self.layoutChanged.emit()
return True
def populate_dictionary(self, path):
self.tag_data[path] = {}
self.tag_data[path]['name'] = None
self.tag_data[path]['tags'] = None
self.tag_data[path]['check_state'] = QtCore.Qt.Checked
def depopulate_dictionary(self):
# This keeps the tag_data dictionary manageable as well as preventing
# weird ass behaviour when something is deselected and its tags are cleared
deletable = set()
for i in self.tag_data.items():
all_data = [j[1] for j in i[1].items()]
filtered_down = list(filter(lambda x: x is not None and x != 0, all_data))
if not filtered_down:
deletable.add(i[0])
# Get untagged subdirectories too
all_dirs = [i for i in self.tag_data]
all_dirs.sort()
def is_child(this_dir):
this_path = pathlib.Path(this_dir)
for i in all_dirs:
if pathlib.Path(i) in this_path.parents:
# If a parent folder has tags, we only want the deletion
# to kick in in case the parent is also checked
if self.tag_data[i]['check_state'] == QtCore.Qt.Checked:
return True
return False
for i in all_dirs:
if is_child(i):
dir_tags = (self.tag_data[i]['name'], self.tag_data[i]['tags'])
filtered_down = list(filter(lambda x: x is not None and x != '', dir_tags))
if not filtered_down:
deletable.add(i)
for i in deletable:
del self.tag_data[i]
# TODO
# Unbork this
class FileSystemProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(FileSystemProxyModel, self).__init__(parent)
def filterAcceptsRow(self, row_num, parent):
model = self.sourceModel()
filter_out = [
'boot', 'dev', 'etc', 'lost+found', 'opt', 'pdb',
'proc', 'root', 'run', 'srv', 'sys', 'tmp', 'twonky',
'usr', 'var', 'bin', 'kdeinit5__0', 'lib', 'lib64', 'sbin']
name_index = model.index(row_num, 0)
valid_data = model.data(name_index)
print(valid_data)
return True
try:
if valid_data in filter_out:
return False
except AttributeError:
pass
return True

View File

@@ -1,5 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Account for files with passwords
import os import os
import time import time
import collections import collections
@@ -49,7 +68,10 @@ class ParseCBR:
return cover_image return cover_image
def get_isbn(self): def get_isbn(self):
return None return
def get_tags(self):
return
def get_contents(self): def get_contents(self):
file_settings = { file_settings = {

View File

@@ -1,5 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Account for files with passwords
import os import os
import time import time
import zipfile import zipfile
@@ -52,7 +71,10 @@ class ParseCBZ:
return cover_image return cover_image
def get_isbn(self): def get_isbn(self):
return None return
def get_tags(self):
return
def get_contents(self): def get_contents(self):
file_settings = { file_settings = {

View File

@@ -1,12 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Every parser is supposed to have the following methods, even if they return None:
# read_book() # This file is a part of Lector, a Qt based ebook reader
# get_title() # Copyright (C) 2017 BasioMeusPuga
# get_author()
# get_year() # This program is free software: you can redistribute it and/or modify
# get_cover_image() # it under the terms of the GNU General Public License as published by
# get_isbn() # the Free Software Foundation, either version 3 of the License, or
# get_contents() - Should return a tuple with 0: TOC 1: Deletable temp_directory # (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 os
import re import re
@@ -40,13 +48,13 @@ class ParseEPUB:
try: try:
return self.book.metadata['http://purl.org/dc/elements/1.1/']['creator'][0][0] return self.book.metadata['http://purl.org/dc/elements/1.1/']['creator'][0][0]
except KeyError: except KeyError:
return None return
def get_year(self): def get_year(self):
try: try:
return self.book.metadata['http://purl.org/dc/elements/1.1/']['date'][0][0][:4] return self.book.metadata['http://purl.org/dc/elements/1.1/']['date'][0][0][:4]
except KeyError: except KeyError:
return None return
def get_cover_image(self): def get_cover_image(self):
# Get cover image # Get cover image
@@ -89,7 +97,6 @@ class ParseEPUB:
if i.media_type == 'image/jpeg' or i.media_type == 'image/png': if i.media_type == 'image/jpeg' or i.media_type == 'image/png':
return i.get_content() return i.get_content()
def get_isbn(self): def get_isbn(self):
try: try:
identifier = self.book.metadata['http://purl.org/dc/elements/1.1/']['identifier'] identifier = self.book.metadata['http://purl.org/dc/elements/1.1/']['identifier']
@@ -99,7 +106,15 @@ class ParseEPUB:
isbn = i[0] isbn = i[0]
return isbn return isbn
except KeyError: except KeyError:
return None return
def get_tags(self):
try:
subject = self.book.metadata['http://purl.org/dc/elements/1.1/']['subject']
tags = [i[0] for i in subject]
return tags
except KeyError:
return
def get_contents(self): def get_contents(self):
extract_path = os.path.join(self.temp_dir, self.file_md5) extract_path = os.path.join(self.temp_dir, self.file_md5)

14
resources/about.html Normal file
View File

@@ -0,0 +1,14 @@
<html>
<head>
<title></title>
<meta content="HTML is not a programming language">
<style></style>
</head>
<body><h1 style="text-align: center;">Lector</h1>
<h2 style="text-align: center;">A Qt Based ebook reader</h2>
<p>&nbsp;</p>
<p>Author: BasioMeusPuga <a href="mailto:disgruntled.mob@gmail.com">disgruntled.mob@gmail.com</a></p>
<p>Page:&nbsp;<a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p>
<p>License: GPLv3&nbsp;<a href="https://www.gnu.org/licenses/gpl-3.0.en.html">https://www.gnu.org/licenses/gpl-3.0.en.html</a></p>
<p>&nbsp;</p></body>
</html>

View File

@@ -57,9 +57,10 @@ class Ui_MainWindow(object):
self.gridLayout_3.setSpacing(0) self.gridLayout_3.setSpacing(0)
self.gridLayout_3.setObjectName("gridLayout_3") self.gridLayout_3.setObjectName("gridLayout_3")
self.tableView = QtWidgets.QTableView(self.tablePage) self.tableView = QtWidgets.QTableView(self.tablePage)
self.tableView.setFrameShape(QtWidgets.QFrame.NoFrame) self.tableView.setFrameShape(QtWidgets.QFrame.Box)
self.tableView.setFrameShadow(QtWidgets.QFrame.Plain)
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow) self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed) self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
self.tableView.setAlternatingRowColors(True) self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.tableView.setGridStyle(QtCore.Qt.NoPen) self.tableView.setGridStyle(QtCore.Qt.NoPen)

View File

@@ -110,10 +110,11 @@ def pixmapper(current_chapter, total_chapters, temp_dir, size):
# TODO # TODO
# See if saving the svg to disk can be avoided # See if saving the svg to disk can be avoided
# Shift to lines to track progress # Shift to lines to track progress
# Maybe make the alignment a little more uniform across emblems
progress_percent = int(current_chapter * 100 / total_chapters) progress_percent = int(current_chapter * 100 / total_chapters)
generate_pie(progress_percent, temp_dir) generate_pie(progress_percent, temp_dir)
svg_path = os.path.join(temp_dir, 'lector_progress.svg') svg_path = os.path.join(temp_dir, 'lector_progress.svg')
return_pixmap = QtGui.QIcon(svg_path).pixmap(size) return_pixmap = QtGui.QIcon(svg_path).pixmap(size - 4) ## The -4 looks more proportional
return return_pixmap return return_pixmap

BIN
resources/raw/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

View File

@@ -117,13 +117,16 @@
<item row="0" column="0"> <item row="0" column="0">
<widget class="QTableView" name="tableView"> <widget class="QTableView" name="tableView">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::NoFrame</enum> <enum>QFrame::Box</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property> </property>
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum> <enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property> </property>
<property name="editTriggers"> <property name="editTriggers">
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set> <set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
</property> </property>
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
@@ -141,7 +144,7 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<attribute name="horizontalHeaderVisible"> <attribute name="horizontalHeaderVisible">
<bool>true</bool> <bool>false</bool>
</attribute> </attribute>
<attribute name="verticalHeaderVisible"> <attribute name="verticalHeaderVisible">
<bool>false</bool> <bool>false</bool>

View File

@@ -1,5 +1,6 @@
<RCC> <RCC>
<qresource prefix="images"> <qresource prefix="images">
<file>blank.png</file>
<file>gray-shadow.png</file> <file>gray-shadow.png</file>
<file>NotFound.png</file> <file>NotFound.png</file>
<file>checkmark.svg</file> <file>checkmark.svg</file>

View File

@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>879</width> <width>929</width>
<height>673</height> <height>638</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -23,82 +23,35 @@
</property> </property>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QTableView" name="tableView"> <widget class="QTreeView" name="treeView"/>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectItems</enum>
</property>
<property name="gridStyle">
<enum>Qt::NoPen</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout"> <widget class="QTextBrowser" name="aboutBox">
<item> <property name="openExternalLinks">
<widget class="QLineEdit" name="tableFilterEdit"> <bool>true</bool>
<property name="placeholderText"> </property>
<string>Search for Paths, Names, Tags...</string> <property name="openLinks">
<bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="addButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeButton">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Startup</string> <string>Switches</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QCheckBox" name="checkBox"> <layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="refreshLibrary">
<property name="text"> <property name="text">
<string>Auto add files</string> <string>Startup: Refresh library</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -110,21 +63,28 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkBox_2"> <widget class="QCheckBox" name="coverShadows">
<property name="text"> <property name="text">
<string>Show Library</string> <string>Cover shadows</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkBox_3"> <widget class="QCheckBox" name="autoTags">
<property name="text"> <property name="text">
<string>Cover Shadows</string> <string>Generate tags from files</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="0"> <item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
@@ -152,11 +112,6 @@
</item> </item>
</layout> </layout>
</widget> </widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/> <resources/>
<connections/> <connections/>
</ui> </ui>

View File

@@ -2,7 +2,7 @@
# Resource object code # Resource object code
# #
# Created by: The Resource Compiler for PyQt5 (Qt v5.9.2) # Created by: The Resource Compiler for PyQt5 (Qt v5.10.0)
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@@ -1117,6 +1117,50 @@ qt_resource_data = b"\
\x00\x00\xd2\x04\x2f\x00\x00\x69\x82\x17\x00\x80\xb4\x2f\xb6\xe1\ \x00\x00\xd2\x04\x2f\x00\x00\x69\x82\x17\x00\x80\xb4\x2f\xb6\xe1\
\xd6\x4d\xdd\x20\x9a\xae\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ \xd6\x4d\xdd\x20\x9a\xae\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\
\x60\x82\ \x60\x82\
\x00\x00\x01\xb6\
\x3c\
\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\
\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\
\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\
\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x65\
\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x20\x76\x69\x65\x77\
\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x36\x20\x31\x36\x22\x3e\
\x0a\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\
\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x20\x2d\x31\x30\x33\
\x36\x2e\x34\x29\x22\x3e\x0a\x20\x20\x3c\x63\x69\x72\x63\x6c\x65\
\x20\x66\x69\x6c\x6c\x3d\x22\x23\x66\x34\x34\x33\x33\x36\x22\x20\
\x63\x78\x3d\x22\x38\x22\x20\x63\x79\x3d\x22\x31\x30\x34\x34\x2e\
\x34\x22\x20\x72\x3d\x22\x37\x22\x2f\x3e\x0a\x20\x20\x3c\x67\x20\
\x66\x69\x6c\x6c\x3d\x22\x23\x66\x66\x66\x22\x20\x74\x72\x61\x6e\
\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x2e\x37\
\x30\x37\x31\x31\x20\x2e\x37\x30\x37\x31\x31\x20\x2d\x2e\x37\x30\
\x37\x31\x31\x20\x2e\x37\x30\x37\x31\x31\x20\x37\x34\x30\x2e\x38\
\x32\x20\x33\x30\x30\x2e\x32\x33\x29\x22\x3e\x0a\x20\x20\x20\x3c\
\x72\x65\x63\x74\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x20\x68\
\x65\x69\x67\x68\x74\x3d\x22\x31\x30\x22\x20\x78\x3d\x22\x31\x30\
\x34\x33\x2e\x34\x22\x20\x79\x3d\x22\x2d\x31\x33\x22\x20\x74\x72\
\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x72\x6f\x74\x61\x74\x65\x28\
\x39\x30\x29\x22\x2f\x3e\x0a\x20\x20\x20\x3c\x72\x65\x63\x74\x20\
\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x20\x68\x65\x69\x67\x68\x74\
\x3d\x22\x31\x30\x22\x20\x78\x3d\x22\x2d\x39\x22\x20\x79\x3d\x22\
\x2d\x31\x30\x34\x39\x2e\x34\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\
\x72\x6d\x3d\x22\x73\x63\x61\x6c\x65\x28\x2d\x31\x29\x22\x2f\x3e\
\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\
\x73\x76\x67\x3e\x0a\
\x00\x00\x00\xb1\
\x00\
\x00\x02\x97\x78\x9c\xeb\x0c\xf0\x73\xe7\xe5\x92\xe2\x62\x60\x60\
\xe0\xf5\xf4\x70\x09\x62\x60\x60\x5c\xc2\xc0\xc0\x14\xc1\xc1\x02\
\x14\x69\xff\x1b\xe5\x08\xa4\x98\x92\xbc\xdd\x5d\x18\xfe\xb7\xf7\
\x9f\xd9\x0f\xe4\x71\x16\x78\x44\x16\x03\x69\x0d\x10\x66\x8c\x39\
\x6e\x1f\x0a\x64\xb0\x97\x78\xfa\xba\xb2\x3f\xe4\xe1\xe7\x93\xd2\
\x9f\x6b\x5b\x1a\x01\x14\x92\xcd\x0c\x89\x28\x71\xce\xcf\xcd\x4d\
\xcd\x2b\x61\x00\x01\xe7\xa2\xd4\xc4\x92\xd4\x14\x85\xf2\xcc\x92\
\x0c\x05\x77\x4f\xdf\x80\x14\xbd\x54\x76\xa0\x7d\xff\x3d\x5d\x1c\
\x43\x2a\x6e\xbd\x3d\xc8\xc8\x0b\x54\x75\x68\xc1\x77\xff\x5c\x7e\
\x76\x11\x86\x51\x30\x22\xc0\x87\xb4\xcd\x8d\x0c\x8c\x9e\x1e\xcf\
\xbe\x83\x78\x9e\xae\x7e\x2e\xeb\x9c\x12\x9a\x00\x8a\x79\x2e\x80\
\
\x00\x00\x00\xa3\ \x00\x00\x00\xa3\
\x00\ \x00\
\x00\x09\x38\x78\x9c\xeb\x0c\xf0\x73\xe7\xe5\x92\xe2\x62\x60\x60\ \x00\x09\x38\x78\x9c\xeb\x0c\xf0\x73\xe7\xe5\x92\xe2\x62\x60\x60\
@@ -1154,36 +1198,6 @@ qt_resource_data = b"\
\x36\x35\x36\x33\x2d\x31\x2e\x34\x31\x34\x31\x2d\x31\x2e\x34\x31\ \x36\x35\x36\x33\x2d\x31\x2e\x34\x31\x34\x31\x2d\x31\x2e\x34\x31\
\x34\x31\x7a\x22\x2f\x3e\x0a\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\ \x34\x31\x7a\x22\x2f\x3e\x0a\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\
\x76\x67\x3e\x0a\ \x76\x67\x3e\x0a\
\x00\x00\x01\xb6\
\x3c\
\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\
\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\
\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\
\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x65\
\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x20\x76\x69\x65\x77\
\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x36\x20\x31\x36\x22\x3e\
\x0a\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\
\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x20\x2d\x31\x30\x33\
\x36\x2e\x34\x29\x22\x3e\x0a\x20\x20\x3c\x63\x69\x72\x63\x6c\x65\
\x20\x66\x69\x6c\x6c\x3d\x22\x23\x66\x34\x34\x33\x33\x36\x22\x20\
\x63\x78\x3d\x22\x38\x22\x20\x63\x79\x3d\x22\x31\x30\x34\x34\x2e\
\x34\x22\x20\x72\x3d\x22\x37\x22\x2f\x3e\x0a\x20\x20\x3c\x67\x20\
\x66\x69\x6c\x6c\x3d\x22\x23\x66\x66\x66\x22\x20\x74\x72\x61\x6e\
\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x2e\x37\
\x30\x37\x31\x31\x20\x2e\x37\x30\x37\x31\x31\x20\x2d\x2e\x37\x30\
\x37\x31\x31\x20\x2e\x37\x30\x37\x31\x31\x20\x37\x34\x30\x2e\x38\
\x32\x20\x33\x30\x30\x2e\x32\x33\x29\x22\x3e\x0a\x20\x20\x20\x3c\
\x72\x65\x63\x74\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x20\x68\
\x65\x69\x67\x68\x74\x3d\x22\x31\x30\x22\x20\x78\x3d\x22\x31\x30\
\x34\x33\x2e\x34\x22\x20\x79\x3d\x22\x2d\x31\x33\x22\x20\x74\x72\
\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x72\x6f\x74\x61\x74\x65\x28\
\x39\x30\x29\x22\x2f\x3e\x0a\x20\x20\x20\x3c\x72\x65\x63\x74\x20\
\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x20\x68\x65\x69\x67\x68\x74\
\x3d\x22\x31\x30\x22\x20\x78\x3d\x22\x2d\x39\x22\x20\x79\x3d\x22\
\x2d\x31\x30\x34\x39\x2e\x34\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\
\x72\x6d\x3d\x22\x73\x63\x61\x6c\x65\x28\x2d\x31\x29\x22\x2f\x3e\
\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\
\x73\x76\x67\x3e\x0a\
" "
qt_resource_name = b"\ qt_resource_name = b"\
@@ -1195,6 +1209,14 @@ qt_resource_name = b"\
\x02\xea\x4d\x87\ \x02\xea\x4d\x87\
\x00\x4e\ \x00\x4e\
\x00\x6f\x00\x74\x00\x46\x00\x6f\x00\x75\x00\x6e\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x6f\x00\x74\x00\x46\x00\x6f\x00\x75\x00\x6e\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x09\
\x09\x65\x83\xe7\
\x00\x65\
\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x09\
\x08\x4e\x85\x07\
\x00\x62\
\x00\x6c\x00\x61\x00\x6e\x00\x6b\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0f\ \x00\x0f\
\x0a\xe2\xd1\x87\ \x0a\xe2\xd1\x87\
\x00\x67\ \x00\x67\
@@ -1203,33 +1225,32 @@ qt_resource_name = b"\
\x0b\x5d\x1f\x07\ \x0b\x5d\x1f\x07\
\x00\x63\ \x00\x63\
\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x6d\x00\x61\x00\x72\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x68\x00\x65\x00\x63\x00\x6b\x00\x6d\x00\x61\x00\x72\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x09\
\x09\x65\x83\xe7\
\x00\x65\
\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\
" "
qt_resource_struct_v1 = b"\ qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x74\x00\x00\x00\x00\x00\x01\x00\x00\x47\x17\ \x00\x00\x00\x48\x00\x01\x00\x00\x00\x01\x00\x00\x46\xd1\
\x00\x00\x00\x30\x00\x01\x00\x00\x00\x01\x00\x00\x45\x17\ \x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x45\x17\
\x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x45\xbe\ \x00\x00\x00\x60\x00\x01\x00\x00\x00\x01\x00\x00\x47\x86\
\x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x00\x48\x2d\
" "
qt_resource_struct_v2 = b"\ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x5f\xb9\x9f\xcd\x26\ \x00\x00\x01\x5f\xb9\x9f\xca\xd0\
\x00\x00\x00\x74\x00\x00\x00\x00\x00\x01\x00\x00\x47\x17\ \x00\x00\x00\x48\x00\x01\x00\x00\x00\x01\x00\x00\x46\xd1\
\x00\x00\x01\x60\x5a\x92\x05\xe5\
\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x45\x17\
\x00\x00\x01\x5f\x7e\xcc\x7f\x20\ \x00\x00\x01\x5f\x7e\xcc\x7f\x20\
\x00\x00\x00\x30\x00\x01\x00\x00\x00\x01\x00\x00\x45\x17\ \x00\x00\x00\x60\x00\x01\x00\x00\x00\x01\x00\x00\x47\x86\
\x00\x00\x01\x5f\xf0\x1d\xc5\xb3\ \x00\x00\x01\x5f\xf0\x1d\xc2\x38\
\x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x45\xbe\ \x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x00\x48\x2d\
\x00\x00\x01\x5f\x7e\xcc\x7f\x20\ \x00\x00\x01\x5f\x7e\xcc\x7f\x20\
" "

View File

@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object): class Ui_Dialog(object):
def setupUi(self, Dialog): def setupUi(self, Dialog):
Dialog.setObjectName("Dialog") Dialog.setObjectName("Dialog")
Dialog.resize(879, 673) Dialog.resize(929, 638)
self.gridLayout_3 = QtWidgets.QGridLayout(Dialog) self.gridLayout_3 = QtWidgets.QGridLayout(Dialog)
self.gridLayout_3.setObjectName("gridLayout_3") self.gridLayout_3.setObjectName("gridLayout_3")
self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2 = QtWidgets.QVBoxLayout()
@@ -20,66 +20,51 @@ class Ui_Dialog(object):
self.groupBox_2.setObjectName("groupBox_2") self.groupBox_2.setObjectName("groupBox_2")
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2) self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2)
self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout_2.setObjectName("gridLayout_2")
self.tableView = QtWidgets.QTableView(self.groupBox_2) self.treeView = QtWidgets.QTreeView(self.groupBox_2)
self.tableView.setFrameShape(QtWidgets.QFrame.NoFrame) self.treeView.setObjectName("treeView")
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow) self.gridLayout_2.addWidget(self.treeView, 0, 0, 1, 1)
self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed) self.aboutBox = QtWidgets.QTextBrowser(self.groupBox_2)
self.tableView.setAlternatingRowColors(True) self.aboutBox.setOpenExternalLinks(True)
self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.aboutBox.setOpenLinks(False)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems) self.aboutBox.setObjectName("aboutBox")
self.tableView.setGridStyle(QtCore.Qt.NoPen) self.gridLayout_2.addWidget(self.aboutBox, 1, 0, 1, 1)
self.tableView.setSortingEnabled(True)
self.tableView.setWordWrap(False)
self.tableView.setObjectName("tableView")
self.tableView.horizontalHeader().setVisible(True)
self.tableView.verticalHeader().setVisible(False)
self.gridLayout_2.addWidget(self.tableView, 0, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.tableFilterEdit = QtWidgets.QLineEdit(self.groupBox_2)
self.tableFilterEdit.setObjectName("tableFilterEdit")
self.horizontalLayout.addWidget(self.tableFilterEdit)
self.addButton = QtWidgets.QPushButton(self.groupBox_2)
self.addButton.setObjectName("addButton")
self.horizontalLayout.addWidget(self.addButton)
self.removeButton = QtWidgets.QPushButton(self.groupBox_2)
self.removeButton.setObjectName("removeButton")
self.horizontalLayout.addWidget(self.removeButton)
self.gridLayout_2.addLayout(self.horizontalLayout, 1, 0, 1, 1)
self.verticalLayout_2.addWidget(self.groupBox_2) self.verticalLayout_2.addWidget(self.groupBox_2)
self.groupBox = QtWidgets.QGroupBox(Dialog) self.groupBox = QtWidgets.QGroupBox(Dialog)
self.groupBox.setObjectName("groupBox") self.groupBox.setObjectName("groupBox")
self.gridLayout = QtWidgets.QGridLayout(self.groupBox) self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
self.gridLayout.setObjectName("gridLayout") self.gridLayout.setObjectName("gridLayout")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout() self.verticalLayout = QtWidgets.QVBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.verticalLayout.setObjectName("verticalLayout")
self.checkBox = QtWidgets.QCheckBox(self.groupBox) self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.checkBox.setObjectName("checkBox") self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.horizontalLayout_3.addWidget(self.checkBox) self.refreshLibrary = QtWidgets.QCheckBox(self.groupBox)
self.refreshLibrary.setObjectName("refreshLibrary")
self.horizontalLayout_4.addWidget(self.refreshLibrary)
self.fileRemember = QtWidgets.QCheckBox(self.groupBox) self.fileRemember = QtWidgets.QCheckBox(self.groupBox)
self.fileRemember.setObjectName("fileRemember") self.fileRemember.setObjectName("fileRemember")
self.horizontalLayout_3.addWidget(self.fileRemember) self.horizontalLayout_4.addWidget(self.fileRemember)
self.checkBox_2 = QtWidgets.QCheckBox(self.groupBox) self.coverShadows = QtWidgets.QCheckBox(self.groupBox)
self.checkBox_2.setObjectName("checkBox_2") self.coverShadows.setObjectName("coverShadows")
self.horizontalLayout_3.addWidget(self.checkBox_2) self.horizontalLayout_4.addWidget(self.coverShadows)
self.checkBox_3 = QtWidgets.QCheckBox(self.groupBox) self.autoTags = QtWidgets.QCheckBox(self.groupBox)
self.checkBox_3.setObjectName("checkBox_3") self.autoTags.setObjectName("autoTags")
self.horizontalLayout_3.addWidget(self.checkBox_3) self.horizontalLayout_4.addWidget(self.autoTags)
self.gridLayout.addLayout(self.horizontalLayout_3, 0, 0, 1, 1) self.verticalLayout.addLayout(self.horizontalLayout_4)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.okButton = QtWidgets.QPushButton(self.groupBox)
self.okButton.setObjectName("okButton")
self.horizontalLayout_2.addWidget(self.okButton)
self.cancelButton = QtWidgets.QPushButton(self.groupBox)
self.cancelButton.setObjectName("cancelButton")
self.horizontalLayout_2.addWidget(self.cancelButton)
self.aboutButton = QtWidgets.QPushButton(self.groupBox)
self.aboutButton.setObjectName("aboutButton")
self.horizontalLayout_2.addWidget(self.aboutButton)
self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
self.verticalLayout_2.addWidget(self.groupBox) self.verticalLayout_2.addWidget(self.groupBox)
self.gridLayout_3.addLayout(self.verticalLayout_2, 0, 0, 1, 1) self.gridLayout_3.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.okButton = QtWidgets.QPushButton(Dialog)
self.okButton.setObjectName("okButton")
self.horizontalLayout_2.addWidget(self.okButton)
self.cancelButton = QtWidgets.QPushButton(Dialog)
self.cancelButton.setObjectName("cancelButton")
self.horizontalLayout_2.addWidget(self.cancelButton)
self.aboutButton = QtWidgets.QPushButton(Dialog)
self.aboutButton.setObjectName("aboutButton")
self.horizontalLayout_2.addWidget(self.aboutButton)
self.gridLayout_3.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
self.retranslateUi(Dialog) self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog) QtCore.QMetaObject.connectSlotsByName(Dialog)
@@ -88,14 +73,11 @@ class Ui_Dialog(object):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Settings")) Dialog.setWindowTitle(_translate("Dialog", "Settings"))
self.groupBox_2.setTitle(_translate("Dialog", "Library")) self.groupBox_2.setTitle(_translate("Dialog", "Library"))
self.tableFilterEdit.setPlaceholderText(_translate("Dialog", "Search for Paths, Names, Tags...")) self.groupBox.setTitle(_translate("Dialog", "Switches"))
self.addButton.setText(_translate("Dialog", "Add")) self.refreshLibrary.setText(_translate("Dialog", "Startup: Refresh library"))
self.removeButton.setText(_translate("Dialog", "Remove"))
self.groupBox.setTitle(_translate("Dialog", "Startup"))
self.checkBox.setText(_translate("Dialog", "Auto add files"))
self.fileRemember.setText(_translate("Dialog", "Remember open files")) self.fileRemember.setText(_translate("Dialog", "Remember open files"))
self.checkBox_2.setText(_translate("Dialog", "Show Library")) self.coverShadows.setText(_translate("Dialog", "Cover shadows"))
self.checkBox_3.setText(_translate("Dialog", "Cover Shadows")) self.autoTags.setText(_translate("Dialog", "Generate tags from files"))
self.okButton.setText(_translate("Dialog", "OK")) self.okButton.setText(_translate("Dialog", "OK"))
self.cancelButton.setText(_translate("Dialog", "Cancel")) self.cancelButton.setText(_translate("Dialog", "Cancel"))
self.aboutButton.setText(_translate("Dialog", "About")) self.aboutButton.setText(_translate("Dialog", "About"))

View File

@@ -1,13 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Keep in mind that all integer settings are returned as strings # Keep in mind that all integer / boolean settings are returned as strings
import os import os
from ast import literal_eval
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
class Settings: class Settings:
def __init__(self, parent): def __init__(self, parent):
self.parent_window = parent self.parent = parent
self.settings = QtCore.QSettings('Lector', 'Lector') self.settings = QtCore.QSettings('Lector', 'Lector')
default_profile1 = { default_profile1 = {
@@ -44,87 +45,106 @@ class Settings:
def read_settings(self): def read_settings(self):
self.settings.beginGroup('mainWindow') self.settings.beginGroup('mainWindow')
self.parent_window.resize(self.settings.value('windowSize', QtCore.QSize(1299, 748))) self.parent.resize(self.settings.value('windowSize', QtCore.QSize(1299, 748)))
self.parent_window.move(self.settings.value('windowPosition', QtCore.QPoint(0, 0))) self.parent.move(self.settings.value('windowPosition', QtCore.QPoint(0, 0)))
self.parent_window.current_view = int(self.settings.value('currentView', 0)) self.parent.settings['current_view'] = int(self.settings.value('currentView', 0))
self.parent_window.table_header_sizes = self.settings.value('tableHeaders', None) self.parent.settings['main_window_headers'] = self.settings.value('tableHeaders', None)
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('runtimeVariables') self.settings.beginGroup('runtimeVariables')
self.parent_window.last_open_path = self.settings.value( self.parent.last_open_path = self.settings.value(
'lastOpenPath', os.path.expanduser('~')) 'lastOpenPath', os.path.expanduser('~'))
self.parent_window.database_path = self.settings.value( self.parent.database_path = self.settings.value(
'databasePath', 'databasePath',
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation)) QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation))
self.parent_window.display_profiles = self.settings.value( self.parent.display_profiles = self.settings.value(
'displayProfiles', self.default_profiles) 'displayProfiles', self.default_profiles)
self.parent_window.current_profile_index = int(self.settings.value( self.parent.current_profile_index = int(self.settings.value(
'currentProfileIndex', 0)) 'currentProfileIndex', 0))
self.parent_window.comic_profile = self.settings.value( self.parent.comic_profile = self.settings.value(
'comicProfile', self.default_comic_profile) 'comicProfile', self.default_comic_profile)
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('lastOpen') self.settings.beginGroup('lastOpen')
self.parent_window.last_open_books = self.settings.value('lastOpenFiles', []) self.parent.settings['last_open_books'] = self.settings.value('lastOpenBooks', [])
self.parent_window.last_open_tab = self.settings.value('lastOpenTab', 'library') self.parent.last_open_tab = self.settings.value('lastOpenTab', 'library')
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('settingsWindow') self.settings.beginGroup('settingsWindow')
self.parent_window.settings_dialog_settings = {} self.parent.settings['settings_dialog_size'] = self.settings.value(
self.parent_window.settings_dialog_settings['size'] = self.settings.value(
'windowSize', QtCore.QSize(700, 500)) 'windowSize', QtCore.QSize(700, 500))
self.parent_window.settings_dialog_settings['position'] = self.settings.value( self.parent.settings['settings_dialog_position'] = self.settings.value(
'windowPosition', QtCore.QPoint(0, 0)) 'windowPosition', QtCore.QPoint(0, 0))
self.parent_window.settings_dialog_settings['headers'] = self.settings.value( self.parent.settings['settings_dialog_headers'] = self.settings.value(
'tableHeaders', [200, 150]) 'tableHeaders', [200, 150])
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('settingsSwitches')
# The default is string true because literal eval will convert it anyway
self.parent.settings['cover_shadows'] = literal_eval(self.settings.value(
'coverShadows', 'True').capitalize())
self.parent.settings['auto_tags'] = literal_eval(self.settings.value(
'autoTags', 'True').capitalize())
self.parent.settings['scan_library'] = literal_eval(self.settings.value(
'scanLibraryAtStart', 'False').capitalize())
self.parent.settings['remember_files'] = literal_eval(self.settings.value(
'rememberFiles', 'False').capitalize())
self.settings.endGroup()
def save_settings(self): def save_settings(self):
print('Saving settings...') print('Saving settings...')
current_settings = self.parent.settings
self.settings.beginGroup('mainWindow') self.settings.beginGroup('mainWindow')
self.settings.setValue('windowSize', self.parent_window.size()) self.settings.setValue('windowSize', self.parent.size())
self.settings.setValue('windowPosition', self.parent_window.pos()) self.settings.setValue('windowPosition', self.parent.pos())
self.settings.setValue('currentView', self.parent_window.stackedWidget.currentIndex()) self.settings.setValue('currentView', self.parent.stackedWidget.currentIndex())
table_headers = [] table_headers = []
for i in range(3): for i in range(3):
table_headers.append(self.parent_window.tableView.horizontalHeader().sectionSize(i)) table_headers.append(self.parent.tableView.horizontalHeader().sectionSize(i))
self.settings.setValue('tableHeaders', table_headers) self.settings.setValue('tableHeaders', table_headers)
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('runtimeVariables') self.settings.beginGroup('runtimeVariables')
self.settings.setValue('lastOpenPath', self.parent_window.last_open_path) self.settings.setValue('lastOpenPath', self.parent.last_open_path)
self.settings.setValue('databasePath', self.parent_window.database_path) self.settings.setValue('databasePath', self.parent.database_path)
current_profile1 = self.parent_window.bookToolBar.profileBox.itemData( current_profile1 = self.parent.bookToolBar.profileBox.itemData(
0, QtCore.Qt.UserRole) 0, QtCore.Qt.UserRole)
current_profile2 = self.parent_window.bookToolBar.profileBox.itemData( current_profile2 = self.parent.bookToolBar.profileBox.itemData(
1, QtCore.Qt.UserRole) 1, QtCore.Qt.UserRole)
current_profile3 = self.parent_window.bookToolBar.profileBox.itemData( current_profile3 = self.parent.bookToolBar.profileBox.itemData(
2, QtCore.Qt.UserRole) 2, QtCore.Qt.UserRole)
current_profile_index = self.parent_window.bookToolBar.profileBox.currentIndex() current_profile_index = self.parent.bookToolBar.profileBox.currentIndex()
self.settings.setValue('displayProfiles', [ self.settings.setValue('displayProfiles', [
current_profile1, current_profile1,
current_profile2, current_profile2,
current_profile3]) current_profile3])
self.settings.setValue('currentProfileIndex', current_profile_index) self.settings.setValue('currentProfileIndex', current_profile_index)
self.settings.setValue('comicProfile', self.parent_window.comic_profile) self.settings.setValue('comicProfile', self.parent.comic_profile)
self.settings.endGroup() self.settings.endGroup()
current_tab_index = self.parent_window.tabWidget.currentIndex() current_tab_index = self.parent.tabWidget.currentIndex()
if current_tab_index == 0: if current_tab_index == 0:
last_open_tab = 'library' last_open_tab = 'library'
else: else:
last_open_tab = self.parent_window.tabWidget.widget(current_tab_index).metadata['path'] last_open_tab = self.parent.tabWidget.widget(current_tab_index).metadata['path']
self.settings.beginGroup('lastOpen') self.settings.beginGroup('lastOpen')
self.settings.setValue('lastOpenFiles', self.parent_window.last_open_books) self.settings.setValue('lastOpenBooks', current_settings['last_open_books'])
self.settings.setValue('lastOpenTab', last_open_tab) self.settings.setValue('lastOpenTab', last_open_tab)
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('settingsWindow') self.settings.beginGroup('settingsWindow')
these_settings = self.parent_window.settings_dialog_settings self.settings.setValue('windowSize', current_settings['settings_dialog_size'])
self.settings.setValue('windowSize', these_settings['size']) self.settings.setValue('windowPosition', current_settings['settings_dialog_position'])
self.settings.setValue('windowPosition', these_settings['position']) self.settings.setValue('tableHeaders', current_settings['settings_dialog_headers'])
self.settings.setValue('tableHeaders', these_settings['headers']) self.settings.endGroup()
self.settings.beginGroup('settingsSwitches')
self.settings.setValue('rememberFiles', current_settings['remember_files'])
self.settings.setValue('coverShadows', current_settings['cover_shadows'])
self.settings.setValue('autoTags', current_settings['auto_tags'])
self.settings.setValue('scanLibraryAtStart', current_settings['scan_library'])
self.settings.endGroup()

View File

@@ -1,156 +1,247 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os # This file is a part of Lector, a Qt based ebook reader
import collections # Copyright (C) 2017 BasioMeusPuga
from PyQt5 import QtWidgets, QtGui, QtCore # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Get Cancel working with the file system model
import os
import copy
from PyQt5 import QtWidgets, QtCore
import database import database
from resources import settingswindow from resources import settingswindow
from models import MostExcellentTableModel, TableProxyModel from models import MostExcellentFileSystemModel, FileSystemProxyModel
from threaded import BackGroundBookSearch, BackGroundBookAddition from threaded import BackGroundBookSearch, BackGroundBookAddition
class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog): class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog):
# TODO def __init__(self, parent):
# Deletion from table
# Cancel behavior
# Update database on table model update
def __init__(self, parent_window):
super(SettingsUI, self).__init__() super(SettingsUI, self).__init__()
self.setupUi(self) self.setupUi(self)
self.last_open_directory = None self.parent = parent
self.parent_window = parent_window self.database_path = self.parent.database_path
self.database_path = self.parent_window.database_path
self.resize(self.parent_window.settings_dialog_settings['size']) self.resize(self.parent.settings['settings_dialog_size'])
self.move(self.parent_window.settings_dialog_settings['position']) self.move(self.parent.settings['settings_dialog_position'])
self.aboutBox.setVisible(False)
with open('resources/about.html') as about_html:
self.aboutBox.setHtml(about_html.read())
self.table_model = None
self.old_table_model = None
self.table_proxy_model = None
self.paths = None self.paths = None
self.thread = None self.thread = None
self.filesystem_model = None
self.tag_data_copy = None
self.tableFilterEdit.textChanged.connect(self.update_table_proxy_model) self.okButton.setToolTip('Save changes and start library scan')
self.addButton.clicked.connect(self.add_directories) self.okButton.clicked.connect(self.start_library_scan)
self.cancelButton.clicked.connect(self.cancel_pressed) self.cancelButton.clicked.connect(self.cancel_pressed)
self.okButton.clicked.connect(self.ok_pressed) self.aboutButton.clicked.connect(self.about_pressed)
self.generate_table() # Check boxes
header_sizes = self.parent_window.settings_dialog_settings['headers'] self.autoTags.setChecked(self.parent.settings['auto_tags'])
if header_sizes: self.coverShadows.setChecked(self.parent.settings['cover_shadows'])
for count, i in enumerate(header_sizes): self.refreshLibrary.setChecked(self.parent.settings['scan_library'])
self.tableView.horizontalHeader().resizeSection(count, int(i)) self.fileRemember.setChecked(self.parent.settings['remember_files'])
self.tableView.horizontalHeader().setSectionResizeMode( self.autoTags.clicked.connect(self.manage_checkboxes)
QtWidgets.QHeaderView.Interactive) self.coverShadows.clicked.connect(self.manage_checkboxes)
self.tableView.horizontalHeader().setHighlightSections(False) self.refreshLibrary.clicked.connect(self.manage_checkboxes)
self.tableView.horizontalHeader().setStretchLastSection(True) self.fileRemember.clicked.connect(self.manage_checkboxes)
# self.tableView.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)
def generate_table(self): # Generate the filesystem treeView
self.generate_tree()
def generate_tree(self):
# Fetch all directories in the database # Fetch all directories in the database
self.paths = database.DatabaseFunctions( paths = database.DatabaseFunctions(
self.database_path).fetch_data( self.database_path).fetch_data(
('Path', 'Name', 'Tags'), ('Path', 'Name', 'Tags', 'CheckState'),
'directories', 'directories',
{'Path': ''}, {'Path': ''},
'LIKE') 'LIKE')
if not self.paths: self.parent.generate_library_filter_menu(paths)
directory_data = {}
if not paths:
print('Database returned no paths for settings...') print('Database returned no paths for settings...')
else: else:
# Convert to a list because tuples, well, they're tuples # Convert to the dictionary format that is
self.paths = [list(i) for i in self.paths] # to be fed into the QFileSystemModel
for i in paths:
directory_data[i[0]] = {
'name': i[1],
'tags': i[2],
'check_state': i[3]}
table_header = ['Path', 'Name', 'Tags'] self.filesystem_model = MostExcellentFileSystemModel(directory_data)
self.table_model = MostExcellentTableModel( self.filesystem_model.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
table_header, self.paths, None) self.treeView.setModel(self.filesystem_model)
self.create_table_proxy_model()
def create_table_proxy_model(self):
self.table_proxy_model = TableProxyModel()
self.table_proxy_model.setSourceModel(self.table_model)
self.table_proxy_model.setSortCaseSensitivity(False)
self.table_proxy_model.sort(1, QtCore.Qt.AscendingOrder)
self.tableView.setModel(self.table_proxy_model)
self.tableView.horizontalHeader().setSortIndicator(
1, QtCore.Qt.AscendingOrder)
def update_table_proxy_model(self):
self.table_proxy_model.invalidateFilter()
self.table_proxy_model.setFilterParams(
self.tableFilterEdit.text(), [0, 1, 2])
self.table_proxy_model.setFilterFixedString(
self.tableFilterEdit.text())
def add_directories(self):
# Directories will be added recursively
# Sub directory addition is not allowed
# In case it is to be allowed eventually, files will not
# be duplicated. However, any additional tags will get
# added to file tags
add_directory = QtWidgets.QFileDialog.getExistingDirectory(
self, 'Select Directory', self.last_open_directory,
QtWidgets.QFileDialog.ShowDirsOnly)
add_directory = os.path.realpath(add_directory)
# TODO # TODO
# Account for a parent folder getting added after a subfolder # This here might break on them pestilent non unixy OSes
# Currently this does the inverse only # Check and see
for i in self.paths: root_directory = QtCore.QDir().rootPath()
already_present = os.path.realpath(i[0]) self.treeView.setRootIndex(self.filesystem_model.setRootPath(root_directory))
if already_present == add_directory or already_present in add_directory:
QtWidgets.QMessageBox.critical(
self,
'Error',
'Duplicate or sub folder: ' + already_present + ' ',
QtWidgets.QMessageBox.Ok)
return
# Set default name for the directory # Set the treeView and QFileSystemModel to its desired state
directory_name = os.path.basename(add_directory).title() selected_paths = [
data_pair = [[add_directory, directory_name, None]] i for i in directory_data if directory_data[i]['check_state'] == QtCore.Qt.Checked]
database.DatabaseFunctions(self.database_path).set_library_paths(data_pair) expand_paths = set()
self.generate_table() for i in selected_paths:
def ok_pressed(self): # Recursively grind down parent paths for expansion
# Traverse directories looking for files this_path = i
self.thread = BackGroundBookSearch(self, self.table_model.display_data) while True:
self.thread.finished.connect(self.do_something) parent_path = os.path.dirname(this_path)
self.thread.start() if parent_path == this_path:
break
expand_paths.add(parent_path)
this_path = parent_path
def do_something(self): # Expand all the parent paths derived from the selected path
print('Book search completed') if root_directory in expand_paths:
expand_paths.remove(root_directory)
for i in expand_paths:
this_index = self.filesystem_model.index(i)
self.treeView.expand(this_index)
header_sizes = self.parent.settings['settings_dialog_headers']
if header_sizes:
for count, i in enumerate((0, 4)):
self.treeView.setColumnWidth(i, int(header_sizes[count]))
# TODO
# Set a QSortFilterProxy model on top of the existing QFileSystem model
# self.filesystem_proxy_model = FileSystemProxyModel()
# self.filesystem_proxy_model.setSourceModel(self.filesystem_model)
# self.treeView.setModel(self.filesystem_proxy_model)
for i in range(1, 4):
self.treeView.hideColumn(i)
def start_library_scan(self):
# TODO
# return in case the treeView is not edited
def cancel_pressed(self):
self.hide() self.hide()
data_pairs = []
for i in self.filesystem_model.tag_data.items():
data_pairs.append([
i[0], i[1]['name'], i[1]['tags'], i[1]['check_state']
])
database.DatabaseFunctions(
self.database_path).set_library_paths(data_pairs)
if not data_pairs:
try:
if self.sender().objectName() == 'reloadLibrary':
self.show()
except AttributeError:
pass
self.parent.lib_ref.view_model.clear()
self.parent.lib_ref.table_rows = []
# TODO # TODO
# Implement cancel by restoring the table model to an older version # Change this to no longer include files added manually
# def showEvent(self, event):
# event.accept() database.DatabaseFunctions(
self.database_path).delete_from_database('*', '*')
return
# Update the main window library filter menu
self.parent.generate_library_filter_menu(data_pairs)
# Disallow rechecking until the first check completes
self.okButton.setEnabled(False)
self.parent.reloadLibrary.setEnabled(False)
self.okButton.setToolTip('Library scan in progress...')
# Traverse directories looking for files
self.parent.statusMessage.setText('Checking library folders')
self.thread = BackGroundBookSearch(data_pairs, self)
self.thread.finished.connect(self.finished_iterating)
self.thread.start()
def finished_iterating(self):
# TODO
# Account for file tags
# The books the search thread has found
# are now in self.thread.valid_files
valid_files = [i[0] for i in self.thread.valid_files]
if not valid_files:
return
# Hey, messaging is important, okay?
self.parent.sorterProgress.setVisible(True)
self.parent.statusMessage.setText('Parsing files')
# We now create a new thread to put those files into the database
self.thread = BackGroundBookAddition(
valid_files, self.database_path, True, self.parent)
self.thread.finished.connect(self.parent.move_on)
self.thread.start()
def cancel_pressed(self):
self.filesystem_model.tag_data = copy.deepcopy(self.tag_data_copy)
self.hide()
def hideEvent(self, event): def hideEvent(self, event):
self.no_more_settings() self.no_more_settings()
event.accept() event.accept()
def showEvent(self, event):
self.tag_data_copy = copy.deepcopy(self.filesystem_model.tag_data)
event.accept()
def no_more_settings(self): def no_more_settings(self):
self.table_model = self.old_table_model self.parent.libraryToolBar.settingsButton.setChecked(False)
self.parent_window.libraryToolBar.settingsButton.setChecked(False) self.aboutBox.hide()
self.treeView.show()
self.resizeEvent() self.resizeEvent()
def resizeEvent(self, event=None): def resizeEvent(self, event=None):
self.parent_window.settings_dialog_settings['size'] = self.size() self.parent.settings['settings_dialog_size'] = self.size()
self.parent_window.settings_dialog_settings['position'] = self.pos() self.parent.settings['settings_dialog_position'] = self.pos()
table_headers = [] table_headers = []
for i in range(2): for i in [0, 4]:
table_headers.append(self.tableView.horizontalHeader().sectionSize(i)) table_headers.append(self.treeView.columnWidth(i))
self.parent_window.settings_dialog_settings['headers'] = table_headers self.parent.settings['settings_dialog_headers'] = table_headers
def manage_checkboxes(self, event=None):
sender = self.sender().objectName()
sender_dict = {
'coverShadows': 'cover_shadows',
'autoTags': 'auto_tags',
'refreshLibrary': 'scan_library',
'fileRemember': 'remember_files'}
self.parent.settings[sender_dict[sender]] = not self.parent.settings[sender_dict[sender]]
def about_pressed(self):
self.treeView.setVisible(not self.treeView.isVisible())
self.aboutBox.setVisible(not self.aboutBox.isVisible())

182
sorter.py
View File

@@ -1,17 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# TODO # This file is a part of Lector, a Qt based ebook reader
# See if you want to include a hash of the book's name and author # Copyright (C) 2017 BasioMeusPuga
import io # This program is free software: you can redistribute it and/or modify
import os # it under the terms of the GNU General Public License as published by
import pickle # the Free Software Foundation, either version 3 of the License, or
import hashlib # (at your option) any later version.
from multiprocessing.dummy import Pool
from PyQt5 import QtCore, QtGui
import database # 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/>.
# INSTRUCTIONS
# Every parser is supposed to have the following methods, even if they return None: # Every parser is supposed to have the following methods, even if they return None:
# read_book() # read_book()
# get_title() # get_title()
@@ -19,19 +24,42 @@ import database
# get_year() # get_year()
# get_cover_image() # get_cover_image()
# get_isbn() # get_isbn()
# get_tags()
# get_contents() - Should return a tuple with 0: TOC 1: special_settings (dict) # get_contents() - Should return a tuple with 0: TOC 1: special_settings (dict)
# Parsers for files containing only images need to return only images_only = True # Parsers for files containing only images need to return only images_only = True
# TODO
# Maybe shift to insert or replace instead of hash checking
# See if you want to include a hash of the book's name and author
# Change thread niceness
import io
import os
import time
import pickle
import hashlib
import threading
from multiprocessing import Pool, Manager
from PyQt5 import QtCore, QtGui
import database
from parsers.epub import ParseEPUB from parsers.epub import ParseEPUB
from parsers.cbz import ParseCBZ from parsers.cbz import ParseCBZ
from parsers.cbr import ParseCBR from parsers.cbr import ParseCBR
available_parsers = ['epub', 'cbz', 'cbr'] sorter = {
'epub': ParseEPUB,
'cbz': ParseCBZ,
'cbr': ParseCBR}
available_parsers = [i for i in sorter]
progressbar = None # This is populated by __main__ progressbar = None # This is populated by __main__
progress_emitter = None # This is to be made into a global variable
# This is for thread safety
class UpdateProgress(QtCore.QObject): class UpdateProgress(QtCore.QObject):
# This is for thread safety
update_signal = QtCore.pyqtSignal(int) update_signal = QtCore.pyqtSignal(int)
def connect_to_progressbar(self): def connect_to_progressbar(self):
@@ -42,7 +70,7 @@ class UpdateProgress(QtCore.QObject):
class BookSorter: class BookSorter:
def __init__(self, file_list, mode, database_path, temp_dir=None): def __init__(self, file_list, mode, database_path, auto_tags=True, temp_dir=None):
# Have the GUI pass a list of files straight to here # Have the GUI pass a list of files straight to here
# Then, on the basis of what is needed, pass the # Then, on the basis of what is needed, pass the
# filenames to the requisite functions # filenames to the requisite functions
@@ -51,17 +79,20 @@ class BookSorter:
# Caching upon closing # Caching upon closing
self.file_list = [i for i in file_list if os.path.exists(i)] self.file_list = [i for i in file_list if os.path.exists(i)]
self.statistics = [0, (len(file_list))] self.statistics = [0, (len(file_list))]
self.all_books = {}
self.hashes = [] self.hashes = []
self.mode = mode self.mode = mode
self.database_path = database_path self.database_path = database_path
self.auto_tags = auto_tags
self.temp_dir = temp_dir self.temp_dir = temp_dir
if database_path: if database_path:
self.database_hashes() self.database_hashes()
self.threading_completed = []
self.queue = Manager().Queue()
self.processed_books = []
if self.mode == 'addition': if self.mode == 'addition':
self.progress_emitter = UpdateProgress() progress_object_generator()
self.progress_emitter.connect_to_progressbar()
def database_hashes(self): def database_hashes(self):
# TODO # TODO
@@ -101,32 +132,21 @@ class BookSorter:
# This should speed up addition for larger files # This should speed up addition for larger files
# without compromising the integrity of the process # without compromising the integrity of the process
first_bytes = current_book.read(1024 * 32) # First 32KB of the file first_bytes = current_book.read(1024 * 32) # First 32KB of the file
salt = 'Caesar si viveret, ad remum dareris'.encode()
first_bytes += salt
file_md5 = hashlib.md5(first_bytes).hexdigest() file_md5 = hashlib.md5(first_bytes).hexdigest()
if self.mode == 'addition': # Update the progress queue
self.statistics[0] += 1 self.queue.put(filename)
self.progress_emitter.update_progress(
self.statistics[0] * 100 // self.statistics[1])
# IF the file is NOT being loaded into the reader,
# Do not allow addition in case the file is dupicated in the directory
# OR is already in the database
# This should not get triggered in reading mode # This should not get triggered in reading mode
if (self.mode == 'addition' # IF the file is NOT being loaded into the reader,
and (file_md5 in self.all_books.items() or file_md5 in self.hashes)): # Do not allow addition in case the file
# is already in the database
if self.mode == 'addition' and file_md5 in self.hashes:
return return
# ___________SORTING TAKES PLACE HERE___________
sorter = {
'epub': ParseEPUB,
'cbz': ParseCBZ,
'cbr': ParseCBR
}
file_extension = os.path.splitext(filename)[1][1:] file_extension = os.path.splitext(filename)[1][1:]
try: try:
# Get the requisite parser from the sorter dict
book_ref = sorter[file_extension](filename, self.temp_dir, file_md5) book_ref = sorter[file_extension](filename, self.temp_dir, file_md5)
except KeyError: except KeyError:
print(filename + ' has an unsupported extension') print(filename + ' has an unsupported extension')
@@ -137,7 +157,7 @@ class BookSorter:
book_ref.read_book() book_ref.read_book()
if book_ref.book: if book_ref.book:
title = book_ref.get_title().title() title = book_ref.get_title()
author = book_ref.get_author() author = book_ref.get_author()
if not author: if not author:
@@ -150,22 +170,32 @@ class BookSorter:
isbn = book_ref.get_isbn() isbn = book_ref.get_isbn()
# Different modes require different values tags = None
if self.mode == 'addition': if self.auto_tags:
cover_image_raw = book_ref.get_cover_image() tags = book_ref.get_tags()
if cover_image_raw:
# Reduce the size of the incoming image
cover_image = resize_image(cover_image_raw)
else:
cover_image = None
self.all_books[file_md5] = { this_book = {}
this_book[file_md5] = {
'title': title, 'title': title,
'author': author, 'author': author,
'year': year, 'year': year,
'isbn': isbn, 'isbn': isbn,
'hash': file_md5,
'path': filename, 'path': filename,
'cover_image': cover_image} 'tags': tags}
# Different modes require different values
if self.mode == 'addition':
# Reduce the size of the incoming image
# if one is found
cover_image_raw = book_ref.get_cover_image()
if cover_image_raw:
cover_image = resize_image(cover_image_raw)
else:
cover_image = None
this_book[file_md5]['cover_image'] = cover_image
if self.mode == 'reading': if self.mode == 'reading':
all_content = book_ref.get_contents() all_content = book_ref.get_contents()
@@ -183,25 +213,65 @@ class BookSorter:
content['Invalid'] = 'Possible Parse Error' content['Invalid'] = 'Possible Parse Error'
position = self.database_position(file_md5) position = self.database_position(file_md5)
self.all_books[file_md5] = {
'title': title,
'author': author,
'year': year,
'isbn': isbn,
'hash': file_md5,
'path': filename,
'position': position,
'content': content,
'images_only': images_only}
this_book[file_md5]['position'] = position
this_book[file_md5]['content'] = content
this_book[file_md5]['images_only'] = images_only
return this_book
def read_progress(self):
while True:
processed_file = self.queue.get()
self.threading_completed.append(processed_file)
total_number = len(self.file_list)
completed_number = len(self.threading_completed)
if progress_emitter: # Skip update in reading mode
progress_emitter.update_progress(
completed_number * 100 // total_number)
if total_number == completed_number:
break
def initiate_threads(self): def initiate_threads(self):
def pool_creator():
_pool = Pool(5) _pool = Pool(5)
_pool.map(self.read_book, self.file_list) self.processed_books = _pool.map(
self.read_book, self.file_list)
_pool.close() _pool.close()
_pool.join() _pool.join()
return self.all_books start_time = time.time()
worker_thread = threading.Thread(target=pool_creator)
progress_thread = threading.Thread(target=self.read_progress)
worker_thread.start()
progress_thread.start()
worker_thread.join()
progress_thread.join(timeout=.5)
return_books = {}
# Exclude None returns generated in case of duplication / parse errors
self.processed_books = [i for i in self.processed_books if i]
for i in self.processed_books:
for j in i:
return_books[j] = i[j]
del self.processed_books
print('Finished processing in', time.time() - start_time)
return return_books
def progress_object_generator():
# This has to be kept separate from the BookSorter class because
# the QtObject inheritance disallows pickling
global progress_emitter
progress_emitter = UpdateProgress()
progress_emitter.connect_to_progressbar()
def resize_image(cover_image_raw): def resize_image(cover_image_raw):

View File

@@ -1,6 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
import pathlib
from multiprocessing.dummy import Pool from multiprocessing.dummy import Pool
from PyQt5 import QtCore from PyQt5 import QtCore
@@ -26,38 +43,72 @@ class BackGroundTabUpdate(QtCore.QThread):
class BackGroundBookAddition(QtCore.QThread): class BackGroundBookAddition(QtCore.QThread):
def __init__(self, parent_window, file_list, database_path, parent=None): def __init__(self, file_list, database_path, prune_required, parent=None):
super(BackGroundBookAddition, self).__init__(parent) super(BackGroundBookAddition, self).__init__(parent)
self.parent_window = parent_window
self.file_list = file_list self.file_list = file_list
self.parent = parent
self.database_path = database_path self.database_path = database_path
self.prune_required = prune_required
def run(self): def run(self):
books = sorter.BookSorter( books = sorter.BookSorter(
self.file_list, self.file_list,
'addition', 'addition',
self.database_path) self.database_path,
self.parent.settings['auto_tags'])
parsed_books = books.initiate_threads() parsed_books = books.initiate_threads()
self.parent.lib_ref.generate_model('addition', parsed_books)
if self.prune_required:
self.parent.lib_ref.prune_models(self.file_list)
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books) database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
self.parent_window.lib_ref.generate_model('addition', parsed_books)
class BackGroundBookDeletion(QtCore.QThread):
def __init__(self, hash_list, database_path, parent=None):
super(BackGroundBookDeletion, self).__init__(parent)
self.parent = parent
self.hash_list = hash_list
self.database_path = database_path
def run(self):
database.DatabaseFunctions(
self.database_path).delete_from_database('Hash', self.hash_list)
class BackGroundBookSearch(QtCore.QThread): class BackGroundBookSearch(QtCore.QThread):
def __init__(self, parent_window, data_list, parent=None): # TODO
# Change existing sorter module functionality to handle preset tags
# Change database to accomodate User Tags, Folder Name, Folder Tags
def __init__(self, data_list, parent=None):
super(BackGroundBookSearch, self).__init__(parent) super(BackGroundBookSearch, self).__init__(parent)
self.parent_window = parent_window self.parent = parent
self.data_list = data_list self.valid_files = []
self.valid_files = [] # A tuple should get added to this containing the
# file path and the folder name / tags # Filter for checked directories
self.valid_directories = [
[i[0], i[1], i[2]] for i in data_list if i[3] == QtCore.Qt.Checked]
self.unwanted_directories = [
pathlib.Path(i[0]) for i in data_list if i[3] == QtCore.Qt.Unchecked]
def run(self): def run(self):
def is_wanted(directory):
directory_parents = pathlib.Path(directory).parents
for i in self.unwanted_directories:
if i in directory_parents:
return False
return True
def traverse_directory(incoming_data): def traverse_directory(incoming_data):
root_directory = incoming_data[0] root_directory = incoming_data[0]
folder_name = incoming_data[1] folder_name = incoming_data[1]
folder_tags = incoming_data[2] folder_tags = incoming_data[2]
for directory, subdir, files in os.walk(root_directory): for directory, subdirs, files in os.walk(root_directory, topdown=True):
# Black magic fuckery
# Skip subdir tree in case it's not wanted
subdirs[:] = [d for d in subdirs if is_wanted(os.path.join(directory, d))]
for filename in files: for filename in files:
if os.path.splitext(filename)[1][1:] in sorter.available_parsers: if os.path.splitext(filename)[1][1:] in sorter.available_parsers:
self.valid_files.append( self.valid_files.append(
@@ -65,17 +116,9 @@ class BackGroundBookSearch(QtCore.QThread):
def initiate_threads(): def initiate_threads():
_pool = Pool(5) _pool = Pool(5)
_pool.map(traverse_directory, self.data_list) _pool.map(traverse_directory, self.valid_directories)
_pool.close() _pool.close()
_pool.join() _pool.join()
initiate_threads() initiate_threads()
print(len(self.valid_files), 'books found')
# TODO
# Change existing sorter module functionality to handle
# preset tags
# Change database to accomodate User Tags, Folder Name, Folder Tags
# self.valid_files will now be added to the database
# and models will be rebuilt accordingly
# Coming soon to a commit near you

View File

@@ -1,9 +1,31 @@
#!usr/bin/env python3 #!usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Reading modes
# Double page, Continuous etc
# Especially for comics
import os import os
from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5 import QtWidgets, QtGui, QtCore
from resources import resources, pie_chart from resources import pie_chart
class BookToolBar(QtWidgets.QToolBar): class BookToolBar(QtWidgets.QToolBar):
@@ -31,6 +53,10 @@ class BookToolBar(QtWidgets.QToolBar):
self.fullscreenButton = QtWidgets.QAction( self.fullscreenButton = QtWidgets.QAction(
QtGui.QIcon.fromTheme('view-fullscreen'), QtGui.QIcon.fromTheme('view-fullscreen'),
'Fullscreen', self) 'Fullscreen', self)
self.bookmarkButton = QtWidgets.QAction(
QtGui.QIcon.fromTheme('bookmarks'),
'Bookmark', self)
self.bookmarkButton.setObjectName('bookmarkButton')
self.resetProfile = QtWidgets.QAction( self.resetProfile = QtWidgets.QAction(
QtGui.QIcon.fromTheme('view-refresh'), QtGui.QIcon.fromTheme('view-refresh'),
'Reset profile', self) 'Reset profile', self)
@@ -40,6 +66,8 @@ class BookToolBar(QtWidgets.QToolBar):
self.fontButton.setCheckable(True) self.fontButton.setCheckable(True)
self.fontButton.triggered.connect(self.toggle_font_settings) self.fontButton.triggered.connect(self.toggle_font_settings)
self.addSeparator() self.addSeparator()
self.addAction(self.bookmarkButton)
self.bookmarkButton.setCheckable(True)
self.addAction(self.fullscreenButton) self.addAction(self.fullscreenButton)
# Font modification # Font modification
@@ -191,6 +219,7 @@ class BookToolBar(QtWidgets.QToolBar):
self.searchBarAction = self.addWidget(self.searchBar) self.searchBarAction = self.addWidget(self.searchBar)
self.bookActions = [ self.bookActions = [
self.bookmarkButton,
self.fullscreenButton, self.fullscreenButton,
self.tocBoxAction, self.tocBoxAction,
self.searchBarAction] self.searchBarAction]
@@ -269,6 +298,11 @@ class LibraryToolBar(QtWidgets.QToolBar):
QtGui.QIcon.fromTheme('table'), 'View as table', self) QtGui.QIcon.fromTheme('table'), 'View as table', self)
self.tableViewButton.setCheckable(True) self.tableViewButton.setCheckable(True)
self.libraryFilterButton = QtWidgets.QToolButton(self)
self.libraryFilterButton.setIcon(QtGui.QIcon.fromTheme('view-readermode'))
self.libraryFilterButton.setText('Filter library')
self.libraryFilterButton.setToolTip('Filter library')
# Auto unchecks the other QToolButton in case of clicking # Auto unchecks the other QToolButton in case of clicking
self.viewButtons = QtWidgets.QActionGroup(self) self.viewButtons = QtWidgets.QActionGroup(self)
self.viewButtons.setExclusive(True) self.viewButtons.setExclusive(True)
@@ -282,6 +316,8 @@ class LibraryToolBar(QtWidgets.QToolBar):
self.addAction(self.coverViewButton) self.addAction(self.coverViewButton)
self.addAction(self.tableViewButton) self.addAction(self.tableViewButton)
self.addSeparator() self.addSeparator()
self.addWidget(self.libraryFilterButton)
self.addSeparator()
self.addAction(self.settingsButton) self.addAction(self.settingsButton)
# Filter # Filter
@@ -346,8 +382,10 @@ class Tab(QtWidgets.QWidget):
self.parent = parent self.parent = parent
self.metadata = metadata # Save progress data into this dictionary self.metadata = metadata # Save progress data into this dictionary
self.gridLayout = QtWidgets.QGridLayout(self) self.masterLayout = QtWidgets.QHBoxLayout(self)
self.gridLayout.setObjectName("gridLayout") self.horzLayout = QtWidgets.QSplitter(self)
self.horzLayout.setOrientation(QtCore.Qt.Horizontal)
self.masterLayout.addWidget(self.horzLayout)
position = self.metadata['position'] position = self.metadata['position']
@@ -374,6 +412,7 @@ class Tab(QtWidgets.QWidget):
self.contentView.loadImage(chapter_content) self.contentView.loadImage(chapter_content)
else: else:
self.contentView = PliantQTextBrowser(self.window()) self.contentView = PliantQTextBrowser(self.window())
# print(dir(self.contentView.document())) ## TODO USE this for modifying formatting and searching
relative_path_root = os.path.join( relative_path_root = os.path.join(
self.window().temp_dir.path(), self.metadata['hash']) self.window().temp_dir.path(), self.metadata['hash'])
@@ -393,9 +432,20 @@ class Tab(QtWidgets.QWidget):
self.contentView.setHorizontalScrollBarPolicy( self.contentView.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOff) QtCore.Qt.ScrollBarAlwaysOff)
# Create the dock widget for context specific display
self.dockWidget = QtWidgets.QDockWidget(self)
self.dockWidget.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
self.dockWidget.setFloating(False)
self.dockListWidget = QtWidgets.QListWidget()
self.dockListWidget.setResizeMode(QtWidgets.QListWidget.Adjust)
self.dockListWidget.setMaximumWidth(350)
self.dockWidget.setWidget(self.dockListWidget)
self.dockWidget.hide()
self.generate_keyboard_shortcuts() self.generate_keyboard_shortcuts()
self.gridLayout.addWidget(self.contentView, 0, 0, 1, 1) self.horzLayout.addWidget(self.contentView)
self.horzLayout.addWidget(self.dockWidget)
title = self.metadata['title'] title = self.metadata['title']
self.parent.addTab(self, title) self.parent.addTab(self, title)
@@ -462,6 +512,11 @@ class Tab(QtWidgets.QWidget):
self.contentView.clear() self.contentView.clear()
self.contentView.setHtml(required_content) self.contentView.setHtml(required_content)
# TODO
# This here. Use it for stuff.
# self.contentView.document().begin().blockFormat().setLineHeight(1000, QtGui.QTextBlockFormat.FixedHeight)
# self.contentView.document().end().blockFormat().setLineHeight(1000, QtGui.QTextBlockFormat.FixedHeight)
def format_view(self, font, font_size, foreground, background, padding): def format_view(self, font, font_size, foreground, background, padding):
if self.are_we_doing_images_only: if self.are_we_doing_images_only:
# Tab color does not need to be set separately in case # Tab color does not need to be set separately in case
@@ -473,11 +528,22 @@ class Tab(QtWidgets.QWidget):
self.contentView.resizeEvent() self.contentView.resizeEvent()
else: else:
# print(dir(self.contentView.document().begin().blockFormat())) ## TODO Line Height here
# self.contentView.document().begin().blockFormat().setLineHeight(1000, QtGui.QTextBlockFormat.FixedHeight)
# self.contentView.document().end().blockFormat().setLineHeight(1000, QtGui.QTextBlockFormat.FixedHeight)
self.contentView.setViewportMargins(padding, 0, padding, 0) self.contentView.setViewportMargins(padding, 0, padding, 0)
self.contentView.setStyleSheet( self.contentView.setStyleSheet(
"QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format( "QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format(
font, font_size, foreground.name(), background.name())) font, font_size, foreground.name(), background.name()))
def toggle_bookmarks(self):
self.dockWidget.setWindowTitle('Bookmarks')
if self.dockWidget.isVisible():
self.dockWidget.hide()
else:
self.dockWidget.show()
def sneaky_change(self): def sneaky_change(self):
direction = -1 direction = -1
if self.sender().objectName() == 'nextChapter': if self.sender().objectName() == 'nextChapter':
@@ -661,6 +727,7 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, temp_dir, parent=None): def __init__(self, temp_dir, parent=None):
super(LibraryDelegate, self).__init__(parent) super(LibraryDelegate, self).__init__(parent)
self.temp_dir = temp_dir self.temp_dir = temp_dir
self.parent = parent
def paint(self, painter, option, index): def paint(self, painter, option, index):
# This is a hint for the future # This is a hint for the future
@@ -673,27 +740,27 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
position = index.data(QtCore.Qt.UserRole + 7) position = index.data(QtCore.Qt.UserRole + 7)
# The shadow pixmap currently is set to 420 x 600 # The shadow pixmap currently is set to 420 x 600
# Only draw the cover shadow in case the setting is enabled
if self.parent.settings['cover_shadows']:
shadow_pixmap = QtGui.QPixmap() shadow_pixmap = QtGui.QPixmap()
shadow_pixmap.load(':/images/gray-shadow.png') shadow_pixmap.load(':/images/gray-shadow.png')
shadow_pixmap = shadow_pixmap.scaled(160, 230, QtCore.Qt.IgnoreAspectRatio) shadow_pixmap = shadow_pixmap.scaled(160, 230, QtCore.Qt.IgnoreAspectRatio)
shadow_x = option.rect.topLeft().x() + 10 shadow_x = option.rect.topLeft().x() + 10
shadow_y = option.rect.topLeft().y() - 5 shadow_y = option.rect.topLeft().y() - 5
painter.setOpacity(.7)
painter.drawPixmap(shadow_x, shadow_y, shadow_pixmap)
painter.setOpacity(1)
if not file_exists: if not file_exists:
painter.setOpacity(.7) painter.setOpacity(.7)
painter.drawPixmap(shadow_x, shadow_y, shadow_pixmap)
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
painter.setOpacity(1)
read_icon = pie_chart.pixmapper(-1, None, None, 36) read_icon = pie_chart.pixmapper(-1, None, None, 36)
x_draw = option.rect.bottomRight().x() - 30 x_draw = option.rect.bottomRight().x() - 30
y_draw = option.rect.bottomRight().y() - 35 y_draw = option.rect.bottomRight().y() - 35
painter.drawPixmap(x_draw, y_draw, read_icon) painter.drawPixmap(x_draw, y_draw, read_icon)
painter.setOpacity(1)
return return
painter.setOpacity(.8)
painter.drawPixmap(shadow_x, shadow_y, shadow_pixmap)
painter.setOpacity(1)
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
if position: if position:
current_chapter = position['current_chapter'] current_chapter = position['current_chapter']