Reformat using black

This commit is contained in:
Deniz Şafak
2025-10-27 17:26:54 +03:00
parent 9d2e8bed00
commit 47fe6341b4
7 changed files with 701 additions and 381 deletions
+219 -142
View File
@@ -32,7 +32,12 @@ from PyQt6.QtCore import (
QPoint, QPoint,
QRect, QRect,
) )
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
import urllib.parse import urllib.parse
@@ -50,7 +55,7 @@ class HandlerDialog(QDialog):
_save_chapters_separately = False _save_chapters_separately = False
_merge_chapters_at_end = True _merge_chapters_at_end = True
_save_as_project = False # New class variable for save_as_project option _save_as_project = False # New class variable for save_as_project option
# Cache for processed book content to avoid reprocessing # Cache for processed book content to avoid reprocessing
# Key: (book_path, modification_time, file_type) # Key: (book_path, modification_time, file_type)
# Value: dict with content_texts, content_lengths, doc_content (for epub), markdown_toc (for markdown) # Value: dict with content_texts, content_lengths, doc_content (for epub), markdown_toc (for markdown)
@@ -58,6 +63,7 @@ class HandlerDialog(QDialog):
class _LoaderThread(QThread): class _LoaderThread(QThread):
"""Minimal QThread that runs a callable and emits an error string on exception.""" """Minimal QThread that runs a callable and emits an error string on exception."""
error = pyqtSignal(str) error = pyqtSignal(str)
def __init__(self, target_callable): def __init__(self, target_callable):
@@ -77,7 +83,9 @@ class HandlerDialog(QDialog):
cls._content_cache.clear() cls._content_cache.clear()
logging.info("Cleared all content cache") logging.info("Cleared all content cache")
else: else:
keys_to_remove = [key for key in cls._content_cache.keys() if key[0] == book_path] keys_to_remove = [
key for key in cls._content_cache.keys() if key[0] == book_path
]
for key in keys_to_remove: for key in keys_to_remove:
del cls._content_cache[key] del cls._content_cache[key]
if keys_to_remove: if keys_to_remove:
@@ -102,12 +110,14 @@ class HandlerDialog(QDialog):
# Set window title based on file type and book name # Set window title based on file type and book name
item_type = "Chapters" if self.file_type in ["epub", "markdown"] else "Pages" item_type = "Chapters" if self.file_type in ["epub", "markdown"] else "Pages"
self.setWindowTitle(f'Select {item_type} - {book_name}') self.setWindowTitle(f"Select {item_type} - {book_name}")
self.resize(1200, 900) self.resize(1200, 900)
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.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMaximizeButtonHint Qt.WindowType.Window
| Qt.WindowType.WindowCloseButtonHint
| Qt.WindowType.WindowMaximizeButtonHint
) )
self.setWindowModality(Qt.WindowModality.NonModal) self.setWindowModality(Qt.WindowModality.NonModal)
# Initialize save chapters flags from class variables # Initialize save chapters flags from class variables
@@ -199,7 +209,7 @@ class HandlerDialog(QDialog):
# Create a centered loading overlay and show it while background load runs # Create a centered loading overlay and show it while background load runs
self._create_loading_overlay() self._create_loading_overlay()
# Hide the main UI so only the overlay is visible initially # Hide the main UI so only the overlay is visible initially
if getattr(self, 'splitter', None) is not None: if getattr(self, "splitter", None) is not None:
self.splitter.setVisible(False) self.splitter.setVisible(False)
self._show_loading_overlay("Loading...") self._show_loading_overlay("Loading...")
@@ -214,8 +224,6 @@ class HandlerDialog(QDialog):
break break
self.treeWidget.setRootIsDecorated(has_parents) self.treeWidget.setRootIsDecorated(has_parents)
def _create_loading_overlay(self): def _create_loading_overlay(self):
"""Create a centered loading indicator with a GIF on the left and text on the right. """Create a centered loading indicator with a GIF on the left and text on the right.
@@ -277,10 +285,10 @@ class HandlerDialog(QDialog):
self._loading_movie = None self._loading_movie = None
def _show_loading_overlay(self, text: str): def _show_loading_overlay(self, text: str):
container = getattr(self, '_loading_container', None) container = getattr(self, "_loading_container", None)
text_lbl = getattr(self, '_loading_text_label', None) text_lbl = getattr(self, "_loading_text_label", None)
movie = getattr(self, '_loading_movie', None) movie = getattr(self, "_loading_movie", None)
gif_lbl = getattr(self, '_loading_gif_label', None) gif_lbl = getattr(self, "_loading_gif_label", None)
if container is None or text_lbl is None: if container is None or text_lbl is None:
return return
text_lbl.setText(text) text_lbl.setText(text)
@@ -293,8 +301,8 @@ class HandlerDialog(QDialog):
container.setVisible(True) container.setVisible(True)
def _hide_loading_overlay(self): def _hide_loading_overlay(self):
container = getattr(self, '_loading_container', None) container = getattr(self, "_loading_container", None)
movie = getattr(self, '_loading_movie', None) movie = getattr(self, "_loading_movie", None)
if container is None: if container is None:
return return
if movie is not None: if movie is not None:
@@ -304,7 +312,6 @@ class HandlerDialog(QDialog):
pass pass
container.setVisible(False) container.setVisible(False)
def _start_background_load(self): def _start_background_load(self):
"""Start a QThread that runs the preprocessing in background.""" """Start a QThread that runs the preprocessing in background."""
# Start a minimal QThread which executes _preprocess_content # Start a minimal QThread which executes _preprocess_content
@@ -317,9 +324,9 @@ class HandlerDialog(QDialog):
def _on_load_error(self, err_msg): def _on_load_error(self, err_msg):
logging.error(f"Error loading book in background: {err_msg}") logging.error(f"Error loading book in background: {err_msg}")
if getattr(self, 'previewEdit', None) is not None: if getattr(self, "previewEdit", None) is not None:
self.previewEdit.setPlainText(f"Error loading book: {err_msg}") self.previewEdit.setPlainText(f"Error loading book: {err_msg}")
if getattr(self, 'splitter', None) is not None: if getattr(self, "splitter", None) is not None:
self.splitter.setVisible(True) self.splitter.setVisible(True)
self._hide_loading_overlay() self._hide_loading_overlay()
@@ -337,7 +344,9 @@ class HandlerDialog(QDialog):
# Connect signals (after tree exists) # Connect signals (after tree exists)
self.treeWidget.currentItemChanged.connect(self.update_preview) self.treeWidget.currentItemChanged.connect(self.update_preview)
self.treeWidget.itemChanged.connect(self.handle_item_check) self.treeWidget.itemChanged.connect(self.handle_item_check)
self.treeWidget.itemChanged.connect(lambda _: self._update_checkbox_states()) self.treeWidget.itemChanged.connect(
lambda _: self._update_checkbox_states()
)
self.treeWidget.itemDoubleClicked.connect(self.handle_item_double_click) self.treeWidget.itemDoubleClicked.connect(self.handle_item_double_click)
# Expand and select first item # Expand and select first item
@@ -356,7 +365,7 @@ class HandlerDialog(QDialog):
except Exception as e: except Exception as e:
logging.error(f"Error finalizing book load: {e}") logging.error(f"Error finalizing book load: {e}")
# Show the main UI and hide loading text # Show the main UI and hide loading text
if getattr(self, 'splitter', None) is not None: if getattr(self, "splitter", None) is not None:
self.splitter.setVisible(True) self.splitter.setVisible(True)
self._hide_loading_overlay() self._hide_loading_overlay()
@@ -367,21 +376,21 @@ class HandlerDialog(QDialog):
mod_time = os.path.getmtime(self.book_path) mod_time = os.path.getmtime(self.book_path)
except Exception: except Exception:
mod_time = 0 mod_time = 0
cache_key = (self.book_path, mod_time, self.file_type) cache_key = (self.book_path, mod_time, self.file_type)
# Check if content is already cached # Check if content is already cached
if cache_key in HandlerDialog._content_cache: if cache_key in HandlerDialog._content_cache:
cached_data = HandlerDialog._content_cache[cache_key] cached_data = HandlerDialog._content_cache[cache_key]
self.content_texts = cached_data['content_texts'] self.content_texts = cached_data["content_texts"]
self.content_lengths = cached_data['content_lengths'] self.content_lengths = cached_data["content_lengths"]
if 'doc_content' in cached_data: if "doc_content" in cached_data:
self.doc_content = cached_data['doc_content'] self.doc_content = cached_data["doc_content"]
if 'markdown_toc' in cached_data: if "markdown_toc" in cached_data:
self.markdown_toc = cached_data['markdown_toc'] self.markdown_toc = cached_data["markdown_toc"]
logging.info(f"Using cached content for {os.path.basename(self.book_path)}") logging.info(f"Using cached content for {os.path.basename(self.book_path)}")
return return
# Process content if not cached # Process content if not cached
if self.file_type == "epub": if self.file_type == "epub":
try: try:
@@ -397,17 +406,17 @@ class HandlerDialog(QDialog):
self._preprocess_markdown_content() self._preprocess_markdown_content()
else: else:
self._preprocess_pdf_content() self._preprocess_pdf_content()
# Cache the processed content # Cache the processed content
cache_data = { cache_data = {
'content_texts': self.content_texts, "content_texts": self.content_texts,
'content_lengths': self.content_lengths, "content_lengths": self.content_lengths,
} }
if hasattr(self, 'doc_content'): if hasattr(self, "doc_content"):
cache_data['doc_content'] = self.doc_content cache_data["doc_content"] = self.doc_content
if hasattr(self, 'markdown_toc'): if hasattr(self, "markdown_toc"):
cache_data['markdown_toc'] = self.markdown_toc cache_data["markdown_toc"] = self.markdown_toc
HandlerDialog._content_cache[cache_key] = cache_data HandlerDialog._content_cache[cache_key] = cache_data
logging.info(f"Cached content for {os.path.basename(self.book_path)}") logging.info(f"Cached content for {os.path.basename(self.book_path)}")
@@ -432,7 +441,6 @@ class HandlerDialog(QDialog):
page_id = f"page_{page_num + 1}" page_id = f"page_{page_num + 1}"
self.content_texts[page_id] = text self.content_texts[page_id] = text
self.content_lengths[page_id] = calculate_text_length(text) self.content_lengths[page_id] = calculate_text_length(text)
def _preprocess_markdown_content(self): def _preprocess_markdown_content(self):
if not self.markdown_text: if not self.markdown_text:
@@ -441,14 +449,14 @@ class HandlerDialog(QDialog):
# Generate TOC from the original (dedented) markdown BEFORE cleaning, # Generate TOC from the original (dedented) markdown BEFORE cleaning,
# so header ids/anchors are preserved for reliable position detection. # so header ids/anchors are preserved for reliable position detection.
original_text = textwrap.dedent(self.markdown_text) original_text = textwrap.dedent(self.markdown_text)
md = markdown.Markdown(extensions=['toc', 'fenced_code']) md = markdown.Markdown(extensions=["toc", "fenced_code"])
html = md.convert(original_text) html = md.convert(original_text)
self.markdown_toc = md.toc_tokens self.markdown_toc = md.toc_tokens
# Use cleaned text for stored content/length calculations # Use cleaned text for stored content/length calculations
cleaned_full_text = clean_text(original_text) cleaned_full_text = clean_text(original_text)
soup = BeautifulSoup(html, 'html.parser') soup = BeautifulSoup(html, "html.parser")
self.content_texts = {} self.content_texts = {}
self.content_lengths = {} self.content_lengths = {}
@@ -459,35 +467,39 @@ class HandlerDialog(QDialog):
return return
all_headers = [] all_headers = []
def flatten_toc(toc_list): def flatten_toc(toc_list):
for header in toc_list: for header in toc_list:
all_headers.append(header) all_headers.append(header)
if header.get('children'): if header.get("children"):
flatten_toc(header['children']) flatten_toc(header["children"])
flatten_toc(self.markdown_toc) flatten_toc(self.markdown_toc)
header_positions = [] header_positions = []
for header in all_headers: for header in all_headers:
header_id = header['id'] header_id = header["id"]
id_pattern = f'id="{header_id}"' id_pattern = f'id="{header_id}"'
pos = html.find(id_pattern) pos = html.find(id_pattern)
if pos != -1: if pos != -1:
tag_start = html.rfind('<', 0, pos) tag_start = html.rfind("<", 0, pos)
header_positions.append({ header_positions.append(
'id': header_id, {"id": header_id, "start": tag_start, "name": header["name"]}
'start': tag_start, )
'name': header['name'] header_positions.sort(key=lambda x: x["start"])
})
header_positions.sort(key=lambda x: x['start'])
for i, header_pos in enumerate(header_positions): for i, header_pos in enumerate(header_positions):
header_id = header_pos['id'] header_id = header_pos["id"]
header_name = header_pos['name'] header_name = header_pos["name"]
content_start = header_pos['start'] content_start = header_pos["start"]
content_end = header_positions[i + 1]['start'] if i + 1 < len(header_positions) else len(html) content_end = (
header_positions[i + 1]["start"]
if i + 1 < len(header_positions)
else len(html)
)
section_html = html[content_start:content_end] section_html = html[content_start:content_end]
section_soup = BeautifulSoup(section_html, 'html.parser') section_soup = BeautifulSoup(section_html, "html.parser")
header_tag = section_soup.find(attrs={'id': header_id}) header_tag = section_soup.find(attrs={"id": header_id})
if header_tag: if header_tag:
header_tag.decompose() header_tag.decompose()
# Clean section text for storage/lengths # Clean section text for storage/lengths
@@ -533,7 +545,7 @@ class HandlerDialog(QDialog):
html_content = self.doc_content.get(doc_href, "") html_content = self.doc_content.get(doc_href, "")
if html_content: if html_content:
soup = BeautifulSoup(html_content, "html.parser") soup = BeautifulSoup(html_content, "html.parser")
# Handle ordered lists by prepending numbers to list items # Handle ordered lists by prepending numbers to list items
for ol in soup.find_all("ol"): for ol in soup.find_all("ol"):
# Get start attribute or default to 1 # Get start attribute or default to 1
@@ -545,11 +557,11 @@ class HandlerDialog(QDialog):
li.string.replace_with(number_text + li.string) li.string.replace_with(number_text + li.string)
else: else:
li.insert(0, NavigableString(number_text)) li.insert(0, NavigableString(number_text))
# Remove sup and sub tags # Remove sup and sub tags
for tag in soup.find_all(["sup", "sub"]): for tag in soup.find_all(["sup", "sub"]):
tag.decompose() tag.decompose()
text = clean_text(soup.get_text()).strip() text = clean_text(soup.get_text()).strip()
if text: if text:
self.content_texts[doc_href] = text self.content_texts[doc_href] = text
@@ -569,7 +581,7 @@ class HandlerDialog(QDialog):
# Replace book.toc with the synthetic one if it was empty or fallback was triggered # Replace book.toc with the synthetic one if it was empty or fallback was triggered
if not self.book.toc or not hasattr( if not self.book.toc or not hasattr(
self, "processed_nav_structure" self, "processed_nav_structure"
): # Check if nav processing failed ): # Check if nav processing failed
self.book.toc = synthetic_toc self.book.toc = synthetic_toc
logging.info(f"Generated synthetic TOC with {len(synthetic_toc)} entries.") logging.info(f"Generated synthetic TOC with {len(synthetic_toc)} entries.")
@@ -594,7 +606,7 @@ class HandlerDialog(QDialog):
item item
for item in nav_items for item in nav_items
if "nav" in item.get_name().lower() if "nav" in item.get_name().lower()
and item.get_name().lower().endswith((".xhtml", ".html")) and item.get_name().lower().endswith((".xhtml", ".html"))
), ),
None, None,
) )
@@ -707,8 +719,8 @@ class HandlerDialog(QDialog):
for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT): for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
href = item.get_name() href = item.get_name()
if href in doc_order or any( if href in doc_order or any(
href in nav_point.get("src", "") href in nav_point.get("src", "")
for nav_point in nav_soup.find_all(["content", "a"]) for nav_point in nav_soup.find_all(["content", "a"])
): ):
try: try:
html_content = item.get_content().decode("utf-8", errors="ignore") html_content = item.get_content().decode("utf-8", errors="ignore")
@@ -856,7 +868,7 @@ class HandlerDialog(QDialog):
# Add line breaks after paragraphs and divs # Add line breaks after paragraphs and divs
for tag in slice_soup.find_all(["p", "div"]): for tag in slice_soup.find_all(["p", "div"]):
tag.append("\n\n") tag.append("\n\n")
# Handle ordered lists by prepending numbers to list items # Handle ordered lists by prepending numbers to list items
for ol in slice_soup.find_all("ol"): for ol in slice_soup.find_all("ol"):
# Get start attribute or default to 1 # Get start attribute or default to 1
@@ -868,11 +880,11 @@ class HandlerDialog(QDialog):
li.string.replace_with(number_text + li.string) li.string.replace_with(number_text + li.string)
else: else:
li.insert(0, NavigableString(number_text)) li.insert(0, NavigableString(number_text))
# Remove sup and sub tags that might contain footnotes # Remove sup and sub tags that might contain footnotes
for tag in slice_soup.find_all(["sup", "sub"]): for tag in slice_soup.find_all(["sup", "sub"]):
tag.decompose() tag.decompose()
text = clean_text(slice_soup.get_text()).strip() text = clean_text(slice_soup.get_text()).strip()
if text: if text:
self.content_texts[current_src] = text self.content_texts[current_src] = text
@@ -948,13 +960,13 @@ class HandlerDialog(QDialog):
return None, None return None, None
def _parse_ncx_navpoint( def _parse_ncx_navpoint(
self, self,
nav_point, nav_point,
ordered_entries, ordered_entries,
doc_order, doc_order,
doc_order_decoded, doc_order_decoded,
tree_structure_list, tree_structure_list,
find_position_func, find_position_func,
): ):
nav_label = nav_point.find("navLabel") nav_label = nav_point.find("navLabel")
content = nav_point.find("content") content = nav_point.find("content")
@@ -1006,19 +1018,19 @@ class HandlerDialog(QDialog):
) )
if title and ( if title and (
current_entry_node.get("has_content", False) current_entry_node.get("has_content", False)
or current_entry_node["children"] or current_entry_node["children"]
): ):
tree_structure_list.append(current_entry_node) tree_structure_list.append(current_entry_node)
def _parse_html_nav_li( def _parse_html_nav_li(
self, self,
li_element, li_element,
ordered_entries, ordered_entries,
doc_order, doc_order,
doc_order_decoded, doc_order_decoded,
tree_structure_list, tree_structure_list,
find_position_func, find_position_func,
): ):
link = li_element.find("a", recursive=False) link = li_element.find("a", recursive=False)
span_text = li_element.find("span", recursive=False) span_text = li_element.find("span", recursive=False)
@@ -1166,8 +1178,8 @@ class HandlerDialog(QDialog):
if self.file_type == "epub": if self.file_type == "epub":
if ( if (
hasattr(self, "processed_nav_structure") hasattr(self, "processed_nav_structure")
and self.processed_nav_structure and self.processed_nav_structure
): ):
self._build_epub_tree_from_nav( self._build_epub_tree_from_nav(
self.processed_nav_structure, self.treeWidget self.processed_nav_structure, self.treeWidget
@@ -1195,7 +1207,7 @@ class HandlerDialog(QDialog):
self._update_item_checkbox_state(item) self._update_item_checkbox_state(item)
def _build_epub_tree_from_nav( def _build_epub_tree_from_nav(
self, nav_nodes, parent_item, seen_content_hashes=None self, nav_nodes, parent_item, seen_content_hashes=None
): ):
if seen_content_hashes is None: if seen_content_hashes is None:
seen_content_hashes = set() seen_content_hashes = set()
@@ -1208,9 +1220,9 @@ class HandlerDialog(QDialog):
item.setData(0, Qt.ItemDataRole.UserRole, src) item.setData(0, Qt.ItemDataRole.UserRole, src)
is_empty = ( is_empty = (
src src
and (src in self.content_texts) and (src in self.content_texts)
and (not self.content_texts[src].strip()) and (not self.content_texts[src].strip())
) )
is_duplicate = False is_duplicate = False
if src and src in self.content_texts and self.content_texts[src].strip(): if src and src in self.content_texts and self.content_texts[src].strip():
@@ -1223,7 +1235,9 @@ class HandlerDialog(QDialog):
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.ItemFlag.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.CheckState.Checked if is_checked else Qt.CheckState.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)")
@@ -1265,13 +1279,15 @@ class HandlerDialog(QDialog):
item.setData(0, Qt.ItemDataRole.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.ItemFlag.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.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) item.setCheckState(
0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked
)
else: else:
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
@@ -1358,8 +1374,8 @@ class HandlerDialog(QDialog):
next_page = next_page_boundaries.get(page_num, len(self.pdf_doc)) next_page = next_page_boundaries.get(page_num, len(self.pdf_doc))
for sub_page_num in range(page_num + 1, next_page): for sub_page_num in range(page_num + 1, next_page):
if ( if (
sub_page_num in page_to_bookmark sub_page_num in page_to_bookmark
or sub_page_num in added_pages or sub_page_num in added_pages
): ):
continue continue
@@ -1420,7 +1436,12 @@ class HandlerDialog(QDialog):
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.ItemFlag.ItemIsUserCheckable) page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
page_item.setCheckState( page_item.setCheckState(
0, Qt.CheckState.Checked if page_id in self.checked_chapters else Qt.CheckState.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.ItemFlag.ItemIsUserCheckable) page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
@@ -1440,31 +1461,45 @@ class HandlerDialog(QDialog):
if self.content_lengths.get(chapter_id, 0) > 0: if self.content_lengths.get(chapter_id, 0) > 0:
item.setFlags(item.flags() | Qt.ItemFlag.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.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) item.setCheckState(
0,
(
Qt.CheckState.Checked
if is_checked
else Qt.CheckState.Unchecked
),
)
else: else:
item.setFlags(item.flags() & ~Qt.ItemFlag.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):
for header in toc_list: for header in toc_list:
title = header['name'] title = header["name"]
chapter_id = header['id'] chapter_id = header["id"]
item = QTreeWidgetItem(parent_item, [title]) item = QTreeWidgetItem(parent_item, [title])
item.setData(0, Qt.ItemDataRole.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.ItemFlag.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.CheckState.Checked if is_checked else Qt.CheckState.Unchecked) item.setCheckState(
0,
(
Qt.CheckState.Checked
if is_checked
else Qt.CheckState.Unchecked
),
)
else: else:
item.setFlags(item.flags() & ~Qt.ItemFlag.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)
build_from_toc(self.markdown_toc, self.treeWidget) build_from_toc(self.markdown_toc, self.treeWidget)
@@ -1491,7 +1526,12 @@ class HandlerDialog(QDialog):
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.ItemFlag.ItemIsUserCheckable) page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
page_item.setCheckState( page_item.setCheckState(
0, Qt.CheckState.Checked if page_id in self.checked_chapters else Qt.CheckState.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.ItemFlag.ItemIsUserCheckable) page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
@@ -1535,7 +1575,10 @@ class HandlerDialog(QDialog):
rightWidget = QWidget() rightWidget = QWidget()
rightWidget.setLayout(previewLayout) rightWidget.setLayout(previewLayout)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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)
@@ -1632,15 +1675,15 @@ class HandlerDialog(QDialog):
def _update_checkbox_states(self): def _update_checkbox_states(self):
if ( if (
not hasattr(self, "save_chapters_checkbox") not hasattr(self, "save_chapters_checkbox")
or not self.save_chapters_checkbox or not self.save_chapters_checkbox
): ):
return return
if ( if (
self.file_type == "pdf" self.file_type == "pdf"
and hasattr(self, "has_pdf_bookmarks") and hasattr(self, "has_pdf_bookmarks")
and not self.has_pdf_bookmarks and not self.has_pdf_bookmarks
): ):
self.save_chapters_checkbox.setEnabled(False) self.save_chapters_checkbox.setEnabled(False)
self.merge_chapters_checkbox.setEnabled(False) self.merge_chapters_checkbox.setEnabled(False)
@@ -1653,8 +1696,8 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if ( if (
item.flags() & Qt.ItemFlag.ItemIsUserCheckable item.flags() & Qt.ItemFlag.ItemIsUserCheckable
and item.checkState(0) == Qt.CheckState.Checked and item.checkState(0) == Qt.CheckState.Checked
): ):
checked_count += 1 checked_count += 1
if checked_count >= 2: if checked_count >= 2:
@@ -1668,8 +1711,8 @@ class HandlerDialog(QDialog):
while iterator.value(): while iterator.value():
item = iterator.value() item = iterator.value()
if ( if (
item.flags() & Qt.ItemFlag.ItemIsUserCheckable item.flags() & Qt.ItemFlag.ItemIsUserCheckable
and item.checkState(0) == Qt.CheckState.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():
@@ -1769,7 +1812,7 @@ class HandlerDialog(QDialog):
if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
child_src = child.data(0, Qt.ItemDataRole.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:
@@ -1791,7 +1834,9 @@ class HandlerDialog(QDialog):
identifier = item.data(0, Qt.ItemDataRole.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:
@@ -1803,7 +1848,8 @@ class HandlerDialog(QDialog):
if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
child_identifier = child.data(0, Qt.ItemDataRole.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:
@@ -1837,8 +1883,8 @@ class HandlerDialog(QDialog):
continue continue
if ( if (
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.CheckState.Checked) item.setCheckState(0, Qt.CheckState.Checked)
@@ -1881,7 +1927,9 @@ class HandlerDialog(QDialog):
if mouse_pos.x() > rect.x() + checkbox_width: if mouse_pos.x() > rect.x() + checkbox_width:
new_state = ( new_state = (
Qt.CheckState.Unchecked if item.checkState(0) == Qt.CheckState.Checked else Qt.CheckState.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)
@@ -1949,7 +1997,7 @@ class HandlerDialog(QDialog):
html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>" html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>"
if self.book_metadata["publisher"] or self.book_metadata.get( if self.book_metadata["publisher"] or self.book_metadata.get(
"publication_year" "publication_year"
): ):
pub_info = [] pub_info = []
if self.book_metadata["publisher"]: if self.book_metadata["publisher"]:
@@ -2039,27 +2087,49 @@ class HandlerDialog(QDialog):
# Extract metadata from markdown frontmatter or first heading # Extract metadata from markdown frontmatter or first heading
if self.markdown_text: if self.markdown_text:
# Try to extract YAML frontmatter # Try to extract YAML frontmatter
frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', self.markdown_text, re.DOTALL) frontmatter_match = re.match(
r"^---\s*\n(.*?)\n---\s*\n", self.markdown_text, re.DOTALL
)
if frontmatter_match: if frontmatter_match:
try: try:
frontmatter = frontmatter_match.group(1) frontmatter = frontmatter_match.group(1)
# Simple YAML-like parsing for common fields # Simple YAML-like parsing for common fields
title_match = re.search(r'^title:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE) title_match = re.search(
r"^title:\s*(.+)$",
frontmatter,
re.MULTILINE | re.IGNORECASE,
)
if title_match: if title_match:
metadata["title"] = title_match.group(1).strip().strip('"\'') metadata["title"] = (
title_match.group(1).strip().strip("\"'")
)
author_match = re.search(r'^author:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE) author_match = re.search(
r"^author:\s*(.+)$",
frontmatter,
re.MULTILINE | re.IGNORECASE,
)
if author_match: if author_match:
metadata["authors"] = [author_match.group(1).strip().strip('"\'')] metadata["authors"] = [
author_match.group(1).strip().strip("\"'")
]
desc_match = re.search(r'^description:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE) desc_match = re.search(
r"^description:\s*(.+)$",
frontmatter,
re.MULTILINE | re.IGNORECASE,
)
if desc_match: if desc_match:
metadata["description"] = desc_match.group(1).strip().strip('"\'') metadata["description"] = (
desc_match.group(1).strip().strip("\"'")
)
date_match = re.search(r'^date:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE) date_match = re.search(
r"^date:\s*(.+)$", frontmatter, re.MULTILINE | re.IGNORECASE
)
if date_match: if date_match:
date_str = date_match.group(1).strip().strip('"\'') date_str = date_match.group(1).strip().strip("\"'")
year_match = re.search(r'\b(19|20)\d{2}\b', date_str) year_match = re.search(r"\b(19|20)\d{2}\b", date_str)
if year_match: if year_match:
metadata["publication_year"] = year_match.group(0) metadata["publication_year"] = year_match.group(0)
except Exception as e: except Exception as e:
@@ -2068,9 +2138,11 @@ class HandlerDialog(QDialog):
# Fallback: use first H1 header as title if no frontmatter title # Fallback: use first H1 header as title if no frontmatter title
if not metadata["title"] and self.markdown_toc: if not metadata["title"] and self.markdown_toc:
# Find the first level 1 header # Find the first level 1 header
first_h1 = next((h for h in self.markdown_toc if h['level'] == 1), None) first_h1 = next(
(h for h in self.markdown_toc if h["level"] == 1), None
)
if first_h1: if first_h1:
metadata["title"] = first_h1['name'] metadata["title"] = first_h1["name"]
else: else:
pdf_info = self.pdf_doc.metadata pdf_info = self.pdf_doc.metadata
if pdf_info: if pdf_info:
@@ -2117,7 +2189,10 @@ class HandlerDialog(QDialog):
# preserve compatibility with callers that expect content to be ready # preserve compatibility with callers that expect content to be ready
# when they create a HandlerDialog and immediately request selected text. # when they create a HandlerDialog and immediately request selected text.
try: try:
if hasattr(self, '_loader_thread') and getattr(self, '_loader_thread') is not None: if (
hasattr(self, "_loader_thread")
and getattr(self, "_loader_thread") is not None
):
# Wait for thread to finish (blocks until done) # Wait for thread to finish (blocks until done)
if self._loader_thread.isRunning(): if self._loader_thread.isRunning():
self._loader_thread.wait() self._loader_thread.wait()
@@ -2145,7 +2220,7 @@ class HandlerDialog(QDialog):
authors_text = ", ".join(authors) authors_text = ", ".join(authors)
album_artist = authors_text or "Unknown" album_artist = authors_text or "Unknown"
year = ( year = (
metadata.get("publication_year") or current_year metadata.get("publication_year") or current_year
) # Use publication year if available ) # Use publication year if available
# Count chapters/pages # Count chapters/pages
@@ -2248,7 +2323,7 @@ class HandlerDialog(QDialog):
metadata_tags = self._format_metadata_tags() metadata_tags = self._format_metadata_tags()
pdf_has_no_bookmarks = ( pdf_has_no_bookmarks = (
hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks
) )
iterator = QTreeWidgetItemIterator(self.treeWidget) iterator = QTreeWidgetItemIterator(self.treeWidget)
@@ -2288,9 +2363,9 @@ class HandlerDialog(QDialog):
child = item.child(i) child = item.child(i)
child_id = child.data(0, Qt.ItemDataRole.UserRole) child_id = child.data(0, Qt.ItemDataRole.UserRole)
if ( if (
child.checkState(0) == Qt.CheckState.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
): ):
checked_children.append((child, child_id)) checked_children.append((child, child_id))
if parent_checked and parent_id and parent_id not in included_text_ids: if parent_checked and parent_id and parent_id not in included_text_ids:
@@ -2319,9 +2394,9 @@ class HandlerDialog(QDialog):
elif item.flags() & Qt.ItemFlag.ItemIsUserCheckable: elif item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
identifier = item.data(0, Qt.ItemDataRole.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.CheckState.Checked and item.checkState(0) == Qt.CheckState.Checked
): ):
text = self.content_texts.get(identifier, "") text = self.content_texts.get(identifier, "")
if text: if text:
@@ -2374,7 +2449,9 @@ class HandlerDialog(QDialog):
self.treeWidget.blockSignals(True) self.treeWidget.blockSignals(True)
for item in self.treeWidget.selectedItems(): for item in self.treeWidget.selectedItems():
if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
item.setCheckState(0, Qt.CheckState.Checked if state else Qt.CheckState.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()
@@ -2391,9 +2468,9 @@ class HandlerDialog(QDialog):
return return
if ( if (
not item not item
or item.childCount() == 0 or item.childCount() == 0
or not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable) or not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable)
): ):
return return
+101 -48
View File
@@ -6,7 +6,12 @@ from platformdirs import user_desktop_dir
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt6.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 (
LANGUAGE_DESCRIPTIONS, LANGUAGE_DESCRIPTIONS,
SAMPLE_VOICE_TEXTS, SAMPLE_VOICE_TEXTS,
@@ -31,60 +36,62 @@ def get_sample_voice_text(lang_code):
def sanitize_name_for_os(name, is_folder=True): def sanitize_name_for_os(name, is_folder=True):
""" """
Sanitize a filename or folder name based on the operating system. Sanitize a filename or folder name based on the operating system.
Args: Args:
name: The name to sanitize name: The name to sanitize
is_folder: Whether this is a folder name (default: True) is_folder: Whether this is a folder name (default: True)
Returns: Returns:
Sanitized name safe for the current OS Sanitized name safe for the current OS
""" """
if not name: if not name:
return "audiobook" return "audiobook"
system = platform.system() system = platform.system()
if system == "Windows": if system == "Windows":
# Windows illegal characters: < > : " / \ | ? * # Windows illegal characters: < > : " / \ | ? *
# Also can't end with space or dot # Also can't end with space or dot
sanitized = re.sub(r'[<>:"/\\|?*]', '_', name) sanitized = re.sub(r'[<>:"/\\|?*]', "_", name)
# Remove control characters (0-31) # Remove control characters (0-31)
sanitized = re.sub(r'[\x00-\x1f]', '_', sanitized) sanitized = re.sub(r"[\x00-\x1f]", "_", sanitized)
# Remove trailing spaces and dots # Remove trailing spaces and dots
sanitized = sanitized.rstrip('. ') sanitized = sanitized.rstrip(". ")
# Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) # Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
reserved = ['CON', 'PRN', 'AUX', 'NUL'] + \ reserved = (
[f'COM{i}' for i in range(1, 10)] + \ ["CON", "PRN", "AUX", "NUL"]
[f'LPT{i}' for i in range(1, 10)] + [f"COM{i}" for i in range(1, 10)]
if sanitized.upper() in reserved or sanitized.upper().split('.')[0] in reserved: + [f"LPT{i}" for i in range(1, 10)]
)
if sanitized.upper() in reserved or sanitized.upper().split(".")[0] in reserved:
sanitized = f"_{sanitized}" sanitized = f"_{sanitized}"
elif system == "Darwin": # macOS elif system == "Darwin": # macOS
# macOS illegal characters: : (colon is converted to / by the system) # macOS illegal characters: : (colon is converted to / by the system)
# Also can't start with dot (hidden file) for folders typically # Also can't start with dot (hidden file) for folders typically
sanitized = re.sub(r'[:]', '_', name) sanitized = re.sub(r"[:]", "_", name)
# Remove control characters # Remove control characters
sanitized = re.sub(r'[\x00-\x1f]', '_', sanitized) sanitized = re.sub(r"[\x00-\x1f]", "_", sanitized)
# Avoid leading dot for folders (creates hidden folders) # Avoid leading dot for folders (creates hidden folders)
if is_folder and sanitized.startswith('.'): if is_folder and sanitized.startswith("."):
sanitized = '_' + sanitized[1:] sanitized = "_" + sanitized[1:]
else: # Linux and others else: # Linux and others
# Linux illegal characters: / and null character # Linux illegal characters: / and null character
# Though / is illegal, most other chars are technically allowed # Though / is illegal, most other chars are technically allowed
sanitized = re.sub(r'[/\x00]', '_', name) sanitized = re.sub(r"[/\x00]", "_", name)
# Remove other control characters for safety # Remove other control characters for safety
sanitized = re.sub(r'[\x01-\x1f]', '_', sanitized) sanitized = re.sub(r"[\x01-\x1f]", "_", sanitized)
# Avoid leading dot for folders (creates hidden folders) # Avoid leading dot for folders (creates hidden folders)
if is_folder and sanitized.startswith('.'): if is_folder and sanitized.startswith("."):
sanitized = '_' + sanitized[1:] sanitized = "_" + sanitized[1:]
# Ensure the name is not empty after sanitization # Ensure the name is not empty after sanitization
if not sanitized or sanitized.strip() == '': if not sanitized or sanitized.strip() == "":
sanitized = "audiobook" sanitized = "audiobook"
# Limit length to 255 characters (common limit across filesystems) # Limit length to 255 characters (common limit across filesystems)
if len(sanitized) > 255: if len(sanitized) > 255:
sanitized = sanitized[:255].rstrip('. ') sanitized = sanitized[:255].rstrip(". ")
return sanitized return sanitized
@@ -310,7 +317,11 @@ class ConversionThread(QThread):
# Normalize paths for consistent display (fixes Windows path separator issues) # Normalize paths for consistent display (fixes Windows path separator issues)
input_file = os.path.normpath(input_file) if input_file else input_file input_file = os.path.normpath(input_file) if input_file else input_file
processing_file = os.path.normpath(processing_file) if processing_file else processing_file processing_file = (
os.path.normpath(processing_file)
if processing_file
else processing_file
)
self.log_updated.emit(f"- Input File: {input_file}") self.log_updated.emit(f"- Input File: {input_file}")
if input_file != processing_file: if input_file != processing_file:
@@ -318,7 +329,9 @@ class ConversionThread(QThread):
# Use file_name for logs if from_queue, otherwise use display_path if available # Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False): if getattr(self, "from_queue", False):
base_path = self.save_base_path or self.file_name # Use save_base_path if available base_path = (
self.save_base_path or self.file_name
) # Use save_base_path if available
else: else:
base_path = self.display_path if self.display_path else self.file_name base_path = self.display_path if self.display_path else self.file_name
@@ -476,14 +489,16 @@ class ConversionThread(QThread):
# Use file_name for logs if from_queue, otherwise use display_path if available # Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False): if getattr(self, "from_queue", False):
base_path = self.save_base_path or self.file_name # Use save_base_path if available base_path = (
self.save_base_path or self.file_name
) # Use save_base_path if available
else: else:
base_path = self.display_path if self.display_path else self.file_name base_path = self.display_path if self.display_path else self.file_name
base_name = os.path.splitext(os.path.basename(base_path))[0] base_name = os.path.splitext(os.path.basename(base_path))[0]
# Sanitize base_name for folder/file creation based on OS # Sanitize base_name for folder/file creation based on OS
sanitized_base_name = sanitize_name_for_os(base_name, is_folder=True) sanitized_base_name = sanitize_name_for_os(base_name, is_folder=True)
if self.save_option == "Save to Desktop": if self.save_option == "Save to Desktop":
parent_dir = user_desktop_dir() parent_dir = user_desktop_dir()
elif self.save_option == "Save next to input file": elif self.save_option == "Save next to input file":
@@ -528,7 +543,9 @@ class ConversionThread(QThread):
# Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True # Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True
if merge_chapters_at_end: if merge_chapters_at_end:
out_dir = parent_dir out_dir = parent_dir
base_filepath_no_ext = os.path.join(out_dir, f"{sanitized_base_name}{suffix}") base_filepath_no_ext = os.path.join(
out_dir, f"{sanitized_base_name}{suffix}"
)
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}" merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
subtitle_entries = [] subtitle_entries = []
current_time = 0.0 current_time = 0.0
@@ -642,8 +659,12 @@ class ConversionThread(QThread):
# Add style definitions for karaoke highlighting # Add style definitions for karaoke highlighting
if self.subtitle_mode == "Sentence + Highlighting": if self.subtitle_mode == "Sentence + Highlighting":
merged_subtitle_file.write("[V4+ Styles]\n") merged_subtitle_file.write("[V4+ Styles]\n")
merged_subtitle_file.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n") merged_subtitle_file.write(
merged_subtitle_file.write("Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n") "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
)
merged_subtitle_file.write(
"Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n"
)
merged_subtitle_file.write("[Events]\n") merged_subtitle_file.write("[Events]\n")
merged_subtitle_file.write( merged_subtitle_file.write(
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n" "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
@@ -806,8 +827,12 @@ class ConversionThread(QThread):
# Add style definitions for karaoke highlighting # Add style definitions for karaoke highlighting
if self.subtitle_mode == "Sentence + Highlighting": if self.subtitle_mode == "Sentence + Highlighting":
chapter_subtitle_file.write("[V4+ Styles]\n") chapter_subtitle_file.write("[V4+ Styles]\n")
chapter_subtitle_file.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n") chapter_subtitle_file.write(
chapter_subtitle_file.write("Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n") "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
)
chapter_subtitle_file.write(
"Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n"
)
chapter_subtitle_file.write("[Events]\n") chapter_subtitle_file.write("[Events]\n")
chapter_subtitle_file.write( chapter_subtitle_file.write(
@@ -922,7 +947,12 @@ class ConversionThread(QThread):
start_time = self._ass_time(start) start_time = self._ass_time(start)
end_time = self._ass_time(end) end_time = self._ass_time(end)
# Use karaoke effect for highlighting mode # Use karaoke effect for highlighting mode
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else "" effect = (
"karaoke"
if self.subtitle_mode
== "Sentence + Highlighting"
else ""
)
merged_subtitle_file.write( merged_subtitle_file.write(
f"Dialogue: 0,{start_time},{end_time},Default,,{merged_subtitle_margin},{merged_subtitle_margin},0,{effect},{merged_subtitle_alignment_tag}{text}\n" f"Dialogue: 0,{start_time},{end_time},Default,,{merged_subtitle_margin},{merged_subtitle_margin},0,{effect},{merged_subtitle_alignment_tag}{text}\n"
) )
@@ -951,7 +981,12 @@ class ConversionThread(QThread):
start_time = self._ass_time(start) start_time = self._ass_time(start)
end_time = self._ass_time(end) end_time = self._ass_time(end)
# Use karaoke effect for highlighting mode # Use karaoke effect for highlighting mode
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else "" effect = (
"karaoke"
if self.subtitle_mode
== "Sentence + Highlighting"
else ""
)
chapter_subtitle_file.write( chapter_subtitle_file.write(
f"Dialogue: 0,{start_time},{end_time},Default,,{chapter_subtitle_margin},{chapter_subtitle_margin},0,{effect},{chapter_subtitle_alignment_tag}{text}\n" f"Dialogue: 0,{start_time},{end_time},Default,,{chapter_subtitle_margin},{chapter_subtitle_margin},0,{effect},{chapter_subtitle_alignment_tag}{text}\n"
) )
@@ -997,15 +1032,17 @@ class ConversionThread(QThread):
# Add silence between chapters for merged output (except after the last chapter) # Add silence between chapters for merged output (except after the last chapter)
if merge_chapters_at_end and chapter_idx < total_chapters: if merge_chapters_at_end and chapter_idx < total_chapters:
silence_samples = int(self.silence_duration * 24000) # Silence duration at 24,000 Hz silence_samples = int(
self.silence_duration * 24000
) # Silence duration at 24,000 Hz
silence_audio = self.np.zeros(silence_samples, dtype="float32") silence_audio = self.np.zeros(silence_samples, dtype="float32")
silence_bytes = silence_audio.tobytes() silence_bytes = silence_audio.tobytes()
if merged_out_file: if merged_out_file:
merged_out_file.write(silence_audio) merged_out_file.write(silence_audio)
elif ffmpeg_proc: elif ffmpeg_proc:
ffmpeg_proc.stdin.write(silence_bytes) ffmpeg_proc.stdin.write(silence_bytes)
# Update timing for the silence # Update timing for the silence
current_time += self.silence_duration current_time += self.silence_duration
if chapter_out_file or chapter_ffmpeg_proc: if chapter_out_file or chapter_ffmpeg_proc:
@@ -1270,7 +1307,7 @@ class ConversionThread(QThread):
"""Helper function to process subtitle tokens according to the subtitle mode""" """Helper function to process subtitle tokens according to the subtitle mode"""
if not tokens_with_timestamps: if not tokens_with_timestamps:
return return
processed_tokens = tokens_with_timestamps # Use tokens directly processed_tokens = tokens_with_timestamps # Use tokens directly
# Use processed_tokens instead of tokens_with_timestamps for the rest of the method # Use processed_tokens instead of tokens_with_timestamps for the rest of the method
@@ -1297,7 +1334,11 @@ class ConversionThread(QThread):
karaoke_text = "" karaoke_text = ""
for t in current_sentence: for t in current_sentence:
# Calculate duration in centiseconds # Calculate duration in centiseconds
duration = t["end"] - t["start"] if t["end"] and t["start"] else 0.5 duration = (
t["end"] - t["start"]
if t["end"] and t["start"]
else 0.5
)
duration_cs = int(duration * 100) duration_cs = int(duration * 100)
# Add karaoke effect - relies on style's SecondaryColour for highlighting # Add karaoke effect - relies on style's SecondaryColour for highlighting
karaoke_text += f"{{\\kf{duration_cs}}}{t['text']}{t.get('whitespace', '') or ''}" karaoke_text += f"{{\\kf{duration_cs}}}{t['text']}{t.get('whitespace', '') or ''}"
@@ -1394,22 +1435,35 @@ class ConversionThread(QThread):
for token in processed_tokens: for token in processed_tokens:
current_group.append(token) current_group.append(token)
# Count spaces after tokens (in the whitespace field) # Count spaces after tokens (in the whitespace field)
if token.get("whitespace", "") == " ": if token.get("whitespace", "") == " ":
space_count += 1 space_count += 1
# Split after counting N spaces # Split after counting N spaces
if space_count >= word_count: if space_count >= word_count:
text = "".join(t["text"] + (t.get("whitespace", "") or "") for t in current_group) text = "".join(
subtitle_entries.append((current_group[0]["start"], current_group[-1]["end"], text.strip())) t["text"] + (t.get("whitespace", "") or "")
for t in current_group
)
subtitle_entries.append(
(
current_group[0]["start"],
current_group[-1]["end"],
text.strip(),
)
)
current_group = [] current_group = []
space_count = 0 space_count = 0
# Add any remaining tokens # Add any remaining tokens
if current_group: if current_group:
text = "".join(t["text"] + (t.get("whitespace", "") or "") for t in current_group) text = "".join(
subtitle_entries.append((current_group[0]["start"], current_group[-1]["end"], text.strip())) t["text"] + (t.get("whitespace", "") or "") for t in current_group
)
subtitle_entries.append(
(current_group[0]["start"], current_group[-1]["end"], text.strip())
)
# Fallback for last entry # Fallback for last entry
if subtitle_entries and fallback_end_time is not None: if subtitle_entries and fallback_end_time is not None:
@@ -1418,7 +1472,6 @@ class ConversionThread(QThread):
if end is None or end <= start or end <= 0: if end is None or end <= start or end <= 0:
subtitle_entries[-1] = (start, fallback_end_time, text) subtitle_entries[-1] = (start, fallback_end_time, text)
def cancel(self): def cancel(self):
self.cancel_requested = True self.cancel_requested = True
self.should_cancel = True self.should_cancel = True
+179 -112
View File
@@ -270,9 +270,9 @@ class InputBox(QLabel):
if cache is not None: if cache is not None:
cached_char_count = cache.get(file_path) cached_char_count = cache.get(file_path)
if ( if (
cached_char_count is None cached_char_count is None
and char_source_path and char_source_path
and char_source_path != file_path and char_source_path != file_path
): ):
cached_char_count = cache.get(char_source_path) cached_char_count = cache.get(char_source_path)
@@ -280,7 +280,9 @@ class InputBox(QLabel):
char_count = cached_char_count char_count = cached_char_count
elif char_source_path: elif char_source_path:
try: try:
with open(char_source_path, "r", encoding="utf-8", errors="ignore") as f: with open(
char_source_path, "r", encoding="utf-8", errors="ignore"
) as f:
text = f.read() text = f.read()
cleaned_text = clean_text(text) cleaned_text = clean_text(text)
char_count = calculate_text_length(cleaned_text) char_count = calculate_text_length(cleaned_text)
@@ -328,8 +330,9 @@ class InputBox(QLabel):
# For epub/pdf files, show edit if we have a selected_file (temp txt) # For epub/pdf files, show edit if we have a selected_file (temp txt)
if ( if (
window.selected_file_type in ["epub", "pdf", "md", "markdown", "md", "markdown"] window.selected_file_type
and window.selected_file in ["epub", "pdf", "md", "markdown", "md", "markdown"]
and window.selected_file
): ):
should_show_edit = True should_show_edit = True
@@ -401,10 +404,10 @@ class InputBox(QLabel):
if urls: if urls:
ext = urls[0].toLocalFile().lower() ext = urls[0].toLocalFile().lower()
if ( if (
ext.endswith(".txt") ext.endswith(".txt")
or ext.endswith(".epub") or ext.endswith(".epub")
or ext.endswith(".pdf") or ext.endswith(".pdf")
or ext.endswith((".md", ".markdown")) or ext.endswith((".md", ".markdown"))
): ):
event.acceptProposedAction() event.acceptProposedAction()
# Set hover style based on current state # Set hover style based on current state
@@ -454,9 +457,11 @@ class InputBox(QLabel):
) )
self.set_file_info(file_path) self.set_file_info(file_path)
event.acceptProposedAction() event.acceptProposedAction()
elif (file_path.lower().endswith(".epub") elif (
or file_path.lower().endswith(".pdf") file_path.lower().endswith(".epub")
or file_path.lower().endswith((".md", ".markdown"))): or file_path.lower().endswith(".pdf")
or file_path.lower().endswith((".md", ".markdown"))
):
# Determine file type # Determine file type
if file_path.lower().endswith(".epub"): if file_path.lower().endswith(".epub"):
file_type = "epub" file_type = "epub"
@@ -480,7 +485,10 @@ class InputBox(QLabel):
def on_chapters_clicked(self): def on_chapters_clicked(self):
win = self.window() win = self.window()
if win.selected_file_type in ["epub", "pdf", "md", "markdown"] and win.selected_book_path: if (
win.selected_file_type in ["epub", "pdf", "md", "markdown"]
and win.selected_book_path
):
# Call open_book_file which shows the dialog and updates selected_chapters # Call open_book_file which shows the dialog and updates selected_chapters
if win.open_book_file(win.selected_book_path): if win.open_book_file(win.selected_book_path):
# Refresh the info label and button text after dialog closes # Refresh the info label and button text after dialog closes
@@ -492,7 +500,10 @@ class InputBox(QLabel):
def on_edit_clicked(self): def on_edit_clicked(self):
win = self.window() win = self.window()
# For PDFs and EPUBs, use the temporary text file # For PDFs and EPUBs, use the temporary text file
if win.selected_file_type in ["epub", "pdf", "md", "markdown"] and win.selected_file: if (
win.selected_file_type in ["epub", "pdf", "md", "markdown"]
and win.selected_file
):
# Use the temporary .txt file that was generated # Use the temporary .txt file that was generated
win.open_textbox_dialog(win.selected_file) win.open_textbox_dialog(win.selected_file)
else: else:
@@ -514,15 +525,22 @@ class InputBox(QLabel):
is_cached_doc = False is_cached_doc = False
if ( if (
file_to_check file_to_check
and os.path.exists(file_to_check) and os.path.exists(file_to_check)
and os.path.isfile(file_to_check) and os.path.isfile(file_to_check)
and cache_dir and cache_dir
): ):
# Consider it cached when the file is under the cache directory and is a .txt # Consider it cached when the file is under the cache directory and is a .txt
if file_to_check.endswith('.txt') and os.path.commonpath([os.path.abspath(file_to_check), os.path.abspath(cache_dir)]) == os.path.abspath(cache_dir): if file_to_check.endswith(".txt") and os.path.commonpath(
[os.path.abspath(file_to_check), os.path.abspath(cache_dir)]
) == os.path.abspath(cache_dir):
# Only treat as document-cache when original type was a document # Only treat as document-cache when original type was a document
if getattr(win, 'selected_file_type', None) in ['epub', 'pdf', 'md', 'markdown']: if getattr(win, "selected_file_type", None) in [
"epub",
"pdf",
"md",
"markdown",
]:
is_cached_doc = True is_cached_doc = True
if is_cached_doc: if is_cached_doc:
@@ -538,8 +556,11 @@ class InputBox(QLabel):
act_input = QAction("Go to input file", self) act_input = QAction("Go to input file", self)
# Prefer displayed_file_path (original input path) then selected_book_path # Prefer displayed_file_path (original input path) then selected_book_path
input_path = getattr(win, 'displayed_file_path', None) or getattr(win, 'selected_book_path', None) input_path = getattr(win, "displayed_file_path", None) or getattr(
win, "selected_book_path", None
)
if input_path and os.path.exists(input_path): if input_path and os.path.exists(input_path):
def open_input(): def open_input():
folder_path = os.path.dirname(input_path) folder_path = os.path.dirname(input_path)
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
@@ -550,12 +571,16 @@ 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
and os.path.exists(file_to_check) and os.path.exists(file_to_check)
and os.path.isfile(file_to_check) and os.path.isfile(file_to_check)
): ):
folder_path = os.path.dirname(file_to_check) folder_path = os.path.dirname(file_to_check)
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
@@ -568,7 +593,9 @@ class TextboxDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Enter Text") self.setWindowTitle("Enter Text")
self.setWindowFlags( self.setWindowFlags(
Qt.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMaximizeButtonHint Qt.WindowType.Window
| Qt.WindowType.WindowCloseButtonHint
| Qt.WindowType.WindowMaximizeButtonHint
) )
self.resize(700, 500) self.resize(700, 500)
@@ -648,7 +675,9 @@ class TextboxDialog(QDialog):
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.StandardButton.Yes | QMessageBox.StandardButton.No) msg_box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msg_box.setDefaultButton(QMessageBox.StandardButton.No) msg_box.setDefaultButton(QMessageBox.StandardButton.No)
if msg_box.exec() != QMessageBox.StandardButton.Yes: if msg_box.exec() != QMessageBox.StandardButton.Yes:
@@ -674,8 +703,8 @@ class TextboxDialog(QDialog):
# This gives a better filename instead of the cache file path # This gives a better filename instead of the cache file path
main_window = self.parent() main_window = self.parent()
if ( if (
hasattr(main_window, "displayed_file_path") hasattr(main_window, "displayed_file_path")
and main_window.displayed_file_path and main_window.displayed_file_path
): ):
if main_window.selected_file_type in ["epub", "pdf", "md", "markdown"]: if main_window.selected_file_type in ["epub", "pdf", "md", "markdown"]:
# Use the base name of the displayed file but change extension to .txt # Use the base name of the displayed file but change extension to .txt
@@ -918,7 +947,9 @@ 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.Policy.Expanding, QSizePolicy.Policy.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)
@@ -984,14 +1015,20 @@ class abogen(QWidget):
for lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION for lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
) )
) )
subtitle_options = ["Disabled", "Line", "Sentence", "Sentence + Comma", "Sentence + Highlighting"] + [ subtitle_options = [
f"{i} word" if i == 1 else f"{i} words" for i in range(1, 11) "Disabled",
] "Line",
"Sentence",
"Sentence + Comma",
"Sentence + Highlighting",
] + [f"{i} word" if i == 1 else f"{i} words" for i in range(1, 11)]
self.subtitle_combo.addItems(subtitle_options) self.subtitle_combo.addItems(subtitle_options)
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.Policy.Expanding, QSizePolicy.Policy.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)
@@ -1009,7 +1046,9 @@ 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.Policy.Expanding, QSizePolicy.Policy.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"),
@@ -1055,7 +1094,10 @@ class abogen(QWidget):
# If subtitle mode requires highlighting, SRT is not supported. Disable SRT item # If subtitle mode requires highlighting, SRT is not supported. Disable SRT item
# and auto-switch to a compatible ASS format if SRT is currently selected. # and auto-switch to a compatible ASS format if SRT is currently selected.
try: try:
if hasattr(self, "subtitle_mode") and self.subtitle_mode == "Sentence + Highlighting": if (
hasattr(self, "subtitle_mode")
and self.subtitle_mode == "Sentence + Highlighting"
):
idx_srt = self.subtitle_format_combo.findData("srt") idx_srt = self.subtitle_format_combo.findData("srt")
if idx_srt >= 0: if idx_srt >= 0:
item = self.subtitle_format_combo.model().item(idx_srt) item = self.subtitle_format_combo.model().item(idx_srt)
@@ -1067,7 +1109,9 @@ class abogen(QWidget):
if new_idx >= 0: if new_idx >= 0:
self.subtitle_format_combo.setCurrentIndex(new_idx) self.subtitle_format_combo.setCurrentIndex(new_idx)
# Persist the change # Persist the change
self.set_subtitle_format(self.subtitle_format_combo.itemData(new_idx)) self.set_subtitle_format(
self.subtitle_format_combo.itemData(new_idx)
)
except Exception: except Exception:
# Fail-safe: don't crash UI if model manipulation isn't supported on some platforms # Fail-safe: don't crash UI if model manipulation isn't supported on some platforms
pass pass
@@ -1114,7 +1158,9 @@ 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.Policy.Expanding, QSizePolicy.Policy.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)
@@ -1129,7 +1175,9 @@ 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.Policy.Expanding, QSizePolicy.Policy.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)
@@ -1175,7 +1223,9 @@ class abogen(QWidget):
# Add controls to a container widget # 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.Policy.Preferred, QSizePolicy.Policy.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)
@@ -1241,9 +1291,11 @@ class abogen(QWidget):
) )
if not file_path: if not file_path:
return return
if (file_path.lower().endswith(".epub") if (
or file_path.lower().endswith(".pdf") file_path.lower().endswith(".epub")
or file_path.lower().endswith((".md", ".markdown"))): or file_path.lower().endswith(".pdf")
or file_path.lower().endswith((".md", ".markdown"))
):
# Determine file type # Determine file type
if file_path.lower().endswith(".epub"): if file_path.lower().endswith(".epub"):
self.selected_file_type = "epub" self.selected_file_type = "epub"
@@ -1270,8 +1322,8 @@ class abogen(QWidget):
def open_book_file(self, book_path): def open_book_file(self, book_path):
# Clear selected chapters if this is a different book than the last one # Clear selected chapters if this is a different book than the last one
if ( if (
not hasattr(self, "last_opened_book_path") not hasattr(self, "last_opened_book_path")
or self.last_opened_book_path != book_path or self.last_opened_book_path != book_path
): ):
self.selected_chapters = set() self.selected_chapters = set()
self.last_opened_book_path = book_path self.last_opened_book_path = book_path
@@ -1279,9 +1331,9 @@ class abogen(QWidget):
# HandlerDialog uses internal caching to avoid reprocessing the same book # HandlerDialog uses internal caching to avoid reprocessing the same book
dialog = HandlerDialog( dialog = HandlerDialog(
book_path, book_path,
file_type=getattr(self, 'selected_file_type', None), file_type=getattr(self, "selected_file_type", None),
checked_chapters=self.selected_chapters, checked_chapters=self.selected_chapters,
parent=self parent=self,
) )
dialog.setWindowModality(Qt.WindowModality.NonModal) dialog.setWindowModality(Qt.WindowModality.NonModal)
dialog.setModal(False) dialog.setModal(False)
@@ -1429,9 +1481,9 @@ class abogen(QWidget):
is_cache_file = get_user_cache_path() in file_path is_cache_file = get_user_cache_path() in file_path
# Otherwise use selected_file if it's a txt file # Otherwise use selected_file if it's a txt file
elif ( elif (
self.selected_file_type == "txt" self.selected_file_type == "txt"
and self.selected_file and self.selected_file
and os.path.exists(self.selected_file) and os.path.exists(self.selected_file)
): ):
editing = True editing = True
edit_file = self.selected_file edit_file = self.selected_file
@@ -1642,9 +1694,9 @@ class abogen(QWidget):
def _get_queue_progress_format(self, value=None): def _get_queue_progress_format(self, value=None):
"""Return the progress bar format string for queue mode.""" """Return the progress bar format string for queue mode."""
if ( if (
hasattr(self, "queued_items") hasattr(self, "queued_items")
and self.queued_items and self.queued_items
and hasattr(self, "current_queue_index") and hasattr(self, "current_queue_index")
): ):
N = self.current_queue_index + 1 N = self.current_queue_index + 1
M = len(self.queued_items) M = len(self.queued_items)
@@ -1661,9 +1713,9 @@ class abogen(QWidget):
self.progress_bar.setValue(value) self.progress_bar.setValue(value)
# Show queue progress if in queue mode # Show queue progress if in queue mode
if ( if (
hasattr(self, "queued_items") hasattr(self, "queued_items")
and self.queued_items and self.queued_items
and hasattr(self, "current_queue_index") and hasattr(self, "current_queue_index")
): ):
N = self.current_queue_index + 1 N = self.current_queue_index + 1
M = len(self.queued_items) M = len(self.queued_items)
@@ -1724,7 +1776,9 @@ class abogen(QWidget):
if self.selected_file_type in ["epub", "pdf", "md", "markdown"]: if self.selected_file_type in ["epub", "pdf", "md", "markdown"]:
file_to_queue = self.selected_file file_to_queue = self.selected_file
# Use the original file path for save location # Use the original file path for save location
save_base_path = self.displayed_file_path if self.displayed_file_path else file_to_queue save_base_path = (
self.displayed_file_path if self.displayed_file_path else file_to_queue
)
else: else:
file_to_queue = ( file_to_queue = (
self.displayed_file_path self.displayed_file_path
@@ -1732,7 +1786,7 @@ class abogen(QWidget):
else self.selected_file else self.selected_file
) )
save_base_path = file_to_queue # For non-EPUB, it's the same save_base_path = file_to_queue # For non-EPUB, it's the same
if not file_to_queue: if not file_to_queue:
self.input_box.set_error("Please add a file.") self.input_box.set_error("Please add a file.")
return return
@@ -1759,22 +1813,22 @@ class abogen(QWidget):
# Prevent adding duplicate items to the queue # Prevent adding duplicate items to the queue
for queued_item in self.queued_items: for queued_item in self.queued_items:
if ( if (
queued_item.file_name == item_queue.file_name queued_item.file_name == item_queue.file_name
and queued_item.lang_code == item_queue.lang_code and queued_item.lang_code == item_queue.lang_code
and queued_item.speed == item_queue.speed and queued_item.speed == item_queue.speed
and queued_item.voice == item_queue.voice and queued_item.voice == item_queue.voice
and queued_item.save_option == item_queue.save_option and queued_item.save_option == item_queue.save_option
and queued_item.output_folder == item_queue.output_folder and queued_item.output_folder == item_queue.output_folder
and queued_item.subtitle_mode == item_queue.subtitle_mode and queued_item.subtitle_mode == item_queue.subtitle_mode
and queued_item.output_format == item_queue.output_format and queued_item.output_format == item_queue.output_format
and getattr(queued_item, "replace_single_newlines", False) and getattr(queued_item, "replace_single_newlines", False)
== 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) and getattr(queued_item, "save_chapters_separately", None)
== item_queue.save_chapters_separately == item_queue.save_chapters_separately
and getattr(queued_item, "merge_chapters_at_end", None) and getattr(queued_item, "merge_chapters_at_end", None)
== item_queue.merge_chapters_at_end == 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."
@@ -1835,7 +1889,9 @@ class abogen(QWidget):
queued_item, "replace_single_newlines", False queued_item, "replace_single_newlines", False
) )
# Restore the original file path for save location # Restore the original file path for save location
self.displayed_file_path = queued_item.save_base_path or queued_item.file_name self.displayed_file_path = (
queued_item.save_base_path or queued_item.file_name
)
self.start_conversion(from_queue=True) self.start_conversion(from_queue=True)
else: else:
# Queue finished, reset index # Queue finished, reset index
@@ -1897,10 +1953,10 @@ class abogen(QWidget):
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
# Show queue progress if in queue mode # Show queue progress if in queue mode
if ( if (
from_queue from_queue
and hasattr(self, "queued_items") and hasattr(self, "queued_items")
and self.queued_items and self.queued_items
and hasattr(self, "current_queue_index") and hasattr(self, "current_queue_index")
): ):
N = self.current_queue_index + 1 N = self.current_queue_index + 1
M = len(self.queued_items) M = len(self.queued_items)
@@ -1987,7 +2043,7 @@ class abogen(QWidget):
) )
# Pass chapter count for EPUB or PDF files # Pass chapter count for EPUB or PDF files
if self.selected_file_type in ["epub", "pdf", "md", "markdown"] and hasattr( if self.selected_file_type in ["epub", "pdf", "md", "markdown"] and hasattr(
self, "selected_chapters" self, "selected_chapters"
): ):
self.conversion_thread.chapter_count = len(self.selected_chapters) self.conversion_thread.chapter_count = len(self.selected_chapters)
# Pass save_chapters_separately flag if available # Pass save_chapters_separately flag if available
@@ -2130,8 +2186,8 @@ class abogen(QWidget):
# Only show finish_widget if queue is done # Only show finish_widget if queue is done
if ( if (
self.current_queue_index + 1 >= len(self.queued_items) self.current_queue_index + 1 >= len(self.queued_items)
or not self.queued_items or not self.queued_items
): ):
# Queue finished, show finish screen # Queue finished, show finish screen
self.controls_widget.hide() self.controls_widget.hide()
@@ -2496,14 +2552,14 @@ class abogen(QWidget):
def cleanup(): def cleanup():
# Only remove if not from cache AND it's a temp file from VoicePreviewThread # Only remove if not from cache AND it's a temp file from VoicePreviewThread
if ( if (
not from_cache not from_cache
and hasattr(self, "preview_thread") and hasattr(self, "preview_thread")
and hasattr(self.preview_thread, "temp_wav") and hasattr(self.preview_thread, "temp_wav")
and self.preview_thread.temp_wav == temp_wav and self.preview_thread.temp_wav == temp_wav
): ):
try: try:
if os.path.exists( if os.path.exists(
temp_wav temp_wav
): # Ensure it exists before trying to remove ): # Ensure it exists before trying to remove
os.remove(temp_wav) os.remove(temp_wav)
except Exception: except Exception:
@@ -2571,14 +2627,16 @@ class abogen(QWidget):
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.StandardButton.Yes | QMessageBox.StandardButton.No) box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
box.setDefaultButton(QMessageBox.StandardButton.No) box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec() != QMessageBox.StandardButton.Yes: if box.exec() != QMessageBox.StandardButton.Yes:
return return
try: try:
if ( if (
hasattr(self, "conversion_thread") hasattr(self, "conversion_thread")
and self.conversion_thread.isRunning() and self.conversion_thread.isRunning()
): ):
if not hasattr(self, "_conversion_lock"): if not hasattr(self, "_conversion_lock"):
self._conversion_lock = threading.Lock() self._conversion_lock = threading.Lock()
@@ -2641,7 +2699,9 @@ class abogen(QWidget):
new_idx = self.subtitle_format_combo.findData("ass_centered_narrow") new_idx = self.subtitle_format_combo.findData("ass_centered_narrow")
if new_idx >= 0: if new_idx >= 0:
self.subtitle_format_combo.setCurrentIndex(new_idx) self.subtitle_format_combo.setCurrentIndex(new_idx)
self.set_subtitle_format(self.subtitle_format_combo.itemData(new_idx)) self.set_subtitle_format(
self.subtitle_format_combo.itemData(new_idx)
)
else: else:
# Re-enable SRT option when not in highlighting mode # Re-enable SRT option when not in highlighting mode
if idx_srt >= 0: if idx_srt >= 0:
@@ -2665,9 +2725,9 @@ class abogen(QWidget):
def cleanup_conversion_thread(self): def cleanup_conversion_thread(self):
# Stop conversion thread # Stop conversion thread
if ( if (
hasattr(self, "conversion_thread") hasattr(self, "conversion_thread")
and self.conversion_thread is not None and self.conversion_thread is not None
and self.conversion_thread.isRunning() and self.conversion_thread.isRunning()
): ):
self.conversion_thread.cancel() self.conversion_thread.cancel()
self.conversion_thread.wait() self.conversion_thread.wait()
@@ -2680,7 +2740,9 @@ class abogen(QWidget):
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.StandardButton.Yes | QMessageBox.StandardButton.No) box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
box.setDefaultButton(QMessageBox.StandardButton.No) box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec() == QMessageBox.StandardButton.Yes: if box.exec() == QMessageBox.StandardButton.Yes:
self.cleanup_conversion_thread() self.cleanup_conversion_thread()
@@ -2702,8 +2764,8 @@ class abogen(QWidget):
if dialog.exec() == QDialog.DialogCode.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")
and self.conversion_thread.isRunning() and self.conversion_thread.isRunning()
): ):
self.conversion_thread.set_chapter_options(options) self.conversion_thread.set_chapter_options(options)
else: else:
@@ -2712,7 +2774,6 @@ class abogen(QWidget):
def apply_theme(self, theme): def apply_theme(self, theme):
app = QApplication.instance() app = QApplication.instance()
is_windows = platform.system() == "Windows" is_windows = platform.system() == "Windows"
available_styles = [s.lower() for s in QStyleFactory.keys()] available_styles = [s.lower() for s in QStyleFactory.keys()]
@@ -2722,8 +2783,8 @@ class abogen(QWidget):
import winreg import winreg
with winreg.OpenKey( with winreg.OpenKey(
winreg.HKEY_CURRENT_USER, winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
) as key: ) as key:
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme") value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
return value == 0 return value == 0
@@ -2819,7 +2880,7 @@ class abogen(QWidget):
# Main logic # Main logic
dark_mode = theme == "dark" or ( dark_mode = theme == "dark" or (
theme == "system" and is_windows and is_windows_dark_mode() theme == "system" and is_windows and is_windows_dark_mode()
) )
if dark_mode: if dark_mode:
app.setStyle("Fusion") app.setStyle("Fusion")
@@ -2855,7 +2916,7 @@ class abogen(QWidget):
def get_dark_mode(): def get_dark_mode():
return theme == "dark" or ( return theme == "dark" or (
theme == "system" and is_windows and is_windows_dark_mode() theme == "system" and is_windows and is_windows_dark_mode()
) )
app._dark_titlebar_event_filter = DarkTitleBarEventFilter( app._dark_titlebar_event_filter = DarkTitleBarEventFilter(
@@ -3305,7 +3366,9 @@ 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.WindowType.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)
@@ -3384,7 +3447,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.StandardButton.Yes | QMessageBox.StandardButton.No) msg_box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msg_box.setDefaultButton(QMessageBox.StandardButton.Yes) msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
if msg_box.exec() == QMessageBox.StandardButton.Yes: if msg_box.exec() == QMessageBox.StandardButton.Yes:
try: try:
@@ -3394,8 +3459,8 @@ Categories=AudioVideo;Audio;Utility;
# Reset flag to track if we should show "no updates" message # Reset flag to track if we should show "no updates" message
show_result = ( show_result = (
hasattr(self, "_show_update_check_result") hasattr(self, "_show_update_check_result")
and self._show_update_check_result and self._show_update_check_result
) )
self._show_update_check_result = False self._show_update_check_result = False
@@ -3487,7 +3552,9 @@ Categories=AudioVideo;Audio;Utility;
msg_box.setCheckBox(preview_cache_checkbox) msg_box.setCheckBox(preview_cache_checkbox)
# Add buttons # Add buttons
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) msg_box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msg_box.setDefaultButton(QMessageBox.StandardButton.Yes) msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
if msg_box.exec() != QMessageBox.StandardButton.Yes: if msg_box.exec() != QMessageBox.StandardButton.Yes:
@@ -3522,9 +3589,9 @@ Categories=AudioVideo;Audio;Utility;
# If currently selected file is in the cache directory, clear the UI # If currently selected file is in the cache directory, clear the UI
if ( if (
self.selected_file self.selected_file
and os.path.dirname(self.selected_file) == cache_dir and os.path.dirname(self.selected_file) == cache_dir
and self.selected_file.endswith(".txt") and self.selected_file.endswith(".txt")
): ):
self.input_box.clear_input() self.input_box.clear_input()
@@ -3588,7 +3655,6 @@ 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"""
current_value = self.config.get("silence_duration", 2.0) current_value = self.config.get("silence_duration", 2.0)
dlg = QInputDialog(self) dlg = QInputDialog(self)
@@ -3619,6 +3685,7 @@ Categories=AudioVideo;Audio;Utility;
"Setting Saved", "Setting Saved",
f"Silence duration between chapters set to {value:.1f} seconds.", f"Silence duration between chapters set to {value:.1f} seconds.",
) )
def set_separate_chapters_format(self, fmt): def set_separate_chapters_format(self, fmt):
"""Set the format for separate chapters audio files.""" """Set the format for separate chapters audio files."""
self.separate_chapters_format = fmt self.separate_chapters_format = fmt
+5 -4
View File
@@ -8,16 +8,16 @@ import signal
# Qt platform plugin detection (fixes #59) # Qt platform plugin detection (fixes #59)
try: try:
from PyQt6.QtCore import QLibraryInfo from PyQt6.QtCore import QLibraryInfo
# Get the path to the plugins directory # Get the path to the plugins directory
plugins = QLibraryInfo.path(QLibraryInfo.LibraryPath.PluginsPath) 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)
@@ -31,6 +31,7 @@ if platform.system() == "Windows":
try: try:
from abogen.constants import PROGRAM_NAME, VERSION from abogen.constants import PROGRAM_NAME, VERSION
import ctypes import ctypes
app_id = f"{PROGRAM_NAME}.{VERSION}" app_id = f"{PROGRAM_NAME}.{VERSION}"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
except Exception as e: except Exception as e:
+134 -55
View File
@@ -36,7 +36,9 @@ 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.TextElideMode.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)
@@ -55,8 +57,12 @@ 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.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) char_label.setAlignment(
char_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
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)
@@ -74,7 +80,9 @@ class DroppableQueueListWidget(QListWidget):
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.WidgetAttribute.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():
@@ -134,7 +142,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(QAbstractItemView.SelectionMode.ExtendedSelection) self.listwidget.setSelectionMode(
QAbstractItemView.SelectionMode.ExtendedSelection
)
self.listwidget.setAlternatingRowColors(True) self.listwidget.setAlternatingRowColors(True)
self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.listwidget.customContextMenuRequested.connect(self.show_context_menu) self.listwidget.customContextMenuRequested.connect(self.show_context_menu)
@@ -161,7 +171,9 @@ class QueueManager(QDialog):
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.WidgetAttribute.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()
@@ -194,7 +206,10 @@ 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.StandardButton.Ok | QDialogButtonBox.StandardButton.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)
@@ -210,7 +225,7 @@ class QueueManager(QDialog):
def process_queue(self): def process_queue(self):
"""Process the queue items.""" """Process the queue items."""
import os import os
self.listwidget.clear() self.listwidget.clear()
if not self.queue: if not self.queue:
self.empty_overlay.show() self.empty_overlay.show()
@@ -223,11 +238,19 @@ class QueueManager(QDialog):
# Determine display file path (prefer save_base_path for original file) # Determine display file path (prefer save_base_path for original file)
display_file_path = getattr(item, "save_base_path", None) or item.file_name display_file_path = getattr(item, "save_base_path", None) or item.file_name
processing_file_path = item.file_name processing_file_path = item.file_name
# Normalize paths for consistent display (fixes Windows path separator issues) # Normalize paths for consistent display (fixes Windows path separator issues)
display_file_path = os.path.normpath(display_file_path) if display_file_path else display_file_path display_file_path = (
processing_file_path = os.path.normpath(processing_file_path) if processing_file_path else processing_file_path os.path.normpath(display_file_path)
if display_file_path
else display_file_path
)
processing_file_path = (
os.path.normpath(processing_file_path)
if processing_file_path
else processing_file_path
)
# Only show the file name, not the full path # Only show the file name, not the full path
display_name = display_file_path display_name = display_file_path
@@ -241,13 +264,19 @@ class QueueManager(QDialog):
# For plain .txt inputs we don't need to show a separate processing file # For plain .txt inputs we don't need to show a separate processing file
show_processing = True show_processing = True
try: try:
if isinstance(display_file_path, str) and display_file_path.lower().endswith('.txt'): if isinstance(
display_file_path, str
) and display_file_path.lower().endswith(".txt"):
show_processing = False show_processing = False
except Exception: except Exception:
show_processing = True show_processing = True
tooltip = f"<b>Input File:</b> {display_file_path}<br>" tooltip = f"<b>Input File:</b> {display_file_path}<br>"
if show_processing and processing_file_path and processing_file_path != display_file_path: if (
show_processing
and processing_file_path
and processing_file_path != display_file_path
):
tooltip += f"<b>Processing File:</b> {processing_file_path}<br>" tooltip += f"<b>Processing File:</b> {processing_file_path}<br>"
tooltip += ( tooltip += (
f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>" f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>"
@@ -264,8 +293,8 @@ class QueueManager(QDialog):
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 # Add book handler options if present
save_chapters_separately = getattr(item, 'save_chapters_separately', None) save_chapters_separately = getattr(item, "save_chapters_separately", None)
merge_chapters_at_end = getattr(item, 'merge_chapters_at_end', None) merge_chapters_at_end = getattr(item, "merge_chapters_at_end", None)
if save_chapters_separately is not None: if save_chapters_separately is not None:
tooltip += f"<br><b>Save chapters separately:</b> {'Yes' if save_chapters_separately else 'No'}" tooltip += f"<br><b>Save chapters separately:</b> {'Yes' if save_chapters_separately else 'No'}"
# Only show merge option if saving chapters separately # Only show merge option if saving chapters separately
@@ -274,10 +303,13 @@ class QueueManager(QDialog):
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.ItemDataRole.UserRole, { list_item.setData(
'display_path': display_file_path, Qt.ItemDataRole.UserRole,
'processing_path': processing_file_path {
}) "display_path": display_file_path,
"processing_path": processing_file_path,
},
)
# Use custom widget for display # Use custom widget for display
char_count = getattr(item, "total_char_count", 0) char_count = getattr(item, "total_char_count", 0)
widget = QueueListItemWidget(display_file_path, char_count) widget = QueueListItemWidget(display_file_path, char_count)
@@ -412,7 +444,9 @@ class QueueManager(QDialog):
item = QueueItem() item = QueueItem()
item.file_name = file_path item.file_name = file_path
item.save_base_path = file_path # For .txt files, processing and save paths are the same item.save_base_path = (
file_path # For .txt files, processing and save paths are the same
)
for attr, value in current_attrs.items(): for attr, value in current_attrs.items():
setattr(item, attr, value) setattr(item, attr, value)
# Read file content and calculate total_char_count using calculate_text_length # Read file content and calculate total_char_count using calculate_text_length
@@ -517,66 +551,86 @@ class QueueManager(QDialog):
item = selected_items[0] item = selected_items[0]
paths = item.data(Qt.ItemDataRole.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", "")
else: else:
display_path = paths display_path = paths
processing_path = paths processing_path = paths
doc_exts = ('.md', '.markdown', '.pdf', '.epub') doc_exts = (".md", ".markdown", ".pdf", ".epub")
is_document_input = ( is_document_input = (
isinstance(display_path, str) and display_path.lower().endswith(doc_exts) isinstance(display_path, str)
and display_path.lower().endswith(doc_exts)
) or ( ) or (
isinstance(processing_path, str) and processing_path.lower().endswith(doc_exts) isinstance(processing_path, str)
and processing_path.lower().endswith(doc_exts)
) )
# Add Open file action(s) # Add Open file action(s)
def open_file_by_path(path_label: str): def open_file_by_path(path_label: str):
from PyQt6.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
p = display_path if path_label == 'display' else processing_path p = display_path if path_label == "display" else processing_path
if not p: if not p:
QMessageBox.warning(self, "File Not Found", "Path is not available.") QMessageBox.warning(
self, "File Not Found", "Path is not available."
)
return return
# Find the queue item and resolve the target path # Find the queue item and resolve the target path
target_path = None target_path = None
for q in self.queue: for q in self.queue:
if getattr(q, 'save_base_path', None) == display_path or q.file_name == display_path: if (
if path_label == 'display': getattr(q, "save_base_path", None) == display_path
target_path = getattr(q, 'save_base_path', None) or q.file_name or q.file_name == display_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else: else:
target_path = q.file_name target_path = q.file_name
break break
if getattr(q, 'save_base_path', None) == processing_path or q.file_name == processing_path: if (
if path_label == 'display': getattr(q, "save_base_path", None) == processing_path
target_path = getattr(q, 'save_base_path', None) or q.file_name or q.file_name == processing_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else: else:
target_path = q.file_name target_path = q.file_name
break break
# Fallback to the raw path if resolution failed # Fallback to the raw path if resolution failed
if not target_path: if not target_path:
target_path = p target_path = p
if not os.path.exists(target_path): if not os.path.exists(target_path):
QMessageBox.warning(self, "File Not Found", f"The file does not exist.") QMessageBox.warning(
self, "File Not Found", f"The file does not exist."
)
return return
QDesktopServices.openUrl(QUrl.fromLocalFile(target_path)) QDesktopServices.openUrl(QUrl.fromLocalFile(target_path))
if is_document_input: if is_document_input:
# For documents, show two open options # For documents, show two open options
open_processed_action = QAction("Open processed file", self) open_processed_action = QAction("Open processed file", self)
open_processed_action.triggered.connect(lambda: open_file_by_path('processing')) open_processed_action.triggered.connect(
lambda: open_file_by_path("processing")
)
menu.addAction(open_processed_action) menu.addAction(open_processed_action)
open_input_action = QAction("Open input file", self) open_input_action = QAction("Open input file", self)
open_input_action.triggered.connect(lambda: open_file_by_path('display')) open_input_action.triggered.connect(
lambda: open_file_by_path("display")
)
menu.addAction(open_input_action) menu.addAction(open_input_action)
else: else:
# For plain text files, show single open option # For plain text files, show single open option
open_file_action = QAction("Open file", self) open_file_action = QAction("Open file", self)
open_file_action.triggered.connect(lambda: open_file_by_path('display')) open_file_action.triggered.connect(lambda: open_file_by_path("display"))
menu.addAction(open_file_action) menu.addAction(open_file_action)
# Add Go to folder action # Add Go to folder action
@@ -587,23 +641,35 @@ class QueueManager(QDialog):
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'
p = display_path if path_label == 'display' else processing_path p = display_path if path_label == "display" else processing_path
if not p: if not p:
QMessageBox.warning(self, "File Not Found", "Path is not available.") QMessageBox.warning(
self, "File Not Found", "Path is not available."
)
return return
# If the stored path is the display path (original) but the actual file may be # If the stored path is the display path (original) but the actual file may be
# stored on the queue object differently, try to resolve via the queue entry. # stored on the queue object differently, try to resolve via the queue entry.
target_path = None target_path = None
for q in self.queue: for q in self.queue:
if getattr(q, 'save_base_path', None) == display_path or q.file_name == display_path: if (
if path_label == 'display': getattr(q, "save_base_path", None) == display_path
target_path = getattr(q, 'save_base_path', None) or q.file_name or q.file_name == display_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else: else:
target_path = q.file_name target_path = q.file_name
break break
if getattr(q, 'save_base_path', None) == processing_path or q.file_name == processing_path: if (
if path_label == 'display': getattr(q, "save_base_path", None) == processing_path
target_path = getattr(q, 'save_base_path', None) or q.file_name or q.file_name == processing_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else: else:
target_path = q.file_name target_path = q.file_name
break break
@@ -612,7 +678,11 @@ class QueueManager(QDialog):
target_path = p target_path = p
if not os.path.exists(target_path): if not os.path.exists(target_path):
QMessageBox.warning(self, "File Not Found", f"The file does not exist: {target_path}") QMessageBox.warning(
self,
"File Not Found",
f"The file does not exist: {target_path}",
)
return return
folder = os.path.dirname(target_path) folder = os.path.dirname(target_path)
if os.path.exists(folder): if os.path.exists(folder):
@@ -620,11 +690,13 @@ class QueueManager(QDialog):
if is_document_input: if is_document_input:
processed_action = QAction("Go to processed file", self) processed_action = QAction("Go to processed file", self)
processed_action.triggered.connect(lambda: open_folder_for('processing')) processed_action.triggered.connect(
lambda: open_folder_for("processing")
)
menu.addAction(processed_action) menu.addAction(processed_action)
input_action = QAction("Go to input file", self) input_action = QAction("Go to input file", self)
input_action.triggered.connect(lambda: open_folder_for('display')) input_action.triggered.connect(lambda: open_folder_for("display"))
menu.addAction(input_action) menu.addAction(input_action)
else: else:
# Default behavior for non-document inputs: single "Go to folder" action # Default behavior for non-document inputs: single "Go to folder" action
@@ -634,13 +706,20 @@ class QueueManager(QDialog):
item = selected_items[0] item = selected_items[0]
paths = item.data(Qt.ItemDataRole.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:
file_path = paths # Fallback for old format file_path = paths # Fallback for old format
# Find the queue item # Find the queue item
for q in self.queue: for q in self.queue:
if (getattr(q, "save_base_path", None) == file_path or q.file_name == file_path): if (
target_path = getattr(q, "save_base_path", None) or q.file_name getattr(q, "save_base_path", None) == file_path
or q.file_name == file_path
):
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
if not os.path.exists(target_path): if not os.path.exists(target_path):
QMessageBox.warning( QMessageBox.warning(
self, "File Not Found", f"The file does not exist." self, "File Not Found", f"The file does not exist."
+5 -3
View File
@@ -14,6 +14,7 @@ warnings.filterwarnings("ignore")
def detect_encoding(file_path): def detect_encoding(file_path):
import chardet import chardet
import charset_normalizer import charset_normalizer
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
raw_data = f.read() raw_data = f.read()
detected_encoding = None detected_encoding = None
@@ -28,6 +29,7 @@ def detect_encoding(file_path):
encoding = detected_encoding if detected_encoding else "utf-8" encoding = detected_encoding if detected_encoding else "utf-8"
return encoding.lower() return encoding.lower()
def get_resource_path(package, resource): def get_resource_path(package, resource):
""" """
Get the path to a resource file, with fallback to local file system. Get the path to a resource file, with fallback to local file system.
@@ -261,18 +263,18 @@ def get_gpu_acceleration(enabled):
if not enabled: if not enabled:
return "GPU available but using CPU.", False return "GPU available but using CPU.", False
# Check for Apple Silicon MPS # Check for Apple Silicon MPS
if platform.system() == "Darwin" and platform.processor() == "arm": if platform.system() == "Darwin" and platform.processor() == "arm":
if torch.backends.mps.is_available(): if torch.backends.mps.is_available():
return "MPS GPU available and enabled.", True return "MPS GPU available and enabled.", True
else: else:
return "MPS GPU not available on Apple Silicon. Using CPU.", False return "MPS GPU not available on Apple Silicon. Using CPU.", False
# Check for CUDA # Check for CUDA
if cuda_available(): if cuda_available():
return "CUDA GPU available and enabled.", True return "CUDA GPU available and enabled.", True
# Gather CUDA diagnostic info if not available # Gather CUDA diagnostic info if not available
try: try:
cuda_devices = torch.cuda.device_count() cuda_devices = torch.cuda.device_count()
+58 -17
View File
@@ -131,10 +131,14 @@ 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.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Horizontal QSizePolicy.ControlType.PushButton,
QSizePolicy.ControlType.PushButton,
Qt.Orientation.Horizontal,
) )
layout_spacing_y = style.layoutSpacing( layout_spacing_y = style.layoutSpacing(
QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.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
@@ -180,7 +184,9 @@ 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.AlignmentFlag.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(
@@ -193,11 +199,21 @@ 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.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.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.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.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)
@@ -221,7 +237,9 @@ class VoiceMixer(QWidget):
self.slider = QSlider(Qt.Orientation.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.Policy.Fixed, QSizePolicy.Policy.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
@@ -251,7 +269,9 @@ class VoiceMixer(QWidget):
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignmentFlag.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.AlignmentFlag.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()
@@ -307,7 +327,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.FocusPolicy.NoFocus) self.delete_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.delete_button.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) self.delete_button.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents, False
)
self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor) self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.delete_button.hide() self.delete_button.hide()
@@ -408,7 +430,9 @@ class VoiceFormulaDialog(QDialog):
self.setWindowTitle("Voice Mixer") self.setWindowTitle("Voice Mixer")
self.setWindowFlags( self.setWindowFlags(
Qt.WindowType.Window | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.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)
@@ -473,8 +497,12 @@ class VoiceFormulaDialog(QDialog):
# 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.ScrollBarPolicy.ScrollBarAsNeeded) self.scroll_area.setHorizontalScrollBarPolicy(
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) Qt.ScrollBarPolicy.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()
@@ -564,7 +592,9 @@ class VoiceFormulaDialog(QDialog):
self, self,
"Unsaved Changes", "Unsaved Changes",
msg, msg,
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Save
| QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Save, QMessageBox.StandardButton.Save,
) )
if ret == QMessageBox.StandardButton.Save: if ret == QMessageBox.StandardButton.Save:
@@ -620,7 +650,9 @@ class VoiceFormulaDialog(QDialog):
"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.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel QMessageBox.StandardButton.Save
| QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel
) )
box.setDefaultButton(QMessageBox.StandardButton.Save) box.setDefaultButton(QMessageBox.StandardButton.Save)
ret = box.exec() ret = box.exec()
@@ -769,7 +801,9 @@ 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.Policy.Preferred, QSizePolicy.Policy.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)
) )
@@ -1138,7 +1172,10 @@ 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.StandardButton.Yes | QMessageBox.StandardButton.No self,
"Import Profile",
msg,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
@@ -1155,7 +1192,10 @@ 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.StandardButton.Yes | QMessageBox.StandardButton.No self,
"Import Profiles",
msg,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
@@ -1394,7 +1434,8 @@ class VoiceFormulaDialog(QDialog):
item.setData(Qt.ItemDataRole.BackgroundRole, color) item.setData(Qt.ItemDataRole.BackgroundRole, color)
else: else:
item.setData( item.setData(
Qt.ItemDataRole.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