#!/usr/bin/env python3 """ TODO ✓ sqlite3 for cover images cache ✓ sqlite3 for storing metadata ✓ Drop down for SortBy (library view) ✓ Define every widget in code because you're going to need to create separate tabs ✓ Override the keypress event of the textedit ✓ Search bar in toolbar ✓ Shift focus to the tab that has the book open ✓ Search bar in toolbar ✓ Drop down for TOC (book view) ✓ Image reflow ✓ Implement book view settings with a(nother) toolbar ✓ Use format* icons for toolbar buttons ✓ Image delegates Options: Ignore a and the for sorting purposes Check files (hashes) upon restart Recursive file addition Show what on startup Maybe include icons for emblems mobi, azw support txt, doc, djvu support pdf support? Goodreads API: Ratings, Read, Recommendations Get ISBN using python-isbnlib All ebooks should first be added to the database and then returned as HTML Theming Pagination Information dialog widget Library context menu: Cache, Read, Edit database, delete Set context menu for definitions and the like """ import os import sys from PyQt5 import QtWidgets, QtGui, QtCore import mainwindow import database import sorter from widgets import LibraryToolBar, BookToolBar, Tab, LibraryDelegate from subclasses import Settings, Library class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) # Initialize application Settings(self).read_settings() # This should populate all variables that need # to be remembered across sessions # Create the database in case it doesn't exist database.DatabaseInit(self.database_path) # Create and right align the statusbar label widget self.statusMessage = QtWidgets.QLabel() self.statusMessage.setObjectName('statusMessage') self.statusBar.addPermanentWidget(self.statusMessage) # Init the QListView self.viewModel = None self.lib_ref = Library(self) # Create toolbars self.libraryToolBar = LibraryToolBar(self) self.libraryToolBar.addButton.triggered.connect(self.add_books) self.libraryToolBar.deleteButton.triggered.connect(self.delete_books) self.libraryToolBar.filterEdit.textChanged.connect(self.only_update_listview) self.libraryToolBar.sortingBox.activated.connect(self.only_update_listview) self.addToolBar(self.libraryToolBar) self.bookToolBar = BookToolBar(self) self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen) self.addToolBar(self.bookToolBar) # Make the correct toolbar visible self.tab_switch() self.tabWidget.currentChanged.connect(self.tab_switch) # New tabs and their contents self.current_tab = None self.current_textEdit = None # Tab closing self.tabWidget.setTabsClosable(True) self.tabWidget.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, None) self.tabWidget.tabCloseRequested.connect(self.close_tab) # ListView # self.listView.setSpacing(0) self.listView.setGridSize(QtCore.QSize(175, 240)) self.listView.verticalScrollBar().setSingleStep(7) self.listView.doubleClicked.connect(self.list_doubleclick) self.listView.setItemDelegate(LibraryDelegate()) self.reload_listview() # Keyboard shortcuts self.exit_all = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'), self) self.exit_all.activated.connect(self.closeEvent) def resizeEvent(self, event=None): if event: # This implies a vertical resize event only # We ain't about that lifestyle if event.oldSize().width() == event.size().width(): return # The hackiness of this hack is just... default_size = 175 # This is size of the QIcon (160 by default) + # minimum margin is needed between thumbnails # for n icons, the n + 1th icon will appear at > n +1.11875 # First, calculate the number of images per row i = self.listView.viewport().width() / default_size rem = i - int(i) if rem >= .11875 and rem <= .9999: num_images = int(i) else: num_images = int(i) - 1 # The rest is illustrated using informative variable names space_occupied = num_images * default_size space_left = ( self.listView.viewport().width() - space_occupied - 19) # 19 is the scrollbar width try: layout_extra_space_per_image = space_left // num_images self.listView.setGridSize( QtCore.QSize(default_size + layout_extra_space_per_image, 240)) except ZeroDivisionError: # Initial resize is ignored return def add_books(self): # TODO # Maybe expand this to traverse directories recursively self.statusMessage.setText('Adding books...') my_file = QtWidgets.QFileDialog.getOpenFileNames( self, 'Open file', self.last_open_path, "eBooks (*.epub *.mobi *.aws *.txt *.pdf *.fb2 *.djvu)") if my_file[0]: self.listView.setEnabled(False) self.last_open_path = os.path.dirname(my_file[0][0]) books = sorter.BookSorter(my_file[0], self.database_path) parsed_books = books.initiate_threads() database.DatabaseFunctions(self.database_path).add_to_database(parsed_books) self.listView.setEnabled(True) self.viewModel = None self.reload_listview() def delete_books(self): selected_books = self.listView.selectedIndexes() if selected_books: def ifcontinue(box_button): if box_button.text() == '&Yes': selected_hashes = [] for i in selected_books: book_data = i.data(QtCore.Qt.UserRole + 3) selected_hashes.append(book_data['book_hash']) database.DatabaseFunctions( self.database_path).delete_from_database(selected_hashes) self.viewModel = None self.reload_listview() selected_number = len(selected_books) msg_box = QtWidgets.QMessageBox() msg_box.setText('Delete %d book(s)?' % selected_number) msg_box.setIcon(QtWidgets.QMessageBox.Question) msg_box.setWindowTitle('Confirm deletion') msg_box.setStandardButtons( QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) msg_box.buttonClicked.connect(ifcontinue) msg_box.show() msg_box.exec_() def only_update_listview(self): self.lib_ref.update_proxyModel() def reload_listview(self): if not self.viewModel: self.lib_ref.generate_model() self.lib_ref.create_proxyModel() self.lib_ref.update_proxyModel() def tab_switch(self): if self.tabWidget.currentIndex() == 0: self.bookToolBar.hide() self.libraryToolBar.show() if self.lib_ref.proxy_model: # Making the proxy model available doesn't affect # memory utilization at all. Bleh. self.statusMessage.setText( str(self.lib_ref.proxy_model.rowCount()) + ' Books') else: self.bookToolBar.show() self.libraryToolBar.hide() current_metadata = self.tabWidget.widget( self.tabWidget.currentIndex()).book_metadata current_title = current_metadata['book_title'] current_author = current_metadata['book_author'] self.statusMessage.setText( current_author + ' - ' + current_title) def set_fullscreen(self): self.current_tab = self.tabWidget.currentIndex() self.current_textEdit = self.tabWidget.widget(self.current_tab) self.exit_shortcut = QtWidgets.QShortcut( QtGui.QKeySequence('Escape'), self.current_textEdit) self.exit_shortcut.activated.connect(self.set_normalsize) self.current_textEdit.setWindowFlags(QtCore.Qt.Window) self.current_textEdit.setWindowState(QtCore.Qt.WindowFullScreen) self.hide() self.current_textEdit.show() def set_normalsize(self): self.current_textEdit.setWindowState(QtCore.Qt.WindowNoState) self.current_textEdit.setWindowFlags(QtCore.Qt.Widget) self.show() self.current_textEdit.show() def list_doubleclick(self, myindex): # TODO # Load the book. index = self.listView.model().index(myindex.row(), 0) book_metadata = self.listView.model().data(index, QtCore.Qt.UserRole + 3) # Shift focus to the tab that has the book open (if there is one) for i in range(1, self.tabWidget.count()): tab_book_metadata = self.tabWidget.widget(i).book_metadata if tab_book_metadata['book_hash'] == book_metadata['book_hash']: self.tabWidget.setCurrentIndex(i) return tab_ref = Tab(book_metadata, self.tabWidget) self.tabWidget.setCurrentWidget(tab_ref) print(tab_ref.book_metadata) # Metadata upon tab creation def close_tab(self, tab_index): print(self.tabWidget.widget(tab_index).book_metadata) # Metadata upon tab deletion self.tabWidget.removeTab(tab_index) def closeEvent(self, event=None): Settings(self).save_settings() QtWidgets.qApp.exit() def main(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName('Lector') # This is needed for QStandardPaths # and my own hubris form = MainUI() form.show() form.resizeEvent() app.exec_() if __name__ == '__main__': main()