Upgraded Abogen's interface from PyQt5 to PyQt6, Added tooltip indicators in queue manager

This commit is contained in:
Deniz Şafak
2025-10-27 17:08:32 +03:00
parent 0d0d3b7871
commit eedf347866
11 changed files with 406 additions and 340 deletions
+2
View File
@@ -1,4 +1,6 @@
# 1.2.1 (pre-release) # 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. - Added loading gif animation to book handler window.
- Fixed subtitle word-count splitting logic for more accurate segmentation. - Fixed subtitle word-count splitting logic for more accurate segmentation.
+1 -1
View File
@@ -12,7 +12,7 @@ RUN apt-get update \
python3 \ python3 \
python3-venv \ python3-venv \
python3-pip \ python3-pip \
python3-pyqt5 \ python3-pyqt6 \
espeak-ng \ espeak-ng \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+131 -121
View File
@@ -4,24 +4,34 @@ import base64
import fitz # PyMuPDF for PDF support import fitz # PyMuPDF for PDF support
from ebooklib import epub from ebooklib import epub
from bs4 import BeautifulSoup, NavigableString from bs4 import BeautifulSoup, NavigableString
from PyQt5.QtWidgets import ( from PyQt6.QtGui import QAction, QMovie, QFont
from PyQt6.QtWidgets import (
QApplication,
QDialog, QDialog,
QTreeWidget, QTreeWidget,
QTreeWidgetItem, QTreeWidgetItem,
QDialogButtonBox, QTextEdit,
QPushButton,
QVBoxLayout, QVBoxLayout,
QHBoxLayout, QHBoxLayout,
QTextEdit, QDialogButtonBox,
QTreeWidgetItemIterator,
QSplitter, QSplitter,
QWidget, QWidget,
QPushButton,
QCheckBox, QCheckBox,
QMenu, QTreeWidgetItemIterator,
QLabel, 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 from abogen.utils import clean_text, calculate_text_length, detect_encoding, get_resource_path
import os import os
import logging # Add logging import logging # Add logging
@@ -97,9 +107,9 @@ class HandlerDialog(QDialog):
self._block_signals = False # Flag to prevent recursive signals self._block_signals = False # Flag to prevent recursive signals
# Configure window: remove help button and allow resizing # Configure window: remove help button and allow resizing
self.setWindowFlags( 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 # Initialize save chapters flags from class variables
self.save_chapters_separately = HandlerDialog._save_chapters_separately self.save_chapters_separately = HandlerDialog._save_chapters_separately
self.merge_chapters_at_end = HandlerDialog._merge_chapters_at_end self.merge_chapters_at_end = HandlerDialog._merge_chapters_at_end
@@ -164,8 +174,8 @@ class HandlerDialog(QDialog):
# Build treeview # Build treeview
self.treeWidget = QTreeWidget(self) self.treeWidget = QTreeWidget(self)
self.treeWidget.setHeaderHidden(True) self.treeWidget.setHeaderHidden(True)
self.treeWidget.setSelectionMode(QTreeWidget.ExtendedSelection) self.treeWidget.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) self.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.treeWidget.customContextMenuRequested.connect(self.on_tree_context_menu) self.treeWidget.customContextMenuRequested.connect(self.on_tree_context_menu)
# Initialize checked_chapters set # Initialize checked_chapters set
@@ -177,8 +187,8 @@ class HandlerDialog(QDialog):
# Add a placeholder "Information" item so the tree isn't empty immediately # Add a placeholder "Information" item so the tree isn't empty immediately
info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) info_item = QTreeWidgetItem(self.treeWidget, ["Information"])
info_item.setData(0, Qt.UserRole, "info:bookinfo") info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo")
info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable) info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
font = info_item.font(0) font = info_item.font(0)
font.setBold(True) font.setBold(True)
info_item.setFont(0, font) info_item.setFont(0, font)
@@ -243,8 +253,8 @@ class HandlerDialog(QDialog):
# Add stretches to center the content horizontally # Add stretches to center the content horizontally
h.addStretch(1) h.addStretch(1)
h.addWidget(gif_label, 0, Qt.AlignVCenter) h.addWidget(gif_label, 0, Qt.AlignmentFlag.AlignVCenter)
h.addWidget(text_label, 0, Qt.AlignVCenter) h.addWidget(text_label, 0, Qt.AlignmentFlag.AlignVCenter)
h.addStretch(1) h.addStretch(1)
# Insert at top of main layout if present, otherwise keep as child # Insert at top of main layout if present, otherwise keep as child
@@ -1148,8 +1158,8 @@ class HandlerDialog(QDialog):
self.treeWidget.clear() self.treeWidget.clear()
info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) info_item = QTreeWidgetItem(self.treeWidget, ["Information"])
info_item.setData(0, Qt.UserRole, "info:bookinfo") info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo")
info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable) info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
font = info_item.font(0) font = info_item.font(0)
font.setBold(True) font.setBold(True)
info_item.setFont(0, font) info_item.setFont(0, font)
@@ -1172,7 +1182,7 @@ class HandlerDialog(QDialog):
has_parents = False has_parents = False
iterator = QTreeWidgetItemIterator( iterator = QTreeWidgetItemIterator(
self.treeWidget, QTreeWidgetItemIterator.HasChildren self.treeWidget, QTreeWidgetItemIterator.IteratorFlag.HasChildren
) )
if iterator.value(): if iterator.value():
has_parents = True has_parents = True
@@ -1195,7 +1205,7 @@ class HandlerDialog(QDialog):
children = node.get("children", []) children = node.get("children", [])
item = QTreeWidgetItem(parent_item, [title]) item = QTreeWidgetItem(parent_item, [title])
item.setData(0, Qt.UserRole, src) item.setData(0, Qt.ItemDataRole.UserRole, src)
is_empty = ( is_empty = (
src src
@@ -1211,18 +1221,18 @@ class HandlerDialog(QDialog):
seen_content_hashes.add(content_hash) seen_content_hashes.add(content_hash)
if src and not is_empty and not is_duplicate: 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 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: elif is_duplicate:
# Mark as duplicate and remove checkbox # Mark as duplicate and remove checkbox
item.setText(0, f"{title} (Duplicate)") item.setText(0, f"{title} (Duplicate)")
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
elif children: elif children:
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(0, Qt.Unchecked) item.setCheckState(0, Qt.CheckState.Unchecked)
else: else:
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
if children: if children:
self._build_epub_tree_from_nav(children, item, seen_content_hashes) self._build_epub_tree_from_nav(children, item, seen_content_hashes)
@@ -1252,18 +1262,18 @@ class HandlerDialog(QDialog):
continue continue
item = QTreeWidgetItem(parent_item, [title]) item = QTreeWidgetItem(parent_item, [title])
item.setData(0, Qt.UserRole, href) item.setData(0, Qt.ItemDataRole.UserRole, href)
has_content = ( has_content = (
href and href in self.content_texts and self.content_texts[href].strip() href and href in self.content_texts and self.content_texts[href].strip()
) )
if has_content or children: 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 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: else:
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
if children: if children:
self._build_epub_tree_fallback(children, item) self._build_epub_tree_fallback(children, item)
@@ -1318,27 +1328,27 @@ class HandlerDialog(QDialog):
if page_num in self.bookmark_items_map: if page_num in self.bookmark_items_map:
orig = self.bookmark_items_map[page_num] orig = self.bookmark_items_map[page_num]
child = QTreeWidgetItem(orig, [f"{title} (Same page)"]) child = QTreeWidgetItem(orig, [f"{title} (Same page)"])
child.setData(0, Qt.UserRole, page_id) child.setData(0, Qt.ItemDataRole.UserRole, page_id)
child.setFlags(child.flags() & ~Qt.ItemIsUserCheckable) child.setFlags(child.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
continue continue
bookmark_item = QTreeWidgetItem(parent_item, [title]) 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 # only allow checking if this chapter has content
if self.content_lengths.get(page_id, 0) > 0: if self.content_lengths.get(page_id, 0) > 0:
bookmark_item.setFlags( bookmark_item.setFlags(
bookmark_item.flags() | Qt.ItemIsUserCheckable bookmark_item.flags() | Qt.ItemFlag.ItemIsUserCheckable
) )
bookmark_item.setCheckState( bookmark_item.setCheckState(
0, 0,
( (
Qt.Checked Qt.CheckState.Checked
if page_id in self.checked_chapters if page_id in self.checked_chapters
else Qt.Unchecked else Qt.CheckState.Unchecked
), ),
) )
else: else:
bookmark_item.setFlags( bookmark_item.setFlags(
bookmark_item.flags() & ~Qt.ItemIsUserCheckable bookmark_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable
) )
# map for uncategorized pages # map for uncategorized pages
self.bookmark_items_map[page_num] = bookmark_item self.bookmark_items_map[page_num] = bookmark_item
@@ -1363,23 +1373,23 @@ class HandlerDialog(QDialog):
page_title += f" - {first_line}" page_title += f" - {first_line}"
page_item = QTreeWidgetItem(bookmark_item, [page_title]) 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 # only allow checking if this sub-page has content
if self.content_lengths.get(page_id, 0) > 0: if self.content_lengths.get(page_id, 0) > 0:
page_item.setFlags( page_item.setFlags(
page_item.flags() | Qt.ItemIsUserCheckable page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable
) )
page_item.setCheckState( page_item.setCheckState(
0, 0,
( (
Qt.Checked Qt.CheckState.Checked
if page_id in self.checked_chapters if page_id in self.checked_chapters
else Qt.Unchecked else Qt.CheckState.Unchecked
), ),
) )
else: else:
page_item.setFlags( page_item.setFlags(
page_item.flags() & ~Qt.ItemIsUserCheckable page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable
) )
added_pages.add(sub_page_num) added_pages.add(sub_page_num)
@@ -1405,15 +1415,15 @@ class HandlerDialog(QDialog):
if first and len(first) < 100: if first and len(first) < 100:
title += f" - {first}" title += f" - {first}"
page_item = QTreeWidgetItem(parent_item, [title]) 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 # only allow checking if uncategorized page has content
if self.content_lengths.get(page_id, 0) > 0: 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( 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: else:
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable) page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
def _build_markdown_tree(self): def _build_markdown_tree(self):
"""Build tree structure for markdown file based on parsed TOC.""" """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] chapter_id = list(self.content_texts.keys())[0]
title = "Content" title = "Content"
item = QTreeWidgetItem(self.treeWidget, [title]) 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: 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 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: else:
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
return return
def build_from_toc(toc_list, parent_item): def build_from_toc(toc_list, parent_item):
@@ -1441,17 +1451,17 @@ class HandlerDialog(QDialog):
chapter_id = header['id'] chapter_id = header['id']
item = QTreeWidgetItem(parent_item, [title]) 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_content = self.content_lengths.get(chapter_id, 0) > 0
has_children = bool(header.get('children')) has_children = bool(header.get('children'))
if has_content or has_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 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: else:
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
if has_children: if has_children:
build_from_toc(header['children'], item) build_from_toc(header['children'], item)
@@ -1460,7 +1470,7 @@ class HandlerDialog(QDialog):
def _build_pdf_pages_tree(self): def _build_pdf_pages_tree(self):
pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"]) 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 = pages_item.font(0)
font.setBold(True) font.setBold(True)
pages_item.setFont(0, font) pages_item.setFont(0, font)
@@ -1476,15 +1486,15 @@ class HandlerDialog(QDialog):
page_title += f" - {first_line}" page_title += f" - {first_line}"
page_item = QTreeWidgetItem(pages_item, [page_title]) 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 # only allow checking if standalone page has content
if self.content_lengths.get(page_id, 0) > 0: 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( 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: else:
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable) page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
def _are_provided_checks_relevant(self): def _are_provided_checks_relevant(self):
if not self.checked_chapters: if not self.checked_chapters:
@@ -1494,8 +1504,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.flags() & Qt.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if identifier: if identifier:
all_identifiers.add(identifier) all_identifiers.add(identifier)
iterator += 1 iterator += 1
@@ -1525,7 +1535,7 @@ class HandlerDialog(QDialog):
rightWidget = QWidget() rightWidget = QWidget()
rightWidget.setLayout(previewLayout) 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.accepted.connect(self.accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
@@ -1611,7 +1621,7 @@ class HandlerDialog(QDialog):
leftWidget = QWidget() leftWidget = QWidget()
leftWidget.setLayout(leftLayout) leftWidget.setLayout(leftLayout)
self.splitter = QSplitter(Qt.Horizontal) self.splitter = QSplitter(Qt.Orientation.Horizontal)
self.splitter.addWidget(leftWidget) self.splitter.addWidget(leftWidget)
self.splitter.addWidget(rightWidget) self.splitter.addWidget(rightWidget)
self.splitter.setSizes([280, 420]) self.splitter.setSizes([280, 420])
@@ -1643,8 +1653,8 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if ( if (
item.flags() & Qt.ItemIsUserCheckable item.flags() & Qt.ItemFlag.ItemIsUserCheckable
and item.checkState(0) == Qt.Checked and item.checkState(0) == Qt.CheckState.Checked
): ):
checked_count += 1 checked_count += 1
if checked_count >= 2: if checked_count >= 2:
@@ -1658,8 +1668,8 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if ( if (
item.flags() & Qt.ItemIsUserCheckable item.flags() & Qt.ItemFlag.ItemIsUserCheckable
and item.checkState(0) == Qt.Checked and item.checkState(0) == Qt.CheckState.Checked
): ):
parent = item.parent() parent = item.parent()
if parent and parent != self.treeWidget.invisibleRootItem(): if parent and parent != self.treeWidget.invisibleRootItem():
@@ -1683,8 +1693,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.flags() & Qt.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
item.setCheckState(0, Qt.Checked) item.setCheckState(0, Qt.CheckState.Checked)
iterator += 1 iterator += 1
self._block_signals = False self._block_signals = False
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
@@ -1694,8 +1704,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.flags() & Qt.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
item.setCheckState(0, Qt.Unchecked) item.setCheckState(0, Qt.CheckState.Unchecked)
iterator += 1 iterator += 1
self._block_signals = False self._block_signals = False
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
@@ -1705,8 +1715,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.flags() & Qt.ItemIsUserCheckable and item.childCount() > 0: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0:
item.setCheckState(0, Qt.Checked) item.setCheckState(0, Qt.CheckState.Checked)
iterator += 1 iterator += 1
self._block_signals = False self._block_signals = False
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
@@ -1716,8 +1726,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.flags() & Qt.ItemIsUserCheckable and item.childCount() > 0: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0:
item.setCheckState(0, Qt.Unchecked) item.setCheckState(0, Qt.CheckState.Unchecked)
iterator += 1 iterator += 1
self._block_signals = False self._block_signals = False
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
@@ -1742,30 +1752,30 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if not (item.flags() & Qt.ItemIsUserCheckable): if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
iterator += 1 iterator += 1
continue 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 has_significant_content = src and self.content_lengths.get(src, 0) > 1000
is_parent = item.childCount() > 0 is_parent = item.childCount() > 0
if has_significant_content or is_parent: if has_significant_content or is_parent:
item.setCheckState(0, Qt.Checked) item.setCheckState(0, Qt.CheckState.Checked)
if is_parent: if is_parent:
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) child = item.child(i)
if child.flags() & Qt.ItemIsUserCheckable: if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
child_src = child.data(0, Qt.UserRole) child_src = child.data(0, Qt.ItemDataRole.UserRole)
child_has_content = ( child_has_content = (
child_src and self.content_lengths.get(child_src, 0) > 0 child_src and self.content_lengths.get(child_src, 0) > 0
) )
child_is_parent = child.childCount() > 0 child_is_parent = child.childCount() > 0
if child_has_content or child_is_parent: if child_has_content or child_is_parent:
child.setCheckState(0, Qt.Checked) child.setCheckState(0, Qt.CheckState.Checked)
else: else:
item.setCheckState(0, Qt.Unchecked) item.setCheckState(0, Qt.CheckState.Unchecked)
iterator += 1 iterator += 1
@@ -1774,32 +1784,32 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if not (item.flags() & Qt.ItemIsUserCheckable): if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
iterator += 1 iterator += 1
continue continue
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
# Select chapters with content > 500 characters or parent items # Select chapters with content > 500 characters or parent items
has_significant_content = identifier and self.content_lengths.get(identifier, 0) > 500 has_significant_content = identifier and self.content_lengths.get(identifier, 0) > 500
is_parent = item.childCount() > 0 is_parent = item.childCount() > 0
if has_significant_content or is_parent: 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 # Also check children if this is a parent
if is_parent: if is_parent:
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) child = item.child(i)
if child.flags() & Qt.ItemIsUserCheckable: if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
child_identifier = child.data(0, Qt.UserRole) child_identifier = child.data(0, Qt.ItemDataRole.UserRole)
child_has_content = ( child_has_content = (
child_identifier and self.content_lengths.get(child_identifier, 0) > 0 child_identifier and self.content_lengths.get(child_identifier, 0) > 0
) )
child_is_parent = child.childCount() > 0 child_is_parent = child.childCount() > 0
if child_has_content or child_is_parent: if child_has_content or child_is_parent:
child.setCheckState(0, Qt.Checked) child.setCheckState(0, Qt.CheckState.Checked)
else: else:
item.setCheckState(0, Qt.Unchecked) item.setCheckState(0, Qt.CheckState.Unchecked)
iterator += 1 iterator += 1
@@ -1808,19 +1818,19 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.flags() & Qt.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
item.setCheckState(0, Qt.Checked) item.setCheckState(0, Qt.CheckState.Checked)
iterator += 1 iterator += 1
return return
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if not (item.flags() & Qt.ItemIsUserCheckable): if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
iterator += 1 iterator += 1
continue continue
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if not identifier: if not identifier:
iterator += 1 iterator += 1
@@ -1830,7 +1840,7 @@ class HandlerDialog(QDialog):
not identifier.startswith("page_") not identifier.startswith("page_")
or self.content_lengths.get(identifier, 0) > 0 or self.content_lengths.get(identifier, 0) > 0
): ):
item.setCheckState(0, Qt.Checked) item.setCheckState(0, Qt.CheckState.Checked)
iterator += 1 iterator += 1
@@ -1839,8 +1849,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.checkState(0) == Qt.Checked: if item.checkState(0) == Qt.CheckState.Checked:
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if identifier: if identifier:
self.checked_chapters.add(identifier) self.checked_chapters.add(identifier)
iterator += 1 iterator += 1
@@ -1853,17 +1863,17 @@ class HandlerDialog(QDialog):
self._block_signals = True self._block_signals = True
if item.flags() & Qt.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) child = item.child(i)
if child.flags() & Qt.ItemIsUserCheckable: if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
child.setCheckState(0, item.checkState(0)) child.setCheckState(0, item.checkState(0))
self._block_signals = False self._block_signals = False
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
def handle_item_double_click(self, item, column=0): 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) rect = self.treeWidget.visualItemRect(item)
checkbox_width = 20 checkbox_width = 20
@@ -1871,7 +1881,7 @@ class HandlerDialog(QDialog):
if mouse_pos.x() > rect.x() + checkbox_width: if mouse_pos.x() > rect.x() + checkbox_width:
new_state = ( 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) item.setCheckState(0, new_state)
@@ -1880,7 +1890,7 @@ class HandlerDialog(QDialog):
self.previewEdit.clear() self.previewEdit.clear()
return return
identifier = current.data(0, Qt.UserRole) identifier = current.data(0, Qt.ItemDataRole.UserRole)
if identifier == "info:bookinfo": if identifier == "info:bookinfo":
self._display_book_info() self._display_book_info()
@@ -2172,8 +2182,8 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
item_order_counter += 1 item_order_counter += 1
if item.checkState(0) == Qt.Checked: if item.checkState(0) == Qt.CheckState.Checked:
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if identifier and identifier != "info:bookinfo": if identifier and identifier != "info:bookinfo":
all_checked_identifiers.add(identifier) all_checked_identifiers.add(identifier)
@@ -2208,8 +2218,8 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
item_order_counter += 1 item_order_counter += 1
if item.checkState(0) == Qt.Checked: if item.checkState(0) == Qt.CheckState.Checked:
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if identifier and identifier != "info:bookinfo": if identifier and identifier != "info:bookinfo":
all_checked_identifiers.add(identifier) all_checked_identifiers.add(identifier)
ordered_checked_items.append((item_order_counter, item, identifier)) ordered_checked_items.append((item_order_counter, item, identifier))
@@ -2244,8 +2254,8 @@ class HandlerDialog(QDialog):
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.checkState(0) == Qt.Checked: if item.checkState(0) == Qt.CheckState.Checked:
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if identifier: if identifier:
all_checked_identifiers.add(identifier) all_checked_identifiers.add(identifier)
iterator += 1 iterator += 1
@@ -2270,15 +2280,15 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if item.childCount() > 0: if item.childCount() > 0:
parent_checked = item.checkState(0) == Qt.Checked parent_checked = item.checkState(0) == Qt.CheckState.Checked
parent_id = item.data(0, Qt.UserRole) parent_id = item.data(0, Qt.ItemDataRole.UserRole)
parent_title = item.text(0) parent_title = item.text(0)
checked_children = [] checked_children = []
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) child = item.child(i)
child_id = child.data(0, Qt.UserRole) child_id = child.data(0, Qt.ItemDataRole.UserRole)
if ( if (
child.checkState(0) == Qt.Checked child.checkState(0) == Qt.CheckState.Checked
and child_id and child_id
and child_id not in included_text_ids and child_id not in included_text_ids
): ):
@@ -2306,12 +2316,12 @@ class HandlerDialog(QDialog):
else: else:
section_titles.append((title, text)) section_titles.append((title, text))
included_text_ids.add(child_id) included_text_ids.add(child_id)
elif item.flags() & Qt.ItemIsUserCheckable: elif item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
identifier = item.data(0, Qt.UserRole) identifier = item.data(0, Qt.ItemDataRole.UserRole)
if ( if (
identifier identifier
and identifier not in included_text_ids 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, "") text = self.content_texts.get(identifier, "")
if text: if text:
@@ -2363,8 +2373,8 @@ class HandlerDialog(QDialog):
print(f"Checking selected items: {state}") print(f"Checking selected items: {state}")
self.treeWidget.blockSignals(True) self.treeWidget.blockSignals(True)
for item in self.treeWidget.selectedItems(): for item in self.treeWidget.selectedItems():
if item.flags() & Qt.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
item.setCheckState(0, Qt.Checked if state else Qt.Unchecked) item.setCheckState(0, Qt.CheckState.Checked if state else Qt.CheckState.Unchecked)
self.treeWidget.blockSignals(False) self.treeWidget.blockSignals(False)
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
@@ -2377,30 +2387,30 @@ class HandlerDialog(QDialog):
action.triggered.connect(self.check_selected_items) action.triggered.connect(self.check_selected_items)
action = menu.addAction("Clear") action = menu.addAction("Clear")
action.triggered.connect(self.uncheck_selected_items) action.triggered.connect(self.uncheck_selected_items)
menu.exec_(self.treeWidget.mapToGlobal(pos)) menu.exec(self.treeWidget.mapToGlobal(pos))
return return
if ( if (
not item not item
or item.childCount() == 0 or item.childCount() == 0
or not (item.flags() & Qt.ItemIsUserCheckable) or not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable)
): ):
return return
menu = QMenu(self) 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" text = "Unselect only this" if checked else "Select only this"
action = menu.addAction(text) action = menu.addAction(text)
def do_toggle(): def do_toggle():
self.treeWidget.blockSignals(True) 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) item.setCheckState(0, new_state)
self.treeWidget.blockSignals(False) self.treeWidget.blockSignals(False)
self._update_checked_set_from_tree() self._update_checked_set_from_tree()
action.triggered.connect(do_toggle) action.triggered.connect(do_toggle)
menu.exec_(self.treeWidget.mapToGlobal(pos)) menu.exec(self.treeWidget.mapToGlobal(pos))
def closeEvent(self, event): def closeEvent(self, event):
if self.pdf_doc is not None: if self.pdf_doc is not None:
+1
View File
@@ -148,6 +148,7 @@ COLORS = {
"BLUE_BORDER_HOVER": "#6ab0de", "BLUE_BORDER_HOVER": "#6ab0de",
"YELLOW_BACKGROUND": "rgba(255, 221, 51, 0.40)", "YELLOW_BACKGROUND": "rgba(255, 221, 51, 0.40)",
"GREY_BACKGROUND": "rgba(128, 128, 128, 0.15)", "GREY_BACKGROUND": "rgba(128, 128, 128, 0.15)",
"GREY_BORDER": "#808080",
"RED_BACKGROUND": "rgba(232, 78, 60, 0.15)", "RED_BACKGROUND": "rgba(232, 78, 60, 0.15)",
"RED_BG": "rgba(232, 78, 60, 0.10)", "RED_BG": "rgba(232, 78, 60, 0.10)",
"RED_BG_HOVER": "rgba(232, 78, 60, 0.15)", "RED_BG_HOVER": "rgba(232, 78, 60, 0.15)",
+6 -6
View File
@@ -3,8 +3,8 @@ import re
import time import time
import hashlib # For generating unique cache filenames import hashlib # For generating unique cache filenames
from platformdirs import user_desktop_dir from platformdirs import user_desktop_dir
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
import soundfile as sf import soundfile as sf
from abogen.utils import clean_text, create_process, get_user_cache_path, detect_encoding from abogen.utils import clean_text, create_process, get_user_cache_path, detect_encoding
from abogen.constants import ( from abogen.constants import (
@@ -96,8 +96,8 @@ class ChapterOptionsDialog(QDialog):
# Prevent closing with the X button and remove the help button # Prevent closing with the X button and remove the help button
self.setWindowFlags( self.setWindowFlags(
self.windowFlags() self.windowFlags()
& ~Qt.WindowCloseButtonHint & ~Qt.WindowType.WindowCloseButtonHint
& ~Qt.WindowContextHelpButtonHint & ~Qt.WindowType.WindowContextHelpButtonHint
) )
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@@ -131,7 +131,7 @@ class ChapterOptionsDialog(QDialog):
layout.addWidget(self.countdown_label) layout.addWidget(self.countdown_label)
# Add OK button # Add OK button
button_box = QDialogButtonBox(QDialogButtonBox.Ok) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
button_box.accepted.connect(self.accept) button_box.accepted.connect(self.accept)
layout.addWidget(button_box) layout.addWidget(button_box)
@@ -180,7 +180,7 @@ class ChapterOptionsDialog(QDialog):
# Prevent escape key from closing the dialog # Prevent escape key from closing the dialog
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape: if event.key() == Qt.Key.Key_Escape:
event.ignore() event.ignore()
else: else:
super().keyPressEvent(event) super().keyPressEvent(event)
+110 -97
View File
@@ -8,28 +8,31 @@ from abogen.queue_manager_gui import QueueManager
from abogen.queued_item import QueuedItem from abogen.queued_item import QueuedItem
import abogen.hf_tracker as hf_tracker import abogen.hf_tracker as hf_tracker
import hashlib # Added for cache path generation import hashlib # Added for cache path generation
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QApplication,
QMainWindow,
QWidget, QWidget,
QVBoxLayout, QVBoxLayout,
QHBoxLayout, QHBoxLayout,
QPushButton, QPushButton,
QFileDialog,
QLabel,
QProgressBar,
QSlider,
QComboBox, QComboBox,
QSizePolicy,
QTextEdit, QTextEdit,
QFileIconProvider, QLabel,
QSlider,
QMessageBox, QMessageBox,
QFileDialog,
QProgressBar,
QFrame,
QStyleFactory,
QInputDialog,
QFileIconProvider,
QSizePolicy,
QDialog, QDialog,
QCheckBox, QCheckBox,
QMenu, QMenu,
QAction,
QActionGroup,
) )
from PyQt5.QtCore import ( from PyQt6.QtGui import QAction, QActionGroup
from PyQt6.QtCore import (
Qt, Qt,
QUrl, QUrl,
QPoint, QPoint,
@@ -42,8 +45,9 @@ from PyQt5.QtCore import (
QSize, QSize,
QTimer, QTimer,
QEvent, QEvent,
QProcess,
) )
from PyQt5.QtGui import ( from PyQt6.QtGui import (
QTextCursor, QTextCursor,
QDesktopServices, QDesktopServices,
QIcon, QIcon,
@@ -52,6 +56,7 @@ from PyQt5.QtGui import (
QPolygon, QPolygon,
QColor, QColor,
QMovie, QMovie,
QPalette,
) )
from abogen.utils import ( from abogen.utils import (
load_config, load_config,
@@ -95,12 +100,12 @@ class DarkTitleBarEventFilter(QObject):
self.set_title_bar_dark_mode = set_title_bar_dark_mode_func self.set_title_bar_dark_mode = set_title_bar_dark_mode_func
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
if event.type() == QEvent.Show: if event.type() == QEvent.Type.Show:
# Only apply to QWidget windows # Only apply to QWidget windows
if isinstance(obj, QWidget) and obj.isWindow(): if isinstance(obj, QWidget) and obj.isWindow():
if self.is_windows and self.get_dark_mode(): if self.is_windows and self.get_dark_mode():
self.set_title_bar_dark_mode(obj, True) self.set_title_bar_dark_mode(obj, True)
return False return super().eventFilter(obj, event)
class ShowWarningSignalEmitter(QObject): # New class to handle signal emission class ShowWarningSignalEmitter(QObject): # New class to handle signal emission
@@ -154,7 +159,7 @@ class InputBox(QLabel):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setAlignment(Qt.AlignCenter) self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.setText( self.setText(
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)" "Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)"
@@ -162,8 +167,8 @@ class InputBox(QLabel):
self.setStyleSheet( self.setStyleSheet(
f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}" f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
) )
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.setCursor(Qt.PointingHandCursor) self.setCursor(Qt.CursorShape.PointingHandCursor)
# Add clear button # Add clear button
self.clear_btn = QPushButton("", self) self.clear_btn = QPushButton("", self)
@@ -223,7 +228,7 @@ class InputBox(QLabel):
pixmap = qicon.pixmap(size) pixmap = qicon.pixmap(size)
# convert to base64 PNG # convert to base64 PNG
buffer = QBuffer() buffer = QBuffer()
buffer.open(QIODevice.WriteOnly) buffer.open(QIODevice.OpenModeFlag.WriteOnly)
pixmap.save(buffer, "PNG") pixmap.save(buffer, "PNG")
img_data = base64.b64encode(buffer.data()).decode() img_data = base64.b64encode(buffer.data()).decode()
@@ -356,6 +361,9 @@ class InputBox(QLabel):
self.window().displayed_file_path = ( self.window().displayed_file_path = (
None # Reset the displayed file path when clearing input 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( self.setText(
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)" "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" return f"{size:.{decimal_places}f} PB"
def mousePressEvent(self, event): def mousePressEvent(self, event):
if event.button() == Qt.LeftButton: if event.button() == Qt.MouseButton.LeftButton:
self.window().open_file_dialog() self.window().open_file_dialog()
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
@@ -542,7 +550,7 @@ class InputBox(QLabel):
menu.addAction(act_input) menu.addAction(act_input)
# Show the menu anchored to the button # 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: else:
if ( if (
file_to_check file_to_check
@@ -560,7 +568,7 @@ class TextboxDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Enter Text") self.setWindowTitle("Enter Text")
self.setWindowFlags( self.setWindowFlags(
Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint Qt.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMaximizeButtonHint
) )
self.resize(700, 500) self.resize(700, 500)
@@ -634,16 +642,16 @@ class TextboxDialog(QDialog):
# Check if we need to warn about overwriting a non-temporary file # 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: if hasattr(self, "is_non_cache_file") and self.is_non_cache_file:
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Warning) msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("File Overwrite Warning") msg_box.setWindowTitle("File Overwrite Warning")
msg_box.setText( msg_box.setText(
f"You are about to overwrite the original file:\n{self.non_cache_file_path}" 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.setInformativeText("Do you want to continue?")
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
msg_box.setDefaultButton(QMessageBox.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 # User canceled, don't close the dialog
return return
@@ -873,7 +881,7 @@ class abogen(QWidget):
self.log_text = QTextEdit(self) self.log_text = QTextEdit(self)
self.log_text.setReadOnly(True) self.log_text.setReadOnly(True)
self.log_text.setUndoRedoEnabled(False) 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.setStyleSheet("QTextEdit { border: none; }")
self.log_text.hide() self.log_text.hide()
container_layout.addWidget(self.log_text, 1) container_layout.addWidget(self.log_text, 1)
@@ -884,11 +892,11 @@ class abogen(QWidget):
speed_layout = QVBoxLayout() speed_layout = QVBoxLayout()
speed_layout.setSpacing(2) speed_layout.setSpacing(2)
speed_layout.addWidget(QLabel("Speed:", self)) 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.setMinimum(10)
self.speed_slider.setMaximum(200) self.speed_slider.setMaximum(200)
self.speed_slider.setValue(100) 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.setTickInterval(5)
self.speed_slider.setSingleStep(5) self.speed_slider.setSingleStep(5)
speed_layout.addWidget(self.speed_slider) speed_layout.addWidget(self.speed_slider)
@@ -910,7 +918,7 @@ class abogen(QWidget):
"The first character represents the language:\n" "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' '"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_layout.addWidget(self.voice_combo)
# Voice formula button # Voice formula button
self.btn_voice_formula_mixer = QPushButton(self) self.btn_voice_formula_mixer = QPushButton(self)
@@ -925,11 +933,11 @@ class abogen(QWidget):
# Play/Stop icons # Play/Stop icons
def make_icon(color, shape): def make_icon(color, shape):
pix = QPixmap(20, 20) pix = QPixmap(20, 20)
pix.fill(Qt.transparent) pix.fill(Qt.GlobalColor.transparent)
p = QPainter(pix) p = QPainter(pix)
p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.setBrush(QColor(*color)) p.setBrush(QColor(*color))
p.setPen(Qt.NoPen) p.setPen(Qt.PenStyle.NoPen)
if shape == "play": if shape == "play":
pts = [ pts = [
pix.rect().topLeft() + QPoint(4, 2), pix.rect().topLeft() + QPoint(4, 2),
@@ -983,7 +991,7 @@ class abogen(QWidget):
self.subtitle_combo.setStyleSheet( self.subtitle_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }" "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.setCurrentText(self.subtitle_mode)
self.subtitle_combo.currentTextChanged.connect(self.on_subtitle_mode_changed) self.subtitle_combo.currentTextChanged.connect(self.on_subtitle_mode_changed)
# Enable/disable subtitle options based on selected language (profile or voice) # Enable/disable subtitle options based on selected language (profile or voice)
@@ -1001,7 +1009,7 @@ class abogen(QWidget):
self.format_combo.setStyleSheet( self.format_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }" "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 # Add items with display labels and underlying keys
for key, label in [ for key, label in [
("wav", "wav"), ("wav", "wav"),
@@ -1032,7 +1040,7 @@ class abogen(QWidget):
"QComboBox { min-height: 20px; padding: 6px 12px; }" "QComboBox { min-height: 20px; padding: 6px 12px; }"
) )
self.subtitle_format_combo.setSizePolicy( self.subtitle_format_combo.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
for value, text in SUBTITLE_FORMATS: for value, text in SUBTITLE_FORMATS:
self.subtitle_format_combo.addItem(text, value) self.subtitle_format_combo.addItem(text, value)
@@ -1079,7 +1087,7 @@ class abogen(QWidget):
"QComboBox { min-height: 20px; padding: 6px 12px; }" "QComboBox { min-height: 20px; padding: 6px 12px; }"
) )
self.replace_newlines_combo.setSizePolicy( self.replace_newlines_combo.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
# Set initial value based on config # Set initial value based on config
self.replace_newlines_combo.setCurrentIndex( self.replace_newlines_combo.setCurrentIndex(
@@ -1106,7 +1114,7 @@ class abogen(QWidget):
self.save_combo.setStyleSheet( self.save_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }" "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.setCurrentText(self.save_option)
self.save_combo.currentTextChanged.connect(self.on_save_option_changed) self.save_combo.currentTextChanged.connect(self.on_save_option_changed)
save_layout.addWidget(self.save_combo) save_layout.addWidget(self.save_combo)
@@ -1121,7 +1129,7 @@ class abogen(QWidget):
save_path_row.addWidget(selected_folder_label) save_path_row.addWidget(selected_folder_label)
self.save_path_label = QLabel("", self.save_path_row_widget) self.save_path_label = QLabel("", self.save_path_row_widget)
self.save_path_label.setStyleSheet(f"QLabel {{ color: {COLORS['GREEN']}; }}") 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) save_path_row.addWidget(self.save_path_label)
self.save_path_row_widget.hide() # Hide the whole row by default self.save_path_row_widget.hide() # Hide the whole row by default
controls_layout.addWidget(self.save_path_row_widget) controls_layout.addWidget(self.save_path_row_widget)
@@ -1164,9 +1172,10 @@ class abogen(QWidget):
self.btn_start.setFixedHeight(60) self.btn_start.setFixedHeight(60)
self.btn_start.clicked.connect(self.start_conversion) self.btn_start.clicked.connect(self.start_conversion)
controls_layout.addWidget(self.btn_start) controls_layout.addWidget(self.btn_start)
# Add controls to a container widget
self.controls_widget = QWidget() self.controls_widget = QWidget()
self.controls_widget.setLayout(controls_layout) 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) container_layout.addWidget(self.controls_widget)
# Progress bar # Progress bar
self.progress_bar = QProgressBar(self) self.progress_bar = QProgressBar(self)
@@ -1175,7 +1184,7 @@ class abogen(QWidget):
container_layout.addWidget(self.progress_bar) container_layout.addWidget(self.progress_bar)
# ETR Label # ETR Label
self.etr_label = QLabel("Estimated time remaining: Calculating...", self) 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() self.etr_label.hide()
container_layout.addWidget(self.etr_label) container_layout.addWidget(self.etr_label)
# Cancel button # Cancel button
@@ -1274,12 +1283,12 @@ class abogen(QWidget):
checked_chapters=self.selected_chapters, checked_chapters=self.selected_chapters,
parent=self parent=self
) )
dialog.setWindowModality(Qt.NonModal) dialog.setWindowModality(Qt.WindowModality.NonModal)
dialog.setModal(False) dialog.setModal(False)
dialog.show() # We'll handle the dialog result asynchronously dialog.show() # We'll handle the dialog result asynchronously
def on_dialog_finished(result): def on_dialog_finished(result):
if result != QDialog.Accepted: if result != QDialog.DialogCode.Accepted:
return False return False
chapters_text, all_checked_hrefs = dialog.get_selected_text() chapters_text, all_checked_hrefs = dialog.get_selected_text()
if not all_checked_hrefs: if not all_checked_hrefs:
@@ -1319,7 +1328,7 @@ class abogen(QWidget):
if self.save_as_project: if self.save_as_project:
# Get project directory from user # Get project directory from user
project_dir = QFileDialog.getExistingDirectory( project_dir = QFileDialog.getExistingDirectory(
self, "Select Project Folder", "", QFileDialog.ShowDirsOnly self, "Select Project Folder", "", QFileDialog.Option.ShowDirsOnly
) )
if not project_dir: if not project_dir:
# User cancelled, fallback to cache # User cancelled, fallback to cache
@@ -1445,7 +1454,7 @@ class abogen(QWidget):
dialog.non_cache_file_path = edit_file dialog.non_cache_file_path = edit_file
except Exception: except Exception:
pass pass
if dialog.exec_() == QDialog.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
text = dialog.get_text() text = dialog.get_text()
if not text.strip(): if not text.strip():
self._show_error_message_box("Textbox Error", "Text cannot be empty.") self._show_error_message_box("Textbox Error", "Text cannot be empty.")
@@ -1605,7 +1614,7 @@ class abogen(QWidget):
at_bottom = sb.value() == sb.maximum() at_bottom = sb.value() == sb.maximum()
cursor = txt.textCursor() cursor = txt.textCursor()
cursor.movePosition(QTextCursor.End) cursor.movePosition(QTextCursor.MoveOperation.End)
fmt = cursor.charFormat() fmt = cursor.charFormat()
if isinstance(message, tuple): if isinstance(message, tuple):
@@ -1624,7 +1633,7 @@ class abogen(QWidget):
end = doc.findBlockByNumber(excess).position() end = doc.findBlockByNumber(excess).position()
trim_cursor = QTextCursor(doc) trim_cursor = QTextCursor(doc)
trim_cursor.setPosition(start) trim_cursor.setPosition(start)
trim_cursor.setPosition(end, QTextCursor.KeepAnchor) trim_cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
trim_cursor.removeSelectedText() trim_cursor.removeSelectedText()
if at_bottom: if at_bottom:
@@ -1743,6 +1752,8 @@ class abogen(QWidget):
total_char_count=self.char_count, total_char_count=self.char_count,
replace_single_newlines=self.replace_single_newlines, replace_single_newlines=self.replace_single_newlines,
save_base_path=save_base_path, 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 # Prevent adding duplicate items to the queue
@@ -1760,6 +1771,10 @@ class abogen(QWidget):
== item_queue.replace_single_newlines == item_queue.replace_single_newlines
and getattr(queued_item, "save_base_path", None) and getattr(queued_item, "save_base_path", None)
== item_queue.save_base_path == 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( QMessageBox.warning(
self, "Duplicate Item", "This item is already in the queue." self, "Duplicate Item", "This item is already in the queue."
@@ -1779,10 +1794,10 @@ class abogen(QWidget):
self, self,
"Confirm Clear Queue", "Confirm Clear Queue",
f"Are you sure you want to clear {len(self.queued_items)} items from the queue?", f"Are you sure you want to clear {len(self.queued_items)} items from the queue?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.No, QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
self.queued_items = [] self.queued_items = []
self.enable_disable_queue_buttons() self.enable_disable_queue_buttons()
@@ -1790,7 +1805,7 @@ class abogen(QWidget):
def manage_queue(self): def manage_queue(self):
# show a dialog to manage the queue # show a dialog to manage the queue
dialog = QueueManager(self, self.queued_items) dialog = QueueManager(self, self.queued_items)
if dialog.exec_() == QDialog.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
self.queued_items = dialog.get_queue() self.queued_items = dialog.get_queue()
# re-enable/disable buttons based on queue state # re-enable/disable buttons based on queue state
self.enable_disable_queue_buttons() self.enable_disable_queue_buttons()
@@ -2057,7 +2072,7 @@ class abogen(QWidget):
dialog.setLayout(layout) dialog.setLayout(layout)
dialog.setMinimumSize(400, 300) dialog.setMinimumSize(400, 300)
dialog.setSizeGripEnabled(True) # Allow resizing dialog.setSizeGripEnabled(True) # Allow resizing
dialog.exec_() dialog.exec()
def on_conversion_finished(self, message, output_path): def on_conversion_finished(self, message, output_path):
prevent_sleep_end() prevent_sleep_end()
@@ -2516,14 +2531,14 @@ class abogen(QWidget):
def _show_error_message_box(self, title, message): def _show_error_message_box(self, title, message):
box = QMessageBox(self) box = QMessageBox(self)
box.setIcon(QMessageBox.Critical) box.setIcon(QMessageBox.Icon.Critical)
box.setWindowTitle(title) box.setWindowTitle(title)
box.setText(message) box.setText(message)
copy_btn = QPushButton("Copy") copy_btn = QPushButton("Copy")
box.addButton(copy_btn, QMessageBox.ActionRole) box.addButton(copy_btn, QMessageBox.ButtonRole.ActionRole)
box.addButton(QMessageBox.Ok) box.addButton(QMessageBox.StandardButton.Ok)
copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(message)) copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(message))
box.exec_() box.exec()
def _show_preview_error_box(self, msg): def _show_preview_error_box(self, msg):
self._show_error_message_box("Preview Error", f"Preview error: {msg}") self._show_error_message_box("Preview Error", f"Preview error: {msg}")
@@ -2551,14 +2566,14 @@ class abogen(QWidget):
def cancel_conversion(self): def cancel_conversion(self):
if self.is_converting: if self.is_converting:
box = QMessageBox(self) box = QMessageBox(self)
box.setIcon(QMessageBox.Warning) box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Cancel Conversion") box.setWindowTitle("Cancel Conversion")
box.setText( box.setText(
"A conversion is currently running. Are you sure you want to cancel?" "A conversion is currently running. Are you sure you want to cancel?"
) )
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
box.setDefaultButton(QMessageBox.No) box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec_() != QMessageBox.Yes: if box.exec() != QMessageBox.StandardButton.Yes:
return return
try: try:
if ( if (
@@ -2643,7 +2658,7 @@ class abogen(QWidget):
save_config(self.config) save_config(self.config)
def on_gpu_setting_changed(self, state): 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 self.config["use_gpu"] = self.use_gpu
save_config(self.config) save_config(self.config)
@@ -2660,14 +2675,14 @@ class abogen(QWidget):
def closeEvent(self, event): def closeEvent(self, event):
if self.is_converting: if self.is_converting:
box = QMessageBox(self) box = QMessageBox(self)
box.setIcon(QMessageBox.Warning) box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Conversion in Progress") box.setWindowTitle("Conversion in Progress")
box.setText( box.setText(
"A conversion is currently running. Are you sure you want to exit?" "A conversion is currently running. Are you sure you want to exit?"
) )
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
box.setDefaultButton(QMessageBox.No) box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec_() == QMessageBox.Yes: if box.exec() == QMessageBox.StandardButton.Yes:
self.cleanup_conversion_thread() self.cleanup_conversion_thread()
event.accept() event.accept()
else: else:
@@ -2681,10 +2696,10 @@ class abogen(QWidget):
from abogen.conversion import ChapterOptionsDialog from abogen.conversion import ChapterOptionsDialog
dialog = ChapterOptionsDialog(chapter_count, parent=self) 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 is accepted, pass the options to the conversion thread
if dialog.exec_() == QDialog.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
options = dialog.get_options() options = dialog.get_options()
if ( if (
hasattr(self, "conversion_thread") hasattr(self, "conversion_thread")
@@ -2696,8 +2711,7 @@ class abogen(QWidget):
self.cancel_conversion() self.cancel_conversion()
def apply_theme(self, theme): def apply_theme(self, theme):
from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import QStyleFactory
app = QApplication.instance() app = QApplication.instance()
is_windows = platform.system() == "Windows" is_windows = platform.system() == "Windows"
@@ -2981,7 +2995,7 @@ class abogen(QWidget):
about_action.triggered.connect(self.show_about_dialog) about_action.triggered.connect(self.show_about_dialog)
menu.addAction(about_action) 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): def toggle_replace_single_newlines(self, enabled):
self.replace_single_newlines = enabled self.replace_single_newlines = enabled
@@ -2989,7 +3003,7 @@ class abogen(QWidget):
save_config(self.config) save_config(self.config)
def restart_app(self): def restart_app(self):
from PyQt5.QtCore import QProcess
import sys import sys
exe = sys.executable exe = sys.executable
@@ -3022,10 +3036,10 @@ class abogen(QWidget):
self, self,
"Restart Required", "Restart Required",
message, message,
QMessageBox.Yes | QMessageBox.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.No, QMessageBox.StandardButton.No,
) )
if reply == QMessageBox.Yes: if reply == QMessageBox.StandardButton.Yes:
self.config["disable_kokoro_internet"] = disabled self.config["disable_kokoro_internet"] = disabled
save_config(self.config) save_config(self.config)
try: try:
@@ -3040,10 +3054,10 @@ class abogen(QWidget):
self, self,
"Reset Settings", "Reset Settings",
"This will reset all settings to their default values and restart the application.\n\nDo you want to continue?", "This will reset all settings to their default values and restart the application.\n\nDo you want to continue?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.No, QMessageBox.StandardButton.No,
) )
if reply == QMessageBox.Yes: if reply == QMessageBox.StandardButton.Yes:
from abogen.utils import get_user_config_path from abogen.utils import get_user_config_path
config_path = get_user_config_path() config_path = get_user_config_path()
@@ -3204,9 +3218,9 @@ Categories=AudioVideo;Audio;Utility;
self, self,
"Install Application Entry", "Install Application Entry",
"Install application entry for current user?", "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 import shutil
user_app_dir = os.path.expanduser("~/.local/share/applications") user_app_dir = os.path.expanduser("~/.local/share/applications")
@@ -3266,12 +3280,11 @@ Categories=AudioVideo;Audio;Utility;
initial_state = entry.get("voices", []) initial_state = entry.get("voices", [])
else: else:
initial_state = entry initial_state = entry
else: self.selected_lang = entry[0][0] if entry and entry[0] else None
initial_state = []
dialog = VoiceFormulaDialog( dialog = VoiceFormulaDialog(
self, initial_state=initial_state, selected_profile=selected_profile self, initial_state=initial_state, selected_profile=selected_profile
) )
if dialog.exec_() == QDialog.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
if dialog.current_profile: if dialog.current_profile:
self.selected_profile_name = dialog.current_profile self.selected_profile_name = dialog.current_profile
self.config["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 # Create custom dialog
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle(f"About {PROGRAM_NAME}") 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 dialog.setFixedSize(400, 320) # Increased height for new button
layout = QVBoxLayout(dialog) layout = QVBoxLayout(dialog)
@@ -3314,7 +3327,7 @@ Categories=AudioVideo;Audio;Utility;
title_label = QLabel( title_label = QLabel(
f"<h1 style='margin-bottom: 0;'>{PROGRAM_NAME} <span style='font-size: 12px; font-weight: normal; color: #666;'>v{VERSION}</span></h1><h3 style='margin-top: 5px;'>Audiobook Generator</h3>" f"<h1 style='margin-bottom: 0;'>{PROGRAM_NAME} <span style='font-size: 12px; font-weight: normal; color: #666;'>v{VERSION}</span></h1><h3 style='margin-top: 5px;'>Audiobook Generator</h3>"
) )
title_label.setTextFormat(Qt.RichText) title_label.setTextFormat(Qt.TextFormat.RichText)
header_layout.addWidget(title_label, 1) header_layout.addWidget(title_label, 1)
layout.addLayout(header_layout) layout.addLayout(header_layout)
@@ -3323,7 +3336,7 @@ Categories=AudioVideo;Audio;Utility;
f"<p>{PROGRAM_DESCRIPTION}</p>" f"<p>{PROGRAM_DESCRIPTION}</p>"
"<p>Visit the GitHub repository for updates, documentation, and to report issues.</p>" "<p>Visit the GitHub repository for updates, documentation, and to report issues.</p>"
) )
desc_label.setTextFormat(Qt.RichText) desc_label.setTextFormat(Qt.TextFormat.RichText)
desc_label.setWordWrap(True) desc_label.setWordWrap(True)
layout.addWidget(desc_label) layout.addWidget(desc_label)
@@ -3346,7 +3359,7 @@ Categories=AudioVideo;Audio;Utility;
close_btn.setFixedHeight(32) close_btn.setFixedHeight(32)
layout.addWidget(close_btn) layout.addWidget(close_btn)
dialog.exec_() dialog.exec()
def manual_check_for_updates(self): def manual_check_for_updates(self):
"""Manually check for updates and always show result""" """Manually check for updates and always show result"""
@@ -3359,7 +3372,7 @@ Categories=AudioVideo;Audio;Utility;
def show_update_message(remote_version, local_version): def show_update_message(remote_version, local_version):
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Information) msg_box.setIcon(QMessageBox.Icon.Information)
msg_box.setWindowTitle("Update Available") msg_box.setWindowTitle("Update Available")
msg_box.setText( msg_box.setText(
f"A new version of {PROGRAM_NAME} is available! ({local_version} > {remote_version})" 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. " "Alternatively, visit the GitHub repository for more information. "
"Would you like to view the changelog?" "Would you like to view the changelog?"
) )
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
msg_box.setDefaultButton(QMessageBox.Yes) msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
if msg_box.exec_() == QMessageBox.Yes: if msg_box.exec() == QMessageBox.StandardButton.Yes:
try: try:
QDesktopServices.openUrl(QUrl(GITHUB_URL + "/releases/latest")) QDesktopServices.openUrl(QUrl(GITHUB_URL + "/releases/latest"))
except Exception: except Exception:
@@ -3455,7 +3468,7 @@ Categories=AudioVideo;Audio;Utility;
# Create a custom message box with checkbox # Create a custom message box with checkbox
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Question) msg_box.setIcon(QMessageBox.Icon.Question)
msg_box.setWindowTitle("Clear Cache Files") 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." 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) msg_box.setCheckBox(preview_cache_checkbox)
# Add buttons # Add buttons
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
msg_box.setDefaultButton(QMessageBox.Yes) msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
if msg_box.exec_() != QMessageBox.Yes: if msg_box.exec() != QMessageBox.StandardButton.Yes:
return return
# Delete the text files # Delete the text files
@@ -3522,7 +3535,7 @@ Categories=AudioVideo;Audio;Utility;
def set_max_log_lines(self): def set_max_log_lines(self):
"""Open a dialog to set the maximum lines in the log window.""" """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( value, ok = QInputDialog.getInt(
self, self,
@@ -3545,7 +3558,7 @@ Categories=AudioVideo;Audio;Utility;
def set_max_subtitle_words(self): def set_max_subtitle_words(self):
"""Open a dialog to set the maximum words per subtitle""" """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) current_value = self.config.get("max_subtitle_words", 50)
@@ -3574,7 +3587,7 @@ Categories=AudioVideo;Audio;Utility;
def set_silence_between_chapters(self): def set_silence_between_chapters(self):
"""Open a dialog to set the silence duration between chapters""" """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) current_value = self.config.get("silence_duration", 2.0)
@@ -3583,14 +3596,14 @@ Categories=AudioVideo;Audio;Utility;
dlg.setLabelText( dlg.setLabelText(
"Enter the duration of silence\nbetween chapters (in seconds):" "Enter the duration of silence\nbetween chapters (in seconds):"
) )
dlg.setInputMode(QInputDialog.DoubleInput) dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
dlg.setDoubleDecimals(1) dlg.setDoubleDecimals(1)
dlg.setDoubleMinimum(0.0) dlg.setDoubleMinimum(0.0)
dlg.setDoubleMaximum(60.0) dlg.setDoubleMaximum(60.0)
dlg.setDoubleValue(current_value) dlg.setDoubleValue(current_value)
dlg.setDoubleStep(0.1) # <-- set step to 0.1 dlg.setDoubleStep(0.1) # <-- set step to 0.1
if dlg.exec_() == QDialog.Accepted: if dlg.exec() == QDialog.DialogCode.Accepted:
value = dlg.doubleValue() value = dlg.doubleValue()
# Round to one decimal to avoid floating-point representation noise # Round to one decimal to avoid floating-point representation noise
value = round(value, 1) value = round(value, 1)
+22 -8
View File
@@ -7,19 +7,24 @@ import signal
# Qt platform plugin detection (fixes #59) # Qt platform plugin detection (fixes #59)
try: try:
from PyQt5.QtCore import QLibraryInfo from PyQt6.QtCore import QLibraryInfo
plugins = QLibraryInfo.location(QLibraryInfo.PluginsPath)
# Get the path to the plugins directory
plugins = QLibraryInfo.path(QLibraryInfo.LibraryPath.PluginsPath)
# Normalize path to use the OS-native separators and absolute path # Normalize path to use the OS-native separators and absolute path
platform_dir = os.path.normpath(os.path.join(plugins, "platforms")) platform_dir = os.path.normpath(os.path.join(plugins, "platforms"))
# Ensure we work with an absolute path for clarity # Ensure we work with an absolute path for clarity
platform_dir = os.path.abspath(platform_dir) platform_dir = os.path.abspath(platform_dir)
if os.path.isdir(platform_dir): if os.path.isdir(platform_dir):
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = platform_dir os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = platform_dir
print("QT_QPA_PLATFORM_PLUGIN_PATH set to:", platform_dir) print("QT_QPA_PLATFORM_PLUGIN_PATH set to:", platform_dir)
else: else:
print("PyQt5 platform plugins not found at", platform_dir) print("PyQt6 platform plugins not found at", platform_dir)
except ImportError: except ImportError:
print("PyQt5 not installed.") print("PyQt6 not installed.")
# Set application ID for Windows taskbar icon # Set application ID for Windows taskbar icon
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -31,9 +36,16 @@ if platform.system() == "Windows":
except Exception as e: except Exception as e:
print("Warning: failed to set AppUserModelID:", e) print("Warning: failed to set AppUserModelID:", e)
from PyQt5.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from PyQt5.QtGui import QIcon from PyQt6.QtGui import QIcon
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType from PyQt6.QtCore import (
Qt,
QLibraryInfo,
qInstallMessageHandler,
QStandardPaths,
QSettings,
QtMsgType,
)
# Add the directory to Python path # Add the directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__))) 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 # Custom message handler to filter out specific Qt warnings
def qt_message_handler(mode, context, message): 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: if "Wayland does not support QWindow::requestActivate()" in message:
return # Suppress this specific message return # Suppress this specific message
if "setGrabPopup called with a parent, QtWaylandClient" in message: if "setGrabPopup called with a parent, QtWaylandClient" in message:
return return
if mode == QtMsgType.QtWarningMsg: if mode == QtMsgType.QtWarningMsg:
print(f"Qt Warning: {message}") print(f"Qt Warning: {message}")
elif mode == QtMsgType.QtCriticalMsg: elif mode == QtMsgType.QtCriticalMsg:
@@ -129,7 +143,7 @@ def main():
ex = abogen() ex = abogen()
ex.show() ex.show()
sys.exit(app.exec_()) sys.exit(app.exec())
if __name__ == "__main__": if __name__ == "__main__":
+66 -44
View File
@@ -2,7 +2,7 @@
# button to remove an item from the queue # button to remove an item from the queue
# button to clear the queue # button to clear the queue
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QDialog,
QVBoxLayout, QVBoxLayout,
QHBoxLayout, QHBoxLayout,
@@ -14,18 +14,20 @@ from PyQt5.QtWidgets import (
QLabel, QLabel,
QWidget, QWidget,
QSizePolicy, QSizePolicy,
QAbstractItemView,
) )
from PyQt5.QtCore import QFileInfo, Qt from PyQt6.QtCore import QFileInfo, Qt
from abogen.constants import COLORS from abogen.constants import COLORS
from copy import deepcopy from copy import deepcopy
from PyQt5.QtGui import QFontMetrics from PyQt6.QtGui import QFontMetrics
class ElidedLabel(QLabel): class ElidedLabel(QLabel):
def __init__(self, text, parent=None): def __init__(self, text):
super().__init__(text, parent) super().__init__(text)
self._full_text = 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): def setText(self, text):
self._full_text = text self._full_text = text
@@ -34,7 +36,7 @@ class ElidedLabel(QLabel):
def resizeEvent(self, event): def resizeEvent(self, event):
metrics = QFontMetrics(self.font()) 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().setText(elided)
super().resizeEvent(event) super().resizeEvent(event)
@@ -53,8 +55,8 @@ class QueueListItemWidget(QWidget):
name_label = ElidedLabel(os.path.basename(file_name)) name_label = ElidedLabel(os.path.basename(file_name))
char_label = QLabel(f"Chars: {char_count}") char_label = QLabel(f"Chars: {char_count}")
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};") char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
char_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) char_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
char_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) char_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
layout.addWidget(name_label, 1) layout.addWidget(name_label, 1)
layout.addWidget(char_label, 0) layout.addWidget(char_label, 0)
self.setLayout(layout) self.setLayout(layout)
@@ -67,12 +69,12 @@ class DroppableQueueListWidget(QListWidget):
self.setAcceptDrops(True) self.setAcceptDrops(True)
# Overlay for drag hover # Overlay for drag hover
self.drag_overlay = QLabel("", self) self.drag_overlay = QLabel("", self)
self.drag_overlay.setAlignment(Qt.AlignCenter) self.drag_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.drag_overlay.setStyleSheet( self.drag_overlay.setStyleSheet(
f"border:2px dashed {COLORS['BLUE_BORDER_HOVER']}; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG_HOVER']};" 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.setVisible(False)
self.drag_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.drag_overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
if event.mimeData().hasUrls(): if event.mimeData().hasUrls():
@@ -132,9 +134,9 @@ class QueueManager(QDialog):
layout.setSpacing(12) # set spacing between widgets in main layout layout.setSpacing(12) # set spacing between widgets in main layout
# list of queued items # list of queued items
self.listwidget = DroppableQueueListWidget(self) self.listwidget = DroppableQueueListWidget(self)
self.listwidget.setSelectionMode(QListWidget.ExtendedSelection) self.listwidget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.listwidget.setAlternatingRowColors(True) self.listwidget.setAlternatingRowColors(True)
self.listwidget.setContextMenuPolicy(Qt.CustomContextMenu) self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.listwidget.customContextMenuRequested.connect(self.show_context_menu) self.listwidget.customContextMenuRequested.connect(self.show_context_menu)
# Add informative instructions at the top # Add informative instructions at the top
instructions = QLabel( instructions = QLabel(
@@ -145,7 +147,7 @@ class QueueManager(QDialog):
"Changing the main window configuration afterward <b>does not</b> affect files already in the queue. " "Changing the main window configuration afterward <b>does not</b> affect files already in the queue. "
"You can view each file's configuration by hovering over them." "You can view each file's configuration by hovering over them."
) )
instructions.setAlignment(Qt.AlignLeft) instructions.setAlignment(Qt.AlignmentFlag.AlignLeft)
instructions.setWordWrap(True) instructions.setWordWrap(True)
instructions.setStyleSheet("margin-bottom: 8px;") instructions.setStyleSheet("margin-bottom: 8px;")
layout.addWidget(instructions) layout.addWidget(instructions)
@@ -154,12 +156,12 @@ class QueueManager(QDialog):
"Drag and drop your text files here or use the 'Add files' button.", "Drag and drop your text files here or use the 'Add files' button.",
self.listwidget, self.listwidget,
) )
self.empty_overlay.setAlignment(Qt.AlignCenter) self.empty_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.empty_overlay.setStyleSheet( self.empty_overlay.setStyleSheet(
f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;" f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;"
) )
self.empty_overlay.setWordWrap(True) 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() self.empty_overlay.hide()
# add queue items to the list # add queue items to the list
self.process_queue() self.process_queue()
@@ -192,7 +194,7 @@ class QueueManager(QDialog):
self.listwidget.currentItemChanged.connect(self.update_button_states) self.listwidget.currentItemChanged.connect(self.update_button_states)
self.listwidget.itemSelectionChanged.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.accepted.connect(self.accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
@@ -211,7 +213,6 @@ class QueueManager(QDialog):
self.listwidget.clear() self.listwidget.clear()
if not self.queue: if not self.queue:
self.empty_overlay.resize(self.listwidget.size())
self.empty_overlay.show() self.empty_overlay.show()
self.update_button_states() self.update_button_states()
return return
@@ -262,10 +263,18 @@ class QueueManager(QDialog):
f"<b>Characters:</b> {getattr(item, 'total_char_count', '')}<br>" f"<b>Characters:</b> {getattr(item, 'total_char_count', '')}<br>"
f"<b>Replace Single Newlines:</b> {getattr(item, 'replace_single_newlines', False)}" f"<b>Replace Single Newlines:</b> {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"<br><b>Save chapters separately:</b> {'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"<br><b>Merge chapters at the end:</b> {'Yes' if merge_chapters_at_end else 'No'}"
list_item.setToolTip(tooltip) list_item.setToolTip(tooltip)
list_item.setIcon(icon) list_item.setIcon(icon)
# Store both paths for context menu # Store both paths for context menu
list_item.setData(Qt.UserRole, { list_item.setData(Qt.ItemDataRole.UserRole, {
'display_path': display_file_path, 'display_path': display_file_path,
'processing_path': processing_file_path 'processing_path': processing_file_path
}) })
@@ -280,7 +289,7 @@ class QueueManager(QDialog):
items = self.listwidget.selectedItems() items = self.listwidget.selectedItems()
if not items: if not items:
return return
from PyQt5.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
# Remove by index to ensure correct mapping # Remove by index to ensure correct mapping
rows = sorted([self.listwidget.row(item) for item in items], reverse=True) rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
@@ -290,10 +299,10 @@ class QueueManager(QDialog):
self, self,
"Confirm Remove", "Confirm Remove",
f"Are you sure you want to remove {len(rows)} selected items from the queue?", f"Are you sure you want to remove {len(rows)} selected items from the queue?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.No, QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
for row in rows: for row in rows:
if 0 <= row < len(self.queue): if 0 <= row < len(self.queue):
@@ -302,17 +311,17 @@ class QueueManager(QDialog):
self.update_button_states() self.update_button_states()
def clear_queue(self): def clear_queue(self):
from PyQt5.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
if len(self.queue) > 1: if len(self.queue) > 1:
reply = QMessageBox.question( reply = QMessageBox.question(
self, self,
"Confirm Clear Queue", "Confirm Clear Queue",
f"Are you sure you want to clear {len(self.queue)} items from the queue?", f"Are you sure you want to clear {len(self.queue)} items from the queue?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.No, QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
self.queue.clear() self.queue.clear()
self.listwidget.clear() self.listwidget.clear()
@@ -362,6 +371,13 @@ class QueueManager(QDialog):
attrs["replace_single_newlines"] = getattr( attrs["replace_single_newlines"] = getattr(
parent, "replace_single_newlines", False 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: else:
# fallback: empty values # fallback: empty values
attrs = { attrs = {
@@ -378,11 +394,13 @@ class QueueManager(QDialog):
"replace_single_newlines", "replace_single_newlines",
] ]
} }
attrs["save_chapters_separately"] = None
attrs["merge_chapters_at_end"] = None
return attrs return attrs
def add_files_from_paths(self, file_paths): def add_files_from_paths(self, file_paths):
from abogen.utils import calculate_text_length from abogen.utils import calculate_text_length
from PyQt5.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
import os import os
current_attrs = self.get_current_attributes() current_attrs = self.get_current_attributes()
@@ -430,6 +448,10 @@ class QueueManager(QDialog):
== getattr(item, "replace_single_newlines", False) == getattr(item, "replace_single_newlines", False)
and getattr(queued_item, "save_base_path", None) and getattr(queued_item, "save_base_path", None)
== getattr(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 is_duplicate = True
break break
@@ -447,7 +469,7 @@ class QueueManager(QDialog):
self.update_button_states() self.update_button_states()
def add_more_files(self): 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 from abogen.utils import calculate_text_length # import the function
# Only allow .txt files # Only allow .txt files
@@ -477,9 +499,9 @@ class QueueManager(QDialog):
self.clear_button.setEnabled(bool(self.queue)) self.clear_button.setEnabled(bool(self.queue))
def show_context_menu(self, pos): def show_context_menu(self, pos):
from PyQt5.QtWidgets import QMenu, QAction from PyQt6.QtWidgets import QMenu
from PyQt5.QtGui import QDesktopServices from PyQt6.QtGui import QAction, QDesktopServices
from PyQt5.QtCore import QUrl from PyQt6.QtCore import QUrl
import os import os
global_pos = self.listwidget.viewport().mapToGlobal(pos) global_pos = self.listwidget.viewport().mapToGlobal(pos)
@@ -495,10 +517,10 @@ class QueueManager(QDialog):
open_file_action = QAction("Open file", self) open_file_action = QAction("Open file", self)
def open_file(): def open_file():
from PyQt5.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
item = selected_items[0] item = selected_items[0]
paths = item.data(Qt.UserRole) paths = item.data(Qt.ItemDataRole.UserRole)
if isinstance(paths, dict): if isinstance(paths, dict):
file_path = paths.get('display_path', paths.get('processing_path', '')) file_path = paths.get('display_path', paths.get('processing_path', ''))
else: else:
@@ -524,7 +546,7 @@ class QueueManager(QDialog):
# If the queued item represents a converted document (markdown, pdf, epub) # 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) # show two actions: Go to processed file (the cached .txt) and Go to input file (original source)
item = selected_items[0] item = selected_items[0]
paths = item.data(Qt.UserRole) paths = item.data(Qt.ItemDataRole.UserRole)
if isinstance(paths, dict): if isinstance(paths, dict):
display_path = paths.get('display_path', '') display_path = paths.get('display_path', '')
processing_path = paths.get('processing_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) 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): def open_folder_for(path_label: str):
# path_label should be either 'display' or 'processing' # path_label should be either 'display' or 'processing'
@@ -588,7 +610,7 @@ class QueueManager(QDialog):
def go_to_folder(): def go_to_folder():
item = selected_items[0] item = selected_items[0]
paths = item.data(Qt.UserRole) paths = item.data(Qt.ItemDataRole.UserRole)
if isinstance(paths, dict): if isinstance(paths, dict):
file_path = paths.get('display_path', paths.get('processing_path', '')) file_path = paths.get('display_path', paths.get('processing_path', ''))
else: else:
@@ -618,7 +640,7 @@ class QueueManager(QDialog):
clear_action = QAction("Clear Queue", self) clear_action = QAction("Clear Queue", self)
clear_action.triggered.connect(self.clear_queue) clear_action.triggered.connect(self.clear_queue)
menu.addAction(clear_action) menu.addAction(clear_action)
menu.exec_(global_pos) menu.exec(global_pos)
def accept(self): def accept(self):
# Accept: keep changes # Accept: keep changes
@@ -626,7 +648,7 @@ class QueueManager(QDialog):
def reject(self): def reject(self):
# Cancel: restore original queue # 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) # Warn if user changed a lot (e.g., more than 1 items difference)
original_count = len(self._original_queue) original_count = len(self._original_queue)
@@ -636,19 +658,19 @@ class QueueManager(QDialog):
self, self,
"Confirm Cancel", "Confirm Cancel",
f"Are you sure you want to cancel and discard all changes?", f"Are you sure you want to cancel and discard all changes?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.No, QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
self.queue.clear() self.queue.clear()
self.queue.extend(deepcopy(self._original_queue)) self.queue.extend(deepcopy(self._original_queue))
super().reject() super().reject()
def keyPressEvent(self, event): 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() self.remove_item()
else: else:
super().keyPressEvent(event) super().keyPressEvent(event)
+2
View File
@@ -15,3 +15,5 @@ class QueuedItem:
total_char_count: int total_char_count: int
replace_single_newlines: bool = False replace_single_newlines: bool = False
save_base_path: str = None save_base_path: str = None
save_chapters_separately: bool = None
merge_chapters_at_end: bool = None
+64 -62
View File
@@ -1,6 +1,6 @@
import json import json
import os import os
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QDialog,
QVBoxLayout, QVBoxLayout,
QCheckBox, QCheckBox,
@@ -22,12 +22,11 @@ from PyQt5.QtWidgets import (
QFileDialog, QFileDialog,
QSplitter, QSplitter,
QMenu, QMenu,
QAction,
QComboBox,
QApplication, QApplication,
QComboBox,
) )
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize from PyQt6.QtCore import Qt, QTimer, QPoint, QRect, QSize
from PyQt5.QtGui import QPixmap, QIcon from PyQt6.QtGui import QPixmap, QIcon, QAction
from abogen.constants import ( from abogen.constants import (
VOICES_INTERNAL, VOICES_INTERNAL,
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
@@ -91,7 +90,7 @@ class FlowLayout(QLayout):
return len(self._item_list) return len(self._item_list)
def expandingDirections(self): def expandingDirections(self):
return Qt.Orientations(Qt.Orientation(0)) return Qt.Orientation(0)
def hasHeightForWidth(self): def hasHeightForWidth(self):
return True return True
@@ -132,10 +131,10 @@ class FlowLayout(QLayout):
for item in self._item_list: for item in self._item_list:
style = self.parentWidget().style() if self.parentWidget() else QStyle() style = self.parentWidget().style() if self.parentWidget() else QStyle()
layout_spacing_x = style.layoutSpacing( 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( 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_x = spacing if spacing >= 0 else layout_spacing_x
space_y = spacing if spacing >= 0 else layout_spacing_y space_y = spacing if spacing >= 0 else layout_spacing_y
@@ -163,7 +162,7 @@ class VoiceMixer(QWidget):
super().__init__() super().__init__()
self.voice_name = voice_name self.voice_name = voice_name
self.setFixedWidth(VOICE_MIXER_WIDTH) 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 # TODO Set CSS for rounded corners
# self.setObjectName("VoiceMixer") # self.setObjectName("VoiceMixer")
@@ -173,7 +172,7 @@ class VoiceMixer(QWidget):
# Name label at the top # Name label at the top
name = voice_name name = voice_name
layout.addWidget(QLabel(name), alignment=Qt.AlignCenter) layout.addWidget(QLabel(name), alignment=Qt.AlignmentFlag.AlignCenter)
# Voice name label with gender icon # Voice name label with gender icon
is_female = self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f" 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 (flag and gender)
icons_layout = QHBoxLayout() icons_layout = QHBoxLayout()
icons_layout.setSpacing(3) 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
flag_icon_path = get_resource_path( flag_icon_path = get_resource_path(
@@ -194,11 +193,11 @@ class VoiceMixer(QWidget):
gender_label = QLabel() gender_label = QLabel()
flag_pixmap = QPixmap(flag_icon_path) flag_pixmap = QPixmap(flag_icon_path)
flag_label.setPixmap( 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_pixmap = QPixmap(gender_icon_path)
gender_label.setPixmap( 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(flag_label)
icons_layout.addWidget(gender_label) icons_layout.addWidget(gender_label)
@@ -210,7 +209,7 @@ class VoiceMixer(QWidget):
self.checkbox = QCheckBox() self.checkbox = QCheckBox()
self.checkbox.setChecked(initial_status) self.checkbox.setChecked(initial_status)
self.checkbox.stateChanged.connect(self.toggle_inputs) 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 # Spinbox and slider
self.spin_box = QDoubleSpinBox() self.spin_box = QDoubleSpinBox()
@@ -219,10 +218,10 @@ class VoiceMixer(QWidget):
self.spin_box.setDecimals(2) self.spin_box.setDecimals(2)
self.spin_box.setValue(initial_weight) self.spin_box.setValue(initial_weight)
self.slider = QSlider(Qt.Vertical) self.slider = QSlider(Qt.Orientation.Vertical)
self.slider.setRange(0, 100) self.slider.setRange(0, 100)
self.slider.setValue(int(initial_weight * 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) self.slider.setFixedWidth(SLIDER_WIDTH)
# Fix slider in Windows # Fix slider in Windows
@@ -249,17 +248,17 @@ class VoiceMixer(QWidget):
# Layout for slider and labels # Layout for slider and labels
slider_layout = QVBoxLayout() slider_layout = QVBoxLayout()
slider_layout.addWidget(self.spin_box) 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 = 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_layout.setContentsMargins(0, 0, 0, 0)
slider_center_widget = QWidget() slider_center_widget = QWidget()
slider_center_widget.setLayout(slider_center_layout) slider_center_widget.setLayout(slider_center_layout)
slider_layout.addWidget(slider_center_widget, stretch=1) 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) slider_layout.setStretch(2, 1)
layout.addLayout(slider_layout, stretch=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 # Make sure the entire button is clickable, not just the text
self.delete_button.setFocusPolicy(Qt.NoFocus) self.delete_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.delete_button.setAttribute(Qt.WA_TransparentForMouseEvents, False) self.delete_button.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
self.delete_button.setCursor(Qt.PointingHandCursor) self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.delete_button.hide() self.delete_button.hide()
def resizeEvent(self, event): def resizeEvent(self, event):
@@ -357,7 +356,7 @@ class VoiceFormulaDialog(QDialog):
if parent is not None and hasattr(parent, "subtitle_combo"): if parent is not None and hasattr(parent, "subtitle_combo"):
self.subtitle_combo = parent.subtitle_combo self.subtitle_combo = parent.subtitle_combo
# Create main container layout with profile section and mixer section # Create main container layout with profile section and mixer section
splitter = QSplitter(Qt.Horizontal) splitter = QSplitter(Qt.Orientation.Horizontal)
# Profile section # Profile section
profile_widget = QWidget() profile_widget = QWidget()
profile_layout = QVBoxLayout(profile_widget) profile_layout = QVBoxLayout(profile_widget)
@@ -371,8 +370,8 @@ class VoiceFormulaDialog(QDialog):
profile_layout.addLayout(header_layout) profile_layout.addLayout(header_layout)
# Profile list # Profile list
self.profile_list = QListWidget() self.profile_list = QListWidget()
self.profile_list.setSelectionMode(QListWidget.SingleSelection) self.profile_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
self.profile_list.setSelectionBehavior(QListWidget.SelectRows) self.profile_list.setSelectionBehavior(QListWidget.SelectionBehavior.SelectRows)
self.profile_list.setStyleSheet( self.profile_list.setStyleSheet(
"QListWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }" "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) idx = list(profiles.keys()).index(self.current_profile)
self.profile_list.setCurrentRow(idx) self.profile_list.setCurrentRow(idx)
profile_layout.addWidget(self.profile_list) 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.profile_list.customContextMenuRequested.connect(
self.show_profile_context_menu self.show_profile_context_menu
) )
@@ -409,7 +408,7 @@ class VoiceFormulaDialog(QDialog):
self.setWindowTitle("Voice Mixer") self.setWindowTitle("Voice Mixer")
self.setWindowFlags( 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.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT) self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)
@@ -468,21 +467,21 @@ class VoiceFormulaDialog(QDialog):
# Separator # Separator
separator = QFrame() separator = QFrame()
separator.setFrameShadow(QFrame.Sunken) separator.setFrameShadow(QFrame.Shadow.Sunken)
mixer_layout.addWidget(separator) mixer_layout.addWidget(separator)
# Voice list scroll area # Voice list scroll area
self.scroll_area = QScrollArea() self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.scroll_area.viewport().installEventFilter(self) self.scroll_area.viewport().installEventFilter(self)
self.voice_list_widget = QWidget() self.voice_list_widget = QWidget()
self.voice_list_layout = QHBoxLayout() self.voice_list_layout = QHBoxLayout()
self.voice_list_widget.setLayout(self.voice_list_layout) self.voice_list_widget.setLayout(self.voice_list_layout)
self.voice_list_widget.setSizePolicy( self.voice_list_widget.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Expanding QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
) )
self.scroll_area.setWidget(self.voice_list_widget) self.scroll_area.setWidget(self.voice_list_widget)
mixer_layout.addWidget(self.scroll_area, stretch=1) mixer_layout.addWidget(self.scroll_area, stretch=1)
@@ -497,6 +496,9 @@ class VoiceFormulaDialog(QDialog):
ok_button.setDefault(True) ok_button.setDefault(True)
ok_button.setFocus() ok_button.setFocus()
# Connect buttons
clear_all_button.clicked.connect(self.clear_all_voices)
ok_button.clicked.connect(self.accept)
# Connect buttons # Connect buttons
clear_all_button.clicked.connect(self.clear_all_voices) clear_all_button.clicked.connect(self.clear_all_voices)
ok_button.clicked.connect(self.accept) ok_button.clicked.connect(self.accept)
@@ -533,7 +535,7 @@ class VoiceFormulaDialog(QDialog):
def keyPressEvent(self, event): def keyPressEvent(self, event):
# Bind Delete key to delete_profile when a profile is selected # 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() item = self.profile_list.currentItem()
if item: if item:
self.delete_profile(item) self.delete_profile(item)
@@ -562,10 +564,10 @@ class VoiceFormulaDialog(QDialog):
self, self,
"Unsaved Changes", "Unsaved Changes",
msg, msg,
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
QMessageBox.Save, QMessageBox.StandardButton.Save,
) )
if ret == QMessageBox.Save: if ret == QMessageBox.StandardButton.Save:
# Save all using stored states # Save all using stored states
profiles = load_profiles() profiles = load_profiles()
for i in dirty_indices: for i in dirty_indices:
@@ -590,7 +592,7 @@ class VoiceFormulaDialog(QDialog):
self.update_profile_save_buttons() self.update_profile_save_buttons()
self.update_profile_list_colors() self.update_profile_list_colors()
return True return True
elif ret == QMessageBox.Discard: elif ret == QMessageBox.StandardButton.Discard:
# Discard all modifications # Discard all modifications
self._profile_states.clear() self._profile_states.clear()
for i in dirty_indices: for i in dirty_indices:
@@ -612,17 +614,17 @@ class VoiceFormulaDialog(QDialog):
else: else:
# Fallback to original logic for 0 or 1 dirty profile # Fallback to original logic for 0 or 1 dirty profile
box = QMessageBox(self) box = QMessageBox(self)
box.setIcon(QMessageBox.Warning) box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Unsaved Changes") box.setWindowTitle("Unsaved Changes")
box.setText( box.setText(
"You have unsaved changes in your profile. Do you want to save the changes?" "You have unsaved changes in your profile. Do you want to save the changes?"
) )
box.setStandardButtons( box.setStandardButtons(
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel
) )
box.setDefaultButton(QMessageBox.Save) box.setDefaultButton(QMessageBox.StandardButton.Save)
ret = box.exec_() ret = box.exec()
if ret == QMessageBox.Save: if ret == QMessageBox.StandardButton.Save:
for i in range(self.profile_list.count()): for i in range(self.profile_list.count()):
item = self.profile_list.item(i) item = self.profile_list.item(i)
name = item.text().lstrip("*") name = item.text().lstrip("*")
@@ -636,7 +638,7 @@ class VoiceFormulaDialog(QDialog):
if hasattr(parent, "populate_profiles_in_voice_combo"): if hasattr(parent, "populate_profiles_in_voice_combo"):
parent.populate_profiles_in_voice_combo() parent.populate_profiles_in_voice_combo()
return True return True
elif ret == QMessageBox.Discard: elif ret == QMessageBox.StandardButton.Discard:
profiles = load_profiles() profiles = load_profiles()
for i in range(self.profile_list.count()): for i in range(self.profile_list.count()):
item = self.profile_list.item(i) item = self.profile_list.item(i)
@@ -717,7 +719,7 @@ class VoiceFormulaDialog(QDialog):
return voice_mixer return voice_mixer
def handle_voice_checkbox(self, voice_mixer, state): 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.last_enabled_voice = voice_mixer.voice_name
self.update_weighted_sums() self.update_weighted_sums()
@@ -767,7 +769,7 @@ class VoiceFormulaDialog(QDialog):
f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>', f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>',
name, name,
) )
voice_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) voice_label.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
voice_label.delete_button.clicked.connect( voice_label.delete_button.clicked.connect(
lambda _, vn=name: self.disable_voice_by_name(vn) lambda _, vn=name: self.disable_voice_by_name(vn)
) )
@@ -787,7 +789,7 @@ class VoiceFormulaDialog(QDialog):
mixer.checkbox.setChecked(False) mixer.checkbox.setChecked(False)
def eventFilter(self, source, event): 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 # Skip if over an enabled slider
if any( if any(
mixer.slider.underMouse() and mixer.slider.isEnabled() mixer.slider.underMouse() and mixer.slider.isEnabled()
@@ -890,10 +892,10 @@ class VoiceFormulaDialog(QDialog):
self, self,
"Invalid Profiles", "Invalid Profiles",
msg, msg,
QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
QMessageBox.Yes, QMessageBox.StandardButton.Yes,
) )
if reply == QMessageBox.Yes: if reply == QMessageBox.StandardButton.Yes:
for i, name in reversed(zero): for i, name in reversed(zero):
self.profile_list.takeItem(i) self.profile_list.takeItem(i)
delete_profile(name) delete_profile(name)
@@ -980,8 +982,8 @@ class VoiceFormulaDialog(QDialog):
super().closeEvent(event) super().closeEvent(event)
def _parse_rgba_to_qcolor(self, rgba_str): def _parse_rgba_to_qcolor(self, rgba_str):
from PyQt5.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt5.QtGui import QColor from PyQt6.QtGui import QColor
"""Helper to convert 'rgba(R,G,B,A_float)' string to 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) 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 += f"\nThis will overwrite an existing profile."
msg += "\nContinue?" msg += "\nContinue?"
reply = QMessageBox.question( 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 return
profiles.update(imported_profiles) profiles.update(imported_profiles)
save_profiles(profiles) save_profiles(profiles)
@@ -1153,9 +1155,9 @@ class VoiceFormulaDialog(QDialog):
msg += f"\n{len(collisions)} profile(s) will be overwritten." msg += f"\n{len(collisions)} profile(s) will be overwritten."
msg += "\nContinue?" msg += "\nContinue?"
reply = QMessageBox.question( 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 return
profiles.update(imported_profiles) profiles.update(imported_profiles)
save_profiles(profiles) save_profiles(profiles)
@@ -1196,7 +1198,7 @@ class VoiceFormulaDialog(QDialog):
menu.addAction(dup_act) menu.addAction(dup_act)
menu.addAction(export_act) menu.addAction(export_act)
menu.addAction(delete_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: if act == rename_act:
self.rename_profile(item) self.rename_profile(item)
elif act == delete_act: elif act == delete_act:
@@ -1323,9 +1325,9 @@ class VoiceFormulaDialog(QDialog):
self, self,
"Delete Profile", "Delete Profile",
f"Delete profile '{name}'?", 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) delete_profile(name)
row = self.profile_list.row(item) row = self.profile_list.row(item)
self.profile_list.takeItem(row) self.profile_list.takeItem(row)
@@ -1378,7 +1380,7 @@ class VoiceFormulaDialog(QDialog):
self.profile_list.setItemWidget(item, widget) self.profile_list.setItemWidget(item, widget)
def update_profile_list_colors(self): def update_profile_list_colors(self):
from PyQt5.QtCore import Qt from PyQt6.QtCore import Qt
profiles = load_profiles() profiles = load_profiles()
for i in range(self.profile_list.count()): for i in range(self.profile_list.count()):
@@ -1386,13 +1388,13 @@ class VoiceFormulaDialog(QDialog):
name = item.text().lstrip("*") name = item.text().lstrip("*")
if self._virtual_new_profile and name == "New profile": if self._virtual_new_profile and name == "New profile":
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND")) 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("*"): elif item.text().startswith("*"):
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND")) color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
item.setData(Qt.BackgroundRole, color) item.setData(Qt.ItemDataRole.BackgroundRole, color)
else: else:
item.setData( 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", []) weights = profiles.get(name, {}).get("voices", [])
total = 0 total = 0
@@ -1406,7 +1408,7 @@ class VoiceFormulaDialog(QDialog):
total += entry[1] total += entry[1]
if total == 0: if total == 0:
color = self._parse_rgba_to_qcolor(COLORS.get("RED_BACKGROUND")) 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() self.update_profile_save_buttons()
def preview_current_mix(self): def preview_current_mix(self):
+1 -1
View File
@@ -13,7 +13,7 @@ license = "MIT"
requires-python = ">=3.10, <3.13" 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"] keywords = ["audiobook", "epub", "pdf", "text-to-speech", "subtitle", "tts", "kokoro", "accessibility", "book-converter", "voice-synthesis", "multilingual", "chapter-management", "subtitles", "content-creation", "media-generation"]
dependencies = [ dependencies = [
"PyQt5>=5.15.11", "PyQt6>=6.10.0",
"kokoro>=0.9.4", "kokoro>=0.9.4",
"misaki[zh]>=0.9.4", "misaki[zh]>=0.9.4",
"ebooklib>=0.19", "ebooklib>=0.19",