From 01d1be9ddc5d3bf70f7b6a5e42c582590103e49f Mon Sep 17 00:00:00 2001 From: BasioMeusPuga Date: Thu, 28 Dec 2017 18:27:42 +0530 Subject: [PATCH] Fairly substantial rewrite. --- AUTHORS | 1 + TODO | 15 +- __main__.py | 317 ++++++++++++++++++++++++++++-------- database.py | 78 ++++++--- library.py | 112 +++++++++---- models.py | 180 ++++++++++++++++++-- parsers/cbr.py | 24 ++- parsers/cbz.py | 24 ++- parsers/epub.py | 39 +++-- resources/about.html | 14 ++ resources/mainwindow.py | 5 +- resources/pie_chart.py | 3 +- resources/raw/blank.png | Bin 0 -> 663 bytes resources/raw/main.ui | 9 +- resources/raw/resources.qrc | 1 + resources/raw/settings.ui | 181 ++++++++------------ resources/resources.py | 111 ++++++++----- resources/settingswindow.py | 100 +++++------- settings.py | 90 ++++++---- settingsdialog.py | 303 ++++++++++++++++++++++------------ sorter.py | 184 ++++++++++++++------- threaded.py | 83 +++++++--- widgets.py | 97 +++++++++-- 23 files changed, 1360 insertions(+), 611 deletions(-) create mode 100644 AUTHORS create mode 100644 resources/about.html create mode 100644 resources/raw/blank.png diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..401c117 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +BasioMeusPuga diff --git a/TODO b/TODO index 05e6f47..76f3497 100644 --- a/TODO +++ b/TODO @@ -1,13 +1,12 @@ TODO Options: Automatic library management + ✓ Recursive file addition Auto deletion - Recursive file addition Add only one file type if multiple are present - Remember files - Check files (hashes) upon restart - Show what on startup - Draw shadows + ✓ Remember files + ✓ Check files (hashes) upon restart + ✓ Draw shadows Library: ✓ sqlite3 for cover images cache ✓ sqlite3 for storing metadata @@ -19,6 +18,7 @@ TODO ✓ Tie file deletion and tab closing to model updates ✓ Create separate thread for parser - Show progress in main window ? Create emblem per filetype + Memory management Table view Ignore a / the / numbers for sorting purposes Put the path in the scope of the search @@ -27,6 +27,7 @@ TODO Information dialog widget Context menu: Cache, Read, Edit database, delete, Mark read/unread Set focus to newly added file + Add capability to sort by new Reading: ✓ Drop down for TOC ✓ Override the keypress event of the textedit @@ -45,6 +46,7 @@ TODO Record progress Pagination Set context menu for definitions and the like + Hide progressbar Filetypes: ✓ cbz, cbr support ✓ Keep font settings enabled but only for background color @@ -59,4 +61,5 @@ TODO Get ISBN using python-isbnlib Other: ✓ Define every widget in code - ✓ Include icons for emblems \ No newline at end of file + ✓ Include icons for emblems + Shift to logging instead of print statements \ No newline at end of file diff --git a/__main__.py b/__main__.py index e116172..f44edfc 100755 --- a/__main__.py +++ b/__main__.py @@ -1,5 +1,24 @@ #!/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 . + +# TODO +# Consider using sender().text() instead of sender().objectName() + import os import sys @@ -8,9 +27,9 @@ from PyQt5 import QtWidgets, QtGui, QtCore import sorter import database -from resources import mainwindow +from resources import mainwindow, resources from widgets import LibraryToolBar, BookToolBar, Tab, LibraryDelegate -from threaded import BackGroundTabUpdate, BackGroundBookAddition +from threaded import BackGroundTabUpdate, BackGroundBookAddition, BackGroundBookDeletion from library import Library from settings import Settings @@ -23,8 +42,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.setupUi(self) # Empty variables that will be infested soon - self.current_view = None - self.last_open_books = None + self.settings = {} self.last_open_tab = None self.last_open_path = None self.thread = None # Background Thread @@ -32,8 +50,17 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.display_profiles = None self.current_profile_index = None self.database_path = None - self.table_header_sizes = None - self.settings_dialog_settings = None + self.library_filter_menu = 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 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 self.settings_dialog = SettingsUI(self) - # Create and right align the statusbar label widget - self.statusMessage = QtWidgets.QLabel() + # Statusbar widgets self.statusMessage.setObjectName('statusMessage') self.statusBar.addPermanentWidget(self.statusMessage) self.sorterProgress = QtWidgets.QProgressBar() + self.sorterProgress.setMaximumWidth(300) self.sorterProgress.setObjectName('sorterProgress') sorter.progressbar = self.sorterProgress # This is so that updates can be # connected to setValue self.statusBar.addWidget(self.sorterProgress) 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 self.temp_dir = QtCore.QTemporaryDir() # Init the Library self.lib_ref = Library(self) + # Toolbar display + # Maybe make this a persistent option + self.settings['show_toolbars'] = True + # Library toolbar - self.libraryToolBar = LibraryToolBar(self) self.libraryToolBar.addButton.triggered.connect(self.add_books) self.libraryToolBar.deleteButton.triggered.connect(self.delete_books) 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_table_proxy_model) self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodel) - - if self.current_view == 0: - self.libraryToolBar.coverViewButton.trigger() - elif self.current_view == 1: - self.libraryToolBar.tableViewButton.trigger() - + self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.addToolBar(self.libraryToolBar) + if self.settings['current_view'] == 0: + self.libraryToolBar.coverViewButton.trigger() + else: + self.libraryToolBar.tableViewButton.trigger() + # Book toolbar - self.bookToolBar = BookToolBar(self) + self.bookToolBar.bookmarkButton.triggered.connect(self.toggle_dock_widget) self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen) 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) 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.setObjectName('reloadLibrary') self.reloadLibrary.setAutoRaise(True) - self.reloadLibrary.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.reloadLibrary.triggered.connect(self.switch_library_view) + self.reloadLibrary.clicked.connect(self.settings_dialog.start_library_scan) + # self.reloadLibrary.clicked.connect(self.cull_covers) # TODO self.tabWidget.tabBar().setTabButton( 0, QtWidgets.QTabBar.RightSide, self.reloadLibrary) @@ -140,7 +187,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.listView.setMouseTracking(True) self.listView.verticalScrollBar().setSingleStep(9) 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 self.tableView.doubleClicked.connect(self.library_doubleclick) @@ -148,11 +195,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): QtWidgets.QHeaderView.Interactive) self.tableView.horizontalHeader().setSortIndicator(0, QtCore.Qt.AscendingOrder) self.tableView.horizontalHeader().setHighlightSections(False) - if self.table_header_sizes: - for count, i in enumerate(self.table_header_sizes): + if self.settings['main_window_headers']: + for count, i in enumerate(self.settings['main_window_headers']): self.tableView.horizontalHeader().resizeSection(count, int(i)) self.tableView.horizontalHeader().setStretchLastSection(True) - self.tableView.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch) # Keyboard shortcuts 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. # Then set the value to None for the next run - self.open_files(self.last_open_books) - self.last_open_books = None + self.open_files(self.settings['last_open_books']) + + # 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): if event: @@ -206,58 +278,95 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): def add_books(self): # 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( self, 'Open file', self.last_open_path, f'eBooks ({self.available_parsers})') - if opened_files[0]: - self.last_open_path = os.path.dirname(opened_files[0][0]) - self.sorterProgress.setVisible(True) - self.statusMessage.setText('Adding books...') - self.thread = BackGroundBookAddition(self, opened_files[0], self.database_path) - self.thread.finished.connect(self.move_on) - self.thread.start() + if not opened_files[0]: + return - def move_on(self): - self.sorterProgress.setVisible(False) - self.lib_ref.create_table_model() - self.lib_ref.create_proxymodel() + self.settings_dialog.okButton.setEnabled(False) + self.reloadLibrary.setEnabled(False) + + self.last_open_path = os.path.dirname(opened_files[0][0]) + self.sorterProgress.setVisible(True) + self.statusMessage.setText('Adding books...') + self.thread = BackGroundBookAddition( + opened_files[0], self.database_path, False, self) + self.thread.finished.connect(self.move_on) + self.thread.start() def delete_books(self): # TODO - # Use maptosource() here to get the view_model - # indices selected in the listView # Implement this for the tableview # 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() - if selected_books: - def ifcontinue(box_button): - if box_button.text() == '&Yes': - selected_hashes = [] - for i in selected_books: - data = i.data(QtCore.Qt.UserRole + 3) - selected_hashes.append(data['hash']) + # Get a list of QItemSelection objects + # What we're interested in is the indexes()[0] in each of them + # That gives a list of indexes from the view model + selected_books = self.lib_ref.proxy_model.mapSelectionToSource( + self.listView.selectionModel().selection()) - database.DatabaseFunctions( - self.database_path).delete_from_database(selected_hashes) + if not selected_books: + return - self.lib_ref.generate_model('build') - self.lib_ref.create_table_model() - self.lib_ref.create_proxymodel() + # Deal with message box selection + def ifcontinue(box_button): + if box_button.text() != '&Yes': + return - 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_() + # Generate list of selected indexes and deletable 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.create_table_model() + self.lib_ref.create_proxymodel() def switch_library_view(self): if self.libraryToolBar.coverViewButton.isChecked(): @@ -273,8 +382,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): if self.tabWidget.currentIndex() == 0: self.resizeEvent() - self.bookToolBar.hide() - self.libraryToolBar.show() + if self.settings['show_toolbars']: + self.bookToolBar.hide() + self.libraryToolBar.show() if self.lib_ref.proxy_model: # Making the proxy model available doesn't affect @@ -282,8 +392,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.statusMessage.setText( str(self.lib_ref.proxy_model.rowCount()) + ' Books') else: - self.bookToolBar.show() - self.libraryToolBar.hide() + + if self.settings['show_toolbars']: + self.bookToolBar.show() + self.libraryToolBar.hide() current_metadata = self.tabWidget.widget( self.tabWidget.currentIndex()).metadata @@ -375,14 +487,24 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): current_tab_widget = self.tabWidget.widget(current_tab) 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() 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) 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) # 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, 'reading', self.database_path, + True, self.temp_dir.path()).initiate_threads() found_a_focusable_tab = False @@ -609,19 +732,69 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): else: 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): # All tabs must be iterated upon here self.hide() self.settings_dialog.hide() self.temp_dir.remove() - self.last_open_books = [] - if self.tabWidget.count() > 1: + self.settings['last_open_books'] = [] + if self.tabWidget.count() > 1 and self.settings['remember_files']: all_metadata = [] for i in range(1, self.tabWidget.count()): 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) Settings(self).save_settings() diff --git a/database.py b/database.py index aff1616..03c6e7f 100644 --- a/database.py +++ b/database.py @@ -1,5 +1,21 @@ #!/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 . + import os import pickle import sqlite3 @@ -16,13 +32,20 @@ class DatabaseInit: def create_database(self): # TODO - # Add a separate column for directory tags + # Add separate columns for: + # directory tags + # bookmarks + # date added + # addition mode self.database.execute( "CREATE TABLE books \ (id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, \ Path TEXT, Position BLOB, ISBN TEXT, Tags TEXT, Hash TEXT, CoverImage BLOB)") + + # CheckState is the standard QtCore.Qt.Checked / Unchecked 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.close() @@ -33,15 +56,22 @@ class DatabaseFunctions: self.database = sqlite3.connect(database_path) def set_library_paths(self, data_iterable): + # TODO + # INSERT OR REPLACE is not working + # So this is the old fashion kitchen sink approach + + self.database.execute("DELETE FROM directories") + for i in data_iterable: path = i[0] name = i[1] tags = i[2] + is_checked = i[3] sql_command = ( - "INSERT OR REPLACE INTO directories (ID, Path, Name, Tags)\ - VALUES ((SELECT ID FROM directories WHERE Path = ?), ?, ?, ?)") - self.database.execute(sql_command, [path, path, name, tags]) + "INSERT OR REPLACE INTO directories (ID, Path, Name, Tags, CheckState)\ + VALUES ((SELECT ID FROM directories WHERE Path = ?), ?, ?, ?, ?)") + self.database.execute(sql_command, [path, path, name, tags, is_checked]) self.database.commit() self.close_database() @@ -61,9 +91,15 @@ class DatabaseFunctions: path = i[1]['path'] cover = i[1]['cover_image'] 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 = ( - "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 if cover: @@ -72,7 +108,7 @@ class DatabaseFunctions: self.database.execute( sql_command_add, [title, author, year, - path, isbn, book_hash, cover_insert]) + path, isbn, tags, book_hash, cover_insert]) self.database.commit() self.close_database() @@ -121,8 +157,7 @@ class DatabaseFunctions: else: return None - # except sqlite3.OperationalError: - except KeyError: + except (KeyError, sqlite3.OperationalError): print('SQLite is in rebellion, Commander') self.close_database() @@ -136,7 +171,9 @@ class DatabaseFunctions: sql_command = "UPDATE books SET Position = ? WHERE Hash = ?" 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: print('SQLite is in rebellion, Commander') return @@ -144,22 +181,19 @@ class DatabaseFunctions: self.database.commit() self.close_database() - def delete_from_database(self, file_hashes): - # file_hashes is expected as a list that will be iterated upon - # This should enable multiple deletion + def delete_from_database(self, column_name, target_data): + # target_data is an iterable - first = file_hashes[0] - sql_command = f"DELETE FROM books WHERE Hash = '{first}'" - - if len(file_hashes) > 1: - for i in file_hashes[1:]: - sql_command += f" OR Hash = '{i}'" - - self.database.execute(sql_command) + if column_name == '*': + self.database.execute('DELETE FROM books') + else: + sql_command = f'DELETE FROM books WHERE {column_name} = ?' + for i in target_data: + self.database.execute(sql_command, (i,)) self.database.commit() self.close_database() - + def close_database(self): self.database.execute("VACUUM") self.database.close() diff --git a/library.py b/library.py index b807688..4f70b7e 100644 --- a/library.py +++ b/library.py @@ -1,16 +1,36 @@ #!/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 . + +# TODO +# Implement filterAcceptsRow for the view_model + + import os import pickle import database -from PyQt5 import QtWidgets, QtGui, QtCore -from models import LibraryItemModel, MostExcellentTableModel, TableProxyModel +from PyQt5 import QtGui, QtCore +from models import MostExcellentTableModel, TableProxyModel class Library: def __init__(self, parent): - self.parent_window = parent + self.parent = parent self.view_model = None self.proxy_model = None self.table_model = None @@ -18,16 +38,12 @@ class Library: self.table_rows = [] 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': self.table_rows = [] - self.view_model = LibraryItemModel() + self.view_model = QtGui.QStandardItemModel() books = database.DatabaseFunctions( - self.parent_window.database_path).fetch_data( + self.parent.database_path).fetch_data( ('*',), 'books', {'Title': ''}, @@ -43,20 +59,19 @@ class Library: # database using background threads books = [] - for i in parsed_books: - parsed_title = parsed_books[i]['title'] - parsed_author = parsed_books[i]['author'] - parsed_year = parsed_books[i]['year'] - parsed_path = parsed_books[i]['path'] - parsed_position = None - parsed_isbn = parsed_books[i]['isbn'] - parsed_tags = None - parsed_hash = i - parsed_cover = parsed_books[i]['cover_image'] + for i in parsed_books.items(): + # Scheme + # 1: Title, 2: Author, 3: Year, 4: Path + # 5: Position, 6: isbn, 7: Tags, 8: Hash + # 9: CoverImage + + _tags = i[1]['tags'] + if _tags: + _tags = ', '.join([j for j in _tags if j]) books.append([ - None, parsed_title, parsed_author, parsed_year, parsed_path, - parsed_position, parsed_isbn, parsed_tags, parsed_hash, parsed_cover]) + None, i[1]['title'], i[1]['author'], i[1]['year'], i[1]['path'], + None, i[1]['isbn'], _tags, i[0], i[1]['cover_image']]) else: return @@ -134,7 +149,7 @@ class Library: def create_table_model(self): table_header = ['Title', 'Author', 'Status', 'Year', 'Tags'] 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() def create_table_proxy_model(self): @@ -142,38 +157,73 @@ class Library: self.table_proxy_model.setSourceModel(self.table_model) self.table_proxy_model.setSortCaseSensitivity(False) self.table_proxy_model.sort(0, QtCore.Qt.AscendingOrder) - self.parent_window.tableView.setModel(self.table_proxy_model) - self.parent_window.tableView.horizontalHeader().setSortIndicator( + self.parent.tableView.setModel(self.table_proxy_model) + self.parent.tableView.horizontalHeader().setSortIndicator( 0, QtCore.Qt.AscendingOrder) + self.update_table_proxy_model() def update_table_proxy_model(self): self.table_proxy_model.invalidateFilter() 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 # text in the line edit changes. So I guess it is needed. self.table_proxy_model.setFilterFixedString( - self.parent_window.libraryToolBar.searchBar.text()) + self.parent.libraryToolBar.searchBar.text()) def create_proxymodel(self): self.proxy_model = QtCore.QSortFilterProxyModel() self.proxy_model.setSourceModel(self.view_model) self.proxy_model.setSortCaseSensitivity(False) s = QtCore.QSize(160, 250) # Set icon sizing here - self.parent_window.listView.setIconSize(s) - self.parent_window.listView.setModel(self.proxy_model) + self.parent.listView.setIconSize(s) + self.parent.listView.setModel(self.proxy_model) self.update_proxymodel() def update_proxymodel(self): self.proxy_model.setFilterRole(QtCore.Qt.UserRole + 4) self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) 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') # Sorting according to roles and the drop down in the library 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) + + 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) diff --git a/models.py b/models.py index 535a204..9153f51 100644 --- a/models.py +++ b/models.py @@ -1,15 +1,33 @@ #!/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 . + +import pathlib + +from PyQt5 import QtCore, QtWidgets from resources import pie_chart -class LibraryItemModel(QtGui.QStandardItemModel, QtCore.QAbstractItemModel): +class ItemProxyModel(QtCore.QSortFilterProxyModel): + # TODO + # Implement filterAcceptsRow + def __init__(self, parent=None): - # We're using this to be able to access the match() method - super(LibraryItemModel, self).__init__(parent) + super(ItemProxyModel, self).__init__(parent) class MostExcellentTableModel(QtCore.QAbstractTableModel): @@ -39,7 +57,7 @@ class MostExcellentTableModel(QtCore.QAbstractTableModel): if not index.isValid(): 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 if self.temp_dir: if role == QtCore.Qt.DecorationRole and index.column() == 2: @@ -71,7 +89,8 @@ class MostExcellentTableModel(QtCore.QAbstractTableModel): 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()] return value @@ -84,8 +103,8 @@ class MostExcellentTableModel(QtCore.QAbstractTableModel): return None def flags(self, index): - # In case of the settings model, model column index 1+ are editable - if not self.temp_dir and index.column() != 0: + # This means only the Tags column is editable + if self.temp_dir and index.column() == 4: return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable else: # These are standard select but don't edit values @@ -124,10 +143,151 @@ class TableProxyModel(QtCore.QSortFilterProxyModel): model = self.sourceModel() 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: if self.filter_string in i: return True 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 diff --git a/parsers/cbr.py b/parsers/cbr.py index 0b183fe..ac851f9 100644 --- a/parsers/cbr.py +++ b/parsers/cbr.py @@ -1,5 +1,24 @@ #!/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 . + +# TODO +# Account for files with passwords + import os import time import collections @@ -49,7 +68,10 @@ class ParseCBR: return cover_image def get_isbn(self): - return None + return + + def get_tags(self): + return def get_contents(self): file_settings = { diff --git a/parsers/cbz.py b/parsers/cbz.py index 6456e7d..a512a9a 100644 --- a/parsers/cbz.py +++ b/parsers/cbz.py @@ -1,5 +1,24 @@ #!/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 . + +# TODO +# Account for files with passwords + import os import time import zipfile @@ -52,7 +71,10 @@ class ParseCBZ: return cover_image def get_isbn(self): - return None + return + + def get_tags(self): + return def get_contents(self): file_settings = { diff --git a/parsers/epub.py b/parsers/epub.py index 7529d0f..a7dcc51 100644 --- a/parsers/epub.py +++ b/parsers/epub.py @@ -1,12 +1,20 @@ #!/usr/bin/env python3 -# Every parser is supposed to have the following methods, even if they return None: -# read_book() -# get_title() -# get_author() -# get_year() -# get_cover_image() -# get_isbn() -# get_contents() - Should return a tuple with 0: TOC 1: Deletable temp_directory + +# 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 . import os import re @@ -40,13 +48,13 @@ class ParseEPUB: try: return self.book.metadata['http://purl.org/dc/elements/1.1/']['creator'][0][0] except KeyError: - return None + return def get_year(self): try: return self.book.metadata['http://purl.org/dc/elements/1.1/']['date'][0][0][:4] except KeyError: - return None + return def get_cover_image(self): # Get cover image @@ -89,7 +97,6 @@ class ParseEPUB: if i.media_type == 'image/jpeg' or i.media_type == 'image/png': return i.get_content() - def get_isbn(self): try: identifier = self.book.metadata['http://purl.org/dc/elements/1.1/']['identifier'] @@ -99,7 +106,15 @@ class ParseEPUB: isbn = i[0] return isbn 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): extract_path = os.path.join(self.temp_dir, self.file_md5) diff --git a/resources/about.html b/resources/about.html new file mode 100644 index 0000000..fee1cf9 --- /dev/null +++ b/resources/about.html @@ -0,0 +1,14 @@ + + + + + + +

Lector

+

A Qt Based ebook reader

+

 

+

Author: BasioMeusPuga disgruntled.mob@gmail.com

+

Page: https://github.com/BasioMeusPuga/Lector

+

License: GPLv3 https://www.gnu.org/licenses/gpl-3.0.en.html

+

 

+ diff --git a/resources/mainwindow.py b/resources/mainwindow.py index a4a6de1..6df390e 100644 --- a/resources/mainwindow.py +++ b/resources/mainwindow.py @@ -57,9 +57,10 @@ class Ui_MainWindow(object): self.gridLayout_3.setSpacing(0) self.gridLayout_3.setObjectName("gridLayout_3") 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.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.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.tableView.setGridStyle(QtCore.Qt.NoPen) diff --git a/resources/pie_chart.py b/resources/pie_chart.py index a51a212..6ff99a8 100644 --- a/resources/pie_chart.py +++ b/resources/pie_chart.py @@ -110,10 +110,11 @@ def pixmapper(current_chapter, total_chapters, temp_dir, size): # TODO # See if saving the svg to disk can be avoided # Shift to lines to track progress + # Maybe make the alignment a little more uniform across emblems progress_percent = int(current_chapter * 100 / total_chapters) generate_pie(progress_percent, temp_dir) 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 diff --git a/resources/raw/blank.png b/resources/raw/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..63fb27b6b4a2bda683fb235ee2a392e705ed1208 GIT binary patch literal 663 zcmeAS@N?(olHy`uVBq!ia0y~yU|hn$z!bs30u*Wg8|4V3n3BBRT^Rni_n+Ah2>S4G?CGIc^^cWU!Zb`ns||Uf%g6Pba4!+xb^lRBQH?Zp#|UlbNSgtz*mdKI;Vst0E&4ofB*mh literal 0 HcmV?d00001 diff --git a/resources/raw/main.ui b/resources/raw/main.ui index 54f41d3..a284e88 100644 --- a/resources/raw/main.ui +++ b/resources/raw/main.ui @@ -117,13 +117,16 @@ - QFrame::NoFrame + QFrame::Box + + + QFrame::Plain QAbstractScrollArea::AdjustToContentsOnFirstShow - QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked true @@ -141,7 +144,7 @@ false - true + false false diff --git a/resources/raw/resources.qrc b/resources/raw/resources.qrc index e5725d0..85008f6 100644 --- a/resources/raw/resources.qrc +++ b/resources/raw/resources.qrc @@ -1,5 +1,6 @@ + blank.png gray-shadow.png NotFound.png checkmark.svg diff --git a/resources/raw/settings.ui b/resources/raw/settings.ui index 198f5fa..bccc677 100644 --- a/resources/raw/settings.ui +++ b/resources/raw/settings.ui @@ -6,8 +6,8 @@ 0 0 - 879 - 673 + 929 + 638 @@ -23,66 +23,17 @@ - - - QFrame::NoFrame - - - QAbstractScrollArea::AdjustToContentsOnFirstShow - - - QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectItems - - - Qt::NoPen - - - true - - - false - - - true - - - false - - + - - - - - Search for Paths, Names, Tags... - - - - - - - Add - - - - - - - Remove - - - - + + + true + + + false + + @@ -90,63 +41,42 @@ - Startup + Switches - + - - - Auto add files - - - - - - - Remember open files - - - - - - - Show Library - - - - - - - Cover Shadows - - - - - - - - - - - OK - - - - - - - Cancel - - - - - - - About - - + + + + + Startup: Refresh library + + + + + + + Remember open files + + + + + + + Cover shadows + + + + + + + Generate tags from files + + + + @@ -155,6 +85,31 @@ + + + + + + OK + + + + + + + Cancel + + + + + + + About + + + + + diff --git a/resources/resources.py b/resources/resources.py index 585fd24..129dbc2 100644 --- a/resources/resources.py +++ b/resources/resources.py @@ -2,7 +2,7 @@ # 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! @@ -1117,6 +1117,50 @@ qt_resource_data = b"\ \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\ \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\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\ \x34\x31\x7a\x22\x2f\x3e\x0a\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\ \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"\ @@ -1195,6 +1209,14 @@ qt_resource_name = b"\ \x02\xea\x4d\x87\ \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\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\ \x0a\xe2\xd1\x87\ \x00\x67\ @@ -1203,33 +1225,32 @@ qt_resource_name = b"\ \x0b\x5d\x1f\x07\ \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\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"\ \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\x74\x00\x00\x00\x00\x00\x01\x00\x00\x47\x17\ -\x00\x00\x00\x30\x00\x01\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\x48\x00\x01\x00\x00\x00\x01\x00\x00\x46\xd1\ +\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x45\x17\ +\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"\ \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\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\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x5f\xb9\x9f\xcd\x26\ -\x00\x00\x00\x74\x00\x00\x00\x00\x00\x01\x00\x00\x47\x17\ +\x00\x00\x01\x5f\xb9\x9f\xca\xd0\ +\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\x00\x30\x00\x01\x00\x00\x00\x01\x00\x00\x45\x17\ -\x00\x00\x01\x5f\xf0\x1d\xc5\xb3\ -\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\x01\x5f\xf0\x1d\xc2\x38\ +\x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x00\x48\x2d\ \x00\x00\x01\x5f\x7e\xcc\x7f\x20\ " diff --git a/resources/settingswindow.py b/resources/settingswindow.py index de34349..2e04872 100644 --- a/resources/settingswindow.py +++ b/resources/settingswindow.py @@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(879, 673) + Dialog.resize(929, 638) self.gridLayout_3 = QtWidgets.QGridLayout(Dialog) self.gridLayout_3.setObjectName("gridLayout_3") self.verticalLayout_2 = QtWidgets.QVBoxLayout() @@ -20,66 +20,51 @@ class Ui_Dialog(object): self.groupBox_2.setObjectName("groupBox_2") self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2) self.gridLayout_2.setObjectName("gridLayout_2") - self.tableView = QtWidgets.QTableView(self.groupBox_2) - self.tableView.setFrameShape(QtWidgets.QFrame.NoFrame) - self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow) - self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed) - self.tableView.setAlternatingRowColors(True) - self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems) - self.tableView.setGridStyle(QtCore.Qt.NoPen) - self.tableView.setSortingEnabled(True) - self.tableView.setWordWrap(False) - self.tableView.setObjectName("tableView") - self.tableView.horizontalHeader().setVisible(True) - self.tableView.verticalHeader().setVisible(False) - self.gridLayout_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.treeView = QtWidgets.QTreeView(self.groupBox_2) + self.treeView.setObjectName("treeView") + self.gridLayout_2.addWidget(self.treeView, 0, 0, 1, 1) + self.aboutBox = QtWidgets.QTextBrowser(self.groupBox_2) + self.aboutBox.setOpenExternalLinks(True) + self.aboutBox.setOpenLinks(False) + self.aboutBox.setObjectName("aboutBox") + self.gridLayout_2.addWidget(self.aboutBox, 1, 0, 1, 1) self.verticalLayout_2.addWidget(self.groupBox_2) self.groupBox = QtWidgets.QGroupBox(Dialog) self.groupBox.setObjectName("groupBox") self.gridLayout = QtWidgets.QGridLayout(self.groupBox) self.gridLayout.setObjectName("gridLayout") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.checkBox = QtWidgets.QCheckBox(self.groupBox) - self.checkBox.setObjectName("checkBox") - self.horizontalLayout_3.addWidget(self.checkBox) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.refreshLibrary = QtWidgets.QCheckBox(self.groupBox) + self.refreshLibrary.setObjectName("refreshLibrary") + self.horizontalLayout_4.addWidget(self.refreshLibrary) self.fileRemember = QtWidgets.QCheckBox(self.groupBox) self.fileRemember.setObjectName("fileRemember") - self.horizontalLayout_3.addWidget(self.fileRemember) - self.checkBox_2 = QtWidgets.QCheckBox(self.groupBox) - self.checkBox_2.setObjectName("checkBox_2") - self.horizontalLayout_3.addWidget(self.checkBox_2) - self.checkBox_3 = QtWidgets.QCheckBox(self.groupBox) - self.checkBox_3.setObjectName("checkBox_3") - self.horizontalLayout_3.addWidget(self.checkBox_3) - self.gridLayout.addLayout(self.horizontalLayout_3, 0, 0, 1, 1) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - 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.horizontalLayout_4.addWidget(self.fileRemember) + self.coverShadows = QtWidgets.QCheckBox(self.groupBox) + self.coverShadows.setObjectName("coverShadows") + self.horizontalLayout_4.addWidget(self.coverShadows) + self.autoTags = QtWidgets.QCheckBox(self.groupBox) + self.autoTags.setObjectName("autoTags") + self.horizontalLayout_4.addWidget(self.autoTags) + self.verticalLayout.addLayout(self.horizontalLayout_4) + self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) self.verticalLayout_2.addWidget(self.groupBox) 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) QtCore.QMetaObject.connectSlotsByName(Dialog) @@ -88,14 +73,11 @@ class Ui_Dialog(object): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Settings")) self.groupBox_2.setTitle(_translate("Dialog", "Library")) - self.tableFilterEdit.setPlaceholderText(_translate("Dialog", "Search for Paths, Names, Tags...")) - self.addButton.setText(_translate("Dialog", "Add")) - self.removeButton.setText(_translate("Dialog", "Remove")) - self.groupBox.setTitle(_translate("Dialog", "Startup")) - self.checkBox.setText(_translate("Dialog", "Auto add files")) + self.groupBox.setTitle(_translate("Dialog", "Switches")) + self.refreshLibrary.setText(_translate("Dialog", "Startup: Refresh library")) self.fileRemember.setText(_translate("Dialog", "Remember open files")) - self.checkBox_2.setText(_translate("Dialog", "Show Library")) - self.checkBox_3.setText(_translate("Dialog", "Cover Shadows")) + self.coverShadows.setText(_translate("Dialog", "Cover shadows")) + self.autoTags.setText(_translate("Dialog", "Generate tags from files")) self.okButton.setText(_translate("Dialog", "OK")) self.cancelButton.setText(_translate("Dialog", "Cancel")) self.aboutButton.setText(_translate("Dialog", "About")) diff --git a/settings.py b/settings.py index 4bda41c..7664bd6 100644 --- a/settings.py +++ b/settings.py @@ -1,13 +1,14 @@ #!/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 +from ast import literal_eval from PyQt5 import QtCore, QtGui class Settings: def __init__(self, parent): - self.parent_window = parent + self.parent = parent self.settings = QtCore.QSettings('Lector', 'Lector') default_profile1 = { @@ -44,87 +45,106 @@ class Settings: def read_settings(self): self.settings.beginGroup('mainWindow') - self.parent_window.resize(self.settings.value('windowSize', QtCore.QSize(1299, 748))) - self.parent_window.move(self.settings.value('windowPosition', QtCore.QPoint(0, 0))) - self.parent_window.current_view = int(self.settings.value('currentView', 0)) - self.parent_window.table_header_sizes = self.settings.value('tableHeaders', None) + self.parent.resize(self.settings.value('windowSize', QtCore.QSize(1299, 748))) + self.parent.move(self.settings.value('windowPosition', QtCore.QPoint(0, 0))) + self.parent.settings['current_view'] = int(self.settings.value('currentView', 0)) + self.parent.settings['main_window_headers'] = self.settings.value('tableHeaders', None) self.settings.endGroup() 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('~')) - self.parent_window.database_path = self.settings.value( + self.parent.database_path = self.settings.value( 'databasePath', 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) - self.parent_window.current_profile_index = int(self.settings.value( + self.parent.current_profile_index = int(self.settings.value( 'currentProfileIndex', 0)) - self.parent_window.comic_profile = self.settings.value( + self.parent.comic_profile = self.settings.value( 'comicProfile', self.default_comic_profile) self.settings.endGroup() self.settings.beginGroup('lastOpen') - self.parent_window.last_open_books = self.settings.value('lastOpenFiles', []) - self.parent_window.last_open_tab = self.settings.value('lastOpenTab', 'library') + self.parent.settings['last_open_books'] = self.settings.value('lastOpenBooks', []) + self.parent.last_open_tab = self.settings.value('lastOpenTab', 'library') self.settings.endGroup() self.settings.beginGroup('settingsWindow') - self.parent_window.settings_dialog_settings = {} - self.parent_window.settings_dialog_settings['size'] = self.settings.value( + self.parent.settings['settings_dialog_size'] = self.settings.value( '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)) - self.parent_window.settings_dialog_settings['headers'] = self.settings.value( + self.parent.settings['settings_dialog_headers'] = self.settings.value( 'tableHeaders', [200, 150]) 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): print('Saving settings...') + current_settings = self.parent.settings self.settings.beginGroup('mainWindow') - self.settings.setValue('windowSize', self.parent_window.size()) - self.settings.setValue('windowPosition', self.parent_window.pos()) - self.settings.setValue('currentView', self.parent_window.stackedWidget.currentIndex()) + self.settings.setValue('windowSize', self.parent.size()) + self.settings.setValue('windowPosition', self.parent.pos()) + self.settings.setValue('currentView', self.parent.stackedWidget.currentIndex()) table_headers = [] 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.endGroup() self.settings.beginGroup('runtimeVariables') - self.settings.setValue('lastOpenPath', self.parent_window.last_open_path) - self.settings.setValue('databasePath', self.parent_window.database_path) + self.settings.setValue('lastOpenPath', self.parent.last_open_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) - current_profile2 = self.parent_window.bookToolBar.profileBox.itemData( + current_profile2 = self.parent.bookToolBar.profileBox.itemData( 1, QtCore.Qt.UserRole) - current_profile3 = self.parent_window.bookToolBar.profileBox.itemData( + current_profile3 = self.parent.bookToolBar.profileBox.itemData( 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', [ current_profile1, current_profile2, current_profile3]) 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() - current_tab_index = self.parent_window.tabWidget.currentIndex() + current_tab_index = self.parent.tabWidget.currentIndex() if current_tab_index == 0: last_open_tab = 'library' 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.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.endGroup() self.settings.beginGroup('settingsWindow') - these_settings = self.parent_window.settings_dialog_settings - self.settings.setValue('windowSize', these_settings['size']) - self.settings.setValue('windowPosition', these_settings['position']) - self.settings.setValue('tableHeaders', these_settings['headers']) + self.settings.setValue('windowSize', current_settings['settings_dialog_size']) + self.settings.setValue('windowPosition', current_settings['settings_dialog_position']) + self.settings.setValue('tableHeaders', current_settings['settings_dialog_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() diff --git a/settingsdialog.py b/settingsdialog.py index c5dec87..46279bd 100644 --- a/settingsdialog.py +++ b/settingsdialog.py @@ -1,156 +1,247 @@ #!/usr/bin/env python3 -import os -import collections +# This file is a part of Lector, a Qt based ebook reader +# 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 . + +# TODO +# Get Cancel working with the file system model + +import os +import copy +from PyQt5 import QtWidgets, QtCore import database from resources import settingswindow -from models import MostExcellentTableModel, TableProxyModel +from models import MostExcellentFileSystemModel, FileSystemProxyModel from threaded import BackGroundBookSearch, BackGroundBookAddition class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog): - # TODO - # Deletion from table - # Cancel behavior - # Update database on table model update - - def __init__(self, parent_window): + def __init__(self, parent): super(SettingsUI, self).__init__() self.setupUi(self) - self.last_open_directory = None - self.parent_window = parent_window - self.database_path = self.parent_window.database_path + self.parent = parent + self.database_path = self.parent.database_path - self.resize(self.parent_window.settings_dialog_settings['size']) - self.move(self.parent_window.settings_dialog_settings['position']) + self.resize(self.parent.settings['settings_dialog_size']) + 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.thread = None + self.filesystem_model = None + self.tag_data_copy = None - self.tableFilterEdit.textChanged.connect(self.update_table_proxy_model) - self.addButton.clicked.connect(self.add_directories) + self.okButton.setToolTip('Save changes and start library scan') + self.okButton.clicked.connect(self.start_library_scan) self.cancelButton.clicked.connect(self.cancel_pressed) - self.okButton.clicked.connect(self.ok_pressed) + self.aboutButton.clicked.connect(self.about_pressed) - self.generate_table() - header_sizes = self.parent_window.settings_dialog_settings['headers'] - if header_sizes: - for count, i in enumerate(header_sizes): - self.tableView.horizontalHeader().resizeSection(count, int(i)) + # Check boxes + self.autoTags.setChecked(self.parent.settings['auto_tags']) + self.coverShadows.setChecked(self.parent.settings['cover_shadows']) + self.refreshLibrary.setChecked(self.parent.settings['scan_library']) + self.fileRemember.setChecked(self.parent.settings['remember_files']) - self.tableView.horizontalHeader().setSectionResizeMode( - QtWidgets.QHeaderView.Interactive) - self.tableView.horizontalHeader().setHighlightSections(False) - self.tableView.horizontalHeader().setStretchLastSection(True) - # self.tableView.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch) + self.autoTags.clicked.connect(self.manage_checkboxes) + self.coverShadows.clicked.connect(self.manage_checkboxes) + self.refreshLibrary.clicked.connect(self.manage_checkboxes) + self.fileRemember.clicked.connect(self.manage_checkboxes) - def generate_table(self): + # Generate the filesystem treeView + self.generate_tree() + + def generate_tree(self): # Fetch all directories in the database - self.paths = database.DatabaseFunctions( + paths = database.DatabaseFunctions( self.database_path).fetch_data( - ('Path', 'Name', 'Tags'), + ('Path', 'Name', 'Tags', 'CheckState'), 'directories', {'Path': ''}, 'LIKE') - if not self.paths: + self.parent.generate_library_filter_menu(paths) + directory_data = {} + if not paths: print('Database returned no paths for settings...') else: - # Convert to a list because tuples, well, they're tuples - self.paths = [list(i) for i in self.paths] + # Convert to the dictionary format that is + # 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.table_model = MostExcellentTableModel( - table_header, self.paths, None) - - 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) + self.filesystem_model = MostExcellentFileSystemModel(directory_data) + self.filesystem_model.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs) + self.treeView.setModel(self.filesystem_model) # TODO - # Account for a parent folder getting added after a subfolder - # Currently this does the inverse only + # This here might break on them pestilent non unixy OSes + # Check and see - for i in self.paths: - already_present = os.path.realpath(i[0]) - 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 + root_directory = QtCore.QDir().rootPath() + self.treeView.setRootIndex(self.filesystem_model.setRootPath(root_directory)) - # Set default name for the directory - directory_name = os.path.basename(add_directory).title() - data_pair = [[add_directory, directory_name, None]] - database.DatabaseFunctions(self.database_path).set_library_paths(data_pair) - self.generate_table() + # Set the treeView and QFileSystemModel to its desired state + selected_paths = [ + i for i in directory_data if directory_data[i]['check_state'] == QtCore.Qt.Checked] + expand_paths = set() + for i in selected_paths: - def ok_pressed(self): - # Traverse directories looking for files - self.thread = BackGroundBookSearch(self, self.table_model.display_data) - self.thread.finished.connect(self.do_something) - self.thread.start() + # Recursively grind down parent paths for expansion + this_path = i + while True: + parent_path = os.path.dirname(this_path) + if parent_path == this_path: + break + expand_paths.add(parent_path) + this_path = parent_path - def do_something(self): - print('Book search completed') + # Expand all the parent paths derived from the selected path + 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() - # TODO - # Implement cancel by restoring the table model to an older version - # def showEvent(self, event): - # event.accept() + 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 + # Change this to no longer include files added manually + + 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): self.no_more_settings() event.accept() + def showEvent(self, event): + self.tag_data_copy = copy.deepcopy(self.filesystem_model.tag_data) + event.accept() + def no_more_settings(self): - self.table_model = self.old_table_model - self.parent_window.libraryToolBar.settingsButton.setChecked(False) + self.parent.libraryToolBar.settingsButton.setChecked(False) + self.aboutBox.hide() + self.treeView.show() self.resizeEvent() def resizeEvent(self, event=None): - self.parent_window.settings_dialog_settings['size'] = self.size() - self.parent_window.settings_dialog_settings['position'] = self.pos() + self.parent.settings['settings_dialog_size'] = self.size() + self.parent.settings['settings_dialog_position'] = self.pos() table_headers = [] - for i in range(2): - table_headers.append(self.tableView.horizontalHeader().sectionSize(i)) - self.parent_window.settings_dialog_settings['headers'] = table_headers + for i in [0, 4]: + table_headers.append(self.treeView.columnWidth(i)) + 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()) diff --git a/sorter.py b/sorter.py index c3ccdc4..93d6671 100644 --- a/sorter.py +++ b/sorter.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 -# TODO -# See if you want to include a hash of the book's name and author +# This file is a part of Lector, a Qt based ebook reader +# Copyright (C) 2017 BasioMeusPuga -import io -import os -import pickle -import hashlib -from multiprocessing.dummy import Pool -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. -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 . + +# INSTRUCTIONS # Every parser is supposed to have the following methods, even if they return None: # read_book() # get_title() @@ -19,19 +24,42 @@ import database # get_year() # get_cover_image() # get_isbn() +# get_tags() # get_contents() - Should return a tuple with 0: TOC 1: special_settings (dict) # Parsers for files containing only images need to return only images_only = True +# TODO +# Maybe shift to insert or replace instead of hash checking +# See if you want to include a hash of the book's name and author +# Change thread niceness + +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.cbz import ParseCBZ 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__ +progress_emitter = None # This is to be made into a global variable -# This is for thread safety class UpdateProgress(QtCore.QObject): + # This is for thread safety update_signal = QtCore.pyqtSignal(int) def connect_to_progressbar(self): @@ -42,7 +70,7 @@ class UpdateProgress(QtCore.QObject): 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 # Then, on the basis of what is needed, pass the # filenames to the requisite functions @@ -51,17 +79,20 @@ class BookSorter: # Caching upon closing self.file_list = [i for i in file_list if os.path.exists(i)] self.statistics = [0, (len(file_list))] - self.all_books = {} self.hashes = [] self.mode = mode self.database_path = database_path + self.auto_tags = auto_tags self.temp_dir = temp_dir if database_path: self.database_hashes() + self.threading_completed = [] + self.queue = Manager().Queue() + self.processed_books = [] + if self.mode == 'addition': - self.progress_emitter = UpdateProgress() - self.progress_emitter.connect_to_progressbar() + progress_object_generator() def database_hashes(self): # TODO @@ -101,32 +132,21 @@ class BookSorter: # This should speed up addition for larger files # without compromising the integrity of the process 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() - if self.mode == 'addition': - self.statistics[0] += 1 - self.progress_emitter.update_progress( - self.statistics[0] * 100 // self.statistics[1]) + # Update the progress queue + self.queue.put(filename) - # 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 - if (self.mode == 'addition' - and (file_md5 in self.all_books.items() or file_md5 in self.hashes)): + # IF the file is NOT being loaded into the reader, + # Do not allow addition in case the file + # is already in the database + if self.mode == 'addition' and file_md5 in self.hashes: return - # ___________SORTING TAKES PLACE HERE___________ - sorter = { - 'epub': ParseEPUB, - 'cbz': ParseCBZ, - 'cbr': ParseCBR - } - file_extension = os.path.splitext(filename)[1][1:] try: + # Get the requisite parser from the sorter dict book_ref = sorter[file_extension](filename, self.temp_dir, file_md5) except KeyError: print(filename + ' has an unsupported extension') @@ -137,7 +157,7 @@ class BookSorter: book_ref.read_book() if book_ref.book: - title = book_ref.get_title().title() + title = book_ref.get_title() author = book_ref.get_author() if not author: @@ -150,22 +170,32 @@ class BookSorter: isbn = book_ref.get_isbn() + tags = None + if self.auto_tags: + tags = book_ref.get_tags() + + this_book = {} + this_book[file_md5] = { + 'title': title, + 'author': author, + 'year': year, + 'isbn': isbn, + 'hash': file_md5, + 'path': filename, + '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: - # Reduce the size of the incoming image cover_image = resize_image(cover_image_raw) else: cover_image = None - self.all_books[file_md5] = { - 'title': title, - 'author': author, - 'year': year, - 'isbn': isbn, - 'path': filename, - 'cover_image': cover_image} + this_book[file_md5]['cover_image'] = cover_image if self.mode == 'reading': all_content = book_ref.get_contents() @@ -183,25 +213,65 @@ class BookSorter: content['Invalid'] = 'Possible Parse Error' 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): - _pool = Pool(5) - _pool.map(self.read_book, self.file_list) - _pool.close() - _pool.join() + def pool_creator(): + _pool = Pool(5) + self.processed_books = _pool.map( + self.read_book, self.file_list) - return self.all_books + _pool.close() + _pool.join() + + 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): diff --git a/threaded.py b/threaded.py index 334eedc..65bd0be 100644 --- a/threaded.py +++ b/threaded.py @@ -1,6 +1,23 @@ #!/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 . + import os +import pathlib from multiprocessing.dummy import Pool from PyQt5 import QtCore @@ -26,38 +43,72 @@ class BackGroundTabUpdate(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) - self.parent_window = parent_window self.file_list = file_list + self.parent = parent self.database_path = database_path + self.prune_required = prune_required def run(self): books = sorter.BookSorter( self.file_list, 'addition', - self.database_path) + self.database_path, + self.parent.settings['auto_tags']) 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) - 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): - 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) - self.parent_window = parent_window - self.data_list = data_list - self.valid_files = [] # A tuple should get added to this containing the - # file path and the folder name / tags + self.parent = parent + self.valid_files = [] + + # 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 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): root_directory = incoming_data[0] folder_name = incoming_data[1] 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: if os.path.splitext(filename)[1][1:] in sorter.available_parsers: self.valid_files.append( @@ -65,17 +116,9 @@ class BackGroundBookSearch(QtCore.QThread): def initiate_threads(): _pool = Pool(5) - _pool.map(traverse_directory, self.data_list) + _pool.map(traverse_directory, self.valid_directories) _pool.close() _pool.join() initiate_threads() - - # 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 + print(len(self.valid_files), 'books found') diff --git a/widgets.py b/widgets.py index 3b76510..0f46509 100644 --- a/widgets.py +++ b/widgets.py @@ -1,9 +1,31 @@ #!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 . + +# TODO +# Reading modes +# Double page, Continuous etc +# Especially for comics + + import os from PyQt5 import QtWidgets, QtGui, QtCore -from resources import resources, pie_chart +from resources import pie_chart class BookToolBar(QtWidgets.QToolBar): @@ -31,6 +53,10 @@ class BookToolBar(QtWidgets.QToolBar): self.fullscreenButton = QtWidgets.QAction( QtGui.QIcon.fromTheme('view-fullscreen'), 'Fullscreen', self) + self.bookmarkButton = QtWidgets.QAction( + QtGui.QIcon.fromTheme('bookmarks'), + 'Bookmark', self) + self.bookmarkButton.setObjectName('bookmarkButton') self.resetProfile = QtWidgets.QAction( QtGui.QIcon.fromTheme('view-refresh'), 'Reset profile', self) @@ -40,6 +66,8 @@ class BookToolBar(QtWidgets.QToolBar): self.fontButton.setCheckable(True) self.fontButton.triggered.connect(self.toggle_font_settings) self.addSeparator() + self.addAction(self.bookmarkButton) + self.bookmarkButton.setCheckable(True) self.addAction(self.fullscreenButton) # Font modification @@ -191,6 +219,7 @@ class BookToolBar(QtWidgets.QToolBar): self.searchBarAction = self.addWidget(self.searchBar) self.bookActions = [ + self.bookmarkButton, self.fullscreenButton, self.tocBoxAction, self.searchBarAction] @@ -269,6 +298,11 @@ class LibraryToolBar(QtWidgets.QToolBar): QtGui.QIcon.fromTheme('table'), 'View as table', self) 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 self.viewButtons = QtWidgets.QActionGroup(self) self.viewButtons.setExclusive(True) @@ -282,6 +316,8 @@ class LibraryToolBar(QtWidgets.QToolBar): self.addAction(self.coverViewButton) self.addAction(self.tableViewButton) self.addSeparator() + self.addWidget(self.libraryFilterButton) + self.addSeparator() self.addAction(self.settingsButton) # Filter @@ -346,8 +382,10 @@ class Tab(QtWidgets.QWidget): self.parent = parent self.metadata = metadata # Save progress data into this dictionary - self.gridLayout = QtWidgets.QGridLayout(self) - self.gridLayout.setObjectName("gridLayout") + self.masterLayout = QtWidgets.QHBoxLayout(self) + self.horzLayout = QtWidgets.QSplitter(self) + self.horzLayout.setOrientation(QtCore.Qt.Horizontal) + self.masterLayout.addWidget(self.horzLayout) position = self.metadata['position'] @@ -374,6 +412,7 @@ class Tab(QtWidgets.QWidget): self.contentView.loadImage(chapter_content) else: self.contentView = PliantQTextBrowser(self.window()) + # print(dir(self.contentView.document())) ## TODO USE this for modifying formatting and searching relative_path_root = os.path.join( self.window().temp_dir.path(), self.metadata['hash']) @@ -393,9 +432,20 @@ class Tab(QtWidgets.QWidget): self.contentView.setHorizontalScrollBarPolicy( 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.gridLayout.addWidget(self.contentView, 0, 0, 1, 1) + self.horzLayout.addWidget(self.contentView) + self.horzLayout.addWidget(self.dockWidget) title = self.metadata['title'] self.parent.addTab(self, title) @@ -462,6 +512,11 @@ class Tab(QtWidgets.QWidget): self.contentView.clear() 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): if self.are_we_doing_images_only: # Tab color does not need to be set separately in case @@ -473,11 +528,22 @@ class Tab(QtWidgets.QWidget): self.contentView.resizeEvent() 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.setStyleSheet( "QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format( 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): direction = -1 if self.sender().objectName() == 'nextChapter': @@ -661,6 +727,7 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, temp_dir, parent=None): super(LibraryDelegate, self).__init__(parent) self.temp_dir = temp_dir + self.parent = parent def paint(self, painter, option, index): # This is a hint for the future @@ -673,27 +740,27 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate): position = index.data(QtCore.Qt.UserRole + 7) # The shadow pixmap currently is set to 420 x 600 - shadow_pixmap = QtGui.QPixmap() - shadow_pixmap.load(':/images/gray-shadow.png') - shadow_pixmap = shadow_pixmap.scaled(160, 230, QtCore.Qt.IgnoreAspectRatio) - shadow_x = option.rect.topLeft().x() + 10 - shadow_y = option.rect.topLeft().y() - 5 + # Only draw the cover shadow in case the setting is enabled + if self.parent.settings['cover_shadows']: + shadow_pixmap = QtGui.QPixmap() + shadow_pixmap.load(':/images/gray-shadow.png') + shadow_pixmap = shadow_pixmap.scaled(160, 230, QtCore.Qt.IgnoreAspectRatio) + shadow_x = option.rect.topLeft().x() + 10 + 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: painter.setOpacity(.7) - painter.drawPixmap(shadow_x, shadow_y, shadow_pixmap) QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) - painter.setOpacity(1) - read_icon = pie_chart.pixmapper(-1, None, None, 36) x_draw = option.rect.bottomRight().x() - 30 y_draw = option.rect.bottomRight().y() - 35 painter.drawPixmap(x_draw, y_draw, read_icon) + painter.setOpacity(1) return - painter.setOpacity(.8) - painter.drawPixmap(shadow_x, shadow_y, shadow_pixmap) - painter.setOpacity(1) QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) if position: current_chapter = position['current_chapter']