mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Add Markdown File Support for Audiobook Generation
This commit is contained in:
+331
-72
@@ -43,18 +43,22 @@ class HandlerDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# Determine file type if not explicitly provided
|
# Determine file type if not explicitly provided
|
||||||
self.file_type = file_type or (
|
if file_type:
|
||||||
"pdf" if book_path.lower().endswith(".pdf") else "epub"
|
self.file_type = file_type
|
||||||
)
|
elif book_path.lower().endswith(".pdf"):
|
||||||
|
self.file_type = "pdf"
|
||||||
|
elif book_path.lower().endswith((".md", ".markdown")):
|
||||||
|
self.file_type = "markdown"
|
||||||
|
else:
|
||||||
|
self.file_type = "epub"
|
||||||
self.book_path = book_path
|
self.book_path = book_path
|
||||||
|
|
||||||
# Extract book name from file path
|
# Extract book name from file path
|
||||||
book_name = os.path.splitext(os.path.basename(book_path))[0]
|
book_name = os.path.splitext(os.path.basename(book_path))[0]
|
||||||
|
|
||||||
# Set window title based on file type and book name
|
# Set window title based on file type and book name
|
||||||
self.setWindowTitle(
|
item_type = "Chapters" if self.file_type in ["epub", "markdown"] else "Pages"
|
||||||
f'Select {"Chapters" if self.file_type == "epub" else "Pages"} - {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
|
||||||
@@ -69,7 +73,12 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
# Load the book based on file type
|
# Load the book based on file type
|
||||||
try:
|
try:
|
||||||
self.book = epub.read_epub(book_path) if self.file_type == "epub" else None
|
if self.file_type == "epub":
|
||||||
|
self.book = epub.read_epub(book_path)
|
||||||
|
elif self.file_type == "markdown":
|
||||||
|
self.book = None # Markdown doesn't use ebooklib
|
||||||
|
else:
|
||||||
|
self.book = None
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"EPUB file is missing a referenced file: {e}. Skipping missing file."
|
f"EPUB file is missing a referenced file: {e}. Skipping missing file."
|
||||||
@@ -100,6 +109,15 @@ class HandlerDialog(QDialog):
|
|||||||
logging.error(f"Failed to patch ebooklib for missing files: {patch_e}")
|
logging.error(f"Failed to patch ebooklib for missing files: {patch_e}")
|
||||||
raise e
|
raise e
|
||||||
self.pdf_doc = fitz.open(book_path) if self.file_type == "pdf" else None
|
self.pdf_doc = fitz.open(book_path) if self.file_type == "pdf" else None
|
||||||
|
self.markdown_text = None
|
||||||
|
if self.file_type == "markdown":
|
||||||
|
try:
|
||||||
|
encoding = self._detect_encoding(book_path)
|
||||||
|
with open(book_path, "r", encoding=encoding, errors="replace") as f:
|
||||||
|
self.markdown_text = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error reading markdown file: {e}")
|
||||||
|
self.markdown_text = ""
|
||||||
|
|
||||||
# Extract book metadata
|
# Extract book metadata
|
||||||
self.book_metadata = self._extract_book_metadata()
|
self.book_metadata = self._extract_book_metadata()
|
||||||
@@ -166,6 +184,17 @@ class HandlerDialog(QDialog):
|
|||||||
# Update checkbox states
|
# Update checkbox states
|
||||||
self._update_checkbox_states()
|
self._update_checkbox_states()
|
||||||
|
|
||||||
|
def _detect_encoding(self, file_path):
|
||||||
|
"""Detect encoding of a text file"""
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
raw_data = f.read()
|
||||||
|
try:
|
||||||
|
import chardet
|
||||||
|
result = chardet.detect(raw_data)
|
||||||
|
return result.get("encoding", "utf-8")
|
||||||
|
except ImportError:
|
||||||
|
return "utf-8"
|
||||||
|
|
||||||
def _preprocess_content(self):
|
def _preprocess_content(self):
|
||||||
"""Pre-process content from the document"""
|
"""Pre-process content from the document"""
|
||||||
if self.file_type == "epub":
|
if self.file_type == "epub":
|
||||||
@@ -178,6 +207,8 @@ class HandlerDialog(QDialog):
|
|||||||
)
|
)
|
||||||
# Fallback to a simpler spine-based processing if nav fails
|
# Fallback to a simpler spine-based processing if nav fails
|
||||||
self._process_epub_content_spine_fallback()
|
self._process_epub_content_spine_fallback()
|
||||||
|
elif self.file_type == "markdown":
|
||||||
|
self._preprocess_markdown_content()
|
||||||
else:
|
else:
|
||||||
self._preprocess_pdf_content()
|
self._preprocess_pdf_content()
|
||||||
|
|
||||||
@@ -199,10 +230,70 @@ class HandlerDialog(QDialog):
|
|||||||
# (common in headers/footers like "- 42 -")
|
# (common in headers/footers like "- 42 -")
|
||||||
text = re.sub(r"\s+[-–—]\s*\d+\s*[-–—]?\s*$", "", text, flags=re.MULTILINE)
|
text = re.sub(r"\s+[-–—]\s*\d+\s*[-–—]?\s*$", "", text, flags=re.MULTILINE)
|
||||||
|
|
||||||
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):
|
||||||
|
"""Pre-process markdown content and extract chapters/sections"""
|
||||||
|
if not self.markdown_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Split markdown by headers to create chapters
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Clean the text first
|
||||||
|
text = clean_text(self.markdown_text)
|
||||||
|
|
||||||
|
# Find all markdown headers (# ## ### etc.)
|
||||||
|
header_pattern = r'^(#{1,6})\s+(.+)$'
|
||||||
|
headers = list(re.finditer(header_pattern, text, re.MULTILINE))
|
||||||
|
|
||||||
|
self.content_texts = {}
|
||||||
|
self.content_lengths = {}
|
||||||
|
|
||||||
|
if not headers:
|
||||||
|
# No headers found, treat entire content as single chapter
|
||||||
|
chapter_id = "markdown_content"
|
||||||
|
self.content_texts[chapter_id] = text
|
||||||
|
self.content_lengths[chapter_id] = calculate_text_length(text)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process headers and extract content between them
|
||||||
|
for i, header_match in enumerate(headers):
|
||||||
|
header_level = len(header_match.group(1)) # Number of # symbols
|
||||||
|
header_text = header_match.group(2).strip()
|
||||||
|
header_start = header_match.start()
|
||||||
|
|
||||||
|
# Find content between this header and the next one of same or higher level
|
||||||
|
content_start = header_match.end()
|
||||||
|
content_end = len(text)
|
||||||
|
|
||||||
|
# Look for next header of same or higher level (fewer # symbols)
|
||||||
|
for j in range(i + 1, len(headers)):
|
||||||
|
next_header = headers[j]
|
||||||
|
next_level = len(next_header.group(1))
|
||||||
|
if next_level <= header_level:
|
||||||
|
content_end = next_header.start()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Extract content for this chapter
|
||||||
|
chapter_content = text[content_start:content_end].strip()
|
||||||
|
|
||||||
|
# Create unique identifier for this chapter
|
||||||
|
chapter_id = f"markdown_chapter_{i + 1}"
|
||||||
|
|
||||||
|
# Store the chapter content
|
||||||
|
if chapter_content:
|
||||||
|
# Include the header in the content
|
||||||
|
full_content = f"{header_text}\n\n{chapter_content}"
|
||||||
|
self.content_texts[chapter_id] = full_content
|
||||||
|
self.content_lengths[chapter_id] = calculate_text_length(full_content)
|
||||||
|
else:
|
||||||
|
# Even if no content, store the header
|
||||||
|
self.content_texts[chapter_id] = header_text
|
||||||
|
self.content_lengths[chapter_id] = calculate_text_length(header_text)
|
||||||
|
|
||||||
def _process_epub_content_spine_fallback(self):
|
def _process_epub_content_spine_fallback(self):
|
||||||
"""Fallback EPUB processing based purely on spine order."""
|
"""Fallback EPUB processing based purely on spine order."""
|
||||||
logging.info("Using spine fallback for EPUB processing.")
|
logging.info("Using spine fallback for EPUB processing.")
|
||||||
@@ -250,14 +341,14 @@ class HandlerDialog(QDialog):
|
|||||||
title = h1.get_text(strip=True)
|
title = h1.get_text(strip=True)
|
||||||
|
|
||||||
if not title:
|
if not title:
|
||||||
title = f"Untitled Chapter {i+1}"
|
title = f"Untitled Chapter {i + 1}"
|
||||||
synthetic_toc.append(
|
synthetic_toc.append(
|
||||||
(epub.Link(doc_href, title, doc_href), [])
|
(epub.Link(doc_href, title, doc_href), [])
|
||||||
) # Wrap in tuple and empty list for compatibility
|
) # Wrap in tuple and empty list for compatibility
|
||||||
|
|
||||||
# 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.")
|
||||||
@@ -282,7 +373,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,
|
||||||
)
|
)
|
||||||
@@ -395,8 +486,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")
|
||||||
@@ -620,13 +711,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")
|
||||||
@@ -678,19 +769,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)
|
||||||
@@ -838,8 +929,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
|
||||||
@@ -847,6 +938,8 @@ class HandlerDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
logging.warning("Building EPUB tree using fallback book.toc.")
|
logging.warning("Building EPUB tree using fallback book.toc.")
|
||||||
self._build_epub_tree_fallback(self.book.toc, self.treeWidget)
|
self._build_epub_tree_fallback(self.book.toc, self.treeWidget)
|
||||||
|
elif self.file_type == "markdown":
|
||||||
|
self._build_markdown_tree()
|
||||||
else:
|
else:
|
||||||
self._build_pdf_tree()
|
self._build_pdf_tree()
|
||||||
|
|
||||||
@@ -865,7 +958,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()
|
||||||
@@ -878,9 +971,9 @@ class HandlerDialog(QDialog):
|
|||||||
item.setData(0, Qt.UserRole, src)
|
item.setData(0, Qt.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():
|
||||||
@@ -935,7 +1028,7 @@ class HandlerDialog(QDialog):
|
|||||||
item.setData(0, Qt.UserRole, href)
|
item.setData(0, Qt.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:
|
||||||
@@ -993,7 +1086,7 @@ class HandlerDialog(QDialog):
|
|||||||
if isinstance(page, int)
|
if isinstance(page, int)
|
||||||
else self.pdf_doc.resolve_link(page)[0]
|
else self.pdf_doc.resolve_link(page)[0]
|
||||||
)
|
)
|
||||||
page_id = f"page_{page_num+1}"
|
page_id = f"page_{page_num + 1}"
|
||||||
# attach chapters on same page under original
|
# attach chapters on same page under original
|
||||||
if page_num in self.bookmark_items_map:
|
if page_num in self.bookmark_items_map:
|
||||||
orig = self.bookmark_items_map[page_num]
|
orig = self.bookmark_items_map[page_num]
|
||||||
@@ -1028,13 +1121,13 @@ 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
|
||||||
|
|
||||||
page_id = f"page_{sub_page_num+1}"
|
page_id = f"page_{sub_page_num + 1}"
|
||||||
page_title = f"Page {sub_page_num+1}"
|
page_title = f"Page {sub_page_num + 1}"
|
||||||
|
|
||||||
page_text = self.content_texts.get(page_id, "").strip()
|
page_text = self.content_texts.get(page_id, "").strip()
|
||||||
if page_text:
|
if page_text:
|
||||||
@@ -1077,8 +1170,8 @@ class HandlerDialog(QDialog):
|
|||||||
parent_item = (
|
parent_item = (
|
||||||
self.bookmark_items_map[prev_nums[-1]] if prev_nums else self.treeWidget
|
self.bookmark_items_map[prev_nums[-1]] if prev_nums else self.treeWidget
|
||||||
)
|
)
|
||||||
page_id = f"page_{page_num+1}"
|
page_id = f"page_{page_num + 1}"
|
||||||
title = f"Page {page_num+1}"
|
title = f"Page {page_num + 1}"
|
||||||
text = self.content_texts.get(page_id, "").strip()
|
text = self.content_texts.get(page_id, "").strip()
|
||||||
if text:
|
if text:
|
||||||
first = text.split("\n", 1)[0].strip()
|
first = text.split("\n", 1)[0].strip()
|
||||||
@@ -1095,6 +1188,63 @@ class HandlerDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
|
|
||||||
|
def _build_markdown_tree(self):
|
||||||
|
"""Build tree structure for markdown file based on headers"""
|
||||||
|
if not self.content_texts:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If only one content item (no headers), create simple structure
|
||||||
|
if len(self.content_texts) == 1:
|
||||||
|
chapter_id = list(self.content_texts.keys())[0]
|
||||||
|
title = "Content"
|
||||||
|
|
||||||
|
item = QTreeWidgetItem(self.treeWidget, [title])
|
||||||
|
item.setData(0, Qt.UserRole, chapter_id)
|
||||||
|
|
||||||
|
if self.content_lengths.get(chapter_id, 0) > 0:
|
||||||
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||||
|
is_checked = chapter_id in self.checked_chapters
|
||||||
|
item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked)
|
||||||
|
else:
|
||||||
|
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build hierarchical tree based on markdown headers
|
||||||
|
# First, parse the original markdown to get header structure
|
||||||
|
import re
|
||||||
|
header_pattern = r'^(#{1,6})\s+(.+)$'
|
||||||
|
headers = list(re.finditer(header_pattern, self.markdown_text, re.MULTILINE))
|
||||||
|
|
||||||
|
# Create tree items for each header
|
||||||
|
stack = [] # Stack to track parent items at different levels
|
||||||
|
|
||||||
|
for i, header_match in enumerate(headers):
|
||||||
|
header_level = len(header_match.group(1))
|
||||||
|
header_text = header_match.group(2).strip()
|
||||||
|
chapter_id = f"markdown_chapter_{i + 1}"
|
||||||
|
|
||||||
|
# Pop stack until we find appropriate parent level
|
||||||
|
while stack and stack[-1][0] >= header_level:
|
||||||
|
stack.pop()
|
||||||
|
|
||||||
|
# Determine parent item
|
||||||
|
parent_item = stack[-1][1] if stack else self.treeWidget
|
||||||
|
|
||||||
|
# Create tree item
|
||||||
|
item = QTreeWidgetItem(parent_item, [header_text])
|
||||||
|
item.setData(0, Qt.UserRole, chapter_id)
|
||||||
|
|
||||||
|
# Set checkable state based on content
|
||||||
|
if self.content_lengths.get(chapter_id, 0) > 0:
|
||||||
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||||
|
is_checked = chapter_id in self.checked_chapters
|
||||||
|
item.setCheckState(0, Qt.Checked if is_checked else Qt.Unchecked)
|
||||||
|
else:
|
||||||
|
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
|
|
||||||
|
# Add to stack for potential children
|
||||||
|
stack.append((header_level, item))
|
||||||
|
|
||||||
def _build_pdf_pages_tree(self):
|
def _build_pdf_pages_tree(self):
|
||||||
pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"])
|
pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"])
|
||||||
pages_item.setFlags(pages_item.flags() & ~Qt.ItemIsUserCheckable)
|
pages_item.setFlags(pages_item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
@@ -1103,8 +1253,8 @@ class HandlerDialog(QDialog):
|
|||||||
pages_item.setFont(0, font)
|
pages_item.setFont(0, font)
|
||||||
|
|
||||||
for page_num in range(len(self.pdf_doc)):
|
for page_num in range(len(self.pdf_doc)):
|
||||||
page_id = f"page_{page_num+1}"
|
page_id = f"page_{page_num + 1}"
|
||||||
page_title = f"Page {page_num+1}"
|
page_title = f"Page {page_num + 1}"
|
||||||
|
|
||||||
page_text = self.content_texts.get(page_id, "").strip()
|
page_text = self.content_texts.get(page_id, "").strip()
|
||||||
if page_text:
|
if page_text:
|
||||||
@@ -1166,7 +1316,7 @@ class HandlerDialog(QDialog):
|
|||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
||||||
item_type = "chapters" if self.file_type == "epub" else "pages"
|
item_type = "chapters" if self.file_type in ["epub", "markdown"] else "pages"
|
||||||
|
|
||||||
self.auto_select_btn = QPushButton(f"Auto-select {item_type}", self)
|
self.auto_select_btn = QPushButton(f"Auto-select {item_type}", self)
|
||||||
self.auto_select_btn.clicked.connect(self.auto_select_chapters)
|
self.auto_select_btn.clicked.connect(self.auto_select_chapters)
|
||||||
@@ -1214,7 +1364,7 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
checkbox_text = (
|
checkbox_text = (
|
||||||
"Save each chapter separately"
|
"Save each chapter separately"
|
||||||
if self.file_type == "epub"
|
if self.file_type in ["epub", "markdown"]
|
||||||
else "Save each page separately"
|
else "Save each page separately"
|
||||||
)
|
)
|
||||||
self.save_chapters_checkbox = QCheckBox(checkbox_text, self)
|
self.save_chapters_checkbox = QCheckBox(checkbox_text, self)
|
||||||
@@ -1259,15 +1409,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)
|
||||||
@@ -1275,13 +1425,13 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
checked_count = 0
|
checked_count = 0
|
||||||
|
|
||||||
if self.file_type == "epub":
|
if self.file_type in ["epub", "markdown"]:
|
||||||
iterator = QTreeWidgetItemIterator(self.treeWidget)
|
iterator = QTreeWidgetItemIterator(self.treeWidget)
|
||||||
while iterator.value():
|
while iterator.value():
|
||||||
item = iterator.value()
|
item = iterator.value()
|
||||||
if (
|
if (
|
||||||
item.flags() & Qt.ItemIsUserCheckable
|
item.flags() & Qt.ItemIsUserCheckable
|
||||||
and item.checkState(0) == Qt.Checked
|
and item.checkState(0) == Qt.Checked
|
||||||
):
|
):
|
||||||
checked_count += 1
|
checked_count += 1
|
||||||
if checked_count >= 2:
|
if checked_count >= 2:
|
||||||
@@ -1295,8 +1445,8 @@ class HandlerDialog(QDialog):
|
|||||||
while iterator.value():
|
while iterator.value():
|
||||||
item = iterator.value()
|
item = iterator.value()
|
||||||
if (
|
if (
|
||||||
item.flags() & Qt.ItemIsUserCheckable
|
item.flags() & Qt.ItemIsUserCheckable
|
||||||
and item.checkState(0) == Qt.Checked
|
and item.checkState(0) == Qt.Checked
|
||||||
):
|
):
|
||||||
parent = item.parent()
|
parent = item.parent()
|
||||||
if parent and parent != self.treeWidget.invisibleRootItem():
|
if parent and parent != self.treeWidget.invisibleRootItem():
|
||||||
@@ -1367,6 +1517,8 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
if self.file_type == "epub":
|
if self.file_type == "epub":
|
||||||
self._run_epub_auto_check()
|
self._run_epub_auto_check()
|
||||||
|
elif self.file_type == "markdown":
|
||||||
|
self._run_markdown_auto_check()
|
||||||
else:
|
else:
|
||||||
self._run_pdf_auto_check()
|
self._run_pdf_auto_check()
|
||||||
|
|
||||||
@@ -1394,7 +1546,41 @@ class HandlerDialog(QDialog):
|
|||||||
if child.flags() & Qt.ItemIsUserCheckable:
|
if child.flags() & Qt.ItemIsUserCheckable:
|
||||||
child_src = child.data(0, Qt.UserRole)
|
child_src = child.data(0, Qt.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
|
||||||
|
if child_has_content or child_is_parent:
|
||||||
|
child.setCheckState(0, Qt.Checked)
|
||||||
|
else:
|
||||||
|
item.setCheckState(0, Qt.Unchecked)
|
||||||
|
|
||||||
|
iterator += 1
|
||||||
|
|
||||||
|
def _run_markdown_auto_check(self):
|
||||||
|
"""Auto-select markdown chapters with significant content"""
|
||||||
|
iterator = QTreeWidgetItemIterator(self.treeWidget)
|
||||||
|
while iterator.value():
|
||||||
|
item = iterator.value()
|
||||||
|
if not (item.flags() & Qt.ItemIsUserCheckable):
|
||||||
|
iterator += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
identifier = item.data(0, Qt.UserRole)
|
||||||
|
|
||||||
|
# Select chapters with content > 500 characters or parent items
|
||||||
|
has_significant_content = identifier and self.content_lengths.get(identifier, 0) > 500
|
||||||
|
is_parent = item.childCount() > 0
|
||||||
|
|
||||||
|
if has_significant_content or is_parent:
|
||||||
|
item.setCheckState(0, Qt.Checked)
|
||||||
|
# Also check children if this is a parent
|
||||||
|
if is_parent:
|
||||||
|
for i in range(item.childCount()):
|
||||||
|
child = item.child(i)
|
||||||
|
if child.flags() & Qt.ItemIsUserCheckable:
|
||||||
|
child_identifier = child.data(0, Qt.UserRole)
|
||||||
|
child_has_content = (
|
||||||
|
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:
|
||||||
@@ -1428,8 +1614,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.Checked)
|
item.setCheckState(0, Qt.Checked)
|
||||||
|
|
||||||
@@ -1540,7 +1726,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"]:
|
||||||
@@ -1626,6 +1812,41 @@ class HandlerDialog(QDialog):
|
|||||||
if "cover" in item.get_name().lower():
|
if "cover" in item.get_name().lower():
|
||||||
metadata["cover_image"] = item.get_content()
|
metadata["cover_image"] = item.get_content()
|
||||||
break
|
break
|
||||||
|
elif self.file_type == "markdown":
|
||||||
|
# Extract metadata from markdown frontmatter or first heading
|
||||||
|
if self.markdown_text:
|
||||||
|
# Try to extract YAML frontmatter
|
||||||
|
frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', self.markdown_text, re.DOTALL)
|
||||||
|
if frontmatter_match:
|
||||||
|
try:
|
||||||
|
frontmatter = frontmatter_match.group(1)
|
||||||
|
# Simple YAML-like parsing for common fields
|
||||||
|
title_match = re.search(r'^title:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE)
|
||||||
|
if title_match:
|
||||||
|
metadata["title"] = title_match.group(1).strip().strip('"\'')
|
||||||
|
|
||||||
|
author_match = re.search(r'^author:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE)
|
||||||
|
if author_match:
|
||||||
|
metadata["authors"] = [author_match.group(1).strip().strip('"\'')]
|
||||||
|
|
||||||
|
desc_match = re.search(r'^description:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE)
|
||||||
|
if desc_match:
|
||||||
|
metadata["description"] = desc_match.group(1).strip().strip('"\'')
|
||||||
|
|
||||||
|
date_match = re.search(r'^date:\s*(.+)$', frontmatter, re.MULTILINE | re.IGNORECASE)
|
||||||
|
if date_match:
|
||||||
|
date_str = date_match.group(1).strip().strip('"\'')
|
||||||
|
year_match = re.search(r'\b(19|20)\d{2}\b', date_str)
|
||||||
|
if year_match:
|
||||||
|
metadata["publication_year"] = year_match.group(0)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Error parsing markdown frontmatter: {e}")
|
||||||
|
|
||||||
|
# Fallback: use first H1 header as title if no frontmatter title
|
||||||
|
if not metadata["title"]:
|
||||||
|
first_h1_match = re.search(r'^#\s+(.+)$', self.markdown_text, re.MULTILINE)
|
||||||
|
if first_h1_match:
|
||||||
|
metadata["title"] = first_h1_match.group(1).strip()
|
||||||
else:
|
else:
|
||||||
pdf_info = self.pdf_doc.metadata
|
pdf_info = self.pdf_doc.metadata
|
||||||
if pdf_info:
|
if pdf_info:
|
||||||
@@ -1670,6 +1891,8 @@ class HandlerDialog(QDialog):
|
|||||||
def get_selected_text(self):
|
def get_selected_text(self):
|
||||||
if self.file_type == "epub":
|
if self.file_type == "epub":
|
||||||
return self._get_epub_selected_text()
|
return self._get_epub_selected_text()
|
||||||
|
elif self.file_type == "markdown":
|
||||||
|
return self._get_markdown_selected_text()
|
||||||
else:
|
else:
|
||||||
return self._get_pdf_selected_text()
|
return self._get_pdf_selected_text()
|
||||||
|
|
||||||
@@ -1687,7 +1910,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
|
||||||
@@ -1709,6 +1932,42 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
return "\n".join(metadata_tags)
|
return "\n".join(metadata_tags)
|
||||||
|
|
||||||
|
def _get_markdown_selected_text(self):
|
||||||
|
"""Get selected text from markdown chapters"""
|
||||||
|
all_checked_identifiers = set()
|
||||||
|
chapter_texts = []
|
||||||
|
|
||||||
|
# Add metadata tags at the beginning
|
||||||
|
metadata_tags = self._format_metadata_tags()
|
||||||
|
|
||||||
|
item_order_counter = 0
|
||||||
|
ordered_checked_items = []
|
||||||
|
|
||||||
|
iterator = QTreeWidgetItemIterator(self.treeWidget)
|
||||||
|
while iterator.value():
|
||||||
|
item = iterator.value()
|
||||||
|
item_order_counter += 1
|
||||||
|
if item.checkState(0) == Qt.Checked:
|
||||||
|
identifier = item.data(0, Qt.UserRole)
|
||||||
|
if identifier and identifier != "info:bookinfo":
|
||||||
|
all_checked_identifiers.add(identifier)
|
||||||
|
ordered_checked_items.append((item_order_counter, item, identifier))
|
||||||
|
iterator += 1
|
||||||
|
|
||||||
|
ordered_checked_items.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
for order, item, identifier in ordered_checked_items:
|
||||||
|
text = self.content_texts.get(identifier)
|
||||||
|
if text and text.strip():
|
||||||
|
title = item.text(0)
|
||||||
|
# Remove leading dashes from title
|
||||||
|
title = re.sub(r"^\s*[-–—]\s*", "", title).strip()
|
||||||
|
marker = f"<<CHAPTER_MARKER:{title}>>"
|
||||||
|
chapter_texts.append(marker + "\n" + text)
|
||||||
|
|
||||||
|
full_text = metadata_tags + "\n\n" + "\n\n".join(chapter_texts)
|
||||||
|
return full_text, all_checked_identifiers
|
||||||
|
|
||||||
def _get_epub_selected_text(self):
|
def _get_epub_selected_text(self):
|
||||||
all_checked_identifiers = set()
|
all_checked_identifiers = set()
|
||||||
chapter_texts = []
|
chapter_texts = []
|
||||||
@@ -1753,7 +2012,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)
|
||||||
@@ -1793,9 +2052,9 @@ class HandlerDialog(QDialog):
|
|||||||
child = item.child(i)
|
child = item.child(i)
|
||||||
child_id = child.data(0, Qt.UserRole)
|
child_id = child.data(0, Qt.UserRole)
|
||||||
if (
|
if (
|
||||||
child.checkState(0) == Qt.Checked
|
child.checkState(0) == Qt.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:
|
||||||
@@ -1824,9 +2083,9 @@ class HandlerDialog(QDialog):
|
|||||||
elif item.flags() & Qt.ItemIsUserCheckable:
|
elif item.flags() & Qt.ItemIsUserCheckable:
|
||||||
identifier = item.data(0, Qt.UserRole)
|
identifier = item.data(0, Qt.UserRole)
|
||||||
if (
|
if (
|
||||||
identifier
|
identifier
|
||||||
and identifier not in included_text_ids
|
and identifier not in included_text_ids
|
||||||
and item.checkState(0) == Qt.Checked
|
and item.checkState(0) == Qt.Checked
|
||||||
):
|
):
|
||||||
text = self.content_texts.get(identifier, "")
|
text = self.content_texts.get(identifier, "")
|
||||||
if text:
|
if text:
|
||||||
@@ -1896,9 +2155,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.ItemIsUserCheckable)
|
or not (item.flags() & Qt.ItemIsUserCheckable)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+107
-82
@@ -158,7 +158,7 @@ class InputBox(QLabel):
|
|||||||
self.setAlignment(Qt.AlignCenter)
|
self.setAlignment(Qt.AlignCenter)
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self.setText(
|
self.setText(
|
||||||
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf)"
|
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)"
|
||||||
)
|
)
|
||||||
self.setStyleSheet(
|
self.setStyleSheet(
|
||||||
f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
|
f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
|
||||||
@@ -251,9 +251,11 @@ class InputBox(QLabel):
|
|||||||
return str(n)
|
return str(n)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
file_path.lower().endswith(".epub") or file_path.lower().endswith(".pdf")
|
file_path.lower().endswith(".epub")
|
||||||
|
or file_path.lower().endswith(".pdf")
|
||||||
|
or file_path.lower().endswith((".md", ".markdown"))
|
||||||
) and hasattr(self.window(), "selected_chapters"):
|
) and hasattr(self.window(), "selected_chapters"):
|
||||||
# EPUB or PDF: sum character counts for selected chapters
|
# EPUB, PDF, or Markdown: sum character counts for selected chapters
|
||||||
try:
|
try:
|
||||||
|
|
||||||
book_path = file_path
|
book_path = file_path
|
||||||
@@ -311,8 +313,8 @@ 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 (
|
||||||
self.window().selected_file_type in ["epub", "pdf"]
|
self.window().selected_file_type in ["epub", "pdf"]
|
||||||
and self.window().selected_file
|
and self.window().selected_file
|
||||||
):
|
):
|
||||||
should_show_edit = True
|
should_show_edit = True
|
||||||
|
|
||||||
@@ -345,7 +347,7 @@ class InputBox(QLabel):
|
|||||||
None # Reset the displayed file path when clearing input
|
None # Reset the displayed file path when clearing input
|
||||||
)
|
)
|
||||||
self.setText(
|
self.setText(
|
||||||
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf)"
|
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)"
|
||||||
)
|
)
|
||||||
self.setStyleSheet(
|
self.setStyleSheet(
|
||||||
f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
|
f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
|
||||||
@@ -381,9 +383,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"))
|
||||||
):
|
):
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
# Set hover style based on current state
|
# Set hover style based on current state
|
||||||
@@ -433,20 +436,26 @@ 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") or file_path.lower().endswith(
|
elif (file_path.lower().endswith(".epub")
|
||||||
".pdf"
|
or file_path.lower().endswith(".pdf")
|
||||||
):
|
or file_path.lower().endswith((".md", ".markdown"))):
|
||||||
|
# Determine file type
|
||||||
|
if file_path.lower().endswith(".epub"):
|
||||||
|
file_type = "epub"
|
||||||
|
elif file_path.lower().endswith(".pdf"):
|
||||||
|
file_type = "pdf"
|
||||||
|
else:
|
||||||
|
file_type = "markdown"
|
||||||
|
|
||||||
# Just store the file path but don't set the file info yet
|
# Just store the file path but don't set the file info yet
|
||||||
win.selected_file_type = (
|
win.selected_file_type = file_type
|
||||||
"epub" if file_path.lower().endswith(".epub") else "pdf"
|
|
||||||
)
|
|
||||||
win.selected_book_path = file_path
|
win.selected_book_path = file_path
|
||||||
win.open_book_file(
|
win.open_book_file(
|
||||||
file_path # This will handle the dialog and setting file info
|
file_path # This will handle the dialog and setting file info
|
||||||
)
|
)
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
else:
|
else:
|
||||||
self.set_error("Please drop a .txt, .epub, or .pdf file.")
|
self.set_error("Please drop a .txt, .epub, .pdf, or .md file.")
|
||||||
event.ignore()
|
event.ignore()
|
||||||
else:
|
else:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
@@ -478,9 +487,9 @@ class InputBox(QLabel):
|
|||||||
file_to_check = win.selected_file
|
file_to_check = win.selected_file
|
||||||
|
|
||||||
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))
|
||||||
@@ -599,8 +608,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"]:
|
if main_window.selected_file_type in ["epub", "pdf"]:
|
||||||
# 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
|
||||||
@@ -1155,16 +1164,21 @@ class abogen(QWidget):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
file_path, _ = QFileDialog.getOpenFileName(
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
self, "Select File", "", "Supported Files (*.txt *.epub *.pdf)"
|
self, "Select File", "", "Supported Files (*.txt *.epub *.pdf *.md)"
|
||||||
)
|
)
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return
|
return
|
||||||
if file_path.lower().endswith(".epub") or file_path.lower().endswith(
|
if (file_path.lower().endswith(".epub")
|
||||||
".pdf"
|
or file_path.lower().endswith(".pdf")
|
||||||
):
|
or file_path.lower().endswith((".md", ".markdown"))):
|
||||||
self.selected_file_type = (
|
# Determine file type
|
||||||
"epub" if file_path.lower().endswith(".epub") else "pdf"
|
if file_path.lower().endswith(".epub"):
|
||||||
)
|
self.selected_file_type = "epub"
|
||||||
|
elif file_path.lower().endswith(".pdf"):
|
||||||
|
self.selected_file_type = "pdf"
|
||||||
|
else:
|
||||||
|
self.selected_file_type = "markdown"
|
||||||
|
|
||||||
self.selected_book_path = file_path
|
self.selected_book_path = file_path
|
||||||
# Don't set file info immediately, open_book_file will handle it after dialog is accepted
|
# Don't set file info immediately, open_book_file will handle it after dialog is accepted
|
||||||
if not self.open_book_file(file_path):
|
if not self.open_book_file(file_path):
|
||||||
@@ -1183,14 +1197,17 @@ 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
|
||||||
|
|
||||||
dialog = HandlerDialog(
|
dialog = HandlerDialog(
|
||||||
book_path, checked_chapters=self.selected_chapters, parent=self
|
book_path,
|
||||||
|
file_type=getattr(self, 'selected_file_type', None),
|
||||||
|
checked_chapters=self.selected_chapters,
|
||||||
|
parent=self
|
||||||
)
|
)
|
||||||
dialog.setWindowModality(Qt.NonModal)
|
dialog.setWindowModality(Qt.NonModal)
|
||||||
dialog.setModal(False)
|
dialog.setModal(False)
|
||||||
@@ -1201,10 +1218,18 @@ class abogen(QWidget):
|
|||||||
return False
|
return False
|
||||||
chapters_text, all_checked_hrefs = dialog.get_selected_text()
|
chapters_text, all_checked_hrefs = dialog.get_selected_text()
|
||||||
if not all_checked_hrefs:
|
if not all_checked_hrefs:
|
||||||
file_type = "pdf" if book_path.lower().endswith(".pdf") else "epub"
|
# Determine file type for error message
|
||||||
error_msg = (
|
if book_path.lower().endswith(".pdf"):
|
||||||
f"No {'pages' if file_type == 'pdf' else 'chapters'} selected."
|
file_type = "pdf"
|
||||||
)
|
item_type = "pages"
|
||||||
|
elif book_path.lower().endswith((".md", ".markdown")):
|
||||||
|
file_type = "markdown"
|
||||||
|
item_type = "chapters"
|
||||||
|
else:
|
||||||
|
file_type = "epub"
|
||||||
|
item_type = "chapters"
|
||||||
|
|
||||||
|
error_msg = f"No {item_type} selected."
|
||||||
self._show_error_message_box(f"{file_type.upper()} Error", error_msg)
|
self._show_error_message_box(f"{file_type.upper()} Error", error_msg)
|
||||||
return False
|
return False
|
||||||
self.selected_chapters = all_checked_hrefs
|
self.selected_chapters = all_checked_hrefs
|
||||||
@@ -1322,9 +1347,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
|
||||||
@@ -1535,9 +1560,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)
|
||||||
@@ -1554,9 +1579,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)
|
||||||
@@ -1646,16 +1671,16 @@ 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
|
||||||
):
|
):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "Duplicate Item", "This item is already in the queue."
|
self, "Duplicate Item", "This item is already in the queue."
|
||||||
@@ -1764,10 +1789,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)
|
||||||
@@ -1851,7 +1876,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"] and hasattr(
|
if self.selected_file_type in ["epub", "pdf"] 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
|
||||||
@@ -1994,8 +2019,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()
|
||||||
@@ -2360,14 +2385,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:
|
||||||
@@ -2441,8 +2466,8 @@ class abogen(QWidget):
|
|||||||
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()
|
||||||
@@ -2529,9 +2554,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()
|
||||||
@@ -2566,8 +2591,8 @@ class abogen(QWidget):
|
|||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.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:
|
||||||
@@ -2587,8 +2612,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
|
||||||
@@ -2684,7 +2709,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")
|
||||||
@@ -2720,7 +2745,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(
|
||||||
@@ -3255,8 +3280,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
|
||||||
|
|
||||||
@@ -3383,9 +3408,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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user