# main.py import sys import os import shutil from PyQt6.QtWidgets import ( QMainWindow, QTabWidget, QApplication, QMessageBox, QVBoxLayout, QWidget, QFileDialog ) from PyQt6.QtGui import QAction, QKeySequence from PyQt6.QtCore import pyqtSlot # Import our unified data layer import database # Import the UI components from our sub-package from tabs.sandbox_tab import SandboxTab from tabs.review_tab import ReviewTab from tabs.settings_tab import SettingsTab # Import our background engine runners import anki_exporter import video_generator class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Spanish Voice Trainer") self.setMinimumSize(900, 650) # 1. Ensure database structure exists on startup database.ensure_database_populated() # 2. In-memory cache for application configurations self.app_settings = {} self.refresh_settings_cache() # 3. Setup central tabbed layout container self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.main_layout = QVBoxLayout(self.central_widget) self.tab_widget = QTabWidget() self.main_layout.addWidget(self.tab_widget) # 4. Instantiate isolated tab modules self.sandbox_tab = SandboxTab() self.review_tab = ReviewTab() self.settings_tab = SettingsTab() # 5. Mount tabs with Database Sandbox on the far left (Index 0) self.tab_widget.addTab(self.sandbox_tab, "Database Sandbox") self.tab_widget.addTab(self.review_tab, "Flashcard Review") self.tab_widget.addTab(self.settings_tab, "Configuration Settings") # 6. Build application Menu Bar self.create_menu_bar() # 7. Wire up the cross-module communication channels (Signals) self.wire_application_signals() def create_menu_bar(self): """Constructs native macOS Menu Bar integrating directly with the primary system process menu.""" menu_bar = self.menuBar() # --- 1. APP MENU (Spanish Voice Trainer) --- # An empty string title merges actions directly into the native primary macOS Application Menu app_menu = menu_bar.addMenu("") about_action = QAction("About Spanish Voice Trainer", self) about_action.setMenuRole(QAction.MenuRole.AboutRole) about_action.triggered.connect(self.show_about_dialog) app_menu.addAction(about_action) update_action = QAction("Check for Updates...", self) update_action.setMenuRole(QAction.MenuRole.ApplicationSpecificRole) update_action.triggered.connect(self.check_for_updates) app_menu.addAction(update_action) quit_action = QAction("Quit Spanish Voice Trainer", self) quit_action.setMenuRole(QAction.MenuRole.QuitRole) quit_action.setShortcut(QKeySequence.StandardKey.Quit) quit_action.triggered.connect(QApplication.instance().quit) app_menu.addAction(quit_action) # --- 2. TOOLS MENU --- tools_menu = menu_bar.addMenu("Tools") backup_action = QAction("Backup Database...", self) backup_action.triggered.connect(self.handle_backup_database) tools_menu.addAction(backup_action) # --- 3. HELP MENU --- help_menu = menu_bar.addMenu("Help") help_instructions_action = QAction("User Guide & Instructions", self) help_instructions_action.setMenuRole(QAction.MenuRole.ApplicationSpecificRole) help_instructions_action.setShortcut(QKeySequence.StandardKey.HelpContents) help_instructions_action.triggered.connect(self.show_help_dialog) help_menu.addAction(help_instructions_action) # --- Menu Action Handlers --- @pyqtSlot() def show_about_dialog(self): """Displays application details and copyright information.""" QMessageBox.about( self, "About Spanish Voice Trainer", "

Spanish Voice Trainer

" "

Version: 1.0.0

" "

A local-first application for Castilian Spanish vocabulary practice, " "neural TTS voice previewing, and Anki deck generation.

" ) @pyqtSlot() def check_for_updates(self): """Placeholder action for update checks.""" QMessageBox.information( self, "Check for Updates", "Check for Updates is not implemented yet in this version." ) @pyqtSlot() def handle_backup_database(self): """Copies the primary SQLite database file to a user-selected location.""" db_path = database.DB_PATH if hasattr(database, 'DB_PATH') else "spanish_app.db" if not os.path.exists(db_path): QMessageBox.warning(self, "Backup Error", f"Database file '{db_path}' could not be located.") return save_path, _ = QFileDialog.getSaveFileName( self, "Save Database Backup", os.path.expanduser("~/Desktop/spanish_app_backup.db"), "SQLite Database (*.db *.sqlite);;All Files (*)" ) if save_path: try: shutil.copy2(db_path, save_path) QMessageBox.information( self, "Backup Complete", f"Successfully created a backup copy of your database at:\n{save_path}" ) except Exception as e: QMessageBox.critical(self, "Backup Failed", f"Could not create database backup:\n{str(e)}") @pyqtSlot() def show_help_dialog(self): """Displays user guide and usage instructions.""" instructions_text = ( "

Spanish Voice Trainer - Quick Start Guide

" "" ) QMessageBox.information(self, "Instructions & Help", instructions_text) # --- Standard Application Signal & Cache Logic --- def refresh_settings_cache(self): """Loads all database settings into an easy-to-read dictionary.""" self.app_settings = database.load_all_settings() def wire_application_signals(self): """Connects tab user-actions to execution managers inside main.py.""" # Settings Tab Signal Actions self.settings_tab.settings_changed.connect(self.handle_settings_update) self.settings_tab.export_anki_requested.connect(self.execute_anki_export) self.settings_tab.generate_video_requested.connect(self.execute_video_generation) # Sandbox / Review Tab synchronization signals self.sandbox_tab.data_mutated.connect(self.refresh_ui_views) @pyqtSlot(str, str) def handle_settings_update(self, key, value): """Saves a modified setting value directly down to the database and updates memory cache.""" database.save_setting_to_db(key, value) self.refresh_settings_cache() @pyqtSlot(str) def execute_anki_export(self, full_deck_name): """Bridges data coordinates out of SQLite to trigger the text-to-speech anki file compiler.""" self.refresh_settings_cache() output_dir = self.app_settings.get("anki_export_directory", os.path.expanduser("~")) output_file = os.path.join(output_dir, f"{full_deck_name.replace('::', '_')}.apkg") # Pull all active translation records out via explicit SQL matching records = database.get_all_translations_explicit() if not records: QMessageBox.warning(self, "Export Failed", "The database contains no translation records to export.") return try: anki_exporter.compile_anki_package(records, output_file, full_deck_name) QMessageBox.information(self, "Export Success", f"Successfully compiled light-weight TTS deck to:\n{output_file}") except Exception as e: QMessageBox.critical(self, "Export Error", f"Failed to parse or write Anki package archive:\n{str(e)}") @pyqtSlot() def execute_video_generation(self): """Coordinates data streams to invoke the independent video compilation generator.""" self.refresh_settings_cache() records = database.get_all_translations_explicit() if not records: QMessageBox.warning(self, "Generation Failed", "No phrases available to construct training video playlist loops.") return # UI updates can block execution loops, so we notify before processing self.statusBar().showMessage("Generating training video assets. Please wait...") QApplication.processEvents() try: video_generator.generate_training_video(records, self.app_settings) QMessageBox.information(self, "Success", "MP4 Video compilation completed successfully.") self.statusBar().showMessage("Video compilation completed.", 5000) except Exception as e: QMessageBox.critical(self, "Video Error", f"An error occurred during frame rendering sequences:\n{str(e)}") self.statusBar().clearMessage() @pyqtSlot() def refresh_ui_views(self): """Instructs distinct active tabs to reload data windows after database changes occur.""" self.sandbox_tab.reload_table_display() self.review_tab.reload_review_pool() if __name__ == "__main__": app = QApplication(sys.argv) # Explicitly set application identity for macOS menu bar and dock integration app.setApplicationName("Spanish Voice Trainer") app.setApplicationDisplayName("Spanish Voice Trainer") # Modern clean styling choices for desktop applications app.setStyle('Fusion') window = MainWindow() window.show() sys.exit(app.exec())