Fairly substantial rewrite.

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

1
AUTHORS Normal file
View File

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

13
TODO
View File

@@ -1,13 +1,12 @@
TODO
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
@@ -60,3 +62,4 @@ TODO
Other:
✓ Define every widget in code
✓ Include icons for emblems
Shift to logging instead of print statements

View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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()

View File

@@ -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 <http://www.gnu.org/licenses/>.
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,18 +181,15 @@ 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()

View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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)

180
models.py
View File

@@ -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 <http://www.gnu.org/licenses/>.
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

View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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 = {

View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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 = {

View File

@@ -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 <http://www.gnu.org/licenses/>.
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)

14
resources/about.html Normal file
View File

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

View File

@@ -57,9 +57,10 @@ class Ui_MainWindow(object):
self.gridLayout_3.setSpacing(0)
self.gridLayout_3.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)

View File

@@ -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

BIN
resources/raw/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

View File

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

View File

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

View File

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

View File

@@ -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\
"

View File

@@ -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"))

View File

@@ -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()

View File

@@ -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 <http://www.gnu.org/licenses/>.
# TODO
# Get Cancel working with the file system model
import os
import copy
from PyQt5 import QtWidgets, QtCore
import database
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())

184
sorter.py
View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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):

View File

@@ -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 <http://www.gnu.org/licenses/>.
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')

View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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']