diff --git a/abogen/book_handler.py b/abogen/book_handler.py deleted file mode 100644 index bf565fd..0000000 --- a/abogen/book_handler.py +++ /dev/null @@ -1,1549 +0,0 @@ -<<<<<<< HEAD -import re -import base64 -from bs4 import BeautifulSoup, NavigableString -from PyQt6.QtGui import QMovie -from PyQt6.QtWidgets import ( - QDialog, - QTreeWidget, - QTreeWidgetItem, - QTextEdit, - QPushButton, - QVBoxLayout, - QHBoxLayout, - QDialogButtonBox, - QSplitter, - QWidget, - """Backwards-compatible re-export of the PyQt book handler. - - The actual implementation lives in abogen.pyqt.book_handler. - """ - - from __future__ import annotations - - from abogen.pyqt.book_handler import * # noqa: F401, F403 - from abogen.pyqt.book_handler import HandlerDialog - - __all__ = ["HandlerDialog"] - - # Build treeview - self.treeWidget = QTreeWidget(self) - self.treeWidget.setHeaderHidden(True) - self.treeWidget.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection) - self.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.treeWidget.customContextMenuRequested.connect(self.on_tree_context_menu) - - # Initialize checked_chapters set - self.checked_chapters = set(checked_chapters) if checked_chapters else set() - - # For storing content and lengths (will be filled by background loader) - self.content_texts = {} - self.content_lengths = {} - # Also maintain refs for structure - self.processed_nav_structure = [] - - # Add a placeholder "Information" item so the tree isn't empty immediately - info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) - info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo") - info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - font = info_item.font(0) - font.setBold(True) - info_item.setFont(0, font) - - # Setup UI now so dialog appears immediately - 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 - has_parents = False - for i in range(self.treeWidget.topLevelItemCount()): - if self.treeWidget.topLevelItem(i).childCount() > 0: - has_parents = True - break - self.treeWidget.setRootIsDecorated(has_parents) - - def _create_loading_overlay(self): - """Create a centered loading indicator with a GIF on the left and text on the right. - - The indicator is added to the dialog's main layout above the splitter so - when the splitter is hidden only the indicator is visible. - """ - try: - # Container to hold gif + text and allow centering via stretches - container = QWidget(self) - container.setVisible(False) - h = QHBoxLayout(container) - h.setContentsMargins(0, 8, 0, 8) - h.setSpacing(10) - - # Left: GIF label (animated) - gif_label = QLabel(container) - gif_label.setVisible(False) - - loading_gif_path = get_resource_path("abogen.assets", "loading.gif") - movie = None - if loading_gif_path: - try: - movie = QMovie(loading_gif_path) - # Make GIF smaller so it doesn't dominate the text - movie.setScaledSize(QSize(25, 25)) - gif_label.setMovie(movie) - gif_label.setFixedSize(25, 25) - gif_label.setVisible(True) - except Exception: - movie = None - - # Right: Text label - text_label = QLabel(container) - text_label.setStyleSheet("font-size: 14pt;") - - # Add stretches to center the content horizontally - h.addStretch(1) - h.addWidget(gif_label, 0, Qt.AlignmentFlag.AlignVCenter) - h.addWidget(text_label, 0, Qt.AlignmentFlag.AlignVCenter) - h.addStretch(1) - - # Insert at top of main layout if present, otherwise keep as child - try: - layout = self.layout() - if layout is not None: - layout.insertWidget(0, container) - except Exception: - pass - - # Store refs - self._loading_container = container - self._loading_gif_label = gif_label - self._loading_text_label = text_label - self._loading_movie = movie - except Exception: - self._loading_container = None - self._loading_gif_label = None - self._loading_text_label = None - self._loading_movie = None - - def _show_loading_overlay(self, text: str): - container = getattr(self, "_loading_container", None) - text_lbl = getattr(self, "_loading_text_label", None) - movie = getattr(self, "_loading_movie", None) - gif_lbl = getattr(self, "_loading_gif_label", None) - if container is None or text_lbl is None: - return - text_lbl.setText(text) - if movie is not None and gif_lbl is not None: - try: - movie.start() - gif_lbl.setVisible(True) - except Exception: - pass - container.setVisible(True) - - def _hide_loading_overlay(self): - container = getattr(self, "_loading_container", None) - movie = getattr(self, "_loading_movie", None) - if container is None: - return - if movie is not None: - try: - movie.stop() - except Exception: - pass - container.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(): - self._run_auto_check() - - # Connect signals (after tree exists) - self.treeWidget.currentItemChanged.connect(self.update_preview) - self.treeWidget.itemChanged.connect(self.handle_item_check) - self.treeWidget.itemChanged.connect( - lambda _: self._update_checkbox_states() - ) - self.treeWidget.itemDoubleClicked.connect(self.handle_item_double_click) - - # Expand and select first item - self.treeWidget.expandAll() - if self.treeWidget.topLevelItemCount() > 0: - self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(0)) - self.treeWidget.setFocus() - - # 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): - """Pre-process content from the document""" - # Create cache key from file path, modification time, file type, and replace_single_newlines setting - try: - mod_time = os.path.getmtime(self.book_path) - except Exception: - mod_time = 0 - - # Include replace_single_newlines in cache key since it affects text cleaning - from abogen.utils import load_config - - cfg = load_config() - replace_single_newlines = cfg.get("replace_single_newlines", True) - - cache_key = (self.book_path, mod_time, self.parser.file_type, replace_single_newlines) - - # Check if content is already cached - if cache_key in HandlerDialog._content_cache: - cached_data = HandlerDialog._content_cache[cache_key] - self.content_texts = cached_data["content_texts"] - self.content_lengths = cached_data["content_lengths"] - if "processed_nav_structure" in cached_data: - self.processed_nav_structure = cached_data["processed_nav_structure"] - if "book_metadata" in cached_data: - self.book_metadata = cached_data["book_metadata"] - - # Apply to parser so it stays in sync if used elsewhere - self.parser.content_texts = self.content_texts - self.parser.content_lengths = self.content_lengths - self.parser.processed_nav_structure = self.processed_nav_structure - self.parser.book_metadata = self.book_metadata - - logging.info(f"Using cached content for {os.path.basename(self.book_path)}") - return - - # Process content if not cached - try: - self.parser.process_content(replace_single_newlines=replace_single_newlines) - self.content_texts = self.parser.content_texts - self.content_lengths = self.parser.content_lengths - self.processed_nav_structure = self.parser.processed_nav_structure - self.book_metadata = self.parser.get_metadata() - except Exception as e: - logging.error(f"Error processing content: {e}", exc_info=True) - # Handle empty/failure case - self.content_texts = {} - self.content_lengths = {} - - # Cache the processed content - cache_data = { - "content_texts": self.content_texts, - "content_lengths": self.content_lengths, - "processed_nav_structure": self.processed_nav_structure, - "book_metadata": self.book_metadata, - } - - HandlerDialog._content_cache[cache_key] = cache_data - logging.info(f"Cached content for {os.path.basename(self.book_path)}") - - - def _build_tree(self): - self.treeWidget.clear() - - info_item = QTreeWidgetItem(self.treeWidget, ["Information"]) - info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo") - info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - font = info_item.font(0) - font.setBold(True) - info_item.setFont(0, font) - - # TODO look into why we need this here and not just rely on logic in PDFParser - if self.parser.file_type == "pdf": - self._build_pdf_tree() - elif self.processed_nav_structure: - self._build_tree_from_nav( - self.processed_nav_structure, self.treeWidget - ) - else: - # If no structure found but content exists (rare fallback), list flat - for ch_id, ch_len in self.content_lengths.items(): - # Simple flat list - item = QTreeWidgetItem(self.treeWidget, [ch_id]) - item.setData(0, Qt.ItemDataRole.UserRole, ch_id) - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) - if self.content_texts.get(ch_id): - item.setCheckState(0, Qt.CheckState.Checked if ch_id in self.checked_chapters else Qt.CheckState.Unchecked) - - has_parents = False - iterator = QTreeWidgetItemIterator( - self.treeWidget, QTreeWidgetItemIterator.IteratorFlag.HasChildren - ) - if iterator.value(): - has_parents = True - self.treeWidget.setRootIsDecorated(has_parents) - - def _update_checkbox_states(self): - """Update the checkbox states based on the current checked chapters.""" - for i in range(self.treeWidget.topLevelItemCount()): - item = self.treeWidget.topLevelItem(i) - self._update_item_checkbox_state(item) - - def _build_tree_from_nav( - self, nav_nodes, parent_item, seen_content_hashes=None - ): - if seen_content_hashes is None: - seen_content_hashes = set() - for node in nav_nodes: - title = node.get("title", "Unknown") - src = node.get("src") - children = node.get("children", []) - - item = QTreeWidgetItem(parent_item, [title]) - item.setData(0, Qt.ItemDataRole.UserRole, src) - - is_empty = ( - src - and (src in self.content_texts) - and (not self.content_texts[src].strip()) - ) - is_duplicate = False - if src and src in self.content_texts and self.content_texts[src].strip(): - content_hash = hash(self.content_texts[src]) - if content_hash in seen_content_hashes: - is_duplicate = True - else: - seen_content_hashes.add(content_hash) - - if src and not is_empty and not is_duplicate: - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) - is_checked = src in self.checked_chapters - item.setCheckState( - 0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked - ) - elif is_duplicate: - # Mark as duplicate and remove checkbox - item.setText(0, f"{title} (Duplicate)") - item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - elif children: - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) - item.setCheckState(0, Qt.CheckState.Unchecked) - else: - item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - - if children: - self._build_tree_from_nav(children, item, seen_content_hashes) - - - # TODO look into why we need this here and not just rely on logic in PDFParser - def _build_pdf_tree(self): - pdf_doc = getattr(self.parser, "pdf_doc", None) - if not pdf_doc: - return - - outline = pdf_doc.get_toc() - self.has_pdf_bookmarks = bool(outline) - self.bookmark_items_map = {} - - if not outline: - self._build_pdf_pages_tree() - return - - bookmark_pages = [] - page_to_bookmark = {} - next_page_boundaries = {} - added_pages = set() - - def extract_page_numbers(entries): - for entry in entries: - if len(entry) >= 3: - _, title, page = entry[:3] - page_num = ( - page - 1 - if isinstance(page, int) - else pdf_doc.resolve_link(page)[0] - ) - bookmark_pages.append((page_num, title)) - - if len(entry) > 3 and isinstance(entry[3], list): - extract_page_numbers(entry[3]) - - extract_page_numbers(outline) - bookmark_pages.sort() - - for i, (page_num, title) in enumerate(bookmark_pages): - if i < len(bookmark_pages) - 1: - next_page_boundaries[page_num] = bookmark_pages[i + 1][0] - page_to_bookmark[page_num] = title - - def build_outline_tree(entries, parent_item): - for entry in entries: - if len(entry) >= 3: - entry_level, title, page = entry[:3] - page_num = ( - page - 1 - if isinstance(page, int) - else pdf_doc.resolve_link(page)[0] - ) - page_id = f"page_{page_num + 1}" - # attach chapters on same page under original - if page_num in self.bookmark_items_map: - orig = self.bookmark_items_map[page_num] - child = QTreeWidgetItem(orig, [f"{title} (Same page)"]) - child.setData(0, Qt.ItemDataRole.UserRole, page_id) - child.setFlags(child.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - continue - bookmark_item = QTreeWidgetItem(parent_item, [title]) - bookmark_item.setData(0, Qt.ItemDataRole.UserRole, page_id) - # only allow checking if this chapter has content - if self.content_lengths.get(page_id, 0) > 0: - bookmark_item.setFlags( - bookmark_item.flags() | Qt.ItemFlag.ItemIsUserCheckable - ) - bookmark_item.setCheckState( - 0, - ( - Qt.CheckState.Checked - if page_id in self.checked_chapters - else Qt.CheckState.Unchecked - ), - ) - else: - bookmark_item.setFlags( - bookmark_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable - ) - # map for uncategorized pages - self.bookmark_items_map[page_num] = bookmark_item - - added_pages.add(page_num) - - next_page = next_page_boundaries.get(page_num, len(pdf_doc)) - for sub_page_num in range(page_num + 1, next_page): - if ( - sub_page_num in page_to_bookmark - or sub_page_num in added_pages - ): - continue - - page_id = f"page_{sub_page_num + 1}" - page_title = f"Page {sub_page_num + 1}" - - page_text = self.content_texts.get(page_id, "").strip() - if page_text: - first_line = page_text.split("\n", 1)[0].strip() - if first_line and len(first_line) < 100: - page_title += f" - {first_line}" - - page_item = QTreeWidgetItem(bookmark_item, [page_title]) - page_item.setData(0, Qt.ItemDataRole.UserRole, page_id) - # only allow checking if this sub-page has content - if self.content_lengths.get(page_id, 0) > 0: - page_item.setFlags( - page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable - ) - page_item.setCheckState( - 0, - ( - Qt.CheckState.Checked - if page_id in self.checked_chapters - else Qt.CheckState.Unchecked - ), - ) - else: - page_item.setFlags( - page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable - ) - - added_pages.add(sub_page_num) - - build_outline_tree(outline, self.treeWidget) - - covered_pages = set(added_pages) - # attach any pages without direct bookmarks under nearest preceding chapter - uncategorized_pages = [ - i for i in range(len(pdf_doc)) if i not in covered_pages - ] - for page_num in uncategorized_pages: - # find nearest previous bookmark - prev_nums = [n for n in sorted(self.bookmark_items_map) if n < page_num] - parent_item = ( - self.bookmark_items_map[prev_nums[-1]] if prev_nums else self.treeWidget - ) - page_id = f"page_{page_num + 1}" - title = f"Page {page_num + 1}" - text = self.content_texts.get(page_id, "").strip() - if text: - first = text.split("\n", 1)[0].strip() - if first and len(first) < 100: - title += f" - {first}" - page_item = QTreeWidgetItem(parent_item, [title]) - page_item.setData(0, Qt.ItemDataRole.UserRole, page_id) - # only allow checking if uncategorized page has content - if self.content_lengths.get(page_id, 0) > 0: - page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable) - page_item.setCheckState( - 0, - ( - Qt.CheckState.Checked - if page_id in self.checked_chapters - else Qt.CheckState.Unchecked - ), - ) - else: - page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - - def _build_pdf_pages_tree(self): - pdf_doc = getattr(self.parser, "pdf_doc", None) - if not pdf_doc: - return - - pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"]) - pages_item.setFlags(pages_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - font = pages_item.font(0) - font.setBold(True) - pages_item.setFont(0, font) - - for page_num in range(len(pdf_doc)): - page_id = f"page_{page_num + 1}" - page_title = f"Page {page_num + 1}" - - page_text = self.content_texts.get(page_id, "").strip() - if page_text: - first_line = page_text.split("\n", 1)[0].strip() - if first_line and len(first_line) < 100: - page_title += f" - {first_line}" - - page_item = QTreeWidgetItem(pages_item, [page_title]) - page_item.setData(0, Qt.ItemDataRole.UserRole, page_id) - # only allow checking if standalone page has content - if self.content_lengths.get(page_id, 0) > 0: - page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable) - page_item.setCheckState( - 0, - ( - Qt.CheckState.Checked - if page_id in self.checked_chapters - else Qt.CheckState.Unchecked - ), - ) - else: - page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) - - def _are_provided_checks_relevant(self): - if not self.checked_chapters: - return False - - all_identifiers = set() - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: - identifier = item.data(0, Qt.ItemDataRole.UserRole) - if identifier: - all_identifiers.add(identifier) - iterator += 1 - - return bool(self.checked_chapters.intersection(all_identifiers)) - - def _setup_ui(self): - self.previewEdit = QTextEdit(self) - self.previewEdit.setReadOnly(True) - self.previewEdit.setMinimumWidth(300) - self.previewEdit.setStyleSheet("QTextEdit { border: none; }") - - self.previewInfoLabel = QLabel( - '*Note: You can modify the content later using the "Edit" button in the input box or by accessing the temporary files directory through settings (if not saved in a project folder).', - self, - ) - self.previewInfoLabel.setWordWrap(True) - self.previewInfoLabel.setStyleSheet( - "QLabel { color: #666; font-style: italic; }" - ) - - previewLayout = QVBoxLayout() - previewLayout.setContentsMargins(0, 0, 0, 0) - previewLayout.addWidget(self.previewEdit, 1) - previewLayout.addWidget(self.previewInfoLabel, 0) - - rightWidget = QWidget() - rightWidget.setLayout(previewLayout) - - buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, - self, - ) - buttons.accepted.connect(self.accept) - buttons.rejected.connect(self.reject) - - item_type = "chapters" if self.parser.file_type in ["epub", "markdown"] else "pages" - - 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.setToolTip(f"Automatically select main {item_type}") - - buttons_layout = QVBoxLayout() - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.setSpacing(10) - - auto_select_layout = QHBoxLayout() - auto_select_layout.addWidget(self.auto_select_btn) - buttons_layout.addLayout(auto_select_layout) - - select_layout = QHBoxLayout() - self.select_all_btn = QPushButton("Select all", self) - self.select_all_btn.clicked.connect(self.select_all_chapters) - self.deselect_all_btn = QPushButton("Clear all", self) - self.deselect_all_btn.clicked.connect(self.deselect_all_chapters) - select_layout.addWidget(self.select_all_btn) - select_layout.addWidget(self.deselect_all_btn) - buttons_layout.addLayout(select_layout) - - parent_layout = QHBoxLayout() - self.select_parents_btn = QPushButton("Select parents", self) - self.select_parents_btn.clicked.connect(self.select_parent_chapters) - self.deselect_parents_btn = QPushButton("Unselect parents", self) - self.deselect_parents_btn.clicked.connect(self.deselect_parent_chapters) - parent_layout.addWidget(self.select_parents_btn) - parent_layout.addWidget(self.deselect_parents_btn) - buttons_layout.addLayout(parent_layout) - - expand_layout = QHBoxLayout() - self.expand_all_btn = QPushButton("Expand All", self) - self.expand_all_btn.clicked.connect(self.treeWidget.expandAll) - self.collapse_all_btn = QPushButton("Collapse All", self) - self.collapse_all_btn.clicked.connect(self.treeWidget.collapseAll) - expand_layout.addWidget(self.expand_all_btn) - expand_layout.addWidget(self.collapse_all_btn) - buttons_layout.addLayout(expand_layout) - - leftLayout = QVBoxLayout() - leftLayout.setContentsMargins(0, 0, 5, 0) - leftLayout.addLayout(buttons_layout) - leftLayout.addWidget(self.treeWidget) - - checkbox_text = ( - "Save each chapter separately" - if self.parser.file_type in ["epub", "markdown"] - else "Save each page separately" - ) - self.save_chapters_checkbox = QCheckBox(checkbox_text, self) - self.save_chapters_checkbox.setChecked(self.save_chapters_separately) - self.save_chapters_checkbox.stateChanged.connect(self.on_save_chapters_changed) - leftLayout.addWidget(self.save_chapters_checkbox) - self.merge_chapters_checkbox = QCheckBox( - "Create a merged version at the end", self - ) - self.merge_chapters_checkbox.setChecked(self.merge_chapters_at_end) - self.merge_chapters_checkbox.stateChanged.connect( - self.on_merge_chapters_changed - ) - leftLayout.addWidget(self.merge_chapters_checkbox) - - self.save_as_project_checkbox = QCheckBox( - "Save in a project folder with metadata", self - ) - self.save_as_project_checkbox.setToolTip( - "Save the converted item in a project folder with metadata files. " - "(Useful if you want to work with converted items in the future.)" - ) - self.save_as_project_checkbox.setChecked(self.save_as_project) - self.save_as_project_checkbox.stateChanged.connect( - self.on_save_as_project_changed - ) - leftLayout.addWidget(self.save_as_project_checkbox) - - leftLayout.addWidget(buttons) - - leftWidget = QWidget() - leftWidget.setLayout(leftLayout) - - self.splitter = QSplitter(Qt.Orientation.Horizontal) - self.splitter.addWidget(leftWidget) - self.splitter.addWidget(rightWidget) - self.splitter.setSizes([280, 420]) - - mainLayout = QVBoxLayout(self) - mainLayout.addWidget(self.splitter) - self.setLayout(mainLayout) - - def _update_checkbox_states(self): - if ( - not hasattr(self, "save_chapters_checkbox") - or not self.save_chapters_checkbox - ): - return - - if ( - self.parser.file_type == "pdf" - and hasattr(self, "has_pdf_bookmarks") - and not self.has_pdf_bookmarks - ): - self.save_chapters_checkbox.setEnabled(False) - self.merge_chapters_checkbox.setEnabled(False) - return - - checked_count = 0 - - if self.parser.file_type in ["epub", "markdown"]: - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if ( - item.flags() & Qt.ItemFlag.ItemIsUserCheckable - and item.checkState(0) == Qt.CheckState.Checked - ): - checked_count += 1 - if checked_count >= 2: - break - iterator += 1 - - else: - parent_groups = set() - - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if ( - item.flags() & Qt.ItemFlag.ItemIsUserCheckable - and item.checkState(0) == Qt.CheckState.Checked - ): - parent = item.parent() - if parent and parent != self.treeWidget.invisibleRootItem(): - parent_groups.add(id(parent)) - else: - parent_groups.add(id(item)) - iterator += 1 - - checked_count = len(parent_groups) - - min_groups_required = 2 - self.save_chapters_checkbox.setEnabled(checked_count >= min_groups_required) - - self.merge_chapters_checkbox.setEnabled( - self.save_chapters_checkbox.isEnabled() - and self.save_chapters_checkbox.isChecked() - ) - - def select_all_chapters(self): - self._block_signals = True - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: - item.setCheckState(0, Qt.CheckState.Checked) - iterator += 1 - self._block_signals = False - self._update_checked_set_from_tree() - - def deselect_all_chapters(self): - self._block_signals = True - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: - item.setCheckState(0, Qt.CheckState.Unchecked) - iterator += 1 - self._block_signals = False - self._update_checked_set_from_tree() - - def select_parent_chapters(self): - self._block_signals = True - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0: - item.setCheckState(0, Qt.CheckState.Checked) - iterator += 1 - self._block_signals = False - self._update_checked_set_from_tree() - - def deselect_parent_chapters(self): - self._block_signals = True - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0: - item.setCheckState(0, Qt.CheckState.Unchecked) - iterator += 1 - self._block_signals = False - self._update_checked_set_from_tree() - - def auto_select_chapters(self): - self._run_auto_check() - - def _run_auto_check(self): - self._block_signals = True - - if self.parser.file_type == "epub": - self._run_epub_auto_check() - elif self.parser.file_type == "markdown": - self._run_markdown_auto_check() - else: - self._run_pdf_auto_check() - - self._block_signals = False - self._update_checked_set_from_tree() - - def _run_epub_auto_check(self): - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable): - iterator += 1 - continue - - src = item.data(0, Qt.ItemDataRole.UserRole) - - has_significant_content = src and self.content_lengths.get(src, 0) > 1000 - is_parent = item.childCount() > 0 - - if has_significant_content or is_parent: - item.setCheckState(0, Qt.CheckState.Checked) - if is_parent: - for i in range(item.childCount()): - child = item.child(i) - if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: - child_src = child.data(0, Qt.ItemDataRole.UserRole) - child_has_content = ( - 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.CheckState.Checked) - else: - item.setCheckState(0, Qt.CheckState.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.ItemFlag.ItemIsUserCheckable): - iterator += 1 - continue - - identifier = item.data(0, Qt.ItemDataRole.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.CheckState.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.ItemFlag.ItemIsUserCheckable: - child_identifier = child.data(0, Qt.ItemDataRole.UserRole) - child_has_content = ( - child_identifier - and self.content_lengths.get(child_identifier, 0) > 0 - ) - child_is_parent = child.childCount() > 0 - if child_has_content or child_is_parent: - child.setCheckState(0, Qt.CheckState.Checked) - else: - item.setCheckState(0, Qt.CheckState.Unchecked) - - iterator += 1 - - def _run_pdf_auto_check(self): - if hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks: - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: - item.setCheckState(0, Qt.CheckState.Checked) - iterator += 1 - return - - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable): - iterator += 1 - continue - - identifier = item.data(0, Qt.ItemDataRole.UserRole) - - if not identifier: - iterator += 1 - continue - - if ( - not identifier.startswith("page_") - or self.content_lengths.get(identifier, 0) > 0 - ): - item.setCheckState(0, Qt.CheckState.Checked) - - iterator += 1 - - def _update_checked_set_from_tree(self): - self.checked_chapters.clear() - iterator = QTreeWidgetItemIterator(self.treeWidget) - while iterator.value(): - item = iterator.value() - if item.checkState(0) == Qt.CheckState.Checked: - identifier = item.data(0, Qt.ItemDataRole.UserRole) - if identifier: - self.checked_chapters.add(identifier) - iterator += 1 - if hasattr(self, "save_chapters_checkbox") and self.save_chapters_checkbox: - self._update_checkbox_states() - - def handle_item_check(self, item): - if self._block_signals: - return - - self._block_signals = True - - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable: - for i in range(item.childCount()): - child = item.child(i) - if child.flags() & Qt.ItemFlag.ItemIsUserCheckable: - child.setCheckState(0, item.checkState(0)) - - self._block_signals = False - self._update_checked_set_from_tree() - - def handle_item_double_click(self, item, column=0): - if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() == 0: - rect = self.treeWidget.visualItemRect(item) - checkbox_width = 20 - - mouse_pos = self.treeWidget.mapFromGlobal(self.treeWidget.cursor().pos()) - - if mouse_pos.x() > rect.x() + checkbox_width: - new_state = ( - Qt.CheckState.Unchecked - if item.checkState(0) == Qt.CheckState.Checked - else Qt.CheckState.Checked - ) - item.setCheckState(0, new_state) - - def update_preview(self, current): - if not current: - self.previewEdit.clear() - return - - identifier = current.data(0, Qt.ItemDataRole.UserRole) - - if identifier == "info:bookinfo": - self._display_book_info() - return - - text = None - if self.parser.file_type == "epub": - text = self.content_texts.get(identifier) - else: - text = self.content_texts.get(identifier) - - if text is None: - title = current.text(0) - self.previewEdit.setPlainText( - f"{title}\n\n(No content available for this item)" - ) - elif not text.strip(): - title = current.text(0) - self.previewEdit.setPlainText(f"{title}\n\n(This item is empty)") - else: - # Apply clean_text to preview so replace_single_newlines setting is respected - cleaned_text = clean_text(text) - self.previewEdit.setPlainText(cleaned_text) - - def _display_book_info(self): - self.previewEdit.clear() - html_content = "
" - - cover_image = self.book_metadata.get("cover_image") - if cover_image: - try: - image_data = base64.b64encode(cover_image).decode("utf-8") - - image_type = "jpeg" - if cover_image.startswith(b"\x89PNG"): - image_type = "png" - elif cover_image.startswith(b"GIF"): - image_type = "gif" - - html_content += ( - f"Error displaying cover image: {str(e)}
" - - title = self.book_metadata.get("title") - if title: - html_content += ( - f"By {authors_text}
" - - publisher = self.book_metadata.get("publisher") - pub_year = self.book_metadata.get("publication_year") - - if publisher or pub_year: - pub_info = [] - if publisher: - pub_info.append(f"Published by {publisher}") - if pub_year: - pub_info.append(f"Year: {pub_year}") - html_content += f"{' | '.join(pub_info)}
" - - html_content += "{desc}
" - - if self.parser.file_type == "pdf": - # Access pdf_doc from parser if available - pdf_doc = getattr(self.parser, "pdf_doc", None) - page_count = len(pdf_doc) if pdf_doc else 0 - html_content += f"File type: PDF
Page count: {page_count}