139_spanish-voice-trainer/tabs/settings_tab.py

202 lines
8.8 KiB
Python
Raw Normal View History

2026-06-24 05:37:22 +00:00
# tabs/settings_tab.py
import os
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGroupBox, QFormLayout,
QMessageBox, QFrame
)
from PyQt6.QtCore import pyqtSignal, pyqtSlot
import database
class SettingsTab(QWidget):
# Signals to communicate up to the centralized main.py loop coordinator
settings_changed = pyqtSignal(str, str) # Emits: (key, value)
export_anki_requested = pyqtSignal(str) # Emits: (full_deck_name)
generate_video_requested = pyqtSignal() # Emits: trigger
def __init__(self, parent=None):
super().__init__(parent)
main_layout = QVBoxLayout(self)
main_layout.setSpacing(15)
2026-08-21 12:24:50 +00:00
# --- SECTION 0: DATABASE FILE CONFIGURATION ---
db_group = QGroupBox("Database Storage Configuration")
db_form = QFormLayout(db_group)
db_form.setSpacing(10)
db_picker_layout = QHBoxLayout()
self.txt_db_path = QLineEdit()
self.txt_db_path.setReadOnly(True)
self.txt_db_path.setStyleSheet("background-color: #F8F9F9; color: #34495E;")
btn_browse_db = QPushButton("Browse...")
btn_browse_db.clicked.connect(self.browse_database_file)
btn_reset_db = QPushButton("Reset Default")
btn_reset_db.clicked.connect(self.reset_default_database)
db_picker_layout.addWidget(self.txt_db_path)
db_picker_layout.addWidget(btn_browse_db)
db_picker_layout.addWidget(btn_reset_db)
db_form.addRow("Active Database File:", db_picker_layout)
main_layout.addWidget(db_group)
2026-06-24 05:37:22 +00:00
# --- SECTION 1: GLOBAL ANKI PACKAGING CONFIGURATIONS ---
anki_group = QGroupBox("Anki Compilation Settings")
anki_form = QFormLayout(anki_group)
anki_form.setSpacing(10)
self.txt_root_deck = QLineEdit()
self.txt_root_deck.setPlaceholderText("e.g., Spanish::CAE_Course")
self.txt_root_deck.textChanged.connect(lambda text: self.update_setting("anki_root_deck_name", text.strip()))
self.txt_sub_deck = QLineEdit()
self.txt_sub_deck.setPlaceholderText("e.g., Vocabulary::Unit_1")
self.txt_sub_deck.textChanged.connect(lambda text: self.update_setting("anki_sub_deck_name", text.strip()))
# Export Destination Directory Picker
dir_picker_layout = QHBoxLayout()
self.txt_export_dir = QLineEdit()
self.txt_export_dir.setReadOnly(True)
self.txt_export_dir.setStyleSheet("background-color: #F8F9F9; color: #34495E;")
btn_browse = QPushButton("Browse...")
btn_browse.clicked.connect(self.browse_export_directory)
dir_picker_layout.addWidget(self.txt_export_dir)
dir_picker_layout.addWidget(btn_browse)
anki_form.addRow("Root Deck Name:", self.txt_root_deck)
anki_form.addRow("Sub-Deck Namespace Hierarchy:", self.txt_sub_deck)
anki_form.addRow("Export Target Directory:", dir_picker_layout)
main_layout.addWidget(anki_group)
# --- SECTION 2: AUDIO ENGINE & COMPILATION OVERRIDES ---
engine_group = QGroupBox("Voice Synthesis & Training Configuration")
engine_form = QFormLayout(engine_group)
self.txt_tts_voice = QLineEdit()
self.txt_tts_voice.setPlaceholderText("Apple_Monica")
self.txt_tts_voice.textChanged.connect(lambda text: self.update_setting("tts_preferred_voice", text.strip()))
self.txt_tts_speed = QLineEdit()
2026-08-21 12:24:50 +00:00
self.txt_tts_speed.setPlaceholderText("0.75")
2026-06-24 05:37:22 +00:00
self.txt_tts_speed.textChanged.connect(lambda text: self.update_setting("tts_playback_speed", text.strip()))
engine_form.addRow("Fallback System Voice Name:", self.txt_tts_voice)
engine_form.addRow("Target Speech Playback Multiplier:", self.txt_tts_speed)
main_layout.addWidget(engine_group)
# Decorative divider line
divider = QFrame()
divider.setFrameShape(QFrame.Shape.HLine)
divider.setFrameShadow(QFrame.Shadow.Sunken)
main_layout.addWidget(divider)
# --- SECTION 3: SYSTEM ACTION EXECUTION BAR ---
actions_group = QGroupBox("Execution Pipelines")
actions_layout = QHBoxLayout(actions_group)
actions_layout.setSpacing(20)
self.btn_export_anki = QPushButton("🚀 Compile Lightweight Anki APKG")
self.btn_export_anki.setStyleSheet("""
QPushButton { background-color: #2980B9; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; }
QPushButton:hover { background-color: #3498DB; }
""")
self.btn_export_anki.clicked.connect(self.dispatch_anki_export)
self.btn_gen_video = QPushButton("🎬 Generate MP4 Loop Playlists")
self.btn_gen_video.setStyleSheet("""
QPushButton { background-color: #8E44AD; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; }
QPushButton:hover { background-color: #9B59B6; }
""")
self.btn_gen_video.clicked.connect(self.generate_video_requested.emit)
actions_layout.addWidget(self.btn_export_anki)
actions_layout.addWidget(self.btn_gen_video)
main_layout.addWidget(actions_group)
main_layout.addStretch() # Push everything neatly to the top
# Load settings from database onto inputs on initialization
self.populate_fields_from_db_state()
def populate_fields_from_db_state(self):
"""Fetches stored parameters on view load initialization."""
# Block signals briefly so loading state doesn't trigger write-back loops
self.blockSignals(True)
2026-08-21 12:24:50 +00:00
# Populate active database path
self.txt_db_path.setText(database.get_db_path())
2026-06-24 05:37:22 +00:00
stored_settings = database.load_all_settings()
self.txt_root_deck.setText(stored_settings.get("anki_root_deck_name", "Spanish"))
self.txt_sub_deck.setText(stored_settings.get("anki_sub_deck_name", ""))
self.txt_export_dir.setText(stored_settings.get("anki_export_directory", os.path.expanduser("~")))
self.txt_tts_voice.setText(stored_settings.get("tts_preferred_voice", "Apple_Monica"))
2026-08-21 12:24:50 +00:00
self.txt_tts_speed.setText(stored_settings.get("tts_playback_speed", "0.75"))
2026-06-24 05:37:22 +00:00
self.blockSignals(False)
def update_setting(self, key, value):
"""Internal helper to communicate state mutations instantly upward."""
self.settings_changed.emit(key, value)
2026-08-21 12:24:50 +00:00
def browse_database_file(self):
"""Allows user to choose an existing SQLite database file or create a new one."""
current_db = self.txt_db_path.text()
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select SQLite Database File",
current_db,
"SQLite Database (*.db *.sqlite *.sqlite3);;All Files (*)",
)
if file_path:
database.set_db_path(file_path)
database.ensure_database_populated()
self.populate_fields_from_db_state()
QMessageBox.information(
self,
"Database Switched",
f"Active database switched to:\n{file_path}",
)
def reset_default_database(self):
"""Resets the database path back to macOS Application Support default directory."""
default_path = database.get_default_db_path()
database.set_db_path(default_path)
database.ensure_database_populated()
self.populate_fields_from_db_state()
QMessageBox.information(
self,
"Database Reset",
f"Reset database path to default location:\n{default_path}",
)
2026-06-24 05:37:22 +00:00
def browse_export_directory(self):
"""Invokes a native macOS directory finder path browser window."""
current_dir = self.txt_export_dir.text() or os.path.expanduser("~")
selected_directory = QFileDialog.getExistingDirectory(
self, "Select Anki Export Target Location", current_dir
)
if selected_directory:
self.txt_export_dir.setText(selected_directory)
self.update_setting("anki_export_directory", selected_directory)
def dispatch_anki_export(self):
"""Constructs and validates the structured deck names namespace before signaling main.py."""
root = self.txt_root_deck.text().strip()
sub = self.txt_sub_deck.text().strip()
if not root:
QMessageBox.warning(self, "Invalid Parameters", "A root deck namespace destination must be provided.")
return
# Combine hierarchy into standard Anki format: 'Root::SubDeck'
full_deck_name = f"{root}::{sub}" if sub else root
self.export_anki_requested.emit(full_deck_name)