icon glory
This commit is contained in:
parent
d870a395fd
commit
ff5aefc82f
1 changed files with 402 additions and 369 deletions
|
|
@ -20,25 +20,50 @@ from dotenv import load_dotenv
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox,
|
||||||
QTextEdit, QMessageBox, QListWidget, QListWidgetItem,
|
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.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from gui.log_viewer import LogViewer
|
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):
|
class TranslationWorker(QThread):
|
||||||
"""Worker thread for running translations without blocking the UI."""
|
"""Worker thread for running translations without blocking the UI."""
|
||||||
|
|
||||||
log_signal = pyqtSignal(str)
|
log_signal = pyqtSignal(str)
|
||||||
progress_signal = pyqtSignal(int, int, str) # current_file, total_files, filename
|
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)
|
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__()
|
super().__init__()
|
||||||
self.project_root = project_root
|
self.project_root = project_root
|
||||||
self.module_info = module_info # [name, extensions, handler_function]
|
self.module_info = module_info # [name, extensions, handler_function]
|
||||||
self.estimate_only = estimate_only
|
self.estimate_only = estimate_only
|
||||||
|
self.selected_files = selected_files # List of files to process
|
||||||
self.should_stop = False
|
self.should_stop = False
|
||||||
self.mutex = QMutex() # For thread safety
|
self.mutex = QMutex() # For thread safety
|
||||||
self.executor = None # Store reference to executor for proper shutdown
|
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")
|
self.finished_signal.emit(False, "Files directory missing")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find files matching the selected module's extensions
|
# Use selected files or find all matching files
|
||||||
matching_files = []
|
if self.selected_files:
|
||||||
for file_path in files_dir.iterdir():
|
matching_files = self.selected_files
|
||||||
if file_path.is_file() and file_path.name != '.gitkeep':
|
else:
|
||||||
for ext in self.module_info[1]:
|
# Find files matching the selected module's extensions
|
||||||
if file_path.name.endswith(ext):
|
matching_files = []
|
||||||
matching_files.append(file_path.name)
|
for file_path in files_dir.iterdir():
|
||||||
break
|
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:
|
if not matching_files:
|
||||||
self.emit_log(f"❌ No files found matching extensions: {', '.join(self.module_info[1])}")
|
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.files_dir.mkdir(exist_ok=True)
|
||||||
self.translated_dir.mkdir(exist_ok=True)
|
self.translated_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
self.setup_ui()
|
# Initialize tracking variables
|
||||||
self.refresh_file_lists()
|
self.files_completed = 0
|
||||||
|
self.files_total = 0
|
||||||
|
|
||||||
# Auto-refresh timer for file lists (less frequent during translation)
|
self.setup_ui()
|
||||||
self.refresh_timer = QTimer()
|
self.setup_module_list()
|
||||||
self.refresh_timer.timeout.connect(self.refresh_file_lists)
|
self.refresh_file_lists()
|
||||||
self.refresh_timer.start(3000) # Refresh every 3 seconds
|
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Set up the user interface."""
|
"""Set up the user interface."""
|
||||||
|
|
@ -482,52 +511,220 @@ class TranslationTab(QWidget):
|
||||||
# Left side - translation controls
|
# Left side - translation controls
|
||||||
left_widget = QWidget()
|
left_widget = QWidget()
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setSpacing(15)
|
layout.setSpacing(8)
|
||||||
|
layout.setContentsMargins(15, 15, 15, 15)
|
||||||
|
|
||||||
# Title
|
# Translation Settings Section
|
||||||
title_label = QLabel("Translation")
|
layout.addWidget(create_section_header("🌐 Translation Settings"))
|
||||||
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc; margin-bottom: 5px;")
|
|
||||||
layout.addWidget(title_label)
|
|
||||||
|
|
||||||
# Description
|
trans_form = QFormLayout()
|
||||||
desc_label = QLabel(
|
trans_form.setSpacing(6)
|
||||||
"Manage your files and run translations. Add files to 'files' folder, select module, then click translate."
|
trans_form.setContentsMargins(0, 0, 0, 12)
|
||||||
)
|
trans_form.setFieldGrowthPolicy(QFormLayout.FieldsStayAtSizeHint)
|
||||||
desc_label.setWordWrap(True)
|
trans_form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
desc_label.setStyleSheet("color: #cccccc; margin-bottom: 15px;")
|
|
||||||
layout.addWidget(desc_label)
|
|
||||||
|
|
||||||
# Module selection
|
engine_label = QLabel("Game Engine:")
|
||||||
module_group = QGroupBox("Translation Settings")
|
engine_label.setFixedWidth(150)
|
||||||
module_group.setStyleSheet("QGroupBox { font-weight: bold; color: #007acc; margin-top: 10px; }")
|
engine_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
module_layout = QVBoxLayout()
|
|
||||||
selector_layout = QHBoxLayout()
|
|
||||||
selector_layout.addWidget(QLabel("Game Engine:"))
|
|
||||||
self.module_combo = QComboBox()
|
self.module_combo = QComboBox()
|
||||||
self.module_combo.currentTextChanged.connect(self._on_module_changed)
|
self.module_combo.currentTextChanged.connect(self._on_module_changed)
|
||||||
selector_layout.addWidget(self.module_combo)
|
self.module_combo.setFixedWidth(300)
|
||||||
self.estimate_checkbox = QCheckBox("Estimate only (don't translate)")
|
trans_form.addRow(engine_label, self.module_combo)
|
||||||
selector_layout.addWidget(self.estimate_checkbox)
|
|
||||||
module_layout.addLayout(selector_layout)
|
|
||||||
module_group.setLayout(module_layout)
|
|
||||||
layout.addWidget(module_group)
|
|
||||||
|
|
||||||
# File management splitter
|
mode_label = QLabel("Mode:")
|
||||||
file_splitter = QSplitter(Qt.Horizontal)
|
mode_label.setFixedWidth(150)
|
||||||
input_widget = self.create_input_files_widget()
|
mode_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
file_splitter.addWidget(input_widget)
|
self.mode_combo = QComboBox()
|
||||||
output_widget = self.create_output_files_widget()
|
self.mode_combo.addItem("Translate")
|
||||||
file_splitter.addWidget(output_widget)
|
self.mode_combo.addItem("Estimate")
|
||||||
file_splitter.setStretchFactor(0, 1)
|
self.mode_combo.setFixedWidth(200)
|
||||||
file_splitter.setStretchFactor(1, 1)
|
self.mode_combo.currentTextChanged.connect(self._on_mode_changed)
|
||||||
layout.addWidget(file_splitter)
|
trans_form.addRow(mode_label, self.mode_combo)
|
||||||
|
|
||||||
# Log
|
layout.addLayout(trans_form)
|
||||||
log_widget = self.create_log_widget()
|
layout.addWidget(create_horizontal_line())
|
||||||
layout.addWidget(log_widget)
|
|
||||||
|
|
||||||
# Modules list
|
# Files Section
|
||||||
self.setup_module_list()
|
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
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
|
|
@ -550,7 +747,7 @@ class TranslationTab(QWidget):
|
||||||
button_layout.addWidget(self.translate_button)
|
button_layout.addWidget(self.translate_button)
|
||||||
self.stop_button = QPushButton("Stop Translation")
|
self.stop_button = QPushButton("Stop Translation")
|
||||||
self.stop_button.clicked.connect(self.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("""
|
self.stop_button.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #cc0000;
|
background-color: #cc0000;
|
||||||
|
|
@ -650,142 +847,82 @@ class TranslationTab(QWidget):
|
||||||
elif "mv/mz" in lowered:
|
elif "mv/mz" in lowered:
|
||||||
self.engine_changed.emit("mvmz")
|
self.engine_changed.emit("mvmz")
|
||||||
|
|
||||||
def create_input_files_widget(self):
|
# Update mode dropdown based on engine
|
||||||
"""Create the input files widget."""
|
current_mode = self.mode_combo.currentText()
|
||||||
input_group = QGroupBox("Input Files (files folder)")
|
self.mode_combo.clear()
|
||||||
input_group.setStyleSheet("QGroupBox { font-weight: bold; color: #007acc; margin-top: 10px; }")
|
self.mode_combo.addItem("Translate")
|
||||||
input_layout = QVBoxLayout()
|
self.mode_combo.addItem("Estimate")
|
||||||
|
|
||||||
self.input_list = QListWidget()
|
# Add Parse Speakers for RPG Maker MV/MZ
|
||||||
self.input_list.setMinimumHeight(80)
|
if "mv/mz" in lowered:
|
||||||
self.input_list.setMaximumHeight(150)
|
self.mode_combo.addItem("Parse Speakers")
|
||||||
input_layout.addWidget(self.input_list)
|
|
||||||
|
|
||||||
input_buttons = QHBoxLayout()
|
# Restore previous selection if it still exists
|
||||||
add_input_btn = QPushButton("Add Files")
|
index = self.mode_combo.findText(current_mode)
|
||||||
add_input_btn.clicked.connect(self.add_input_files)
|
if index >= 0:
|
||||||
input_buttons.addWidget(add_input_btn)
|
self.mode_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
self.mode_combo.setCurrentIndex(0)
|
||||||
|
|
||||||
remove_input_btn = QPushButton("Remove Selected")
|
def _toggle_file_checkbox(self, item):
|
||||||
remove_input_btn.clicked.connect(self.remove_input_file)
|
"""Toggle checkbox when clicking anywhere on the item."""
|
||||||
input_buttons.addWidget(remove_input_btn)
|
if item.checkState() == Qt.Checked:
|
||||||
|
item.setCheckState(Qt.Unchecked)
|
||||||
|
else:
|
||||||
|
item.setCheckState(Qt.Checked)
|
||||||
|
|
||||||
open_input_btn = QPushButton("Open Folder")
|
def _on_mode_changed(self, mode_text):
|
||||||
open_input_btn.clicked.connect(self.open_input_folder)
|
"""Update the translate button text based on selected mode."""
|
||||||
input_buttons.addWidget(open_input_btn)
|
if mode_text == "Translate":
|
||||||
|
self.translate_button.setText("Start Translation")
|
||||||
input_layout.addLayout(input_buttons)
|
elif mode_text == "Estimate":
|
||||||
input_group.setLayout(input_layout)
|
self.translate_button.setText("Start Estimation")
|
||||||
return input_group
|
elif mode_text == "Parse Speakers":
|
||||||
|
self.translate_button.setText("Parse Speakers")
|
||||||
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
|
|
||||||
|
|
||||||
def refresh_file_lists(self):
|
def refresh_file_lists(self):
|
||||||
"""Refresh both input and output file lists."""
|
"""Refresh the file list with checkboxes, preserving checked states."""
|
||||||
# Refresh input files
|
# Save current check states
|
||||||
self.input_list.clear()
|
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():
|
if self.files_dir.exists():
|
||||||
for file_path in self.files_dir.iterdir():
|
for file_path in self.files_dir.iterdir():
|
||||||
if file_path.is_file() and file_path.name != '.gitkeep':
|
if file_path.is_file() and file_path.name != '.gitkeep':
|
||||||
self.input_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)
|
||||||
|
|
||||||
# Refresh output files
|
def select_all_files(self):
|
||||||
self.output_list.clear()
|
"""Select all files in the list."""
|
||||||
if self.translated_dir.exists():
|
for i in range(self.file_list.count()):
|
||||||
for file_path in self.translated_dir.iterdir():
|
item = self.file_list.item(i)
|
||||||
if file_path.is_file() and file_path.name != '.gitkeep':
|
item.setCheckState(Qt.Checked)
|
||||||
self.output_list.addItem(file_path.name)
|
|
||||||
|
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):
|
def add_input_files(self):
|
||||||
"""Add files to the input directory."""
|
"""Add files to the input directory."""
|
||||||
|
|
@ -824,78 +961,30 @@ class TranslationTab(QWidget):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to add files:\n{str(e)}")
|
QMessageBox.critical(self, "Error", f"Failed to add files:\n{str(e)}")
|
||||||
|
|
||||||
def remove_input_file(self):
|
def remove_selected_files(self):
|
||||||
"""Remove selected input file."""
|
"""Remove selected (checked) files from the input directory."""
|
||||||
current_item = self.input_list.currentItem()
|
selected_files = self.get_selected_files()
|
||||||
if not current_item:
|
|
||||||
QMessageBox.information(self, "No Selection", "Please select a file to remove.")
|
|
||||||
return
|
|
||||||
|
|
||||||
file_path = self.files_dir / current_item.text()
|
if not selected_files:
|
||||||
|
QMessageBox.information(self, "No Selection", "Please check files to remove.")
|
||||||
|
return
|
||||||
|
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Confirm Deletion",
|
"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
|
QMessageBox.Yes | QMessageBox.No
|
||||||
)
|
)
|
||||||
|
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
try:
|
try:
|
||||||
file_path.unlink()
|
for filename in selected_files:
|
||||||
|
file_path = self.files_dir / filename
|
||||||
|
file_path.unlink()
|
||||||
self.refresh_file_lists()
|
self.refresh_file_lists()
|
||||||
|
QMessageBox.information(self, "Files Removed", f"Successfully removed {len(selected_files)} file(s).")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}")
|
QMessageBox.critical(self, "Error", f"Failed to delete files:\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)}")
|
|
||||||
|
|
||||||
def open_input_folder(self):
|
def open_input_folder(self):
|
||||||
"""Open the input directory."""
|
"""Open the input directory."""
|
||||||
|
|
@ -920,8 +1009,11 @@ class TranslationTab(QWidget):
|
||||||
|
|
||||||
def start_translation(self):
|
def start_translation(self):
|
||||||
"""Start the translation process."""
|
"""Start the translation process."""
|
||||||
if self.input_list.count() == 0:
|
# Get checked files
|
||||||
QMessageBox.warning(self, "No Files", "No files found in the input folder to translate.")
|
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
|
return
|
||||||
|
|
||||||
# Get selected module
|
# Get selected module
|
||||||
|
|
@ -931,138 +1023,93 @@ class TranslationTab(QWidget):
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_module = self.modules[selected_index]
|
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
|
# Confirm start
|
||||||
action = "estimate" if estimate_only else "translate"
|
action = mode.lower()
|
||||||
action_ing = "estimation of" if estimate_only else "translating"
|
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
f"Start {action.title()}",
|
f"Start {mode}",
|
||||||
f"Start {action_ing} {self.input_list.count()} files using {selected_module[0]}?",
|
f"Start {action} for {len(selected_files)} file(s) using {selected_module[0]}?",
|
||||||
QMessageBox.Yes | QMessageBox.No
|
QMessageBox.Yes | QMessageBox.No
|
||||||
)
|
)
|
||||||
|
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
self.translate_button.setEnabled(False)
|
# Toggle button visibility
|
||||||
self.stop_button.setEnabled(True)
|
self.translate_button.setVisible(False)
|
||||||
self.clear_log()
|
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
|
# Create and start translation worker
|
||||||
self.translation_worker = TranslationWorker(
|
self.translation_worker = TranslationWorker(
|
||||||
self.project_root,
|
self.project_root,
|
||||||
selected_module,
|
selected_module,
|
||||||
estimate_only
|
estimate_only,
|
||||||
|
selected_files # Pass selected files
|
||||||
)
|
)
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
self.translation_worker.log_signal.connect(self.append_log)
|
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)
|
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
|
# Start the worker
|
||||||
self.translation_worker.start()
|
self.translation_worker.start()
|
||||||
|
|
||||||
def append_log(self, message):
|
def append_log(self, message):
|
||||||
"""Append a message to the log buffer for batched display."""
|
"""Append a message to the log - now just for internal tracking."""
|
||||||
# Strip ANSI color codes from colorama
|
# We no longer display logs in the UI, but we can keep this for debugging
|
||||||
import re
|
# or future use with the log viewer
|
||||||
clean_message = re.sub(r'\x1b\[[0-9;]*m', '', message)
|
pass
|
||||||
|
|
||||||
# Check if this is a cost message and update the cost display
|
def update_file_progress(self, current_file, total_files, filename):
|
||||||
if "Cost: $" in clean_message:
|
"""Update the file-level progress."""
|
||||||
# Extract cost for display
|
self.files_completed = current_file
|
||||||
cost_match = re.search(r'Cost: \$([0-9,\.]+)', clean_message)
|
self.files_translated_label.setText(f"{current_file}/{total_files}")
|
||||||
if cost_match:
|
self.translating_label.setText(filename)
|
||||||
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)
|
def update_item_progress(self, current_item, total_items):
|
||||||
elif "💰" in message:
|
"""Update the item-level progress (from tqdm)."""
|
||||||
# Extract cost from the emoji format
|
self.item_progress_label.setText(f"{current_item}/{total_items}")
|
||||||
cost_match = re.search(r'💰\s*(.+)', message)
|
self.item_progress_bar.setMaximum(total_items if total_items > 0 else 100)
|
||||||
if cost_match:
|
self.item_progress_bar.setValue(current_item)
|
||||||
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)
|
|
||||||
|
|
||||||
def flush_log_buffer(self):
|
def flush_log_buffer(self):
|
||||||
"""Flush the log buffer to the display."""
|
"""No longer needed - kept for compatibility."""
|
||||||
if self.log_buffer:
|
self.log_buffer.clear()
|
||||||
# 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
|
|
||||||
self.log_timer.stop()
|
self.log_timer.stop()
|
||||||
|
|
||||||
def update_progress(self, current_file, total_files, filename):
|
def update_progress(self, current_file, total_files, filename):
|
||||||
"""Update the progress bar and label."""
|
"""Legacy method - redirect to new method."""
|
||||||
self.progress_bar.setMaximum(total_files)
|
self.update_file_progress(current_file, total_files, filename)
|
||||||
self.progress_bar.setValue(current_file)
|
|
||||||
self.progress_label.setText(f"Processing {filename} ({current_file}/{total_files})")
|
|
||||||
|
|
||||||
def on_translation_finished(self, success, message):
|
def on_translation_finished(self, success, message):
|
||||||
"""Handle translation completion."""
|
"""Handle translation completion."""
|
||||||
self.translate_button.setEnabled(True)
|
# Toggle button visibility
|
||||||
self.stop_button.setEnabled(False)
|
self.translate_button.setVisible(True)
|
||||||
self.progress_bar.setVisible(False)
|
self.stop_button.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()
|
|
||||||
|
|
||||||
|
# Update progress display
|
||||||
if success:
|
if success:
|
||||||
self.append_log("")
|
self.translating_label.setText("Completed!")
|
||||||
self.append_log("🎉 Process completed successfully!")
|
|
||||||
else:
|
else:
|
||||||
self.append_log("")
|
self.translating_label.setText(f"Failed: {message}")
|
||||||
self.append_log(f"❌ Process failed: {message}")
|
|
||||||
|
|
||||||
# Force flush the final messages
|
# Refresh file list to show any new translated files
|
||||||
self.flush_log_buffer()
|
self.refresh_file_lists()
|
||||||
|
|
||||||
def stop_translation(self):
|
def stop_translation(self):
|
||||||
"""Stop the translation process."""
|
"""Stop the translation process."""
|
||||||
|
|
@ -1071,30 +1118,16 @@ class TranslationTab(QWidget):
|
||||||
|
|
||||||
# Wait for the worker to stop gracefully
|
# Wait for the worker to stop gracefully
|
||||||
if not self.translation_worker.wait(5000): # Wait up to 5 seconds
|
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.terminate()
|
||||||
self.translation_worker.wait(2000) # Wait for termination
|
self.translation_worker.wait(2000) # Wait for termination
|
||||||
|
|
||||||
self.translate_button.setEnabled(True)
|
# Toggle button visibility
|
||||||
self.stop_button.setEnabled(False)
|
self.translate_button.setVisible(True)
|
||||||
self.progress_bar.setVisible(False)
|
self.stop_button.setVisible(False)
|
||||||
self.progress_label.setText("Ready")
|
self.translating_label.setText("Stopped")
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Handle widget close event."""
|
"""Handle widget close event."""
|
||||||
if hasattr(self, 'refresh_timer'):
|
|
||||||
self.refresh_timer.stop()
|
|
||||||
if hasattr(self, 'log_timer'):
|
if hasattr(self, 'log_timer'):
|
||||||
self.log_timer.stop()
|
self.log_timer.stop()
|
||||||
if hasattr(self, 'translation_worker') and self.translation_worker.isRunning():
|
if hasattr(self, 'translation_worker') and self.translation_worker.isRunning():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue