From eedf347866c24729c0b875af4b9f8b9c69bafe1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Mon, 27 Oct 2025 17:08:32 +0300 Subject: [PATCH] Upgraded Abogen's interface from PyQt5 to PyQt6, Added tooltip indicators in queue manager --- CHANGELOG.md | 2 + abogen/Dockerfile | 2 +- abogen/book_handler.py | 252 +++++++++++++++++++----------------- abogen/constants.py | 1 + abogen/conversion.py | 12 +- abogen/gui.py | 207 +++++++++++++++-------------- abogen/main.py | 30 +++-- abogen/queue_manager_gui.py | 110 +++++++++------- abogen/queued_item.py | 2 + abogen/voice_formula_gui.py | 126 +++++++++--------- pyproject.toml | 2 +- 11 files changed, 406 insertions(+), 340 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf96bd7..68ce173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # 1.2.1 (pre-release) +- Upgraded Abogen's interface from PyQt5 to PyQt6 for better compatibility and long-term support. +- Added tooltip indicators in queue manager to display book handler options (`Save chapters separately` and `Merge chapters at the end`) for queued items. - Added loading gif animation to book handler window. - Fixed subtitle word-count splitting logic for more accurate segmentation. diff --git a/abogen/Dockerfile b/abogen/Dockerfile index af76df7..f141c4f 100644 --- a/abogen/Dockerfile +++ b/abogen/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update \ python3 \ python3-venv \ python3-pip \ - python3-pyqt5 \ + python3-pyqt6 \ espeak-ng \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/abogen/book_handler.py b/abogen/book_handler.py index 4d7ca8e..67ac0cd 100644 --- a/abogen/book_handler.py +++ b/abogen/book_handler.py @@ -4,24 +4,34 @@ import base64 import fitz # PyMuPDF for PDF support from ebooklib import epub from bs4 import BeautifulSoup, NavigableString -from PyQt5.QtWidgets import ( +from PyQt6.QtGui import QAction, QMovie, QFont +from PyQt6.QtWidgets import ( + QApplication, QDialog, QTreeWidget, QTreeWidgetItem, - QDialogButtonBox, + QTextEdit, + QPushButton, QVBoxLayout, QHBoxLayout, - QTextEdit, - QTreeWidgetItemIterator, + QDialogButtonBox, QSplitter, QWidget, - QPushButton, QCheckBox, - QMenu, + QTreeWidgetItemIterator, QLabel, + QMenu, +) +from PyQt6.QtCore import ( + Qt, + QThread, + pyqtSignal, + QObject, + QSize, + QEvent, + QPoint, + QRect, ) -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize -from PyQt5.QtGui import QMovie from abogen.utils import clean_text, calculate_text_length, detect_encoding, get_resource_path import os import logging # Add logging @@ -97,9 +107,9 @@ class HandlerDialog(QDialog): self._block_signals = False # Flag to prevent recursive signals # Configure window: remove help button and allow resizing self.setWindowFlags( - Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint + Qt.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMaximizeButtonHint ) - self.setWindowModality(Qt.NonModal) + self.setWindowModality(Qt.WindowModality.NonModal) # Initialize save chapters flags from class variables self.save_chapters_separately = HandlerDialog._save_chapters_separately self.merge_chapters_at_end = HandlerDialog._merge_chapters_at_end @@ -164,8 +174,8 @@ class HandlerDialog(QDialog): # Build treeview self.treeWidget = QTreeWidget(self) self.treeWidget.setHeaderHidden(True) - self.treeWidget.setSelectionMode(QTreeWidget.ExtendedSelection) - self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) + self.treeWidget.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection) + self.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.treeWidget.customContextMenuRequested.connect(self.on_tree_context_menu) # Initialize checked_chapters set @@ -177,8 +187,8 @@ class HandlerDialog(QDialog): # Add a placeholder "Information" item so the tree isn't empty immediately info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) - info_item.setData(0, Qt.UserRole, "info:bookinfo") - info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable) + info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo") + info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) font = info_item.font(0) font.setBold(True) info_item.setFont(0, font) @@ -243,8 +253,8 @@ class HandlerDialog(QDialog): # Add stretches to center the content horizontally h.addStretch(1) - h.addWidget(gif_label, 0, Qt.AlignVCenter) - h.addWidget(text_label, 0, Qt.AlignVCenter) + h.addWidget(gif_label, 0, Qt.AlignmentFlag.AlignVCenter) + h.addWidget(text_label, 0, Qt.AlignmentFlag.AlignVCenter) h.addStretch(1) # Insert at top of main layout if present, otherwise keep as child @@ -1148,8 +1158,8 @@ class HandlerDialog(QDialog): self.treeWidget.clear() info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) - info_item.setData(0, Qt.UserRole, "info:bookinfo") - info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable) + info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo") + info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) font = info_item.font(0) font.setBold(True) info_item.setFont(0, font) @@ -1172,7 +1182,7 @@ class HandlerDialog(QDialog): has_parents = False iterator = QTreeWidgetItemIterator( - self.treeWidget, QTreeWidgetItemIterator.HasChildren + self.treeWidget, QTreeWidgetItemIterator.IteratorFlag.HasChildren ) if iterator.value(): has_parents = True @@ -1195,7 +1205,7 @@ class HandlerDialog(QDialog): children = node.get("children", []) item = QTreeWidgetItem(parent_item, [title]) - item.setData(0, Qt.UserRole, src) + item.setData(0, Qt.ItemDataRole.UserRole, src) is_empty = ( src @@ -1211,18 +1221,18 @@ class HandlerDialog(QDialog): seen_content_hashes.add(content_hash) if src and not is_empty and not is_duplicate: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) is_checked = src in self.checked_chapters - item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + item.setCheckState(0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) elif is_duplicate: # Mark as duplicate and remove checkbox item.setText(0, f"{title} (Duplicate)") - item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) elif children: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) - item.setCheckState(0, Qt.Unchecked) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(0, Qt.CheckState.Unchecked) else: - item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) if children: self._build_epub_tree_from_nav(children, item, seen_content_hashes) @@ -1252,18 +1262,18 @@ class HandlerDialog(QDialog): continue item = QTreeWidgetItem(parent_item, [title]) - item.setData(0, Qt.UserRole, href) + item.setData(0, Qt.ItemDataRole.UserRole, href) has_content = ( href and href in self.content_texts and self.content_texts[href].strip() ) if has_content or children: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) is_checked = href and href in self.checked_chapters - item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + item.setCheckState(0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) else: - item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) if children: self._build_epub_tree_fallback(children, item) @@ -1318,27 +1328,27 @@ class HandlerDialog(QDialog): if page_num in self.bookmark_items_map: orig = self.bookmark_items_map[page_num] child = QTreeWidgetItem(orig, [f"{title} (Same page)"]) - child.setData(0, Qt.UserRole, page_id) - child.setFlags(child.flags() & ~Qt.ItemIsUserCheckable) + child.setData(0, Qt.ItemDataRole.UserRole, page_id) + child.setFlags(child.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) continue bookmark_item = QTreeWidgetItem(parent_item, [title]) - bookmark_item.setData(0, Qt.UserRole, page_id) + bookmark_item.setData(0, Qt.ItemDataRole.UserRole, page_id) # only allow checking if this chapter has content if self.content_lengths.get(page_id, 0) > 0: bookmark_item.setFlags( - bookmark_item.flags() | Qt.ItemIsUserCheckable + bookmark_item.flags() | Qt.ItemFlag.ItemIsUserCheckable ) bookmark_item.setCheckState( 0, ( - Qt.Checked + Qt.CheckState.Checked if page_id in self.checked_chapters - else Qt.Unchecked + else Qt.CheckState.Unchecked ), ) else: bookmark_item.setFlags( - bookmark_item.flags() & ~Qt.ItemIsUserCheckable + bookmark_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable ) # map for uncategorized pages self.bookmark_items_map[page_num] = bookmark_item @@ -1363,23 +1373,23 @@ class HandlerDialog(QDialog): page_title += f" - {first_line}" page_item = QTreeWidgetItem(bookmark_item, [page_title]) - page_item.setData(0, Qt.UserRole, page_id) + page_item.setData(0, Qt.ItemDataRole.UserRole, page_id) # only allow checking if this sub-page has content if self.content_lengths.get(page_id, 0) > 0: page_item.setFlags( - page_item.flags() | Qt.ItemIsUserCheckable + page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable ) page_item.setCheckState( 0, ( - Qt.Checked + Qt.CheckState.Checked if page_id in self.checked_chapters - else Qt.Unchecked + else Qt.CheckState.Unchecked ), ) else: page_item.setFlags( - page_item.flags() & ~Qt.ItemIsUserCheckable + page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable ) added_pages.add(sub_page_num) @@ -1405,15 +1415,15 @@ class HandlerDialog(QDialog): if first and len(first) < 100: title += f" - {first}" page_item = QTreeWidgetItem(parent_item, [title]) - page_item.setData(0, Qt.UserRole, page_id) + page_item.setData(0, Qt.ItemDataRole.UserRole, page_id) # only allow checking if uncategorized page has content if self.content_lengths.get(page_id, 0) > 0: - page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable) + page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable) page_item.setCheckState( - 0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked + 0, Qt.CheckState.Checked if page_id in self.checked_chapters else Qt.CheckState.Unchecked ) else: - page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable) + page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) def _build_markdown_tree(self): """Build tree structure for markdown file based on parsed TOC.""" @@ -1426,13 +1436,13 @@ class HandlerDialog(QDialog): chapter_id = list(self.content_texts.keys())[0] title = "Content" item = QTreeWidgetItem(self.treeWidget, [title]) - item.setData(0, Qt.UserRole, chapter_id) + item.setData(0, Qt.ItemDataRole.UserRole, chapter_id) if self.content_lengths.get(chapter_id, 0) > 0: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) is_checked = chapter_id in self.checked_chapters - item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + item.setCheckState(0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) else: - item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) return def build_from_toc(toc_list, parent_item): @@ -1441,17 +1451,17 @@ class HandlerDialog(QDialog): chapter_id = header['id'] item = QTreeWidgetItem(parent_item, [title]) - item.setData(0, Qt.UserRole, chapter_id) + item.setData(0, Qt.ItemDataRole.UserRole, chapter_id) has_content = self.content_lengths.get(chapter_id, 0) > 0 has_children = bool(header.get('children')) if has_content or has_children: - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) is_checked = chapter_id in self.checked_chapters - item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked) + item.setCheckState(0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) else: - item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) if has_children: build_from_toc(header['children'], item) @@ -1460,7 +1470,7 @@ class HandlerDialog(QDialog): def _build_pdf_pages_tree(self): pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"]) - pages_item.setFlags(pages_item.flags() & ~Qt.ItemIsUserCheckable) + pages_item.setFlags(pages_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) font = pages_item.font(0) font.setBold(True) pages_item.setFont(0, font) @@ -1476,15 +1486,15 @@ class HandlerDialog(QDialog): page_title += f" - {first_line}" page_item = QTreeWidgetItem(pages_item, [page_title]) - page_item.setData(0, Qt.UserRole, page_id) + page_item.setData(0, Qt.ItemDataRole.UserRole, page_id) # only allow checking if standalone page has content if self.content_lengths.get(page_id, 0) > 0: - page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable) + page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable) page_item.setCheckState( - 0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked + 0, Qt.CheckState.Checked if page_id in self.checked_chapters else Qt.CheckState.Unchecked ) else: - page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable) + page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) def _are_provided_checks_relevant(self): if not self.checked_chapters: @@ -1494,8 +1504,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.flags() & Qt.ItemIsUserCheckable: - identifier = item.data(0, Qt.UserRole) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: + identifier = item.data(0, Qt.ItemDataRole.UserRole) if identifier: all_identifiers.add(identifier) iterator += 1 @@ -1525,7 +1535,7 @@ class HandlerDialog(QDialog): rightWidget = QWidget() rightWidget.setLayout(previewLayout) - buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) @@ -1611,7 +1621,7 @@ class HandlerDialog(QDialog): leftWidget = QWidget() leftWidget.setLayout(leftLayout) - self.splitter = QSplitter(Qt.Horizontal) + self.splitter = QSplitter(Qt.Orientation.Horizontal) self.splitter.addWidget(leftWidget) self.splitter.addWidget(rightWidget) self.splitter.setSizes([280, 420]) @@ -1643,8 +1653,8 @@ class HandlerDialog(QDialog): while iterator.value(): item = iterator.value() if ( - item.flags() & Qt.ItemIsUserCheckable - and item.checkState(0) == Qt.Checked + item.flags() & Qt.ItemFlag.ItemIsUserCheckable + and item.checkState(0) == Qt.CheckState.Checked ): checked_count += 1 if checked_count >= 2: @@ -1658,8 +1668,8 @@ class HandlerDialog(QDialog): while iterator.value(): item = iterator.value() if ( - item.flags() & Qt.ItemIsUserCheckable - and item.checkState(0) == Qt.Checked + item.flags() & Qt.ItemFlag.ItemIsUserCheckable + and item.checkState(0) == Qt.CheckState.Checked ): parent = item.parent() if parent and parent != self.treeWidget.invisibleRootItem(): @@ -1683,8 +1693,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.flags() & Qt.ItemIsUserCheckable: - item.setCheckState(0, Qt.Checked) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: + item.setCheckState(0, Qt.CheckState.Checked) iterator += 1 self._block_signals = False self._update_checked_set_from_tree() @@ -1694,8 +1704,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.flags() & Qt.ItemIsUserCheckable: - item.setCheckState(0, Qt.Unchecked) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: + item.setCheckState(0, Qt.CheckState.Unchecked) iterator += 1 self._block_signals = False self._update_checked_set_from_tree() @@ -1705,8 +1715,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.flags() & Qt.ItemIsUserCheckable and item.childCount() > 0: - item.setCheckState(0, Qt.Checked) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0: + item.setCheckState(0, Qt.CheckState.Checked) iterator += 1 self._block_signals = False self._update_checked_set_from_tree() @@ -1716,8 +1726,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.flags() & Qt.ItemIsUserCheckable and item.childCount() > 0: - item.setCheckState(0, Qt.Unchecked) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0: + item.setCheckState(0, Qt.CheckState.Unchecked) iterator += 1 self._block_signals = False self._update_checked_set_from_tree() @@ -1742,30 +1752,30 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if not (item.flags() & Qt.ItemIsUserCheckable): + if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable): iterator += 1 continue - src = item.data(0, Qt.UserRole) + src = item.data(0, Qt.ItemDataRole.UserRole) has_significant_content = src and self.content_lengths.get(src, 0) > 1000 is_parent = item.childCount() > 0 if has_significant_content or is_parent: - item.setCheckState(0, Qt.Checked) + item.setCheckState(0, Qt.CheckState.Checked) if is_parent: for i in range(item.childCount()): child = item.child(i) - if child.flags() & Qt.ItemIsUserCheckable: - child_src = child.data(0, Qt.UserRole) + if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: + child_src = child.data(0, Qt.ItemDataRole.UserRole) child_has_content = ( child_src and self.content_lengths.get(child_src, 0) > 0 ) child_is_parent = child.childCount() > 0 if child_has_content or child_is_parent: - child.setCheckState(0, Qt.Checked) + child.setCheckState(0, Qt.CheckState.Checked) else: - item.setCheckState(0, Qt.Unchecked) + item.setCheckState(0, Qt.CheckState.Unchecked) iterator += 1 @@ -1774,32 +1784,32 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if not (item.flags() & Qt.ItemIsUserCheckable): + if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable): iterator += 1 continue - identifier = item.data(0, Qt.UserRole) + identifier = item.data(0, Qt.ItemDataRole.UserRole) # Select chapters with content > 500 characters or parent items has_significant_content = identifier and self.content_lengths.get(identifier, 0) > 500 is_parent = item.childCount() > 0 if has_significant_content or is_parent: - item.setCheckState(0, Qt.Checked) + item.setCheckState(0, Qt.CheckState.Checked) # Also check children if this is a parent if is_parent: for i in range(item.childCount()): child = item.child(i) - if child.flags() & Qt.ItemIsUserCheckable: - child_identifier = child.data(0, Qt.UserRole) + if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: + child_identifier = child.data(0, Qt.ItemDataRole.UserRole) child_has_content = ( child_identifier and self.content_lengths.get(child_identifier, 0) > 0 ) child_is_parent = child.childCount() > 0 if child_has_content or child_is_parent: - child.setCheckState(0, Qt.Checked) + child.setCheckState(0, Qt.CheckState.Checked) else: - item.setCheckState(0, Qt.Unchecked) + item.setCheckState(0, Qt.CheckState.Unchecked) iterator += 1 @@ -1808,19 +1818,19 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.flags() & Qt.ItemIsUserCheckable: - item.setCheckState(0, Qt.Checked) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: + item.setCheckState(0, Qt.CheckState.Checked) iterator += 1 return iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if not (item.flags() & Qt.ItemIsUserCheckable): + if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable): iterator += 1 continue - identifier = item.data(0, Qt.UserRole) + identifier = item.data(0, Qt.ItemDataRole.UserRole) if not identifier: iterator += 1 @@ -1830,7 +1840,7 @@ class HandlerDialog(QDialog): not identifier.startswith("page_") or self.content_lengths.get(identifier, 0) > 0 ): - item.setCheckState(0, Qt.Checked) + item.setCheckState(0, Qt.CheckState.Checked) iterator += 1 @@ -1839,8 +1849,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.checkState(0) == Qt.Checked: - identifier = item.data(0, Qt.UserRole) + if item.checkState(0) == Qt.CheckState.Checked: + identifier = item.data(0, Qt.ItemDataRole.UserRole) if identifier: self.checked_chapters.add(identifier) iterator += 1 @@ -1853,17 +1863,17 @@ class HandlerDialog(QDialog): self._block_signals = True - if item.flags() & Qt.ItemIsUserCheckable: + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: for i in range(item.childCount()): child = item.child(i) - if child.flags() & Qt.ItemIsUserCheckable: + if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: child.setCheckState(0, item.checkState(0)) self._block_signals = False self._update_checked_set_from_tree() def handle_item_double_click(self, item, column=0): - if item.flags() & Qt.ItemIsUserCheckable and item.childCount() == 0: + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() == 0: rect = self.treeWidget.visualItemRect(item) checkbox_width = 20 @@ -1871,7 +1881,7 @@ class HandlerDialog(QDialog): if mouse_pos.x() > rect.x() + checkbox_width: new_state = ( - Qt.Unchecked if item.checkState(0) == Qt.Checked else Qt.Checked + Qt.CheckState.Unchecked if item.checkState(0) == Qt.CheckState.Checked else Qt.CheckState.Checked ) item.setCheckState(0, new_state) @@ -1880,7 +1890,7 @@ class HandlerDialog(QDialog): self.previewEdit.clear() return - identifier = current.data(0, Qt.UserRole) + identifier = current.data(0, Qt.ItemDataRole.UserRole) if identifier == "info:bookinfo": self._display_book_info() @@ -2172,8 +2182,8 @@ class HandlerDialog(QDialog): while iterator.value(): item = iterator.value() item_order_counter += 1 - if item.checkState(0) == Qt.Checked: - identifier = item.data(0, Qt.UserRole) + if item.checkState(0) == Qt.CheckState.Checked: + identifier = item.data(0, Qt.ItemDataRole.UserRole) if identifier and identifier != "info:bookinfo": all_checked_identifiers.add(identifier) @@ -2208,8 +2218,8 @@ class HandlerDialog(QDialog): while iterator.value(): item = iterator.value() item_order_counter += 1 - if item.checkState(0) == Qt.Checked: - identifier = item.data(0, Qt.UserRole) + if item.checkState(0) == Qt.CheckState.Checked: + identifier = item.data(0, Qt.ItemDataRole.UserRole) if identifier and identifier != "info:bookinfo": all_checked_identifiers.add(identifier) ordered_checked_items.append((item_order_counter, item, identifier)) @@ -2244,8 +2254,8 @@ class HandlerDialog(QDialog): iterator = QTreeWidgetItemIterator(self.treeWidget) while iterator.value(): item = iterator.value() - if item.checkState(0) == Qt.Checked: - identifier = item.data(0, Qt.UserRole) + if item.checkState(0) == Qt.CheckState.Checked: + identifier = item.data(0, Qt.ItemDataRole.UserRole) if identifier: all_checked_identifiers.add(identifier) iterator += 1 @@ -2270,15 +2280,15 @@ class HandlerDialog(QDialog): while iterator.value(): item = iterator.value() if item.childCount() > 0: - parent_checked = item.checkState(0) == Qt.Checked - parent_id = item.data(0, Qt.UserRole) + parent_checked = item.checkState(0) == Qt.CheckState.Checked + parent_id = item.data(0, Qt.ItemDataRole.UserRole) parent_title = item.text(0) checked_children = [] for i in range(item.childCount()): child = item.child(i) - child_id = child.data(0, Qt.UserRole) + child_id = child.data(0, Qt.ItemDataRole.UserRole) if ( - child.checkState(0) == Qt.Checked + child.checkState(0) == Qt.CheckState.Checked and child_id and child_id not in included_text_ids ): @@ -2306,12 +2316,12 @@ class HandlerDialog(QDialog): else: section_titles.append((title, text)) included_text_ids.add(child_id) - elif item.flags() & Qt.ItemIsUserCheckable: - identifier = item.data(0, Qt.UserRole) + elif item.flags() & Qt.ItemFlag.ItemIsUserCheckable: + identifier = item.data(0, Qt.ItemDataRole.UserRole) if ( identifier and identifier not in included_text_ids - and item.checkState(0) == Qt.Checked + and item.checkState(0) == Qt.CheckState.Checked ): text = self.content_texts.get(identifier, "") if text: @@ -2363,8 +2373,8 @@ class HandlerDialog(QDialog): print(f"Checking selected items: {state}") self.treeWidget.blockSignals(True) for item in self.treeWidget.selectedItems(): - if item.flags() & Qt.ItemIsUserCheckable: - item.setCheckState(0, Qt.Checked if state else Qt.Unchecked) + if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: + item.setCheckState(0, Qt.CheckState.Checked if state else Qt.CheckState.Unchecked) self.treeWidget.blockSignals(False) self._update_checked_set_from_tree() @@ -2377,30 +2387,30 @@ class HandlerDialog(QDialog): action.triggered.connect(self.check_selected_items) action = menu.addAction("Clear") action.triggered.connect(self.uncheck_selected_items) - menu.exec_(self.treeWidget.mapToGlobal(pos)) + menu.exec(self.treeWidget.mapToGlobal(pos)) return if ( not item or item.childCount() == 0 - or not (item.flags() & Qt.ItemIsUserCheckable) + or not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable) ): return menu = QMenu(self) - checked = item.checkState(0) == Qt.Checked + checked = item.checkState(0) == Qt.CheckState.Checked text = "Unselect only this" if checked else "Select only this" action = menu.addAction(text) def do_toggle(): self.treeWidget.blockSignals(True) - new_state = Qt.Unchecked if checked else Qt.Checked + new_state = Qt.CheckState.Unchecked if checked else Qt.CheckState.Checked item.setCheckState(0, new_state) self.treeWidget.blockSignals(False) self._update_checked_set_from_tree() action.triggered.connect(do_toggle) - menu.exec_(self.treeWidget.mapToGlobal(pos)) + menu.exec(self.treeWidget.mapToGlobal(pos)) def closeEvent(self, event): if self.pdf_doc is not None: diff --git a/abogen/constants.py b/abogen/constants.py index fa88ee7..7092686 100644 --- a/abogen/constants.py +++ b/abogen/constants.py @@ -148,6 +148,7 @@ COLORS = { "BLUE_BORDER_HOVER": "#6ab0de", "YELLOW_BACKGROUND": "rgba(255, 221, 51, 0.40)", "GREY_BACKGROUND": "rgba(128, 128, 128, 0.15)", + "GREY_BORDER": "#808080", "RED_BACKGROUND": "rgba(232, 78, 60, 0.15)", "RED_BG": "rgba(232, 78, 60, 0.10)", "RED_BG_HOVER": "rgba(232, 78, 60, 0.15)", diff --git a/abogen/conversion.py b/abogen/conversion.py index b937b5a..5be2c9f 100644 --- a/abogen/conversion.py +++ b/abogen/conversion.py @@ -3,8 +3,8 @@ import re import time import hashlib # For generating unique cache filenames from platformdirs import user_desktop_dir -from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer -from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox +from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer +from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox import soundfile as sf from abogen.utils import clean_text, create_process, get_user_cache_path, detect_encoding from abogen.constants import ( @@ -96,8 +96,8 @@ class ChapterOptionsDialog(QDialog): # Prevent closing with the X button and remove the help button self.setWindowFlags( self.windowFlags() - & ~Qt.WindowCloseButtonHint - & ~Qt.WindowContextHelpButtonHint + & ~Qt.WindowType.WindowCloseButtonHint + & ~Qt.WindowType.WindowContextHelpButtonHint ) layout = QVBoxLayout(self) @@ -131,7 +131,7 @@ class ChapterOptionsDialog(QDialog): layout.addWidget(self.countdown_label) # Add OK button - button_box = QDialogButtonBox(QDialogButtonBox.Ok) + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) button_box.accepted.connect(self.accept) layout.addWidget(button_box) @@ -180,7 +180,7 @@ class ChapterOptionsDialog(QDialog): # Prevent escape key from closing the dialog def keyPressEvent(self, event): - if event.key() == Qt.Key_Escape: + if event.key() == Qt.Key.Key_Escape: event.ignore() else: super().keyPressEvent(event) diff --git a/abogen/gui.py b/abogen/gui.py index f742ca3..229b2c6 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -8,28 +8,31 @@ from abogen.queue_manager_gui import QueueManager from abogen.queued_item import QueuedItem import abogen.hf_tracker as hf_tracker import hashlib # Added for cache path generation -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QApplication, + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, - QFileDialog, - QLabel, - QProgressBar, - QSlider, QComboBox, - QSizePolicy, QTextEdit, - QFileIconProvider, + QLabel, + QSlider, QMessageBox, + QFileDialog, + QProgressBar, + QFrame, + QStyleFactory, + QInputDialog, + QFileIconProvider, + QSizePolicy, QDialog, QCheckBox, QMenu, - QAction, - QActionGroup, ) -from PyQt5.QtCore import ( +from PyQt6.QtGui import QAction, QActionGroup +from PyQt6.QtCore import ( Qt, QUrl, QPoint, @@ -42,8 +45,9 @@ from PyQt5.QtCore import ( QSize, QTimer, QEvent, + QProcess, ) -from PyQt5.QtGui import ( +from PyQt6.QtGui import ( QTextCursor, QDesktopServices, QIcon, @@ -52,6 +56,7 @@ from PyQt5.QtGui import ( QPolygon, QColor, QMovie, + QPalette, ) from abogen.utils import ( load_config, @@ -95,12 +100,12 @@ class DarkTitleBarEventFilter(QObject): self.set_title_bar_dark_mode = set_title_bar_dark_mode_func def eventFilter(self, obj, event): - if event.type() == QEvent.Show: + if event.type() == QEvent.Type.Show: # Only apply to QWidget windows if isinstance(obj, QWidget) and obj.isWindow(): if self.is_windows and self.get_dark_mode(): self.set_title_bar_dark_mode(obj, True) - return False + return super().eventFilter(obj, event) class ShowWarningSignalEmitter(QObject): # New class to handle signal emission @@ -154,7 +159,7 @@ class InputBox(QLabel): def __init__(self, parent=None): super().__init__(parent) - self.setAlignment(Qt.AlignCenter) + self.setAlignment(Qt.AlignmentFlag.AlignCenter) self.setAcceptDrops(True) self.setText( "Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)" @@ -162,8 +167,8 @@ class InputBox(QLabel): self.setStyleSheet( f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}" ) - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.setCursor(Qt.PointingHandCursor) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.setCursor(Qt.CursorShape.PointingHandCursor) # Add clear button self.clear_btn = QPushButton("✕", self) @@ -223,7 +228,7 @@ class InputBox(QLabel): pixmap = qicon.pixmap(size) # convert to base64 PNG buffer = QBuffer() - buffer.open(QIODevice.WriteOnly) + buffer.open(QIODevice.OpenModeFlag.WriteOnly) pixmap.save(buffer, "PNG") img_data = base64.b64encode(buffer.data()).decode() @@ -356,6 +361,9 @@ class InputBox(QLabel): self.window().displayed_file_path = ( None # Reset the displayed file path when clearing input ) + # Reset book handler attributes + self.window().save_chapters_separately = None + self.window().merge_chapters_at_end = None self.setText( "Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)" ) @@ -384,7 +392,7 @@ class InputBox(QLabel): return f"{size:.{decimal_places}f} PB" def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: + if event.button() == Qt.MouseButton.LeftButton: self.window().open_file_dialog() def dragEnterEvent(self, event): @@ -542,7 +550,7 @@ class InputBox(QLabel): menu.addAction(act_input) # Show the menu anchored to the button - menu.exec_(self.go_to_folder_btn.mapToGlobal(QPoint(0, self.go_to_folder_btn.height()))) + menu.exec(self.go_to_folder_btn.mapToGlobal(QPoint(0, self.go_to_folder_btn.height()))) else: if ( file_to_check @@ -560,7 +568,7 @@ class TextboxDialog(QDialog): super().__init__(parent) self.setWindowTitle("Enter Text") self.setWindowFlags( - Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint + Qt.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMaximizeButtonHint ) self.resize(700, 500) @@ -634,16 +642,16 @@ class TextboxDialog(QDialog): # Check if we need to warn about overwriting a non-temporary file if hasattr(self, "is_non_cache_file") and self.is_non_cache_file: msg_box = QMessageBox(self) - msg_box.setIcon(QMessageBox.Warning) + msg_box.setIcon(QMessageBox.Icon.Warning) msg_box.setWindowTitle("File Overwrite Warning") msg_box.setText( f"You are about to overwrite the original file:\n{self.non_cache_file_path}" ) msg_box.setInformativeText("Do you want to continue?") - msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msg_box.setDefaultButton(QMessageBox.No) + msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + msg_box.setDefaultButton(QMessageBox.StandardButton.No) - if msg_box.exec_() != QMessageBox.Yes: + if msg_box.exec() != QMessageBox.StandardButton.Yes: # User canceled, don't close the dialog return @@ -873,7 +881,7 @@ class abogen(QWidget): self.log_text = QTextEdit(self) self.log_text.setReadOnly(True) self.log_text.setUndoRedoEnabled(False) - self.log_text.setFrameStyle(QTextEdit.NoFrame) + self.log_text.setFrameStyle(QFrame.Shape.NoFrame) self.log_text.setStyleSheet("QTextEdit { border: none; }") self.log_text.hide() container_layout.addWidget(self.log_text, 1) @@ -884,11 +892,11 @@ class abogen(QWidget): speed_layout = QVBoxLayout() speed_layout.setSpacing(2) speed_layout.addWidget(QLabel("Speed:", self)) - self.speed_slider = QSlider(Qt.Horizontal, self) + self.speed_slider = QSlider(Qt.Orientation.Horizontal, self) self.speed_slider.setMinimum(10) self.speed_slider.setMaximum(200) self.speed_slider.setValue(100) - self.speed_slider.setTickPosition(QSlider.TicksBelow) + self.speed_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.speed_slider.setTickInterval(5) self.speed_slider.setSingleStep(5) speed_layout.addWidget(self.speed_slider) @@ -910,7 +918,7 @@ class abogen(QWidget): "The first character represents the language:\n" '"a" => American English\n"b" => British English\n"e" => Spanish\n"f" => French\n"h" => Hindi\n"i" => Italian\n"j" => Japanese\n"p" => Brazilian Portuguese\n"z" => Mandarin Chinese\nThe second character represents the gender:\n"m" => Male\n"f" => Female' ) - self.voice_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.voice_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) voice_layout.addWidget(self.voice_combo) # Voice formula button self.btn_voice_formula_mixer = QPushButton(self) @@ -925,11 +933,11 @@ class abogen(QWidget): # Play/Stop icons def make_icon(color, shape): pix = QPixmap(20, 20) - pix.fill(Qt.transparent) + pix.fill(Qt.GlobalColor.transparent) p = QPainter(pix) - p.setRenderHint(QPainter.Antialiasing) + p.setRenderHint(QPainter.RenderHint.Antialiasing) p.setBrush(QColor(*color)) - p.setPen(Qt.NoPen) + p.setPen(Qt.PenStyle.NoPen) if shape == "play": pts = [ pix.rect().topLeft() + QPoint(4, 2), @@ -983,7 +991,7 @@ class abogen(QWidget): self.subtitle_combo.setStyleSheet( "QComboBox { min-height: 20px; padding: 6px 12px; }" ) - self.subtitle_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.subtitle_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.subtitle_combo.setCurrentText(self.subtitle_mode) self.subtitle_combo.currentTextChanged.connect(self.on_subtitle_mode_changed) # Enable/disable subtitle options based on selected language (profile or voice) @@ -1001,7 +1009,7 @@ class abogen(QWidget): self.format_combo.setStyleSheet( "QComboBox { min-height: 20px; padding: 6px 12px; }" ) - self.format_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.format_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) # Add items with display labels and underlying keys for key, label in [ ("wav", "wav"), @@ -1032,7 +1040,7 @@ class abogen(QWidget): "QComboBox { min-height: 20px; padding: 6px 12px; }" ) self.subtitle_format_combo.setSizePolicy( - QSizePolicy.Expanding, QSizePolicy.Fixed + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) for value, text in SUBTITLE_FORMATS: self.subtitle_format_combo.addItem(text, value) @@ -1079,7 +1087,7 @@ class abogen(QWidget): "QComboBox { min-height: 20px; padding: 6px 12px; }" ) self.replace_newlines_combo.setSizePolicy( - QSizePolicy.Expanding, QSizePolicy.Fixed + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) # Set initial value based on config self.replace_newlines_combo.setCurrentIndex( @@ -1106,7 +1114,7 @@ class abogen(QWidget): self.save_combo.setStyleSheet( "QComboBox { min-height: 20px; padding: 6px 12px; }" ) - self.save_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.save_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.save_combo.setCurrentText(self.save_option) self.save_combo.currentTextChanged.connect(self.on_save_option_changed) save_layout.addWidget(self.save_combo) @@ -1121,7 +1129,7 @@ class abogen(QWidget): save_path_row.addWidget(selected_folder_label) self.save_path_label = QLabel("", self.save_path_row_widget) self.save_path_label.setStyleSheet(f"QLabel {{ color: {COLORS['GREEN']}; }}") - self.save_path_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + self.save_path_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) save_path_row.addWidget(self.save_path_label) self.save_path_row_widget.hide() # Hide the whole row by default controls_layout.addWidget(self.save_path_row_widget) @@ -1164,9 +1172,10 @@ class abogen(QWidget): self.btn_start.setFixedHeight(60) self.btn_start.clicked.connect(self.start_conversion) controls_layout.addWidget(self.btn_start) + # Add controls to a container widget self.controls_widget = QWidget() self.controls_widget.setLayout(controls_layout) - self.controls_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + self.controls_widget.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) container_layout.addWidget(self.controls_widget) # Progress bar self.progress_bar = QProgressBar(self) @@ -1175,7 +1184,7 @@ class abogen(QWidget): container_layout.addWidget(self.progress_bar) # ETR Label self.etr_label = QLabel("Estimated time remaining: Calculating...", self) - self.etr_label.setAlignment(Qt.AlignCenter) + self.etr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.etr_label.hide() container_layout.addWidget(self.etr_label) # Cancel button @@ -1274,12 +1283,12 @@ class abogen(QWidget): checked_chapters=self.selected_chapters, parent=self ) - dialog.setWindowModality(Qt.NonModal) + dialog.setWindowModality(Qt.WindowModality.NonModal) dialog.setModal(False) dialog.show() # We'll handle the dialog result asynchronously def on_dialog_finished(result): - if result != QDialog.Accepted: + if result != QDialog.DialogCode.Accepted: return False chapters_text, all_checked_hrefs = dialog.get_selected_text() if not all_checked_hrefs: @@ -1319,7 +1328,7 @@ class abogen(QWidget): if self.save_as_project: # Get project directory from user project_dir = QFileDialog.getExistingDirectory( - self, "Select Project Folder", "", QFileDialog.ShowDirsOnly + self, "Select Project Folder", "", QFileDialog.Option.ShowDirsOnly ) if not project_dir: # User cancelled, fallback to cache @@ -1445,7 +1454,7 @@ class abogen(QWidget): dialog.non_cache_file_path = edit_file except Exception: pass - if dialog.exec_() == QDialog.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: text = dialog.get_text() if not text.strip(): self._show_error_message_box("Textbox Error", "Text cannot be empty.") @@ -1605,7 +1614,7 @@ class abogen(QWidget): at_bottom = sb.value() == sb.maximum() cursor = txt.textCursor() - cursor.movePosition(QTextCursor.End) + cursor.movePosition(QTextCursor.MoveOperation.End) fmt = cursor.charFormat() if isinstance(message, tuple): @@ -1624,7 +1633,7 @@ class abogen(QWidget): end = doc.findBlockByNumber(excess).position() trim_cursor = QTextCursor(doc) trim_cursor.setPosition(start) - trim_cursor.setPosition(end, QTextCursor.KeepAnchor) + trim_cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor) trim_cursor.removeSelectedText() if at_bottom: @@ -1743,6 +1752,8 @@ class abogen(QWidget): total_char_count=self.char_count, replace_single_newlines=self.replace_single_newlines, save_base_path=save_base_path, + save_chapters_separately=getattr(self, "save_chapters_separately", None), + merge_chapters_at_end=getattr(self, "merge_chapters_at_end", None), ) # Prevent adding duplicate items to the queue @@ -1760,6 +1771,10 @@ class abogen(QWidget): == item_queue.replace_single_newlines and getattr(queued_item, "save_base_path", None) == item_queue.save_base_path + and getattr(queued_item, "save_chapters_separately", None) + == item_queue.save_chapters_separately + and getattr(queued_item, "merge_chapters_at_end", None) + == item_queue.merge_chapters_at_end ): QMessageBox.warning( self, "Duplicate Item", "This item is already in the queue." @@ -1779,10 +1794,10 @@ class abogen(QWidget): self, "Confirm Clear Queue", f"Are you sure you want to clear {len(self.queued_items)} items from the queue?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return self.queued_items = [] self.enable_disable_queue_buttons() @@ -1790,7 +1805,7 @@ class abogen(QWidget): def manage_queue(self): # show a dialog to manage the queue dialog = QueueManager(self, self.queued_items) - if dialog.exec_() == QDialog.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: self.queued_items = dialog.get_queue() # re-enable/disable buttons based on queue state self.enable_disable_queue_buttons() @@ -2057,7 +2072,7 @@ class abogen(QWidget): dialog.setLayout(layout) dialog.setMinimumSize(400, 300) dialog.setSizeGripEnabled(True) # Allow resizing - dialog.exec_() + dialog.exec() def on_conversion_finished(self, message, output_path): prevent_sleep_end() @@ -2516,14 +2531,14 @@ class abogen(QWidget): def _show_error_message_box(self, title, message): box = QMessageBox(self) - box.setIcon(QMessageBox.Critical) + box.setIcon(QMessageBox.Icon.Critical) box.setWindowTitle(title) box.setText(message) copy_btn = QPushButton("Copy") - box.addButton(copy_btn, QMessageBox.ActionRole) - box.addButton(QMessageBox.Ok) + box.addButton(copy_btn, QMessageBox.ButtonRole.ActionRole) + box.addButton(QMessageBox.StandardButton.Ok) copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(message)) - box.exec_() + box.exec() def _show_preview_error_box(self, msg): self._show_error_message_box("Preview Error", f"Preview error: {msg}") @@ -2551,14 +2566,14 @@ class abogen(QWidget): def cancel_conversion(self): if self.is_converting: box = QMessageBox(self) - box.setIcon(QMessageBox.Warning) + box.setIcon(QMessageBox.Icon.Warning) box.setWindowTitle("Cancel Conversion") box.setText( "A conversion is currently running. Are you sure you want to cancel?" ) - box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - box.setDefaultButton(QMessageBox.No) - if box.exec_() != QMessageBox.Yes: + box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + box.setDefaultButton(QMessageBox.StandardButton.No) + if box.exec() != QMessageBox.StandardButton.Yes: return try: if ( @@ -2643,7 +2658,7 @@ class abogen(QWidget): save_config(self.config) def on_gpu_setting_changed(self, state): - self.use_gpu = state == Qt.Checked + self.use_gpu = state == Qt.CheckState.Checked.value self.config["use_gpu"] = self.use_gpu save_config(self.config) @@ -2660,14 +2675,14 @@ class abogen(QWidget): def closeEvent(self, event): if self.is_converting: box = QMessageBox(self) - box.setIcon(QMessageBox.Warning) + box.setIcon(QMessageBox.Icon.Warning) box.setWindowTitle("Conversion in Progress") box.setText( "A conversion is currently running. Are you sure you want to exit?" ) - box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - box.setDefaultButton(QMessageBox.No) - if box.exec_() == QMessageBox.Yes: + box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + box.setDefaultButton(QMessageBox.StandardButton.No) + if box.exec() == QMessageBox.StandardButton.Yes: self.cleanup_conversion_thread() event.accept() else: @@ -2681,10 +2696,10 @@ class abogen(QWidget): from abogen.conversion import ChapterOptionsDialog dialog = ChapterOptionsDialog(chapter_count, parent=self) - dialog.setWindowModality(Qt.ApplicationModal) + dialog.setWindowModality(Qt.WindowModality.ApplicationModal) # If dialog is accepted, pass the options to the conversion thread - if dialog.exec_() == QDialog.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: options = dialog.get_options() if ( hasattr(self, "conversion_thread") @@ -2696,8 +2711,7 @@ class abogen(QWidget): self.cancel_conversion() def apply_theme(self, theme): - from PyQt5.QtGui import QPalette, QColor - from PyQt5.QtWidgets import QStyleFactory + app = QApplication.instance() is_windows = platform.system() == "Windows" @@ -2981,7 +2995,7 @@ class abogen(QWidget): about_action.triggered.connect(self.show_about_dialog) menu.addAction(about_action) - menu.exec_(self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height()))) + menu.exec(self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height()))) def toggle_replace_single_newlines(self, enabled): self.replace_single_newlines = enabled @@ -2989,7 +3003,7 @@ class abogen(QWidget): save_config(self.config) def restart_app(self): - from PyQt5.QtCore import QProcess + import sys exe = sys.executable @@ -3022,10 +3036,10 @@ class abogen(QWidget): self, "Restart Required", message, - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) - if reply == QMessageBox.Yes: + if reply == QMessageBox.StandardButton.Yes: self.config["disable_kokoro_internet"] = disabled save_config(self.config) try: @@ -3040,10 +3054,10 @@ class abogen(QWidget): self, "Reset Settings", "This will reset all settings to their default values and restart the application.\n\nDo you want to continue?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) - if reply == QMessageBox.Yes: + if reply == QMessageBox.StandardButton.Yes: from abogen.utils import get_user_config_path config_path = get_user_config_path() @@ -3204,9 +3218,9 @@ Categories=AudioVideo;Audio;Utility; self, "Install Application Entry", "Install application entry for current user?", - QMessageBox.Yes | QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if reply == QMessageBox.Yes: + if reply == QMessageBox.StandardButton.Yes: import shutil user_app_dir = os.path.expanduser("~/.local/share/applications") @@ -3266,12 +3280,11 @@ Categories=AudioVideo;Audio;Utility; initial_state = entry.get("voices", []) else: initial_state = entry - else: - initial_state = [] + self.selected_lang = entry[0][0] if entry and entry[0] else None dialog = VoiceFormulaDialog( self, initial_state=initial_state, selected_profile=selected_profile ) - if dialog.exec_() == QDialog.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: if dialog.current_profile: self.selected_profile_name = dialog.current_profile self.config["selected_profile_name"] = dialog.current_profile @@ -3292,7 +3305,7 @@ Categories=AudioVideo;Audio;Utility; # Create custom dialog dialog = QDialog(self) dialog.setWindowTitle(f"About {PROGRAM_NAME}") - dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) + dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) dialog.setFixedSize(400, 320) # Increased height for new button layout = QVBoxLayout(dialog) @@ -3314,7 +3327,7 @@ Categories=AudioVideo;Audio;Utility; title_label = QLabel( f"

{PROGRAM_NAME} v{VERSION}

Audiobook Generator

" ) - title_label.setTextFormat(Qt.RichText) + title_label.setTextFormat(Qt.TextFormat.RichText) header_layout.addWidget(title_label, 1) layout.addLayout(header_layout) @@ -3323,7 +3336,7 @@ Categories=AudioVideo;Audio;Utility; f"

{PROGRAM_DESCRIPTION}

" "

Visit the GitHub repository for updates, documentation, and to report issues.

" ) - desc_label.setTextFormat(Qt.RichText) + desc_label.setTextFormat(Qt.TextFormat.RichText) desc_label.setWordWrap(True) layout.addWidget(desc_label) @@ -3346,7 +3359,7 @@ Categories=AudioVideo;Audio;Utility; close_btn.setFixedHeight(32) layout.addWidget(close_btn) - dialog.exec_() + dialog.exec() def manual_check_for_updates(self): """Manually check for updates and always show result""" @@ -3359,7 +3372,7 @@ Categories=AudioVideo;Audio;Utility; def show_update_message(remote_version, local_version): msg_box = QMessageBox(self) - msg_box.setIcon(QMessageBox.Information) + msg_box.setIcon(QMessageBox.Icon.Information) msg_box.setWindowTitle("Update Available") msg_box.setText( f"A new version of {PROGRAM_NAME} is available! ({local_version} > {remote_version})" @@ -3371,9 +3384,9 @@ Categories=AudioVideo;Audio;Utility; "Alternatively, visit the GitHub repository for more information. " "Would you like to view the changelog?" ) - msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msg_box.setDefaultButton(QMessageBox.Yes) - if msg_box.exec_() == QMessageBox.Yes: + msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + msg_box.setDefaultButton(QMessageBox.StandardButton.Yes) + if msg_box.exec() == QMessageBox.StandardButton.Yes: try: QDesktopServices.openUrl(QUrl(GITHUB_URL + "/releases/latest")) except Exception: @@ -3455,7 +3468,7 @@ Categories=AudioVideo;Audio;Utility; # Create a custom message box with checkbox msg_box = QMessageBox(self) - msg_box.setIcon(QMessageBox.Question) + msg_box.setIcon(QMessageBox.Icon.Question) msg_box.setWindowTitle("Clear Cache Files") msg_text = f"Found {file_count} cache file{'s' if file_count != 1 else ''} in the {PROGRAM_NAME} cache folder." @@ -3474,10 +3487,10 @@ Categories=AudioVideo;Audio;Utility; msg_box.setCheckBox(preview_cache_checkbox) # Add buttons - msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msg_box.setDefaultButton(QMessageBox.Yes) + msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + msg_box.setDefaultButton(QMessageBox.StandardButton.Yes) - if msg_box.exec_() != QMessageBox.Yes: + if msg_box.exec() != QMessageBox.StandardButton.Yes: return # Delete the text files @@ -3522,7 +3535,7 @@ Categories=AudioVideo;Audio;Utility; def set_max_log_lines(self): """Open a dialog to set the maximum lines in the log window.""" - from PyQt5.QtWidgets import QInputDialog + from PyQt6.QtWidgets import QInputDialog value, ok = QInputDialog.getInt( self, @@ -3545,7 +3558,7 @@ Categories=AudioVideo;Audio;Utility; def set_max_subtitle_words(self): """Open a dialog to set the maximum words per subtitle""" - from PyQt5.QtWidgets import QInputDialog + from PyQt6.QtWidgets import QInputDialog current_value = self.config.get("max_subtitle_words", 50) @@ -3574,7 +3587,7 @@ Categories=AudioVideo;Audio;Utility; def set_silence_between_chapters(self): """Open a dialog to set the silence duration between chapters""" - from PyQt5.QtWidgets import QInputDialog, QDialog + current_value = self.config.get("silence_duration", 2.0) @@ -3583,14 +3596,14 @@ Categories=AudioVideo;Audio;Utility; dlg.setLabelText( "Enter the duration of silence\nbetween chapters (in seconds):" ) - dlg.setInputMode(QInputDialog.DoubleInput) + dlg.setInputMode(QInputDialog.InputMode.DoubleInput) dlg.setDoubleDecimals(1) dlg.setDoubleMinimum(0.0) dlg.setDoubleMaximum(60.0) dlg.setDoubleValue(current_value) dlg.setDoubleStep(0.1) # <-- set step to 0.1 - if dlg.exec_() == QDialog.Accepted: + if dlg.exec() == QDialog.DialogCode.Accepted: value = dlg.doubleValue() # Round to one decimal to avoid floating-point representation noise value = round(value, 1) diff --git a/abogen/main.py b/abogen/main.py index 8838ac3..ac4a2c6 100644 --- a/abogen/main.py +++ b/abogen/main.py @@ -7,19 +7,24 @@ import signal # Qt platform plugin detection (fixes #59) try: - from PyQt5.QtCore import QLibraryInfo - plugins = QLibraryInfo.location(QLibraryInfo.PluginsPath) + from PyQt6.QtCore import QLibraryInfo + + # Get the path to the plugins directory + plugins = QLibraryInfo.path(QLibraryInfo.LibraryPath.PluginsPath) + # Normalize path to use the OS-native separators and absolute path platform_dir = os.path.normpath(os.path.join(plugins, "platforms")) + # Ensure we work with an absolute path for clarity platform_dir = os.path.abspath(platform_dir) + if os.path.isdir(platform_dir): os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = platform_dir print("QT_QPA_PLATFORM_PLUGIN_PATH set to:", platform_dir) else: - print("PyQt5 platform plugins not found at", platform_dir) + print("PyQt6 platform plugins not found at", platform_dir) except ImportError: - print("PyQt5 not installed.") + print("PyQt6 not installed.") # Set application ID for Windows taskbar icon if platform.system() == "Windows": @@ -31,9 +36,16 @@ if platform.system() == "Windows": except Exception as e: print("Warning: failed to set AppUserModelID:", e) -from PyQt5.QtWidgets import QApplication -from PyQt5.QtGui import QIcon -from PyQt5.QtCore import qInstallMessageHandler, QtMsgType +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import ( + Qt, + QLibraryInfo, + qInstallMessageHandler, + QStandardPaths, + QSettings, + QtMsgType, +) # Add the directory to Python path sys.path.insert(0, os.path.join(os.path.dirname(__file__))) @@ -82,10 +94,12 @@ if platform.system() == "Darwin" and platform.processor() == "arm": # Custom message handler to filter out specific Qt warnings def qt_message_handler(mode, context, message): + # In PyQt6, the mode is an enum, so we compare with the enum members if "Wayland does not support QWindow::requestActivate()" in message: return # Suppress this specific message if "setGrabPopup called with a parent, QtWaylandClient" in message: return + if mode == QtMsgType.QtWarningMsg: print(f"Qt Warning: {message}") elif mode == QtMsgType.QtCriticalMsg: @@ -129,7 +143,7 @@ def main(): ex = abogen() ex.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) if __name__ == "__main__": diff --git a/abogen/queue_manager_gui.py b/abogen/queue_manager_gui.py index 1a5d2ee..a8ff439 100644 --- a/abogen/queue_manager_gui.py +++ b/abogen/queue_manager_gui.py @@ -2,7 +2,7 @@ # button to remove an item from the queue # button to clear the queue -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, @@ -14,18 +14,20 @@ from PyQt5.QtWidgets import ( QLabel, QWidget, QSizePolicy, + QAbstractItemView, ) -from PyQt5.QtCore import QFileInfo, Qt +from PyQt6.QtCore import QFileInfo, Qt from abogen.constants import COLORS from copy import deepcopy -from PyQt5.QtGui import QFontMetrics +from PyQt6.QtGui import QFontMetrics class ElidedLabel(QLabel): - def __init__(self, text, parent=None): - super().__init__(text, parent) + def __init__(self, text): + super().__init__(text) self._full_text = text - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + self.setTextFormat(Qt.TextFormat.PlainText) def setText(self, text): self._full_text = text @@ -34,7 +36,7 @@ class ElidedLabel(QLabel): def resizeEvent(self, event): metrics = QFontMetrics(self.font()) - elided = metrics.elidedText(self._full_text, Qt.ElideRight, self.width()) + elided = metrics.elidedText(self._full_text, Qt.TextElideMode.ElideRight, self.width()) super().setText(elided) super().resizeEvent(event) @@ -53,8 +55,8 @@ class QueueListItemWidget(QWidget): name_label = ElidedLabel(os.path.basename(file_name)) char_label = QLabel(f"Chars: {char_count}") char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};") - char_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - char_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + char_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + char_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) layout.addWidget(name_label, 1) layout.addWidget(char_label, 0) self.setLayout(layout) @@ -67,12 +69,12 @@ class DroppableQueueListWidget(QListWidget): self.setAcceptDrops(True) # Overlay for drag hover self.drag_overlay = QLabel("", self) - self.drag_overlay.setAlignment(Qt.AlignCenter) + self.drag_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter) self.drag_overlay.setStyleSheet( f"border:2px dashed {COLORS['BLUE_BORDER_HOVER']}; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG_HOVER']};" ) self.drag_overlay.setVisible(False) - self.drag_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True) + self.drag_overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): @@ -132,9 +134,9 @@ class QueueManager(QDialog): layout.setSpacing(12) # set spacing between widgets in main layout # list of queued items self.listwidget = DroppableQueueListWidget(self) - self.listwidget.setSelectionMode(QListWidget.ExtendedSelection) + self.listwidget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.listwidget.setAlternatingRowColors(True) - self.listwidget.setContextMenuPolicy(Qt.CustomContextMenu) + self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.listwidget.customContextMenuRequested.connect(self.show_context_menu) # Add informative instructions at the top instructions = QLabel( @@ -145,7 +147,7 @@ class QueueManager(QDialog): "Changing the main window configuration afterward does not affect files already in the queue. " "You can view each file's configuration by hovering over them." ) - instructions.setAlignment(Qt.AlignLeft) + instructions.setAlignment(Qt.AlignmentFlag.AlignLeft) instructions.setWordWrap(True) instructions.setStyleSheet("margin-bottom: 8px;") layout.addWidget(instructions) @@ -154,12 +156,12 @@ class QueueManager(QDialog): "Drag and drop your text files here or use the 'Add files' button.", self.listwidget, ) - self.empty_overlay.setAlignment(Qt.AlignCenter) + self.empty_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter) self.empty_overlay.setStyleSheet( f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;" ) self.empty_overlay.setWordWrap(True) - self.empty_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True) + self.empty_overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) self.empty_overlay.hide() # add queue items to the list self.process_queue() @@ -192,7 +194,7 @@ class QueueManager(QDialog): self.listwidget.currentItemChanged.connect(self.update_button_states) self.listwidget.itemSelectionChanged.connect(self.update_button_states) - buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) @@ -211,7 +213,6 @@ class QueueManager(QDialog): self.listwidget.clear() if not self.queue: - self.empty_overlay.resize(self.listwidget.size()) self.empty_overlay.show() self.update_button_states() return @@ -262,10 +263,18 @@ class QueueManager(QDialog): f"Characters: {getattr(item, 'total_char_count', '')}
" f"Replace Single Newlines: {getattr(item, 'replace_single_newlines', False)}" ) + # Add book handler options if present + save_chapters_separately = getattr(item, 'save_chapters_separately', None) + merge_chapters_at_end = getattr(item, 'merge_chapters_at_end', None) + if save_chapters_separately is not None: + tooltip += f"
Save chapters separately: {'Yes' if save_chapters_separately else 'No'}" + # Only show merge option if saving chapters separately + if save_chapters_separately and merge_chapters_at_end is not None: + tooltip += f"
Merge chapters at the end: {'Yes' if merge_chapters_at_end else 'No'}" list_item.setToolTip(tooltip) list_item.setIcon(icon) # Store both paths for context menu - list_item.setData(Qt.UserRole, { + list_item.setData(Qt.ItemDataRole.UserRole, { 'display_path': display_file_path, 'processing_path': processing_file_path }) @@ -280,7 +289,7 @@ class QueueManager(QDialog): items = self.listwidget.selectedItems() if not items: return - from PyQt5.QtWidgets import QMessageBox + from PyQt6.QtWidgets import QMessageBox # Remove by index to ensure correct mapping rows = sorted([self.listwidget.row(item) for item in items], reverse=True) @@ -290,10 +299,10 @@ class QueueManager(QDialog): self, "Confirm Remove", f"Are you sure you want to remove {len(rows)} selected items from the queue?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return for row in rows: if 0 <= row < len(self.queue): @@ -302,17 +311,17 @@ class QueueManager(QDialog): self.update_button_states() def clear_queue(self): - from PyQt5.QtWidgets import QMessageBox + from PyQt6.QtWidgets import QMessageBox if len(self.queue) > 1: reply = QMessageBox.question( self, "Confirm Clear Queue", f"Are you sure you want to clear {len(self.queue)} items from the queue?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return self.queue.clear() self.listwidget.clear() @@ -362,6 +371,13 @@ class QueueManager(QDialog): attrs["replace_single_newlines"] = getattr( parent, "replace_single_newlines", False ) + # book handler options + attrs["save_chapters_separately"] = getattr( + parent, "save_chapters_separately", None + ) + attrs["merge_chapters_at_end"] = getattr( + parent, "merge_chapters_at_end", None + ) else: # fallback: empty values attrs = { @@ -378,11 +394,13 @@ class QueueManager(QDialog): "replace_single_newlines", ] } + attrs["save_chapters_separately"] = None + attrs["merge_chapters_at_end"] = None return attrs def add_files_from_paths(self, file_paths): from abogen.utils import calculate_text_length - from PyQt5.QtWidgets import QMessageBox + from PyQt6.QtWidgets import QMessageBox import os current_attrs = self.get_current_attributes() @@ -430,6 +448,10 @@ class QueueManager(QDialog): == getattr(item, "replace_single_newlines", False) and getattr(queued_item, "save_base_path", None) == getattr(item, "save_base_path", None) + and getattr(queued_item, "save_chapters_separately", None) + == getattr(item, "save_chapters_separately", None) + and getattr(queued_item, "merge_chapters_at_end", None) + == getattr(item, "merge_chapters_at_end", None) ): is_duplicate = True break @@ -447,7 +469,7 @@ class QueueManager(QDialog): self.update_button_states() def add_more_files(self): - from PyQt5.QtWidgets import QFileDialog + from PyQt6.QtWidgets import QFileDialog from abogen.utils import calculate_text_length # import the function # Only allow .txt files @@ -477,9 +499,9 @@ class QueueManager(QDialog): self.clear_button.setEnabled(bool(self.queue)) def show_context_menu(self, pos): - from PyQt5.QtWidgets import QMenu, QAction - from PyQt5.QtGui import QDesktopServices - from PyQt5.QtCore import QUrl + from PyQt6.QtWidgets import QMenu + from PyQt6.QtGui import QAction, QDesktopServices + from PyQt6.QtCore import QUrl import os global_pos = self.listwidget.viewport().mapToGlobal(pos) @@ -495,10 +517,10 @@ class QueueManager(QDialog): open_file_action = QAction("Open file", self) def open_file(): - from PyQt5.QtWidgets import QMessageBox + from PyQt6.QtWidgets import QMessageBox item = selected_items[0] - paths = item.data(Qt.UserRole) + paths = item.data(Qt.ItemDataRole.UserRole) if isinstance(paths, dict): file_path = paths.get('display_path', paths.get('processing_path', '')) else: @@ -524,7 +546,7 @@ class QueueManager(QDialog): # If the queued item represents a converted document (markdown, pdf, epub) # show two actions: Go to processed file (the cached .txt) and Go to input file (original source) item = selected_items[0] - paths = item.data(Qt.UserRole) + paths = item.data(Qt.ItemDataRole.UserRole) if isinstance(paths, dict): display_path = paths.get('display_path', '') processing_path = paths.get('processing_path', '') @@ -539,7 +561,7 @@ class QueueManager(QDialog): isinstance(processing_path, str) and processing_path.lower().endswith(doc_exts) ) - from PyQt5.QtWidgets import QMessageBox + from PyQt6.QtWidgets import QMessageBox def open_folder_for(path_label: str): # path_label should be either 'display' or 'processing' @@ -588,7 +610,7 @@ class QueueManager(QDialog): def go_to_folder(): item = selected_items[0] - paths = item.data(Qt.UserRole) + paths = item.data(Qt.ItemDataRole.UserRole) if isinstance(paths, dict): file_path = paths.get('display_path', paths.get('processing_path', '')) else: @@ -618,7 +640,7 @@ class QueueManager(QDialog): clear_action = QAction("Clear Queue", self) clear_action.triggered.connect(self.clear_queue) menu.addAction(clear_action) - menu.exec_(global_pos) + menu.exec(global_pos) def accept(self): # Accept: keep changes @@ -626,7 +648,7 @@ class QueueManager(QDialog): def reject(self): # Cancel: restore original queue - from PyQt5.QtWidgets import QMessageBox + from PyQt6.QtWidgets import QMessageBox # Warn if user changed a lot (e.g., more than 1 items difference) original_count = len(self._original_queue) @@ -636,19 +658,19 @@ class QueueManager(QDialog): self, "Confirm Cancel", f"Are you sure you want to cancel and discard all changes?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return self.queue.clear() self.queue.extend(deepcopy(self._original_queue)) super().reject() def keyPressEvent(self, event): - from PyQt5.QtCore import Qt + from PyQt6.QtCore import Qt - if event.key() == Qt.Key_Delete: + if event.key() == Qt.Key.Key_Delete: self.remove_item() else: super().keyPressEvent(event) diff --git a/abogen/queued_item.py b/abogen/queued_item.py index f8c9ece..17f26d5 100644 --- a/abogen/queued_item.py +++ b/abogen/queued_item.py @@ -15,3 +15,5 @@ class QueuedItem: total_char_count: int replace_single_newlines: bool = False save_base_path: str = None + save_chapters_separately: bool = None + merge_chapters_at_end: bool = None diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py index ae361a3..0bd0bfc 100644 --- a/abogen/voice_formula_gui.py +++ b/abogen/voice_formula_gui.py @@ -1,6 +1,6 @@ import json import os -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QCheckBox, @@ -22,12 +22,11 @@ from PyQt5.QtWidgets import ( QFileDialog, QSplitter, QMenu, - QAction, - QComboBox, QApplication, + QComboBox, ) -from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize -from PyQt5.QtGui import QPixmap, QIcon +from PyQt6.QtCore import Qt, QTimer, QPoint, QRect, QSize +from PyQt6.QtGui import QPixmap, QIcon, QAction from abogen.constants import ( VOICES_INTERNAL, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, @@ -91,7 +90,7 @@ class FlowLayout(QLayout): return len(self._item_list) def expandingDirections(self): - return Qt.Orientations(Qt.Orientation(0)) + return Qt.Orientation(0) def hasHeightForWidth(self): return True @@ -132,10 +131,10 @@ class FlowLayout(QLayout): for item in self._item_list: style = self.parentWidget().style() if self.parentWidget() else QStyle() layout_spacing_x = style.layoutSpacing( - QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal + QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Horizontal ) layout_spacing_y = style.layoutSpacing( - QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical + QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Vertical ) space_x = spacing if spacing >= 0 else layout_spacing_x space_y = spacing if spacing >= 0 else layout_spacing_y @@ -163,7 +162,7 @@ class VoiceMixer(QWidget): super().__init__() self.voice_name = voice_name self.setFixedWidth(VOICE_MIXER_WIDTH) - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # TODO Set CSS for rounded corners # self.setObjectName("VoiceMixer") @@ -173,7 +172,7 @@ class VoiceMixer(QWidget): # Name label at the top name = voice_name - layout.addWidget(QLabel(name), alignment=Qt.AlignCenter) + layout.addWidget(QLabel(name), alignment=Qt.AlignmentFlag.AlignCenter) # Voice name label with gender icon is_female = self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f" @@ -181,7 +180,7 @@ class VoiceMixer(QWidget): # Icons layout (flag and gender) icons_layout = QHBoxLayout() icons_layout.setSpacing(3) - icons_layout.setAlignment(Qt.AlignCenter) # Center the icons horizontally + icons_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the icons horizontally # Flag icon flag_icon_path = get_resource_path( @@ -194,11 +193,11 @@ class VoiceMixer(QWidget): gender_label = QLabel() flag_pixmap = QPixmap(flag_icon_path) flag_label.setPixmap( - flag_pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation) + flag_pixmap.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) ) gender_pixmap = QPixmap(gender_icon_path) gender_label.setPixmap( - gender_pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation) + gender_pixmap.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) ) icons_layout.addWidget(flag_label) icons_layout.addWidget(gender_label) @@ -210,7 +209,7 @@ class VoiceMixer(QWidget): self.checkbox = QCheckBox() self.checkbox.setChecked(initial_status) self.checkbox.stateChanged.connect(self.toggle_inputs) - layout.addWidget(self.checkbox, alignment=Qt.AlignCenter) + layout.addWidget(self.checkbox, alignment=Qt.AlignmentFlag.AlignCenter) # Spinbox and slider self.spin_box = QDoubleSpinBox() @@ -219,10 +218,10 @@ class VoiceMixer(QWidget): self.spin_box.setDecimals(2) self.spin_box.setValue(initial_weight) - self.slider = QSlider(Qt.Vertical) + self.slider = QSlider(Qt.Orientation.Vertical) self.slider.setRange(0, 100) self.slider.setValue(int(initial_weight * 100)) - self.slider.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) + self.slider.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) self.slider.setFixedWidth(SLIDER_WIDTH) # Fix slider in Windows @@ -249,17 +248,17 @@ class VoiceMixer(QWidget): # Layout for slider and labels slider_layout = QVBoxLayout() slider_layout.addWidget(self.spin_box) - slider_layout.addWidget(QLabel("1", alignment=Qt.AlignCenter)) + slider_layout.addWidget(QLabel("1", alignment=Qt.AlignmentFlag.AlignCenter)) slider_center_layout = QHBoxLayout() - slider_center_layout.addWidget(self.slider, alignment=Qt.AlignHCenter) + slider_center_layout.addWidget(self.slider, alignment=Qt.AlignmentFlag.AlignHCenter) slider_center_layout.setContentsMargins(0, 0, 0, 0) slider_center_widget = QWidget() slider_center_widget.setLayout(slider_center_layout) slider_layout.addWidget(slider_center_widget, stretch=1) - slider_layout.addWidget(QLabel("0", alignment=Qt.AlignCenter)) + slider_layout.addWidget(QLabel("0", alignment=Qt.AlignmentFlag.AlignCenter)) slider_layout.setStretch(2, 1) layout.addLayout(slider_layout, stretch=1) @@ -307,9 +306,9 @@ class HoverLabel(QLabel): """ ) # Make sure the entire button is clickable, not just the text - self.delete_button.setFocusPolicy(Qt.NoFocus) - self.delete_button.setAttribute(Qt.WA_TransparentForMouseEvents, False) - self.delete_button.setCursor(Qt.PointingHandCursor) + self.delete_button.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.delete_button.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor) self.delete_button.hide() def resizeEvent(self, event): @@ -357,7 +356,7 @@ class VoiceFormulaDialog(QDialog): if parent is not None and hasattr(parent, "subtitle_combo"): self.subtitle_combo = parent.subtitle_combo # Create main container layout with profile section and mixer section - splitter = QSplitter(Qt.Horizontal) + splitter = QSplitter(Qt.Orientation.Horizontal) # Profile section profile_widget = QWidget() profile_layout = QVBoxLayout(profile_widget) @@ -371,8 +370,8 @@ class VoiceFormulaDialog(QDialog): profile_layout.addLayout(header_layout) # Profile list self.profile_list = QListWidget() - self.profile_list.setSelectionMode(QListWidget.SingleSelection) - self.profile_list.setSelectionBehavior(QListWidget.SelectRows) + self.profile_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection) + self.profile_list.setSelectionBehavior(QListWidget.SelectionBehavior.SelectRows) self.profile_list.setStyleSheet( "QListWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }" ) @@ -388,7 +387,7 @@ class VoiceFormulaDialog(QDialog): idx = list(profiles.keys()).index(self.current_profile) self.profile_list.setCurrentRow(idx) profile_layout.addWidget(self.profile_list) - self.profile_list.setContextMenuPolicy(Qt.CustomContextMenu) + self.profile_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.profile_list.customContextMenuRequested.connect( self.show_profile_context_menu ) @@ -409,7 +408,7 @@ class VoiceFormulaDialog(QDialog): self.setWindowTitle("Voice Mixer") self.setWindowFlags( - Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint + Qt.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMaximizeButtonHint ) self.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT) @@ -468,21 +467,21 @@ class VoiceFormulaDialog(QDialog): # Separator separator = QFrame() - separator.setFrameShadow(QFrame.Sunken) + separator.setFrameShadow(QFrame.Shadow.Sunken) mixer_layout.addWidget(separator) # Voice list scroll area self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) - self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.scroll_area.viewport().installEventFilter(self) self.voice_list_widget = QWidget() self.voice_list_layout = QHBoxLayout() self.voice_list_widget.setLayout(self.voice_list_layout) self.voice_list_widget.setSizePolicy( - QSizePolicy.Expanding, QSizePolicy.Expanding + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) self.scroll_area.setWidget(self.voice_list_widget) mixer_layout.addWidget(self.scroll_area, stretch=1) @@ -497,6 +496,9 @@ class VoiceFormulaDialog(QDialog): ok_button.setDefault(True) ok_button.setFocus() + # Connect buttons + clear_all_button.clicked.connect(self.clear_all_voices) + ok_button.clicked.connect(self.accept) # Connect buttons clear_all_button.clicked.connect(self.clear_all_voices) ok_button.clicked.connect(self.accept) @@ -533,7 +535,7 @@ class VoiceFormulaDialog(QDialog): def keyPressEvent(self, event): # Bind Delete key to delete_profile when a profile is selected - if event.key() == Qt.Key_Delete and self.profile_list.hasFocus(): + if event.key() == Qt.Key.Key_Delete and self.profile_list.hasFocus(): item = self.profile_list.currentItem() if item: self.delete_profile(item) @@ -562,10 +564,10 @@ class VoiceFormulaDialog(QDialog): self, "Unsaved Changes", msg, - QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, - QMessageBox.Save, + QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, + QMessageBox.StandardButton.Save, ) - if ret == QMessageBox.Save: + if ret == QMessageBox.StandardButton.Save: # Save all using stored states profiles = load_profiles() for i in dirty_indices: @@ -590,7 +592,7 @@ class VoiceFormulaDialog(QDialog): self.update_profile_save_buttons() self.update_profile_list_colors() return True - elif ret == QMessageBox.Discard: + elif ret == QMessageBox.StandardButton.Discard: # Discard all modifications self._profile_states.clear() for i in dirty_indices: @@ -612,17 +614,17 @@ class VoiceFormulaDialog(QDialog): else: # Fallback to original logic for 0 or 1 dirty profile box = QMessageBox(self) - box.setIcon(QMessageBox.Warning) + box.setIcon(QMessageBox.Icon.Warning) box.setWindowTitle("Unsaved Changes") box.setText( "You have unsaved changes in your profile. Do you want to save the changes?" ) box.setStandardButtons( - QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel + QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel ) - box.setDefaultButton(QMessageBox.Save) - ret = box.exec_() - if ret == QMessageBox.Save: + box.setDefaultButton(QMessageBox.StandardButton.Save) + ret = box.exec() + if ret == QMessageBox.StandardButton.Save: for i in range(self.profile_list.count()): item = self.profile_list.item(i) name = item.text().lstrip("*") @@ -636,7 +638,7 @@ class VoiceFormulaDialog(QDialog): if hasattr(parent, "populate_profiles_in_voice_combo"): parent.populate_profiles_in_voice_combo() return True - elif ret == QMessageBox.Discard: + elif ret == QMessageBox.StandardButton.Discard: profiles = load_profiles() for i in range(self.profile_list.count()): item = self.profile_list.item(i) @@ -717,7 +719,7 @@ class VoiceFormulaDialog(QDialog): return voice_mixer def handle_voice_checkbox(self, voice_mixer, state): - if state == Qt.Checked: + if state == Qt.CheckState.Checked.value: self.last_enabled_voice = voice_mixer.voice_name self.update_weighted_sums() @@ -767,7 +769,7 @@ class VoiceFormulaDialog(QDialog): f'{name}: {percentage:.1f}%', name, ) - voice_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + voice_label.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) voice_label.delete_button.clicked.connect( lambda _, vn=name: self.disable_voice_by_name(vn) ) @@ -787,7 +789,7 @@ class VoiceFormulaDialog(QDialog): mixer.checkbox.setChecked(False) def eventFilter(self, source, event): - if source is self.scroll_area.viewport() and event.type() == event.Wheel: + if source is self.scroll_area.viewport() and event.type() == event.Type.Wheel: # Skip if over an enabled slider if any( mixer.slider.underMouse() and mixer.slider.isEnabled() @@ -890,10 +892,10 @@ class VoiceFormulaDialog(QDialog): self, "Invalid Profiles", msg, - QMessageBox.Yes | QMessageBox.Cancel, - QMessageBox.Yes, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, + QMessageBox.StandardButton.Yes, ) - if reply == QMessageBox.Yes: + if reply == QMessageBox.StandardButton.Yes: for i, name in reversed(zero): self.profile_list.takeItem(i) delete_profile(name) @@ -980,8 +982,8 @@ class VoiceFormulaDialog(QDialog): super().closeEvent(event) def _parse_rgba_to_qcolor(self, rgba_str): - from PyQt5.QtCore import Qt - from PyQt5.QtGui import QColor + from PyQt6.QtCore import Qt + from PyQt6.QtGui import QColor """Helper to convert 'rgba(R,G,B,A_float)' string to QColor.""" match = re.match(r"rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)", rgba_str) @@ -1136,9 +1138,9 @@ class VoiceFormulaDialog(QDialog): msg += f"\nThis will overwrite an existing profile." msg += "\nContinue?" reply = QMessageBox.question( - self, "Import Profile", msg, QMessageBox.Yes | QMessageBox.No + self, "Import Profile", msg, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return profiles.update(imported_profiles) save_profiles(profiles) @@ -1153,9 +1155,9 @@ class VoiceFormulaDialog(QDialog): msg += f"\n{len(collisions)} profile(s) will be overwritten." msg += "\nContinue?" reply = QMessageBox.question( - self, "Import Profiles", msg, QMessageBox.Yes | QMessageBox.No + self, "Import Profiles", msg, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return profiles.update(imported_profiles) save_profiles(profiles) @@ -1196,7 +1198,7 @@ class VoiceFormulaDialog(QDialog): menu.addAction(dup_act) menu.addAction(export_act) menu.addAction(delete_act) - act = menu.exec_(self.profile_list.viewport().mapToGlobal(pos)) + act = menu.exec(self.profile_list.viewport().mapToGlobal(pos)) if act == rename_act: self.rename_profile(item) elif act == delete_act: @@ -1323,9 +1325,9 @@ class VoiceFormulaDialog(QDialog): self, "Delete Profile", f"Delete profile '{name}'?", - QMessageBox.Yes | QMessageBox.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if reply == QMessageBox.Yes: + if reply == QMessageBox.StandardButton.Yes: delete_profile(name) row = self.profile_list.row(item) self.profile_list.takeItem(row) @@ -1378,7 +1380,7 @@ class VoiceFormulaDialog(QDialog): self.profile_list.setItemWidget(item, widget) def update_profile_list_colors(self): - from PyQt5.QtCore import Qt + from PyQt6.QtCore import Qt profiles = load_profiles() for i in range(self.profile_list.count()): @@ -1386,13 +1388,13 @@ class VoiceFormulaDialog(QDialog): name = item.text().lstrip("*") if self._virtual_new_profile and name == "New profile": color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND")) - item.setData(Qt.BackgroundRole, color) + item.setData(Qt.ItemDataRole.BackgroundRole, color) elif item.text().startswith("*"): color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND")) - item.setData(Qt.BackgroundRole, color) + item.setData(Qt.ItemDataRole.BackgroundRole, color) else: item.setData( - Qt.BackgroundRole, self.profile_list.palette().base().color() + Qt.ItemDataRole.BackgroundRole, self.profile_list.palette().base().color() ) weights = profiles.get(name, {}).get("voices", []) total = 0 @@ -1406,7 +1408,7 @@ class VoiceFormulaDialog(QDialog): total += entry[1] if total == 0: color = self._parse_rgba_to_qcolor(COLORS.get("RED_BACKGROUND")) - item.setData(Qt.BackgroundRole, color) + item.setData(Qt.ItemDataRole.BackgroundRole, color) self.update_profile_save_buttons() def preview_current_mix(self): diff --git a/pyproject.toml b/pyproject.toml index b668986..a167281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ license = "MIT" requires-python = ">=3.10, <3.13" keywords = ["audiobook", "epub", "pdf", "text-to-speech", "subtitle", "tts", "kokoro", "accessibility", "book-converter", "voice-synthesis", "multilingual", "chapter-management", "subtitles", "content-creation", "media-generation"] dependencies = [ - "PyQt5>=5.15.11", + "PyQt6>=6.10.0", "kokoro>=0.9.4", "misaki[zh]>=0.9.4", "ebooklib>=0.19",