Added a loading indicator to the book handler window

This commit is contained in:
Deniz Şafak
2025-10-20 01:17:14 +03:00
parent ccb350fa08
commit 1e89b566d1
3 changed files with 130 additions and 27 deletions
+1
View File
@@ -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
View File
@@ -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":
-1
View File
@@ -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):