diff --git a/README.md b/README.md index f3d9d00..17e3957 100644 --- a/README.md +++ b/README.md @@ -11,26 +11,37 @@ **KOHighlights** is a utility for viewing and exporting the [Koreader](https://github.com/koreader/koreader)'s highlights to simple text, html, csv or markdown files. -This is a totally re-written application using the Qt framework (PySide). -The original KOHighlights (using the wxPython) can be found -[here](https://github.com/noonkey/KoHighlights), but is considered deprecated.. - -### Screenshots +___ +#### Screenshots +

+ + + + + + + + + + + + + + + + + +

-

- - - - - - -

+___ -## Usage +## Usage/Features * Load items by: * Selecting the reader's drive or any folder that contains books that where opened with Koreader. This will automatically load all the metadata files from all subdirectories. * Drag and drop files or folders. This will load the files and/or all the files inside the folders. @@ -41,26 +52,20 @@ The original KOHighlights (using the wxPython) can be found * Comma-separated values files (.csv) * Markdown files (.md) * View the highlights and various info for a book by selecting it in the list. -* Save the highlights to the "Archive" and view them, even if your reader is not connected. -* Merge highlights/Sync position from the same book that is read in two different devices and/or sync its reading position. To do it you have to: - * Load both metadata (e.g. by scanning your reader's _and_ your tablet's books). - * Select the relevant rows of the (same) book. - * If the book has the same cre_dom_version (version of the CREngine), then the "Merge/Sync" button gets activated and you get the options to sync the highlights or the position or both. -* Merge highlights/Sync position of a book with its archived version - (book's right click menu) -* Show/hide the page, date, chapter or even the highlight text while viewing or saving the highlights of the books. +* Save the highlights to the "Archive" database and view them, even if your reader is not connected. +* Merge highlights/Sync position from the same book that is read in different devices and/or sync its reading position. +* Merge highlights/Sync position of a book with its archived version. +* Show/hide the page, date, chapter or even the highlight (!) text while viewing or exporting the highlights of the books. * Double click or press the Open Book button to view the book in your system's reader. * Delete some or all the highlights of any book. * Clear/reset the .sdr folders with the metadata or the books in the eReader. ### Prerequisites -These plugins must be enabled in KOReader -* Progress sync plugin -* Reading statistics plugin +The progress sync plugin must be enabled in KOReader ### Portable In Windows, KOHighlights can run in Portable mode using a `portable_settings` directory to store its settings, that is located inside the installation directory of the app. -Because of this, it is advised to not install the app inside the `Program Files` folder if you indent to use it as portable. +Because of this, it is advised to not install the app inside the default `Program Files` folder if you indent to use it as portable. There are two ways to start the app in Portable mode: * Run the `KoHighlights Portable.exe` that is located next to the `KoHighlights.exe`. * Run `KoHighlights.exe` with a `-p` argument. @@ -71,13 +76,27 @@ Check the latest release on the [Downloads Page][ReleaseLink]. Read the version history at [App's Page](http://www.noembryo.com/apps.php?kohighlights). ## Dependencies -Should run in any system with Python 2.7.x or 3.x (more testing required) -It needs the [PySide](https://pypi.org/project/PySide/), +* **Source code:** +Should run in any system with Python 3.6+ (more testing required) +It needs the [PySide2](https://pypi.org/project/PySide2/), [BeautifulSoup4](https://pypi.org/project/beautifulsoup4/), -[future](https://pypi.org/project/future/) and +[packaging](https://pypi.org/project/packaging/) and [requests](https://pypi.org/project/requests/) libraries. -In Linux the `libqt4-sql-sqlite` package must be installed. -PySide2/PySide6 are also supported (download the archive from the releases) +In Windows, it might also need the [PyWin32](https://pypi.org/project/PyWin32/) and the [Pypiwin32](https://pypi.org/project/pypiwin32/) libraries. +PySide6 is also supported (download the archive from the releases) +* **Compiled binaries:** + * ***Windows***: + From version 2.x, KOHighlights dropped support for Windows XP. + Can run on any version of Windows from Windows 7 upwards. + For Windows 7, Microsoft Visual C++ 14.0 is required. Get it + [here](https://aka.ms/vs/17/release/vc_redist.x86.exe). + The Windows 7 32bit version also needs the KB2533623 update thats is included in [KB3063858](https://www.microsoft.com/en-us/download/details.aspx?id=47409) ([direct link](https://download.microsoft.com/download/C/9/6/C96CD606-3E05-4E1C-B201-51211AE80B1E/Windows6.1-KB3063858-x86.msu)). + * ***Linux***: + The binary is compiled using Xubuntu 18.04. + Any newer version should work. + + + ## Extra KOHighlights includes SLPPU (a converter between python and lua objects). diff --git a/boot_config.py b/boot_config.py index 80a0ab0..a173842 100644 --- a/boot_config.py +++ b/boot_config.py @@ -1,46 +1,35 @@ # coding=utf-8 -from __future__ import (absolute_import, division, print_function, unicode_literals) - import time import sys, os import traceback import gzip, json from os.path import dirname, join, isdir, expanduser +__author__ = "noEmbryo" + def _(text): # for future gettext support return text + APP_NAME = "KOHighlights" APP_DIR = dirname(os.path.abspath(sys.argv[0])) os.chdir(APP_DIR) # Set the current working directory to the app's directory -PORTABLE = False -PYTHON2 = sys.version_info < (3, 0) - USE_QT6 = False # select between PySide2/Qt5 and Pyside6/Qt6 if both are installed - -if PYTHON2: - from io import open - from codecs import open as c_open - from PySide.QtCore import qVersion +if USE_QT6: + from PySide6.QtCore import qVersion else: - # noinspection PyShadowingBuiltins - unicode, basestring = str, str - c_open = open - if USE_QT6: - from PySide6.QtCore import qVersion - else: - from PySide2.QtCore import qVersion + from PySide2.QtCore import qVersion # noinspection PyTypeChecker qt_version = qVersion().split(".")[0] -QT4 = qt_version == "4" QT5 = qt_version == "5" QT6 = qt_version == "6" if QT6 and QT5 and USE_QT6: QT5 = False +PORTABLE = False if sys.platform == "win32": # Windows import win32api import win32event @@ -65,7 +54,7 @@ def __del__(self): sys.exit(0) try: # noinspection PyUnresolvedReferences - portable_arg = sys.argv[1] if not PYTHON2 else sys.argv[1].decode("mbcs") + portable_arg = sys.argv[1] PORTABLE = portable_arg == "-p" except IndexError: # no arguments in the call pass @@ -86,7 +75,7 @@ def __del__(self): import socket app_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) # Create an abstract socket, by prefixing it with null. - app_socket.bind(str("\0{}_lock_port".format(APP_NAME))) + app_socket.bind(str(f"\0{APP_NAME}_lock_port")) except socket.error: # port in use - another instance is running sys.exit(0) SETTINGS_DIR = join(expanduser("~"), ".config", APP_NAME) @@ -96,10 +85,10 @@ def __del__(self): def except_hook(class_type, value, trace_back): """ Print the error to a log file """ - name = join(SETTINGS_DIR, "error_log_{}.txt".format(time.strftime(str("%Y-%m-%d")))) + name = join(SETTINGS_DIR, f"error_log_{time.strftime(str('%Y-%m-%d'))}.txt") with open(name, "a", encoding="utf8") as log: - log.write("\nCrash@{}\n".format(time.strftime(str("%Y-%m-%d %H:%M:%S")))) - traceback.print_exception(class_type, value, trace_back, file=c_open(name, str("a"))) + log.write(f"\nCrash@{time.strftime(str('%Y-%m-%d %H:%M:%S'))}\n") + traceback.print_exception(class_type, value, trace_back, file=open(name, str("a"))) sys.__excepthook__(class_type, value, trace_back) @@ -108,14 +97,13 @@ def except_hook(class_type, value, trace_back): # noinspection PyBroadException try: with gzip.GzipFile(join(SETTINGS_DIR, "settings.json.gz")) as settings: - j_text = settings.read() if PYTHON2 else settings.read().decode("utf8") - app_config = json.loads(j_text) + app_config = json.loads(settings.read().decode("utf8")) except Exception: # IOError on first run or everything else app_config = {} FIRST_RUN = True -BOOKS_VIEW, HIGHLIGHTS_VIEW = range(2) # app views +BOOKS_VIEW, HIGHLIGHTS_VIEW, SYNC_VIEW = range(3) # app views CHANGE_DB, NEW_DB, RELOAD_DB = range(3) # db change mode (TITLE, AUTHOR, TYPE, PERCENT, RATING, HIGH_COUNT, MODIFIED, PATH) = range(8) # file_table columns @@ -125,7 +113,11 @@ def except_hook(class_type, value, trace_back): (MANY_TEXT, ONE_TEXT, MANY_HTML, ONE_HTML, MANY_CSV, ONE_CSV, MANY_MD, ONE_MD) = range(8) # save_actions DB_MD5, DB_DATE, DB_PATH, DB_DATA = range(4) # db data (columns) -FILTER_ALL, FILTER_HIGH, FILTER_COMM, FILTER_TITLES = range(4) # db data (columns) +FILTER_ALL, FILTER_HIGH, FILTER_COMM, FILTER_TITLES = range(4) # filter type +(THEME_NONE_OLD, THEME_NONE_NEW, THEME_DARK_OLD, THEME_DARK_NEW, + THEME_LIGHT_OLD, THEME_LIGHT_NEW) = range(6) # theme idx +ACT_PAGE, ACT_DATE, ACT_TEXT, ACT_CHAPTER, ACT_COMMENT = range(5) # show items actions + NO_TITLE = _("NO TITLE FOUND") NO_AUTHOR = _("NO AUTHOR FOUND") @@ -133,6 +125,12 @@ def except_hook(class_type, value, trace_back): DO_NOT_SHOW = _("Don't show this again") DB_VERSION = 0 DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +TOOLTIP_MERGE = _("Merge the highlights from the same book in two different\ndevices, " + "and/or sync their reading position.\nActivated only if two entries " + "of the same book are selected.") +TOOLTIP_SYNC = _("Start the sync process for all enabled groups") +SYNC_FILE = join(SETTINGS_DIR, "sync_groups.json") + CSV_HEAD = "Title\tAuthors\tPage\tDate\tChapter\tHighlight\tComment\n" CSV_KEYS = ["title", "authors", "page", "date", "chapter", "text", "comment"] HTML_HEAD = """ diff --git a/gui_about.py b/gui_about.py index aa16794..28d4727 100644 --- a/gui_about.py +++ b/gui_about.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_about.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_about.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_about.ui' applies. # -# Created: Thu Feb 6 12:55:05 2020 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_About(object): def setupUi(self, About): @@ -16,28 +17,28 @@ def setupUi(self, About): About.resize(480, 560) About.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) About.setModal(False) - self.verticalLayout = QtGui.QVBoxLayout(About) + self.verticalLayout = QtWidgets.QVBoxLayout(About) self.verticalLayout.setObjectName("verticalLayout") - self.about_tabs = QtGui.QTabWidget(About) - self.about_tabs.setTabShape(QtGui.QTabWidget.Rounded) + self.about_tabs = QtWidgets.QTabWidget(About) + self.about_tabs.setTabShape(QtWidgets.QTabWidget.Rounded) self.about_tabs.setObjectName("about_tabs") - self.info_tab = QtGui.QWidget() + self.info_tab = QtWidgets.QWidget() self.info_tab.setObjectName("info_tab") - self.verticalLayout_2 = QtGui.QVBoxLayout(self.info_tab) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.info_tab) self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.scrollArea_2 = QtGui.QScrollArea(self.info_tab) + self.scrollArea_2 = QtWidgets.QScrollArea(self.info_tab) self.scrollArea_2.setStyleSheet("QScrollArea {background-color:transparent;}") self.scrollArea_2.setWidgetResizable(True) self.scrollArea_2.setObjectName("scrollArea_2") - self.scrollAreaWidgetContents_2 = QtGui.QWidget() + self.scrollAreaWidgetContents_2 = QtWidgets.QWidget() self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 454, 485)) self.scrollAreaWidgetContents_2.setStyleSheet("background-color:transparent;") self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2") - self.verticalLayout_6 = QtGui.QVBoxLayout(self.scrollAreaWidgetContents_2) + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_2) self.verticalLayout_6.setContentsMargins(6, 0, 6, 0) self.verticalLayout_6.setObjectName("verticalLayout_6") - self.text_lbl = QtGui.QLabel(self.scrollAreaWidgetContents_2) + self.text_lbl = QtWidgets.QLabel(self.scrollAreaWidgetContents_2) self.text_lbl.setText("") self.text_lbl.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.text_lbl.setWordWrap(True) @@ -47,12 +48,12 @@ def setupUi(self, About): self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2) self.verticalLayout_2.addWidget(self.scrollArea_2) self.about_tabs.addTab(self.info_tab, "") - self.log_tab = QtGui.QWidget() + self.log_tab = QtWidgets.QWidget() self.log_tab.setObjectName("log_tab") - self.verticalLayout_8 = QtGui.QVBoxLayout(self.log_tab) + self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.log_tab) self.verticalLayout_8.setObjectName("verticalLayout_8") - self.log_txt = QtGui.QPlainTextEdit(self.log_tab) - self.log_txt.setFrameShape(QtGui.QFrame.WinPanel) + self.log_txt = QtWidgets.QPlainTextEdit(self.log_tab) + self.log_txt.setFrameShape(QtWidgets.QFrame.WinPanel) self.log_txt.setDocumentTitle("") self.log_txt.setUndoRedoEnabled(False) self.log_txt.setReadOnly(True) @@ -61,27 +62,27 @@ def setupUi(self, About): self.verticalLayout_8.addWidget(self.log_txt) self.about_tabs.addTab(self.log_tab, "") self.verticalLayout.addWidget(self.about_tabs) - self.btn_box = QtGui.QFrame(About) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.btn_box = QtWidgets.QFrame(About) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.btn_box.sizePolicy().hasHeightForWidth()) self.btn_box.setSizePolicy(sizePolicy) self.btn_box.setObjectName("btn_box") - self.horizontalLayout = QtGui.QHBoxLayout(self.btn_box) + self.horizontalLayout = QtWidgets.QHBoxLayout(self.btn_box) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setObjectName("horizontalLayout") - self.about_qt_btn = QtGui.QPushButton(self.btn_box) + self.about_qt_btn = QtWidgets.QPushButton(self.btn_box) self.about_qt_btn.setObjectName("about_qt_btn") self.horizontalLayout.addWidget(self.about_qt_btn) - spacerItem = QtGui.QSpacerItem(92, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(92, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) - self.updates_btn = QtGui.QPushButton(self.btn_box) + self.updates_btn = QtWidgets.QPushButton(self.btn_box) self.updates_btn.setObjectName("updates_btn") self.horizontalLayout.addWidget(self.updates_btn) - spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) - self.close_btn = QtGui.QPushButton(self.btn_box) + self.close_btn = QtWidgets.QPushButton(self.btn_box) self.close_btn.setObjectName("close_btn") self.horizontalLayout.addWidget(self.close_btn) self.verticalLayout.addWidget(self.btn_box) @@ -92,11 +93,11 @@ def setupUi(self, About): QtCore.QMetaObject.connectSlotsByName(About) def retranslateUi(self, About): - self.about_tabs.setTabText(self.about_tabs.indexOf(self.info_tab), QtGui.QApplication.translate("About", "Information", None, QtGui.QApplication.UnicodeUTF8)) - self.about_tabs.setTabText(self.about_tabs.indexOf(self.log_tab), QtGui.QApplication.translate("About", "Log", None, QtGui.QApplication.UnicodeUTF8)) - self.about_qt_btn.setText(QtGui.QApplication.translate("About", "About Qt", None, QtGui.QApplication.UnicodeUTF8)) - self.updates_btn.setToolTip(QtGui.QApplication.translate("About", "Check online for an updated version", None, QtGui.QApplication.UnicodeUTF8)) - self.updates_btn.setText(QtGui.QApplication.translate("About", "Check for Updates", None, QtGui.QApplication.UnicodeUTF8)) - self.close_btn.setText(QtGui.QApplication.translate("About", "Close", None, QtGui.QApplication.UnicodeUTF8)) + self.about_tabs.setTabText(self.about_tabs.indexOf(self.info_tab), QtWidgets.QApplication.translate("About", "Information", None, -1)) + self.about_tabs.setTabText(self.about_tabs.indexOf(self.log_tab), QtWidgets.QApplication.translate("About", "Log", None, -1)) + self.about_qt_btn.setText(QtWidgets.QApplication.translate("About", "About Qt", None, -1)) + self.updates_btn.setToolTip(QtWidgets.QApplication.translate("About", "Check online for an updated version", None, -1)) + self.updates_btn.setText(QtWidgets.QApplication.translate("About", "Check for Updates", None, -1)) + self.close_btn.setText(QtWidgets.QApplication.translate("About", "Close", None, -1)) import images_rc diff --git a/gui_auto_info.py b/gui_auto_info.py index 2aff20f..a99639f 100644 --- a/gui_auto_info.py +++ b/gui_auto_info.py @@ -1,37 +1,38 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_auto_info.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_auto_info.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_auto_info.ui' applies. # -# Created: Thu Nov 24 15:53:16 2022 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_AutoInfo(object): def setupUi(self, AutoInfo): AutoInfo.setObjectName("AutoInfo") AutoInfo.setWindowModality(QtCore.Qt.NonModal) AutoInfo.resize(300, 100) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(AutoInfo.sizePolicy().hasHeightForWidth()) AutoInfo.setSizePolicy(sizePolicy) AutoInfo.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) AutoInfo.setModal(True) - self.verticalLayout = QtGui.QVBoxLayout(AutoInfo) + self.verticalLayout = QtWidgets.QVBoxLayout(AutoInfo) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setObjectName("verticalLayout") - self.label = QtGui.QLabel(AutoInfo) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.label = QtWidgets.QLabel(AutoInfo) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) self.label.setSizePolicy(sizePolicy) - self.label.setFrameShape(QtGui.QFrame.Box) - self.label.setFrameShadow(QtGui.QFrame.Raised) + self.label.setFrameShape(QtWidgets.QFrame.Box) + self.label.setFrameShadow(QtWidgets.QFrame.Raised) self.label.setText("") self.label.setTextFormat(QtCore.Qt.AutoText) self.label.setAlignment(QtCore.Qt.AlignCenter) @@ -43,6 +44,6 @@ def setupUi(self, AutoInfo): QtCore.QMetaObject.connectSlotsByName(AutoInfo) def retranslateUi(self, AutoInfo): - AutoInfo.setWindowTitle(QtGui.QApplication.translate("AutoInfo", "Info", None, QtGui.QApplication.UnicodeUTF8)) + AutoInfo.setWindowTitle(QtWidgets.QApplication.translate("AutoInfo", "Info", None, -1)) import images_rc diff --git a/gui_edit.py b/gui_edit.py index 092f663..f4918db 100644 --- a/gui_edit.py +++ b/gui_edit.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_edit.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_edit.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_edit.ui' applies. # -# Created: Sun Mar 31 18:17:28 2019 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_TextDialog(object): def setupUi(self, TextDialog): @@ -16,29 +17,29 @@ def setupUi(self, TextDialog): TextDialog.resize(360, 180) TextDialog.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) TextDialog.setModal(False) - self.verticalLayout = QtGui.QVBoxLayout(TextDialog) + self.verticalLayout = QtWidgets.QVBoxLayout(TextDialog) self.verticalLayout.setObjectName("verticalLayout") - self.high_edit_txt = QtGui.QTextEdit(TextDialog) - self.high_edit_txt.setFrameShape(QtGui.QFrame.WinPanel) + self.high_edit_txt = QtWidgets.QTextEdit(TextDialog) + self.high_edit_txt.setFrameShape(QtWidgets.QFrame.WinPanel) self.high_edit_txt.setAcceptRichText(False) self.high_edit_txt.setObjectName("high_edit_txt") self.verticalLayout.addWidget(self.high_edit_txt) - self.btn_box = QtGui.QFrame(TextDialog) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.btn_box = QtWidgets.QFrame(TextDialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.btn_box.sizePolicy().hasHeightForWidth()) self.btn_box.setSizePolicy(sizePolicy) self.btn_box.setObjectName("btn_box") - self.horizontalLayout = QtGui.QHBoxLayout(self.btn_box) + self.horizontalLayout = QtWidgets.QHBoxLayout(self.btn_box) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setObjectName("horizontalLayout") - spacerItem = QtGui.QSpacerItem(175, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(175, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) - self.ok_btn = QtGui.QPushButton(self.btn_box) + self.ok_btn = QtWidgets.QPushButton(self.btn_box) self.ok_btn.setObjectName("ok_btn") self.horizontalLayout.addWidget(self.ok_btn) - self.cancel_btn = QtGui.QPushButton(self.btn_box) + self.cancel_btn = QtWidgets.QPushButton(self.btn_box) self.cancel_btn.setObjectName("cancel_btn") self.horizontalLayout.addWidget(self.cancel_btn) self.verticalLayout.addWidget(self.btn_box) @@ -49,8 +50,8 @@ def setupUi(self, TextDialog): QtCore.QMetaObject.connectSlotsByName(TextDialog) def retranslateUi(self, TextDialog): - self.ok_btn.setToolTip(QtGui.QApplication.translate("TextDialog", "Check online for an updated version", None, QtGui.QApplication.UnicodeUTF8)) - self.ok_btn.setText(QtGui.QApplication.translate("TextDialog", "OK", None, QtGui.QApplication.UnicodeUTF8)) - self.cancel_btn.setText(QtGui.QApplication.translate("TextDialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) + self.ok_btn.setToolTip(QtWidgets.QApplication.translate("TextDialog", "Check online for an updated version", None, -1)) + self.ok_btn.setText(QtWidgets.QApplication.translate("TextDialog", "OK", None, -1)) + self.cancel_btn.setText(QtWidgets.QApplication.translate("TextDialog", "Cancel", None, -1)) import images_rc diff --git a/gui_filter.py b/gui_filter.py index a09243b..4f456bd 100644 --- a/gui_filter.py +++ b/gui_filter.py @@ -1,31 +1,32 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_filter.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_filter.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_filter.ui' applies. # -# Created: Thu Apr 6 23:38:59 2023 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_Filter(object): def setupUi(self, Filter): Filter.setObjectName("Filter") Filter.resize(215, 66) Filter.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.verticalLayout = QtGui.QVBoxLayout(Filter) + self.verticalLayout = QtWidgets.QVBoxLayout(Filter) self.verticalLayout.setContentsMargins(4, 4, 4, 4) self.verticalLayout.setObjectName("verticalLayout") - self.filter_frm1 = QtGui.QFrame(Filter) - self.filter_frm1.setFrameShape(QtGui.QFrame.StyledPanel) - self.filter_frm1.setFrameShadow(QtGui.QFrame.Raised) + self.filter_frm1 = QtWidgets.QFrame(Filter) + self.filter_frm1.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.filter_frm1.setFrameShadow(QtWidgets.QFrame.Raised) self.filter_frm1.setObjectName("filter_frm1") - self.horizontalLayout_4 = QtGui.QHBoxLayout(self.filter_frm1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.filter_frm1) self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.filter_txt = QtGui.QLineEdit(self.filter_frm1) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + self.filter_txt = QtWidgets.QLineEdit(self.filter_frm1) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.filter_txt.sizePolicy().hasHeightForWidth()) @@ -33,8 +34,8 @@ def setupUi(self, Filter): self.filter_txt.setText("") self.filter_txt.setObjectName("filter_txt") self.horizontalLayout_4.addWidget(self.filter_txt) - self.filter_btn = QtGui.QPushButton(self.filter_frm1) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + self.filter_btn = QtWidgets.QPushButton(self.filter_frm1) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.filter_btn.sizePolicy().hasHeightForWidth()) @@ -45,28 +46,28 @@ def setupUi(self, Filter): self.filter_btn.setObjectName("filter_btn") self.horizontalLayout_4.addWidget(self.filter_btn) self.verticalLayout.addWidget(self.filter_frm1) - self.filter_frm2 = QtGui.QFrame(Filter) - self.filter_frm2.setFrameShape(QtGui.QFrame.StyledPanel) - self.filter_frm2.setFrameShadow(QtGui.QFrame.Raised) + self.filter_frm2 = QtWidgets.QFrame(Filter) + self.filter_frm2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.filter_frm2.setFrameShadow(QtWidgets.QFrame.Raised) self.filter_frm2.setObjectName("filter_frm2") - self.horizontalLayout = QtGui.QHBoxLayout(self.filter_frm2) + self.horizontalLayout = QtWidgets.QHBoxLayout(self.filter_frm2) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setObjectName("horizontalLayout") - self.filter_box = QtGui.QComboBox(self.filter_frm2) + self.filter_box = QtWidgets.QComboBox(self.filter_frm2) self.filter_box.setObjectName("filter_box") self.filter_box.addItem("") self.filter_box.addItem("") self.filter_box.addItem("") self.filter_box.addItem("") self.horizontalLayout.addWidget(self.filter_box) - self.filtered_lbl = QtGui.QLabel(self.filter_frm2) + self.filtered_lbl = QtWidgets.QLabel(self.filter_frm2) self.filtered_lbl.setText("") self.filtered_lbl.setObjectName("filtered_lbl") self.horizontalLayout.addWidget(self.filtered_lbl) - spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) - self.clear_filter_btn = QtGui.QPushButton(self.filter_frm2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + self.clear_filter_btn = QtWidgets.QPushButton(self.filter_frm2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.clear_filter_btn.sizePolicy().hasHeightForWidth()) @@ -83,17 +84,17 @@ def setupUi(self, Filter): QtCore.QMetaObject.connectSlotsByName(Filter) def retranslateUi(self, Filter): - self.filter_txt.setToolTip(QtGui.QApplication.translate("Filter", "Type the keywords to filter the visible items", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_txt.setPlaceholderText(QtGui.QApplication.translate("Filter", "Type here to filter...", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_btn.setToolTip(QtGui.QApplication.translate("Filter", "Set filter", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_btn.setText(QtGui.QApplication.translate("Filter", "Filter", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_box.setToolTip(QtGui.QApplication.translate("Filter", "Select where to search for the keywords", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_box.setItemText(0, QtGui.QApplication.translate("Filter", "Filter All:", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_box.setItemText(1, QtGui.QApplication.translate("Filter", "Filter Highlights:", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_box.setItemText(2, QtGui.QApplication.translate("Filter", "Filter Comments:", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_box.setItemText(3, QtGui.QApplication.translate("Filter", "Filter Book Titles:", None, QtGui.QApplication.UnicodeUTF8)) - self.clear_filter_btn.setToolTip(QtGui.QApplication.translate("Filter", "Clear the filter field", None, QtGui.QApplication.UnicodeUTF8)) - self.clear_filter_btn.setStatusTip(QtGui.QApplication.translate("Filter", "Clears the filter field", None, QtGui.QApplication.UnicodeUTF8)) - self.clear_filter_btn.setText(QtGui.QApplication.translate("Filter", "Clear", None, QtGui.QApplication.UnicodeUTF8)) + self.filter_txt.setToolTip(QtWidgets.QApplication.translate("Filter", "Type the keywords to filter the visible items", None, -1)) + self.filter_txt.setPlaceholderText(QtWidgets.QApplication.translate("Filter", "Type here to filter...", None, -1)) + self.filter_btn.setToolTip(QtWidgets.QApplication.translate("Filter", "Set filter", None, -1)) + self.filter_btn.setText(QtWidgets.QApplication.translate("Filter", "Filter", None, -1)) + self.filter_box.setToolTip(QtWidgets.QApplication.translate("Filter", "Select where to search for the keywords", None, -1)) + self.filter_box.setItemText(0, QtWidgets.QApplication.translate("Filter", "Filter All:", None, -1)) + self.filter_box.setItemText(1, QtWidgets.QApplication.translate("Filter", "Filter Highlights:", None, -1)) + self.filter_box.setItemText(2, QtWidgets.QApplication.translate("Filter", "Filter Comments:", None, -1)) + self.filter_box.setItemText(3, QtWidgets.QApplication.translate("Filter", "Filter Book Titles:", None, -1)) + self.clear_filter_btn.setToolTip(QtWidgets.QApplication.translate("Filter", "Clear the filter field", None, -1)) + self.clear_filter_btn.setStatusTip(QtWidgets.QApplication.translate("Filter", "Clears the filter field", None, -1)) + self.clear_filter_btn.setText(QtWidgets.QApplication.translate("Filter", "Clear", None, -1)) import images_rc diff --git a/gui_main.py b/gui_main.py index 1a5b1bd..ea6243f 100644 --- a/gui_main.py +++ b/gui_main.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_main.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_main.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_main.ui' applies. # -# Created: Fri Jan 12 21:47:31 2024 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_Base(object): def setupUi(self, Base): @@ -18,50 +19,50 @@ def setupUi(self, Base): Base.setWindowIcon(icon) Base.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) Base.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.centralwidget = QtGui.QWidget(Base) + self.centralwidget = QtWidgets.QWidget(Base) self.centralwidget.setObjectName("centralwidget") - self.verticalLayout_2 = QtGui.QVBoxLayout(self.centralwidget) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget) self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.views = QtGui.QStackedWidget(self.centralwidget) + self.views = QtWidgets.QStackedWidget(self.centralwidget) self.views.setObjectName("views") - self.books_pg = QtGui.QWidget() + self.books_pg = QtWidgets.QWidget() self.books_pg.setObjectName("books_pg") - self.verticalLayout_3 = QtGui.QVBoxLayout(self.books_pg) + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.books_pg) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) self.verticalLayout_3.setObjectName("verticalLayout_3") - self.splitter = QtGui.QSplitter(self.books_pg) + self.splitter = QtWidgets.QSplitter(self.books_pg) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.file_table = DropTableWidget(self.splitter) self.file_table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.file_table.setFrameShape(QtGui.QFrame.WinPanel) - self.file_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) - self.file_table.setDragDropMode(QtGui.QAbstractItemView.DropOnly) + self.file_table.setFrameShape(QtWidgets.QFrame.WinPanel) + self.file_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.file_table.setDragDropMode(QtWidgets.QAbstractItemView.DropOnly) self.file_table.setDefaultDropAction(QtCore.Qt.CopyAction) - self.file_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.file_table.setHorizontalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + self.file_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.file_table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.file_table.setWordWrap(False) self.file_table.setCornerButtonEnabled(False) self.file_table.setColumnCount(8) self.file_table.setObjectName("file_table") self.file_table.setColumnCount(8) self.file_table.setRowCount(0) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(0, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(1, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(2, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(3, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(4, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(5, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(6, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.file_table.setHorizontalHeaderItem(7, item) self.file_table.horizontalHeader().setDefaultSectionSize(22) self.file_table.horizontalHeader().setHighlightSections(False) @@ -70,151 +71,151 @@ def setupUi(self, Base): self.file_table.verticalHeader().setDefaultSectionSize(22) self.file_table.verticalHeader().setHighlightSections(True) self.file_table.verticalHeader().setMinimumSectionSize(22) - self.frame = QtGui.QFrame(self.splitter) - self.frame.setFrameShape(QtGui.QFrame.WinPanel) - self.frame.setFrameShadow(QtGui.QFrame.Sunken) + self.frame = QtWidgets.QFrame(self.splitter) + self.frame.setFrameShape(QtWidgets.QFrame.WinPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Sunken) self.frame.setObjectName("frame") - self.verticalLayout = QtGui.QVBoxLayout(self.frame) + self.verticalLayout = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout.setSpacing(3) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setObjectName("verticalLayout") - self.header = QtGui.QWidget(self.frame) + self.header = QtWidgets.QWidget(self.frame) self.header.setObjectName("header") - self.horizontalLayout = QtGui.QHBoxLayout(self.header) + self.horizontalLayout = QtWidgets.QHBoxLayout(self.header) self.horizontalLayout.setContentsMargins(0, 0, -1, 0) self.horizontalLayout.setObjectName("horizontalLayout") - self.fold_btn = QtGui.QToolButton(self.header) + self.fold_btn = QtWidgets.QToolButton(self.header) self.fold_btn.setStyleSheet("QToolButton{border:none;}") self.fold_btn.setCheckable(True) self.fold_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) self.fold_btn.setArrowType(QtCore.Qt.DownArrow) self.fold_btn.setObjectName("fold_btn") self.horizontalLayout.addWidget(self.fold_btn) - self.frame_2 = QtGui.QFrame(self.header) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + self.frame_2 = QtWidgets.QFrame(self.header) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) self.frame_2.setSizePolicy(sizePolicy) - self.frame_2.setFrameShape(QtGui.QFrame.HLine) - self.frame_2.setFrameShadow(QtGui.QFrame.Sunken) + self.frame_2.setFrameShape(QtWidgets.QFrame.HLine) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Sunken) self.frame_2.setLineWidth(1) self.frame_2.setObjectName("frame_2") self.horizontalLayout.addWidget(self.frame_2) self.verticalLayout.addWidget(self.header) - self.book_info = QtGui.QFrame(self.frame) - self.book_info.setFrameShape(QtGui.QFrame.StyledPanel) - self.book_info.setFrameShadow(QtGui.QFrame.Raised) + self.book_info = QtWidgets.QFrame(self.frame) + self.book_info.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.book_info.setFrameShadow(QtWidgets.QFrame.Raised) self.book_info.setObjectName("book_info") - self.gridLayout = QtGui.QGridLayout(self.book_info) + self.gridLayout = QtWidgets.QGridLayout(self.book_info) self.gridLayout.setContentsMargins(6, 0, 6, 0) self.gridLayout.setObjectName("gridLayout") - self.title_lbl = QtGui.QLabel(self.book_info) + self.title_lbl = QtWidgets.QLabel(self.book_info) self.title_lbl.setObjectName("title_lbl") self.gridLayout.addWidget(self.title_lbl, 0, 0, 1, 1) - self.series_lbl = QtGui.QLabel(self.book_info) + self.series_lbl = QtWidgets.QLabel(self.book_info) self.series_lbl.setObjectName("series_lbl") self.gridLayout.addWidget(self.series_lbl, 2, 0, 1, 1) - self.author_lbl = QtGui.QLabel(self.book_info) + self.author_lbl = QtWidgets.QLabel(self.book_info) self.author_lbl.setObjectName("author_lbl") self.gridLayout.addWidget(self.author_lbl, 1, 0, 1, 1) - self.lang_lbl = QtGui.QLabel(self.book_info) + self.lang_lbl = QtWidgets.QLabel(self.book_info) self.lang_lbl.setObjectName("lang_lbl") self.gridLayout.addWidget(self.lang_lbl, 4, 0, 1, 1) - self.pages_lbl = QtGui.QLabel(self.book_info) + self.pages_lbl = QtWidgets.QLabel(self.book_info) self.pages_lbl.setObjectName("pages_lbl") self.gridLayout.addWidget(self.pages_lbl, 4, 2, 1, 1) - self.lang_txt = QtGui.QLineEdit(self.book_info) + self.lang_txt = QtWidgets.QLineEdit(self.book_info) self.lang_txt.setReadOnly(True) self.lang_txt.setObjectName("lang_txt") self.gridLayout.addWidget(self.lang_txt, 4, 1, 1, 1) - self.pages_txt = QtGui.QLineEdit(self.book_info) + self.pages_txt = QtWidgets.QLineEdit(self.book_info) self.pages_txt.setReadOnly(True) self.pages_txt.setObjectName("pages_txt") self.gridLayout.addWidget(self.pages_txt, 4, 3, 1, 1) - self.tags_lbl = QtGui.QLabel(self.book_info) + self.tags_lbl = QtWidgets.QLabel(self.book_info) self.tags_lbl.setObjectName("tags_lbl") self.gridLayout.addWidget(self.tags_lbl, 3, 0, 1, 1) - self.description_btn = QtGui.QToolButton(self.book_info) + self.description_btn = QtWidgets.QToolButton(self.book_info) icon1 = QtGui.QIcon() icon1.addPixmap(QtGui.QPixmap(":/stuff/description.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.description_btn.setIcon(icon1) self.description_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) self.description_btn.setObjectName("description_btn") self.gridLayout.addWidget(self.description_btn, 4, 4, 1, 1) - self.tags_txt = QtGui.QLineEdit(self.book_info) + self.tags_txt = QtWidgets.QLineEdit(self.book_info) self.tags_txt.setReadOnly(True) self.tags_txt.setObjectName("tags_txt") self.gridLayout.addWidget(self.tags_txt, 3, 1, 1, 4) - self.series_txt = QtGui.QLineEdit(self.book_info) + self.series_txt = QtWidgets.QLineEdit(self.book_info) self.series_txt.setReadOnly(True) self.series_txt.setObjectName("series_txt") self.gridLayout.addWidget(self.series_txt, 2, 1, 1, 4) - self.author_txt = QtGui.QLineEdit(self.book_info) + self.author_txt = QtWidgets.QLineEdit(self.book_info) self.author_txt.setReadOnly(True) self.author_txt.setObjectName("author_txt") self.gridLayout.addWidget(self.author_txt, 1, 1, 1, 4) - self.title_txt = QtGui.QLineEdit(self.book_info) + self.title_txt = QtWidgets.QLineEdit(self.book_info) self.title_txt.setReadOnly(True) self.title_txt.setObjectName("title_txt") self.gridLayout.addWidget(self.title_txt, 0, 1, 1, 4) - self.review_lbl = QtGui.QLabel(self.book_info) + self.review_lbl = QtWidgets.QLabel(self.book_info) self.review_lbl.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.review_lbl.setObjectName("review_lbl") self.gridLayout.addWidget(self.review_lbl, 5, 0, 1, 1) - self.review_txt = QtGui.QLabel(self.book_info) - self.review_txt.setStyleSheet("background-color: rgb(255, 255, 255);") - self.review_txt.setFrameShape(QtGui.QFrame.NoFrame) + self.review_txt = QtWidgets.QLabel(self.book_info) + self.review_txt.setFrameShape(QtWidgets.QFrame.NoFrame) self.review_txt.setText("") self.review_txt.setWordWrap(True) self.review_txt.setObjectName("review_txt") self.gridLayout.addWidget(self.review_txt, 5, 1, 1, 4) self.verticalLayout.addWidget(self.book_info) - self.high_list = QtGui.QListWidget(self.frame) + self.high_list = QtWidgets.QListWidget(self.frame) self.high_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.high_list.setFrameShape(QtGui.QFrame.WinPanel) - self.high_list.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) - self.high_list.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) - self.high_list.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.high_list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + self.high_list.setFrameShape(QtWidgets.QFrame.WinPanel) + self.high_list.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.high_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.high_list.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.high_list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.high_list.setWordWrap(True) self.high_list.setObjectName("high_list") self.verticalLayout.addWidget(self.high_list) self.verticalLayout_3.addWidget(self.splitter) self.views.addWidget(self.books_pg) - self.highlights_pg = QtGui.QWidget() + self.highlights_pg = QtWidgets.QWidget() self.highlights_pg.setObjectName("highlights_pg") - self.verticalLayout_4 = QtGui.QVBoxLayout(self.highlights_pg) + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.highlights_pg) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) self.verticalLayout_4.setObjectName("verticalLayout_4") - self.high_table = QtGui.QTableWidget(self.highlights_pg) + self.high_table = QtWidgets.QTableWidget(self.highlights_pg) self.high_table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.high_table.setFrameShape(QtGui.QFrame.WinPanel) - self.high_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) - self.high_table.setDragDropMode(QtGui.QAbstractItemView.DropOnly) + self.high_table.setFrameShape(QtWidgets.QFrame.WinPanel) + self.high_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.high_table.setDragDropMode(QtWidgets.QAbstractItemView.DropOnly) self.high_table.setDefaultDropAction(QtCore.Qt.CopyAction) - self.high_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.high_table.setHorizontalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + self.high_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.high_table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.high_table.setWordWrap(False) self.high_table.setCornerButtonEnabled(False) self.high_table.setColumnCount(8) self.high_table.setObjectName("high_table") self.high_table.setColumnCount(8) self.high_table.setRowCount(0) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(0, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(1, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(2, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(3, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(4, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(5, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(6, item) - item = QtGui.QTableWidgetItem() + item = QtWidgets.QTableWidgetItem() self.high_table.setHorizontalHeaderItem(7, item) self.high_table.horizontalHeader().setHighlightSections(False) self.high_table.horizontalHeader().setMinimumSectionSize(22) @@ -225,13 +226,35 @@ def setupUi(self, Base): self.high_table.verticalHeader().setMinimumSectionSize(22) self.verticalLayout_4.addWidget(self.high_table) self.views.addWidget(self.highlights_pg) + self.sync_pg = QtWidgets.QWidget() + self.sync_pg.setObjectName("sync_pg") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.sync_pg) + self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.sync_table = XTableWidget(self.sync_pg) + self.sync_table.setAcceptDrops(True) + self.sync_table.setDragEnabled(True) + self.sync_table.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) + self.sync_table.setDefaultDropAction(QtCore.Qt.MoveAction) + self.sync_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.sync_table.setObjectName("sync_table") + self.sync_table.setColumnCount(1) + self.sync_table.setRowCount(0) + item = QtWidgets.QTableWidgetItem() + self.sync_table.setHorizontalHeaderItem(0, item) + self.sync_table.horizontalHeader().setVisible(False) + self.sync_table.horizontalHeader().setStretchLastSection(True) + self.sync_table.verticalHeader().setDefaultSectionSize(90) + self.sync_table.verticalHeader().setMinimumSectionSize(90) + self.verticalLayout_5.addWidget(self.sync_table) + self.views.addWidget(self.sync_pg) self.verticalLayout_2.addWidget(self.views) Base.setCentralWidget(self.centralwidget) - self.statusbar = QtGui.QStatusBar(Base) + self.statusbar = QtWidgets.QStatusBar(Base) self.statusbar.setStyleSheet("QStatusBar{padding-left:8px;font-weight:bold;}") self.statusbar.setObjectName("statusbar") Base.setStatusBar(self.statusbar) - self.tool_bar = QtGui.QToolBar(Base) + self.tool_bar = QtWidgets.QToolBar(Base) self.tool_bar.setWindowTitle("toolBar") self.tool_bar.setMovable(True) self.tool_bar.setAllowedAreas(QtCore.Qt.BottomToolBarArea|QtCore.Qt.TopToolBarArea) @@ -239,15 +262,15 @@ def setupUi(self, Base): self.tool_bar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.tool_bar.setObjectName("tool_bar") Base.addToolBar(QtCore.Qt.TopToolBarArea, self.tool_bar) - self.act_english = QtGui.QAction(Base) + self.act_english = QtWidgets.QAction(Base) self.act_english.setCheckable(True) self.act_english.setChecked(False) self.act_english.setObjectName("act_english") - self.act_greek = QtGui.QAction(Base) + self.act_greek = QtWidgets.QAction(Base) self.act_greek.setCheckable(True) self.act_greek.setChecked(False) self.act_greek.setObjectName("act_greek") - self.act_view_book = QtGui.QAction(Base) + self.act_view_book = QtWidgets.QAction(Base) icon2 = QtGui.QIcon() icon2.addPixmap(QtGui.QPixmap(":/stuff/files_view.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.act_view_book.setIcon(icon2) @@ -259,36 +282,37 @@ def setupUi(self, Base): def retranslateUi(self, Base): self.file_table.setSortingEnabled(True) - self.file_table.horizontalHeaderItem(0).setText(QtGui.QApplication.translate("Base", "Title", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(1).setText(QtGui.QApplication.translate("Base", "Author", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(2).setText(QtGui.QApplication.translate("Base", "Type", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(3).setText(QtGui.QApplication.translate("Base", "Percent", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(4).setText(QtGui.QApplication.translate("Base", "Rating", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(5).setText(QtGui.QApplication.translate("Base", "Highlights", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(6).setText(QtGui.QApplication.translate("Base", "Modified", None, QtGui.QApplication.UnicodeUTF8)) - self.file_table.horizontalHeaderItem(7).setText(QtGui.QApplication.translate("Base", "Path", None, QtGui.QApplication.UnicodeUTF8)) - self.fold_btn.setText(QtGui.QApplication.translate("Base", "Hide Book Info", None, QtGui.QApplication.UnicodeUTF8)) - self.title_lbl.setText(QtGui.QApplication.translate("Base", "Title", None, QtGui.QApplication.UnicodeUTF8)) - self.series_lbl.setText(QtGui.QApplication.translate("Base", "Series", None, QtGui.QApplication.UnicodeUTF8)) - self.author_lbl.setText(QtGui.QApplication.translate("Base", "Author", None, QtGui.QApplication.UnicodeUTF8)) - self.lang_lbl.setText(QtGui.QApplication.translate("Base", "Language", None, QtGui.QApplication.UnicodeUTF8)) - self.pages_lbl.setText(QtGui.QApplication.translate("Base", "Pages", None, QtGui.QApplication.UnicodeUTF8)) - self.tags_lbl.setText(QtGui.QApplication.translate("Base", "Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.description_btn.setText(QtGui.QApplication.translate("Base", "Description", None, QtGui.QApplication.UnicodeUTF8)) - self.review_lbl.setText(QtGui.QApplication.translate("Base", "Review", None, QtGui.QApplication.UnicodeUTF8)) + self.file_table.horizontalHeaderItem(0).setText(QtWidgets.QApplication.translate("Base", "Title", None, -1)) + self.file_table.horizontalHeaderItem(1).setText(QtWidgets.QApplication.translate("Base", "Author", None, -1)) + self.file_table.horizontalHeaderItem(2).setText(QtWidgets.QApplication.translate("Base", "Type", None, -1)) + self.file_table.horizontalHeaderItem(3).setText(QtWidgets.QApplication.translate("Base", "Percent", None, -1)) + self.file_table.horizontalHeaderItem(4).setText(QtWidgets.QApplication.translate("Base", "Rating", None, -1)) + self.file_table.horizontalHeaderItem(5).setText(QtWidgets.QApplication.translate("Base", "Highlights", None, -1)) + self.file_table.horizontalHeaderItem(6).setText(QtWidgets.QApplication.translate("Base", "Modified", None, -1)) + self.file_table.horizontalHeaderItem(7).setText(QtWidgets.QApplication.translate("Base", "Path", None, -1)) + self.fold_btn.setText(QtWidgets.QApplication.translate("Base", "Hide Book Info", None, -1)) + self.title_lbl.setText(QtWidgets.QApplication.translate("Base", "Title", None, -1)) + self.series_lbl.setText(QtWidgets.QApplication.translate("Base", "Series", None, -1)) + self.author_lbl.setText(QtWidgets.QApplication.translate("Base", "Author", None, -1)) + self.lang_lbl.setText(QtWidgets.QApplication.translate("Base", "Language", None, -1)) + self.pages_lbl.setText(QtWidgets.QApplication.translate("Base", "Pages", None, -1)) + self.tags_lbl.setText(QtWidgets.QApplication.translate("Base", "Tags", None, -1)) + self.description_btn.setText(QtWidgets.QApplication.translate("Base", "Description", None, -1)) + self.review_lbl.setText(QtWidgets.QApplication.translate("Base", "Review", None, -1)) self.high_table.setSortingEnabled(True) - self.high_table.horizontalHeaderItem(0).setText(QtGui.QApplication.translate("Base", "Highlight", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(1).setText(QtGui.QApplication.translate("Base", "Comment", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(2).setText(QtGui.QApplication.translate("Base", "Date", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(3).setText(QtGui.QApplication.translate("Base", "Title", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(4).setText(QtGui.QApplication.translate("Base", "Author", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(5).setText(QtGui.QApplication.translate("Base", "Page", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(6).setText(QtGui.QApplication.translate("Base", "Chapter", None, QtGui.QApplication.UnicodeUTF8)) - self.high_table.horizontalHeaderItem(7).setText(QtGui.QApplication.translate("Base", "Book path", None, QtGui.QApplication.UnicodeUTF8)) - self.act_english.setText(QtGui.QApplication.translate("Base", "English", None, QtGui.QApplication.UnicodeUTF8)) - self.act_greek.setText(QtGui.QApplication.translate("Base", "Greek", None, QtGui.QApplication.UnicodeUTF8)) - self.act_view_book.setText(QtGui.QApplication.translate("Base", "View Book", None, QtGui.QApplication.UnicodeUTF8)) - self.act_view_book.setShortcut(QtGui.QApplication.translate("Base", "Ctrl+B", None, QtGui.QApplication.UnicodeUTF8)) + self.high_table.horizontalHeaderItem(0).setText(QtWidgets.QApplication.translate("Base", "Highlight", None, -1)) + self.high_table.horizontalHeaderItem(1).setText(QtWidgets.QApplication.translate("Base", "Comment", None, -1)) + self.high_table.horizontalHeaderItem(2).setText(QtWidgets.QApplication.translate("Base", "Date", None, -1)) + self.high_table.horizontalHeaderItem(3).setText(QtWidgets.QApplication.translate("Base", "Title", None, -1)) + self.high_table.horizontalHeaderItem(4).setText(QtWidgets.QApplication.translate("Base", "Author", None, -1)) + self.high_table.horizontalHeaderItem(5).setText(QtWidgets.QApplication.translate("Base", "Page", None, -1)) + self.high_table.horizontalHeaderItem(6).setText(QtWidgets.QApplication.translate("Base", "Chapter", None, -1)) + self.high_table.horizontalHeaderItem(7).setText(QtWidgets.QApplication.translate("Base", "Path", None, -1)) + self.sync_table.horizontalHeaderItem(0).setText(QtWidgets.QApplication.translate("Base", "Sync Groups", None, -1)) + self.act_english.setText(QtWidgets.QApplication.translate("Base", "English", None, -1)) + self.act_greek.setText(QtWidgets.QApplication.translate("Base", "Greek", None, -1)) + self.act_view_book.setText(QtWidgets.QApplication.translate("Base", "View Book", None, -1)) + self.act_view_book.setShortcut(QtWidgets.QApplication.translate("Base", "Ctrl+B", None, -1)) -from secondary import DropTableWidget +from secondary import DropTableWidget, XTableWidget import images_rc diff --git a/gui_main.ui b/gui_main.ui index 518fd1f..ce815f4 100644 --- a/gui_main.ui +++ b/gui_main.ui @@ -144,6 +144,9 @@ QFrame::Sunken + + 3 + 0 @@ -331,9 +334,6 @@ - - background-color: rgb(255, 255, 255); - QFrame::NoFrame @@ -477,7 +477,50 @@ - Book path + Path + + + + + + + + + + 0 + + + + + true + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + QAbstractItemView::SelectRows + + + false + + + true + + + 90 + + + 90 + + + + Sync Groups @@ -560,6 +603,11 @@ QTableWidget
secondary
+ + XTableWidget + QTableWidget +
secondary
+
diff --git a/gui_status.py b/gui_status.py index 772f75f..941ddbc 100644 --- a/gui_status.py +++ b/gui_status.py @@ -1,58 +1,67 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_status.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_status.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_status.ui' applies. # -# Created: Thu Mar 9 14:39:35 2023 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_Status(object): def setupUi(self, Status): Status.setObjectName("Status") - Status.resize(277, 55) - Status.setWindowTitle("") - self.horizontalLayout_2 = QtGui.QHBoxLayout(Status) + Status.resize(286, 32) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(Status) self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.frame = QtGui.QFrame(Status) - self.frame.setFrameShape(QtGui.QFrame.StyledPanel) - self.frame.setFrameShadow(QtGui.QFrame.Raised) + self.frame = QtWidgets.QFrame(Status) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) self.frame.setObjectName("frame") - self.horizontalLayout = QtGui.QHBoxLayout(self.frame) + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setObjectName("horizontalLayout") - self.anim_lbl = QtGui.QLabel(self.frame) + self.anim_lbl = QtWidgets.QLabel(self.frame) self.anim_lbl.setText("") self.anim_lbl.setObjectName("anim_lbl") self.horizontalLayout.addWidget(self.anim_lbl) - self.show_items_btn = QtGui.QToolButton(self.frame) + self.theme_box = QtWidgets.QComboBox(self.frame) + self.theme_box.setObjectName("theme_box") + self.theme_box.addItem("") + self.theme_box.addItem("") + self.theme_box.addItem("") + self.theme_box.addItem("") + self.theme_box.addItem("") + self.theme_box.addItem("") + self.horizontalLayout.addWidget(self.theme_box) + self.show_items_btn = QtWidgets.QToolButton(self.frame) self.show_items_btn.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap(":/stuff/show_pages.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.show_items_btn.setIcon(icon) self.show_items_btn.setIconSize(QtCore.QSize(24, 24)) self.show_items_btn.setChecked(False) - self.show_items_btn.setPopupMode(QtGui.QToolButton.InstantPopup) + self.show_items_btn.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.show_items_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) self.show_items_btn.setObjectName("show_items_btn") self.horizontalLayout.addWidget(self.show_items_btn) self.horizontalLayout_2.addWidget(self.frame) - self.act_page = QtGui.QAction(Status) + self.act_page = QtWidgets.QAction(Status) self.act_page.setCheckable(True) self.act_page.setObjectName("act_page") - self.act_date = QtGui.QAction(Status) + self.act_date = QtWidgets.QAction(Status) self.act_date.setCheckable(True) self.act_date.setObjectName("act_date") - self.act_text = QtGui.QAction(Status) + self.act_text = QtWidgets.QAction(Status) self.act_text.setCheckable(True) self.act_text.setObjectName("act_text") - self.act_comment = QtGui.QAction(Status) + self.act_comment = QtWidgets.QAction(Status) self.act_comment.setCheckable(True) self.act_comment.setObjectName("act_comment") - self.act_chapter = QtGui.QAction(Status) + self.act_chapter = QtWidgets.QAction(Status) self.act_chapter.setCheckable(True) self.act_chapter.setObjectName("act_chapter") @@ -60,15 +69,21 @@ def setupUi(self, Status): QtCore.QMetaObject.connectSlotsByName(Status) def retranslateUi(self, Status): - self.show_items_btn.setToolTip(QtGui.QApplication.translate("Status", "Show/Hide elements of Highlights. Also affects\n" -"what will be saved to the text/html files.", None, QtGui.QApplication.UnicodeUTF8)) - self.show_items_btn.setStatusTip(QtGui.QApplication.translate("Status", "Show/Hide elements of Highlights. Also affects what will be saved to the text/html files.", None, QtGui.QApplication.UnicodeUTF8)) - self.show_items_btn.setText(QtGui.QApplication.translate("Status", "Show in Highlights", None, QtGui.QApplication.UnicodeUTF8)) - self.act_page.setText(QtGui.QApplication.translate("Status", "Page", None, QtGui.QApplication.UnicodeUTF8)) - self.act_date.setText(QtGui.QApplication.translate("Status", "Date", None, QtGui.QApplication.UnicodeUTF8)) - self.act_text.setText(QtGui.QApplication.translate("Status", "Highlight", None, QtGui.QApplication.UnicodeUTF8)) - self.act_comment.setText(QtGui.QApplication.translate("Status", "Comment", None, QtGui.QApplication.UnicodeUTF8)) - self.act_chapter.setText(QtGui.QApplication.translate("Status", "Chapter", None, QtGui.QApplication.UnicodeUTF8)) - self.act_chapter.setToolTip(QtGui.QApplication.translate("Status", "Chapter", None, QtGui.QApplication.UnicodeUTF8)) + self.theme_box.setItemText(0, QtWidgets.QApplication.translate("Status", "No theme - Old icons", None, -1)) + self.theme_box.setItemText(1, QtWidgets.QApplication.translate("Status", "No theme - New icons", None, -1)) + self.theme_box.setItemText(2, QtWidgets.QApplication.translate("Status", "Dark theme - Old icons", None, -1)) + self.theme_box.setItemText(3, QtWidgets.QApplication.translate("Status", "Dark theme - New icons", None, -1)) + self.theme_box.setItemText(4, QtWidgets.QApplication.translate("Status", "Light theme - Old icons", None, -1)) + self.theme_box.setItemText(5, QtWidgets.QApplication.translate("Status", "Light theme - New icons", None, -1)) + self.show_items_btn.setToolTip(QtWidgets.QApplication.translate("Status", "Show/Hide elements of Highlights. Also affects\n" +"what will be saved to the text/html files.", None, -1)) + self.show_items_btn.setStatusTip(QtWidgets.QApplication.translate("Status", "Show/Hide elements of Highlights. Also affects what will be saved to the text/html files.", None, -1)) + self.show_items_btn.setText(QtWidgets.QApplication.translate("Status", "Show in Highlights", None, -1)) + self.act_page.setText(QtWidgets.QApplication.translate("Status", "Page", None, -1)) + self.act_date.setText(QtWidgets.QApplication.translate("Status", "Date", None, -1)) + self.act_text.setText(QtWidgets.QApplication.translate("Status", "Highlight", None, -1)) + self.act_comment.setText(QtWidgets.QApplication.translate("Status", "Comment", None, -1)) + self.act_chapter.setText(QtWidgets.QApplication.translate("Status", "Chapter", None, -1)) + self.act_chapter.setToolTip(QtWidgets.QApplication.translate("Status", "Chapter", None, -1)) import images_rc diff --git a/gui_status.ui b/gui_status.ui index 268a885..b2e4ce5 100644 --- a/gui_status.ui +++ b/gui_status.ui @@ -6,13 +6,10 @@ 0 0 - 277 - 55 + 286 + 32 - - - 0 @@ -36,6 +33,40 @@
+ + + + + No theme - Old icons + + + + + No theme - New icons + + + + + Dark theme - Old icons + + + + + Dark theme - New icons + + + + + Light theme - Old icons + + + + + Light theme - New icons + + + + diff --git a/gui_sync_group.py b/gui_sync_group.py new file mode 100644 index 0000000..ce76850 --- /dev/null +++ b/gui_sync_group.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_sync_group.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_sync_group.ui' applies. +# +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_SyncGroup(object): + def setupUi(self, SyncGroup): + SyncGroup.setObjectName("SyncGroup") + SyncGroup.resize(560, 65) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout(SyncGroup) + self.horizontalLayout_5.setSpacing(0) + self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.frame = QtWidgets.QFrame(SyncGroup) + self.frame.setObjectName("frame") + self.verticalLayout = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtWidgets.QSpacerItem(20, 30, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.power_btn = QtWidgets.QToolButton(self.frame) + self.power_btn.setText("") + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/stuff/power32gray.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap(QtGui.QPixmap(":/stuff/power32red.png"), QtGui.QIcon.Normal, QtGui.QIcon.On) + self.power_btn.setIcon(icon) + self.power_btn.setIconSize(QtCore.QSize(16, 16)) + self.power_btn.setCheckable(True) + self.power_btn.setChecked(True) + self.power_btn.setAutoRaise(True) + self.power_btn.setObjectName("power_btn") + self.verticalLayout.addWidget(self.power_btn) + spacerItem1 = QtWidgets.QSpacerItem(20, 28, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem1) + self.horizontalLayout_5.addWidget(self.frame) + self.group_frm = QtWidgets.QFrame(SyncGroup) + self.group_frm.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.group_frm.setFrameShape(QtWidgets.QFrame.Box) + self.group_frm.setFrameShadow(QtWidgets.QFrame.Plain) + self.group_frm.setLineWidth(1) + self.group_frm.setObjectName("group_frm") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.group_frm) + self.verticalLayout_3.setSpacing(0) + self.verticalLayout_3.setContentsMargins(4, 0, 2, 0) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.frame_2 = QtWidgets.QFrame(self.group_frm) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.frame_2) + self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.refresh_btn = QtWidgets.QToolButton(self.frame_2) + self.refresh_btn.setText("") + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap(":/stuff/refresh16.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.refresh_btn.setIcon(icon1) + self.refresh_btn.setIconSize(QtCore.QSize(16, 16)) + self.refresh_btn.setChecked(False) + self.refresh_btn.setAutoRaise(True) + self.refresh_btn.setObjectName("refresh_btn") + self.horizontalLayout_4.addWidget(self.refresh_btn) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_4.addItem(spacerItem2) + self.title_lbl = QtWidgets.QLabel(self.frame_2) + self.title_lbl.setAutoFillBackground(True) + self.title_lbl.setText("") + self.title_lbl.setAlignment(QtCore.Qt.AlignCenter) + self.title_lbl.setObjectName("title_lbl") + self.horizontalLayout_4.addWidget(self.title_lbl) + spacerItem3 = QtWidgets.QSpacerItem(161, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_4.addItem(spacerItem3) + self.sync_btn = QtWidgets.QToolButton(self.frame_2) + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap(":/stuff/sync.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.sync_btn.setIcon(icon2) + self.sync_btn.setChecked(False) + self.sync_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) + self.sync_btn.setAutoRaise(True) + self.sync_btn.setObjectName("sync_btn") + self.horizontalLayout_4.addWidget(self.sync_btn) + self.verticalLayout_3.addWidget(self.frame_2) + self.items_frm = QtWidgets.QFrame(self.group_frm) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.items_frm.sizePolicy().hasHeightForWidth()) + self.items_frm.setSizePolicy(sizePolicy) + self.items_frm.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.items_frm.setFrameShadow(QtWidgets.QFrame.Raised) + self.items_frm.setObjectName("items_frm") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.items_frm) + self.verticalLayout_2.setSpacing(2) + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.checks_frm = QtWidgets.QFrame(self.items_frm) + self.checks_frm.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.checks_frm.setFrameShadow(QtWidgets.QFrame.Raised) + self.checks_frm.setObjectName("checks_frm") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.checks_frm) + self.horizontalLayout.setSpacing(4) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.sync_pos_chk = QtWidgets.QCheckBox(self.checks_frm) + self.sync_pos_chk.setObjectName("sync_pos_chk") + self.horizontalLayout.addWidget(self.sync_pos_chk) + self.merge_chk = QtWidgets.QCheckBox(self.checks_frm) + self.merge_chk.setObjectName("merge_chk") + self.horizontalLayout.addWidget(self.merge_chk) + self.sync_db_chk = QtWidgets.QCheckBox(self.checks_frm) + self.sync_db_chk.setObjectName("sync_db_chk") + self.horizontalLayout.addWidget(self.sync_db_chk) + spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem4) + self.verticalLayout_2.addWidget(self.checks_frm) + self.verticalLayout_3.addWidget(self.items_frm) + self.horizontalLayout_5.addWidget(self.group_frm) + + self.retranslateUi(SyncGroup) + QtCore.QObject.connect(self.power_btn, QtCore.SIGNAL("toggled(bool)"), self.group_frm.setEnabled) + QtCore.QMetaObject.connectSlotsByName(SyncGroup) + + def retranslateUi(self, SyncGroup): + self.power_btn.setToolTip(QtWidgets.QApplication.translate("SyncGroup", "Enable or disable this Sync Group", None, -1)) + self.power_btn.setStatusTip(QtWidgets.QApplication.translate("SyncGroup", "Enable or disable this Sync Group", None, -1)) + self.refresh_btn.setToolTip(QtWidgets.QApplication.translate("SyncGroup", "Reload the Group and check the paths for errors", None, -1)) + self.refresh_btn.setStatusTip(QtWidgets.QApplication.translate("SyncGroup", "Reload the Group and check the paths for errors", None, -1)) + self.sync_btn.setToolTip(QtWidgets.QApplication.translate("SyncGroup", "Start the sync/merge process for this group", None, -1)) + self.sync_btn.setStatusTip(QtWidgets.QApplication.translate("SyncGroup", "Start the sync/merge process for this group", None, -1)) + self.sync_btn.setText(QtWidgets.QApplication.translate("SyncGroup", "Sync this group", None, -1)) + self.sync_pos_chk.setToolTip(QtWidgets.QApplication.translate("SyncGroup", "Sync the current position and read percent of the books", None, -1)) + self.sync_pos_chk.setStatusTip(QtWidgets.QApplication.translate("SyncGroup", "Sync the current position and read percent of the books", None, -1)) + self.sync_pos_chk.setText(QtWidgets.QApplication.translate("SyncGroup", "Sync position", None, -1)) + self.merge_chk.setToolTip(QtWidgets.QApplication.translate("SyncGroup", "Merge the highlights from both books\\n(and/or the archived version)", None, -1)) + self.merge_chk.setStatusTip(QtWidgets.QApplication.translate("SyncGroup", "Merge the highlights from both books\\\\n(and/or the archived version)", None, -1)) + self.merge_chk.setText(QtWidgets.QApplication.translate("SyncGroup", "Merge Highlights", None, -1)) + self.sync_db_chk.setToolTip(QtWidgets.QApplication.translate("SyncGroup", "Keep a synced version of the book in the archived database", None, -1)) + self.sync_db_chk.setStatusTip(QtWidgets.QApplication.translate("SyncGroup", "Keep a synced version of the book in the archived database", None, -1)) + self.sync_db_chk.setText(QtWidgets.QApplication.translate("SyncGroup", "Sync with archived", None, -1)) + +import images_rc diff --git a/gui_sync_group.ui b/gui_sync_group.ui new file mode 100644 index 0000000..46b1462 --- /dev/null +++ b/gui_sync_group.ui @@ -0,0 +1,350 @@ + + + SyncGroup + + + + 0 + 0 + 560 + 65 + + + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 30 + + + + + + + + Enable or disable this Sync Group + + + Enable or disable this Sync Group + + + + + + + :/stuff/power32gray.png + :/stuff/power32red.png:/stuff/power32gray.png + + + + 16 + 16 + + + + true + + + true + + + true + + + + + + + Qt::Vertical + + + + 20 + 28 + + + + + + + + + + + Qt::CustomContextMenu + + + QFrame::Box + + + QFrame::Plain + + + 1 + + + + 0 + + + 4 + + + 0 + + + 2 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + + + Reload the Group and check the paths for errors + + + Reload the Group and check the paths for errors + + + + + + + :/stuff/refresh16.png:/stuff/refresh16.png + + + + 16 + 16 + + + + false + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 161 + 20 + + + + + + + + Start the sync/merge process for this group + + + Start the sync/merge process for this group + + + Sync this group + + + + :/stuff/sync.png:/stuff/sync.png + + + false + + + Qt::ToolButtonTextBesideIcon + + + true + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 2 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 0 + + + + + Sync the current position and read percent of the books + + + Sync the current position and read percent of the books + + + Sync position + + + + + + + Merge the highlights from both books\n(and/or the archived version) + + + Merge the highlights from both books\\n(and/or the archived version) + + + Merge Highlights + + + + + + + Keep a synced version of the book in the archived database + + + Keep a synced version of the book in the archived database + + + Sync with archived + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + power_btn + toggled(bool) + group_frm + setEnabled(bool) + + + 22 + 21 + + + 81 + 83 + + + + + diff --git a/gui_sync_item.py b/gui_sync_item.py new file mode 100644 index 0000000..4ebaa1c --- /dev/null +++ b/gui_sync_item.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_sync_item.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_sync_item.ui' applies. +# +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_SyncItem(object): + def setupUi(self, SyncItem): + SyncItem.setObjectName("SyncItem") + SyncItem.resize(446, 25) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(SyncItem) + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.label = QtWidgets.QLabel(SyncItem) + self.label.setObjectName("label") + self.horizontalLayout_2.addWidget(self.label) + self.sync_path_txt = QtWidgets.QLineEdit(SyncItem) + self.sync_path_txt.setReadOnly(True) + self.sync_path_txt.setObjectName("sync_path_txt") + self.horizontalLayout_2.addWidget(self.sync_path_txt) + self.sync_path_btn = QtWidgets.QPushButton(SyncItem) + self.sync_path_btn.setObjectName("sync_path_btn") + self.horizontalLayout_2.addWidget(self.sync_path_btn) + self.frame = QtWidgets.QFrame(SyncItem) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.add_btn = QtWidgets.QToolButton(self.frame) + self.add_btn.setText("") + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/stuff/add.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.add_btn.setIcon(icon) + self.add_btn.setObjectName("add_btn") + self.horizontalLayout.addWidget(self.add_btn) + self.del_btn = QtWidgets.QToolButton(self.frame) + self.del_btn.setText("") + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap(":/stuff/del.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.del_btn.setIcon(icon1) + self.del_btn.setObjectName("del_btn") + self.horizontalLayout.addWidget(self.del_btn) + self.horizontalLayout_2.addWidget(self.frame) + + self.retranslateUi(SyncItem) + QtCore.QMetaObject.connectSlotsByName(SyncItem) + + def retranslateUi(self, SyncItem): + self.label.setText(QtWidgets.QApplication.translate("SyncItem", "Sync path", None, -1)) + self.sync_path_txt.setToolTip(QtWidgets.QApplication.translate("SyncItem", "The path to the book\'s metadata file", None, -1)) + self.sync_path_txt.setStatusTip(QtWidgets.QApplication.translate("SyncItem", "The path to the book\'s metadata file", None, -1)) + self.sync_path_txt.setPlaceholderText(QtWidgets.QApplication.translate("SyncItem", "Select metadata file to sync", None, -1)) + self.sync_path_btn.setToolTip(QtWidgets.QApplication.translate("SyncItem", "Select/Change the metadata file path", None, -1)) + self.sync_path_btn.setStatusTip(QtWidgets.QApplication.translate("SyncItem", "Select/Change the metadata file path", None, -1)) + self.sync_path_btn.setText(QtWidgets.QApplication.translate("SyncItem", "Select", None, -1)) + self.add_btn.setToolTip(QtWidgets.QApplication.translate("SyncItem", "Add a new Sync path", None, -1)) + self.add_btn.setStatusTip(QtWidgets.QApplication.translate("SyncItem", "Add a new Sync path", None, -1)) + self.del_btn.setToolTip(QtWidgets.QApplication.translate("SyncItem", "Remove this Sync path", None, -1)) + self.del_btn.setStatusTip(QtWidgets.QApplication.translate("SyncItem", "Remove this Sync path", None, -1)) + +import images_rc diff --git a/gui_sync_item.ui b/gui_sync_item.ui new file mode 100644 index 0000000..9a8aa7b --- /dev/null +++ b/gui_sync_item.ui @@ -0,0 +1,108 @@ + + + SyncItem + + + + 0 + 0 + 446 + 25 + + + + + 0 + + + + + Sync path + + + + + + + The path to the book's metadata file + + + The path to the book's metadata file + + + true + + + Select metadata file to sync + + + + + + + Select/Change the metadata file path + + + Select/Change the metadata file path + + + Select + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + + + Add a new Sync path + + + Add a new Sync path + + + + + + + :/stuff/add.png:/stuff/add.png + + + + + + + Remove this Sync path + + + Remove this Sync path + + + + + + + :/stuff/del.png:/stuff/del.png + + + + + + + + + + + + + diff --git a/gui_toolbar.py b/gui_toolbar.py index 402892d..36e190e 100644 --- a/gui_toolbar.py +++ b/gui_toolbar.py @@ -1,89 +1,95 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_toolbar.ui' +# Form implementation generated from reading ui file 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_toolbar.ui', +# licensing of 'D:\Apps\DEV\PROJECTS\KoHighlights\gui_toolbar.ui' applies. # -# Created: Thu Mar 9 14:39:30 2023 -# by: pyside-uic 0.2.15 running on PySide 1.2.4 +# Created: Thu May 2 17:29:33 2024 +# by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets class Ui_ToolBar(object): def setupUi(self, ToolBar): ToolBar.setObjectName("ToolBar") - ToolBar.resize(967, 73) + ToolBar.resize(1035, 91) ToolBar.setContextMenuPolicy(QtCore.Qt.PreventContextMenu) ToolBar.setWindowTitle("") ToolBar.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.verticalLayout_2 = QtGui.QVBoxLayout(ToolBar) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(ToolBar) self.verticalLayout_2.setSpacing(0) self.verticalLayout_2.setContentsMargins(0, 0, 2, 0) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.tool_frame = QtGui.QFrame(ToolBar) + self.tool_frame = QtWidgets.QFrame(ToolBar) self.tool_frame.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tool_frame.setObjectName("tool_frame") - self.horizontalLayout = QtGui.QHBoxLayout(self.tool_frame) + self.horizontalLayout = QtWidgets.QHBoxLayout(self.tool_frame) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setObjectName("horizontalLayout") - self.check_btn = QtGui.QToolButton(self.tool_frame) - self.check_btn.setMinimumSize(QtCore.QSize(80, 0)) - self.check_btn.setText("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap("../KataLib/:/stuff/exec.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.check_btn.setIcon(icon) - self.check_btn.setIconSize(QtCore.QSize(48, 48)) - self.check_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) - self.check_btn.setAutoRaise(True) - self.check_btn.setObjectName("check_btn") - self.horizontalLayout.addWidget(self.check_btn) - self.scan_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.scan_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.scan_btn.sizePolicy().hasHeightForWidth()) self.scan_btn.setSizePolicy(sizePolicy) self.scan_btn.setMinimumSize(QtCore.QSize(80, 0)) - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/stuff/folder_reader.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.scan_btn.setIcon(icon1) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/stuff/folder_reader.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.scan_btn.setIcon(icon) self.scan_btn.setIconSize(QtCore.QSize(48, 48)) self.scan_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.scan_btn.setAutoRaise(True) self.scan_btn.setObjectName("scan_btn") self.horizontalLayout.addWidget(self.scan_btn) - self.export_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.export_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.export_btn.sizePolicy().hasHeightForWidth()) self.export_btn.setSizePolicy(sizePolicy) self.export_btn.setMinimumSize(QtCore.QSize(80, 0)) - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/stuff/file_save.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.export_btn.setIcon(icon2) + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap(":/stuff/file_save.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.export_btn.setIcon(icon1) self.export_btn.setIconSize(QtCore.QSize(48, 48)) + self.export_btn.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.export_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.export_btn.setAutoRaise(True) self.export_btn.setObjectName("export_btn") self.horizontalLayout.addWidget(self.export_btn) - self.open_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.open_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.open_btn.sizePolicy().hasHeightForWidth()) self.open_btn.setSizePolicy(sizePolicy) self.open_btn.setMinimumSize(QtCore.QSize(80, 0)) - icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(":/stuff/files_view.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.open_btn.setIcon(icon3) + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap(":/stuff/files_view.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.open_btn.setIcon(icon2) self.open_btn.setIconSize(QtCore.QSize(48, 48)) self.open_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.open_btn.setAutoRaise(True) self.open_btn.setObjectName("open_btn") self.horizontalLayout.addWidget(self.open_btn) - self.filter_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.add_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.add_btn.sizePolicy().hasHeightForWidth()) + self.add_btn.setSizePolicy(sizePolicy) + icon3 = QtGui.QIcon() + icon3.addPixmap(QtGui.QPixmap(":/stuff/files_add.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.add_btn.setIcon(icon3) + self.add_btn.setIconSize(QtCore.QSize(48, 48)) + self.add_btn.setChecked(False) + self.add_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) + self.add_btn.setAutoRaise(True) + self.add_btn.setObjectName("add_btn") + self.horizontalLayout.addWidget(self.add_btn) + self.filter_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.filter_btn.sizePolicy().hasHeightForWidth()) @@ -98,8 +104,8 @@ def setupUi(self, ToolBar): self.filter_btn.setAutoRaise(True) self.filter_btn.setObjectName("filter_btn") self.horizontalLayout.addWidget(self.filter_btn) - self.merge_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.merge_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.merge_btn.sizePolicy().hasHeightForWidth()) @@ -109,13 +115,13 @@ def setupUi(self, ToolBar): icon5.addPixmap(QtGui.QPixmap(":/stuff/files_merge.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.merge_btn.setIcon(icon5) self.merge_btn.setIconSize(QtCore.QSize(48, 48)) - self.merge_btn.setPopupMode(QtGui.QToolButton.MenuButtonPopup) + self.merge_btn.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.merge_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.merge_btn.setAutoRaise(True) self.merge_btn.setObjectName("merge_btn") self.horizontalLayout.addWidget(self.merge_btn) - self.delete_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.delete_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.delete_btn.sizePolicy().hasHeightForWidth()) @@ -125,13 +131,13 @@ def setupUi(self, ToolBar): icon6.addPixmap(QtGui.QPixmap(":/stuff/files_delete.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.delete_btn.setIcon(icon6) self.delete_btn.setIconSize(QtCore.QSize(48, 48)) - self.delete_btn.setPopupMode(QtGui.QToolButton.MenuButtonPopup) + self.delete_btn.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.delete_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.delete_btn.setAutoRaise(True) self.delete_btn.setObjectName("delete_btn") self.horizontalLayout.addWidget(self.delete_btn) - self.clear_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.clear_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.clear_btn.sizePolicy().hasHeightForWidth()) @@ -145,15 +151,15 @@ def setupUi(self, ToolBar): self.clear_btn.setAutoRaise(True) self.clear_btn.setObjectName("clear_btn") self.horizontalLayout.addWidget(self.clear_btn) - spacerItem = QtGui.QSpacerItem(86, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(86, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) - self.view_grp = QtGui.QGroupBox(self.tool_frame) + self.view_grp = QtWidgets.QGroupBox(self.tool_frame) self.view_grp.setObjectName("view_grp") - self.horizontalLayout_3 = QtGui.QHBoxLayout(self.view_grp) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.view_grp) self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.books_view_btn = QtGui.QToolButton(self.view_grp) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + self.books_view_btn = QtWidgets.QToolButton(self.view_grp) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.books_view_btn.sizePolicy().hasHeightForWidth()) @@ -169,8 +175,8 @@ def setupUi(self, ToolBar): self.books_view_btn.setAutoRaise(True) self.books_view_btn.setObjectName("books_view_btn") self.horizontalLayout_3.addWidget(self.books_view_btn) - self.high_view_btn = QtGui.QToolButton(self.view_grp) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + self.high_view_btn = QtWidgets.QToolButton(self.view_grp) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.high_view_btn.sizePolicy().hasHeightForWidth()) @@ -185,28 +191,44 @@ def setupUi(self, ToolBar): self.high_view_btn.setAutoRaise(True) self.high_view_btn.setObjectName("high_view_btn") self.horizontalLayout_3.addWidget(self.high_view_btn) + self.sync_view_btn = QtWidgets.QToolButton(self.view_grp) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sync_view_btn.sizePolicy().hasHeightForWidth()) + self.sync_view_btn.setSizePolicy(sizePolicy) + icon10 = QtGui.QIcon() + icon10.addPixmap(QtGui.QPixmap(":/stuff/view-sync.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.sync_view_btn.setIcon(icon10) + self.sync_view_btn.setIconSize(QtCore.QSize(48, 48)) + self.sync_view_btn.setCheckable(True) + self.sync_view_btn.setAutoExclusive(True) + self.sync_view_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) + self.sync_view_btn.setAutoRaise(True) + self.sync_view_btn.setObjectName("sync_view_btn") + self.horizontalLayout_3.addWidget(self.sync_view_btn) self.horizontalLayout.addWidget(self.view_grp) - self.mode_grp = QtGui.QGroupBox(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.mode_grp = QtWidgets.QGroupBox(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.mode_grp.sizePolicy().hasHeightForWidth()) self.mode_grp.setSizePolicy(sizePolicy) self.mode_grp.setObjectName("mode_grp") - self.verticalLayout = QtGui.QVBoxLayout(self.mode_grp) + self.verticalLayout = QtWidgets.QVBoxLayout(self.mode_grp) self.verticalLayout.setSpacing(0) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setObjectName("verticalLayout") - self.loaded_btn = QtGui.QToolButton(self.mode_grp) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.loaded_btn = QtWidgets.QToolButton(self.mode_grp) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.loaded_btn.sizePolicy().hasHeightForWidth()) self.loaded_btn.setSizePolicy(sizePolicy) self.loaded_btn.setMinimumSize(QtCore.QSize(80, 0)) - icon10 = QtGui.QIcon() - icon10.addPixmap(QtGui.QPixmap(":/stuff/books.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.loaded_btn.setIcon(icon10) + icon11 = QtGui.QIcon() + icon11.addPixmap(QtGui.QPixmap(":/stuff/books.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.loaded_btn.setIcon(icon11) self.loaded_btn.setIconSize(QtCore.QSize(24, 24)) self.loaded_btn.setCheckable(True) self.loaded_btn.setChecked(True) @@ -216,34 +238,34 @@ def setupUi(self, ToolBar): self.loaded_btn.setObjectName("loaded_btn") self.verticalLayout.addWidget(self.loaded_btn) self.db_btn = XToolButton(self.mode_grp) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.db_btn.sizePolicy().hasHeightForWidth()) self.db_btn.setSizePolicy(sizePolicy) self.db_btn.setMinimumSize(QtCore.QSize(80, 0)) - icon11 = QtGui.QIcon() - icon11.addPixmap(QtGui.QPixmap(":/stuff/db.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.db_btn.setIcon(icon11) + icon12 = QtGui.QIcon() + icon12.addPixmap(QtGui.QPixmap(":/stuff/db.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.db_btn.setIcon(icon12) self.db_btn.setIconSize(QtCore.QSize(24, 24)) self.db_btn.setCheckable(True) self.db_btn.setAutoExclusive(True) - self.db_btn.setPopupMode(QtGui.QToolButton.MenuButtonPopup) + self.db_btn.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.db_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) self.db_btn.setAutoRaise(True) self.db_btn.setObjectName("db_btn") self.verticalLayout.addWidget(self.db_btn) self.horizontalLayout.addWidget(self.mode_grp) - self.about_btn = QtGui.QToolButton(self.tool_frame) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.about_btn = QtWidgets.QToolButton(self.tool_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.about_btn.sizePolicy().hasHeightForWidth()) self.about_btn.setSizePolicy(sizePolicy) self.about_btn.setMinimumSize(QtCore.QSize(80, 0)) - icon12 = QtGui.QIcon() - icon12.addPixmap(QtGui.QPixmap(":/stuff/logo64.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.about_btn.setIcon(icon12) + icon13 = QtGui.QIcon() + icon13.addPixmap(QtGui.QPixmap(":/stuff/logo64.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.about_btn.setIcon(icon13) self.about_btn.setIconSize(QtCore.QSize(48, 48)) self.about_btn.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.about_btn.setAutoRaise(True) @@ -255,47 +277,53 @@ def setupUi(self, ToolBar): QtCore.QMetaObject.connectSlotsByName(ToolBar) def retranslateUi(self, ToolBar): - self.scan_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Scans a directory for Koreader metadata files\n" -"Can also be the eReader\'s root directory (Ctrl+L)", None, QtGui.QApplication.UnicodeUTF8)) - self.scan_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Scans a directory for Koreader metadata files. Can also be the eReader\'s root directory (Ctrl+L)", None, QtGui.QApplication.UnicodeUTF8)) - self.scan_btn.setText(QtGui.QApplication.translate("ToolBar", "Scan Directory", None, QtGui.QApplication.UnicodeUTF8)) - self.export_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Export selected highlights (Ctrl+S)", None, QtGui.QApplication.UnicodeUTF8)) - self.export_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Export selected highlights (Ctrl+S)", None, QtGui.QApplication.UnicodeUTF8)) - self.export_btn.setText(QtGui.QApplication.translate("ToolBar", "Export", None, QtGui.QApplication.UnicodeUTF8)) - self.open_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "View the selected book (Ctrl+B)", None, QtGui.QApplication.UnicodeUTF8)) - self.open_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "View the selected book (Ctrl+B)", None, QtGui.QApplication.UnicodeUTF8)) - self.open_btn.setText(QtGui.QApplication.translate("ToolBar", "View", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Open the filtering popup (Alt+F)", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Open the filtering popup (Alt+F)", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_btn.setText(QtGui.QApplication.translate("ToolBar", "Filter", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_btn.setShortcut(QtGui.QApplication.translate("ToolBar", "Alt+F", None, QtGui.QApplication.UnicodeUTF8)) - self.merge_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Merge the highlights from the same book in two different\n" + self.tool_frame.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Right-click to change icon size", None, -1)) + self.scan_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Scans a directory for Koreader metadata files\n" +"Can also be the eReader\'s root directory (Ctrl+L)", None, -1)) + self.scan_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Scans a directory for Koreader metadata files. Can also be the eReader\'s root directory (Ctrl+L)", None, -1)) + self.scan_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Scan Directory", None, -1)) + self.export_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Export selected highlights (Ctrl+S)", None, -1)) + self.export_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Export selected highlights (Ctrl+S)", None, -1)) + self.export_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Export", None, -1)) + self.open_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "View the selected book (Ctrl+B)", None, -1)) + self.open_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "View the selected book (Ctrl+B)", None, -1)) + self.open_btn.setText(QtWidgets.QApplication.translate("ToolBar", "View", None, -1)) + self.add_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Add a sync group", None, -1)) + self.add_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Add a sync group", None, -1)) + self.add_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Add", None, -1)) + self.filter_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Open the filtering popup (Alt+F)", None, -1)) + self.filter_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Open the filtering popup (Alt+F)", None, -1)) + self.filter_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Filter", None, -1)) + self.filter_btn.setShortcut(QtWidgets.QApplication.translate("ToolBar", "Alt+F", None, -1)) + self.merge_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Merge the highlights from the same book in two different\n" "devices, and/or sync their reading position.\n" -"Activated only if two entries of the same book are selected.", None, QtGui.QApplication.UnicodeUTF8)) - self.merge_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Merge the highlights from the same book in two different devices, and/or sync their reading position. Activated only if two entries of the same book are selected.", None, QtGui.QApplication.UnicodeUTF8)) - self.merge_btn.setText(QtGui.QApplication.translate("ToolBar", "Merge/Sync", None, QtGui.QApplication.UnicodeUTF8)) - self.delete_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Delete selected Highlights (Del)", None, QtGui.QApplication.UnicodeUTF8)) - self.delete_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Delete selected Highlights (Del)", None, QtGui.QApplication.UnicodeUTF8)) - self.delete_btn.setText(QtGui.QApplication.translate("ToolBar", "Delete", None, QtGui.QApplication.UnicodeUTF8)) - self.clear_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Clears the books list (Ctrl+Backspace)", None, QtGui.QApplication.UnicodeUTF8)) - self.clear_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Clears the books list (Ctrl+Backspace)", None, QtGui.QApplication.UnicodeUTF8)) - self.clear_btn.setText(QtGui.QApplication.translate("ToolBar", "Clear List", None, QtGui.QApplication.UnicodeUTF8)) - self.books_view_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Books View", None, QtGui.QApplication.UnicodeUTF8)) - self.books_view_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Books View", None, QtGui.QApplication.UnicodeUTF8)) - self.books_view_btn.setText(QtGui.QApplication.translate("ToolBar", "Books", None, QtGui.QApplication.UnicodeUTF8)) - self.high_view_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Highlights View", None, QtGui.QApplication.UnicodeUTF8)) - self.high_view_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Highlights View", None, QtGui.QApplication.UnicodeUTF8)) - self.high_view_btn.setText(QtGui.QApplication.translate("ToolBar", "Highlights", None, QtGui.QApplication.UnicodeUTF8)) - self.loaded_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Show the loaded files", None, QtGui.QApplication.UnicodeUTF8)) - self.loaded_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Show the loaded files", None, QtGui.QApplication.UnicodeUTF8)) - self.loaded_btn.setText(QtGui.QApplication.translate("ToolBar", "Loaded", None, QtGui.QApplication.UnicodeUTF8)) - self.db_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Show the archived files in the database\n" -"(Right click for database actions menu)", None, QtGui.QApplication.UnicodeUTF8)) - self.db_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Show the archived files in the database (Right click for database actions menu)", None, QtGui.QApplication.UnicodeUTF8)) - self.db_btn.setText(QtGui.QApplication.translate("ToolBar", "Archived", None, QtGui.QApplication.UnicodeUTF8)) - self.about_btn.setToolTip(QtGui.QApplication.translate("ToolBar", "Info about the KoHighlights (Ctrl+I)", None, QtGui.QApplication.UnicodeUTF8)) - self.about_btn.setStatusTip(QtGui.QApplication.translate("ToolBar", "Info about the KoHighlights (Ctrl+I)", None, QtGui.QApplication.UnicodeUTF8)) - self.about_btn.setText(QtGui.QApplication.translate("ToolBar", "About", None, QtGui.QApplication.UnicodeUTF8)) +"Activated only if two entries of the same book are selected.", None, -1)) + self.merge_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Merge the highlights from the same book in two different devices, and/or sync their reading position. Activated only if two entries of the same book are selected.", None, -1)) + self.merge_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Merge/Sync", None, -1)) + self.delete_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Delete selected", None, -1)) + self.delete_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Delete", None, -1)) + self.clear_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Clear the list\'s entries (Ctrl+Backspace)", None, -1)) + self.clear_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Clear the list\'s entries (Ctrl+Backspace)", None, -1)) + self.clear_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Clear List", None, -1)) + self.books_view_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Books View", None, -1)) + self.books_view_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Books View", None, -1)) + self.books_view_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Books", None, -1)) + self.high_view_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Highlights View", None, -1)) + self.high_view_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Highlights View", None, -1)) + self.high_view_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Highlights", None, -1)) + self.sync_view_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Sync Groups View", None, -1)) + self.sync_view_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Sync Groups View", None, -1)) + self.sync_view_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Sync Groups", None, -1)) + self.loaded_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Show the loaded files", None, -1)) + self.loaded_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Show the loaded files", None, -1)) + self.loaded_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Loaded", None, -1)) + self.db_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Show the archived files in the database\n" +"(Right click for database actions menu)", None, -1)) + self.db_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Show the archived files in the database (Right click for database actions menu)", None, -1)) + self.db_btn.setText(QtWidgets.QApplication.translate("ToolBar", "Archived", None, -1)) + self.about_btn.setToolTip(QtWidgets.QApplication.translate("ToolBar", "Info about the KoHighlights (Ctrl+I)", None, -1)) + self.about_btn.setStatusTip(QtWidgets.QApplication.translate("ToolBar", "Info about the KoHighlights (Ctrl+I)", None, -1)) + self.about_btn.setText(QtWidgets.QApplication.translate("ToolBar", "About", None, -1)) from secondary import XToolButton import images_rc diff --git a/gui_toolbar.ui b/gui_toolbar.ui index 804168c..5c95e41 100644 --- a/gui_toolbar.ui +++ b/gui_toolbar.ui @@ -6,8 +6,8 @@ 0 0 - 967 - 73 + 1035 + 91 @@ -40,24 +40,40 @@ Qt::CustomContextMenu + + Right-click to change icon size + 0 - + + + + 0 + 0 + + 80 0 + + Scans a directory for Koreader metadata files +Can also be the eReader's root directory (Ctrl+L) + + + Scans a directory for Koreader metadata files. Can also be the eReader's root directory (Ctrl+L) + - + Scan Directory - ../KataLib/:/stuff/exec.png../KataLib/:/stuff/exec.png + :/stuff/folder_reader.png:/stuff/folder_reader.png @@ -74,7 +90,7 @@ - + 0 @@ -88,18 +104,17 @@ - Scans a directory for Koreader metadata files -Can also be the eReader's root directory (Ctrl+L) + Export selected highlights (Ctrl+S) - Scans a directory for Koreader metadata files. Can also be the eReader's root directory (Ctrl+L) + Export selected highlights (Ctrl+S) - Scan Directory + Export - :/stuff/folder_reader.png:/stuff/folder_reader.png + :/stuff/file_save.png:/stuff/file_save.png @@ -107,6 +122,9 @@ Can also be the eReader's root directory (Ctrl+L) 48 + + QToolButton::MenuButtonPopup + Qt::ToolButtonTextUnderIcon @@ -116,7 +134,7 @@ Can also be the eReader's root directory (Ctrl+L) - + 0 @@ -130,17 +148,17 @@ Can also be the eReader's root directory (Ctrl+L) - Export selected highlights (Ctrl+S) + View the selected book (Ctrl+B) - Export selected highlights (Ctrl+S) + View the selected book (Ctrl+B) - Export + View - :/stuff/file_save.png:/stuff/file_save.png + :/stuff/files_view.png:/stuff/files_view.png @@ -157,31 +175,25 @@ Can also be the eReader's root directory (Ctrl+L) - + 0 0 - - - 80 - 0 - - - View the selected book (Ctrl+B) + Add a sync group - View the selected book (Ctrl+B) + Add a sync group - View + Add - :/stuff/files_view.png:/stuff/files_view.png + :/stuff/files_add.png:/stuff/files_add.png @@ -189,6 +201,9 @@ Can also be the eReader's root directory (Ctrl+L) 48 + + false + Qt::ToolButtonTextUnderIcon @@ -302,10 +317,7 @@ Activated only if two entries of the same book are selected. - Delete selected Highlights (Del) - - - Delete selected Highlights (Del) + Delete selected Delete @@ -346,10 +358,10 @@ Activated only if two entries of the same book are selected. - Clears the books list (Ctrl+Backspace) + Clear the list's entries (Ctrl+Backspace) - Clears the books list (Ctrl+Backspace) + Clear the list's entries (Ctrl+Backspace) Clear List @@ -476,6 +488,47 @@ Activated only if two entries of the same book are selected. + + + + + 0 + 0 + + + + Sync Groups View + + + Sync Groups View + + + Sync Groups + + + + :/stuff/view-sync.png:/stuff/view-sync.png + + + + 48 + 48 + + + + true + + + true + + + Qt::ToolButtonTextUnderIcon + + + true + + + diff --git a/images.qrc b/images.qrc index 0a1187d..b28479e 100644 --- a/images.qrc +++ b/images.qrc @@ -1,36 +1,43 @@ -stuff/files_view.png -stuff/exec.png -stuff/refresh16.png -stuff/copy.png -stuff/file_exists.png +stuff/trash.png +stuff/files_add.png +stuff/db_open.png +stuff/folder_open.png stuff/delete.png -stuff/logo64.png -stuff/filter.png -stuff/paypal.png -stuff/wait.gif +stuff/calendar.png stuff/file_missing.png -stuff/trans32.png +stuff/show_pages.png +stuff/paypal.png +stuff/files_delete.png +stuff/label_green.png stuff/file_edit.png -stuff/folder_reader.png -stuff/sort.png -stuff/db_open.png +stuff/file_exists.png stuff/db_add.png -stuff/books.png -stuff/calendar.png -stuff/view_books.png -stuff/view-highlights.png -stuff/label_green.png +stuff/font.ttf +stuff/copy.png +stuff/power32gray.png stuff/paypal76.png -stuff/logo.png +stuff/folder_reader.png +stuff/view-sync.png +stuff/power32red.png +stuff/sync.png stuff/file_save.png +stuff/files_view.png +stuff/logo64.png +stuff/wait.gif +stuff/trans32.png +stuff/logo.png +stuff/add.png +stuff/db.png +stuff/books.png stuff/description.png -stuff/folder_open.png +stuff/filter.png +stuff/refresh16.png +stuff/view-highlights.png +stuff/view_books.png stuff/files_merge.png -stuff/db.png -stuff/trash.png -stuff/show_pages.png -stuff/files_delete.png +stuff/del.png +stuff/sort.png \ No newline at end of file diff --git a/main.py b/main.py index cdc41e3..7733e33 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ # coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals from boot_config import * from boot_config import _ import os, sys, re @@ -12,54 +11,48 @@ import hashlib from datetime import datetime from functools import partial +from copy import deepcopy from collections import defaultdict +from ntpath import normpath from os.path import (isdir, isfile, join, basename, splitext, dirname, split, getmtime, abspath, splitdrive) from pprint import pprint - -if QT4: # ___ ______________ DEPENDENCIES __________________________ - from PySide.QtSql import QSqlDatabase, QSqlQuery - from PySide.QtCore import (Qt, QTimer, Slot, QThread, QMimeData, QModelIndex, - QByteArray, QPoint) - from PySide.QtGui import (QMainWindow, QApplication, QMessageBox, QIcon, QFileDialog, - QTableWidgetItem, QTextCursor, QMenu, QAction, QHeaderView, - QPixmap, QListWidgetItem, QBrush, QColor) -elif QT5: +if QT5: from PySide2.QtWidgets import (QMainWindow, QHeaderView, QApplication, QMessageBox, QAction, QMenu, QTableWidgetItem, QListWidgetItem, - QFileDialog) + QFileDialog, QComboBox) from PySide2.QtCore import (Qt, QTimer, QThread, QModelIndex, Slot, QPoint, QMimeData, QByteArray) from PySide2.QtSql import QSqlDatabase, QSqlQuery - from PySide2.QtGui import QIcon, QPixmap, QTextCursor, QBrush, QColor + from PySide2.QtGui import QIcon, QPixmap, QTextCursor, QBrush, QColor, QFontDatabase else: # Qt6 from PySide6.QtWidgets import (QMainWindow, QHeaderView, QApplication, QMessageBox, - QFileDialog, QTableWidgetItem, QMenu, QListWidgetItem) + QFileDialog, QTableWidgetItem, QMenu, QListWidgetItem, + QComboBox) from PySide6.QtCore import Qt, QTimer, Slot, QPoint, QThread, QModelIndex, QMimeData - from PySide6.QtGui import QIcon, QAction, QBrush, QColor, QPixmap, QTextCursor + from PySide6.QtGui import (QIcon, QAction, QBrush, QColor, QPixmap, QTextCursor, + QFontDatabase) from PySide6.QtSql import QSqlDatabase, QSqlQuery from secondary import * from gui_main import Ui_Base -if PYTHON2: # ___ __________ PYTHON 2/3 COMPATIBILITY ______________ - import cPickle as pickle - from distutils.version import LooseVersion as version_parse -else: - from packaging.version import parse as version_parse - import pickle +from packaging.version import parse as version_parse +import pickle __author__ = "noEmbryo" -__version__ = "1.7.6.0" +__version__ = "2.0.0.0" class Base(QMainWindow, Ui_Base): def __init__(self, parent=None): super(Base, self).__init__(parent) + # noinspection PyArgumentList + self.app = QApplication.instance() self.scan_thread = None self.setupUi(self) @@ -87,43 +80,46 @@ def __init__(self, parent=None): self.exit_msg = True self.db_path = join(SETTINGS_DIR, "data.db") self.date_vacuumed = datetime.now().strftime(DATE_FORMAT) + self.theme = THEME_NONE_OLD self.date_format = DATE_FORMAT + self.show_items = [True, True, True, True, True] # ___ ___________________________________ self.file_selection = None self.sel_idx = None self.sel_indexes = [] - self.high_view_selection = None - self.sel_high_view = [] self.high_list_selection = None self.sel_high_list = [] + self.high_view_selection = None + self.sel_high_view = [] + self.sync_view_selection = None + self.sel_sync_view = [] self.loaded_paths = set() self.books2reload = set() self.parent_book_data = {} self.reload_highlights = True + self.sync_groups_loaded = False self.threads = [] self.query = None self.db = None self.books = [] + self.sync_groups = [] + tooltip = _("Right click to ignore english articles") + self.file_table.horizontalHeaderItem(TITLE).setToolTip(tooltip) self.header_main = self.file_table.horizontalHeader() self.header_main.setDefaultAlignment(Qt.AlignLeft) self.header_main.setContextMenuPolicy(Qt.CustomContextMenu) self.header_high_view = self.high_table.horizontalHeader() self.header_high_view.setDefaultAlignment(Qt.AlignLeft) # self.header_high_view.setResizeMode(HIGHLIGHT_H, QHeaderView.Stretch) - if QT4: - self.file_table.verticalHeader().setResizeMode(QHeaderView.Fixed) - self.header_main.setMovable(True) - self.high_table.verticalHeader().setResizeMode(QHeaderView.Fixed) - self.header_high_view.setMovable(True) - elif QT5 or QT6: - self.file_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) - self.header_main.setSectionsMovable(True) - self.high_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) - self.header_high_view.setSectionsMovable(True) + + self.file_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.header_main.setSectionsMovable(True) + self.high_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.header_high_view.setSectionsMovable(True) self.splitter.setCollapsible(0, False) self.splitter.setCollapsible(1, False) @@ -136,19 +132,38 @@ def __init__(self, parent=None): self.ico_file_save = QIcon(":/stuff/file_save.png") self.ico_files_merge = QIcon(":/stuff/files_merge.png") self.ico_files_delete = QIcon(":/stuff/files_delete.png") - self.ico_file_exists = QIcon(":/stuff/file_exists.png") - self.ico_file_missing = QIcon(":/stuff/file_missing.png") + self.ico_db_add = QIcon(":/stuff/db_add.png") + self.ico_db_open = QIcon(":/stuff/db_open.png") + self.ico_refresh = QIcon(":/stuff/refresh16.png") + self.ico_folder_open = QIcon(":/stuff/folder_open.png") + self.ico_calendar = QIcon(":/stuff/calendar.png") + self.ico_sort = QIcon(":/stuff/sort.png") + self.ico_view_books = QIcon(":/stuff/view_books.png") + self.ico_files_view = QIcon(":/stuff/files_view.png") self.ico_file_edit = QIcon(":/stuff/file_edit.png") self.ico_copy = QIcon(":/stuff/copy.png") self.ico_delete = QIcon(":/stuff/delete.png") + self.def_icons = [ + self.ico_file_save, + self.ico_files_merge, + self.ico_files_delete, + self.ico_db_add, + self.ico_db_open, + self.ico_refresh, + self.ico_folder_open, + self.ico_calendar, + self.ico_sort, + self.ico_view_books, + self.ico_files_view, + self.ico_file_edit, + self.ico_copy, + self.ico_delete, + ] + self.ico_file_exists = QIcon(":/stuff/file_exists.png") + self.ico_file_missing = QIcon(":/stuff/file_missing.png") self.ico_label_green = QIcon(":/stuff/label_green.png") - self.ico_view_books = QIcon(":/stuff/view_books.png") - self.ico_db_add = QIcon(":/stuff/db_add.png") - self.ico_db_open = QIcon(":/stuff/db_open.png") self.ico_app = QIcon(":/stuff/logo64.png") self.ico_empty = QIcon(":/stuff/trans32.png") - self.ico_refresh = QIcon(":/stuff/refresh16.png") - self.ico_folder_open = QIcon(":/stuff/folder_open.png") # noinspection PyArgumentList self.clip = QApplication.clipboard() @@ -163,11 +178,18 @@ def __init__(self, parent=None): self.toolbar.merge_btn.setEnabled(False) self.toolbar.delete_btn.setEnabled(False) + self.export_menu = QMenu(self) + self.export_menu.setTitle(_("Export")) + self.export_menu.setIcon(self.ico_file_save) + self.export_menu.aboutToShow.connect(self.create_export_menu) # assign menu + if QT6: # QT6 requires exec() instead of exec_() + self.export_menu.exec_ = getattr(self.export_menu, "exec") + self.toolbar.export_btn.setMenu(self.export_menu) + self.status = Status(self) self.statusbar.addPermanentWidget(self.status) self.edit_high = TextDialog(self) - self.edit_high.on_ok = self.edit_comment_ok self.edit_high.setWindowTitle(_("Comments")) self.description = TextDialog(self) @@ -179,6 +201,19 @@ def __init__(self, parent=None): self.review_lbl.setVisible(False) self.review_txt.setVisible(False) + tbar = self.toolbar + self.def_btn_icos = [] + self.buttons = [(tbar.scan_btn, "A"), (tbar.export_btn, "B"), + (tbar.open_btn, "C"), (tbar.filter_btn, "D"), + (tbar.merge_btn, "E"), (tbar.add_btn, "X"), + (tbar.delete_btn, "O"), (tbar.clear_btn, "G"), + (tbar.books_view_btn, "H"), (tbar.high_view_btn, "I"), + (tbar.sync_view_btn, "W"), (tbar.loaded_btn, "H"), + (tbar.db_btn, "K"), (self.status.show_items_btn, "U"), + (self.description_btn, "V"), (self.filter.filter_btn, "D"), + (self.filter.clear_filter_btn, "G"), + ] + # noinspection PyTypeChecker,PyCallByClass QTimer.singleShot(10000, self.auto_check4update) # check for updates @@ -192,12 +227,15 @@ def __init__(self, parent=None): def on_load(self): """ Things that must be done after the initialization """ + fdb = QFontDatabase() + fdb.addApplicationFont(":/stuff/font.ttf") + # fdb.removeApplicationFont(0) + self.settings_load() self.init_db() if FIRST_RUN: # on first run self.toolbar.loaded_btn.click() self.splitter.setSizes((500, 250)) - self.toolbar.export_btn.setMenu(self.get_export_menu()) # assign/create menu self.toolbar.merge_btn.setMenu(self.merge_menu()) # assign/create menu self.toolbar.delete_btn.setMenu(self.delete_menu()) # assign/create menu self.connect_gui() @@ -210,7 +248,7 @@ def on_load(self): self.toolbar.loaded_btn.setChecked(True) # open in Loaded mode else: self.toolbar.db_btn.setChecked(True) # open in Archived mode - text = _("Loading {} database").format(APP_NAME) + text = _(f"Loading {APP_NAME} database") self.loading_thread(DBLoader, self.books, text) self.read_books_from_db() # always load db on start if self.current_view == BOOKS_VIEW: @@ -218,8 +256,93 @@ def on_load(self): else: self.toolbar.high_view_btn.click() # open in Highlights view + self.setup_buttons() self.show() + def setup_buttons(self): + for btn, char in self.buttons: + self.def_btn_icos.append(btn.icon()) + size = btn.iconSize().toTuple() + btn.xig = XIconGlyph(self, {"family": "XFont", "size": size, "char": char}) + # btn.glyph = btn.xig.get_icon() + + def set_new_icons(self, menus=True): + """ Create the new icons + + :type menus: bool + :param menus: Create the new menu icons too + """ + # noinspection PyTypeChecker + QTimer.singleShot(0, partial(self.delayed_set_new_icons, menus)) + + def delayed_set_new_icons(self, menus=True): + """ Delay the creation of the icons to allow for the new palette to be set + + :type menus: bool + :param menus: Create the menu icons too + """ + for btn, _ in self.buttons: + size = btn.iconSize().toTuple() + btn.setIcon(btn.xig.get_icon({"size": size})) + + if menus: # recreate the menu icons + xig = XIconGlyph(self, {"family": "XFont", "size": (16, 16)}) + self.export_menu.setIcon(xig.get_icon({"char": "B"})) + self.ico_file_save = xig.get_icon({"char": "B"}) + self.ico_files_merge = xig.get_icon({"char": "E"}) + self.ico_files_delete = xig.get_icon({"char": "O"}) + self.ico_db_add = xig.get_icon({"char": "L"}) + self.ico_db_open = xig.get_icon({"char": "M"}) + self.ico_refresh = xig.get_icon({"char": "N"}) + self.ico_folder_open = xig.get_icon({"char": "P"}) + self.ico_calendar = xig.get_icon({"char": "T"}) + self.ico_sort = xig.get_icon({"char": "S"}) + self.ico_view_books = xig.get_icon({"char": "H"}) + self.act_view_book.setIcon(xig.get_icon({"char": "C"})) + self.ico_file_edit = xig.get_icon({"char": "Q"}) + self.ico_copy = xig.get_icon({"char": "R"}) + self.ico_delete = xig.get_icon({"char": "O"}) + + def set_old_icons(self): + """ Reload the old icons + """ + for idx, item in enumerate(self.buttons): + btn = item[0] + btn.setIcon(self.def_btn_icos[idx]) + + self.ico_file_save = self.def_icons[0] + self.ico_files_merge = self.def_icons[1] + self.ico_files_delete = self.def_icons[2] + self.ico_db_add = self.def_icons[3] + self.ico_db_open = self.def_icons[4] + self.ico_refresh = self.def_icons[5] + self.ico_folder_open = self.def_icons[6] + self.ico_calendar = self.def_icons[7] + self.ico_sort = self.def_icons[8] + self.ico_view_books = self.def_icons[9] + self.act_view_book.setIcon(self.def_icons[10]) + self.ico_file_edit = self.def_icons[11] + self.ico_copy = self.def_icons[12] + self.ico_delete = self.def_icons[13] + + def reset_theme_colors(self): + """ Resets the widget colors after a theme change + """ + color = self.app.palette().base().color().name() + self.review_txt.setStyleSheet(f'background-color: "{color}";') + color = self.app.palette().window().color().name() + for item in [self.status.show_items_btn, self.status.theme_box]: + item.setStyleSheet(f'background-color: "{color}";') + + color = self.app.palette().button().color().name() + for row in range(self.sync_table.rowCount()): + wdg = self.sync_table.cellWidget(row, 0) + wdg.setStyleSheet('QFrame#items_frm {background-color: "%s";}' % color) + wdg.setup_icons() + + self.setup_buttons() + self.reload_table(_("Reloading books...")) + # ___ ___________________ EVENTS STUFF __________________________ def connect_gui(self): @@ -237,6 +360,10 @@ def connect_gui(self): self.header_high_view.sectionClicked.connect(self.on_highlight_column_clicked) self.header_high_view.sectionResized.connect(self.on_highlight_column_resized) + self.sync_table.base = self + self.sync_view_selection = self.sync_table.selectionModel() + self.sync_view_selection.selectionChanged.connect(self.sync_view_selection_update) + sys.stdout = LogStream() sys.stdout.setObjectName("out") sys.stdout.append_to_log.connect(self.write_to_log) @@ -255,40 +382,36 @@ def keyPressEvent(self, event): if mod == Qt.ControlModifier: # if control is pressed if key == Qt.Key_Backspace: self.toolbar.on_clear_btn_clicked() - return True - if key == Qt.Key_L: + elif key == Qt.Key_L: self.toolbar.on_scan_btn_clicked() - return True - if key == Qt.Key_S: + elif key == Qt.Key_S: self.on_export() - return True - if key == Qt.Key_I: + elif key == Qt.Key_I: self.toolbar.on_about_btn_clicked() - return True - if key == Qt.Key_F: + elif key == Qt.Key_F: self.toolbar.filter_btn.click() - return True - if key == Qt.Key_Q: + elif key == Qt.Key_Q: self.close() - if self.current_view == HIGHLIGHTS_VIEW and self.sel_high_view: + elif self.current_view == HIGHLIGHTS_VIEW and self.sel_high_view: if key == Qt.Key_C: self.copy_text_2clip(self.get_highlights()[0]) - return True + if mod == Qt.AltModifier: # if alt is pressed if key == Qt.Key_A: self.on_archive() return True - if self.current_view == HIGHLIGHTS_VIEW and self.sel_high_view: + elif self.current_view == HIGHLIGHTS_VIEW and self.sel_high_view: if key == Qt.Key_C: self.copy_text_2clip(self.get_highlights()[1]) return True if key == Qt.Key_Escape: self.close() - return True - if key == Qt.Key_Delete: - self.delete_actions(0) - return True + elif key == Qt.Key_Delete: + if self.current_view == BOOKS_VIEW: + self.delete_actions(0) + else: + self.toolbar.on_delete_btn_clicked() def closeEvent(self, event): """ Accepts or rejects the `exit` command @@ -300,7 +423,7 @@ def closeEvent(self, event): self.bye_bye_stuff() event.accept() return - popup = self.popup(_("Confirmation"), _("Exit {}?").format(APP_NAME), buttons=2, + popup = self.popup(_("Confirmation"), _(f"Exit {APP_NAME}?"), buttons=2, check_text=DO_NOT_SHOW) self.exit_msg = not popup.checked if popup.buttonRole(popup.clickedButton()) == QMessageBox.AcceptRole: @@ -331,7 +454,7 @@ def init_db(self): self.query.exec_ = getattr(self.query, "exec") if app_config: pass - # self.query.exec_("""PRAGMA user_version""") # 2do: enable if db changes + # self.query.exec_("""PRAGMA user_version""") # 2check: enable if db changes # while self.query.next(): # self.check_db_version(self.query.value(0)) # check the db version self.set_db_version() if not isfile(self.db_path) else None @@ -354,7 +477,7 @@ def update_db(self, db_version): def set_db_version(self): """ Set the current database version """ - self.query.exec_("""PRAGMA user_version = {}""".format(DB_VERSION)) + self.query.exec_(f"""PRAGMA user_version = {DB_VERSION}""") def change_db(self, mode): """ Changes the current db file @@ -424,6 +547,7 @@ def add_books2db(self, books): def read_books_from_db(self): """ Reads the contents of the books' db table """ + # print("Reading data from db") del self.books[:] self.query.setForwardOnly(True) self.query.exec_("""SELECT * FROM books""") @@ -494,7 +618,7 @@ def on_file_table_fileDropped(self, dropped): # self.file_table.setSortingEnabled(False) for i in dropped: if splitext(i)[1] == ".lua": - self.create_row(i) + self.create_row(normpath(i)) # self.file_table.setSortingEnabled(True) folders = [j for j in dropped if isdir(j)] for folder in folders: @@ -535,36 +659,44 @@ def populate_book_info(self, data, row): :type row: int :param row: The item's row number """ + stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" for key, field in zip(self.info_keys, self.info_fields): try: - if key == "title" and not data["stats"][key]: + if key == "title" and not data[stats][key]: path = self.file_table.item(row, PATH).data(0) try: name = path.split("#] ")[1] value = splitext(name)[0] except IndexError: # no "#] " in filename value = "" + elif key == "pages": + if "doc_pages" in data: + value = data["doc_pages"] + else: # older type file + value = data[stats][key] elif key == "keywords": keywords = data["doc_props"][key].split("\n") value = "; ".join([i.rstrip("\\") for i in keywords]) else: - value = data["stats"][key] + value = data[stats][key] try: field.setText(value) except TypeError: # Needs string only field.setText(str(value) if value else "") # "" if 0 except KeyError: # older type file or other problems path = self.file_table.item(row, PATH).data(0) - stats = self.get_item_stats(path, data) + stats_ = self.get_item_stats(data, path) if key == "title": - field.setText(stats[1]) + field.setText(stats_["title"]) elif key == "authors": - field.setText(stats[2]) + field.setText(stats_["authors"]) else: field.setText("") review = data.get("summary", {}).get("note", "") self.review_lbl.setVisible(bool(review)) + color = self.app.palette().base().color().name() + self.review_txt.setStyleSheet(f'background-color: "{color}";') self.review_txt.setVisible(bool(review)) self.review_txt.setText(review) @@ -595,12 +727,7 @@ def on_file_table_customContextMenuRequested(self, point): self.act_view_book.setEnabled(self.toolbar.open_btn.isEnabled()) self.act_view_book.setData(row) menu.addAction(self.act_view_book) - - export_menu = self.get_export_menu() - export_menu.setIcon(self.ico_file_save) - export_menu.setTitle(_("Export")) - menu.addMenu(export_menu) - + menu.addMenu(self.export_menu) if not self.db_mode: action = QAction(_("Archive") + "\tAlt+A", menu) action.setIcon(self.ico_db_add) @@ -611,11 +738,28 @@ def on_file_table_customContextMenuRequested(self, point): sync_group = QMenu(self) sync_group.setTitle(_("Sync")) sync_group.setIcon(self.ico_files_merge) - if self.check4archive_merge() is not False: + + action = QAction(_("Create a sync group"), sync_group) + action.setIcon(self.ico_files_merge) + path = self.file_table.item(row, PATH).data(0) + title = self.file_table.item(row, TITLE).data(0) + data = self.file_table.item(row, TITLE).data(Qt.UserRole) + book_data = {"path": path, "data": data} + info = {"title": title, + "sync_pos": False, + "merge": False, + "sync_db": True, + "items": [book_data], + "enabled": True} + action.triggered.connect(partial(self.create_sync_row, info)) + sync_group.addAction(action) + + if self.check4archive_merge(book_data) is not False: sync_menu = self.create_archive_merge_menu() sync_menu.setTitle(_("Sync with archived")) sync_menu.setIcon(self.ico_files_merge) sync_group.addMenu(sync_menu) + action = QAction(_("Sync with file"), sync_group) action.setIcon(self.ico_files_merge) action.triggered.connect(self.use_meta_files) @@ -635,11 +779,20 @@ def on_file_table_customContextMenuRequested(self, point): action.triggered.connect(partial(self.open_file, folder_path)) menu.addAction(action) + elif len(self.sel_indexes) == 2: + if self.toolbar.merge_btn.isEnabled(): + action = QAction(_("Sync books"), menu) + action.setIcon(self.ico_files_merge) + action.triggered.connect(self.toolbar.on_merge_btn_clicked) + menu.addAction(action) + + menu.addSeparator() delete_menu = self.delete_menu() delete_menu.setIcon(self.ico_files_delete) delete_menu.setTitle(_("Delete") + "\tDel") menu.addMenu(delete_menu) else: + menu.addSeparator() action = QAction(_("Delete") + "\tDel", menu) action.setIcon(self.ico_files_delete) action.triggered.connect(partial(self.delete_actions, 0)) @@ -673,7 +826,7 @@ def get_book_path(meta_path, data): ext = splitext(meta_path)[1] book_path = splitext(split(meta_path)[0])[0] + ext book_exists = isfile(book_path) - if not book_exists: # use the recorded file path + if not book_exists: # use the recorded file path (newer metadata only) doc_path = data.get("doc_path") if doc_path: drive = splitdrive(meta_path)[0] @@ -719,6 +872,8 @@ def file_selection_update(self, selected, deselected): else: self.high_list.clear() self.description_btn.setEnabled(False) + self.review_txt.hide() + self.review_lbl.hide() for field in self.info_fields: field.setText("") self.toolbar.activate_buttons() @@ -760,7 +915,9 @@ def toggle_title_sort(self): """ Toggles the way titles are sorted (use or not A/The) """ self.alt_title_sort = not self.alt_title_sort - text = _("ReSorting books...") + self.reload_table(_("ReSorting books...")) + + def reload_table(self, text): if not self.db_mode: self.loading_thread(ReLoader, self.loaded_paths.copy(), text) else: @@ -790,7 +947,7 @@ def on_archive(self): if self.archive_warning: # warn about book replacement in archive extra = _("these books") if len(self.sel_indexes) > 1 else _("this book") popup = self.popup(_("Question!"), - _("Add or replace {} in the archive?").format(extra), + _(f"Add or replace {extra} in the archive?"), buttons=2, icon=QMessageBox.Question, check_text=DO_NOT_SHOW) self.archive_warning = not popup.checked @@ -806,9 +963,18 @@ def on_archive(self): path = self.file_table.item(row, PATH).text() date = self.file_table.item(row, MODIFIED).text() data = self.file_table.item(row, TITLE).data(Qt.UserRole) - if not data["highlight"]: # no highlights, don't add - empty += 1 - continue + annotations = data.get("annotations") + if annotations is not None: # new format metadata + for high_idx in annotations: + if annotations[high_idx]["pos0"]: + break # there is at least one highlight in the book + else: # no highlights don't add + empty += 1 + continue + else: # old format metadata + if not data["highlight"]: # no highlights, don't add + empty += 1 + continue try: md5 = data["partial_md5_checksum"] except KeyError: # older metadata, don't add @@ -823,24 +989,24 @@ def on_archive(self): extra = "" if empty: - extra += _("\nNot added {} books with no highlights.").format(empty) + extra += _(f"\nNot added {empty} books with no highlights.") if older: - extra += _("\nNot added {} books with old type metadata.").format(older) + extra += _(f"\nNot added {older} books with old type metadata.") self.popup(_("Added!"), - _("{} books were added/updated to the Archive from the {} processed.") - .format(added, len(self.sel_indexes)) + extra, + _(f"{added} books were added/updated to the Archive from the " + f"{len(self.sel_indexes)} processed."), icon=QMessageBox.Information) def loading_thread(self, worker, args, text, clear=True): """ Populates the file_table with different contents """ - if clear: + if clear and self.current_view != SYNC_VIEW: self.toolbar.on_clear_btn_clicked() self.file_table.setSortingEnabled(False) # re-enable it after populating table self.status.animation(True) - self.auto_info.set_text(_("{}.\nPlease Wait...").format(text)) + self.auto_info.set_text(_(f"{text}.\nPlease Wait...")) self.auto_info.show() scan_thread = QThread() @@ -884,20 +1050,26 @@ def create_row(self, meta_path, data=None, date=None): if meta_path in self.loaded_paths: return # already loaded file self.loaded_paths.add(meta_path) - data = decode_data(meta_path) + try: + data = decode_data(meta_path) + except PermissionError: + self.base.error(_(f"Could not access the book's metadata file\n" + f"{meta_path}")) + return if not data: print("No data here!", meta_path) return date = str(datetime.fromtimestamp(getmtime(meta_path))).split(".")[0] - stats = self.get_item_stats(meta_path, data) + stats = self.get_item_stats(data, meta_path) else: # for db entries - stats = self.get_item_db_stats(data) - icon, title, authors, percent, rating, status, high_count = stats - - # noinspection PyArgumentList - color = ("#660000" if status == "abandoned" else - # "#005500" if status == "complete" else - QApplication.palette().text().color()) + stats = self.get_item_stats(data) + title = stats["title"] + authors = stats["authors"] + percent = stats["percent"] + rating = stats["rating"] + status = stats["status"] + high_count = stats["high_count"] + icon = self.ico_label_green if high_count else self.ico_empty self.file_table.setSortingEnabled(False) self.file_table.insertRow(0) @@ -917,7 +1089,7 @@ def create_row(self, meta_path, data=None, date=None): book_icon = self.ico_file_exists if book_exists else self.ico_file_missing type_item = QTableWidgetItem(book_icon, ext) type_item.setToolTip(book_path if book_exists else - _("The {} file is missing!").format(ext)) + _(f"The {ext} file is missing!")) type_item.setData(Qt.UserRole, (book_path, book_exists)) self.file_table.setItem(0, TYPE, type_item) @@ -945,71 +1117,58 @@ def create_row(self, meta_path, data=None, date=None): for i in range(8): # colorize row item = self.file_table.item(0, i) - item.setForeground(QBrush(QColor(color))) + if status == "abandoned": + if self.theme in (THEME_DARK_NEW, THEME_DARK_OLD): + color = "#DD0000" + else: + color = "#660000" + item.setForeground(QBrush(QColor(color))) self.file_table.setSortingEnabled(True) - def get_item_db_stats(self, data): - """ Returns the title and authors of a history file + @staticmethod + def get_item_stats(data, filename=None): + """ Returns the title and authors of a metadata file :type data: dict :param data: The dict converted lua file - """ - if data["highlight"]: - icon = self.ico_label_green - high_count = str(len(data["highlight"])) - else: - icon = self.ico_empty - high_count = "" - title = data["stats"]["title"] - authors = data["stats"]["authors"] - title = title if title else NO_TITLE - authors = authors if authors else NO_AUTHOR - try: - percent = str(int(data["percent_finished"] * 100)) + "%" - except KeyError: - percent = "" - if "summary" in data: - rating = data["summary"].get("rating") - rating = rating * "*" if rating else "" - status = data["summary"].get("status") - else: - rating = "" - status = None - - return icon, title, authors, percent, rating, status, high_count - - def get_item_stats(self, filename, data): - """ Returns the title and authors of a metadata file - :type filename: str|unicode :param filename: The filename to get the stats for - :type data: dict - :param data: The dict converted lua file """ - if data["highlight"]: - icon = self.ico_label_green - high_count = str(len(data["highlight"])) - else: - icon = self.ico_empty - high_count = "" - try: - title = data["stats"]["title"] - authors = data["stats"]["authors"] - except KeyError: # older type file - title = splitext(basename(filename))[0] - try: - name = title.split("#] ")[1] - title = splitext(name)[0] - except IndexError: # no "#] " in filename - pass - authors = OLD_TYPE - if not title: + stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" + if filename: # stats from a file try: - name = filename.split("#] ")[1] - title = splitext(name)[0] - except IndexError: # no "#] " in filename - title = NO_TITLE - authors = authors if authors else NO_AUTHOR + title = data[stats]["title"] + authors = data[stats]["authors"] + except KeyError: # much older type file + title = splitext(basename(filename))[0] + try: + name = title.split("#] ")[1] + title = splitext(name)[0] + except IndexError: # no "#] " in filename + pass + authors = OLD_TYPE + if not title: + try: + name = filename.split("#] ")[1] + title = splitext(name)[0] + except IndexError: # no "#] " in filename + title = NO_TITLE + authors = authors if authors else NO_AUTHOR + else: # stats from a db entry + title = data[stats]["title"] + authors = data[stats]["authors"] + + annotations = data.get("annotations") + if annotations is not None: # new format metadata + high_count = len([i for i in annotations.values() if i.get("pos0")]) + else: # old format metadata + high_count = 0 + if data["highlight"]: + for page in data["highlight"]: + high_count += len(data["highlight"][page]) + high_count = str(high_count) if high_count else "" + high_count = str(high_count) if high_count else "" + try: percent = str(int(data["percent_finished"] * 100)) + "%" except KeyError: @@ -1021,110 +1180,575 @@ def get_item_stats(self, filename, data): else: rating = "" status = None + return {"title": title, "authors": authors, "percent": percent, + "rating": rating, "status": status, "high_count": high_count} - return icon, title, authors, percent, rating, status, high_count - - # ___ ___________________ HIGHLIGHT TABLE STUFF _________________ + # ___ ___________________ HIGHLIGHTS LIST STUFF _________________ - @Slot(QTableWidgetItem) - def on_high_table_itemClicked(self, item): - """ When an item of the high_table is clicked + def populate_high_list(self, data, path=""): + """ Populates the Highlights list of `Book` view - :type item: QTableWidgetItem - :param item: The item (cell) that is clicked + :type data: dict + :param data: The item/book's data + :type path: str|unicode + :param path: The item/book's path """ - row = item.row() - path = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole)["path"] + space = (" " if self.status.act_page.isChecked() and + self.status.act_date.isChecked() else "") + line_break = (":\n" if self.status.act_page.isChecked() or + self.status.act_date.isChecked() else "") + def_date_format = self.date_format == DATE_FORMAT + highlights = self.get_highlights_from_data(data, path) + for i in sorted(highlights, key=self.sort_high4view): + chapter_text = (f"[{i['chapter']}]\n" + if (i["chapter"] and self.status.act_chapter.isChecked()) + else "") + page_text = (_(f"Page {i['page']}") + if self.status.act_page.isChecked() else "") + date = i["date"] if def_date_format else self.get_date_text(i["date"]) + date_text = "[" + date + "]" if self.status.act_date.isChecked() else "" + high_text = i["text"] if self.status.act_text.isChecked() else "" + line_break2 = ("\n" if self.status.act_comment.isChecked() and i["comment"] + else "") + high_comment = line_break2 + "● " + i["comment"] if line_break2 else "" + highlight = (page_text + space + date_text + line_break + chapter_text + + high_text + high_comment + "\n") - # needed for edit "Comments" or "Find in Books" in Highlight View - for row in range(self.file_table.rowCount()): # 2check: need to optimize? - if path == self.file_table.item(row, TYPE).data(Qt.UserRole)[0]: - self.parent_book_data = self.file_table.item(row, TITLE).data(Qt.UserRole) - break + highlight_item = QListWidgetItem(highlight, self.high_list) + highlight_item.setData(Qt.UserRole, i) - @Slot(QModelIndex) - def on_high_table_doubleClicked(self, index): - """ When an item of the high_table is double-clicked + def get_highlights_from_data(self, data, path="", meta_path=""): + """ Get the HighLights from the .sdr data - :type index: QTableWidgetItem - :param index: The item (cell) that is clicked + :type data: dict + :param data: The lua converted book data + :type path: str|unicode + :param path: The book's path + :type meta_path: str|unicode + :param meta_path: The book metadata file's path """ - column = index.column() - if column == COMMENT_H: - self.on_edit_comment() + stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" + authors = data.get(stats, {}).get("authors", NO_AUTHOR) + title = data.get(stats, {}).get("title", NO_TITLE) + common = {"authors": authors, "title": title, + "path": path, "meta_path": meta_path} - @Slot(QPoint) - def on_high_table_customContextMenuRequested(self, point): - """ When an item of the high_table is right-clicked + highlights = [] + annotations = data.get("annotations") + if annotations is not None: # new format metadata + for idx in annotations: + highlight = self.get_new_highlight_info(data, idx) + if highlight: + # noinspection PyTypeChecker + highlight.update(common) + highlights.append(highlight) + else: + for page in data["highlight"]: + for page_id in data["highlight"][page]: + highlight = self.get_old_highlight_info(data, page, page_id) + if highlight: + # noinspection PyTypeChecker + highlight.update(common) + highlights.append(highlight) + return highlights - :type point: QPoint - :param point: The point where the right-click happened - """ - if not len(self.sel_high_view): # no items selected - return + @staticmethod + def get_new_highlight_info(data, idx): + """ Get the highlight's info (text, comment, date and page) [new format] - menu = QMenu(self.high_table) - if QT6: # QT6 requires exec() instead of exec_() - menu.exec_ = getattr(menu, "exec") + :type data: dict + :param data: The book's metadata + :type idx: int + :param idx: The highlight's idx + """ + high_stuff = data["annotations"][idx] + if not high_stuff.get("pos0"): + return # this is a bookmark not a highlight + pages = data["doc_pages"] + page = high_stuff.get("pageno", 0) + highlight = {"text": high_stuff.get("text", "").replace("\\\n", "\n"), + "chapter": high_stuff.get("chapter", ""), + "comment": high_stuff.get("note", "").replace("\\\n", "\n"), + "date": high_stuff.get("datetime", ""), "idx": idx, + "page": page, "pages": pages, "new": True} + return highlight - row = self.high_table.itemAt(point).row() - self.act_view_book.setData(row) - self.act_view_book.setEnabled(self.toolbar.open_btn.isEnabled()) - menu.addAction(self.act_view_book) + @staticmethod + def get_old_highlight_info(data, page, page_id): + """ Get the highlight's info (text, comment, date and page) - highlights, comments = self.get_highlights() + :type data: dict + :param data: The highlight's data + :type page: int + :param page The page where the highlight starts + :type page_id: int + :param page_id The count of this page's highlight + """ + pages = data.get("doc_pages", data.get("stats", {}).get("pages", 0)) + highlight = {"page": str(page), "page_id": page_id, "pages": pages, "new": False} + try: + high_stuf = data["highlight"][page][page_id] + date = high_stuf["datetime"] + text4check = high_stuf["text"] + chapter = high_stuf.get("chapter", "") + pat = r"Page \d+ (.+?) @ \d+-\d+-\d+ \d+:\d+:\d+" + text = text4check.replace("\\\n", "\n") + comment = "" + for idx in data["bookmarks"]: # check for comment text + if text4check == data["bookmarks"][idx]["notes"]: + bkm_text = data["bookmarks"][idx].get("text", "") + if not bkm_text or (bkm_text == text4check): + break + bkm_text = re.sub(pat, r"\1", bkm_text, 1, re.DOTALL | re.MULTILINE) + if text4check != bkm_text: # there is a comment + comment = bkm_text.replace("\\\n", "\n") + break + highlight["date"] = date + highlight["text"] = text + highlight["comment"] = comment + highlight["chapter"] = chapter + except KeyError: # blank highlight + return + return highlight - high_text = _("Copy Highlights") - com_text = _("Copy Comments") - if len(self.sel_high_view) == 1: # single selection - high_text = _("Copy Highlight") - com_text = _("Copy Comment") + @Slot(QPoint) + def on_high_list_customContextMenuRequested(self, point): + """ When a highlight is right-clicked - text = _("Find in Archive") if self.db_mode else _("Find in Books") - action = QAction(text, menu) - action.triggered.connect(partial(self.find_in_books, highlights)) - action.setIcon(self.ico_view_books) - menu.addAction(action) + :type point: QPoint + :param point: The point where the right-click happened + """ + if self.sel_high_list: + menu = QMenu(self.high_list) + if QT6: # QT6 requires exec() instead of exec_() + menu.exec_ = getattr(menu, "exec") - action = QAction(_("Comments"), menu) + action = QAction(_("Comment"), menu) action.triggered.connect(self.on_edit_comment) action.setIcon(self.ico_file_edit) menu.addAction(action) - action = QAction(high_text + "\tCtrl+C", menu) - action.triggered.connect(partial(self.copy_text_2clip, highlights)) - action.setIcon(self.ico_copy) - menu.addAction(action) + action = QAction(_("Copy"), menu) + action.triggered.connect(self.on_copy_highlights) + action.setIcon(self.ico_copy) + menu.addAction(action) - action = QAction(com_text + "\tAlt+C", menu) - action.triggered.connect(partial(self.copy_text_2clip, comments)) - action.setIcon(self.ico_copy) - menu.addAction(action) + menu.addSeparator() - action = QAction(_("Export to file"), menu) - action.triggered.connect(self.on_export) - action.setData(2) - action.setIcon(self.ico_file_save) - menu.addAction(action) + action = QAction(_("Delete"), menu) + action.triggered.connect(self.on_delete_highlights) + action.setIcon(self.ico_delete) + menu.addAction(action) - menu.exec_(self.high_table.mapToGlobal(point)) + menu.exec_(self.high_list.mapToGlobal(point)) - def get_highlights(self): - """ Returns the selected highlights and the comments texts + @Slot() + def on_high_list_itemDoubleClicked(self): + """ An item on the Highlight List is double-clicked """ - highlights = "" - comments = "" - for idx in self.sel_high_view: - item_row = idx.row() - data = self.high_table.item(item_row, HIGHLIGHT_H).data(Qt.UserRole) - highlight = data["text"] - if highlight: - highlights += highlight + "\n\n" - comment = data["comment"] - if comment: - comments += comment + "\n\n" - highlights = highlights.rstrip("\n").replace("\n", os.linesep) - comments = comments.rstrip("\n").replace("\n", os.linesep) + self.on_edit_comment() + + def on_edit_comment(self): + """ Opens a window to edit the selected highlight's comment + """ + if self.file_table.isVisible(): # edit comments from Book View + row = self.sel_high_list[-1].row() + comment = self.high_list.item(row).data(Qt.UserRole)["comment"] + elif self.high_table.isVisible(): # edit comments from Highlights View + row = self.sel_high_view[-1].row() + high_data = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole) + comment = high_data["comment"] + else: + return + self.edit_high.high_edit_txt.setText(comment) + # self.edit_high.high_edit_txt.setFocus() + if QT6: # QT6 requires exec() instead of exec_() + self.edit_high.exec_ = getattr(self.edit_high, "exec") + self.edit_high.exec_() + + def edit_comment_ok(self): + """ Change the selected highlight's comment + """ + note = self.edit_high.high_edit_txt.toPlainText() + if self.file_table.isVisible(): # update comment from Book table + high_row = self.sel_high_list[-1].row() + high_data = self.high_list.item(high_row).data(Qt.UserRole) + item = self.file_table.item + row = self.sel_idx.row() + data = item(row, TITLE).data(Qt.UserRole) + self.update_comment(data, high_data, note) + + if not self.db_mode: # Loaded mode + path = item(row, PATH).text() + self.save_book_data(path, data) + else: # Archived mode + self.update_book2db(data) + self.on_file_table_itemClicked(item(row, 0), reset=False) + + elif self.high_table.isVisible(): # update comment from Highlights table + row = self.sel_high_view[-1].row() + high_data = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole) + meta_path = high_data["meta_path"] + data = self.get_parent_book_data(row)[0] + + self.update_comment(data, high_data, note) + self.high_table.item(row, HIGHLIGHT_H).setData(Qt.UserRole, high_data) + self.high_table.item(row, COMMENT_H).setText(note) + + if not self.db_mode: # Loaded mode + self.save_book_data(meta_path, data) + else: # Archived mode + self.update_book2db(data) + + self.reload_highlights = True + + @staticmethod + def update_comment(data, high_data, note): + """ Update the comment of the selected highlight + """ + date = datetime.now().strftime(DATE_FORMAT) + high_text = high_data["text"].replace("\n", "\\\n") + annotations = data.get("annotations") + if annotations is not None: # new format metadata + for idx in annotations: + if high_text == annotations[idx]["text"]: + annotations[idx]["note"] = note.replace("\n", "\\\n") + annotations[idx]["datetime"] = date # update last edit date + high_data["comment"] = note + break + else: # old format metadata + for bkm in data["bookmarks"]: + if high_text == data["bookmarks"][bkm]["notes"]: + high_data["comment"] = note + data["bookmarks"][bkm]["text"] = note.replace("\n", "\\\n") + data["bookmarks"][bkm]["datetime"] = date # update bkm edit date + for pg in data["highlight"]: # and highlight's too + for pg_id in data["highlight"][pg]: + if data["highlight"][pg][pg_id]["text"] == high_text: + data["highlight"][pg][pg_id]["datetime"] = date + break + + def on_copy_highlights(self): + """ Copy the selected highlights to clipboard + """ + clipboard_text = "" + for highlight in sorted(self.sel_high_list): + row = highlight.row() + text = self.high_list.item(row).text() + clipboard_text += text + "\n" + self.copy_text_2clip(clipboard_text) + + def on_delete_highlights(self): + """ The delete highlights action was invoked + """ + if not self.db_mode: + if self.edit_lua_file_warning: + text = _("This is an one-time warning!\n\nIn order to delete highlights " + "from a book, its \"metadata\" file must be edited. This " + "contains a small risk of corrupting that file and lose all the " + "settings and info of that book.\n\nDo you still want to do it?") + popup = self.popup(_("Warning!"), text, buttons=2, + button_text=(_("Yes"), _("No"))) + if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: + return + else: + self.edit_lua_file_warning = False + text = _("This will delete the selected highlights!\nAre you sure?") + else: + text = _("This will remove the selected highlights from the Archive!\n" + "Are you sure?") + popup = self.popup(_("Warning!"), text, buttons=2, + button_text=(_("Yes"), _("No"))) + if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: + return + self.delete_highlights() + + def delete_highlights(self): + """ Delete the selected highlights + """ + if self.file_table.isVisible(): # delete comments from Book table + row = self.sel_idx.row() + data = self.file_table.item(row, TITLE).data(Qt.UserRole) + hi_count = int(self.file_table.item(row, HIGH_COUNT).text()) + annotations = data.get("annotations") + if annotations is not None: # new format metadata + for high in self.sel_high_list: + high_data = self.high_list.item(high.row()).data(Qt.UserRole) + idx = high_data["idx"] + del annotations[idx] # delete the highlight + hi_count -= 1 + self.finalize_new_highs(data) + else: # old format metadata + for high in self.sel_high_list: + high_data = self.high_list.item(high.row()).data(Qt.UserRole) + self.del_old_high(data, high_data) + hi_count -= 1 + self.finalize_old_highs(data) + self.update_and_save_meta(row, data, hi_count) + self.reload_highlights = True + elif self.high_table.isVisible(): # delete comments from Highlights table + hi2del = {} + idx2del = [] + for hi_idx in self.sel_high_view: # collect the data from the highlights + row = hi_idx.row() + high_data = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole) + data = self.get_parent_book_data(row)[0] + meta_path = high_data["meta_path"] + idx2del.append(row) + data_list = hi2del.get(meta_path, []) + data_list.append((data, high_data, row)) + hi2del[meta_path] = data_list + + for meta_path, data_list in hi2del.items(): + book_row = 0 + for book_row in range(self.file_table.rowCount()): + if self.file_table.item(book_row, PATH).text() == meta_path: + break + hi_count = self.file_table.item(book_row, HIGH_COUNT).text() + hi_count = int(hi_count) if hi_count else 0 + new_format = False + data = None + for data, high_data, row in data_list: + annotations = data.get("annotations") + if annotations is not None: # new format metadata + new_format = True + idx = high_data["idx"] + del annotations[idx] # delete the highlight + hi_count -= 1 + else: # old format metadata + self.del_old_high(data, high_data) + hi_count -= 1 + if new_format: + self.finalize_new_highs(data) + else: + self.finalize_old_highs(data) # data is same for same meta_path + self.update_and_save_meta(book_row, data, hi_count) + + idx2del = sorted(idx2del, reverse=True) + for hi_idx in idx2del: + self.high_table.removeRow(hi_idx) + + @staticmethod + def finalize_new_highs(data): + annotations = data.get("annotations") + new_annot = {idx + 1: annotations[i] # renumbering the annotations + for idx, i in enumerate(sorted(annotations))} + if new_annot: + data["annotations"] = new_annot + + @staticmethod + def del_old_high(data, hi_data): + page = int(hi_data["page"]) + page_id = hi_data["page_id"] + del data["highlight"][page][page_id] # delete the highlight + text = hi_data["text"] # delete the associated bookmark + for bookmark in list(data["bookmarks"].keys()): + if text == data["bookmarks"][bookmark]["notes"]: + del data["bookmarks"][bookmark] + + @staticmethod + def finalize_old_highs(data): + for i in list(data["highlight"].keys()): + if not data["highlight"][i]: # delete page dicts with no highlights + del data["highlight"][i] + else: # renumbering the highlight keys + contents = [data["highlight"][i][j] for j in sorted(data["highlight"][i])] + if contents: + for l in list(data["highlight"][i].keys()): + del data["highlight"][i][l] # delete all the items and + for k in range(len(contents)): # rewrite with the new keys + data["highlight"][i][k + 1] = contents[k] + contents = [data["bookmarks"][bookmark] for bookmark in sorted(data["bookmarks"])] + if contents: # renumbering the bookmarks keys + for bookmark in list(data["bookmarks"].keys()): + del data["bookmarks"][bookmark] # delete all the items and + for content in range(len(contents)): # rewrite them with the new keys + data["bookmarks"][content + 1] = contents[content] + + def update_and_save_meta(self, row, data, hi_count): + if not hi_count: # change icon if no highlights + self.file_table.item(row, TITLE).setIcon(self.ico_empty) + hi_count = "" + self.file_table.item(row, HIGH_COUNT).setText(str(hi_count)) + self.file_table.item(row, HIGH_COUNT).setToolTip(str(hi_count)) + if not self.db_mode: + path = self.file_table.item(row, PATH).text() + data["annotations_externally_modified"] = True + self.save_book_data(path, data) + else: + self.update_book2db(data) + self.on_file_table_itemClicked(self.file_table.item(row, TITLE), reset=False) + + def save_book_data(self, path, data): + """ Saves the data of a book to its lua file + + :type path: str|unicode + :param path: The path to the book's data file + :type data: dict + :param data: The book's data + """ + times = os.stat(path) # read the file's created/modified times + encode_data(path, data) + if data.get("summary", {}).get("status") in ["abandoned", "complete"]: + os.utime(path, (times.st_ctime, times.st_mtime)) # reapply original times + if self.file_table.isVisible(): + self.on_file_table_itemClicked(self.file_table.item(self.sel_idx.row(), 0), + reset=False) + + # noinspection PyUnusedLocal + def high_list_selection_update(self, selected, deselected): + """ When a highlight in gets selected + + :type selected: QModelIndex + :parameter selected: The selected highlight + :type deselected: QModelIndex + :parameter deselected: The deselected highlight + """ + self.sel_high_list = self.high_list_selection.selectedRows() + + def set_highlight_sort(self): + """ Sets the sorting method of displayed highlights + """ + self.high_by_page = self.sender().data() + try: + row = self.sel_idx.row() + self.on_file_table_itemClicked(self.file_table.item(row, 0), False) + except AttributeError: # no book selected + pass + + def sort_high4view(self, data): + """ Sets the sorting method of displayed highlights + + :type data: tuple + param: data: The highlight's data + """ + if not self.high_by_page: + return data["date"] + else: + return int(data["page"]) + + def sort_high4write(self, data): + """ Sets the sorting method of written highlights + + :type data: tuple + param: data: The highlight's data + """ + if self.high_by_page and self.status.act_page.isChecked(): + page = data[3] + if page.startswith("Page"): + page = page[5:] + return int(page) + else: + return data[0] + + # ___ ___________________ HIGHLIGHT TABLE STUFF _________________ + + @Slot(QTableWidgetItem) + def on_high_table_itemClicked(self, item): + """ When an item of the high_table is clicked + + :type item: QTableWidgetItem + :param item: The item (cell) that is clicked + """ + # row = item.row() + # self.get_parent_book_data(row) + + def get_parent_book_data(self, row): + meta_path = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole)["meta_path"] + for row in range(self.file_table.rowCount()): # 2check: need to optimize? + if meta_path == self.file_table.item(row, PATH).data(0): + parent_book_data = self.file_table.item(row, TITLE).data(Qt.UserRole) + return parent_book_data, meta_path + + @Slot(QModelIndex) + def on_high_table_doubleClicked(self, index): + """ When an item of the high_table is double-clicked + + :type index: QTableWidgetItem + :param index: The item (cell) that is clicked + """ + column = index.column() + if column == COMMENT_H: + self.on_edit_comment() + + @Slot(QPoint) + def on_high_table_customContextMenuRequested(self, point): + """ When an item of the high_table is right-clicked + + :type point: QPoint + :param point: The point where the right-click happened + """ + if not len(self.sel_high_view): # no items selected + return + + menu = QMenu(self.high_table) + if QT6: # QT6 requires exec() instead of exec_() + menu.exec_ = getattr(menu, "exec") + + row = self.high_table.itemAt(point).row() + self.act_view_book.setData(row) + self.act_view_book.setEnabled(self.toolbar.open_btn.isEnabled()) + menu.addAction(self.act_view_book) + + highlights, comments = self.get_highlights() + + high_text = _("Copy Highlights") + com_text = _("Copy Comments") + if len(self.sel_high_view) == 1: # single selection + high_text = _("Copy Highlight") + com_text = _("Copy Comment") + + text = _("Find in Archive") if self.db_mode else _("Find in Books") + action = QAction(text, menu) + action.triggered.connect(partial(self.find_in_books, highlights, row)) + action.setIcon(self.ico_view_books) + menu.addAction(action) + + action = QAction(_("Comments"), menu) + action.triggered.connect(self.on_edit_comment) + action.setIcon(self.ico_file_edit) + menu.addAction(action) + + action = QAction(high_text + "\tCtrl+C", menu) + action.triggered.connect(partial(self.copy_text_2clip, highlights)) + action.setIcon(self.ico_copy) + menu.addAction(action) + + action = QAction(com_text + "\tAlt+C", menu) + action.triggered.connect(partial(self.copy_text_2clip, comments)) + action.setIcon(self.ico_copy) + menu.addAction(action) + + action = QAction(_("Export to file"), menu) + action.triggered.connect(self.on_export) + action.setData(2) + action.setIcon(self.ico_file_save) + menu.addAction(action) + + menu.addSeparator() + action = QAction(_("Delete") + "\tDel", menu) + action.setIcon(self.ico_files_delete) + action.triggered.connect(self.toolbar.on_delete_btn_clicked) + menu.addAction(action) + + menu.exec_(self.high_table.mapToGlobal(point)) + + def get_highlights(self): + """ Returns the selected highlights and the comments texts + """ + highlights = "" + comments = "" + for idx in self.sel_high_view: + item_row = idx.row() + data = self.high_table.item(item_row, HIGHLIGHT_H).data(Qt.UserRole) + highlight = data["text"] + if highlight: + highlights += highlight + "\n\n" + comment = data["comment"] + if comment: + comments += comment + "\n\n" + highlights = highlights.rstrip("\n").replace("\n", os.linesep) + comments = comments.rstrip("\n").replace("\n", os.linesep) return highlights, comments def scan_highlights_thread(self): @@ -1173,13 +1797,13 @@ def create_highlight_row(self, data): text = data["text"] item = QTableWidgetItem(text) - item.setToolTip("

{}

".format(text)) + item.setToolTip(f"

{text}

") item.setData(Qt.UserRole, data) self.high_table.setItem(0, HIGHLIGHT_H, item) comment = data["comment"] item = QTableWidgetItem(comment) - item.setToolTip("

{}

".format(comment)) if comment else None + item.setToolTip(f"

{comment}

") if comment else None self.high_table.setItem(0, COMMENT_H, item) date = data["date"] @@ -1207,434 +1831,311 @@ def create_highlight_row(self, data): chapter = data["chapter"] item = XTableWidgetIntItem(chapter) item.setToolTip(chapter) - self.high_table.setItem(0, CHAPTER_H, item) - - path = data["path"] - item = QTableWidgetItem(path) - item.setToolTip(path) - self.high_table.setItem(0, PATH_H, item) - - self.high_table.setSortingEnabled(True) - - # noinspection PyUnusedLocal - def high_view_selection_update(self, selected, deselected): - """ When a row in high_table gets selected - - :type selected: QModelIndex - :parameter selected: The selected row - :type deselected: QModelIndex - :parameter deselected: The deselected row - """ - try: - if not self.filter.isVisible(): - self.sel_high_view = self.high_view_selection.selectedRows() - else: - self.sel_high_view = [i for i in self.high_view_selection.selectedRows() - if not self.high_table.isRowHidden(i.row())] - except IndexError: # empty table - self.sel_high_view = [] - self.toolbar.activate_buttons() - - def on_highlight_column_clicked(self, column): - """ Sets the current sorting column - - :type column: int - :parameter column: The column where the sorting is applied - """ - if column == self.col_sort_h: - self.col_sort_asc_h = not self.col_sort_asc_h - else: - self.col_sort_asc_h = True - self.col_sort_h = column - - # noinspection PyUnusedLocal - def on_highlight_column_resized(self, column, oldSize, newSize): - """ Gets the column size - - :type column: int - :parameter column: The resized column - :type oldSize: int - :parameter oldSize: The old size - :type newSize: int - :parameter newSize: The new size - """ - if column == HIGHLIGHT_H: - self.highlight_width = newSize - elif column == COMMENT_H: - self.comment_width = newSize - - def find_in_books(self, highlight): - """ Finds the current highlight in the "Books View" - - :type highlight: str|unicode - :parameter highlight: The highlight we are searching for - """ - data = self.parent_book_data - - for row in range(self.file_table.rowCount()): - item = self.file_table.item(row, TITLE) - row_data = item.data(Qt.UserRole) - try: # find the book row - if data["stats"]["title"] == row_data["stats"]["title"]: - self.views.setCurrentIndex(BOOKS_VIEW) - self.toolbar.books_view_btn.setChecked(True) - self.toolbar.setup_buttons() - self.toolbar.activate_buttons() - self.file_table.selectRow(row) # select the book - self.on_file_table_itemClicked(item) - for high_row in range(self.high_list.count()): # find the highlight - if (self.high_list.item(high_row) - .data(Qt.UserRole)["text"] == highlight): - self.high_list.setCurrentRow(high_row) # select the highlight - return - except KeyError: # old metadata with no "stats" - continue - - # ___ ___________________ HIGHLIGHTS LIST STUFF _________________ - - def populate_high_list(self, data, path=""): - """ Populates the Highlights list of `Book` view - - :type data: dict - :param data: The item/book's data - :type path: str|unicode - :param path: The item/book's path - """ - space = (" " if self.status.act_page.isChecked() and - self.status.act_date.isChecked() else "") - line_break = (":\n" if self.status.act_page.isChecked() or - self.status.act_date.isChecked() else "") - def_date_format = self.date_format == DATE_FORMAT - highlights = self.get_highlights_from_data(data, path) - for i in sorted(highlights, key=self.sort_high4view): - chapter_text = i["chapter"] - if chapter_text and self.status.act_chapter.isChecked(): - chapter_text = "[{0}]\n".format(chapter_text) - page_text = (_("Page ") + i["page"] - if self.status.act_page.isChecked() else "") - date = i["date"] if def_date_format else self.get_date_text(i["date"]) - date_text = "[" + date + "]" if self.status.act_date.isChecked() else "" - high_text = i["text"] if self.status.act_text.isChecked() else "" - line_break2 = ("\n" if self.status.act_comment.isChecked() and i["comment"] - else "") - high_comment = line_break2 + "● " + i["comment"] if line_break2 else "" - highlight = (page_text + space + date_text + line_break + chapter_text + - high_text + high_comment + "\n") - - highlight_item = QListWidgetItem(highlight, self.high_list) - highlight_item.setData(Qt.UserRole, i) - - def get_highlights_from_data(self, data, path=""): - """ Get the HighLights from the .sdr data - - :type data: dict - :param data: The lua converted book data - :type path: str|unicode - :param path: The book's path - """ - authors = data.get("stats", {}).get("authors", NO_AUTHOR) - title = data.get("stats", {}).get("title", NO_TITLE) - - highlights = [] - for page in data["highlight"]: - for page_id in data["highlight"][page]: - highlight = self.get_highlight_info(data, page, page_id) - if highlight: - # noinspection PyTypeChecker - highlight.update({"authors": authors, "title": title, - "path": path}) - highlights.append(highlight) - return highlights - - @staticmethod - def get_highlight_info(data, page, page_id): - """ Get the highlight's info (text, comment, date and page) - - :type data: dict - :param data: The highlight's data - :type page: int - :param page The page where the highlight starts - :type page_id: int - :param page_id The count of this page's highlight - """ - highlight = {} - try: - high_stuf = data["highlight"][page][page_id] - date = high_stuf["datetime"] - text4check = high_stuf["text"] - chapter = high_stuf.get("chapter", "") - pat = r"Page \d+ (.+?) @ \d+-\d+-\d+ \d+:\d+:\d+" - text = text4check.replace("\\\n", "\n") - comment = "" - for idx in data["bookmarks"]: # check for comment text - if text4check == data["bookmarks"][idx]["notes"]: - bkm_text = data["bookmarks"][idx].get("text", "") - if not bkm_text or (bkm_text == text4check): - break - bkm_text = re.sub(pat, r"\1", bkm_text, 1, re.DOTALL | re.MULTILINE) - if text4check != bkm_text: # there is a comment - comment = bkm_text.replace("\\\n", "\n") - break - highlight["date"] = date - highlight["text"] = text - highlight["comment"] = comment - highlight["chapter"] = chapter - highlight["page"] = str(page) - highlight["page_id"] = page_id - except KeyError: # blank highlight - return - return highlight - - @Slot(QPoint) - def on_high_list_customContextMenuRequested(self, point): - """ When a highlight is right-clicked - - :type point: QPoint - :param point: The point where the right-click happened - """ - if self.sel_high_list: - menu = QMenu(self.high_list) - if QT6: # QT6 requires exec() instead of exec_() - menu.exec_ = getattr(menu, "exec") - - action = QAction(_("Comments"), menu) - action.triggered.connect(self.on_edit_comment) - action.setIcon(self.ico_file_edit) - menu.addAction(action) - - action = QAction(_("Copy"), menu) - action.triggered.connect(self.on_copy_highlights) - action.setIcon(self.ico_copy) - menu.addAction(action) - - action = QAction(_("Delete"), menu) - action.triggered.connect(self.on_delete_highlights) - action.setIcon(self.ico_delete) - menu.addAction(action) - - menu.exec_(self.high_list.mapToGlobal(point)) - - @Slot() - def on_high_list_itemDoubleClicked(self): - """ An item on the Highlight List is double-clicked - """ - self.on_edit_comment() - - def on_edit_comment(self): - """ Opens a window to edit the selected highlight's comment - """ - if self.file_table.isVisible(): # edit comments from Book View - row = self.sel_high_list[-1].row() - comment = self.high_list.item(row).data(Qt.UserRole)["comment"] - elif self.high_table.isVisible(): # edit comments from Highlights View - row = self.sel_high_view[-1].row() - high_data = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole) - comment = high_data["comment"] - else: - return - self.edit_high.high_edit_txt.setText(comment) - # self.edit_high.high_edit_txt.setFocus() - self.edit_high.exec_() - - def edit_comment_ok(self): - """ Change the selected highlight's comment - """ - text = self.edit_high.high_edit_txt.toPlainText() - if self.file_table.isVisible(): - high_index = self.sel_high_list[-1] - high_row = high_index.row() - high_data = self.high_list.item(high_row).data(Qt.UserRole) - high_text = high_data["text"].replace("\n", "\\\n") - - row = self.sel_idx.row() - item = self.file_table.item - data = item(row, TITLE).data(Qt.UserRole) - - for bookmark in data["bookmarks"].keys(): - if high_text == data["bookmarks"][bookmark]["notes"]: - data["bookmarks"][bookmark]["text"] = text.replace("\n", "\\\n") - break - item(row, TITLE).setData(Qt.UserRole, data) - - if not self.db_mode: # Loaded mode - path = item(row, PATH).text() - self.save_book_data(path, data) - else: # Archived mode - self.update_book2db(data) - self.on_file_table_itemClicked(item(row, 0), reset=False) - - elif self.high_table.isVisible(): - data = self.parent_book_data - row = self.sel_high_view[-1].row() - high_data = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole) - high_text = high_data["text"].replace("\n", "\\\n") + self.high_table.setItem(0, CHAPTER_H, item) - for bookmark in data["bookmarks"].keys(): - if high_text == data["bookmarks"][bookmark]["notes"]: - data["bookmarks"][bookmark]["text"] = text.replace("\n", "\\\n") - high_data["comment"] = text - break - self.high_table.item(row, HIGHLIGHT_H).setData(Qt.UserRole, high_data) - self.high_table.item(row, COMMENT_H).setText(text) + # path = data["path"] + path = data.get("meta_path", "") + item = QTableWidgetItem(path) + item.setToolTip(path) + self.high_table.setItem(0, PATH_H, item) - if not self.db_mode: # Loaded mode - book_path, ext = splitext(high_data["path"]) - path = join(book_path + ".sdr", "metadata{}.lua".format(ext)) - self.save_book_data(path, data) - else: # Archived mode - self.update_book2db(data) - path = self.high_table.item(row, PATH_H).text() - for row in range(self.file_table.rowCount()): - if path == self.file_table.item(row, TYPE).data(Qt.UserRole)[0]: - self.file_table.item(row, TITLE).setData(Qt.UserRole, data) - break + self.high_table.setSortingEnabled(True) - self.reload_highlights = True + # noinspection PyUnusedLocal + def high_view_selection_update(self, selected, deselected): + """ When a row in high_table gets selected - def on_copy_highlights(self): - """ Copy the selected highlights to clipboard + :type selected: QModelIndex + :parameter selected: The selected row + :type deselected: QModelIndex + :parameter deselected: The deselected row """ - clipboard_text = "" - for highlight in sorted(self.sel_high_list): - row = highlight.row() - text = self.high_list.item(row).text() - clipboard_text += text + "\n" - self.copy_text_2clip(clipboard_text) + try: + if not self.filter.isVisible(): + self.sel_high_view = self.high_view_selection.selectedRows() + else: + self.sel_high_view = [i for i in self.high_view_selection.selectedRows() + if not self.high_table.isRowHidden(i.row())] + except IndexError: # empty table + self.sel_high_view = [] + self.toolbar.activate_buttons() - def on_delete_highlights(self): - """ The delete highlights action was invoked + def on_highlight_column_clicked(self, column): + """ Sets the current sorting column + + :type column: int + :parameter column: The column where the sorting is applied """ - if not self.db_mode: - if self.edit_lua_file_warning: - text = _("This is an one-time warning!\n\nIn order to delete highlights " - "from a book, its \"metadata\" file must be edited. This " - "contains a small risk of corrupting that file and lose all the " - "settings and info of that book.\n\nDo you still want to do it?") - popup = self.popup(_("Warning!"), text, buttons=2, - button_text=(_("Yes"), _("No"))) - if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: - return - else: - self.edit_lua_file_warning = False - text = _("This will delete the selected highlights!\nAre you sure?") + if column == self.col_sort_h: + self.col_sort_asc_h = not self.col_sort_asc_h else: - text = _("This will remove the selected highlights from the Archive!\n" - "Are you sure?") - popup = self.popup(_("Warning!"), text, buttons=2, - button_text=(_("Yes"), _("No"))) - if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: - return - self.delete_highlights() + self.col_sort_asc_h = True + self.col_sort_h = column - def delete_highlights(self): - """ Delete the selected highlights + # noinspection PyUnusedLocal + def on_highlight_column_resized(self, column, oldSize, newSize): + """ Gets the column size + + :type column: int + :parameter column: The resized column + :type oldSize: int + :parameter oldSize: The old size + :type newSize: int + :parameter newSize: The new size """ - row = self.sel_idx.row() - data = self.file_table.item(row, TITLE).data(Qt.UserRole) + if column == HIGHLIGHT_H: + self.highlight_width = newSize + elif column == COMMENT_H: + self.comment_width = newSize - for highlight in self.sel_high_list: - high_row = highlight.row() - high_data = self.high_list.item(high_row).data(Qt.UserRole) - pprint(high_data) - page = high_data["page"] - page_id = high_data["page_id"] - del data["highlight"][page][page_id] # delete the highlight - - # delete the associated bookmark - text = high_data["text"] - for bookmark in data["bookmarks"].keys(): - if text == data["bookmarks"][bookmark]["notes"]: - del data["bookmarks"][bookmark] - - for i in data["highlight"].keys(): - if not data["highlight"][i]: # delete page dicts with no highlights - del data["highlight"][i] - else: # renumbering the highlight keys - contents = [data["highlight"][i][j] for j in sorted(data["highlight"][i])] - if contents: - for l in data["highlight"][i].keys(): # delete all the items and - del data["highlight"][i][l] - for k in range(len(contents)): # rewrite them with the new keys - data["highlight"][i][k + 1] = contents[k] + def find_in_books(self, highlight, hi_row): + """ Finds the current highlight in the "Books View" - contents = [data["bookmarks"][bookmark] for bookmark in sorted(data["bookmarks"])] - if contents: # renumbering the bookmarks keys - for bookmark in data["bookmarks"].keys(): # delete all the items and - del data["bookmarks"][bookmark] - for content in range(len(contents)): # rewrite them with the new keys - data["bookmarks"][content + 1] = contents[content] + :type highlight: str|unicode + :parameter highlight: The highlight we are searching for + """ + data, meta_path = self.get_parent_book_data(hi_row) - if not data["highlight"]: # change icon if no highlights - item = self.file_table.item(row, 0) - item.setIcon(self.ico_empty) + for row in range(self.file_table.rowCount()): + item = self.file_table.item(row, TITLE) + row_meta_path = self.file_table.item(row, PATH).data(0) + try: # find the book row + if meta_path == row_meta_path: + self.toolbar.books_view_btn.click() + self.toolbar.setup_buttons() + self.toolbar.activate_buttons() + self.file_table.selectRow(row) # select the book + self.on_file_table_itemClicked(item) + for high_row in range(self.high_list.count()): # find the highlight + if (self.high_list.item(high_row) + .data(Qt.UserRole)["text"] == highlight): + self.high_list.setCurrentRow(high_row) # select the highlight + return + except KeyError: # old metadata with no "stats" + continue - if not self.db_mode: - path = self.file_table.item(row, PATH).text() - self.save_book_data(path, data) - else: - self.update_book2db(data) - item = self.file_table.item - item(row, TITLE).setData(Qt.UserRole, data) - self.on_file_table_itemClicked(item(row, 0), reset=False) - self.reload_highlights = True + # ___ ___________________ SYNC GROUPS TABLE STUFF _______________ - def save_book_data(self, path, data): - """ Saves the data of a book to its lua file + @Slot(QTableWidgetItem) + def on_sync_table_itemClicked(self, item): + """ When an item of the sync_table is clicked - :type path: str|unicode - :param path: The path to the book's data file - :type data: dict - :param data: The book's data + :type item: QTableWidgetItem + :param item: The item (cell) that is clicked """ - times = os.stat(path) # read the file's created/modified times - encode_data(path, data) - os.utime(path, (times.st_ctime, times.st_mtime)) # reapply original times - if self.file_table.isVisible(): - self.on_file_table_itemClicked(self.file_table.item(self.sel_idx.row(), 0), - reset=False) + # row = item.row() + # # path = self.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole)["path"] + # print(row) # noinspection PyUnusedLocal - def high_list_selection_update(self, selected, deselected): - """ When a highlight in gets selected + def sync_view_selection_update(self, selected, deselected): + """ When a row in sync_table gets selected :type selected: QModelIndex - :parameter selected: The selected highlight + :parameter selected: The selected row :type deselected: QModelIndex - :parameter deselected: The deselected highlight - """ - self.sel_high_list = self.high_list_selection.selectedRows() - - def set_highlight_sort(self): - """ Sets the sorting method of displayed highlights + :parameter deselected: The deselected row """ - self.high_by_page = self.sender().data() try: - row = self.sel_idx.row() - self.on_file_table_itemClicked(self.file_table.item(row, 0), False) - except AttributeError: # no book selected - pass + self.sel_sync_view = self.sync_view_selection.selectedRows() + except IndexError: # empty table + self.sel_sync_view = [] + self.toolbar.activate_buttons() - def sort_high4view(self, data): - """ Sets the sorting method of displayed highlights + def create_sync_row(self, data, quiet=False): + """ Creates a sync_table row from the given data - :type data: tuple - param: data: The highlight's data + :type data: dict|list + :param data: The sync_group data + :type quiet: bool + :param quiet: Switch to the Sync view """ - return int(data["page"]) if self.high_by_page else data["date"] + if self.current_view != SYNC_VIEW and not quiet: + self.toolbar.sync_view_btn.setChecked(True) + self.toolbar.change_view() - def sort_high4write(self, data): - """ Sets the sorting method of written highlights + self.sync_table.setSortingEnabled(False) + if isinstance(data, dict): + count = self.sync_table.rowCount() + self.sync_table.insertRow(count) + wdg = self.create_sync_widget(data) + wdg.idx = count + self.sync_groups.append(data) + self.sync_table.setCellWidget(count, 0, wdg) + self.sync_table.setRowHeight(count, wdg.sizeHint().height()) + wdg.check_data() + else: + for idx, data in enumerate(data): + self.sync_table.insertRow(idx) + wdg = self.create_sync_widget(data) + self.sync_table.setCellWidget(idx, 0, wdg) + self.sync_table.setRowHeight(idx, wdg.sizeHint().height()) + wdg.on_power_btn_clicked(data.get("enabled", True)) + wdg.idx = idx + self.sync_groups.append(data) + wdg.check_data() + self.sync_table.setSortingEnabled(True) + # noinspection PyTypeChecker + QTimer.singleShot(0, self.save_sync_groups) + + def create_sync_widget(self, data): + wdg = SyncGroup(self) + wdg.data = data + wdg.power_btn.setChecked(data.get("enabled", True)) + wdg.title_lbl.setText(data.get("title", "")) + wdg.sync_pos_chk.setChecked(data.get("sync_pos", True)) + wdg.merge_chk.setChecked(data.get("merge", True)) + wdg.sync_db_chk.setChecked(data.get("sync_db", False)) + + items = deepcopy(data.get("items", [{"path": "", "data": {}}])) + for item in items: + wdg.add_item(item) + return wdg + + def synchronize_group(self, group, multi=False): + """ Start the process of syncing/merging the group + + :type group: SyncGroup + :param group: The group to be processed + """ + group.on_refresh_btn_clicked() + if not group.sync_items[0].ok: + self.popup(_(f'Error in group "{group.data["title"]}"!'), + _("There is a problem with the first metadata file path!\nCheck " + "the Tooltip that appears while hovering the mouse over it."), + icon=QMessageBox.Critical) + return + sync2db = group.sync_db_chk.isChecked() + group_changed = sync2db + to_process = [] + for idx, item in enumerate(group.sync_items): + if not item.ok: + continue + info = group.data["items"][idx] + mod_time = os.stat(info["path"]).st_mtime + to_process.append((info, mod_time)) + to_process = sorted(to_process, key=lambda x: x[1], reverse=True) + to_process = [i[0] for i in to_process] + + if sync2db: + book_data = {"data": group.data["items"][0]["data"], + "path": group.data["items"][0]["path"]} + db_idx = self.check4archive_merge(book_data) + if db_idx: + db_data = self.books[db_idx]["data"] + to_process.append({"data": db_data, "path": ""}) + + if len(to_process) > 1: + if group.sync_pos_chk.isChecked(): + self.sync_pos(to_process) + group_changed = True + if group.merge_chk.isChecked(): + items = [] + for book_info in to_process: # book_info: {"path": str, "data": dict} + data = book_info["data"] + total = data.get("doc_pages", data.get("stats", {}).get("pages", 0)) + if group.new_format: + items.append((data["annotations"], total)) + else: + items.append((data["highlight"], data["bookmarks"], total, + book_info["path"])) + if group.new_format: + self.merge_new_highs(items) + else: + self.merge_old_highs(items) + group_changed = True + + if group_changed: + for item in deepcopy(to_process): + if item["path"]: # db version has no path + item["data"]["annotations_externally_modified"] = True + self.save_book_data(item["path"], item["data"]) + + if sync2db: # add the newest version of the book to db + new_data = deepcopy(to_process[0]) + path = new_data["path"] + data = new_data["data"] + date = str(datetime.fromtimestamp(getmtime(path))).split(".")[0] + md5 = data["partial_md5_checksum"] + self.delete_books_from_db([md5]) # remove the existing version if any + try: # clean up data that can be cluttered + new_data["stats"]["performance_in_pages"] = {} + new_data["page_positions"] = {} + new_data.pop("annotations_externally_modified", None) + except KeyError: + pass + self.add_books2db([{"md5": md5, "path": path, "date": date, + "data": json.dumps(data)}]) + self.update_new_values(group) + text = _("Synchronization process completed") + else: + text = _("Nothing to sync") - :type data: tuple - param: data: The highlight's data + if not multi: # one group Sync + self.popup(_("Information"), text, QMessageBox.Information) + else: # multiple Sync operations + if group_changed: + return True # needed for counting the number of groups changed + + def update_new_values(self, group): + """ Updates the book table after sync_groups execution """ - if self.high_by_page and self.status.act_page.isChecked(): - page = data[3] - if page.startswith("Page"): - page = page[5:] - return int(page) - else: - return data[0] + self.reload_highlights = True + row_infos = {} + for idx, item in enumerate(group.data["items"]): + if not group.sync_items[idx].ok: + continue + path = item["path"] + for row in range(self.file_table.rowCount()): + if self.file_table.item(row, PATH).text() == path: + row_infos[row] = item + break + for row in row_infos: + data = row_infos[row]["data"] + self.file_table.item(row, TITLE).setData(Qt.UserRole, data) + hi_count = self.get_item_stats(data)["high_count"] + if hi_count and hi_count != "0": + self.file_table.item(row, TITLE).setIcon(self.ico_label_green) + self.file_table.item(row, HIGH_COUNT).setText(hi_count) + self.file_table.item(row, HIGH_COUNT).setToolTip(hi_count) + if group.sync_pos_chk.isChecked(): + percent = str(int(data.get("percent_finished", 0) * 100)) + "%" + self.file_table.item(row, PERCENT).setText(percent) + self.file_table.item(row, PERCENT).setToolTip(percent) + + def update_sync_groups(self): + """ Update the sync groups in memory and on disk + """ + del self.sync_groups[:] + for i in range(self.sync_table.rowCount()): + wdg = self.sync_table.cellWidget(i, 0) + wdg.idx = i # update the index of widget + wdg.check_data() + self.sync_groups.append(wdg.data) + + self.save_sync_groups() + + def load_sync_groups(self): + """ Load the sync groups from a file + """ + try: + with open(SYNC_FILE, "r", encoding="utf-8") as f: + sync_groups = json.load(f) + except (IOError, ValueError): # no json file exists or corrupted file + return + self.create_sync_row(sync_groups, quiet=True) + + def save_sync_groups(self): + """ Save the sync groups to a file + """ + sync_groups = deepcopy(self.sync_groups) + sync_groups = [i for i in sync_groups if i["items"][0].get("path")] + with open(SYNC_FILE, "w+", encoding="utf-8") as f: + items = [i for grp in sync_groups for i in grp["items"] if i.get("path")] + for item in items: + item["data"] = {} + item["path"] = normpath(item["path"]) + json.dump(sync_groups, f, indent=4) # ___ ___________________ MERGING - SYNCING STUFF _______________ @@ -1666,18 +2167,20 @@ def same_book(self, data1, data2, book1="", book2=""): def wrong_book(self): """ Shows an info dialog if the book MD5 of two metadata are different """ - text = _("It seems that the selected metadata file belongs to a different book..") + text = _("It seems that the selected metadata file belongs to a different file..") self.popup(_("Book mismatch!"), text, icon=QMessageBox.Critical) @staticmethod - def same_cre_version(data): + def same_cre_version(data1, data2): """ Check if the supplied metadata have the same CRE version - :type data: list[dict] - :param data: The data to get checked + :type data1: dict + :param data1: The data of the first book + :type data2: dict + :param data2: The data of the second book """ try: - if data[0]["cre_dom_version"] == data[1]["cre_dom_version"]: + if data1["cre_dom_version"] == data2["cre_dom_version"]: return True except KeyError: # no "cre_dom_version" key (older metadata) pass @@ -1698,21 +2201,33 @@ def wrong_cre_version(self): "and does not change that often.") self.popup(_("Version mismatch!"), text, icon=QMessageBox.Critical) - def check4archive_merge(self): + def check4archive_merge(self, book_data): """ Check if the selected books' highlights can be merged with its archived version + + :type book_data: dict + :param book_data: The data of the book """ - idx = self.sel_idx - data1 = self.file_table.item(idx.row(), idx.column()).data(Qt.UserRole) - book_path = self.file_table.item(idx.row(), TYPE).data(Qt.UserRole)[0] + data1 = book_data["data"] + book_path = book_data["path"] for index, book in enumerate(self.books): data2 = book["data"] if self.same_book(data1, data2, book_path): - if self.same_cre_version([data1, data2]): + if self.same_cre_version(data1, data2): return index return False + def wrong_meta_format(self): + """ Shows an info dialog if the format of the two metadata are different + """ + text = _("Can not merge these highlights, because their metadata format are " + "different!\nThere was a re-write of the highlight/bookmark " + "structure of KOReader that make them incompatible.\n\nRe-open the " + "books with a newer version of KOReader to update them and then merge " + "them using KOHighlights.") + self.popup(_("Metadata format mismatch!"), text, icon=QMessageBox.Critical) + def merge_menu(self): """ Creates the `Merge/Sync` button menu """ @@ -1751,18 +2266,8 @@ def on_merge_highlights(self, to_archived=False, filename=""): :type filename: str|unicode :param filename: The path to the metadata file to merge the book with """ - if self.high_merge_warning: - text = _("Merging highlights is experimental so, always do backups ;o)\n" - "Because of the different page formats and sizes, some page " - "numbers in {} might be inaccurate. " - "Do you want to continue?").format(APP_NAME) - popup = self.popup(_("Warning!"), text, buttons=2, - button_text=(_("Yes"), _("No")), - check_text=DO_NOT_SHOW) - self.high_merge_warning = not popup.checked - if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: - return - + if self.merge_warning_stop(): + return popup = self.popup(_("Warning!"), _("The highlights of the selected entries will be merged.\n" "This can not be undone! Continue?"), buttons=2, @@ -1771,6 +2276,21 @@ def on_merge_highlights(self, to_archived=False, filename=""): if popup.buttonRole(popup.clickedButton()) == QMessageBox.AcceptRole: self.merge_highlights(popup.checked, True, to_archived, filename) + def merge_warning_stop(self): + """ Stop if the merge warning is answered "No" + """ + if self.high_merge_warning: + text = _(f"Merging highlights is experimental so, always do backups ;o)\n" + f"Because of the different page formats and sizes, some page " + f"numbers in {APP_NAME} might be inaccurate. " + f"Do you want to continue?") + popup = self.popup(_("Warning!"), text, buttons=2, + button_text=(_("Yes"), _("No")), + check_text=DO_NOT_SHOW) + self.high_merge_warning = not popup.checked + if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: + return True + def merge_highlights(self, sync, merge, to_archived=False, filename=""): """ Merge highlights from the same book in two different devices @@ -1783,160 +2303,314 @@ def merge_highlights(self, sync, merge, to_archived=False, filename=""): :type filename: str|unicode :param filename: The path to the metadata file to merge the book with """ + item = self.file_table.item if to_archived: # Merge/Sync a book with archive idx1, idx2 = self.sel_idx, None - data1 = self.file_table.item(idx1.row(), TITLE).data(Qt.UserRole) - data2 = self.books[self.check4archive_merge()]["data"] - path1, path2 = self.file_table.item(idx1.row(), PATH).text(), None + data1 = item(idx1.row(), TITLE).data(Qt.UserRole) + path1, path2 = item(idx1.row(), PATH).text(), None + book_data = {"data": data1, "path": path1} + db_idx = self.check4archive_merge(book_data) + if not db_idx: + return + data2 = self.books[db_idx]["data"] elif filename: # Merge/Sync a book with a metadata file idx1, idx2 = self.sel_idx, None - data1 = self.file_table.item(idx1.row(), TITLE).data(Qt.UserRole) - book1 = self.file_table.item(idx1.row(), TYPE).data(Qt.UserRole)[0] + data1 = item(idx1.row(), TITLE).data(Qt.UserRole) + book1 = item(idx1.row(), TYPE).data(Qt.UserRole)[0] data2 = decode_data(filename) name2 = splitext(dirname(filename))[0] book2 = name2 + splitext(book1)[1] if not self.same_book(data1, data2, book1, book2): self.wrong_book() return - if not self.same_cre_version([data1, data2]): + if not self.same_cre_version(data1, data2): self.wrong_cre_version() return - path1, path2 = self.file_table.item(idx1.row(), PATH).text(), None + path1, path2 = item(idx1.row(), PATH).text(), None else: # Merge/Sync two different book files idx1, idx2 = self.sel_indexes - data1, data2 = [self.file_table.item(idx.row(), TITLE).data(Qt.UserRole) + data1, data2 = [item(idx.row(), TITLE).data(Qt.UserRole) for idx in [idx1, idx2]] - path1, path2 = [self.file_table.item(idx.row(), PATH).text() + path1, path2 = [item(idx.row(), PATH).text() for idx in [idx1, idx2]] if merge: # merge highlights - args = (data1["highlight"], data2["highlight"], - data1["bookmarks"], data2["bookmarks"]) - high1, high2, bkm1, bkm2 = self.get_unique_highlights(*args) - self.update_data(data1, high2, bkm2) - self.update_data(data2, high1, bkm1) - if data1["highlight"] or data2["highlight"]: # since there are highlights - for index in [idx1, idx2]: # set the green icon - if index: - item = self.file_table.item(idx1.row(), TITLE) - item.setIcon(self.ico_label_green) + data1_new = data1.get("annotations") is not None + data2_new = data2.get("annotations") is not None + new_type = data1_new and data2_new + datas = [data1, data2] + if path2 and (os.stat(path1).st_mtime < os.stat(path2).st_mtime): + datas = [data2, data1] # put the newer metadata first + items = [] + if new_type: # new format metadata + for data in datas: + total = data.get("doc_pages", data.get("stats", {}).get("pages", 0)) + items.append([data["annotations"], total]) + self.merge_new_highs(items) + if data1["annotations"]: # update row data for books + num = str(len([i for i in data1["annotations"].values() + if i.get("pos0")])) + for index in [idx1, idx2]: + if index: + item(index.row(), TITLE).setIcon(self.ico_label_green) + item(index.row(), HIGH_COUNT).setText(num) + item(index.row(), HIGH_COUNT).setToolTip(num) + elif (data1_new and not data2_new) or (data2_new and not data1_new): + self.wrong_meta_format() + return # different formats metadata - not supported + else: # old format metadata + for data in datas: + total = data.get("doc_pages", data.get("stats", {}).get("pages", 0)) + items.append([data["highlight"], data["bookmarks"], total]) + self.merge_old_highs(items) + if data1["highlight"]: # update row data for books + num = str(len(data1["highlight"])) + for index in [idx1, idx2]: + if index: + item(index.row(), TITLE).setIcon(self.ico_label_green) + item(index.row(), HIGH_COUNT).setText(num) + item(index.row(), HIGH_COUNT).setToolTip(num) if sync: # sync position and percent - if data1["percent_finished"] > data2["percent_finished"]: - data2["percent_finished"] = data1["percent_finished"] - data2["last_xpointer"] = data1["last_xpointer"] - else: - data1["percent_finished"] = data2["percent_finished"] - data1["last_xpointer"] = data2["last_xpointer"] - - percent = str(int(data1["percent_finished"] * 100)) + "%" - self.file_table.item(idx1.row(), PERCENT).setText(percent) - if not to_archived and not filename: - self.file_table.item(idx2.row(), PERCENT).setToolTip(percent) - - self.file_table.item(idx1.row(), TITLE).setData(Qt.UserRole, data1) + to_process = [{"data": data1, "path": path1}, {"data": data2, "path": path2}] + percent = self.sync_pos(to_process) + for index in [idx1, idx2]: + if index: + item(index.row(), PERCENT).setText(percent) + item(index.row(), PERCENT).setToolTip(percent) + + data1["annotations_externally_modified"] = True self.save_book_data(path1, data1) if to_archived: # update the db item self.update_book2db(data2) elif filename: # do nothing with the loaded file pass else: # update the second item - self.file_table.item(idx2.row(), TITLE).setData(Qt.UserRole, data2) + data2["annotations_externally_modified"] = True self.save_book_data(path2, data2) self.reload_highlights = True @staticmethod - def get_unique_highlights(high1, high2, bkm1, bkm2): - """ Get the highlights, bookmarks from the first book - that do not exist in the second book and vice versa - - :type high1: dict - :param high1: The first book's highlights - :type high2: dict - :param high2: The second book's highlights - :type bkm1: dict - :param bkm1: The first book's bookmarks - :type bkm2: dict - :param bkm2: The second book's bookmarks - """ - unique_high1 = defaultdict(dict) - for page1 in high1: - for page_id1 in high1[page1]: - text1 = high1[page1][page_id1]["text"] - for page2 in high2: - for page_id2 in high2[page2]: - if text1 == high2[page2][page_id2]["text"]: - break # highlight found in book2 - else: # highlight was not found yet in book2 - continue # no break in the inner loop, keep looping - break # highlight already exists in book2 (there was a break) - else: # text not in book2 highlights, add to unique - unique_high1[page1][page_id1] = high1[page1][page_id1] - unique_bkm1 = {} - for page1 in unique_high1: - for page_id1 in unique_high1[page1]: - text1 = unique_high1[page1][page_id1]["text"] - for idx in bkm1: - if text1 == bkm1[idx]["notes"]: # add highlight's bookmark to unique - unique_bkm1[idx] = bkm1[idx] - break - - unique_high2 = defaultdict(dict) - for page2 in high2: - for page_id2 in high2[page2]: - text2 = high2[page2][page_id2]["text"] - for page1 in high1: - for page_id1 in high1[page1]: - if text2 == high1[page1][page_id1]["text"]: - break # highlight found in book1 - else: # highlight was not found yet in book1 - continue # no break in the inner loop, keep looping - break # highlight already exists in book1 (there was a break) - else: # text not in book1 highlights, add to unique - unique_high2[page2][page_id2] = high2[page2][page_id2] - unique_bkm2 = {} - for page2 in unique_high2: - for page_id2 in unique_high2[page2]: - text2 = unique_high2[page2][page_id2]["text"] - for idx in bkm2: - if text2 == bkm2[idx]["notes"]: # add highlight's bookmark to unique - unique_bkm2[idx] = bkm2[idx] - break + def merge_new_highs(items): + """ Merge the highlights of multiple books [new format] + + :type items: [[dict, int], ...] + :param items: [[annotations, total_pg], ...] + :param items: The list of books to be processed + """ + uni_check_hi = set() + uni_highs = [] + uni_check_bkm = set() + uni_bkms = [] + for book_id, info in enumerate(items): # find all unique highlights + source = info[0] # that are not in all books + for target_id, target in enumerate(items): + if target_id == book_id: + continue # don't check self + target = target[0] + for src_hi in source.values(): + if src_hi.get("pos0"): # a highlight + hi_text = src_hi.get("text", "") + for trg_hi in target.values(): + if trg_hi.get("pos0"): # a highlight not a bookmark + if hi_text == trg_hi.get("text", ""): + if src_hi["datetime"] == trg_hi["datetime"]: + break # it's the exact same annotation + src_comm = src_hi.get("note") # try to get the + trg_comm = trg_hi.get("note") # newest change + if src_hi["datetime"] > trg_hi["datetime"]: + if src_comm: # this is the newer comment + trg_hi["note"] = src_comm + elif trg_comm: # the comment was erased lately + trg_hi.pop("note", None) + trg_hi["datetime"] = src_hi["datetime"] + else: + if trg_comm: # this is the newer comment + src_hi["note"] = trg_comm + elif src_comm: # the comment was erased lately + src_hi.pop("note", None) + src_hi["datetime"] = trg_hi["datetime"] + break # highlight found in target book + else: # highlight was not found in target book + if src_hi["pos0"] + src_hi["pos1"] not in uni_check_hi: + uni_check_hi.add(src_hi["pos0"] + src_hi["pos1"]) + uni_highs.append((src_hi, info[1])) + else: # a bookmark + if src_hi["page"] not in uni_check_bkm: + uni_check_bkm.add(src_hi["page"]) + uni_bkms.append((src_hi, info[1])) + + for info in items: # update the annotations with the unique found ones + annots = deepcopy([i for i in info[0].values()]) + total = info[1] + hi_pos_check = {i["pos0"] + i["pos1"] for i in annots if i.get("pos0")} + bkm_pos_check = {i["page"] for i in annots if i.get("page")} + for hi_info in uni_highs: + hi, hi_total = hi_info + if not hi["pos0"] + hi["pos1"] in hi_pos_check: # new highlight + new_hi = deepcopy(hi) + if hi_total != total: # re-calculate the page numbers + percent = int(new_hi["pageno"]) / hi_total + new_hi["pageno"] = str(int(round(percent * total))) + annots.append(new_hi) + for bkm_info in uni_bkms: + bkm, bkm_total = bkm_info + if not bkm["page"] in bkm_pos_check: # new bookmark + new_bkm = deepcopy(bkm) + if bkm_total != total: # re-calculate the page numbers + percent = int(new_bkm["pageno"]) / bkm_total + new_bkm["pageno"] = str(int(round(percent * total))) + annots.append(new_bkm) + info[0].clear() # repopulate the annotations + annots_upd = {} + for i, hi in enumerate(sorted(annots, key=lambda x: x["pageno"])): + annots_upd[i + 1] = hi + info[0].update(annots_upd) - return unique_high1, unique_high2, unique_bkm1, unique_bkm2 + @staticmethod + def merge_old_highs(items): + """ Merge the highlights of multiple books + + :type items: [[dict, dict, int], ...] + :param items: [[highlights, bookmarks, total_pg], ...] + :param items: The list of books to be processed + """ + uni_check = set() + all_uni_highs = {} + all_uni_bkms = {} + # Collect the highlights that are missing even from one book + for book_id, s_info in enumerate(items): + source = s_info[0] + uni_highs = defaultdict(dict) + uni_bkms = defaultdict(dict) + for target_id, t_info in enumerate(items): + if target_id == book_id: + continue # don't check self + target = t_info[0] + for src_pg in source: + for src_pg_id in source[src_pg]: + high = source[src_pg][src_pg_id] + src_text = high["text"] + for target_pg in target: + for target_pg_id in target[target_pg]: + targ = target[target_pg][target_pg_id] + if src_text == targ["text"]: # same annotation + if high["datetime"] == targ["datetime"]: + break + src_comm = "" # if one comment is newer then the + trg_comm = "" # other, we keep the newer for both + src_bkm = {} + trg_bkm = {} + for bk_idx in s_info[1]: + src_bkm = s_info[1][bk_idx] + if src_bkm["notes"] == src_text: + src_comm = src_bkm.get("text", "") + break + for bk_idx in t_info[1]: + trg_bkm = t_info[1][bk_idx] + if trg_bkm["notes"] == targ["text"]: + trg_comm = trg_bkm.get("text", "") + break + if src_bkm["datetime"] > trg_bkm["datetime"]: + if src_comm: # this is the newer comment + trg_bkm["text"] = src_comm + elif trg_comm: # the comment was erased lately + trg_bkm.pop("text", None) + trg_bkm["datetime"] = src_bkm["datetime"] + targ["datetime"] = high["datetime"] + else: + if trg_comm: # this is the newer comment + src_bkm["text"] = trg_comm + elif src_comm: # the comment was erased lately + src_bkm.pop("text", None) + src_bkm["datetime"] = trg_bkm["datetime"] + high["datetime"] = targ["datetime"] + break # highlight found in target book + else: # highlight was not found yet in target book + continue # no break in the inner loop, keep checking + break # highlight exists in target (there was a break) + else: # text not in the target book highlights, add to unique + # but not if already added + if high["pos0"] + high["pos1"] not in uni_check: + uni_check.add(high["pos0"] + high["pos1"]) + uni_highs[src_pg][src_pg_id] = high + for pg in uni_highs: + for pg_id in uni_highs[pg]: + text = uni_highs[pg][pg_id]["text"] + for bkm_idx in s_info[1]: # get the associated bookmarks + if text == s_info[1][bkm_idx]["notes"]: + uni_bkms[pg][pg_id] = s_info[1][bkm_idx] + break + if uni_highs: + all_uni_highs[book_id] = dict(uni_highs) + all_uni_bkms[book_id] = dict(uni_bkms) + + # Merge the highlights that are not on all books + for book_id in all_uni_highs: + uni_highs = all_uni_highs[book_id] + uni_bkms = all_uni_bkms[book_id] + source_total = items[book_id][2] + for item_id, target_item in enumerate(items): + target_total = target_item[2] + recalculate = source_total != target_total + all_highs = [target_item[0][pg][pg_id] + for pg in target_item[0] for pg_id in target_item[0][pg]] + all_bkms = [i for i in target_item[1].values()] + renumber = False # no change made to the highlights/bookmarks + for pg in uni_highs: + new_pg = pg + for pg_id in uni_highs[pg]: + if uni_highs[pg][pg_id] not in all_highs: # add this highlight + renumber = True # mark for renumbering the bookmarks + if recalculate: # diff total pages, recalculate page number + percent = int(pg) / target_total + new_pg = int(round(percent * source_total)) + + if new_pg in target_item[0]: # sort same page highlights + contents = target_item[0][new_pg] + contents = [contents[i] for i in contents] + extras = uni_highs[pg] + extras = [extras[i] for i in extras + if extras[i] not in contents] + highs = sorted(contents + extras, key=lambda x: x["pos0"]) + target_item[0][new_pg] = {i + 1: h + for i, h in enumerate(highs)} + else: # single highlight in page, just add it + target_item[0][new_pg] = uni_highs[pg] + all_bkms.append(uni_bkms[pg][pg_id]) + if renumber: # bookmarks added, so we must merge all and renumber them + bkm_list = target_item[1].copy() + bkm_list = [bkm_list[i] for i in bkm_list] + bkm_check = {i["pos0"] + i["pos1"] for i in bkm_list} + for bkm in all_bkms: + if bkm["pos0"] + bkm["pos1"] not in bkm_check: + bkm_list.append(bkm) + bkm_list = sorted(bkm_list, key=lambda x: x["page"]) + target_item[1].clear() + offset = 1 + for bkm in bkm_list: + target_item[1][offset] = bkm + offset += 1 @staticmethod - def update_data(data, extra_highlights, extra_bookmarks): - """ Adds the new highlights to the book's data + def sync_pos(to_process): + """ Sync the reading position of multiple books - :type data: dict - :param data: The book's data - :type extra_highlights: dict - :param extra_highlights: The other book's highlights - :type extra_bookmarks: dict - :param extra_bookmarks: The other book's bookmarks - """ - highlights = data["highlight"] - for page in extra_highlights: - if page in highlights: # change page number if already exists - new_page = page - while new_page in highlights: - new_page += 1 - highlights[new_page] = extra_highlights[page] - else: - highlights[page] = extra_highlights[page] - - bookmarks = data["bookmarks"] - original = bookmarks.copy() - bookmarks.clear() - counter = 1 - for key in original.keys(): - bookmarks[counter] = original[key] - counter += 1 - for key in extra_bookmarks.keys(): - bookmarks[counter] = extra_bookmarks[key] - counter += 1 + :type to_process: list + :param to_process: The list of books to be processed + """ + percents = [i["data"]["percent_finished"] for i in to_process + if i and i.get("data", {}).get("percent_finished", 0) > 0] + max_idx = percents.index(max(percents)) + max_percent = percents[max_idx] + max_pointer = to_process[max_idx]["data"]["last_xpointer"] + + for item in to_process: + item["data"]["percent_finished"] = max_percent + item["data"]["last_xpointer"] = max_pointer + + return str(int(max_percent * 100)) + "%" def use_meta_files(self): """ Selects a metadata files to sync/merge @@ -2067,26 +2741,37 @@ def get_sdr_folder(self, row): # ___ ___________________ SAVING STUFF __________________________ - def get_export_menu(self): + def create_export_menu(self): """ Creates the `Export Files` button menu """ - menu = QMenu(self) - for idx, item in enumerate([(_("To individual text files"), MANY_TEXT), - (_("Combined to one text file"), ONE_TEXT), - (_("To individual html files"), MANY_HTML), - (_("Combined to one html file"), ONE_HTML), - (_("To individual csv files"), MANY_CSV), - (_("Combined to one csv file"), ONE_CSV), - (_("To individual markdown files"), MANY_MD), - (_("Combined to one markdown file"), ONE_MD)]): - action = QAction(item[0], menu) - action.triggered.connect(self.export_actions) - action.setData(item[1]) - action.setIcon(self.ico_file_save) - if idx and (idx % 2 == 0): - menu.addSeparator() - menu.addAction(action) - return menu + self.export_menu.clear() + single = len(self.sel_indexes) == 1 + if single: + for idx, item in enumerate([(_("To text file"), MANY_TEXT), + (_("To html file"), MANY_HTML), + (_("To csv file"), MANY_CSV), + (_("To md file"), MANY_MD)]): + action = QAction(item[0], self.export_menu) + action.triggered.connect(self.export_actions) + action.setData(item[1]) + action.setIcon(self.ico_file_save) + self.export_menu.addAction(action) + else: + for idx, item in enumerate([(_("To individual text files"), MANY_TEXT), + (_("Combined to one text file"), ONE_TEXT), + (_("To individual html files"), MANY_HTML), + (_("Combined to one html file"), ONE_HTML), + (_("To individual csv files"), MANY_CSV), + (_("Combined to one csv file"), ONE_CSV), + (_("To individual md files"), MANY_MD), + (_("Combined to one md file"), ONE_MD)]): + action = QAction(item[0], self.export_menu) + action.triggered.connect(self.export_actions) + action.setData(item[1]) + action.setIcon(self.ico_file_save) + if idx and (idx % 2 == 0): + self.export_menu.addSeparator() + self.export_menu.addAction(action) # noinspection PyCallByClass def on_export(self): @@ -2095,13 +2780,12 @@ def on_export(self): if self.current_view == BOOKS_VIEW: if not self.sel_indexes: return + self.toolbar.export_btn.showMenu() elif self.current_view == HIGHLIGHTS_VIEW: # Save from high_table, if self.save_sel_highlights(): # combine to one file self.popup(_("Finished!"), _("The Highlights were exported successfully!"), icon=QMessageBox.Information) - return - self.toolbar.export_btn.showMenu() def export_actions(self): """ An `Export as...` menu item is clicked @@ -2146,9 +2830,8 @@ def export(self, idx): ext = "md" else: return - filename = QFileDialog.getSaveFileName(self, - _("Export to {} file").format(ext), - self.last_dir, "*.{}".format(ext))[0] + filename = QFileDialog.getSaveFileName(self, _(f"Export to {ext} file"), + self.last_dir, f"*.{ext}")[0] if not filename: return self.last_dir = dirname(filename) @@ -2156,9 +2839,9 @@ def export(self, idx): self.status.animation(False) all_files = len(self.sel_indexes) - self.popup(_("Finished!"), _("{} texts were exported from the {} processed.\n" - "{} files with no highlights.") - .format(saved, all_files, all_files - saved), + self.popup(_("Finished!"), + _(f"{saved} texts were exported from the {all_files} processed.\n" + f"{all_files - saved} files with no highlights."), icon=QMessageBox.Information) def save_multi_files(self, dir_path, format_, line_break, space): @@ -2185,8 +2868,7 @@ def save_multi_files(self, dir_path, format_, line_break, space): format_, line_break, space, sort_by) saved += 1 except IOError as err: # any problem when writing (like long filename, etc.) - self.popup(_("Warning!"), - _("Could not save the file to disk!\n{}").format(err)) + self.popup(_("Warning!"), _(f"Could not save the file to disk!\n{err}")) return saved def save_merged_file(self, filename, format_, line_break, space): @@ -2222,21 +2904,31 @@ def save_merged_file(self, filename, format_, line_break, space): text_file.write(text) return saved - def get_item_data(self, idx, format_): + def get_item_data(self, index, format_): """ Get the highlight data for an item - :type idx: QModelIndex - :param idx: The item's index + :type index: QModelIndex + :param index: The item's index :type format_: int :param format_: The output format idx """ - row = idx.row() + row = index.row() data = self.file_table.item(row, 0).data(Qt.UserRole) highlights = [] - for page in data["highlight"]: - for page_id in data["highlight"][page]: - highlights.append(self.get_formatted_high(data, page, page_id, format_)) + annotations = data.get("annotations") + if annotations: # new format metadata + for idx in annotations: + highlight = self.get_new_highlight_info(data, idx) + if highlight: + formatted_high = self.get_formatted_high(highlight, format_) + highlights.append(formatted_high) + else: # old format metadata + for page in data["highlight"]: + for page_id in data["highlight"][page]: + highlight = self.get_old_highlight_info(data, page, page_id) + if highlight: + highlights.append(self.get_formatted_high(highlight, format_)) title = self.file_table.item(row, TITLE).data(0) authors = self.file_table.item(row, AUTHOR).data(0) if authors in [OLD_TYPE, NO_AUTHOR]: @@ -2244,19 +2936,14 @@ def get_item_data(self, idx, format_): return authors, title, highlights # noinspection PyTypeChecker - def get_formatted_high(self, data, page, page_id, format_): + def get_formatted_high(self, highlight, format_): """ Create the highlight's texts - :type data: dict - :param data: The highlight's data - :type page: int - :param page The page where the highlight starts - :type page_id: int - :param page_id The idx of this page's highlight + :type highlight: dict + :param highlight: The highlight's data :type format_: int :param format_ The output format idx """ - highlight = self.get_highlight_info(data, page, page_id) linesep = "
" if format_ in [ONE_HTML, MANY_HTML] else os.linesep comment = highlight["comment"].replace("\n", linesep) chapter = (highlight["chapter"].replace("\n", linesep) @@ -2267,12 +2954,13 @@ def get_formatted_high(self, data, page, page_id, format_): date = date if self.date_format == DATE_FORMAT else self.get_date_text(date) line_break2 = (os.linesep if self.status.act_text.isChecked() and comment else "") if format_ in [ONE_CSV, MANY_CSV]: - page_text = str(page) if self.status.act_page.isChecked() else "" + page_text = highlight["page"] if self.status.act_page.isChecked() else "" date_text = date if self.status.act_date.isChecked() else "" high_comment = (comment if self.status.act_comment.isChecked() and comment else "") else: - page_text = "Page " + str(page) if self.status.act_page.isChecked() else "" + page_txt = "Page " + highlight["page"] + page_text = page_txt if self.status.act_page.isChecked() else "" date_text = "[" + date + "]" if self.status.act_date.isChecked() else "" high_comment = (line_break2 + "● " + comment if self.status.act_comment.isChecked() and comment else "") @@ -2293,7 +2981,7 @@ def save_sel_highlights(self): text_out = extra.startswith("text") html_out = extra.startswith("html") csv_out = extra.startswith("csv") - md_out = extra.startswith("mark") + md_out = extra.startswith("md") if text_out: ext = ".txt" text = "" @@ -2327,13 +3015,12 @@ def save_sel_highlights(self): comment = comment.replace("\n", " \n") if text_out: - txt = ("{} [{}]\nPage {} [{}]\n[{}]\n{}{}" - .format(data["title"], data["authors"], data["page"], - data["date"], data["chapter"], data["text"], comment)) + txt = (f"{data['title']} [{data['authors']}]\nPage {data['page']} " + f"[{data['date']}]\n[{data['chapter']}]\n{data['text']}{comment}") text += txt + "\n\n" elif html_out: - left = "{} [{}]".format(data["title"], data["authors"]) - right = "Page {} [{}]".format(data["page"], data["date"]) + left = f"{data['title']} [{data['authors']}]" + right = f"Page {data['page']} [{data['date']}]" text += HIGH_BLOCK % {"page": left, "date": right, "comment": comment, "highlight": data["text"], "chapter": data["chapter"]} @@ -2344,10 +3031,10 @@ def save_sel_highlights(self): txt = data["text"].replace("\n", " \n") chapter = data["chapter"] if chapter: - chapter = "***{0}***\n\n".format(chapter).replace("\n", " \n") - text += ("\n---\n### {} [{}] \n*Page {} [{}]* \n{}{}{}\n" - .format(data["title"], data["authors"], data["page"], - data["date"], chapter, txt, comment)) + chapter = f"***{chapter}***\n\n".replace("\n", " \n") + text += (f'\n---\n### {data["title"]} [{data["authors"]}] \n' + f'*Page {data["page"]} [{data["date"]}]* \n' + f'{chapter}{txt}{comment}\n') else: print("Unknown format export!") return @@ -2389,27 +3076,27 @@ def settings_load(self): self.skip_version = app_config.get("skip_version", None) self.date_vacuumed = app_config.get("date_vacuumed", self.date_vacuumed) self.date_format = app_config.get("date_format", DATE_FORMAT) + self.theme = app_config.get("theme", THEME_NONE_OLD) + self.status.theme_box.setCurrentIndex(self.theme) self.archive_warning = app_config.get("archive_warning", True) self.exit_msg = app_config.get("exit_msg", True) self.high_merge_warning = app_config.get("high_merge_warning", True) self.edit_lua_file_warning = app_config.get("edit_lua_file_warning", True) - checked = app_config.get("show_items", (True, True, True, True, True)) - if len(checked) != 5: # settings from older versions - checked = (True, True, True, True, True) - self.status.act_page.setChecked(checked[0]) - self.status.act_date.setChecked(checked[1]) - self.status.act_chapter.setChecked(checked[2]) - self.status.act_text.setChecked(checked[3]) - self.status.act_comment.setChecked(checked[4]) + self.show_items = app_config.get("show_items", [True, True, True, True, True]) + if len(self.show_items) != 5: # settings from older versions + self.show_items = [True, True, True, True, True] self.high_by_page = app_config.get("high_by_page", False) else: + self.status.theme_box.setCurrentIndex(self.theme) self.resize(800, 600) if self.highlight_width: self.header_high_view.resizeSection(HIGHLIGHT_H, self.highlight_width) if self.comment_width: self.header_high_view.resizeSection(COMMENT_H, self.comment_width) self.toolbar.set_btn_size(self.toolbar_size) + for idx, act in enumerate(self.status.show_actions): + act.setChecked(self.show_items[idx]) def settings_save(self): """ Saves the jason based configuration settings @@ -2427,22 +3114,16 @@ def settings_save(self): "current_view": self.current_view, "db_mode": self.db_mode, "high_by_page": self.high_by_page, "date_vacuumed": self.date_vacuumed, "show_info": self.fold_btn.isChecked(), "date_format": self.date_format, - "show_items": (self.status.act_page.isChecked(), - self.status.act_date.isChecked(), - self.status.act_chapter.isChecked(), - self.status.act_text.isChecked(), - self.status.act_comment.isChecked()), + "theme": self.theme, "show_items": self.show_items, "skip_version": self.skip_version, "opened_times": self.opened_times, "edit_lua_file_warning": self.edit_lua_file_warning, "high_merge_warning": self.high_merge_warning, } try: - if not PYTHON2: - # noinspection PyUnresolvedReferences - for k, v in config.items(): - if type(v) == bytes: - # noinspection PyArgumentList - config[k] = str(v, encoding="latin") + for k, v in config.items(): + if type(v) == bytes: + # noinspection PyArgumentList + config[k] = str(v, encoding="latin") config_json = json.dumps(config, sort_keys=True, indent=4) with gzip.GzipFile(join(SETTINGS_DIR, str("settings.json.gz")), "w+") as gz_file: @@ -2473,17 +3154,9 @@ def unpickle(key): value = app_config.get(key) if not value: return - if PYTHON2: - try: - # noinspection PyTypeChecker - value = pickle.loads(str(value)) - except UnicodeEncodeError: # settings from Python 3.x - return - else: - # noinspection PyUnresolvedReferences - value = value.encode("latin1") - # noinspection PyTypeChecker,PyArgumentList - value = pickle.loads(value, encoding="bytes") + value = value.encode("latin1") + # noinspection PyTypeChecker,PyArgumentList + value = pickle.loads(value, encoding="bytes") except pickle.UnpicklingError as err: print("While unPickling:", err) return @@ -2522,7 +3195,7 @@ def popup(self, title, text, icon=QMessageBox.Warning, buttons=1, popup.setWindowIcon(self.ico_app) if type(icon) == QMessageBox.Icon: popup.setIcon(icon) - elif type(icon) == unicode: + elif type(icon) == str: popup.setIconPixmap(QPixmap(icon)) elif type(icon) == QPixmap: popup.setIconPixmap(icon) @@ -2550,6 +3223,9 @@ def popup(self, title, text, icon=QMessageBox.Warning, buttons=1, popup.exec_() return popup + def error(self, error_txt): + self.popup(_("Error!"), error_txt, icon=QMessageBox.Critical) + def passed_files(self): """ Command line parameters that are passed to the program. """ @@ -2573,8 +3249,7 @@ def open_file(self, path): opener = "open" if sys.platform == "darwin" else "xdg-open" subprocess.call([opener, path]) except OSError: - self.popup(_("Error opening target!"), - _('"{}" does not exists!').format(path)) + self.popup(_("Error opening target!"), _(f'"{path}" does not exists!')) def copy_text_2clip(self, text): """ Copy a text to clipboard @@ -2609,9 +3284,8 @@ def recalculate_md5(self, file_path): data["stats"]["md5"] = md5 if old_md5: - text = _("The MD5 was originally\n{}\nA recalculation produces\n{}\n" - "The MD5 was replaced and saved!").format(old_md5, md5) - self.file_table.item(row, TITLE).setData(Qt.UserRole, data) + text = _(f"The MD5 was originally\n{old_md5}\nA recalculation produces\n" + f"{md5}\nThe MD5 was replaced and saved!") self.save_book_data(path, data) else: text = _("Metadata file has no MD5 information!") @@ -2659,10 +3333,10 @@ def auto_check4update(self): self.opened_times += 1 if self.opened_times == 20: - text = _("Since you are using {} for some time now, perhaps you find it " - "useful enough to consider a donation.\nWould you like to visit " - "the PayPal donation page?\n\nThis is a one-time message. " - "It will never appear again!").format(APP_NAME) + text = _(f"Since you are using {APP_NAME} for some time now, perhaps you find" + f" it useful enough to consider a donation.\nWould you like to visit" + f" the PayPal donation page?\n\nThis is a one-time message. It will " + f"never appear again!") popup = self.popup(_("A reminder..."), text, icon=":/stuff/paypal76.png", buttons=3) @@ -2684,9 +3358,8 @@ def auto_check4update(self): skip_version = version_parse(self.skip_version) if version_new > current_version and version_new != skip_version: popup = self.popup(_("Newer version exists!"), - _("There is a newer version (v.{}) online.\n" - "Open the site to download it now?") - .format(version_new), + _(f"There is a newer version (v.{version_new}) online.\n" + f"Open the site to download it now?"), icon=QMessageBox.Information, buttons=2, check_text=_("Don\"t alert me for this version again")) if popup.checked: @@ -2760,10 +3433,10 @@ def __init__(self, *args, **kwargs): del argv[1] sys.argv = argv self.parser = argparse.ArgumentParser(prog=APP_NAME, - description=_("{} v{} - A KOReader's " - "highlights converter") - .format(APP_NAME, __version__), - epilog=_("Thanks for using %s!") % APP_NAME) + description=_(f"{APP_NAME} v{__version__} -" + f" A KOReader's highlights " + f"converter"), + epilog=_(f"Thanks for using {APP_NAME}!")) self.base = Base() if compiled: # the app is compiled if not on_windows: # no cli in windows @@ -2859,7 +3532,7 @@ def parse_args(self): """ Parse the command line parameters that are passed to the program. """ self.parser.add_argument("-v", "--version", action="version", - version="%(prog)s v{}".format(__version__)) + version=f"%(prog)s v{__version__}") self.parser.add_argument("paths", nargs="*", help="The paths to input files or folder") @@ -2902,9 +3575,15 @@ def parse_args(self): help="The filename of the file (in merge mode) or " "the directory for saving the highlight files") + self.parser.add_argument("-p", "--portable", action="store_true", default=False, + help="Just run the program in portable mode " + "(Windows only)") + # args, paths = self.parser.parse_known_args() args = self.parser.parse_args() - if args.use_cli: + if args.portable: + print("Running in portable mode...") + elif args.use_cli: self.cli_save_highlights(args) sys.exit(0) # quit the app if cli execution @@ -2933,15 +3612,14 @@ def cli_save_highlights(self, args): if isdir(path): ext = ("an .html" if args.html else "a .csv" if args.csv else "an .md" if args.markdown else "a .txt") - self.parser.error("The output path (-o/--output) must be {} filename " - "not a directory!".format(ext)) + self.parser.error(f"The output path (-o/--output) must be {ext} filename " + f"not a directory!") return saved = self.cli_save_merged_file(args, files, line_break, space) all_files = len(files) - sys.stdout.write(_("\n{} files were exported from the {} processed.\n" - "{} files with no highlights.\n").format(saved, all_files, - all_files - saved)) + sys.stdout.write(_(f"\n{saved} files were exported from the {all_files} processed" + f".\n{all_files - saved} files with no highlights.\n")) def cli_save_multi_files(self, args, files, line_break, space): """ Save each selected book's highlights to a different file @@ -2964,7 +3642,7 @@ def cli_save_multi_files(self, args, files, line_break, space): format_ = MANY_MD else: format_ = MANY_TEXT - sort_by = self.cli_sort + sort_by = partial(self.cli_sort, args) path = abspath(args.output) for file_ in files: authors, title, highlights = self.cli_get_item_data(file_, args) @@ -2973,8 +3651,9 @@ def cli_save_multi_files(self, args, files, line_break, space): try: save_file(title, authors, highlights, path, format_, line_break, space, sort_by) + saved += 1 except IOError as err: # any problem when writing (like long filename, etc.) - sys.stdout.write(str("Could not save the file to disk!\n{}").format(err)) + sys.stdout.write(str(f"Could not save the file to disk!\n{err}")) return saved def cli_save_merged_file(self, args, files, line_break, space): @@ -3024,7 +3703,7 @@ def cli_save_merged_file(self, args, files, line_break, space): path = name + new_ext with open(path, "w+", encoding=encoding, newline="") as text_file: text_file.write(text) - sys.stdout.write(str("Created {}\n\n").format(path)) + sys.stdout.write(f"Created {path}\n\n") return saved def cli_get_item_data(self, file_, args): @@ -3037,13 +3716,25 @@ def cli_get_item_data(self, file_, args): """ data = decode_data(file_) highlights = [] - for page in data["highlight"]: - for page_id in data["highlight"][page]: - highlights.append(self.cli_get_formatted_high(data, page, page_id, args)) + + annotations = data.get("annotations") + if annotations: # new format metadata + for idx in annotations: + highlight = self.base.get_new_highlight_info(data, idx) + if highlight: + formatted_high = self.cli_get_formatted_high(highlight, args) + highlights.append(formatted_high) + else: # old format metadata + for page in data["highlight"]: + for page_id in data["highlight"][page]: + highlight = self.base.get_old_highlight_info(data, page, page_id) + if highlight: + highlights.append(self.cli_get_formatted_high(highlight, args)) authors = "" + stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" try: - title = data["stats"]["title"] - authors = data["stats"]["authors"] + title = data[stats]["title"] + authors = data[stats]["authors"] except KeyError: # older type file title = splitext(basename(file_))[0] try: @@ -3059,20 +3750,15 @@ def cli_get_item_data(self, file_, args): title = NO_TITLE return authors, title, highlights - # noinspection PyTypeChecker - def cli_get_formatted_high(self, data, page, page_id, args): - """ Get the highlight's info (text, comment, date and page) + @staticmethod + def cli_get_formatted_high(highlight, args): + """ Return the highlight's info in a formatted way - :type data: dict - :param data: The highlight's data - :type page: int - :param page The page where the highlight starts - :type page_id: int - :param page_id The count of this page's highlight + :type highlight: dict + :param highlight: The highlight's data :type args: argparse.Namespace :param args: The parsed cli args """ - highlight = self.base.get_highlight_info(data, page, page_id) nl = "
" if args.html else os.linesep chapter = highlight["chapter"].replace("\n", nl) if not args.no_chapter else "" high_text = highlight["text"] @@ -3081,11 +3767,11 @@ def cli_get_formatted_high(self, data, page, page_id, args): date = highlight["date"] line_break2 = os.linesep if not args.no_highlight and comment else "" if args.csv: - page_text = str(page) if not args.no_page else "" + page_text = highlight["page"] if not args.no_page else "" date_text = date if not args.no_date else "" high_comment = comment if not args.no_comment and comment else "" else: - page_text = "Page " + str(page) if not args.no_page else "" + page_text = "Page " + highlight["page"] if not args.no_page else "" date_text = "[" + date + "]" if not args.no_date else "" high_comment = (line_break2 + "● " + comment if not args.no_comment and comment else "") @@ -3163,10 +3849,11 @@ def get_name(data, meta_path, title_counter): :type title_counter: list :param title_counter: A list with the current NO TITLE counter """ + stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" authors = "" try: - title = data["stats"]["title"] - authors = data["stats"]["authors"] + title = data[stats]["title"] + authors = data[stats]["authors"] except KeyError: # older type file title = splitext(basename(meta_path))[0] try: @@ -3183,7 +3870,7 @@ def get_name(data, meta_path, title_counter): title_counter[0] += 1 name = title if authors: - name = "{} - {}".format(authors, title) + name = f"{authors} - {title}" return name diff --git a/requirements.txt b/requirements.txt index 39b5618..439f897 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ beautifulsoup4 future -PySide==1.2.4 +PySide2 requests -pypiwin32 -pywin32 \ No newline at end of file +packaging +pypiwin32; sys_platform == 'win32' +pywin32; sys_platform == 'win32' \ No newline at end of file diff --git a/screens/screen d1.png b/screens/screen d1.png index 0a5fde9..b51e793 100644 Binary files a/screens/screen d1.png and b/screens/screen d1.png differ diff --git a/screens/screen l1.png b/screens/screen l1.png index f2c8f3b..fad78fd 100644 Binary files a/screens/screen l1.png and b/screens/screen l1.png differ diff --git a/secondary.py b/secondary.py index 3de81bf..542bf7a 100644 --- a/secondary.py +++ b/secondary.py @@ -1,41 +1,36 @@ # coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals from boot_config import * from boot_config import _ import re import webbrowser from functools import partial +from ntpath import normpath from os.path import join, basename, splitext, isfile -from pprint import pprint - -if QT4: # ___ ______________ DEPENDENCIES __________________________ - from PySide.QtCore import Qt, Slot, QObject, Signal, QSize, QPoint, QEvent - from PySide.QtGui import (QApplication, QMessageBox, QIcon, QFileDialog, QLineEdit, - QDialog, QWidget, QMovie, QFont, QMenu, QAction, QCursor, - QTableWidget, QCheckBox, QToolButton, QActionGroup, - QTableWidgetItem) -elif QT5: - from PySide2.QtCore import QObject, Qt, Signal, QPoint, Slot, QSize, QEvent - from PySide2.QtGui import QFont, QMovie, QIcon, QCursor + +if QT5: # ___ ______________ DEPENDENCIES __________________________ + from PySide2.QtCore import (QObject, Qt, Signal, QPoint, Slot, QSize, QEvent, QRect, + QTimer) + from PySide2.QtGui import (QFont, QMovie, QIcon, QCursor, QPalette, QColor, QPixmap, + QPainter, QPen) from PySide2.QtWidgets import (QTableWidgetItem, QTableWidget, QMessageBox, QLineEdit, QApplication, QWidget, QDialog, QFileDialog, - QActionGroup, QMenu, QAction, QToolButton, QCheckBox) + QStyleFactory, QActionGroup, QMenu, QAction, + QToolButton, QCheckBox) else: # Qt6 - from PySide6.QtCore import QObject, Qt, Signal, QEvent, QPoint, Slot, QSize - from PySide6.QtGui import QFont, QActionGroup, QAction, QCursor, QMovie, QIcon + from PySide6.QtCore import (QObject, Qt, Signal, QEvent, QPoint, Slot, QSize, QRect, + QTimer) + from PySide6.QtGui import (QFont, QActionGroup, QAction, QCursor, QMovie, QIcon, + QPalette, QColor, QPixmap, QPainter, QPen) from PySide6.QtWidgets import (QTableWidgetItem, QTableWidget, QApplication, QLineEdit, QToolButton, QWidget, QMenu, QFileDialog, - QDialog, QMessageBox, QCheckBox) - -if PYTHON2: # ___ __________ PYTHON 2/3 COMPATIBILITY ______________ - from distutils.version import LooseVersion as version_parse -else: - from packaging.version import parse as version_parse - + QDialog, QMessageBox, QCheckBox, QStyleFactory) import requests from bs4 import BeautifulSoup +from packaging.version import parse as version_parse from slppu import slppu as lua # https://github.com/noembryo/slppu +__author__ = "noEmbryo" + def decode_data(path): """ Converts a lua table to a Python dict @@ -105,12 +100,11 @@ def get_book_text(title, authors, highlights, format_, line_break, space, text): elif format_ == ONE_TEXT: name = title if authors: - name = "{} - {}".format(authors, title) + name = f"{authors} - {title}" line = "-" * 80 text += line + nl + name + nl + line + nl - highlights = [i[3] + space + i[0] + line_break + - ("[{}]{}".format(i[4], nl) if i[4] else "") + - i[2] + i[1] for i in highlights] + highlights = [i[3] + space + i[0] + line_break + (f"[{i[4]}]{nl}" if i[4] else "") + + i[2] + i[1] for i in highlights] text += (nl * 2).join(highlights) + nl * 2 elif format_ == ONE_CSV: for high in highlights: @@ -121,7 +115,7 @@ def get_book_text(title, authors, highlights, format_, line_break, space, text): # data = {k.encode("utf8"): v.encode("utf8") for k, v in data.items()} text += get_csv_row(data) + "\n" elif format_ == ONE_MD: - text += "\n---\n## {} \n##### {} \n---\n".format(title, authors) + text += f"\n---\n## {title} \n##### {authors} \n---\n" highs = [] for i in highlights: comment = i[1].replace(nl, " " + nl) @@ -129,7 +123,7 @@ def get_book_text(title, authors, highlights, format_, line_break, space, text): comment = " " + comment chapter = i[4] if chapter: - chapter = "***{0}***{1}{1}".format(chapter, nl).replace(nl, " " + nl) + chapter = f"***{chapter}***{nl}{nl}".replace(nl, " " + nl) high = i[2].replace(nl, " " + nl) h = ("*" + i[3] + space + i[0] + line_break + chapter + high + comment + " \n  \n") @@ -147,7 +141,7 @@ def save_file(title, authors, highlights, path, format_, line_break, space, sort encoding = "utf-8" name = title if authors: - name = "{} - {}".format(authors, title) + name = f"{authors} - {title}" if format_ == MANY_TEXT: ext = ".txt" line = "-" * 80 @@ -161,7 +155,7 @@ def save_file(title, authors, highlights, path, format_, line_break, space, sort encoding = "utf-8-sig" elif format_ == MANY_MD: ext = ".md" - text = "\n---\n## {} \n##### {} \n---\n".format(title, authors) + text = f"\n---\n## {title} \n##### {authors} \n---\n" filename = join(path, sanitize_filename(name)) if _("NO TITLE FOUND") in title: # don't overwrite unknown title files @@ -182,7 +176,7 @@ def save_file(title, authors, highlights, path, format_, line_break, space, sort "chapter": chapter} elif format_ == MANY_TEXT: text += (page_text + space + date_text + line_break + - ("[{}]{}".format(chapter, nl) if chapter else "") + + (f"[{chapter}]{nl}" if chapter else "") + high_text + high_comment) text += 2 * nl elif format_ == MANY_CSV: @@ -196,7 +190,7 @@ def save_file(title, authors, highlights, path, format_, line_break, space, sort if high_comment: high_comment = " " + high_comment if chapter: - chapter = "***{0}***{1}{1}".format(chapter, nl).replace(nl, " " + nl) + chapter = f"***{chapter}***{nl}{nl}".replace(nl, " " + nl) text += ("*" + page_text + space + date_text + line_break + chapter + high_text + high_comment + " \n  \n\n").replace("-", "\\-") @@ -210,7 +204,8 @@ def save_file(title, authors, highlights, path, format_, line_break, space, sort "get_book_text", "save_file", "XTableWidgetIntItem", "XTableWidgetPercentItem", "XTableWidgetTitleItem", "DropTableWidget", "XMessageBox", "About", "AutoInfo", "ToolBar", "TextDialog", "Status", "LogStream", "Scanner", "HighlightScanner", - "ReLoader", "DBLoader", "XToolButton", "Filter") + "ReLoader", "DBLoader", "XToolButton", "Filter", "XThemes", "XIconGlyph", + "SyncGroup", "SyncItem", "XTableWidget") # ___ _______________________ SUBCLASSING ___________________________ @@ -296,6 +291,76 @@ def dropEvent(self, event): return False +class XTableWidget(QTableWidget): + """ QTableWidget with support for drag and drop move of rows that contain widgets + """ + + def __init__(self, *args, **kwargs): + super(XTableWidget, self).__init__(*args, **kwargs) + self.viewport().setAcceptDrops(True) + self.selection = self.selectionModel() + self.base = None + + def dropEvent(self, event): + if not event.isAccepted() and event.source() == self: + drop_row = self.drop_on(event) + # rows = sorted(set(i.row() for i in self.selectedItems())) + rows = sorted(set(i.row() for i in self.selection.selectedRows())) + rows_to_move = [] + for row in rows: + items = dict() + for col in range(self.columnCount()): + # get the widget or item of current cell + widget = self.cellWidget(row, col) + if isinstance(widget, type(None)): # a normal QTableWidgetItem + items[col] = {"kind": "QTableWidgetItem", + "item": QTableWidgetItem(self.item(row, col))} + else: # another kind of widget. + # So we catch the widget's unique characteristics + items[col] = {"kind": "QWidget", "item": widget.data} + rows_to_move.append(items) + + for row in reversed(rows): + self.removeRow(row) + # if row < drop_row: + # drop_row -= 1 + for row, data in enumerate(rows_to_move): + row += drop_row + self.insertRow(row) + + for col, info in data.items(): + if info["kind"] == "QTableWidgetItem": + # for QTableWidgetItem we can re-create the item directly + self.setItem(row, col, info["item"]) + else: # for other widgets we call + # the parent's callback function to get them + widget = self.base.create_sync_widget(info["item"]) + widget.idx = row + self.base.sync_table.setRowHeight(row, widget.sizeHint().height()) + self.setCellWidget(row, col, widget) + + self.base.update_sync_groups() + event.accept() + super(XTableWidget, self).dropEvent(event) + + def drop_on(self, event): + index = self.indexAt(event.pos()) + if not index.isValid(): + return self.rowCount() - 1 + return index.row() + 1 if self.is_below(event.pos(), index) else index.row() + + def is_below(self, pos, index): + rect = self.visualRect(index) + margin = 2 + if pos.y() - rect.top() < margin: + return False + elif rect.bottom() - pos.y() < margin: + return True + # noinspection PyTypeChecker + return rect.contains(pos, True) and not (int(self.model().flags( + index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y() + + class XMessageBox(QMessageBox): """ A QMessageBox with a QCheckBox """ @@ -353,7 +418,7 @@ def add_to_layout(self, widget): :param widget: The widget to be added """ # noinspection PyArgumentList - self.layout().addWidget(widget, 1, 1 if PYTHON2 else 2) + self.layout().addWidget(widget, 1, 2) class XToolButton(QToolButton): @@ -396,7 +461,7 @@ def write(self, text): class Scanner(QObject): - found = Signal(unicode) + found = Signal(str) finished = Signal() def __init__(self, path): @@ -404,10 +469,6 @@ def __init__(self, path): self.path = path def process(self): - self.start_scan() - self.finished.emit() - - def start_scan(self): try: for dir_path, dirs, files in os.walk(self.path): if dir_path.lower().endswith(".sdr"): # a book's metadata folder @@ -425,10 +486,11 @@ def start_scan(self): continue except UnicodeDecodeError: # os.walk error pass + self.finished.emit() class ReLoader(QObject): - found = Signal(unicode) + found = Signal(str) finished = Signal() def __init__(self, paths): @@ -436,13 +498,14 @@ def __init__(self, paths): self.paths = paths def process(self): + # print("Loading data from files") for path in self.paths: self.found.emit(path) self.finished.emit() class DBLoader(QObject): - found = Signal(unicode, dict, unicode) + found = Signal(str, dict, str) finished = Signal() def __init__(self, books): @@ -468,20 +531,319 @@ def process(self): for row in range(self.base.file_table.rowCount()): data = self.base.file_table.item(row, TITLE).data(Qt.UserRole) path = self.base.file_table.item(row, TYPE).data(Qt.UserRole)[0] - self.get_book_highlights(data, path) + meta_path = self.base.file_table.item(row, PATH).data(0) + highlights = self.base.get_highlights_from_data(data, path, meta_path) + for highlight in highlights: + self.found.emit(highlight) self.finished.emit() - def get_book_highlights(self, data, path): - """ Finds all the highlights from a book - :type data: dict - :param data: The book data (converted from the lua file) - :type path: str|unicode - :param path: The book path +class XThemes(QObject): + """ Dark and light theme palettes + """ + + def __init__(self, parent=None): + super(XThemes, self).__init__(parent) + + # noinspection PyArgumentList + self.app = QApplication.instance() + self.def_style = str(self.app.style()) + # noinspection PyArgumentList + themes = QStyleFactory.keys() + if "Fusion" in themes: + self.app_style = "Fusion" + elif "Plastique" in themes: + self.app_style = "Plastique" + else: + self.app_style = self.def_style + self.def_colors = self.get_current() + # self.def_palette = self.app.palette() + + def dark(self): + """ Apply a dark theme """ - highlights = self.base.get_highlights_from_data(data, path) - for highlight in highlights: - self.found.emit(highlight) + + dark_palette = QPalette() + + # base + text = 250 + dark_palette.setColor(QPalette.WindowText, QColor(text, text, text)) + dark_palette.setColor(QPalette.Button, QColor(53, 53, 53)) + dark_palette.setColor(QPalette.Light, QColor(text, text, text)) + dark_palette.setColor(QPalette.Midlight, QColor(90, 90, 90)) + dark_palette.setColor(QPalette.Dark, QColor(35, 35, 35)) + dark_palette.setColor(QPalette.Text, QColor(text, text, text)) + dark_palette.setColor(QPalette.BrightText, QColor(text, text, text)) + dark_palette.setColor(QPalette.ButtonText, QColor(text, text, text)) + dark_palette.setColor(QPalette.Base, QColor(25, 25, 25)) + # dark_palette.setColor(QPalette.Text, QColor(180, 180, 180)) + # dark_palette.setColor(QPalette.BrightText, QColor(180, 180, 180)) + # dark_palette.setColor(QPalette.ButtonText, QColor(180, 180, 180)) + # dark_palette.setColor(QPalette.Base, QColor(42, 42, 42)) + dark_palette.setColor(QPalette.Window, QColor(53, 53, 53)) + dark_palette.setColor(QPalette.Shadow, QColor(10, 10, 10)) + # dark_palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + dark_palette.setColor(QPalette.Highlight, QColor(20, 50, 80)) + dark_palette.setColor(QPalette.HighlightedText, QColor(text, text, text)) + dark_palette.setColor(QPalette.Link, QColor(56, 252, 196)) + dark_palette.setColor(QPalette.AlternateBase, QColor(66, 66, 66)) + dark_palette.setColor(QPalette.ToolTipBase, QColor(53, 53, 53)) + dark_palette.setColor(QPalette.ToolTipText, QColor(text, text, text)) + dark_palette.setColor(QPalette.LinkVisited, QColor(80, 80, 80)) + + # disabled + gray = QColor(100, 100, 100) + dark_palette.setColor(QPalette.Disabled, QPalette.WindowText, gray) + dark_palette.setColor(QPalette.Disabled, QPalette.Text, gray) + dark_palette.setColor(QPalette.Disabled, QPalette.ButtonText, gray) + dark_palette.setColor(QPalette.Disabled, QPalette.HighlightedText, gray) + dark_palette.setColor(QPalette.Disabled, QPalette.Highlight, + QColor(80, 80, 80)) + + self.app.style().unpolish(self.app) + self.app.setPalette(dark_palette) + # self.app.setStyle("Fusion") + self.app.setStyle(self.app_style) + + def light(self): + """ Apply a light theme + """ + + light_palette = QPalette() + + # base + light_palette.setColor(QPalette.WindowText, QColor(0, 0, 0)) + light_palette.setColor(QPalette.Button, QColor(240, 240, 240)) + # light_palette.setColor(QPalette.Light, QColor(180, 180, 180)) + # light_palette.setColor(QPalette.Midlight, QColor(200, 200, 200)) + # light_palette.setColor(QPalette.Dark, QColor(225, 225, 225)) + light_palette.setColor(QPalette.Dark, QColor(180, 180, 180)) + light_palette.setColor(QPalette.Midlight, QColor(200, 200, 200)) + light_palette.setColor(QPalette.Light, QColor(250, 250, 250)) + light_palette.setColor(QPalette.Text, QColor(0, 0, 0)) + light_palette.setColor(QPalette.BrightText, QColor(0, 0, 0)) + light_palette.setColor(QPalette.ButtonText, QColor(0, 0, 0)) + light_palette.setColor(QPalette.Base, QColor(237, 237, 237)) + light_palette.setColor(QPalette.Window, QColor(240, 240, 240)) + light_palette.setColor(QPalette.Shadow, QColor(20, 20, 20)) + # light_palette.setColor(QPalette.Highlight, QColor(76, 163, 224)) + light_palette.setColor(QPalette.Highlight, QColor(200, 230, 255)) + light_palette.setColor(QPalette.HighlightedText, QColor(0, 0, 0)) + light_palette.setColor(QPalette.Link, QColor(0, 162, 232)) + light_palette.setColor(QPalette.AlternateBase, QColor(225, 225, 225)) + light_palette.setColor(QPalette.ToolTipBase, QColor(240, 240, 240)) + light_palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0)) + light_palette.setColor(QPalette.LinkVisited, QColor(222, 222, 222)) + + # disabled + light_palette.setColor(QPalette.Disabled, QPalette.WindowText, + QColor(115, 115, 115)) + light_palette.setColor(QPalette.Disabled, QPalette.Text, + QColor(115, 115, 115)) + light_palette.setColor(QPalette.Disabled, QPalette.ButtonText, + QColor(115, 115, 115)) + light_palette.setColor(QPalette.Disabled, QPalette.Highlight, + QColor(190, 190, 190)) + light_palette.setColor(QPalette.Disabled, QPalette.HighlightedText, + QColor(115, 115, 115)) + + self.app.style().unpolish(self.app) + self.app.setPalette(light_palette) + # self.app.setStyle("Fusion") + self.app.setStyle(self.app_style) + + def normal(self): + """ Apply the normal theme + """ + normal_palette = QPalette() + + normal_palette.setColor(QPalette.WindowText, self.def_colors["WindowText"]) + normal_palette.setColor(QPalette.Button, self.def_colors["Button"]) + normal_palette.setColor(QPalette.Light, self.def_colors["Light"]) + normal_palette.setColor(QPalette.Midlight, self.def_colors["Midlight"]) + normal_palette.setColor(QPalette.Dark, self.def_colors["Dark"]) + normal_palette.setColor(QPalette.Text, self.def_colors["Text"]) + normal_palette.setColor(QPalette.BrightText, self.def_colors["BrightText"]) + normal_palette.setColor(QPalette.ButtonText, self.def_colors["ButtonText"]) + normal_palette.setColor(QPalette.Base, self.def_colors["Base"]) + normal_palette.setColor(QPalette.Window, self.def_colors["Window"]) + normal_palette.setColor(QPalette.Shadow, self.def_colors["Shadow"]) + normal_palette.setColor(QPalette.Highlight, self.def_colors["Highlight"]) + normal_palette.setColor(QPalette.HighlightedText, + self.def_colors["HighlightedText"]) + normal_palette.setColor(QPalette.Link, self.def_colors["Link"]) + normal_palette.setColor(QPalette.AlternateBase, + self.def_colors["AlternateBase"]) + normal_palette.setColor(QPalette.ToolTipBase, self.def_colors["ToolTipBase"]) + normal_palette.setColor(QPalette.ToolTipText, self.def_colors["ToolTipText"]) + normal_palette.setColor(QPalette.LinkVisited, self.def_colors["LinkVisited"]) + + # # disabled + # normal_palette.setColor(QPalette.Disabled, QPalette.WindowText, + # QColor(115, 115, 115)) + # normal_palette.setColor(QPalette.Disabled, QPalette.Text, + # QColor(115, 115, 115)) + # normal_palette.setColor(QPalette.Disabled, QPalette.ButtonText, + # QColor(115, 115, 115)) + # normal_palette.setColor(QPalette.Disabled, QPalette.Highlight, + # QColor(190, 190, 190)) + # normal_palette.setColor(QPalette.Disabled, QPalette.HighlightedText, + # QColor(115, 115, 115)) + + self.app.style().unpolish(self.app.base) + self.app.setPalette(normal_palette) + # self.app.setPalette(self.def_palette) + self.app.setStyle(self.def_style) + + @staticmethod + def get_current(): + """ Return the current theme's data + """ + light_palette = QPalette() + data = {'WindowText': (light_palette.color(QPalette.WindowText)), + 'Button': (light_palette.color(QPalette.Button)), + 'Light': (light_palette.color(QPalette.Light)), + 'Midlight': (light_palette.color(QPalette.Midlight)), + 'Dark': (light_palette.color(QPalette.Dark)), + 'Text': (light_palette.color(QPalette.Text)), + 'BrightText': (light_palette.color(QPalette.BrightText)), + 'ButtonText': (light_palette.color(QPalette.ButtonText)), + 'Base': (light_palette.color(QPalette.Base)), + 'Window': (light_palette.color(QPalette.Window)), + 'Shadow': (light_palette.color(QPalette.Shadow)), + 'Highlight': (light_palette.color(QPalette.Highlight)), + 'HighlightedText': (light_palette.color(QPalette.HighlightedText)), + 'Link': (light_palette.color(QPalette.Link)), + 'AlternateBase': (light_palette.color(QPalette.AlternateBase)), + 'ToolTipBase': (light_palette.color(QPalette.ToolTipBase)), + 'ToolTipText': (light_palette.color(QPalette.ToolTipText)), + 'LinkVisited': (light_palette.color(QPalette.LinkVisited))} + return data + + +class XIconGlyph(QObject): + """ A Font char to QIcon converter + + * Usage in Base: + fdb = QFontDatabase() + fdb.addApplicationFont(":/stuff/font.ttf") # add a custom font or use existing + # pprint(fdb.families()) + + self.font_ico = XIconGlyph(self, glyph=None) + + ico = self.font_ico.get_icon({"char": "✓", + "size": (32, 32), + "size_ratio": 1.2, + "offset": (0, -2), + "family": "XFont", + "color": "#FF0000", + "active": "orange", + "hover": (160, 50, 255), + }) + self.tool_btn.setIcon(ico) + """ + + def __init__(self, parent, glyph=None): + super(XIconGlyph, self).__init__(parent) + self.char = "" + self.family = "" + self.color = parent.palette().text().color().name() # use the default + self.active = None + self.hover = None + self.disabled = parent.palette().dark().color().name() + self.icon_size = 16, 16 + self.size_ratio = 1 + self.offset = 0, 0 + self.glyph = glyph + if glyph: + self._parse_glyph(glyph) + + def _parse_glyph(self, glyph): + """ Set the glyph options + + :type glyph: dict + """ + if self.glyph: + self.glyph.update(glyph) + + family = glyph.get("family") + if family: + self.family = family + char = glyph.get("char") + if char: + self.char = char + icon_size = glyph.get("size") + if icon_size: + self.icon_size = icon_size + offset = glyph.get("offset") + if offset: + self.offset = offset + size_ratio = glyph.get("size_ratio") + if size_ratio: + self.size_ratio = size_ratio + active = glyph.get("active") + if active: + self.active = active + hover = glyph.get("hover") + if hover: + self.hover = hover + color = glyph.get("color") + if color: + self.color = color + disabled = glyph.get("disabled") + if disabled: + self.disabled = disabled + + def _get_char_pixmap(self, color): + """ Create an icon from a font character + + :type color: str|tuple + :param color: The color of the icon + :return: QPixmap + """ + if isinstance(color, tuple): + color = QColor(*color) + else: + color = QColor(color) + font = QFont() + if self.family: + font.setFamily(self.family) + font.setPixelSize(self.icon_size[1] * self.size_ratio) + + pixmap = QPixmap(*self.icon_size) + pixmap.fill(QColor(0, 0, 0, 0)) # fill with transparency + + painter = QPainter(pixmap) + painter.setFont(font) + pen = QPen() + pen.setColor(color) + painter.setPen(pen) + painter.drawText(QRect(QPoint(*self.offset), QSize(*self.icon_size)), + Qt.AlignCenter | Qt.AlignVCenter, self.char) + painter.end() + return pixmap + + def get_icon(self, glyph=None): + """ Get the icon from the glyph + + :type glyph: dict + :return: QIcon + """ + if glyph: + self._parse_glyph(glyph) + + icon = QIcon() + icon.addPixmap(self._get_char_pixmap(self.color), QIcon.Normal, QIcon.Off) + if self.active: # the checkable down state icon + icon.addPixmap(self._get_char_pixmap(self.active), + QIcon.Active, QIcon.On) + if self.hover: # the mouse hover state icon + icon.addPixmap(self._get_char_pixmap(self.hover), + QIcon.Active, QIcon.Off) + if self.disabled: # the disabled state icon + icon.addPixmap(self._get_char_pixmap(self.disabled), + QIcon.Disabled, QIcon.Off) + return icon # ___ _______________________ GUI STUFF _____________________________ @@ -492,6 +854,8 @@ def get_book_highlights(self, data, path): from gui_status import Ui_Status from gui_edit import Ui_TextDialog from gui_filter import Ui_Filter +from gui_sync_group import Ui_SyncGroup +from gui_sync_item import Ui_SyncItem class ToolBar(QWidget, Ui_ToolBar): @@ -505,20 +869,22 @@ def __init__(self, parent=None): self.setupUi(self) self.base = parent - self.buttons = (self.check_btn, self.scan_btn, self.export_btn, self.open_btn, - self.merge_btn, self.delete_btn, self.clear_btn, self.about_btn, - self.books_view_btn, self.high_view_btn, self.filter_btn) - self.size_menu = self.create_size_menu() - self.db_menu = self.create_db_menu() + self.buttons = (self.scan_btn, self.export_btn, self.open_btn, self.merge_btn, + self.delete_btn, self.clear_btn, self.about_btn, self.filter_btn, + self.books_view_btn, self.high_view_btn, self.sync_view_btn, + self.add_btn) + + self.size_menu = QMenu(self) + # self.size_menu.aboutToShow.connect(self.create_size_menu) + + self.db_menu = QMenu() + self.db_menu.aboutToShow.connect(self.create_db_menu) self.db_btn.setMenu(self.db_menu) - for btn in [self.loaded_btn, self.db_btn, - self.books_view_btn, self.high_view_btn]: + for btn in [self.books_view_btn, self.high_view_btn, self.sync_view_btn, + self.loaded_btn, self.db_btn]: btn.clicked.connect(self.change_view) - self.check_btn.clicked.connect(parent.on_check_btn) - self.check_btn.hide() - @Slot(QPoint) def on_tool_frame_customContextMenuRequested(self, point): """ The Toolbar is right-clicked @@ -526,25 +892,20 @@ def on_tool_frame_customContextMenuRequested(self, point): :type point: QPoint :param point: The point where the right-click happened """ - self.size_menu.exec_(self.tool_frame.mapToGlobal(point)) - - def create_size_menu(self): - """ Create the toolbar's buttons size menu - """ - menu = QMenu(self) + self.size_menu.clear() group = QActionGroup(self) sizes = (_("Tiny"), 16), (_("Small"), 32), (_("Medium"), 48), (_("Big"), 64) for name, size in sizes: - action = QAction(name, menu) + action = QAction(name, self.size_menu) action.setCheckable(True) if size == self.base.toolbar_size: action.setChecked(True) action.triggered.connect(partial(self.set_btn_size, size)) group.addAction(action) - menu.addAction(action) + self.size_menu.addAction(action) if QT6: # QT6 requires exec() instead of exec_() - menu.exec_ = getattr(menu, "exec") - return menu + self.size_menu.exec_ = getattr(self.size_menu, "exec") + self.size_menu.exec_(self.tool_frame.mapToGlobal(point)) def set_btn_size(self, size): """ Changes the Toolbar's icons size @@ -559,6 +920,7 @@ def set_btn_size(self, size): for btn in self.buttons: btn.setMinimumWidth(size + 10) btn.setIconSize(button_size) + # btn.setStyleSheet("QToolButton:disabled {background-color: rgb(0, 0, 0);}") for btn in [self.loaded_btn, self.db_btn]: # btn.setMinimumWidth(size + 10) @@ -566,6 +928,9 @@ def set_btn_size(self, size): # noinspection PyArgumentList QApplication.processEvents() + if self.base.theme in [THEME_NONE_NEW, THEME_DARK_NEW, THEME_LIGHT_NEW]: + self.base.set_new_icons(menus=False) + @Slot() def on_scan_btn_clicked(self): """ The `Scan Directory` button is pressed @@ -606,6 +971,19 @@ def on_open_btn_clicked(self): data = self.base.high_table.item(idx.row(), HIGHLIGHT_H).data(Qt.UserRole) self.base.open_file(data["path"]) + @Slot() + def on_add_btn_clicked(self): + """ The `Add` sync group button is pressed + """ + if self.base.current_view == SYNC_VIEW: + info = {"title": "", + "sync_pos": False, + "merge": False, + "sync_db": True, + "items": [{"path": "", "data": {}}], + "enabled": True} + self.base.create_sync_row(info) + @Slot(bool) def on_filter_btn_toggled(self, state): """ The `Find` button is pressed @@ -623,9 +1001,27 @@ def on_filter_btn_toggled(self, state): def on_merge_btn_clicked(self): """ The `Merge` button is pressed """ + if self.base.current_view == SYNC_VIEW: + if self.base.merge_warning_stop(): + return + text = _("Synchronize all active Sync groups?") + popup = self.base.popup(_("Sync"), text, icon=QMessageBox.Question, + buttons=2) + if popup.buttonRole(popup.clickedButton()) == QMessageBox.AcceptRole: + changed_total = 0 + for idx in range(self.base.sync_table.rowCount()): + group = self.base.sync_table.cellWidget(idx, 0) + if group.power_btn.isChecked(): + changed = self.base.synchronize_group(group, multi=True) + if changed: + changed_total += 1 + text = _(f"Synchronization process completed\n" + f"{changed_total} groups were synchronized") + self.base.popup(_("Information"), text, QMessageBox.Information) + return data = [self.base.file_table.item(idx.row(), idx.column()).data(Qt.UserRole) for idx in self.base.sel_indexes] - if self.base.same_cre_version(data): + if self.base.same_cre_version(*data): self.base.on_merge_highlights() else: self.base.wrong_cre_version() @@ -634,18 +1030,31 @@ def on_merge_btn_clicked(self): def on_delete_btn_clicked(self): """ The `Delete` button is pressed """ - self.base.delete_actions(0) + if self.base.current_view == BOOKS_VIEW: + # self.base.delete_actions(0) + if not self.base.db_mode: + self.delete_btn.showMenu() + else: + self.base.delete_actions(0) + elif self.base.current_view == HIGHLIGHTS_VIEW: + self.base.on_delete_highlights() + elif self.base.current_view == SYNC_VIEW: + for index in sorted(self.base.sel_sync_view)[::-1]: + row = index.row() + del self.base.sync_groups[row] + self.base.sync_table.model().removeRow(row) + self.base.update_sync_groups() @Slot() def on_clear_btn_clicked(self): """ The `Clear List` button is pressed """ - if self.base.current_view == HIGHLIGHTS_VIEW: - (self.base.high_table.model() # clear Books view too - .removeRows(0, self.base.high_table.rowCount())) + if self.base.current_view == SYNC_VIEW: + return self.base.loaded_paths.clear() self.base.reload_highlights = True self.base.file_table.model().removeRows(0, self.base.file_table.rowCount()) + self.base.high_table.model().removeRows(0, self.base.high_table.rowCount()) self.activate_buttons() @Slot() @@ -658,44 +1067,52 @@ def on_db_btn_right_clicked(self): def create_db_menu(self): """ Create the database menu """ - menu = QMenu(self) - - action = QAction(_("Create new database"), menu) + self.db_menu.clear() + action = QAction(_("Create new database"), self.db_menu) action.setIcon(self.base.ico_db_add) action.triggered.connect(partial(self.base.change_db, NEW_DB)) - menu.addAction(action) + self.db_menu.addAction(action) - action = QAction(_("Reload database"), menu) + action = QAction(_("Reload database"), self.db_menu) action.setIcon(self.base.ico_refresh) action.triggered.connect(partial(self.base.change_db, RELOAD_DB)) - menu.addAction(action) + self.db_menu.addAction(action) - action = QAction(_("Change database"), menu) + action = QAction(_("Change database"), self.db_menu) action.setIcon(self.base.ico_db_open) action.triggered.connect(partial(self.base.change_db, CHANGE_DB)) - menu.addAction(action) + self.db_menu.addAction(action) if QT6: # QT6 requires exec() instead of exec_() - menu.exec_ = getattr(menu, "exec") - return menu + self.db_menu.exec_ = getattr(self.db_menu, "exec") def change_view(self): """ Changes what is shown in the app """ - new = self.update_archived() if self.db_btn.isChecked() else self.update_loaded() + reloaded = False + if not self.sync_view_btn.isChecked(): # don't reload when coming from Sync view + reloaded = (self.update_archived() + if self.db_btn.isChecked() else self.update_loaded()) if self.books_view_btn.isChecked(): # Books view - # self.add_btn_menu(self.base.toolbar.export_btn) + self.base.current_view = BOOKS_VIEW + self.merge_btn.setToolTip(TOOLTIP_MERGE) + self.merge_btn.setStatusTip(TOOLTIP_MERGE) if self.base.sel_idx: item = self.base.file_table.item(self.base.sel_idx.row(), self.base.sel_idx.column()) self.base.on_file_table_itemClicked(item, reset=False) - else: # Highlights view - for btn in [self.base.toolbar.export_btn, self.base.toolbar.delete_btn]: - self.remove_btn_menu(btn) - if self.base.reload_highlights and not new: + elif self.high_view_btn.isChecked(): # Highlights view + self.base.current_view = HIGHLIGHTS_VIEW + if self.base.reload_highlights and not reloaded: self.base.scan_highlights_thread() + else: # Sync view + self.base.current_view = SYNC_VIEW + self.merge_btn.setToolTip(TOOLTIP_SYNC) + self.merge_btn.setStatusTip(TOOLTIP_SYNC) + if not self.base.sync_groups_loaded: + # noinspection PyTypeChecker + QTimer.singleShot(0, self.base.load_sync_groups) + self.base.sync_groups_loaded = True - self.base.current_view = (BOOKS_VIEW if self.books_view_btn.isChecked() - else HIGHLIGHTS_VIEW) self.base.views.setCurrentIndex(self.base.current_view) self.setup_buttons() self.activate_buttons() @@ -720,7 +1137,7 @@ def update_archived(self): self.base.db_mode = True self.base.reload_highlights = True self.base.read_books_from_db() - text = _("Loading {} database").format(APP_NAME) + text = _(f"Loading {APP_NAME} database") self.base.loading_thread(DBLoader, self.base.books, text) if not len(self.base.books): # no books in the db text = _('There are no books currently in the archive.\nTo add/' @@ -733,74 +1150,84 @@ def setup_buttons(self): """ Shows/Hides toolbar's buttons based on the view selected """ books_view = self.books_view_btn.isChecked() + high_view = self.high_view_btn.isChecked() + sync_view = self.sync_view_btn.isChecked() db_mode = self.db_btn.isChecked() - self.scan_btn.setVisible(not db_mode) - self.merge_btn.setVisible(books_view and not db_mode) - self.delete_btn.setVisible(books_view) - self.clear_btn.setVisible(not db_mode) + self.scan_btn.setVisible(not (db_mode or sync_view)) + self.export_btn.setVisible(not sync_view) + self.open_btn.setVisible(not sync_view) + self.add_btn.setVisible(sync_view) + self.filter_btn.setVisible(not sync_view) + self.merge_btn.setVisible(sync_view or not (db_mode or high_view)) + # self.delete_btn.setVisible(books_view or sync_view) + self.clear_btn.setVisible(not (db_mode or sync_view)) - if self.base.db_mode: - self.remove_btn_menu(self.base.toolbar.delete_btn) - else: - self.add_btn_menu(self.base.toolbar.delete_btn) - self.base.status.setVisible(books_view) + self.mode_grp.setEnabled(not sync_view) + self.base.status.show_items_btn.setVisible(books_view) + + self.set_btn_menu(self.export_btn, books_view) + self.set_btn_menu(self.merge_btn, books_view) + self.set_btn_menu(self.delete_btn, books_view and not db_mode) def activate_buttons(self): """ Enables/Disables toolbar's buttons based on selection/view """ - if self.base.high_table.isVisible(): # Highlights view + count = 0 + sync_enable = False + book_exists = False + if self.base.current_view == HIGHLIGHTS_VIEW: # Highlights view try: idx = self.base.sel_high_view[-1] except IndexError: idx = None count = self.base.high_table.rowCount() - else: + elif self.base.current_view == BOOKS_VIEW: # Books view idx = self.base.sel_idx count = self.base.file_table.rowCount() + if len(self.base.sel_indexes) == 2: # check if we can sync/merge + idx1, idx2 = self.base.sel_indexes + data1 = self.base.file_table.item(idx1.row(), + idx1.column()).data(Qt.UserRole) + path1 = self.base.file_table.item(idx1.row(), TYPE).data(Qt.UserRole)[0] + data2 = self.base.file_table.item(idx2.row(), + idx2.column()).data(Qt.UserRole) + path2 = self.base.file_table.item(idx2.row(), TYPE).data(Qt.UserRole)[0] + sync_enable = self.base.same_book(data1, data2, path1, path2) + else: # Sync view + try: + idx = self.base.sel_sync_view[-1] + except IndexError: + idx = None + sync_enable = True + if idx: row = idx.row() - if self.base.high_table.isVisible(): # Highlights view + if self.base.file_table.isVisible(): # Books view + book_exists = self.base.file_table.item(row, TYPE).data(Qt.UserRole)[1] + elif self.base.high_table.isVisible(): # Highlights view data = self.base.high_table.item(row, HIGHLIGHT_H).data(Qt.UserRole) book_exists = isfile(data["path"]) - else: - book_exists = self.base.file_table.item(row, TYPE).data(Qt.UserRole)[1] - else: - book_exists = False - self.export_btn.setEnabled(bool(idx)) + self.export_btn.setEnabled(bool(idx) and not self.base.sync_table.isVisible()) self.open_btn.setEnabled(book_exists) self.delete_btn.setEnabled(bool(idx)) self.clear_btn.setEnabled(bool(count)) - - self.merge_btn.setEnabled(False) - if len(self.base.sel_indexes) == 2: # check if we can sync/merge - idx1, idx2 = self.base.sel_indexes - data1 = self.base.file_table.item(idx1.row(), idx1.column()).data(Qt.UserRole) - path1 = self.base.file_table.item(idx1.row(), TYPE).data(Qt.UserRole)[0] - data2 = self.base.file_table.item(idx2.row(), idx2.column()).data(Qt.UserRole) - path2 = self.base.file_table.item(idx2.row(), TYPE).data(Qt.UserRole)[0] - self.merge_btn.setEnabled(self.base.same_book(data1, data2, path1, path2)) + self.merge_btn.setEnabled(sync_enable) @staticmethod - def add_btn_menu(btn): + def set_btn_menu(btn, status=True): """ Adds a menu arrow to a toolbar button :type btn: QToolButton :param btn: The button to change """ - btn.setStyleSheet("") - btn.setPopupMode(QToolButton.MenuButtonPopup) - - @staticmethod - def remove_btn_menu(btn): - """ Removes the menu arrow from a toolbar button - - :type btn: QToolButton - :param btn: The button to change - """ - btn.setStyleSheet("QToolButton::menu-indicator{width:0px;}") - btn.setPopupMode(QToolButton.DelayedPopup) + if status: + btn.setStyleSheet("") + btn.setPopupMode(QToolButton.MenuButtonPopup) + else: + btn.setStyleSheet("QToolButton::menu-indicator{width:0px;}") + btn.setPopupMode(QToolButton.DelayedPopup) @Slot() def on_about_btn_clicked(self): @@ -819,10 +1246,7 @@ def __init__(self, parent=None): """ super(Filter, self).__init__(parent) self.setupUi(self) - if QT4: # Remove the question mark widget from dialog - # noinspection PyUnresolvedReferences - self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint) - self.setWindowTitle(_("Filter").format(APP_NAME)) + self.setWindowTitle(_("Filter")) self.base = parent def keyPressEvent(self, event): @@ -899,7 +1323,7 @@ def on_filter(self): else: self.base.file_table.setRowHidden(row, True) filtered += 1 - else: + elif self.base.toolbar.high_view_btn.isChecked(): row_count = self.base.high_table.rowCount() for row in range(row_count): title = self.base.high_table.item(row, TITLE_H).data(0) @@ -934,8 +1358,10 @@ def on_filter(self): else: self.base.high_table.setRowHidden(row, True) filtered += 1 - self.filtered_lbl.setText(_("Showing {}/{}").format(row_count - filtered, - row_count)) + else: + print("SYNC FILTERRRRRRRRRRRRRRRRR") + return + self.filtered_lbl.setText(_(f"Showing {row_count - filtered}/{row_count}")) @Slot() def on_clear_filter_btn_clicked(self): @@ -976,10 +1402,7 @@ def __init__(self, parent=None): """ super(About, self).__init__(parent) self.setupUi(self) - if QT4: # Remove the question mark widget from dialog - # noinspection PyUnresolvedReferences - self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint) - self.setWindowTitle(_("About {}").format(APP_NAME)) + self.setWindowTitle(_(f"About {APP_NAME}")) self.base = parent @Slot() @@ -1007,24 +1430,21 @@ def check_for_updates(self): current_version = version_parse(self.base.version) if version_new > current_version: popup = self.base.popup(_("Newer version exists!"), - _("There is a newer version (v.{}) online.\n" - "Open the site to download it now?") - .format(version_new), + _(f"There is a newer version (v.{version_new}) online" + f".\nOpen the site to download it now?"), icon=QMessageBox.Information, buttons=2) if popup.clickedButton().text() == "OK": webbrowser.open("http://www.noembryo.com/apps.php?katalib") self.close() elif version_new == current_version: self.base.popup(_("No newer version exists!"), - _("{} is up to date (v.{})").format(APP_NAME, - current_version), + _(f"{APP_NAME} is up to date (v.{current_version})"), icon=QMessageBox.Information, buttons=1) elif version_new < current_version: self.base.popup(_("No newer version exists!"), - _("It seems that you are using a newer version ({0})\n" - "than the one online ({1})!").format(current_version, - version_new), - icon=QMessageBox.Question, buttons=1) + _(f"It seems that you are using a newer version " + f"({current_version})\nthan the one online " + f"({version_new})!"), icon=QMessageBox.Question, buttons=1) @staticmethod def get_online_version(): @@ -1051,30 +1471,30 @@ def create_text(self): # color = self.palette().color(QPalette.WindowText).name() # for links splash = ":/stuff/logo.png" paypal = ":/stuff/paypal.png" - info = _(""" + info = _(f"""
-

-

{3} is a utility for viewing - Koreader's +

+

{APP_NAME} is a utility for viewing + KOReader's highlights
and/or export them to simple text

-

Version {1}

+

Version {self.base.version}

Visit - {3} page at GitHub, or

+ {APP_NAME} page at GitHub, or

noEmbryo's page with more Apps and stuff...

Use it and if you like it, consider to

- PayPal Button

 

- """).format(splash, self.base.version, paypal, APP_NAME) + """) self.text_lbl.setText(info) @@ -1101,19 +1521,16 @@ class TextDialog(QDialog, Ui_TextDialog): def __init__(self, parent=None): super(TextDialog, self).__init__(parent) - if QT4: # Remove the question mark widget from dialog - # noinspection PyUnresolvedReferences - self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint) self.setupUi(self) self.base = parent - self.on_ok = None + # self.on_ok = None @Slot() def on_ok_btn_clicked(self): """ The OK button is pressed """ - self.on_ok() + self.base.edit_comment_ok() class Status(QWidget, Ui_Status): @@ -1126,26 +1543,38 @@ def __init__(self, parent=None): super(Status, self).__init__(parent) self.setupUi(self) self.base = parent + self.themes = XThemes(parent) self.wait_anim = QMovie(":/stuff/wait.gif") self.anim_lbl.setMovie(self.wait_anim) self.anim_lbl.hide() + self.show_actions = [self.act_page, self.act_date, self.act_text, + self.act_chapter, self.act_comment] + for idx, act in enumerate(self.show_actions): + act.setData(idx) + act.triggered.connect(partial(self.on_show_items, act)) + self.show_menu = QMenu(self) - for i in [self.act_page, self.act_date, self.act_text, self.act_chapter, - self.act_comment]: - self.show_menu.addAction(i) - # noinspection PyUnresolvedReferences - i.triggered.connect(self.on_show_items) - i.setChecked(True) + self.show_menu.aboutToShow.connect(self.get_show_menu) + self.show_items_btn.setMenu(self.show_menu) + + def get_show_menu(self): + """ Returns the menu with the items to show + """ + self.show_menu.clear() + for idx, act in enumerate(self.show_actions): + act.setChecked(self.base.show_items[idx]) + self.show_menu.addAction(act) action = QAction(_("Date Format"), self.show_menu) - action.setIcon(QIcon(":/stuff/calendar.png")) + action.setIcon(self.base.ico_calendar) action.triggered.connect(self.set_date_format) self.show_menu.addAction(action) sort_menu = QMenu(self) - ico_sort = QIcon(":/stuff/sort.png") + sort_menu.setIcon(self.base.ico_sort) + sort_menu.setTitle(_("Sort by")) group = QActionGroup(self) action = QAction(_("Date"), sort_menu) @@ -1164,20 +1593,57 @@ def __init__(self, parent=None): group.addAction(action) sort_menu.addAction(action) - sort_menu.setIcon(ico_sort) - sort_menu.setTitle(_("Sort by")) self.show_menu.addMenu(sort_menu) - self.show_items_btn.setMenu(self.show_menu) - - def on_show_items(self): + @Slot(int) + def on_theme_box_currentIndexChanged(self, idx): + """ Selects the app's theme style + """ + if idx == THEME_NONE_OLD: + self.themes.normal() + self.base.set_old_icons() + if self.base.theme not in [THEME_NONE_OLD, THEME_NONE_NEW]: + self.no_theme_popup(idx) + return + elif idx == THEME_NONE_NEW: + self.themes.normal() + self.base.set_new_icons() + if self.base.theme not in [THEME_NONE_OLD, THEME_NONE_NEW]: + self.no_theme_popup(idx) + return + elif idx == THEME_DARK_OLD: + self.themes.dark() + self.base.set_old_icons() + elif idx == THEME_DARK_NEW: + self.themes.dark() + self.base.set_new_icons() + elif idx == THEME_LIGHT_OLD: + self.themes.light() + self.base.set_old_icons() + elif idx == THEME_LIGHT_NEW: + self.themes.light() + self.base.set_new_icons() + + self.base.theme = idx + self.base.reset_theme_colors() + + def no_theme_popup(self, idx): + self.base.theme = idx + self.base.reset_theme_colors() + self.base.popup(_("Warning"), _("The theme will be fully reset after " + "the application is restarted.")) + + def on_show_items(self, action=None): """ Show/Hide elements of the highlight info """ + if action: + act_idx = action.data() + self.base.show_items[act_idx] = action.isChecked() try: - idx = self.base.file_table.selectionModel().selectedRows()[-1] + table_idx = self.base.file_table.selectionModel().selectedRows()[-1] except IndexError: # nothing selected return - item = self.base.file_table.item(idx.row(), 0) + item = self.base.file_table.item(table_idx.row(), 0) self.base.on_file_table_itemClicked(item) def set_date_format(self): @@ -1215,6 +1681,407 @@ def animation(self, run): self.wait_anim.stop() +class SyncGroup(QWidget, Ui_SyncGroup): + + def __init__(self, parent=None): + """ Initializes the StatusBar + + :type parent: Base + """ + super(SyncGroup, self).__init__(parent) + self.setupUi(self) + self.base = parent + self.sync_items = [] + self.items_layout = self.items_frm.layout() + + self.idx = None + self.data = None + self.new_format = True + self.def_btn_icos = [] + self.buttons = [(self.power_btn, "Y"), + (self.sync_btn, "E"), + (self.refresh_btn, "Z")] + self.setup_buttons() + self.setup_icons() + + font = QFont() + font.setBold(True) + font.setPointSize(QFont.pointSize(QFont()) + 3) + self.title_lbl.setFont(font) + + power_color = self.base.palette().button().color().name() + self.css = 'QFrame#items_frm {background-color: "%s";}' + self.setStyleSheet(self.css % power_color) + + self.sync_pos_chk.stateChanged.connect(self.update_data) + self.merge_chk.stateChanged.connect(self.update_data) + self.sync_db_chk.stateChanged.connect(self.update_data) + + @Slot(QPoint) + def on_group_frm_customContextMenuRequested(self, point): + """ When the context menu of the SyncGroup is requested + + :type point: QPoint + :param point: The point of the click + """ + menu = QMenu(self) + if QT6: # QT6 requires exec() instead of exec_() + menu.exec_ = getattr(menu, "exec") + + action = QAction(_("Rename group"), menu) + action.setIcon(self.base.ico_file_edit) + action.triggered.connect(self.on_rename) + menu.addAction(action) + + action = QAction(_("Sync group"), menu) + action.setIcon(self.base.ico_files_merge) + action.triggered.connect(self.on_sync_btn_clicked) + menu.addAction(action) + + menu.addSeparator() + + action = QAction(_("Delete selected"), menu) + action.setIcon(self.base.ico_delete) + action.triggered.connect(self.base.toolbar.on_delete_btn_clicked) + menu.addAction(action) + + menu.exec_(self.mapToGlobal(point)) + + def on_rename(self): + """ Renames the SyncGroup + """ + title = self.title_lbl.text() + title = title if title else True + popup = self.base.popup(_("Rename SyncGroup"), + _("Enter the new name of the SyncGroup:"), + icon=QMessageBox.Question, buttons=2, + input_text=title, button_text=(_("OK"), _("Cancel"))) + if popup.buttonRole(popup.clickedButton()) == QMessageBox.AcceptRole: + text = popup.typed_text + self.title_lbl.setText(text) + self.data["title"] = text + self.update_data() + + def setup_buttons(self): + for btn, char in self.buttons: + self.def_btn_icos.append(btn.icon()) + size = btn.iconSize().toTuple() + btn.xig = XIconGlyph(self, {"family": "XFont", "size": size, "char": char}) + for item in self.sync_items: + item.setup_buttons() + + def setup_icons(self): + if self.base.theme in [THEME_NONE_NEW, THEME_DARK_NEW, THEME_LIGHT_NEW]: + # noinspection PyTypeChecker + QTimer.singleShot(0, self.set_new_icons) + else: + self.set_old_icons() + for item in self.sync_items: + item.setup_icons() + + # noinspection DuplicatedCode + def set_new_icons(self): + """ Get the font icons with the new color palette + """ + color = self.palette().text().color().name() + for btn, _ in self.buttons: + size = btn.iconSize().toTuple() + btn.xig.color = color + btn.setIcon(btn.xig.get_icon({"size": size})) + + def set_old_icons(self): + """ Reload the old icons + """ + for idx, item in enumerate(self.buttons): + btn = item[0] + btn.setIcon(self.def_btn_icos[idx]) + + @Slot(bool) + def on_power_btn_clicked(self, state): + """ Enables the Group + """ + if state: + power_color = self.base.palette().button().color().name() + else: + power_color = self.base.palette().dark().color().name() + self.setStyleSheet(self.css % power_color) + + self.data["enable"] = state + self.update_data() + + @Slot() + def on_refresh_btn_clicked(self, ): + """ The `Refresh` button is pressed + """ + items_paths = [i["path"] for i in self.data.get("items", [])] + for item in self.sync_items: + self.items_layout.removeWidget(item) + self.sync_items = [] + for path in items_paths: + self.add_item({"path": path}) + self.check_data() + + @Slot() + def on_sync_btn_clicked(self, ): + """ The `Sync this group` button is pressed + """ + if self.base.merge_warning_stop(): + return + self.base.synchronize_group(self) + + def add_item(self, data): + """ Adds a new sync item + + :type data: dict + :param data: The sync item data + """ + item = SyncItem(self.base) + item.group = self + self.sync_items.append(item) + item.idx = len(self.sync_items) - 1 + path = data["path"] + if path: + item.sync_path_txt.setText(path) + try: + data = decode_data(path) + except FileNotFoundError: # path doesn't exist + self.data["items"][item.idx]["data"] = {} + self.set_erroneous(item, _("Could not access the book's metadata file")) + except PermissionError: + self.set_erroneous(item, _("Could not access the book's metadata file")) + self.base.error(_(f"Could not access the book's metadata file\n{path}\n\n" + f"Merging this group will produce unpredictable " + f"results.")) + self.data["items"][item.idx]["data"] = {} + else: + self.data["items"][item.idx]["data"] = data + if not item.idx: + self.new_format = data.get("annotations") is not None + self.items_layout.addWidget(item) + + def remove_item(self, item): + """ Removes a sync item + + :type item: SyncItem + """ + del self.data["items"][item.idx] + self.items_layout.removeWidget(item) + self.sync_items.remove(item) + item.deleteLater() + for idx, item in enumerate(self.sync_items): + item.idx = idx + + def reset_group_height(self): + """ Reset the height of the group row + """ + height = self.sizeHint().height() + self.base.sync_table.setRowHeight(self.idx, height) + + def check_data(self): + """ Checks if the data is valid for syncing + """ + source = {"path:": "", "data": {}} + for idx, sync_item in enumerate(self.sync_items): + self.set_txt_normal(sync_item) + path = self.data["items"][idx]["path"] + if path and not isfile(path): # file doesn't exist + text = _("The path to the book's metadata file does not exist") + self.set_erroneous(sync_item, text) + if idx: + continue # check the next path + else: + return # missing source file + elif not path: + continue # empty item + + data = self.data["items"][idx]["data"] + if not idx: # source is the first item + source["path"] = path + source["data"] = data + if not data.get("cre_dom_version"): + text = _("The metadata file is of an older, not supported version.\n" + "No syncing is possible for this Sync group.") + self.set_erroneous(sync_item, text) + return + continue # check source with the rest + + # check if the data format is the same + data1_new = source["data"].get("annotations") is not None + try: + data2_new = data.get("annotations") is not None + except AttributeError: + self.set_erroneous(sync_item, + _("Could not access the book's metadata file")) + self.base.error(_(f"Could not access the book's metadata file\n{path}")) + continue + if (data1_new and not data2_new) or (data2_new and not data1_new): + text = _("The book's metadata files are in different format") + self.set_erroneous(sync_item, text) + continue + + # check if the book's md5 is the same + if not self.base.same_book(source["data"], data, source["path"], path): + text = _("The book file is different from the rest") + self.set_erroneous(sync_item, text) + continue + + # check if the books have the same cre version + if not self.base.same_cre_version(source["data"], data): + text = _("The metadata files were produced with a different version " + "of the reader engine") + self.set_erroneous(sync_item, text) + continue + + def set_txt_normal(self, item): + """ Sets the normal state of the item's text + + :type item: SyncItem + """ + item.ok = True + tooltip = _("The path to the book's metadata file") + item.sync_path_txt.setToolTip(tooltip) + item.sync_path_txt.setStatusTip(tooltip) + item.sync_path_txt.setStyleSheet(self.styleSheet()) + + def set_erroneous(self, item, tooltip=""): + """ Sets the erroneous state of the item + + :type item: SyncItem + """ + item.ok = False + item.sync_path_txt.setToolTip(tooltip) + item.sync_path_txt.setStatusTip(tooltip) + + if self.base.theme in (THEME_DARK_NEW, THEME_DARK_OLD): + color = "#DD0000" + else: + color = "#990000" + style = self.styleSheet() + 'QLineEdit {color: "%s";}' % color + item.sync_path_txt.setStyleSheet(style) + + def update_data(self): + """ Saves and updates the sync group data when something is changed + """ + if self.idx is None: # on first load on startup + return + data = {"title": self.title_lbl.text(), + "sync_pos": self.sync_pos_chk.isChecked(), + "merge": self.merge_chk.isChecked(), + "sync_db": self.sync_db_chk.isChecked(), + "items": self.data["items"], + "enabled": self.power_btn.isChecked() + } + self.base.sync_groups[self.idx] = data + self.data = data + self.base.save_sync_groups() + + +class SyncItem(QWidget, Ui_SyncItem): + + def __init__(self, parent=None): + """ Initializes the StatusBar + + :type parent: Base + """ + super(SyncItem, self).__init__(parent) + self.setupUi(self) + self.base = parent + self.group = SyncGroup(self.base) + self.def_btn_icos = [] + self.buttons = [(self.add_btn, "F"), + (self.del_btn, "J")] + self.setup_buttons() + self.setup_icons() + self.ok = True + + def setup_buttons(self): + for btn, char in self.buttons: + self.def_btn_icos.append(btn.icon()) + size = btn.iconSize().toTuple() + btn.xig = XIconGlyph(self, {"family": "XFont", "size": size, "char": char}) + + def setup_icons(self): + if self.base.theme in [THEME_NONE_NEW, THEME_DARK_NEW, THEME_LIGHT_NEW]: + # noinspection PyTypeChecker + QTimer.singleShot(0, self.set_new_icons) + else: + self.set_old_icons() + + # noinspection DuplicatedCode + def set_new_icons(self): + """ Get the font icons with the new color palette + """ + color = self.palette().text().color().name() + for btn, _ in self.buttons: + size = btn.iconSize().toTuple() + btn.xig.color = color + btn.setIcon(btn.xig.get_icon({"size": size})) + + def set_old_icons(self): + """ Reload the old icons + """ + for idx, item in enumerate(self.buttons): + btn = item[0] + btn.setIcon(self.def_btn_icos[idx]) + + @Slot() + def on_sync_path_btn_clicked(self, ): + """ The `Select` path button is pressed + """ + last_dir = self.base.last_dir + text = self.sync_path_txt.text().strip() + if text: + last_dir = dirname(text) + path = QFileDialog.getOpenFileName(self.base, _("Select the metadata file"), + last_dir, "metadata files (*.lua)")[0] + if path: + path = normpath(path) + self.base.last_dir = dirname(path) + for item in self.group.data["items"]: # check existence + if item["path"] == path: + self.base.popup(_("!"), + _("This metadata file already exists in the group!"),) + return + idx = self.group.sync_items.index(self) + self.group.data["items"][idx]["path"] = path + data = decode_data(path) + self.group.data["items"][idx]["data"] = data + if idx == 0: # first item + self.group.new_format = data.get("annotations") is not None + if not self.group.title_lbl.text().strip(): + title = data.get("doc_props", data.get("stats", {})).get("title", "") + self.group.title_lbl.setText(title) + self.sync_path_txt.setText(path) + self.group.update_data() + self.group.check_data() + + @Slot() + def on_add_btn_clicked(self, ): + """ Add a new item to the group + """ + first_item = self.group.sync_items.index(self) == 0 + if first_item and not self.sync_path_txt.text().strip(): # no first sync path + self.base.error(_("The first metadata file path must not be empty!")) + return + item_data = {"path": "", "data": {}} + self.group.add_item(item_data) + self.group.data["items"].append(item_data) + self.group.update_data() + # noinspection PyTypeChecker + QTimer.singleShot(200, self.group.reset_group_height) + + @Slot() + def on_del_btn_clicked(self, ): + """ Delete this item from the group + """ + if not self.idx: # the first item can't be deleted + self.base.error(_("Can't delete the first metadata file path!")) + return + self.group.remove_item(self) + self.group.update_data() + # noinspection PyTypeChecker + QTimer.singleShot(100, self.group.reset_group_height) + # if __name__ == "__main__": # with open("secondary.py", str("r")) as py_text: # import re diff --git a/slppu.py b/slppu.py index e72bb38..1ec1731 100644 --- a/slppu.py +++ b/slppu.py @@ -1,13 +1,5 @@ -from __future__ import print_function import re import sys -try: # ___ _______ PYTHON 2/3 COMPATIBILITY ________________________ - # noinspection PyCompatibility - basestring -except NameError: # python 3.x - # noinspection PyShadowingBuiltins - basestring, unicode, long = str, str, int -from future.utils import iteritems # https://github.com/noembryo/slppu @@ -30,13 +22,13 @@ def __init__(self): self.at = 0 self.len = 0 self.depth = 0 - self.space = re.compile('\s', re.M) - self.alnum = re.compile('\w', re.M) + self.space = re.compile(r'\s', re.M) + self.alnum = re.compile(r'\w', re.M) self.newline = '\n' self.tab = ' ' # or '\t' def decode(self, text): - if not text or not isinstance(text, basestring): + if not text or not isinstance(text, str): return # FIXME: only short comments removed reg = re.compile('--.*$', re.M) @@ -57,9 +49,9 @@ def __encode(self, obj): tab = self.tab newline = self.newline tp = type(obj) - if tp in [str, unicode]: + if tp == str: s += '"%s"' % obj.replace(r'"', r'\"') - elif tp in [int, float, long, complex]: + elif tp in [int, float, complex]: s += str(obj) elif tp is bool: s += str(obj).lower() @@ -68,16 +60,16 @@ def __encode(self, obj): elif tp in [list, tuple, dict]: self.depth += 1 if len(obj) == 0 or (tp is not dict and - len(filter(lambda x: type(x) in (int, float, long) or - (isinstance(x, basestring) and + len(filter(lambda x: type(x) in (int, float, str) or + (isinstance(x, str) and len(x) < 10), obj)) == len(obj)): newline = tab = '' dp = tab * self.depth s += "%s{%s" % (tab * (self.depth - 2), newline) if tp is dict: contents = [] - for k, v in iteritems(obj): - k = ('[{}]'.format(k) if type(k) in [int, float, long, complex] + for k, v in obj.items(): + k = ('[{}]'.format(k) if type(k) in [int, float, complex] else '["{}"]'.format(k)) contents.append(dp + '%s = %s' % (k, (self.__encode(v)))) s += (',%s' % newline).join(contents) @@ -161,8 +153,8 @@ def object(self): if k is not None: o[idx] = k if not numeric_keys and len([key for key in o - if isinstance(key, (str, unicode, float, - bool, tuple))]) == 0: + if isinstance(key, (str, float, bool, + tuple))]) == 0: ar = [] for key in o: ar.insert(key, o[key]) diff --git a/stuff/add.png b/stuff/add.png new file mode 100644 index 0000000..388f8ff Binary files /dev/null and b/stuff/add.png differ diff --git a/stuff/del.png b/stuff/del.png new file mode 100644 index 0000000..9b5b793 Binary files /dev/null and b/stuff/del.png differ diff --git a/stuff/files_add.png b/stuff/files_add.png new file mode 100644 index 0000000..e107e83 Binary files /dev/null and b/stuff/files_add.png differ diff --git a/stuff/font.ttf b/stuff/font.ttf new file mode 100644 index 0000000..3812d43 Binary files /dev/null and b/stuff/font.ttf differ diff --git a/stuff/power32gray.png b/stuff/power32gray.png new file mode 100644 index 0000000..820a4d9 Binary files /dev/null and b/stuff/power32gray.png differ diff --git a/stuff/power32red.png b/stuff/power32red.png new file mode 100644 index 0000000..275a0db Binary files /dev/null and b/stuff/power32red.png differ diff --git a/stuff/sync.png b/stuff/sync.png new file mode 100644 index 0000000..097aa46 Binary files /dev/null and b/stuff/sync.png differ diff --git a/stuff/view-sync.png b/stuff/view-sync.png new file mode 100644 index 0000000..c46e768 Binary files /dev/null and b/stuff/view-sync.png differ