2026-06-12 11:58:47 +00:00
|
|
|
# main.py
|
2026-06-14 09:56:43 +00:00
|
|
|
import sys
|
|
|
|
|
import os
|
2026-10-17 01:13:03 +00:00
|
|
|
import shutil
|
|
|
|
|
from PyQt6.QtWidgets import (
|
|
|
|
|
QMainWindow, QTabWidget, QApplication, QMessageBox,
|
|
|
|
|
QVBoxLayout, QWidget, QFileDialog
|
|
|
|
|
)
|
|
|
|
|
from PyQt6.QtGui import QAction, QKeySequence
|
2026-06-24 05:37:22 +00:00
|
|
|
from PyQt6.QtCore import pyqtSlot
|
2026-06-16 05:06:08 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# Import our unified data layer
|
|
|
|
|
import database
|
2026-06-19 05:30:59 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 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
|
2026-06-12 11:58:47 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# Import our background engine runners
|
|
|
|
|
import anki_exporter
|
|
|
|
|
import video_generator
|
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
2026-06-14 09:56:43 +00:00
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
2026-10-17 01:13:03 +00:00
|
|
|
self.setWindowTitle("Spanish Voice Trainer")
|
2026-06-24 05:37:22 +00:00
|
|
|
self.setMinimumSize(900, 650)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 1. Ensure database structure exists on startup
|
|
|
|
|
database.ensure_database_populated()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 2. In-memory cache for application configurations
|
|
|
|
|
self.app_settings = {}
|
|
|
|
|
self.refresh_settings_cache()
|
2026-06-19 05:09:05 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 3. Setup central tabbed layout container
|
|
|
|
|
self.central_widget = QWidget()
|
|
|
|
|
self.setCentralWidget(self.central_widget)
|
|
|
|
|
self.main_layout = QVBoxLayout(self.central_widget)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
self.tab_widget = QTabWidget()
|
|
|
|
|
self.main_layout.addWidget(self.tab_widget)
|
2026-06-21 13:54:17 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 4. Instantiate isolated tab modules
|
|
|
|
|
self.sandbox_tab = SandboxTab()
|
|
|
|
|
self.review_tab = ReviewTab()
|
|
|
|
|
self.settings_tab = SettingsTab()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 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")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-10-17 01:13:03 +00:00
|
|
|
# 6. Build application Menu Bar
|
|
|
|
|
self.create_menu_bar()
|
|
|
|
|
|
|
|
|
|
# 7. Wire up the cross-module communication channels (Signals)
|
2026-06-24 05:37:22 +00:00
|
|
|
self.wire_application_signals()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-10-17 01:13:03 +00:00
|
|
|
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",
|
|
|
|
|
"<h3>Spanish Voice Trainer</h3>"
|
|
|
|
|
"<p><b>Version:</b> 1.0.0</p>"
|
|
|
|
|
"<p>A local-first application for Castilian Spanish vocabulary practice, "
|
|
|
|
|
"neural TTS voice previewing, and Anki deck generation.</p>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@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 = (
|
|
|
|
|
"<h3>Spanish Voice Trainer - Quick Start Guide</h3>"
|
|
|
|
|
"<ul>"
|
|
|
|
|
"<li><b>Database Sandbox:</b> Add, edit, sort, or tag your English/Spanish phrase translations. "
|
|
|
|
|
"Sort order dictates sequence in flashcards and exports.</li>"
|
|
|
|
|
"<li><b>Flashcard Review:</b> Practice interactive flashcards. Click <i>Flip Card</i> to see and automatically hear "
|
|
|
|
|
"the Spanish phrase. Click <i>Next Card</i> to advance and automatically hear the English prompt.</li>"
|
|
|
|
|
"<li><b>Filter Cards:</b> Use the top filter fields (Context or Tags) to isolate specific review topics or decks.</li>"
|
|
|
|
|
"<li><b>Generate Deck:</b> Click <i>Generate Deck</i> to compile your active phrases into a `.apkg` file ready for Anki import.</li>"
|
|
|
|
|
"<li><b>Configuration Settings:</b> Adjust default export folders, speech speed rate, and Anki deck hierarchy settings.</li>"
|
|
|
|
|
"</ul>"
|
|
|
|
|
)
|
|
|
|
|
QMessageBox.information(self, "Instructions & Help", instructions_text)
|
|
|
|
|
|
|
|
|
|
# --- Standard Application Signal & Cache Logic ---
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
def refresh_settings_cache(self):
|
|
|
|
|
"""Loads all database settings into an easy-to-read dictionary."""
|
|
|
|
|
self.app_settings = database.load_all_settings()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
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)
|
2026-06-19 05:09:05 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# Sandbox / Review Tab synchronization signals
|
|
|
|
|
self.sandbox_tab.data_mutated.connect(self.refresh_ui_views)
|
2026-06-19 05:09:05 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
@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()
|
2026-06-19 05:09:05 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
@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")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# Pull all active translation records out via explicit SQL matching
|
|
|
|
|
records = database.get_all_translations_explicit()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-20 01:55:05 +00:00
|
|
|
if not records:
|
2026-06-24 05:37:22 +00:00
|
|
|
QMessageBox.warning(self, "Export Failed", "The database contains no translation records to export.")
|
2026-06-21 13:54:17 +00:00
|
|
|
return
|
|
|
|
|
|
2026-06-22 10:38:45 +00:00
|
|
|
try:
|
2026-06-24 05:37:22 +00:00
|
|
|
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}")
|
2026-06-21 13:54:17 +00:00
|
|
|
except Exception as e:
|
2026-06-24 05:37:22 +00:00
|
|
|
QMessageBox.critical(self, "Export Error", f"Failed to parse or write Anki package archive:\n{str(e)}")
|
2026-06-21 13:54:17 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
@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()
|
2026-06-21 13:54:17 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
if not records:
|
2026-10-17 01:13:03 +00:00
|
|
|
QMessageBox.warning(self, "Generation Failed", "No phrases available to construct training video playlist loops.")
|
2026-06-14 09:56:43 +00:00
|
|
|
return
|
2026-06-16 05:06:08 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# UI updates can block execution loops, so we notify before processing
|
|
|
|
|
self.statusBar().showMessage("Generating training video assets. Please wait...")
|
|
|
|
|
QApplication.processEvents()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
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()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
@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()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
2026-06-12 11:58:47 +00:00
|
|
|
if __name__ == "__main__":
|
2026-06-21 13:54:17 +00:00
|
|
|
app = QApplication(sys.argv)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
2026-10-17 01:13:03 +00:00
|
|
|
# Explicitly set application identity for macOS menu bar and dock integration
|
|
|
|
|
app.setApplicationName("Spanish Voice Trainer")
|
|
|
|
|
app.setApplicationDisplayName("Spanish Voice Trainer")
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# Modern clean styling choices for desktop applications
|
|
|
|
|
app.setStyle('Fusion')
|
|
|
|
|
|
|
|
|
|
window = MainWindow()
|
2026-06-21 13:54:17 +00:00
|
|
|
window.show()
|
|
|
|
|
sys.exit(app.exec())
|