#!/usr/bin/env python3 # This file is a part of Lector, a Qt based ebook reader # Copyright (C) 2017-2019 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 gc import sys import hashlib import pathlib # This allows for the program to be launched from the # dir where it's been copied instead of needing to be # installed install_dir = os.path.realpath(__file__) install_dir = pathlib.Path(install_dir).parents[1] sys.path.append(str(install_dir)) from PyQt5 import QtWidgets, QtGui, QtCore # Init logging # Must be done first and at the module level # or it won't work properly in case of the imports below from lector.logger import init_logging, VERSION logger = init_logging(sys.argv) logger.log(60, f'Lector {VERSION} - Application started') from lector import database from lector import sorter from lector.toolbars import LibraryToolBar, BookToolBar from lector.widgets import Tab, DragDropListView, DragDropTableView from lector.delegates import LibraryDelegate from lector.threaded import BackGroundTabUpdate, BackGroundBookAddition, BackGroundBookDeletion from lector.library import Library from lector.guifunctions import QImageFactory, CoverLoadingAndCulling, ViewProfileModification from lector.settings import Settings from lector.settingsdialog import SettingsUI from lector.metadatadialog import MetadataUI from lector.definitionsdialog import DefinitionsUI from lector.resources import mainwindow, resources class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): def __init__(self): super(MainUI, self).__init__() self.setupUi(self) # Set window icon self.setWindowIcon( QtGui.QIcon(':/images/Lector.png')) # Central Widget - Make borders disappear self.centralWidget().layout().setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setContentsMargins(0, 0, 0, 0) # Initialize translation function self._translate = QtCore.QCoreApplication.translate # Create library widgets self.listView = DragDropListView(self, self.listPage) self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1) self.tableView = DragDropTableView(self, self.tablePage) self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1) # Empty variables that will be infested soon self.settings = {} self.thread = None # Background Thread self.current_contentView = None # For fullscreening purposes self.display_profiles = None self.current_profile_index = None self.comic_profile = {} self.database_path = None self.active_library_filters = [] self.active_docks = [] # Initialize application Settings(self).read_settings() # This should populate all variables that need # to be remembered across sessions # Initialize icon factory self.QImageFactory = QImageFactory(self) # Initialize toolbars self.libraryToolBar = LibraryToolBar(self) self.bookToolBar = BookToolBar(self) # Widget declarations self.libraryFilterMenu = QtWidgets.QMenu() self.statusMessage = QtWidgets.QLabel() # Reference variables self.alignment_dict = { 'left': self.bookToolBar.alignLeft, 'right': self.bookToolBar.alignRight, 'center': self.bookToolBar.alignCenter, 'justify': self.bookToolBar.alignJustify} # Create the database in case it doesn't exist database.DatabaseInit(self.database_path) # Initialize settings dialog self.settingsDialog = SettingsUI(self) # Initialize metadata dialog self.metadataDialog = MetadataUI(self) # Initialize definition view dialog self.definitionDialog = DefinitionsUI(self) # Make the statusbar invisible by default self.statusBar.setVisible(False) # Statusbar widgets self.statusMessage.setObjectName('statusMessage') self.statusBar.addPermanentWidget(self.statusMessage) self.errorButton = QtWidgets.QPushButton(self.statusBar) self.errorButton.setIcon(QtGui.QIcon(':/images/error.svg')) self.errorButton.setFlat(True) self.errorButton.setVisible(False) self.errorButton.setToolTip('What hast thou done?') self.errorButton.clicked.connect(self.show_errors) self.statusBar.addPermanentWidget(self.errorButton) 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) # Application wide temporary directory self.temp_dir = QtCore.QTemporaryDir() # Init the Library self.lib_ref = Library(self) # Initialize Cover loading functions # Must be after the Library init self.cover_functions = CoverLoadingAndCulling(self) # Init the culling timer self.culling_timer = QtCore.QTimer() self.culling_timer.setSingleShot(True) self.culling_timer.timeout.connect(self.cover_functions.cull_covers) # Initialize profile modification functions self.profile_functions = ViewProfileModification(self) # Toolbar display # Maybe make this a persistent option self.settings['show_bars'] = True # Library toolbar 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) self.libraryToolBar.tableViewButton.triggered.connect(self.switch_library_view) self.libraryToolBar.reloadLibraryButton.triggered.connect( self.settingsDialog.start_library_scan) self.libraryToolBar.colorButton.triggered.connect(self.get_color) self.libraryToolBar.settingsButton.triggered.connect( lambda: self.show_settings(0)) self.libraryToolBar.aboutButton.triggered.connect( lambda: self.show_settings(3)) self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodels) self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodels) self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.libraryToolBar.searchBar.textChanged.connect(self.statusbar_visibility) self.addToolBar(self.libraryToolBar) if self.settings['current_view'] == 0: self.libraryToolBar.coverViewButton.trigger() else: self.libraryToolBar.tableViewButton.trigger() # Book toolbar self.bookToolBar.addBookmarkButton.triggered.connect( lambda: self.tabWidget.currentWidget().sideDock.bookmarks.add_bookmark()) self.bookToolBar.bookmarkButton.triggered.connect( lambda: self.tabWidget.currentWidget().toggle_side_dock(0)) self.bookToolBar.annotationButton.triggered.connect( lambda: self.tabWidget.currentWidget().toggle_side_dock(1)) self.bookToolBar.searchButton.triggered.connect( lambda: self.tabWidget.currentWidget().toggle_side_dock(2)) self.bookToolBar.distractionFreeButton.triggered.connect( self.toggle_distraction_free) self.bookToolBar.fullscreenButton.triggered.connect( lambda: self.tabWidget.currentWidget().go_fullscreen()) self.bookToolBar.doublePageButton.triggered.connect(self.change_page_view) self.bookToolBar.mangaModeButton.triggered.connect(self.change_page_view) self.bookToolBar.invertButton.triggered.connect(self.change_page_view) self.bookToolBar.rotateRightButton.triggered.connect(self.change_page_view) self.bookToolBar.rotateLeftButton.triggered.connect(self.change_page_view) if self.settings['double_page_mode']: self.bookToolBar.doublePageButton.setChecked(True) if self.settings['manga_mode']: self.bookToolBar.mangaModeButton.setChecked(True) if self.settings['invert_colors']: self.bookToolBar.invertButton.setChecked(True) for count, i in enumerate(self.display_profiles): self.bookToolBar.profileBox.setItemData(count, i, QtCore.Qt.UserRole) self.bookToolBar.profileBox.currentIndexChanged.connect( self.profile_functions.format_contentView) self.bookToolBar.profileBox.setCurrentIndex(self.current_profile_index) self.bookToolBar.fontBox.currentFontChanged.connect(self.modify_font) self.bookToolBar.fontSizeBox.currentIndexChanged.connect(self.modify_font) self.bookToolBar.lineSpacingUp.triggered.connect(self.modify_font) self.bookToolBar.lineSpacingDown.triggered.connect(self.modify_font) self.bookToolBar.paddingUp.triggered.connect(self.modify_font) self.bookToolBar.paddingDown.triggered.connect(self.modify_font) self.bookToolBar.resetProfile.triggered.connect( self.profile_functions.reset_profile) profile_index = self.bookToolBar.profileBox.currentIndex() current_profile = self.bookToolBar.profileBox.itemData( profile_index, QtCore.Qt.UserRole) for i in self.alignment_dict.items(): i[1].triggered.connect(self.modify_font) self.alignment_dict[current_profile['text_alignment']].setChecked(True) self.bookToolBar.zoomIn.triggered.connect( self.modify_comic_view) self.bookToolBar.zoomOut.triggered.connect( self.modify_comic_view) self.bookToolBar.fitWidth.triggered.connect( lambda: self.modify_comic_view(False)) self.bookToolBar.bestFit.triggered.connect( lambda: self.modify_comic_view(False)) self.bookToolBar.originalSize.triggered.connect( lambda: self.modify_comic_view(False)) self.bookToolBar.comicBGColor.clicked.connect( self.get_color) self.bookToolBar.colorBoxFG.clicked.connect(self.get_color) self.bookToolBar.colorBoxBG.clicked.connect(self.get_color) self.bookToolBar.tocBox.currentIndexChanged.connect(self.set_toc_position) self.addToolBar(self.bookToolBar) # Make the correct toolbar visible self.current_tab = self.tabWidget.currentIndex() self.tab_switch() self.tabWidget.currentChanged.connect(self.tab_switch) # Tab Widget formatting self.tabWidget.setTabsClosable(True) self.tabWidget.setDocumentMode(True) self.tabWidget.tabBarClicked.connect(self.tab_disallow_library_movement) # Get list of available parsers self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers) logger.log(60, 'Available parsers: ' + self.available_parsers) # The Library tab gets no button self.tabWidget.tabBar().setTabButton( 0, QtWidgets.QTabBar.RightSide, None) self.tabWidget.widget(0).is_library = True self.tabWidget.tabCloseRequested.connect(self.tab_close) self.tabWidget.setTabBarAutoHide(True) # Init display models self.lib_ref.generate_model('build') self.lib_ref.generate_proxymodels() self.lib_ref.generate_library_tags() self.set_library_filter() self.start_culling_timer() # ListView self.listView.setGridSize(QtCore.QSize(175, 240)) 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)) self.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.listView.customContextMenuRequested.connect(self.generate_library_context_menu) self.listView.verticalScrollBar().valueChanged.connect(self.start_culling_timer) self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.listView.setAcceptDrops(True) self.listView.setStyleSheet( "QListView {{background-color: {0}}}".format( self.settings['listview_background'].name())) # TODO # Maybe use this for readjusting the border of the focus rectangle # in the listView. Maybe this is a job for QML? # self.listView.setStyleSheet( # "QListView::item:selected { border-color:blue; border-style:outset;" # "border-width:2px; color:black; }") # TableView self.tableView.doubleClicked.connect(self.library_doubleclick) self.tableView.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Interactive) self.tableView.horizontalHeader().setSortIndicator( 2, QtCore.Qt.AscendingOrder) self.tableView.setColumnHidden(0, True) self.tableView.horizontalHeader().setHighlightSections(False) 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().resizeSection(5, 30) self.tableView.horizontalHeader().setStretchLastSection(True) self.tableView.horizontalHeader().sectionClicked.connect( self.lib_ref.tableProxyModel.sort_table_columns) self.lib_ref.tableProxyModel.sort_table_columns(2) self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tableView.customContextMenuRequested.connect( self.generate_library_context_menu) # Keyboard shortcuts self.ksDistractionFree = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+D'), self) self.ksDistractionFree.setContext(QtCore.Qt.ApplicationShortcut) self.ksDistractionFree.activated.connect(self.toggle_distraction_free) self.ksOpenFile = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+O'), self) self.ksOpenFile.setContext(QtCore.Qt.ApplicationShortcut) self.ksOpenFile.activated.connect(self.add_books) self.ksExitAll = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'), self) self.ksExitAll.setContext(QtCore.Qt.ApplicationShortcut) self.ksExitAll.activated.connect(self.closeEvent) self.ksCloseTab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self) self.ksCloseTab.setContext(QtCore.Qt.ApplicationShortcut) self.ksCloseTab.activated.connect(self.tab_close) self.ksDeletePressed = QtWidgets.QShortcut(QtGui.QKeySequence('Delete'), self) self.ksDeletePressed.setContext(QtCore.Qt.ApplicationShortcut) self.ksDeletePressed.activated.connect(self.delete_pressed) self.listView.setFocus() self.open_books_at_startup() # Scan the library @ startup if self.settings['scan_library']: self.settingsDialog.start_library_scan() def open_books_at_startup(self): # Last open books and command line books aren't being opened together # so that command line books are processed last and therefore retain focus # Open last... open books. # Then set the value to None for the next run if self.settings['last_open_books']: files_to_open = {i: None for i in self.settings['last_open_books']} self.open_files(files_to_open) else: self.settings['last_open_tab'] = None # Open input files if specified cl_parser = QtCore.QCommandLineParser() cl_parser.process(QtWidgets.qApp) my_args = cl_parser.positionalArguments() if my_args: file_list = [QtCore.QFileInfo(i).absoluteFilePath() for i in my_args] self.process_post_hoc_files(file_list, True) def process_post_hoc_files(self, file_list, open_files_after_processing): # Takes care of both dragged and dropped files # As well as files sent as command line arguments file_list = [i for i in file_list if os.path.exists(i)] if not file_list: return books = sorter.BookSorter( file_list, ('addition', 'manual'), self.database_path, self.settings, self.temp_dir.path()) parsed_books, errors = books.initiate_threads() if not parsed_books and not open_files_after_processing: return database.DatabaseFunctions(self.database_path).add_to_database(parsed_books) self.lib_ref.generate_model('addition', parsed_books, True) file_dict = {i: None for i in file_list} if open_files_after_processing: self.open_files(file_dict) self.move_on(errors) def open_files(self, path_hash_dictionary): # file_paths is expected to be a dictionary # This allows for threading file opening # Which should speed up multiple file opening # especially @ application start file_paths = [i for i in path_hash_dictionary] for filename in path_hash_dictionary.items(): file_md5 = filename[1] if not file_md5: try: with open(filename[0], 'rb') as current_book: first_bytes = current_book.read(1024 * 32) # First 32KB of the file file_md5 = hashlib.md5(first_bytes).hexdigest() except FileNotFoundError: return # Remove any already open files # Set focus to last file in case only one is open for i in range(1, self.tabWidget.count()): tab_metadata = self.tabWidget.widget(i).metadata if tab_metadata['hash'] == file_md5: file_paths.remove(filename[0]) if not file_paths: self.tabWidget.setCurrentIndex(i) return if not file_paths: return logger.info( 'Attempting to open: ' + ', '.join(file_paths)) contents, errors = sorter.BookSorter( file_paths, ('reading', None), self.database_path, self.settings, self.temp_dir.path()).initiate_threads() if errors: self.display_error_notification(errors) if not contents: logger.error('No parseable files found') return successfully_opened = [] for i in contents: # New tabs are created here # Initial position adjustment is carried out by the tab itself file_data = contents[i] Tab(file_data, self) successfully_opened.append(file_data['path']) logger.info( 'Successfully opened: ' + ', '.join(file_paths)) if self.settings['last_open_tab'] == 'library': self.tabWidget.setCurrentIndex(0) self.listView.setFocus() self.settings['last_open_tab'] = None return for i in range(1, self.tabWidget.count()): this_path = self.tabWidget.widget(i).metadata['path'] if self.settings['last_open_tab'] == this_path: self.tabWidget.setCurrentIndex(i) self.settings['last_open_tab'] = None return self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1) def add_books(self): dialog_prompt = self._translate('Main_UI', 'Add books to database') ebooks_string = self._translate('Main_UI', 'eBooks') opened_files = QtWidgets.QFileDialog.getOpenFileNames( self, dialog_prompt, self.settings['last_open_path'], f'{ebooks_string}({self.available_parsers})') if not opened_files[0]: return self.settingsDialog.okButton.setEnabled(False) self.libraryToolBar.reloadLibraryButton.setEnabled(False) self.settings['last_open_path'] = os.path.dirname(opened_files[0][0]) self.statusBar.setVisible(True) self.sorterProgress.setVisible(True) self.statusMessage.setText(self._translate('Main_UI', 'Adding books...')) self.thread = BackGroundBookAddition( opened_files[0], self.database_path, 'manual', self) self.thread.finished.connect( lambda: self.move_on(self.thread.errors)) self.thread.start() def get_selection(self): selected_indexes = None if self.listView.isVisible(): selected_books = self.lib_ref.itemProxyModel.mapSelectionToSource( self.listView.selectionModel().selection()) selected_indexes = [i.indexes()[0] for i in selected_books] elif self.tableView.isVisible(): selected_books = self.tableView.selectionModel().selectedRows() selected_indexes = [ self.lib_ref.tableProxyModel.mapToSource(i) for i in selected_books] return selected_indexes def delete_books(self, selected_indexes=None): # 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_indexes = self.get_selection() if not selected_indexes: return # Deal with message box selection def ifcontinue(box_button): if box_button.text() != '&Yes': return # Persistent model indexes are required beause deletion mutates the model # Generate and delete by persistent index delete_hashes = [ self.lib_ref.libraryModel.data( i, QtCore.Qt.UserRole + 6) for i in selected_indexes] persistent_indexes = [ QtCore.QPersistentModelIndex(i) for i in selected_indexes] for i in persistent_indexes: self.lib_ref.libraryModel.removeRow(i.row()) # Update the database in the background self.thread = BackGroundBookDeletion( delete_hashes, self.database_path) self.thread.finished.connect(self.move_on) self.thread.start() # Generate a message box to confirm deletion confirm_deletion = QtWidgets.QMessageBox() deletion_prompt = self._translate( 'Main_UI', f'Delete book(s)?') confirm_deletion.setText(deletion_prompt) confirm_deletion.setIcon(QtWidgets.QMessageBox.Question) confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion')) confirm_deletion.setStandardButtons( QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) confirm_deletion.buttonClicked.connect(ifcontinue) confirm_deletion.show() confirm_deletion.exec_() def delete_pressed(self): if self.tabWidget.currentIndex() == 0: self.delete_books() def move_on(self, errors=None): self.settingsDialog.okButton.setEnabled(True) self.settingsDialog.okButton.setToolTip( self._translate('Main_UI', 'Save changes and start library scan')) self.libraryToolBar.reloadLibraryButton.setEnabled(True) self.sorterProgress.setVisible(False) self.sorterProgress.setValue(0) # The errors argument is a list and will only be present # in case of addition and reading if errors: self.display_error_notification(errors) else: if self.libraryToolBar.searchBar.text() == '': self.statusBar.setVisible(False) self.lib_ref.update_proxymodels() self.lib_ref.generate_library_tags() self.statusMessage.setText( str(self.lib_ref.itemProxyModel.rowCount()) + self._translate('Main_UI', ' books')) if not self.settings['perform_culling']: self.cover_functions.load_all_covers() def switch_library_view(self): if self.libraryToolBar.coverViewButton.isChecked(): self.stackedWidget.setCurrentIndex(0) self.libraryToolBar.sortingBoxAction.setVisible(True) else: self.stackedWidget.setCurrentIndex(1) self.libraryToolBar.sortingBoxAction.setVisible(False) self.resizeEvent() def tab_switch(self): try: # Disallow library tab movement # Does not need to be looped since the library # tab can only ever go to position 1 if not self.tabWidget.widget(0).is_library: self.tabWidget.tabBar().moveTab(1, 0) if self.current_tab != 0: self.tabWidget.widget( self.current_tab).update_last_accessed_time() except AttributeError: pass self.current_tab = self.tabWidget.currentIndex() # Hide all side docks whenever a tab is switched for i in range(1, self.tabWidget.count()): self.tabWidget.widget(i).sideDock.setVisible(False) # If library if self.tabWidget.currentIndex() == 0: self.resizeEvent() self.start_culling_timer() if self.settings['show_bars']: self.bookToolBar.hide() self.libraryToolBar.show() if self.lib_ref.itemProxyModel: # Making the proxy model available doesn't affect # memory utilization at all. Bleh. self.statusMessage.setText( str(self.lib_ref.itemProxyModel.rowCount()) + self._translate('Main_UI', ' Books')) if self.libraryToolBar.searchBar.text() != '': self.statusBar.setVisible(True) else: if self.settings['show_bars']: self.bookToolBar.show() self.libraryToolBar.hide() current_tab = self.tabWidget.currentWidget() self.bookToolBar.tocBox.setModel(current_tab.tocModel) self.bookToolBar.tocTreeView.expandAll() current_tab.set_tocBox_index(None, None) # Needed to set the contentView widget background # on first run. Subsequent runs might be redundant, # but it doesn't seem to visibly affect performance self.profile_functions.format_contentView() self.statusBar.setVisible(False) if self.bookToolBar.fontButton.isChecked(): self.bookToolBar.customize_view_on() else: if current_tab.are_we_doing_images_only: self.bookToolBar.searchButton.setVisible(False) self.bookToolBar.annotationButton.setVisible(False) self.bookToolBar.bookSeparator2.setVisible(False) self.bookToolBar.bookSeparator3.setVisible(False) else: self.bookToolBar.searchButton.setVisible(True) self.bookToolBar.annotationButton.setVisible(True) self.bookToolBar.bookSeparator2.setVisible(True) self.bookToolBar.bookSeparator3.setVisible(True) def tab_close(self, tab_index=None): if not tab_index: tab_index = self.tabWidget.currentIndex() if tab_index == 0: return tab_metadata = self.tabWidget.widget(tab_index).metadata self.thread = BackGroundTabUpdate( self.database_path, [tab_metadata]) self.thread.start() self.tabWidget.widget(tab_index).update_last_accessed_time() self.tabWidget.widget(tab_index).deleteLater() self.tabWidget.widget(tab_index).setParent(None) gc.collect() def tab_disallow_library_movement(self, tab_index): # Makes the library tab immovable if tab_index == 0: self.tabWidget.setMovable(False) else: self.tabWidget.setMovable(True) def set_toc_position(self, event=None): currentIndex = self.bookToolBar.tocTreeView.currentIndex() required_position = currentIndex.data(QtCore.Qt.UserRole) if not required_position: return # Initial startup might return a None # The set_content method is universal # It's going to do position tracking current_tab = self.tabWidget.currentWidget() current_tab.set_content(required_position, True, True) def library_doubleclick(self, index): sender = self.sender().objectName() if sender == 'listView': source_index = self.lib_ref.itemProxyModel.mapToSource(index) elif sender == 'tableView': source_index = self.lib_ref.tableProxyModel.mapToSource(index) item = self.lib_ref.libraryModel.item(source_index.row(), 0) metadata = item.data(QtCore.Qt.UserRole + 3) path = {metadata['path']: metadata['hash']} self.open_files(path) def display_error_notification(self, errors): self.statusBar.setVisible(True) self.errorButton.setVisible(True) def show_errors(self): # TODO # Create a separate viewing area for errors # before showing the log self.show_settings(3) self.settingsDialog.aboutTabWidget.setCurrentIndex(1) self.errorButton.setVisible(False) self.statusBar.setVisible(False) def statusbar_visibility(self): if self.sender() == self.libraryToolBar.searchBar: if self.libraryToolBar.searchBar.text() == '': self.statusBar.setVisible(False) else: self.statusBar.setVisible(True) def show_settings(self, stacked_widget_index): if not self.settingsDialog.isVisible(): self.settingsDialog.show() index = self.settingsDialog.listModel.index( stacked_widget_index, 0) self.settingsDialog.listView.setCurrentIndex(index) else: self.settingsDialog.hide() #================================================================== # The contentView modification functions are in the guifunctions # module. self.profile_functions is the reference here. def get_color(self): self.profile_functions.get_color( self.sender().objectName()) def modify_font(self): self.profile_functions.modify_font( self.sender().objectName()) def modify_comic_view(self, key_pressed=None): if key_pressed: signal_sender = None else: signal_sender = self.sender().objectName() self.profile_functions.modify_comic_view( signal_sender, key_pressed) #================================================================= def change_page_view(self, key_pressed=False): # Switch page to whatever index is selected in the tocBox current_tab = self.tabWidget.currentWidget() chapter_number = current_tab.metadata['position']['current_chapter'] # Set zoom mode to best fit to # make the transition less jarring # if the sender isn't the invert colors button if self.sender() != self.bookToolBar.invertButton: self.comic_profile['zoom_mode'] = 'bestFit' # Rotate the image left or right # The double page mode is incompatible with this if self.sender() == self.bookToolBar.rotateLeftButton: current_tab.generate_rotation(-90) self.bookToolBar.doublePageButton.setChecked(False) if self.sender() == self.bookToolBar.rotateRightButton: current_tab.generate_rotation(90) self.bookToolBar.doublePageButton.setChecked(False) if self.sender() == self.bookToolBar.doublePageButton: current_tab.image_rotation = 0 # Toggle Double page mode / manga mode on keypress if key_pressed == QtCore.Qt.Key_D: self.bookToolBar.doublePageButton.setChecked( not self.bookToolBar.doublePageButton.isChecked()) if key_pressed == QtCore.Qt.Key_M: self.bookToolBar.mangaModeButton.setChecked( not self.bookToolBar.mangaModeButton.isChecked()) # Change settings according to the # current state of each of the toolbar buttons self.settings['double_page_mode'] = self.bookToolBar.doublePageButton.isChecked() self.settings['manga_mode'] = self.bookToolBar.mangaModeButton.isChecked() self.settings['invert_colors'] = self.bookToolBar.invertButton.isChecked() current_tab.set_content(chapter_number, False) def generate_library_context_menu(self, position): index = self.sender().indexAt(position) if not index.isValid(): return # It's worth remembering that these are indexes of the libraryModel # and NOT of the proxy models selected_indexes = self.get_selection() context_menu = QtWidgets.QMenu() openAction = context_menu.addAction( self.QImageFactory.get_image('view-readermode'), self._translate('Main_UI', 'Start reading')) editAction = None if len(selected_indexes) == 1: editAction = context_menu.addAction( self.QImageFactory.get_image('edit-rename'), self._translate('Main_UI', 'Edit')) deleteAction = context_menu.addAction( self.QImageFactory.get_image('trash-empty'), self._translate('Main_UI', 'Delete')) readAction = context_menu.addAction( QtGui.QIcon(':/images/checkmark.svg'), self._translate('Main_UI', 'Mark read')) unreadAction = context_menu.addAction( QtGui.QIcon(':/images/xmark.svg'), self._translate('Main_UI', 'Mark unread')) action = context_menu.exec_(self.sender().mapToGlobal(position)) if action == openAction: books_to_open = {} for i in selected_indexes: metadata = self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 3) books_to_open[metadata['path']] = metadata['hash'] self.open_files(books_to_open) if action == editAction: edit_book = selected_indexes[0] is_cover_loaded = self.lib_ref.libraryModel.data( edit_book, QtCore.Qt.UserRole + 8) # Loads a cover in case culling is enabled and the table view is visible if not is_cover_loaded: book_hash = self.lib_ref.libraryModel.data( edit_book, QtCore.Qt.UserRole + 6) book_item = self.lib_ref.libraryModel.item(edit_book.row()) book_cover = database.DatabaseFunctions( self.database_path).fetch_covers_only([book_hash])[0][1] self.cover_functions.cover_loader(book_item, book_cover) cover = self.lib_ref.libraryModel.item( edit_book.row()).icon() title = self.lib_ref.libraryModel.data( edit_book, QtCore.Qt.UserRole) author = self.lib_ref.libraryModel.data( edit_book, QtCore.Qt.UserRole + 1) year = str(self.lib_ref.libraryModel.data( edit_book, QtCore.Qt.UserRole + 2)) # Text cannot be int tags = self.lib_ref.libraryModel.data( edit_book, QtCore.Qt.UserRole + 4) self.metadataDialog.load_book( cover, title, author, year, tags, edit_book) self.metadataDialog.show() if action == deleteAction: self.delete_books(selected_indexes) if action == readAction or action == unreadAction: for i in selected_indexes: metadata = self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 3) book_hash = self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 6) position = metadata['position'] if position: if action == readAction: position['is_read'] = True position['scroll_value'] = 1 elif action == unreadAction: position['is_read'] = False position['current_chapter'] = 1 position['scroll_value'] = 0 else: position = {} if action == readAction: position['is_read'] = True metadata['position'] = position position_perc = None last_accessed_time = None if action == readAction: last_accessed_time = QtCore.QDateTime().currentDateTime() position_perc = 1 self.lib_ref.libraryModel.setData( i, metadata, QtCore.Qt.UserRole + 3) self.lib_ref.libraryModel.setData( i, position_perc, QtCore.Qt.UserRole + 7) self.lib_ref.libraryModel.setData( i, last_accessed_time, QtCore.Qt.UserRole + 12) self.lib_ref.update_proxymodels() database_dict = { 'Position': position, 'LastAccessed': last_accessed_time} database.DatabaseFunctions( self.database_path).modify_metadata(database_dict, book_hash) def generate_library_filter_menu(self, directory_list=None): self.libraryFilterMenu.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_list.append(self._translate('Main_UI', 'Manually Added')) filter_actions = [QtWidgets.QAction(i, self.libraryFilterMenu) for i in filter_list] filter_all = QtWidgets.QAction('All', self.libraryFilterMenu) filter_actions.append(filter_all) for i in filter_actions: i.setCheckable(True) i.setChecked(True) i.triggered.connect(self.set_library_filter) self.libraryFilterMenu.addActions(filter_actions) self.libraryFilterMenu.insertSeparator(filter_all) self.libraryToolBar.libraryFilterButton.setMenu(self.libraryFilterMenu) def set_library_filter(self, event=None): self.active_library_filters = [] something_was_unchecked = False if self.sender(): # Program startup sends a None here if self.sender().text() == 'All': for i in self.libraryFilterMenu.actions(): i.setChecked(self.sender().isChecked()) for i in self.libraryFilterMenu.actions()[:-2]: if i.isChecked(): self.active_library_filters.append(i.text()) else: something_was_unchecked = True if something_was_unchecked: self.libraryFilterMenu.actions()[-1].setChecked(False) else: self.libraryFilterMenu.actions()[-1].setChecked(True) self.lib_ref.update_proxymodels() def toggle_distraction_free(self): self.settings['show_bars'] = not self.settings['show_bars'] if self.tabWidget.count() > 1: self.tabWidget.tabBar().setVisible( self.settings['show_bars']) current_tab = self.tabWidget.currentIndex() if current_tab == 0: self.libraryToolBar.setVisible( not self.libraryToolBar.isVisible()) else: self.bookToolBar.setVisible( not self.bookToolBar.isVisible()) self.start_culling_timer() def start_culling_timer(self): if self.settings['perform_culling']: self.culling_timer.start(90) 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 = 170 # This is size of the QIcon (160 by default) + # minimum margin 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 >= .21875 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 # 12 is the scrollbar width # Larger numbers keep reduce flickering but also increase # the distance from the scrollbar space_left = ( self.listView.viewport().width() - space_occupied - 19) try: layout_extra_space_per_image = space_left // num_images self.listView.setGridSize( QtCore.QSize(default_size + layout_extra_space_per_image, 250)) self.start_culling_timer() except ZeroDivisionError: # Initial resize is ignored return def closeEvent(self, event=None): if event: event.ignore() self.hide() self.metadataDialog.hide() self.settingsDialog.hide() self.definitionDialog.hide() self.temp_dir.remove() for this_dock in self.active_docks: try: this_dock.setVisible(False) except RuntimeError: pass self.settings['last_open_books'] = [] if self.tabWidget.count() > 1: # All tabs must be iterated upon here all_metadata = [] for i in range(1, self.tabWidget.count()): tab_metadata = self.tabWidget.widget(i).metadata all_metadata.append(tab_metadata) if self.settings['remember_files']: self.settings['last_open_books'].append(tab_metadata['path']) Settings(self).save_settings() self.thread = BackGroundTabUpdate( self.database_path, all_metadata) self.thread.finished.connect(self.database_care) self.thread.start() else: Settings(self).save_settings() self.database_care() def database_care(self): database.DatabaseFunctions(self.database_path).vacuum_database() QtWidgets.qApp.exit() def main(): # before we create the app, we hijack QT_AUTO_SCREEN_SCALE_FACTOR to force device scaling to be accurate os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Make icons sharp in HiDPI screen QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) app = QtWidgets.QApplication(sys.argv) app.setApplicationName('Lector') # This is needed for QStandardPaths # and my own hubris # Internationalization support translator = QtCore.QTranslator() translations_found = translator.load( QtCore.QLocale.system(), ':/translations/translations_bin/Lector_') app.installTranslator(translator) translations_out_string = ' (Translations found)' if not translations_found: translations_out_string = ' (No translations found)' print(f'Locale: {QtCore.QLocale.system().name()}' + translations_out_string) form = MainUI() form.show() form.resizeEvent() app.exec_() if __name__ == '__main__': main()