diff --git a/gui_main.py b/gui_main.py index 7c25352..c6cc618 100644 --- a/gui_main.py +++ b/gui_main.py @@ -3,7 +3,7 @@ # 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: Tue May 7 14:53:08 2024 +# Created: Mon Aug 26 13:41:15 2024 # by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! @@ -339,5 +339,5 @@ def retranslateUi(self, Base): 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, XTableWidget +from secondary import XTableWidget, DropTableWidget import images_rc diff --git a/gui_status.py b/gui_status.py index 941ddbc..9388e4f 100644 --- a/gui_status.py +++ b/gui_status.py @@ -3,7 +3,7 @@ # 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 May 2 17:29:33 2024 +# Created: Mon Aug 26 13:41:16 2024 # by: pyside2-uic running on PySide2 5.13.2 # # WARNING! All changes made in this file will be lost! @@ -80,10 +80,14 @@ def retranslateUi(self, Status): 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_page.setToolTip(QtWidgets.QApplication.translate("Status", "Show the highlight\'s page number", None, -1)) self.act_date.setText(QtWidgets.QApplication.translate("Status", "Date", None, -1)) + self.act_date.setToolTip(QtWidgets.QApplication.translate("Status", "Show the highlight\'s date", None, -1)) self.act_text.setText(QtWidgets.QApplication.translate("Status", "Highlight", None, -1)) + self.act_text.setToolTip(QtWidgets.QApplication.translate("Status", "Show the highlight\'s text", None, -1)) self.act_comment.setText(QtWidgets.QApplication.translate("Status", "Comment", None, -1)) + self.act_comment.setToolTip(QtWidgets.QApplication.translate("Status", "Show the highlight\'s 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)) + self.act_chapter.setToolTip(QtWidgets.QApplication.translate("Status", "Show the highlight\'s chapter", None, -1)) import images_rc diff --git a/gui_status.ui b/gui_status.ui index b2e4ce5..e7128bc 100644 --- a/gui_status.ui +++ b/gui_status.ui @@ -114,6 +114,9 @@ what will be saved to the text/html files. Page + + Show the highlight's page number + @@ -122,6 +125,9 @@ what will be saved to the text/html files. Date + + Show the highlight's date + @@ -130,6 +136,9 @@ what will be saved to the text/html files. Highlight + + Show the highlight's text + @@ -138,6 +147,9 @@ what will be saved to the text/html files. Comment + + Show the highlight's comment + @@ -147,7 +159,7 @@ what will be saved to the text/html files. Chapter - Chapter + Show the highlight's chapter diff --git a/images.qrc b/images.qrc index e4379c0..45d44d2 100644 --- a/images.qrc +++ b/images.qrc @@ -1,43 +1,44 @@ +stuff/power32gray.png +stuff/view-sync.png +stuff/db_open.png +stuff/folder_open.png +stuff/refresh16.png +stuff/font.ttf +stuff/paypal.png +stuff/db.png +stuff/sync.png +stuff/folder_reader.png +stuff/del.png stuff/calendar.png +stuff/view_books.png stuff/file_exists.png stuff/show_pages.png -stuff/folder_reader.png +stuff/wait.gif +stuff/trash.png +stuff/power32red.png +stuff/description.png stuff/files_merge.png +stuff/delete.png stuff/logo64.png -stuff/file_edit.png -stuff/sync.png -stuff/refresh16.png -stuff/folder_open.png -stuff/trans32.png -stuff/file_save.png -stuff/power32gray.png -stuff/trash.png -stuff/books.png stuff/files_add.png -stuff/power32red.png +stuff/file_missing.png +stuff/trans32.png +stuff/paypal76.png +stuff/file_edit.png stuff/filter.png -stuff/add.png -stuff/files_delete.png -stuff/delete.png -stuff/wait.gif -stuff/db_open.png +stuff/file_save.png stuff/files_view.png -stuff/description.png -stuff/label_green.png -stuff/font.ttf +stuff/db_compact.png +stuff/copy.png stuff/logo.png -stuff/db.png -stuff/paypal76.png -stuff/paypal.png -stuff/db_add.png -stuff/sort.png -stuff/view-sync.png -stuff/view_books.png -stuff/del.png +stuff/files_delete.png +stuff/add.png stuff/view-highlights.png -stuff/copy.png -stuff/file_missing.png +stuff/sort.png +stuff/db_add.png +stuff/label_green.png +stuff/books.png \ No newline at end of file diff --git a/main.py b/main.py index b494a16..e52234e 100644 --- a/main.py +++ b/main.py @@ -44,7 +44,7 @@ __author__ = "noEmbryo" -__version__ = "2.0.4.0" +__version__ = "2.1.0.0" class Base(QMainWindow, Ui_Base): @@ -74,7 +74,8 @@ def __init__(self, parent=None): self.db_mode = False self.toolbar_size = 48 self.alt_title_sort = False - self.high_by_page = False + self.high_by_page = True + self.show_ref_pg = False self.high_merge_warning = True self.archive_warning = True self.exit_msg = True @@ -100,6 +101,7 @@ def __init__(self, parent=None): self.parent_book_data = {} self.custom_book_data = {} self.reload_highlights = True + self.reload_from_sync = False self.threads = [] self.query = None @@ -136,6 +138,7 @@ def __init__(self, parent=None): self.ico_files_delete = QIcon(":/stuff/files_delete.png") self.ico_db_add = QIcon(":/stuff/db_add.png") self.ico_db_open = QIcon(":/stuff/db_open.png") + self.ico_db_compact = QIcon(":/stuff/db_compact.png") self.ico_refresh = QIcon(":/stuff/refresh16.png") self.ico_folder_open = QIcon(":/stuff/folder_open.png") self.ico_calendar = QIcon(":/stuff/calendar.png") @@ -160,6 +163,7 @@ def __init__(self, parent=None): self.ico_file_edit, self.ico_copy, self.ico_delete, + self.ico_db_compact, ] self.ico_file_exists = QIcon(":/stuff/file_exists.png") self.ico_file_missing = QIcon(":/stuff/file_missing.png") @@ -205,24 +209,31 @@ def __init__(self, parent=None): 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.custom_btn, "Q"), (self.filter.filter_btn, "D"), - (self.description_btn, "V"), (self.filter.clear_filter_btn, "G")] - - # noinspection PyTypeChecker,PyCallByClass + 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.custom_btn, "Q"), + (self.filter.filter_btn, "D"), + (self.description_btn, "V"), + (self.filter.clear_filter_btn, "G")] + QTimer.singleShot(10000, self.auto_check4update) # check for updates main_timer = QTimer(self) # cleanup threads forever main_timer.timeout.connect(self.thread_cleanup) main_timer.start(2000) - # noinspection PyTypeChecker,PyCallByClass QTimer.singleShot(0, self.on_load) def on_load(self): @@ -231,6 +242,7 @@ def on_load(self): QFontDatabase.addApplicationFont(":/stuff/font.ttf") # QFontDatabase.removeApplicationFont(0) + self.setup_buttons() self.settings_load() self.init_db() if FIRST_RUN: # on first run @@ -248,25 +260,28 @@ 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 = _(f"Loading {APP_NAME} database") - self.loading_thread(DBLoader, self.books, text) + QTimer.singleShot(0, self.load_db_rows) + # self.load_db_rows() self.read_books_from_db() # always load db on start if self.current_view == BOOKS_VIEW: self.toolbar.books_view_btn.click() # open in Books view + elif self.current_view == SYNC_VIEW: + self.toolbar.sync_view_btn.click() # open in Sync view else: self.toolbar.high_view_btn.click() # open in Highlights view - self.setup_buttons() - self.show() - if app_config: - # noinspection PyTypeChecker,PyCallByClass QTimer.singleShot(0, self.restore_windows) else: self.resize(800, 600) - # noinspection PyTypeChecker - QTimer.singleShot(0, self.load_sync_groups) + QTimer.singleShot(0, self.show) + QTimer.singleShot(250, self.load_sync_groups) + + def load_db_rows(self): + """ Load the rows from the database + """ + self.loading_thread(DBLoader, self.books, _(f"Loading {APP_NAME} database")) def setup_buttons(self): for btn, char in self.buttons: @@ -281,7 +296,6 @@ def set_new_icons(self, menus=True): :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): @@ -311,6 +325,7 @@ def delayed_set_new_icons(self, menus=True): 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"}) + self.ico_db_compact = xig.get_icon({"char": "J"}) def set_old_icons(self): """ Reload the old icons @@ -333,6 +348,7 @@ def set_old_icons(self): self.ico_file_edit = self.def_icons[11] self.ico_copy = self.def_icons[12] self.ico_delete = self.def_icons[13] + self.ico_db_compact = self.def_icons[14] def reset_theme_colors(self): """ Resets the widget colors after a theme change @@ -453,8 +469,9 @@ def bye_bye_stuff(self): def init_db(self): """ Initialize the database tables """ - # noinspection PyTypeChecker,PyCallByClass self.db = QSqlDatabase.addDatabase("QSQLITE") + if not isfile(self.db_path): + self.db_path = join(SETTINGS_DIR, "data.db") self.db.setDatabaseName(self.db_path) if not self.db.open(): print("Could not open database!") @@ -519,7 +536,6 @@ def change_db(self, mode): self.init_db() self.read_books_from_db() if self.toolbar.db_btn.isChecked(): - # noinspection PyTypeChecker,PyCallByClass QTimer.singleShot(0, self.toolbar.update_archived) def delete_data(self): @@ -613,7 +629,7 @@ def get_db_book_count(self): def vacuum_db(self, info=True): self.query.exec_("""VACUUM""") if info: - self.popup(_("Information"), _("The database is compacted!"), + self.popup(_("Information"), _("The database has been compacted!"), QMessageBox.Information) # ___ ___________________ FILE TABLE STUFF ______________________ @@ -665,6 +681,7 @@ def on_file_table_itemClicked(self, item, reset=True): self.high_list.setCurrentRow(0) if reset else None + # noinspection PyTestUnpassedFixture def populate_book_info(self, data, row): """ Fill in the `Book Info` fields @@ -692,6 +709,14 @@ def populate_book_info(self, data, row): value = data["doc_pages"] else: # older type file value = data[stats][key] + + # no total pages if reference pages are used + annotations = data.get("annotations") # new type metadata + if self.show_ref_pg and annotations is not None and len(annotations): + annot = annotations[1] # first annotation + ref_page = annot.get("pageref") + if ref_page and ref_page.isdigit(): # there is a ref page number + value = _("|Ref|") elif key == "keywords": keywords = data["doc_props"][key].split("\n") value = "; ".join([i.rstrip("\\") for i in keywords]) @@ -839,6 +864,7 @@ def get_book_path(meta_path, data): doc_path = data.get("doc_path") if doc_path: drive = splitdrive(meta_path)[0] + # noinspection PyTestUnpassedFixture doc_path = join(drive, os.sep, *(doc_path.split("/")[3:])) if isfile(doc_path): book_path = doc_path @@ -1030,8 +1056,9 @@ def on_archive(self): def loading_thread(self, worker, args, text, clear=True): """ Populates the file_table with different contents """ - if clear and self.current_view != SYNC_VIEW: + if clear and (self.current_view != SYNC_VIEW or self.reload_from_sync): self.toolbar.on_clear_btn_clicked() + self.reload_from_sync = False self.file_table.setSortingEnabled(False) # re-enable it after populating table self.status.animation(True) @@ -1165,27 +1192,11 @@ def get_item_stats(data, filename=None): :type filename: str|unicode :param filename: The filename to get the stats for """ - stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" if filename: # stats from a file - try: - 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 + title, authors = Base.get_title_authors(data, filename) else: # stats from a db entry + stats = ("doc_props" if "doc_props" in data + else "stats" if "stats" in data else "") title = data[stats]["title"] authors = data[stats]["authors"] @@ -1214,6 +1225,36 @@ def get_item_stats(data, filename=None): return {"title": title, "authors": authors, "percent": percent, "rating": rating, "status": status, "high_count": high_count} + @staticmethod + def get_title_authors(data, filename): + """ Returns the title and authors of a metadata file + + :type data: dict + :param data: The dict converted lua file + :type filename: str|unicode + :param filename: The filename to get the stats for + """ + stats = "doc_props" if "doc_props" in data else "stats" if "stats" in data else "" + try: + 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 + return title, authors + # ___ ___________________ HIGHLIGHTS LIST STUFF _________________ def populate_high_list(self, data, path=""): @@ -1230,12 +1271,15 @@ def populate_high_list(self, data, path=""): self.status.act_date.isChecked() else "") def_date_format = self.date_format == DATE_FORMAT highlights = self.get_highlights_from_data(data, path) + new = data.get("annotations") is not None 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 "") + page = i["page"] + if new and self.show_ref_pg and i.get("ref_page"): + page = i["ref_page"] + page_text = _(f"Page {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 "" @@ -1297,11 +1341,16 @@ def get_new_highlight_info(data, idx): return # this is a bookmark not a highlight pages = data["doc_pages"] page = high_stuff.get("pageno", 0) + ref_page = high_stuff.get("pageref") + if ref_page and ref_page.isdigit(): + ref_page = int(ref_page) + else: + ref_page = None 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} + "page": page, "ref_page": ref_page, "pages": pages, "new": True} return highlight @staticmethod @@ -1647,6 +1696,17 @@ def set_highlight_sort(self): except AttributeError: # no book selected pass + def set_show_ref_pg(self): + """ Prefer reference page numbers if exists + """ + self.show_ref_pg = not self.show_ref_pg + self.reload_highlights = True + 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 @@ -1656,7 +1716,10 @@ def sort_high4view(self, data): if not self.high_by_page: return data["date"] else: - return int(data["page"]) + if data["new"] and self.show_ref_pg: + return int(data.get("ref_page", "") or data["page"]) + else: + return int(data["page"]) def sort_high4write(self, data): """ Sets the sorting method of written highlights @@ -1724,10 +1787,8 @@ def on_high_table_customContextMenuRequested(self, point): highlights, comments, values = self.get_highlights(col) - high_text = _("Copy Highlights") - com_text = _("Copy Comments") - val_text = _("Copy {} values") - if len(self.sel_high_view) == 1: # single selection + single = len(self.sel_high_view) == 1 + if single: # single selection high_text = _("Copy Highlight") com_text = _("Copy Comment") val_text = _("Copy {} value") @@ -1742,6 +1803,10 @@ def on_high_table_customContextMenuRequested(self, point): action.triggered.connect(self.on_edit_comment) action.setIcon(self.ico_file_edit) menu.addAction(action) + else: + high_text = _("Copy Highlights") + com_text = _("Copy Comments") + val_text = _("Copy {} values") action = QAction(high_text + "\tCtrl+C", menu) action.triggered.connect(partial(self.copy_text_2clip, highlights)) @@ -1753,10 +1818,11 @@ def on_high_table_customContextMenuRequested(self, point): action.setIcon(self.ico_copy) menu.addAction(action) - action = QAction(val_text.format(HIGH_COL_NAMES[col]), menu) - action.triggered.connect(partial(self.copy_text_2clip, values)) - action.setIcon(self.ico_copy) - menu.addAction(action) + if col not in [HIGHLIGHT_H, COMMENT_H]: + action = QAction(val_text.format(HIGH_COL_NAMES[col]), menu) + action.triggered.connect(partial(self.copy_text_2clip, values)) + action.setIcon(self.ico_copy) + menu.addAction(action) action = QAction(_("Export to file"), menu) action.triggered.connect(self.on_export) @@ -1865,7 +1931,10 @@ def create_highlight_row(self, data): item.setToolTip(authors) self.high_table.setItem(0, AUTHOR_H, item) - page = data["page"] + if data["new"] and self.show_ref_pg: + page = str(data.get("ref_page", "") or data["page"]) + else: + page = str(data["page"]) item = XTableWidgetIntItem(page) item.setToolTip(page) item.setTextAlignment(Qt.AlignRight) @@ -2021,7 +2090,6 @@ def create_sync_row(self, data, quiet=False): wdg.on_fold_btn_toggled(folded) wdg.check_data() self.sync_table.setSortingEnabled(True) - # noinspection PyTypeChecker QTimer.singleShot(0, self.save_sync_groups) def create_sync_widget(self, data): @@ -2498,16 +2566,15 @@ def merge_new_highs(items): 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))) + new_hi["pageno"] = 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) + percent = new_bkm["pageno"] / bkm_total + new_bkm["pageno"] = int(round(percent * total)) info[0].clear() # repopulate the annotations annots_upd = {} for i, hi in enumerate(sorted(annots, key=lambda x: int(x["pageno"]))): @@ -2904,14 +2971,13 @@ def save_multi_files(self, dir_path, format_, line_break, space): """ self.status.animation(True) saved = 0 - sort_by = self.sort_high4write for idx in self.sel_indexes: authors, title, highlights = self.get_item_data(idx, format_) if not highlights: # no highlights in book continue try: - save_file(title, authors, highlights, dir_path, - format_, line_break, space, sort_by) + save_file(title, authors, highlights, dir_path, format_, + line_break, space, self.sort_high4write) saved += 1 except IOError as err: # any problem when writing (like long filename, etc.) self.popup(_("Warning!"), _(f"Could not save the file to disk!\n{err}")) @@ -2960,56 +3026,81 @@ def get_item_data(self, index, format_): """ row = index.row() data = self.file_table.item(row, 0).data(Qt.UserRole) + args = {"page": self.status.act_page.isChecked(), + "date": self.status.act_date.isChecked(), + "text": self.status.act_text.isChecked(), + "chapter": self.status.act_chapter.isChecked(), + "comment": self.status.act_comment.isChecked(), + "ref_pg": self.show_ref_pg, + "html": format_ in [ONE_HTML, MANY_HTML], + "csv": format_ in [ONE_CSV, MANY_CSV], + } + highlights = self.get_formatted_highlights(data, args) + 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]: + authors = "" + return authors, title, highlights + + def get_formatted_highlights(self, data, args): + """ Get the highlight texts for an item + :type data: dict + :param data: The item's data + :type args: dict + :param args: The arguments for the highlight texts + """ + if not data: # no highlights + return [] highlights = [] 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_) + if not args["ref_pg"]: + highlight["page"] = str(highlight["page"]) + else: + highlight["page"] = str(highlight.get("ref_page", "") + or highlight["page"]) + # highlight["page"] = str(highlight["page"]) + if self.date_format != DATE_FORMAT: + highlight["date"] = self.get_date_text(highlight["date"]) + formatted_high = self.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.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]: - authors = "" - return authors, title, highlights + highlights.append(self.get_formatted_high(highlight, args)) + return highlights - # noinspection PyTypeChecker - def get_formatted_high(self, highlight, format_): + @staticmethod + def get_formatted_high(highlight, args): """ Create the highlight's texts :type highlight: dict :param highlight: The highlight's data - :type format_: int - :param format_ The output format idx - """ - linesep = "
" if format_ in [ONE_HTML, MANY_HTML] else os.linesep - comment = highlight["comment"].replace("\n", linesep) - chapter = (highlight["chapter"].replace("\n", linesep) - if self.status.act_chapter.isChecked() else "") - high_text = (highlight["text"].replace("\n", linesep) - if self.status.act_text.isChecked() else "") + :type args: dict + :param args: The arguments for the highlight texts + """ + line_break = "
" if args["html"] else os.linesep + chapter = (highlight["chapter"].replace("\n", line_break) + if args["chapter"] else "") + high_text = highlight["text"].replace("\n", line_break) if args["text"] else "" + comment = highlight["comment"].replace("\n", line_break) date = highlight["date"] - 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 = 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 "") + line_break2 = os.linesep if args["text"] and comment else "" + if args["csv"]: + page_text = highlight["page"] if args["page"] else "" + date_text = date if args["date"] else "" + high_comment = comment if args["comment"] and comment else "" 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 "" + page_text = "Page " + str(highlight["page"]) if args["page"] else "" + date_text = "[" + date + "]" if args["date"] else "" high_comment = (line_break2 + "● " + comment - if self.status.act_comment.isChecked() and comment else "") + if args["comment"] and comment else "") return date_text, high_comment, high_text, page_text, chapter def save_sel_highlights(self): @@ -3027,7 +3118,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("md") + md_out = extra.startswith("markdown") if text_out: ext = ".txt" text = "" @@ -3060,26 +3151,33 @@ def save_sel_highlights(self): if md_out and comment: comment = comment.replace("\n", " \n") + if data["new"] and self.show_ref_pg and data.get("ref_page"): + page = data["ref_page"] + else: + page = data["page"] + if text_out: - txt = (f"{data['title']} [{data['authors']}]\nPage {data['page']} " + txt = (f"{data['title']} [{data['authors']}]\nPage {page} " f"[{data['date']}]\n[{data['chapter']}]\n{data['text']}{comment}") text += txt + "\n\n" elif html_out: left = f"{data['title']} [{data['authors']}]" - right = f"Page {data['page']} [{data['date']}]" + right = f"Page {page} [{data['date']}]" text += HIGH_BLOCK % {"page": left, "date": right, "comment": comment, "highlight": data["text"], "chapter": data["chapter"]} text += "\n" elif csv_out: - text += get_csv_row(data) + "\n" + csv_data = data.copy() + csv_data["page"] = str(page) + text += get_csv_row(csv_data) + "\n" elif md_out: txt = data["text"].replace("\n", " \n") chapter = data["chapter"] if chapter: 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'*Page {page} [{data["date"]}]* \n' f'{chapter}{txt}{comment}\n') else: print("Unknown format export!") @@ -3129,6 +3227,7 @@ def settings_load(self): 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) + self.show_ref_pg = app_config.get("show_ref_pg", True) else: self.status.theme_box.setCurrentIndex(self.theme) if self.highlight_width: @@ -3146,6 +3245,7 @@ def restore_windows(self): # self.restoreState(self.unpickle("state")) # 2fix makes window wider (if small) self.splitter.restoreState(self.unpickle("splitter")) self.header_main.restoreState(self.unpickle("header_main")) + self.header_high_view.restoreState(self.unpickle("header_high_view")) self.filter.restoreGeometry(self.unpickle("filter_geometry")) self.about.restoreGeometry(self.unpickle("about_geometry")) QTimer.singleShot(200, self.get_header_width) @@ -3165,6 +3265,7 @@ def settings_save(self): "state": self.pickle(self.saveState()), "splitter": self.pickle(self.splitter.saveState()), "header_main": self.pickle(self.header_main.saveState()), + "header_high_view": self.pickle(self.header_high_view.saveState()), "filter_geometry": self.pickle(self.filter.saveGeometry()), "about_geometry": self.pickle(self.about.saveGeometry()), "col_sort_asc": self.col_sort_asc, "col_sort": self.col_sort, @@ -3174,7 +3275,8 @@ def settings_save(self): "last_dir": self.last_dir, "alt_title_sort": self.alt_title_sort, "archive_warning": self.archive_warning, "exit_msg": self.exit_msg, "current_view": self.current_view, "db_mode": self.db_mode, - "high_by_page": self.high_by_page, "date_vacuumed": self.date_vacuumed, + "high_by_page": self.high_by_page, "show_ref_pg": self.show_ref_pg, + "date_vacuumed": self.date_vacuumed, "show_info": self.fold_btn.isChecked(), "date_format": self.date_format, "theme": self.theme, "show_items": self.show_items, "skip_version": self.skip_version, "opened_times": self.opened_times, @@ -3183,16 +3285,17 @@ def settings_save(self): } try: for k, v in config.items(): - if type(v) == bytes: - # noinspection PyArgumentList - config[k] = str(v, encoding="latin") + + if isinstance(v, bytes): + try: + config[k] = v.decode("latin1") + except UnicodeDecodeError as e: + print(f"Error decoding bytes to string: {e}") + config[k] = v.decode("latin1", errors="ignore") config_json = json.dumps(config, sort_keys=True, indent=4) with gzip.GzipFile(join(SETTINGS_DIR, str("settings.json.gz")), "w+") as gz_file: - try: - gz_file.write(config_json) - except TypeError: # Python3 - gz_file.write(config_json.encode("utf8")) + gz_file.write(config_json.encode("utf8")) except IOError as error: print("On saving settings:", error) @@ -3255,11 +3358,11 @@ def popup(self, title, text, icon=QMessageBox.Warning, buttons=1, """ popup = XMessageBox(self) popup.setWindowIcon(self.ico_app) - if type(icon) == QMessageBox.Icon: + if isinstance(icon, QMessageBox.Icon): popup.setIcon(icon) - elif type(icon) == str: + elif isinstance(icon, str): popup.setIconPixmap(QPixmap(icon)) - elif type(icon) == QPixmap: + elif isinstance(icon, QPixmap): popup.setIconPixmap(icon) else: raise TypeError("Wrong icon type!") @@ -3291,12 +3394,10 @@ def error(self, error_txt): def passed_files(self): """ Command line parameters that are passed to the program. """ - # args = QApplication.instance().arguments() - try: - if sys.argv[1]: - self.on_file_table_fileDropped(sys.argv[1:]) - except IndexError: - pass + if len(sys.argv) > 1 and sys.argv[1].endswith("Portable.exe"): + del sys.argv[1] + if len(sys.argv) > 1: + self.on_file_table_fileDropped(sys.argv[1:]) def open_file(self, path): """ Opens a file with its associated app @@ -3605,6 +3706,8 @@ def parse_args(self): default=False) self.parser.add_argument("-s", "--sort_page", action="store_true", default=False, help="Sort highlights by page, otherwise sort by date") + self.parser.add_argument("-r", "--ref_page", action="store_true", default=True, + help="Use reference page numbers if they exist") self.parser.add_argument("-m", "--merge", action="store_true", default=False, help="Merge the highlights of all input books in a " "single file, otherwise exports every book's " @@ -3777,68 +3880,19 @@ def cli_get_item_data(self, file_, args): :param args: The item's arguments """ data = decode_data(file_) - highlights = [] - - 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"] - except KeyError: # older type file - title = splitext(basename(file_))[0] - try: - name = title.split("#] ")[1] - title = splitext(name)[0] - except IndexError: # no "#] " in filename - pass - if not title: - try: - name = file_.split("#] ")[1] - title = splitext(name)[0] - except IndexError: # no "#] " in filename - title = NO_TITLE + args_ = {"page": not args.no_page, + "date": not args.no_date, + "text": not args.no_highlight, + "chapter": not args.no_chapter, + "comment": not args.no_comment, + "ref_pg": args.ref_page, + "html": args.html, + "csv": args.csv, + } + highlights = self.base.get_formatted_highlights(data, args_) + title, authors = self.base.get_title_authors(data, file_) return authors, title, highlights - @staticmethod - def cli_get_formatted_high(highlight, args): - """ Return the highlight's info in a formatted way - - :type highlight: dict - :param highlight: The highlight's data - :type args: argparse.Namespace - :param args: The parsed cli args - """ - nl = "
" if args.html else os.linesep - chapter = highlight["chapter"].replace("\n", nl) if not args.no_chapter else "" - high_text = highlight["text"] - high_text = high_text.replace("\n", nl) if not args.no_highlight else "" - comment = highlight["comment"].replace("\n", nl) - date = highlight["date"] - line_break2 = os.linesep if not args.no_highlight and comment else "" - if args.csv: - 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 " + 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 "") - return date_text, high_comment, high_text, page_text, chapter - @staticmethod def get_lua_files(dropped): """ Return the paths to the .lua metadata files @@ -3900,41 +3954,6 @@ def cli_sort(args, data): else: return data[0] - @staticmethod - def get_name(data, meta_path, title_counter): - """ Return the name of the book entry - - :type data: dict - :param data: The book's metadata - :type meta_path: str|unicode - :param meta_path: The book's metadata path - :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"] - except KeyError: # older type file - title = splitext(basename(meta_path))[0] - try: - name = title.split("#] ")[1] - title = splitext(name)[0] - except IndexError: # no "#] " in filename - pass - if not title: - try: - name = meta_path.split("#] ")[1] - title = splitext(name)[0] - except IndexError: # no "#] " in filename - title = NO_TITLE + str(title_counter[0]) - title_counter[0] += 1 - name = title - if authors: - name = f"{authors} - {title}" - return name - if __name__ == "__main__": app = KOHighlights(sys.argv) diff --git a/secondary.py b/secondary.py index 9e3257a..08ea974 100644 --- a/secondary.py +++ b/secondary.py @@ -1,4 +1,7 @@ # coding=utf-8 +from copy import deepcopy +from pprint import pprint + from boot_config import * from boot_config import _ import re @@ -41,7 +44,7 @@ def decode_data(path): with open(path, "r", encoding="utf8", newline="\n") as txt_file: header, data = txt_file.read().split("\n", 1) data = lua.decode(data[7:].replace("--", "—")) - if type(data) == dict: + if type(data) is dict: data["original_header"] = header return data @@ -879,6 +882,7 @@ def __init__(self, parent=None): # self.size_menu.aboutToShow.connect(self.create_size_menu) self.db_menu = QMenu() + self.db_menu.setToolTipsVisible(True) self.db_menu.aboutToShow.connect(self.create_db_menu) self.db_btn.setMenu(self.db_menu) @@ -1050,7 +1054,7 @@ def on_delete_btn_clicked(self): def on_clear_btn_clicked(self): """ The `Clear List` button is pressed """ - if self.base.current_view == SYNC_VIEW: + if self.base.current_view == SYNC_VIEW and not self.base.reload_from_sync: return self.base.loaded_paths.clear() self.base.reload_highlights = True @@ -1071,18 +1075,28 @@ def create_db_menu(self): self.db_menu.clear() action = QAction(_("Create new database"), self.db_menu) action.setIcon(self.base.ico_db_add) + action.setToolTip(_("Create a new database file")) action.triggered.connect(partial(self.base.change_db, NEW_DB)) self.db_menu.addAction(action) action = QAction(_("Reload database"), self.db_menu) action.setIcon(self.base.ico_refresh) + action.setToolTip(_("Reload the current database")) action.triggered.connect(partial(self.base.change_db, RELOAD_DB)) self.db_menu.addAction(action) action = QAction(_("Change database"), self.db_menu) action.setIcon(self.base.ico_db_open) + action.setToolTip(_("Load a different database file")) action.triggered.connect(partial(self.base.change_db, CHANGE_DB)) self.db_menu.addAction(action) + + action = QAction(_("Compact database"), self.db_menu) + action.setIcon(self.base.ico_db_compact) + action.setToolTip(_("Compact the database to minimize file size")) + action.triggered.connect(partial(self.base.vacuum_db, True)) + self.db_menu.addAction(action) + if QT6: # QT6 requires exec() instead of exec_() self.db_menu.exec_ = getattr(self.db_menu, "exec") @@ -1090,7 +1104,8 @@ def change_view(self): """ Changes what is shown in the app """ reloaded = False - if not self.sync_view_btn.isChecked(): # don't reload when coming from Sync view + if self.base.reload_from_sync or not self.sync_view_btn.isChecked(): + self.base.reload_from_sync = False reloaded = (self.update_archived() if self.db_btn.isChecked() else self.update_loaded()) if self.books_view_btn.isChecked(): # Books view @@ -1134,8 +1149,9 @@ def update_archived(self): self.base.db_mode = True self.base.reload_highlights = True self.base.read_books_from_db() - text = _(f"Loading {APP_NAME} database") - self.base.loading_thread(DBLoader, self.base.books, text) + self.base.load_db_rows() + # 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/' 'update one or more books, select them in the "Loaded" ' @@ -1553,6 +1569,7 @@ def __init__(self, parent=None): act.triggered.connect(partial(self.on_show_items, act)) self.show_menu = QMenu(self) + self.show_menu.setToolTipsVisible(True) self.show_menu.aboutToShow.connect(self.get_show_menu) self.show_items_btn.setMenu(self.show_menu) @@ -1564,14 +1581,26 @@ def get_show_menu(self): act.setChecked(self.base.show_items[idx]) self.show_menu.addAction(act) + self.show_menu.addSeparator() + self.show_menu.addSeparator() + self.show_menu.addSeparator() + action = QAction(_("Date Format"), self.show_menu) action.setIcon(self.base.ico_calendar) action.triggered.connect(self.set_date_format) + action.setToolTip(_("Change the way the date is displayed")) + self.show_menu.addAction(action) + + action = QAction(_("Reference page numbers"), self.show_menu) + action.setCheckable(True) + action.setChecked(self.base.show_ref_pg) + action.triggered.connect(self.base.set_show_ref_pg) + action.setToolTip(_("Use reference page numbers\nif they are available")) self.show_menu.addAction(action) sort_menu = QMenu(self) sort_menu.setIcon(self.base.ico_sort) - sort_menu.setTitle(_("Sort by")) + sort_menu.setTitle(_("Sort Highlights by")) group = QActionGroup(self) action = QAction(_("Date"), sort_menu) @@ -1723,23 +1752,39 @@ def on_group_frm_customContextMenuRequested(self, point): :param point: The point of the click """ menu = QMenu(self) + menu.setToolTipsVisible(True) 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.setToolTip(_("Change the name of the Sync group")) action.triggered.connect(self.on_rename) menu.addAction(action) action = QAction(_("Sync group"), menu) action.setIcon(self.base.ico_files_merge) + action.setToolTip(_("Sync the Sync group items")) action.triggered.connect(self.on_sync_btn_clicked) menu.addAction(action) + action = QAction(_("Load group items"), menu) + action.setIcon(self.base.ico_folder_open) + action.setToolTip(_("Load the Sync group items to the Books View")) + action.triggered.connect(self.load_group_items) + menu.addAction(action) + + action = QAction(_("Copy Archived to group"), menu) + action.setIcon(self.base.ico_files_merge) + action.setToolTip(_("Copy the archived version to all Sync group items")) + action.triggered.connect(self.archived_to_group) + menu.addAction(action) + menu.addSeparator() action = QAction(_("Delete selected"), menu) action.setIcon(self.base.ico_delete) + action.setToolTip(_("Delete the selected Sync group items")) action.triggered.connect(self.base.toolbar.on_delete_btn_clicked) menu.addAction(action) @@ -1770,7 +1815,6 @@ def setup_buttons(self): 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() @@ -1991,6 +2035,74 @@ def update_data(self): self.data = data self.base.save_sync_groups() + def load_group_items(self): + """ Loads the group's items in the Books view + """ + self.base.toolbar.reload_from_sync = True + self.base.toolbar.books_view_btn.setChecked(True) + self.base.toolbar.change_view() + self.base.toolbar.loaded_btn.click() + self.base.on_file_table_fileDropped([item["path"] for item in self.data["items"]]) + + def archived_to_group(self): + """ Copies the archived data to all the items of the group + """ + path = self.data["items"][0]["path"] + data = decode_data(path) + idx = self.base.check4archive_merge({"path": path, "data": data}) + no_archive_txt = _("Could not find an archived version of the book's metadata") + if idx is False: + self.base.popup(_("Error"), no_archive_txt, icon=QMessageBox.Critical) + return + old_format_txt = _("The metadata file is of an older, not supported version.") + if not self.new_format: + self.base.popup(_("Error"), old_format_txt, icon=QMessageBox.Critical) + return + warn_txt = _("All the Group versions will be overwritten with the archived " + "version!\n\nAre you sure you want to continue?") + popup = self.base.popup(_("Warning"), warn_txt, icon=QMessageBox.Warning, + buttons=2, button_text=(_("Yes"), _("Cancel"))) + if popup.buttonRole(popup.clickedButton()) == QMessageBox.RejectRole: + return + + arch_data = self.base.books[idx]["data"] # archived data + arch_total = arch_data.get("doc_pages", + arch_data.get("stats", {}).get("pages", 0)) + for item in self.data["items"]: + item_data = item["data"] + item_total = item_data.get("doc_pages", + item_data.get("stats", {}).get("pages", 0)) + item_data["annotations"] = deepcopy(arch_data["annotations"]) + if arch_total != item_total: + self.recalculate_pages(item_data, item_total, arch_total) + item_data["annotations_externally_modified"] = True + self.base.save_book_data(item["path"], item_data) + + self.base.reload_from_sync = True + if not self.base.db_mode: + self.base.db_mode = True # need this to trigger the reload of the files + self.base.books2reload = self.base.loaded_paths.copy() + self.base.toolbar.update_loaded() + + self.base.popup(_("Info"), _("The metadata was successfully updated!"), + icon=QMessageBox.Information) + + @staticmethod + def recalculate_pages(item_data, item_total, arch_total): + """ Recalculates the page number of the annotations + based on the total pages number + + :type item_data: dict + :param item_data: The item's data + :type item_total: int + :param item_total: The total number of pages of the item + :type arch_total: int + :param arch_total: The total number of pages of the archived item + """ + for annot in item_data["annotations"].values(): + percent = int(annot["pageno"]) / arch_total + annot["pageno"] = int(round(percent * item_total)) + class SyncItem(QWidget, Ui_SyncItem): @@ -2004,8 +2116,8 @@ def __init__(self, parent=None): self.base = parent self.group = SyncGroup(self.base) self.def_btn_icos = [] - self.buttons = [(self.add_btn, "F"), - (self.del_btn, "J")] + self.buttons = [(self.add_btn, "+"), + (self.del_btn, "-")] self.setup_buttons() self.setup_icons() self.ok = True @@ -2018,7 +2130,6 @@ def setup_buttons(self): 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() @@ -2083,7 +2194,6 @@ def on_add_btn_clicked(self, ): 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() @@ -2095,7 +2205,6 @@ def on_del_btn_clicked(self, ): return self.group.remove_item(self) self.group.update_data() - # noinspection PyTypeChecker QTimer.singleShot(100, self.group.reset_group_height) # if __name__ == "__main__": diff --git a/stuff/font.ttf b/stuff/font.ttf index 3812d43..3a8944b 100644 Binary files a/stuff/font.ttf and b/stuff/font.ttf differ