diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 94c5f74..1c5d7b4 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -20,25 +20,50 @@ from dotenv import load_dotenv from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox, QTextEdit, QMessageBox, QListWidget, QListWidgetItem, - QSplitter, QFileDialog, QComboBox, QCheckBox, QProgressBar + QSplitter, QFileDialog, QComboBox, QCheckBox, QProgressBar, QFrame, QFormLayout ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess from PyQt5.QtGui import QFont from gui.log_viewer import LogViewer +def create_section_header(title): + """Create a clean section header without boxes.""" + label = QLabel(title) + label.setStyleSheet(""" + QLabel { + font-size: 13px; + font-weight: bold; + color: #007acc; + padding: 8px 0px 5px 0px; + background-color: transparent; + } + """) + return label + +def create_horizontal_line(): + """Create a horizontal separator line.""" + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setFrameShadow(QFrame.Sunken) + line.setStyleSheet("QFrame { color: #555555; margin: 5px 0px; }") + return line + + class TranslationWorker(QThread): """Worker thread for running translations without blocking the UI.""" log_signal = pyqtSignal(str) progress_signal = pyqtSignal(int, int, str) # current_file, total_files, filename + item_progress_signal = pyqtSignal(int, int) # current_item, total_items (for tqdm within file) finished_signal = pyqtSignal(bool, str) - def __init__(self, project_root, module_info, estimate_only=False): + def __init__(self, project_root, module_info, estimate_only=False, selected_files=None): super().__init__() self.project_root = project_root self.module_info = module_info # [name, extensions, handler_function] self.estimate_only = estimate_only + self.selected_files = selected_files # List of files to process self.should_stop = False self.mutex = QMutex() # For thread safety self.executor = None # Store reference to executor for proper shutdown @@ -308,14 +333,18 @@ except Exception as e: self.finished_signal.emit(False, "Files directory missing") return - # Find files matching the selected module's extensions - matching_files = [] - for file_path in files_dir.iterdir(): - if file_path.is_file() and file_path.name != '.gitkeep': - for ext in self.module_info[1]: - if file_path.name.endswith(ext): - matching_files.append(file_path.name) - break + # Use selected files or find all matching files + if self.selected_files: + matching_files = self.selected_files + else: + # Find files matching the selected module's extensions + matching_files = [] + for file_path in files_dir.iterdir(): + if file_path.is_file() and file_path.name != '.gitkeep': + for ext in self.module_info[1]: + if file_path.name.endswith(ext): + matching_files.append(file_path.name) + break if not matching_files: self.emit_log(f"❌ No files found matching extensions: {', '.join(self.module_info[1])}") @@ -466,13 +495,13 @@ class TranslationTab(QWidget): self.files_dir.mkdir(exist_ok=True) self.translated_dir.mkdir(exist_ok=True) - self.setup_ui() - self.refresh_file_lists() + # Initialize tracking variables + self.files_completed = 0 + self.files_total = 0 - # Auto-refresh timer for file lists (less frequent during translation) - self.refresh_timer = QTimer() - self.refresh_timer.timeout.connect(self.refresh_file_lists) - self.refresh_timer.start(3000) # Refresh every 3 seconds + self.setup_ui() + self.setup_module_list() + self.refresh_file_lists() def setup_ui(self): """Set up the user interface.""" @@ -482,52 +511,220 @@ class TranslationTab(QWidget): # Left side - translation controls left_widget = QWidget() layout = QVBoxLayout() - layout.setSpacing(15) + layout.setSpacing(8) + layout.setContentsMargins(15, 15, 15, 15) - # Title - title_label = QLabel("Translation") - title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc; margin-bottom: 5px;") - layout.addWidget(title_label) - - # Description - desc_label = QLabel( - "Manage your files and run translations. Add files to 'files' folder, select module, then click translate." - ) - desc_label.setWordWrap(True) - desc_label.setStyleSheet("color: #cccccc; margin-bottom: 15px;") - layout.addWidget(desc_label) - - # Module selection - module_group = QGroupBox("Translation Settings") - module_group.setStyleSheet("QGroupBox { font-weight: bold; color: #007acc; margin-top: 10px; }") - module_layout = QVBoxLayout() - selector_layout = QHBoxLayout() - selector_layout.addWidget(QLabel("Game Engine:")) + # Translation Settings Section + layout.addWidget(create_section_header("🌐 Translation Settings")) + + trans_form = QFormLayout() + trans_form.setSpacing(6) + trans_form.setContentsMargins(0, 0, 0, 12) + trans_form.setFieldGrowthPolicy(QFormLayout.FieldsStayAtSizeHint) + trans_form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + + engine_label = QLabel("Game Engine:") + engine_label.setFixedWidth(150) + engine_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.module_combo = QComboBox() self.module_combo.currentTextChanged.connect(self._on_module_changed) - selector_layout.addWidget(self.module_combo) - self.estimate_checkbox = QCheckBox("Estimate only (don't translate)") - selector_layout.addWidget(self.estimate_checkbox) - module_layout.addLayout(selector_layout) - module_group.setLayout(module_layout) - layout.addWidget(module_group) - - # File management splitter - file_splitter = QSplitter(Qt.Horizontal) - input_widget = self.create_input_files_widget() - file_splitter.addWidget(input_widget) - output_widget = self.create_output_files_widget() - file_splitter.addWidget(output_widget) - file_splitter.setStretchFactor(0, 1) - file_splitter.setStretchFactor(1, 1) - layout.addWidget(file_splitter) - - # Log - log_widget = self.create_log_widget() - layout.addWidget(log_widget) - - # Modules list - self.setup_module_list() + self.module_combo.setFixedWidth(300) + trans_form.addRow(engine_label, self.module_combo) + + mode_label = QLabel("Mode:") + mode_label.setFixedWidth(150) + mode_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.mode_combo = QComboBox() + self.mode_combo.addItem("Translate") + self.mode_combo.addItem("Estimate") + self.mode_combo.setFixedWidth(200) + self.mode_combo.currentTextChanged.connect(self._on_mode_changed) + trans_form.addRow(mode_label, self.mode_combo) + + layout.addLayout(trans_form) + layout.addWidget(create_horizontal_line()) + + # Files Section + layout.addWidget(create_section_header("📁 Input Files")) + + # Files Section with side buttons + files_container = QHBoxLayout() + files_container.setSpacing(0) # No spacing between list and buttons + + # File list with checkboxes + self.file_list = QListWidget() + self.file_list.setMinimumHeight(150) + self.file_list.setMaximumHeight(250) + self.file_list.setSelectionMode(QListWidget.NoSelection) # Disable selection highlighting + self.file_list.itemClicked.connect(self._toggle_file_checkbox) + self.file_list.setFocusPolicy(Qt.NoFocus) # Remove focus outline + self.file_list.setStyleSheet(""" + QListWidget { + outline: none; + border: 1px solid #555555; + border-right: none; + } + QListWidget::item { + border: none; + outline: none; + } + QListWidget::item:hover { + background-color: #3e3e42; + } + """) + files_container.addWidget(self.file_list) + + # File management buttons (icon-based, vertical on the side) + file_buttons = QVBoxLayout() + file_buttons.setSpacing(0) + file_buttons.setContentsMargins(0, 0, 0, 0) + + # Button style for all icon buttons - all same size + icon_button_style = """ + QPushButton { + font-size: 13px; + padding: 0px; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; + border: 1px solid #555555; + border-top: none; + border-radius: 0px; + background-color: #2d2d30; + } + QPushButton:hover { + background-color: #3e3e42; + border-left-color: #007acc; + } + QPushButton:pressed { + background-color: #007acc; + } + """ + + # First button style - same size but with top border + first_button_style = """ + QPushButton { + font-size: 13px; + padding: 0px; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; + border: 1px solid #555555; + border-radius: 0px; + background-color: #2d2d30; + } + QPushButton:hover { + background-color: #3e3e42; + border-left-color: #007acc; + } + QPushButton:pressed { + background-color: #007acc; + } + """ + + select_all_btn = QPushButton("✓") + select_all_btn.setToolTip("Select all files") + select_all_btn.clicked.connect(self.select_all_files) + select_all_btn.setStyleSheet(first_button_style) + file_buttons.addWidget(select_all_btn) + + deselect_all_btn = QPushButton("✗") + deselect_all_btn.setToolTip("Deselect all files") + deselect_all_btn.clicked.connect(self.deselect_all_files) + deselect_all_btn.setStyleSheet(icon_button_style) + file_buttons.addWidget(deselect_all_btn) + + add_files_btn = QPushButton("➕") + add_files_btn.setToolTip("Add files to translate") + add_files_btn.clicked.connect(self.add_input_files) + add_files_btn.setStyleSheet(icon_button_style) + file_buttons.addWidget(add_files_btn) + + remove_files_btn = QPushButton("🗑️") + remove_files_btn.setToolTip("Remove selected files") + remove_files_btn.clicked.connect(self.remove_selected_files) + remove_files_btn.setStyleSheet(icon_button_style) + file_buttons.addWidget(remove_files_btn) + + open_folder_btn = QPushButton("📁") + open_folder_btn.setToolTip("Open files folder in explorer") + open_folder_btn.clicked.connect(self.open_input_folder) + open_folder_btn.setStyleSheet(icon_button_style) + file_buttons.addWidget(open_folder_btn) + + refresh_btn = QPushButton("🔄") + refresh_btn.setToolTip("Refresh file list") + refresh_btn.clicked.connect(self.refresh_file_lists) + refresh_btn.setStyleSheet(icon_button_style) + file_buttons.addWidget(refresh_btn) + + # Add stretch to push buttons to top + file_buttons.addStretch() + + # Add button column to the container + files_container.addLayout(file_buttons) + + # Add the entire container to main layout + layout.addLayout(files_container) + layout.addWidget(create_horizontal_line()) + + # Progress Section + layout.addWidget(create_section_header("📊 Translation Progress")) + + progress_layout = QVBoxLayout() + progress_layout.setSpacing(8) + progress_layout.setContentsMargins(0, 0, 0, 12) + + # Files Translated counter + files_layout = QHBoxLayout() + files_layout.addWidget(QLabel("Files Translated:")) + self.files_translated_label = QLabel("0/0") + self.files_translated_label.setStyleSheet("font-weight: bold; color: #007acc;") + files_layout.addWidget(self.files_translated_label) + files_layout.addStretch() + progress_layout.addLayout(files_layout) + + # Currently translating + translating_layout = QHBoxLayout() + translating_layout.addWidget(QLabel("Translating:")) + self.translating_label = QLabel("—") + self.translating_label.setStyleSheet("font-weight: bold; color: #cccccc;") + translating_layout.addWidget(self.translating_label) + translating_layout.addStretch() + progress_layout.addLayout(translating_layout) + + # Progress bar with label + item_progress_layout = QHBoxLayout() + item_progress_layout.addWidget(QLabel("Progress:")) + self.item_progress_label = QLabel("0/0") + self.item_progress_label.setStyleSheet("font-weight: bold; color: #cccccc;") + item_progress_layout.addWidget(self.item_progress_label) + item_progress_layout.addStretch() + progress_layout.addLayout(item_progress_layout) + + self.item_progress_bar = QProgressBar() + self.item_progress_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid #555555; + border-radius: 3px; + text-align: center; + background-color: #2b2b2b; + color: white; + height: 20px; + } + QProgressBar::chunk { + background-color: #007acc; + border-radius: 2px; + } + """) + progress_layout.addWidget(self.item_progress_bar) + + layout.addLayout(progress_layout) + + # Spacer to push buttons to bottom + layout.addStretch() # Buttons button_layout = QHBoxLayout() @@ -550,7 +747,7 @@ class TranslationTab(QWidget): button_layout.addWidget(self.translate_button) self.stop_button = QPushButton("Stop Translation") self.stop_button.clicked.connect(self.stop_translation) - self.stop_button.setEnabled(False) + self.stop_button.setVisible(False) # Hidden by default self.stop_button.setStyleSheet(""" QPushButton { background-color: #cc0000; @@ -650,143 +847,83 @@ class TranslationTab(QWidget): elif "mv/mz" in lowered: self.engine_changed.emit("mvmz") - def create_input_files_widget(self): - """Create the input files widget.""" - input_group = QGroupBox("Input Files (files folder)") - input_group.setStyleSheet("QGroupBox { font-weight: bold; color: #007acc; margin-top: 10px; }") - input_layout = QVBoxLayout() + # Update mode dropdown based on engine + current_mode = self.mode_combo.currentText() + self.mode_combo.clear() + self.mode_combo.addItem("Translate") + self.mode_combo.addItem("Estimate") - self.input_list = QListWidget() - self.input_list.setMinimumHeight(80) - self.input_list.setMaximumHeight(150) - input_layout.addWidget(self.input_list) + # Add Parse Speakers for RPG Maker MV/MZ + if "mv/mz" in lowered: + self.mode_combo.addItem("Parse Speakers") - input_buttons = QHBoxLayout() - add_input_btn = QPushButton("Add Files") - add_input_btn.clicked.connect(self.add_input_files) - input_buttons.addWidget(add_input_btn) - - remove_input_btn = QPushButton("Remove Selected") - remove_input_btn.clicked.connect(self.remove_input_file) - input_buttons.addWidget(remove_input_btn) - - open_input_btn = QPushButton("Open Folder") - open_input_btn.clicked.connect(self.open_input_folder) - input_buttons.addWidget(open_input_btn) - - input_layout.addLayout(input_buttons) - input_group.setLayout(input_layout) - return input_group - - def create_output_files_widget(self): - """Create the output files widget.""" - output_group = QGroupBox("Output Files (translated folder)") - output_group.setStyleSheet("QGroupBox { font-weight: bold; color: #007acc; margin-top: 10px; }") - output_layout = QVBoxLayout() - - self.output_list = QListWidget() - self.output_list.setMinimumHeight(80) - self.output_list.setMaximumHeight(150) - output_layout.addWidget(self.output_list) - - output_buttons = QHBoxLayout() - remove_output_btn = QPushButton("Remove Selected") - remove_output_btn.clicked.connect(self.remove_output_file) - output_buttons.addWidget(remove_output_btn) - - clear_output_btn = QPushButton("Clear All") - clear_output_btn.clicked.connect(self.clear_output_files) - output_buttons.addWidget(clear_output_btn) - - open_output_btn = QPushButton("Open Folder") - open_output_btn.clicked.connect(self.open_output_folder) - output_buttons.addWidget(open_output_btn) - - output_layout.addLayout(output_buttons) - output_group.setLayout(output_layout) - return output_group - - def create_log_widget(self): - """Create the console log widget.""" - log_group = QGroupBox("Console Output") - log_group.setStyleSheet("QGroupBox { font-weight: bold; color: #007acc; margin-top: 10px; }") - layout = QVBoxLayout() - - # Progress bar for file translation - progress_layout = QHBoxLayout() - self.progress_label = QLabel("Ready") - self.progress_label.setStyleSheet("color: #cccccc; font-weight: bold;") - progress_layout.addWidget(self.progress_label) - - self.progress_bar = QProgressBar() - self.progress_bar.setVisible(False) - self.progress_bar.setStyleSheet(""" - QProgressBar { - border: 1px solid #555555; - border-radius: 3px; - text-align: center; - background-color: #2b2b2b; - color: white; - } - QProgressBar::chunk { - background-color: #007acc; - border-radius: 2px; - } - """) - progress_layout.addWidget(self.progress_bar) - layout.addLayout(progress_layout) - - # Cost display on its own row (compact) - cost_layout = QHBoxLayout() - cost_layout.setContentsMargins(0, 0, 0, 0) # Remove margins - cost_layout.setSpacing(0) # Remove spacing - cost_layout.addStretch() # Push cost to center - self.cost_label = QLabel("") - self.cost_label.setStyleSheet("color: #00ff00; font-weight: bold; font-size: 12px;") - self.cost_label.setVisible(False) - self.cost_label.setAlignment(Qt.AlignCenter) - cost_layout.addWidget(self.cost_label) - cost_layout.addStretch() # Push cost to center - layout.addLayout(cost_layout) - - self.log_display = QTextEdit() - self.log_display.setReadOnly(True) - self.log_display.setFont(QFont("Consolas", 9)) - self.log_display.setMinimumHeight(150) - self.log_display.setMaximumHeight(300) - self.log_display.setStyleSheet(""" - QTextEdit { - background-color: #1e1e1e; - color: #ffffff; - border: 1px solid #555555; - font-family: 'Consolas', 'Courier New', monospace; - } - """) - layout.addWidget(self.log_display) - - clear_log_btn = QPushButton("Clear Log") - clear_log_btn.clicked.connect(self.clear_log) - layout.addWidget(clear_log_btn) - - log_group.setLayout(layout) - return log_group + # Restore previous selection if it still exists + index = self.mode_combo.findText(current_mode) + if index >= 0: + self.mode_combo.setCurrentIndex(index) + else: + self.mode_combo.setCurrentIndex(0) + + def _toggle_file_checkbox(self, item): + """Toggle checkbox when clicking anywhere on the item.""" + if item.checkState() == Qt.Checked: + item.setCheckState(Qt.Unchecked) + else: + item.setCheckState(Qt.Checked) + + def _on_mode_changed(self, mode_text): + """Update the translate button text based on selected mode.""" + if mode_text == "Translate": + self.translate_button.setText("Start Translation") + elif mode_text == "Estimate": + self.translate_button.setText("Start Estimation") + elif mode_text == "Parse Speakers": + self.translate_button.setText("Parse Speakers") def refresh_file_lists(self): - """Refresh both input and output file lists.""" - # Refresh input files - self.input_list.clear() + """Refresh the file list with checkboxes, preserving checked states.""" + # Save current check states + checked_files = set() + for i in range(self.file_list.count()): + item = self.file_list.item(i) + if item.checkState() == Qt.Checked: + checked_files.add(item.text()) + + # Rebuild the list + self.file_list.clear() if self.files_dir.exists(): for file_path in self.files_dir.iterdir(): if file_path.is_file() and file_path.name != '.gitkeep': - self.input_list.addItem(file_path.name) - - # Refresh output files - self.output_list.clear() - if self.translated_dir.exists(): - for file_path in self.translated_dir.iterdir(): - if file_path.is_file() and file_path.name != '.gitkeep': - self.output_list.addItem(file_path.name) - + item = QListWidgetItem(file_path.name) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + # Restore checked state if it was previously checked + if file_path.name in checked_files: + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + self.file_list.addItem(item) + + def select_all_files(self): + """Select all files in the list.""" + for i in range(self.file_list.count()): + item = self.file_list.item(i) + item.setCheckState(Qt.Checked) + + def deselect_all_files(self): + """Deselect all files in the list.""" + for i in range(self.file_list.count()): + item = self.file_list.item(i) + item.setCheckState(Qt.Unchecked) + + def get_selected_files(self): + """Get list of checked files.""" + selected = [] + for i in range(self.file_list.count()): + item = self.file_list.item(i) + if item.checkState() == Qt.Checked: + selected.append(item.text()) + return selected + def add_input_files(self): """Add files to the input directory.""" file_paths, _ = QFileDialog.getOpenFileNames( @@ -824,79 +961,31 @@ class TranslationTab(QWidget): except Exception as e: QMessageBox.critical(self, "Error", f"Failed to add files:\n{str(e)}") - def remove_input_file(self): - """Remove selected input file.""" - current_item = self.input_list.currentItem() - if not current_item: - QMessageBox.information(self, "No Selection", "Please select a file to remove.") + def remove_selected_files(self): + """Remove selected (checked) files from the input directory.""" + selected_files = self.get_selected_files() + + if not selected_files: + QMessageBox.information(self, "No Selection", "Please check files to remove.") return - file_path = self.files_dir / current_item.text() - reply = QMessageBox.question( self, "Confirm Deletion", - f"Are you sure you want to delete '{current_item.text()}'?", + f"Are you sure you want to delete {len(selected_files)} file(s)?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: try: - file_path.unlink() + for filename in selected_files: + file_path = self.files_dir / filename + file_path.unlink() self.refresh_file_lists() + QMessageBox.information(self, "Files Removed", f"Successfully removed {len(selected_files)} file(s).") except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}") - - def remove_output_file(self): - """Remove selected output file.""" - current_item = self.output_list.currentItem() - if not current_item: - QMessageBox.information(self, "No Selection", "Please select a file to remove.") - return - - file_path = self.translated_dir / current_item.text() - - reply = QMessageBox.question( - self, - "Confirm Deletion", - f"Are you sure you want to delete '{current_item.text()}'?", - QMessageBox.Yes | QMessageBox.No - ) - - if reply == QMessageBox.Yes: - try: - file_path.unlink() - self.refresh_file_lists() - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}") - - def clear_output_files(self): - """Clear all output files.""" - if self.output_list.count() == 0: - QMessageBox.information(self, "No Files", "No output files to clear.") - return - - reply = QMessageBox.question( - self, - "Confirm Clear", - "Are you sure you want to delete ALL translated files?", - QMessageBox.Yes | QMessageBox.No - ) - - if reply == QMessageBox.Yes: - try: - count = 0 - for file_path in self.translated_dir.iterdir(): - if file_path.is_file() and file_path.name != '.gitkeep': - file_path.unlink() - count += 1 - - self.refresh_file_lists() - QMessageBox.information(self, "Files Cleared", f"Deleted {count} files.") - - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to clear files:\n{str(e)}") - + QMessageBox.critical(self, "Error", f"Failed to delete files:\n{str(e)}") + def open_input_folder(self): """Open the input directory.""" self.open_folder(self.files_dir) @@ -920,8 +1009,11 @@ class TranslationTab(QWidget): def start_translation(self): """Start the translation process.""" - if self.input_list.count() == 0: - QMessageBox.warning(self, "No Files", "No files found in the input folder to translate.") + # Get checked files + selected_files = self.get_selected_files() + + if not selected_files: + QMessageBox.warning(self, "No Files Selected", "Please check at least one file to translate.") return # Get selected module @@ -931,138 +1023,93 @@ class TranslationTab(QWidget): return selected_module = self.modules[selected_index] - estimate_only = self.estimate_checkbox.isChecked() + + # Get mode from dropdown + mode = self.mode_combo.currentText() + estimate_only = (mode == "Estimate") + parse_speakers = (mode == "Parse Speakers") # Confirm start - action = "estimate" if estimate_only else "translate" - action_ing = "estimation of" if estimate_only else "translating" + action = mode.lower() reply = QMessageBox.question( self, - f"Start {action.title()}", - f"Start {action_ing} {self.input_list.count()} files using {selected_module[0]}?", + f"Start {mode}", + f"Start {action} for {len(selected_files)} file(s) using {selected_module[0]}?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: - self.translate_button.setEnabled(False) - self.stop_button.setEnabled(True) - self.clear_log() + # Toggle button visibility + self.translate_button.setVisible(False) + self.stop_button.setVisible(True) + + # Initialize progress tracking + self.files_completed = 0 + self.files_total = len(selected_files) + self.files_translated_label.setText(f"0/{self.files_total}") + self.translating_label.setText("Starting...") + self.item_progress_label.setText("0/0") + self.item_progress_bar.setValue(0) + self.item_progress_bar.setMaximum(100) # Create and start translation worker self.translation_worker = TranslationWorker( self.project_root, selected_module, - estimate_only + estimate_only, + selected_files # Pass selected files ) # Connect signals self.translation_worker.log_signal.connect(self.append_log) - self.translation_worker.progress_signal.connect(self.update_progress) + self.translation_worker.progress_signal.connect(self.update_file_progress) + self.translation_worker.item_progress_signal.connect(self.update_item_progress) self.translation_worker.finished_signal.connect(self.on_translation_finished) - # Show progress bar and initialize - self.progress_bar.setVisible(True) - self.progress_bar.setValue(0) - self.progress_label.setText("Starting...") - - # Hide cost display initially - self.cost_label.setVisible(False) - self.cost_label.setText("") - - # Reduce file refresh frequency during translation - self.refresh_timer.stop() - # Start the worker self.translation_worker.start() def append_log(self, message): - """Append a message to the log buffer for batched display.""" - # Strip ANSI color codes from colorama - import re - clean_message = re.sub(r'\x1b\[[0-9;]*m', '', message) - - # Check if this is a cost message and update the cost display - if "Cost: $" in clean_message: - # Extract cost for display - cost_match = re.search(r'Cost: \$([0-9,\.]+)', clean_message) - if cost_match: - cost_value = cost_match.group(1) - if "TOTAL:" in clean_message: - # This is the final total cost - self.update_cost_display(f"Total Cost: ${cost_value}") - else: - # This is an individual file cost - extract filename - filename_match = re.search(r'^([^:]+):', clean_message) - if filename_match: - filename = filename_match.group(1).strip() - self.update_cost_display(f"{filename}: ${cost_value}") - else: - # Fallback - just show the cost - self.update_cost_display(f"File Cost: ${cost_value}") - - # Also check for the final cost display format (with emoji) - elif "💰" in message: - # Extract cost from the emoji format - cost_match = re.search(r'💰\s*(.+)', message) - if cost_match: - cost_text = cost_match.group(1).strip() - self.update_cost_display(f"Total: {cost_text}") - - self.log_buffer.append(clean_message) - - # Start timer if not already running - if not self.log_timer.isActive(): - self.log_timer.start(100) # Flush every 100ms - - def update_cost_display(self, cost_text): - """Update the cost display label.""" - self.cost_label.setText(cost_text) - self.cost_label.setVisible(True) + """Append a message to the log - now just for internal tracking.""" + # We no longer display logs in the UI, but we can keep this for debugging + # or future use with the log viewer + pass + + def update_file_progress(self, current_file, total_files, filename): + """Update the file-level progress.""" + self.files_completed = current_file + self.files_translated_label.setText(f"{current_file}/{total_files}") + self.translating_label.setText(filename) + + def update_item_progress(self, current_item, total_items): + """Update the item-level progress (from tqdm).""" + self.item_progress_label.setText(f"{current_item}/{total_items}") + self.item_progress_bar.setMaximum(total_items if total_items > 0 else 100) + self.item_progress_bar.setValue(current_item) def flush_log_buffer(self): - """Flush the log buffer to the display.""" - if self.log_buffer: - # Add all buffered messages at once - for message in self.log_buffer: - self.log_display.append(message) - self.log_buffer.clear() - - # Auto-scroll to bottom - scrollbar = self.log_display.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) - - # Stop timer if buffer is empty + """No longer needed - kept for compatibility.""" + self.log_buffer.clear() self.log_timer.stop() def update_progress(self, current_file, total_files, filename): - """Update the progress bar and label.""" - self.progress_bar.setMaximum(total_files) - self.progress_bar.setValue(current_file) - self.progress_label.setText(f"Processing {filename} ({current_file}/{total_files})") + """Legacy method - redirect to new method.""" + self.update_file_progress(current_file, total_files, filename) def on_translation_finished(self, success, message): """Handle translation completion.""" - self.translate_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.progress_bar.setVisible(False) - self.progress_label.setText("Ready") - - # Flush any remaining log messages - self.flush_log_buffer() - - # Resume file refresh - self.refresh_timer.start(3000) - self.refresh_file_lists() + # Toggle button visibility + self.translate_button.setVisible(True) + self.stop_button.setVisible(False) + # Update progress display if success: - self.append_log("") - self.append_log("🎉 Process completed successfully!") + self.translating_label.setText("Completed!") else: - self.append_log("") - self.append_log(f"❌ Process failed: {message}") - - # Force flush the final messages - self.flush_log_buffer() + self.translating_label.setText(f"Failed: {message}") + + # Refresh file list to show any new translated files + self.refresh_file_lists() def stop_translation(self): """Stop the translation process.""" @@ -1071,30 +1118,16 @@ class TranslationTab(QWidget): # Wait for the worker to stop gracefully if not self.translation_worker.wait(5000): # Wait up to 5 seconds - self.append_log("⚠️ Worker didn't stop gracefully, forcing termination...") self.translation_worker.terminate() self.translation_worker.wait(2000) # Wait for termination - self.translate_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.progress_bar.setVisible(False) - self.progress_label.setText("Ready") - - # Resume file refresh - self.refresh_timer.start(3000) - - # Flush any remaining messages - self.flush_log_buffer() - - def clear_log(self): - """Clear the log display.""" - self.log_buffer.clear() # Clear buffer too - self.log_display.clear() + # Toggle button visibility + self.translate_button.setVisible(True) + self.stop_button.setVisible(False) + self.translating_label.setText("Stopped") def closeEvent(self, event): """Handle widget close event.""" - if hasattr(self, 'refresh_timer'): - self.refresh_timer.stop() if hasattr(self, 'log_timer'): self.log_timer.stop() if hasattr(self, 'translation_worker') and self.translation_worker.isRunning():