mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Reformat using black
This commit is contained in:
+219
-142
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user