145 lines
No EOL
6.6 KiB
Python
145 lines
No EOL
6.6 KiB
Python
# 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)
|
|
|
|
# --- 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()
|
|
self.txt_tts_speed.setPlaceholderText("1.15")
|
|
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)
|
|
|
|
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"))
|
|
self.txt_tts_speed.setText(stored_settings.get("tts_playback_speed", "1.15"))
|
|
|
|
self.blockSignals(False)
|
|
|
|
def update_setting(self, key, value):
|
|
"""Internal helper to communicate state mutations instantly upward."""
|
|
self.settings_changed.emit(key, value)
|
|
|
|
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) |