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