From 9c997c96167404ae5241cafbf8b3484febab7dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Fri, 9 May 2025 19:34:32 +0300 Subject: [PATCH] Added `Save in a project folder with metadata` option, added `Go to folder` button --- CHANGELOG.md | 4 ++- abogen/book_handler.py | 27 +++++++++++++-- abogen/gui.py | 76 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee292f7..09318b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ - Prevent cancellation if process is at 99%, ensuring the process is not interrupted at the last moment. - Improved process handling for subpprocess calls, ensuring better management of subprocesses. - Improved PDF handling, ignoring empty pages/chapters and better chapter handling. -- Added `.opus` as output format for generated audio files. +- Added `Save in a project folder with metadata` option in the book handler, allowing users to save the converted items in a project folder with available metadata files. Useful if you want to work with the converted files in the future, issue mentioned by @Darthagnon in #15 +- Added `Go to folder` button in input box, allowing users to open the folder containing the converted file. +- Added `.opus` as output format for generated audio files, which is a more efficient format for audio files. - Added "Playing..." indicator for "Preview" button in the voice mixer. # v1.0.6 diff --git a/abogen/book_handler.py b/abogen/book_handler.py index 9f950f2..46ddd45 100644 --- a/abogen/book_handler.py +++ b/abogen/book_handler.py @@ -37,6 +37,7 @@ class HandlerDialog(QDialog): # Class variables to remember checkbox states between dialog instances _save_chapters_separately = False _merge_chapters_at_end = True + _save_as_project = False # New class variable for save_as_project option def __init__(self, book_path, file_type=None, checked_chapters=None, parent=None): super().__init__(parent) @@ -64,6 +65,7 @@ class HandlerDialog(QDialog): # Initialize save chapters flags from class variables self.save_chapters_separately = HandlerDialog._save_chapters_separately self.merge_chapters_at_end = HandlerDialog._merge_chapters_at_end + self.save_as_project = HandlerDialog._save_as_project # Load the book based on file type try: @@ -1099,7 +1101,7 @@ class HandlerDialog(QDialog): 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.', + '*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) @@ -1173,8 +1175,7 @@ class HandlerDialog(QDialog): 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) - + leftLayout.addWidget(self.save_chapters_checkbox) self.merge_chapters_checkbox = QCheckBox( "Create a merged version at the end", self ) @@ -1183,6 +1184,19 @@ class HandlerDialog(QDialog): 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) @@ -1696,6 +1710,10 @@ class HandlerDialog(QDialog): self.merge_chapters_at_end = bool(state) HandlerDialog._merge_chapters_at_end = self.merge_chapters_at_end + def on_save_as_project_changed(self, state): + self.save_as_project = bool(state) + HandlerDialog._save_as_project = self.save_as_project + def get_save_chapters_separately(self): return ( self.save_chapters_separately @@ -1706,6 +1724,9 @@ class HandlerDialog(QDialog): def get_merge_chapters_at_end(self): return self.merge_chapters_at_end + def get_save_as_project(self): + return self.save_as_project + def on_tree_context_menu(self, pos): item = self.treeWidget.itemAt(pos) if ( diff --git a/abogen/gui.py b/abogen/gui.py index d3452b7..2265a72 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -128,6 +128,13 @@ class InputBox(QLabel): self.edit_btn.clicked.connect(self.on_edit_clicked) self.edit_btn.hide() + # Add Go to folder button + self.go_to_folder_btn = QPushButton("Go to folder", self) + self.go_to_folder_btn.setStyleSheet("QPushButton { padding: 6px 10px; }") + self.go_to_folder_btn.setToolTip("Open the folder that contains the converted file") + self.go_to_folder_btn.clicked.connect(self.on_go_to_folder_clicked) + self.go_to_folder_btn.hide() + def resizeEvent(self, event): super().resizeEvent(event) margin = 12 @@ -138,6 +145,11 @@ class InputBox(QLabel): # Position textbox button at top left self.textbox_btn.move(margin, margin) self.edit_btn.move(margin, margin) + # Position go to folder button at bottom right + self.go_to_folder_btn.move( + self.width() - self.go_to_folder_btn.width(), + self.height() - self.go_to_folder_btn.height() - margin, + ) def set_file_info(self, file_path): # get icon without resizing using custom provider @@ -227,6 +239,7 @@ class InputBox(QLabel): should_show_edit = True self.edit_btn.setVisible(should_show_edit) + self.go_to_folder_btn.show() def set_error(self, message): self.setText(message) @@ -249,6 +262,7 @@ class InputBox(QLabel): # Show textbox and hide edit when input is cleared self.textbox_btn.show() self.edit_btn.hide() + self.go_to_folder_btn.hide() def _human_readable_size(self, size, decimal_places=2): for unit in ["B", "KB", "MB", "GB", "TB"]: @@ -319,6 +333,17 @@ class InputBox(QLabel): # For regular txt files win.open_textbox_dialog() + def on_go_to_folder_clicked(self): + win = self.window() + # win.selected_file holds the path to the text that is converted. + file_to_check = win.selected_file + + if file_to_check and os.path.exists(file_to_check) and os.path.isfile(file_to_check): + folder_path = os.path.dirname(file_to_check) + QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) + else: + QMessageBox.warning(win, "Error", "Converted file not found.") + class TextboxDialog(QDialog): def __init__(self, parent=None): @@ -861,9 +886,7 @@ class abogen(QWidget): ) dialog.setWindowModality(Qt.NonModal) dialog.setModal(False) - dialog.show() - - # We'll handle the dialog result asynchronously + dialog.show() # We'll handle the dialog result asynchronously def on_dialog_finished(result): if result != QDialog.Accepted: return False @@ -878,6 +901,7 @@ class abogen(QWidget): self.selected_chapters = all_checked_hrefs self.save_chapters_separately = dialog.get_save_chapters_separately() self.merge_chapters_at_end = dialog.get_merge_chapters_at_end() + self.save_as_project = dialog.get_save_as_project() # Store if the PDF has bookmarks for button text display if book_path.lower().endswith(".pdf"): @@ -886,8 +910,50 @@ class abogen(QWidget): # Use "abogen" prefix for temporary files # Extract base name without extension base_name = os.path.splitext(os.path.basename(book_path))[0] - temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME) - os.makedirs(temp_dir, exist_ok=True) + + if self.save_as_project: + # Get project directory from user + project_dir = QFileDialog.getExistingDirectory( + self, "Select Project Folder", "", QFileDialog.ShowDirsOnly + ) + if not project_dir: + # User cancelled, fallback to temp + self.save_as_project = False + temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME) + else: + # Create project folder structure + project_name = f"{base_name}_project" + project_dir = os.path.join(project_dir, project_name) + temp_dir = os.path.join(project_dir, "text") + os.makedirs(temp_dir, exist_ok=True) + + # Save metadata if available + meta_dir = os.path.join(project_dir, "metadata") + os.makedirs(meta_dir, exist_ok=True) # Save book metadata if available + if hasattr(dialog, "book_metadata"): + meta_path = os.path.join(meta_dir, "book_info.txt") + with open(meta_path, "w", encoding="utf-8") as f: + # Clean HTML tags from metadata + title = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('title', 'Unknown'))) + publisher = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publisher', 'Unknown'))) + authors = [re.sub(r'<[^>]+>', '', str(author)) for author in dialog.book_metadata.get('authors', ['Unknown'])] + + f.write(f"Title: {title}\n") + f.write(f"Authors: {', '.join(authors)}\n") + f.write(f"Publisher: {publisher}\n") + if dialog.book_metadata.get('description'): + description = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('description'))) + f.write(f"\nDescription:\n{description}\n") + + # Save cover image if available + if dialog.book_metadata.get("cover_image"): + cover_path = os.path.join(meta_dir, "cover.png") + with open(cover_path, "wb") as f: + f.write(dialog.book_metadata["cover_image"]) + else: + temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME) + os.makedirs(temp_dir, exist_ok=True) + fd, tmp = tempfile.mkstemp( prefix=f"abogen_{base_name}_", suffix=".txt", dir=temp_dir )