mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Added a loading indicator to the book handler window
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# 1.2.0 (pre-release)
|
# 1.2.0 (pre-release)
|
||||||
- Added `Line` option to subtitle generation modes, allowing subtitles to be generated based on line breaks in the text, by @mleg in #94.
|
- Added `Line` option to subtitle generation modes, allowing subtitles to be generated based on line breaks in the text, by @mleg in #94.
|
||||||
|
- Added a loading indicator to the book handler window for better user experience during book preprocessing.
|
||||||
- Fixed `cannot access local variable 'is_narrow'` error when subtitle format `SRT` was selected, mentioned by @Kinasa0096 in #88.
|
- Fixed `cannot access local variable 'is_narrow'` error when subtitle format `SRT` was selected, mentioned by @Kinasa0096 in #88.
|
||||||
- Fixed folder and filename sanitization to properly handle OS-specific illegal characters (Windows, Linux, macOS), ensuring compatibility across all platforms when creating chapter folders and files.
|
- Fixed folder and filename sanitization to properly handle OS-specific illegal characters (Windows, Linux, macOS), ensuring compatibility across all platforms when creating chapter folders and files.
|
||||||
- Fixed `/` and `\` path display by normalizing paths.
|
- Fixed `/` and `\` path display by normalizing paths.
|
||||||
|
|||||||
+117
-14
@@ -1,5 +1,4 @@
|
|||||||
import re
|
import re
|
||||||
import html
|
|
||||||
import ebooklib
|
import ebooklib
|
||||||
import base64
|
import base64
|
||||||
import fitz # PyMuPDF for PDF support
|
import fitz # PyMuPDF for PDF support
|
||||||
@@ -21,7 +20,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QMenu,
|
QMenu,
|
||||||
QLabel,
|
QLabel,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||||
from abogen.utils import clean_text, calculate_text_length, detect_encoding
|
from abogen.utils import clean_text, calculate_text_length, detect_encoding
|
||||||
import os
|
import os
|
||||||
import logging # Add logging
|
import logging # Add logging
|
||||||
@@ -46,6 +45,20 @@ class HandlerDialog(QDialog):
|
|||||||
# 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)
|
||||||
_content_cache = {}
|
_content_cache = {}
|
||||||
|
|
||||||
|
class _LoaderThread(QThread):
|
||||||
|
"""Minimal QThread that runs a callable and emits an error string on exception."""
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, target_callable):
|
||||||
|
super().__init__()
|
||||||
|
self._target = target_callable
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
self._target()
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_content_cache(cls, book_path=None):
|
def clear_content_cache(cls, book_path=None):
|
||||||
"""Clear the content cache. If book_path is provided, only clear that book's cache."""
|
"""Clear the content cache. If book_path is provided, only clear that book's cache."""
|
||||||
@@ -157,14 +170,11 @@ class HandlerDialog(QDialog):
|
|||||||
# Initialize checked_chapters set
|
# Initialize checked_chapters set
|
||||||
self.checked_chapters = set(checked_chapters) if checked_chapters else set()
|
self.checked_chapters = set(checked_chapters) if checked_chapters else set()
|
||||||
|
|
||||||
# For storing content and lengths
|
# For storing content and lengths (will be filled by background loader)
|
||||||
self.content_texts = {}
|
self.content_texts = {}
|
||||||
self.content_lengths = {}
|
self.content_lengths = {}
|
||||||
|
|
||||||
# Pre-process content based on file type
|
# Add a placeholder "Information" item so the tree isn't empty immediately
|
||||||
self._preprocess_content()
|
|
||||||
|
|
||||||
# Add "Information" item at the beginning of the tree
|
|
||||||
info_item = QTreeWidgetItem(self.treeWidget, ["Information"])
|
info_item = QTreeWidgetItem(self.treeWidget, ["Information"])
|
||||||
info_item.setData(0, Qt.UserRole, "info:bookinfo")
|
info_item.setData(0, Qt.UserRole, "info:bookinfo")
|
||||||
info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable)
|
info_item.setFlags(info_item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
@@ -172,8 +182,18 @@ class HandlerDialog(QDialog):
|
|||||||
font.setBold(True)
|
font.setBold(True)
|
||||||
info_item.setFont(0, font)
|
info_item.setFont(0, font)
|
||||||
|
|
||||||
# Build tree based on file type
|
# Setup UI now so dialog appears immediately
|
||||||
self._build_tree()
|
self._setup_ui()
|
||||||
|
|
||||||
|
# Create a centered loading overlay and show it while background load runs
|
||||||
|
self._create_loading_overlay()
|
||||||
|
# Hide the main UI so only the overlay is visible initially
|
||||||
|
if getattr(self, 'splitter', None) is not None:
|
||||||
|
self.splitter.setVisible(False)
|
||||||
|
self._show_loading_overlay("Loading...")
|
||||||
|
|
||||||
|
# Start background loading of book content so the dialog opens immediately
|
||||||
|
self._start_background_load()
|
||||||
|
|
||||||
# Hide expand/collapse decoration if there are no parent items
|
# Hide expand/collapse decoration if there are no parent items
|
||||||
has_parents = False
|
has_parents = False
|
||||||
@@ -183,20 +203,81 @@ class HandlerDialog(QDialog):
|
|||||||
break
|
break
|
||||||
self.treeWidget.setRootIsDecorated(has_parents)
|
self.treeWidget.setRootIsDecorated(has_parents)
|
||||||
|
|
||||||
# Setup UI (creates save_chapters_checkbox and other UI elements)
|
|
||||||
self._setup_ui()
|
|
||||||
|
|
||||||
# Run auto-check after UI is setup
|
|
||||||
|
def _create_loading_overlay(self):
|
||||||
|
"""Create a simple centered QLabel used as loading indicator.
|
||||||
|
|
||||||
|
This label is added to the dialog's main layout above the splitter so
|
||||||
|
when the splitter is hidden only the text is visible.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
label = QLabel(self)
|
||||||
|
label.setAlignment(Qt.AlignCenter)
|
||||||
|
# larger plain text per request
|
||||||
|
label.setStyleSheet("font-size: 14pt;")
|
||||||
|
label.setVisible(False)
|
||||||
|
# insert at top of main layout if present, otherwise keep as child
|
||||||
|
try:
|
||||||
|
layout = self.layout()
|
||||||
|
if layout is not None:
|
||||||
|
layout.insertWidget(0, label)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._loading_label = label
|
||||||
|
except Exception:
|
||||||
|
self._loading_label = None
|
||||||
|
|
||||||
|
def _show_loading_overlay(self, text: str):
|
||||||
|
lbl = getattr(self, '_loading_label', None)
|
||||||
|
if lbl is None:
|
||||||
|
return
|
||||||
|
lbl.setText(text)
|
||||||
|
lbl.setVisible(True)
|
||||||
|
|
||||||
|
def _hide_loading_overlay(self):
|
||||||
|
lbl = getattr(self, '_loading_label', None)
|
||||||
|
if lbl is None:
|
||||||
|
return
|
||||||
|
lbl.setVisible(False)
|
||||||
|
|
||||||
|
|
||||||
|
def _start_background_load(self):
|
||||||
|
"""Start a QThread that runs the preprocessing in background."""
|
||||||
|
# Start a minimal QThread which executes _preprocess_content
|
||||||
|
self._loader_thread = HandlerDialog._LoaderThread(self._preprocess_content)
|
||||||
|
self._loader_thread.finished.connect(self._on_load_finished)
|
||||||
|
self._loader_thread.error.connect(self._on_load_error)
|
||||||
|
# ensure thread instance is deleted when done
|
||||||
|
self._loader_thread.finished.connect(self._loader_thread.deleteLater)
|
||||||
|
self._loader_thread.start()
|
||||||
|
|
||||||
|
def _on_load_error(self, err_msg):
|
||||||
|
logging.error(f"Error loading book in background: {err_msg}")
|
||||||
|
if getattr(self, 'previewEdit', None) is not None:
|
||||||
|
self.previewEdit.setPlainText(f"Error loading book: {err_msg}")
|
||||||
|
if getattr(self, 'splitter', None) is not None:
|
||||||
|
self.splitter.setVisible(True)
|
||||||
|
self._hide_loading_overlay()
|
||||||
|
|
||||||
|
def _on_load_finished(self):
|
||||||
|
"""Called in the main thread when background loading finished."""
|
||||||
|
# Build the tree now that content_texts/content_lengths/etc. are ready
|
||||||
|
try:
|
||||||
|
# Rebuild tree based on file type
|
||||||
|
self._build_tree()
|
||||||
|
|
||||||
|
# Run auto-check if no provided checks are relevant
|
||||||
if not self._are_provided_checks_relevant():
|
if not self._are_provided_checks_relevant():
|
||||||
self._run_auto_check()
|
self._run_auto_check()
|
||||||
|
|
||||||
# Connect signals
|
# 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)
|
||||||
|
|
||||||
# Select first item and expand all
|
# Expand and select first item
|
||||||
self.treeWidget.expandAll()
|
self.treeWidget.expandAll()
|
||||||
if self.treeWidget.topLevelItemCount() > 0:
|
if self.treeWidget.topLevelItemCount() > 0:
|
||||||
self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(0))
|
self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(0))
|
||||||
@@ -205,6 +286,17 @@ class HandlerDialog(QDialog):
|
|||||||
# Update checkbox states
|
# Update checkbox states
|
||||||
self._update_checkbox_states()
|
self._update_checkbox_states()
|
||||||
|
|
||||||
|
# Update preview for the current selection
|
||||||
|
current = self.treeWidget.currentItem()
|
||||||
|
self.update_preview(current)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error finalizing book load: {e}")
|
||||||
|
# Show the main UI and hide loading text
|
||||||
|
if getattr(self, 'splitter', None) is not None:
|
||||||
|
self.splitter.setVisible(True)
|
||||||
|
self._hide_loading_overlay()
|
||||||
|
|
||||||
def _preprocess_content(self):
|
def _preprocess_content(self):
|
||||||
"""Pre-process content from the document"""
|
"""Pre-process content from the document"""
|
||||||
# Create cache key from file path, modification time, and file type
|
# Create cache key from file path, modification time, and file type
|
||||||
@@ -1958,6 +2050,17 @@ class HandlerDialog(QDialog):
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
def get_selected_text(self):
|
def get_selected_text(self):
|
||||||
|
# If a background loader thread is running, wait for it to finish to
|
||||||
|
# preserve compatibility with callers that expect content to be ready
|
||||||
|
# when they create a HandlerDialog and immediately request selected text.
|
||||||
|
try:
|
||||||
|
if hasattr(self, '_loader_thread') and getattr(self, '_loader_thread') is not None:
|
||||||
|
# Wait for thread to finish (blocks until done)
|
||||||
|
if self._loader_thread.isRunning():
|
||||||
|
self._loader_thread.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
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":
|
elif self.file_type == "markdown":
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ from abogen.voice_profiles import load_profiles
|
|||||||
# Import ctypes for Windows-specific taskbar icon
|
# Import ctypes for Windows-specific taskbar icon
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
import ctypes
|
import ctypes
|
||||||
import winreg
|
|
||||||
|
|
||||||
|
|
||||||
class DarkTitleBarEventFilter(QObject):
|
class DarkTitleBarEventFilter(QObject):
|
||||||
|
|||||||
Reference in New Issue
Block a user