diff --git a/anki_exporter.py b/anki_exporter.py index 78980be..4d8a61d 100644 --- a/anki_exporter.py +++ b/anki_exporter.py @@ -55,12 +55,12 @@ def compile_anki_package(records, output_path, deck_name): # Build text for visual notes area, appending tags if they exist notes_parts = [] - if record.get('source_context'): - notes_parts.append(f"Context: {record['source_context']}") - if record.get('notes'): - notes_parts.append(f"Notes: {record['notes']}") - if raw_tags: - notes_parts.append(f"Tags: {raw_tags}") + # if record.get('source_context'): + # notes_parts.append(f"Context: {record['source_context']}") + # #if record.get('notes'): + # #notes_parts.append(f"Notes: {record['notes']}") + # if raw_tags: + # notes_parts.append(f"Tags: {raw_tags}") notes_display_text = " | ".join(notes_parts) diff --git a/spanish_trainer.db b/spanish_trainer.db index 56c75e9..9ae2a39 100644 Binary files a/spanish_trainer.db and b/spanish_trainer.db differ diff --git a/tabs/review_tab.py b/tabs/review_tab.py index 45ead07..61ecdaf 100644 --- a/tabs/review_tab.py +++ b/tabs/review_tab.py @@ -1,26 +1,32 @@ # tabs/review_tab.py import random +import os +import subprocess from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, - QHeaderView, QFormLayout, QMessageBox + QHeaderView, QFormLayout, QMessageBox, QFrame, QStackedWidget ) from PyQt6.QtCore import Qt, pyqtSlot import database +import anki_exporter class ReviewTab(QWidget): def __init__(self, parent=None): super().__init__(parent) - # Master cache of records loaded from the database + # Core Review State Tracking self.all_cached_records = [] + self.filtered_review_pool = [] + self.current_index = -1 + self.is_flipped = False # Track front vs back state of the active flashcard - # Primary Layout + # Primary Main Layout main_layout = QVBoxLayout(self) main_layout.setContentsMargins(30, 20, 30, 20) main_layout.setSpacing(15) - # --- SECTION 1: TOP REGION (Source Context & Tags) --- + # --- SECTION 1: TOP REGION (Source Context & Tags Filters) --- top_container = QWidget() top_layout = QFormLayout(top_container) top_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) @@ -41,19 +47,103 @@ class ReviewTab(QWidget): main_layout.addWidget(top_container) - # --- SECTION 2: MIDDLE REGION (Translations Table View) --- + # --- SECTION 2: MIDDLE REGION (Split Screen Workspace) --- + split_layout = QHBoxLayout() + split_layout.setSpacing(20) + + # Left Half: Live Translation Grid View Table self.table = QTableWidget() self.table.setColumnCount(2) self.table.setHorizontalHeaderLabels(["English Phrase", "Spanish Translation"]) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) - # Format table header behaviors to stretch beautifully header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) - main_layout.addWidget(self.table) + split_layout.addWidget(self.table, stretch=1) + + # Right Half: Live Interactive Flashcard Review Panel container + card_container = QWidget() + card_vbox = QVBoxLayout(card_container) + card_vbox.setContentsMargins(0, 0, 0, 0) + card_vbox.setSpacing(12) + + # The Card Visual Canvas Frame + self.card_frame = QFrame() + self.card_frame.setStyleSheet(""" + QFrame { + background-color: #FAFAFA; + border: 2px solid #E5E7E9; + border-radius: 8px; + } + """) + card_frame_layout = QVBoxLayout(self.card_frame) + card_frame_layout.setContentsMargins(25, 25, 25, 25) + + self.card_stack = QStackedWidget() + + # Card Front View (English Prompt) + self.view_front = QWidget() + front_layout = QVBoxLayout(self.view_front) + front_prompt = QLabel("TRANSLATE TO SPANISH:") + front_prompt.setStyleSheet("font-size: 11px; font-weight: bold; color: #BDC3C7; letter-spacing: 1px;") + front_prompt.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.lbl_english = QLabel("No cards matching active filters.") + self.lbl_english.setStyleSheet("font-size: 20px; color: #34495E; font-weight: 500; margin-top: 15px;") + self.lbl_english.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.lbl_english.setWordWrap(True) + + front_layout.addWidget(front_prompt) + front_layout.addWidget(self.lbl_english) + front_layout.addStretch() + + # Card Back View (Spanish Answer Only) + self.view_back = QWidget() + back_layout = QVBoxLayout(self.view_back) + + self.lbl_spanish = QLabel("Spanish Answer Text") + self.lbl_spanish.setStyleSheet("font-size: 24px; font-weight: bold; color: #2980B9; margin-top: 20px;") + self.lbl_spanish.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.lbl_spanish.setWordWrap(True) + + back_layout.addWidget(self.lbl_spanish) + back_layout.addStretch() + + self.card_stack.addWidget(self.view_front) + self.card_stack.addWidget(self.view_back) + card_frame_layout.addWidget(self.card_stack) + + card_vbox.addWidget(self.card_frame, stretch=1) + + # Buttons Row beneath the Flashcard + card_buttons_layout = QHBoxLayout() + card_buttons_layout.setSpacing(10) + + self.btn_play_audio = QPushButton("🔊 Play Voice") + self.btn_play_audio.setCursor(Qt.CursorShape.PointingHandCursor) + self.btn_play_audio.setStyleSheet(""" + QPushButton { background-color: #E67E22; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; } + QPushButton:hover { background-color: #D35400; } + """) + self.btn_play_audio.clicked.connect(self.play_card_audio) + + self.btn_flip_next = QPushButton("Flip Card") + self.btn_flip_next.setCursor(Qt.CursorShape.PointingHandCursor) + self.btn_flip_next.setStyleSheet(""" + QPushButton { background-color: #34495E; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; } + QPushButton:hover { background-color: #2C3E50; } + """) + self.btn_flip_next.clicked.connect(self.handle_card_interaction) + + card_buttons_layout.addWidget(self.btn_play_audio, stretch=1) + card_buttons_layout.addWidget(self.btn_flip_next, stretch=2) + card_vbox.addLayout(card_buttons_layout) + + split_layout.addWidget(card_container, stretch=1) + main_layout.addLayout(split_layout) # --- SECTION 3: BOTTOM REGION (Action Control Panel) --- bottom_layout = QHBoxLayout() @@ -62,14 +152,7 @@ class ReviewTab(QWidget): self.btn_generate_deck = QPushButton("🗂️ Generate Deck") self.btn_generate_deck.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_generate_deck.setStyleSheet(""" - QPushButton { - background-color: #27AE60; - color: white; - font-weight: bold; - font-size: 14px; - padding: 10px 22px; - border-radius: 5px; - } + QPushButton { background-color: #27AE60; color: white; font-weight: bold; font-size: 14px; padding: 10px 22px; border-radius: 5px; } QPushButton:hover { background-color: #219653; } """) self.btn_generate_deck.clicked.connect(self.generate_deck_action) @@ -77,14 +160,7 @@ class ReviewTab(QWidget): self.btn_generate_video = QPushButton("🎬 Generate Video") self.btn_generate_video.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_generate_video.setStyleSheet(""" - QPushButton { - background-color: #2980B9; - color: white; - font-weight: bold; - font-size: 14px; - padding: 10px 22px; - border-radius: 5px; - } + QPushButton { background-color: #2980B9; color: white; font-weight: bold; font-size: 14px; padding: 10px 22px; border-radius: 5px; } QPushButton:hover { background-color: #1F618D; } """) self.btn_generate_video.clicked.connect(self.generate_video_action) @@ -95,7 +171,7 @@ class ReviewTab(QWidget): main_layout.addLayout(bottom_layout) - # Populate initial table layout on startup + # Populate initial states from backend self.reload_review_pool() @pyqtSlot() @@ -105,44 +181,162 @@ class ReviewTab(QWidget): self.all_cached_records = database.get_all_translations_explicit() self.handle_live_filter() except Exception as e: - print(f"Error initializing flashcard display: {e}") + print(f"Error initializing flashcard review workspace: {e}") @pyqtSlot() def handle_live_filter(self): - """Filters the display table row contents matching current Context and Tags criteria.""" + """Filters grid contents and generates a randomized matching queue for the card engine.""" self.table.blockSignals(True) self.table.setRowCount(0) filter_ctx = self.txt_review_context.text().lower().strip() filter_tag = self.txt_review_tags.text().lower().strip() + self.filtered_review_pool = [] visible_row_index = 0 + for row in self.all_cached_records: val_ctx = (row["source_context"] or "").lower() val_tag = (row["tags"] or "").lower() - # Show row if it satisfies both filter boxes if (filter_ctx in val_ctx) and (filter_tag in val_tag): + self.filtered_review_pool.append(row) + self.table.insertRow(visible_row_index) - - item_en = QTableWidgetItem(row["en_text"]) - item_es = QTableWidgetItem(row["es_text"]) - - self.table.setItem(visible_row_index, 0, item_en) - self.table.setItem(visible_row_index, 1, item_es) - + self.table.setItem(visible_row_index, 0, QTableWidgetItem(row["en_text"])) + self.table.setItem(visible_row_index, 1, QTableWidgetItem(row["es_text"])) visible_row_index += 1 self.table.blockSignals(False) + + # Reshuffle the active localized queue stack and reset card state tracking pointer + random.shuffle(self.filtered_review_pool) + self.current_index = 0 if self.filtered_review_pool else -1 + self.is_flipped = False + self.display_current_card() + + def display_current_card(self): + """Pushes current pool row data configurations to layout containers.""" + if not (0 <= self.current_index < len(self.filtered_review_pool)): + self.lbl_english.setText("No phrases match current active criteria filters.") + self.lbl_spanish.setText("") + self.card_stack.setCurrentIndex(0) + self.btn_flip_next.setText("Flip Card") + self.btn_flip_next.setEnabled(False) + self.btn_play_audio.setEnabled(False) + return + + self.btn_flip_next.setEnabled(True) + self.btn_play_audio.setEnabled(True) + record = self.filtered_review_pool[self.current_index] + + # Setup front and back text labels + self.lbl_english.setText(record["en_text"]) + self.lbl_spanish.setText(record["es_text"]) + + # Sync visual widget indexing configurations + if not self.is_flipped: + self.card_stack.setCurrentIndex(0) + self.btn_flip_next.setText("Flip Card") + else: + self.card_stack.setCurrentIndex(1) + self.btn_flip_next.setText("Next Card ➔") + + @pyqtSlot() + def handle_card_interaction(self): + """State machine cycling through card flipped values or increments indices sequential steps.""" + if not self.filtered_review_pool: + return + + if not self.is_flipped: + # Transition State: Front -> Back + self.is_flipped = True + self.display_current_card() + else: + # Transition State: Advance to next index item row + self.current_index += 1 + if self.current_index >= len(self.filtered_review_pool): + self.current_index = 0 + random.shuffle(self.filtered_review_pool) # Rescramble on completion pass loops + + self.is_flipped = False + self.display_current_card() + + @pyqtSlot() + def play_card_audio(self): + """Auditions native voice files based on active visual canvas sides.""" + if not (0 <= self.current_index < len(self.filtered_review_pool)): + return + + record = self.filtered_review_pool[self.current_index] + if not self.is_flipped: + # Play English text utilizing standard native subsystem default output + if record["en_text"]: + subprocess.Popen(["say", record["en_text"]]) + else: + # Play target translation explicitly targeting the Castilian voice profile Mónica + if record["es_text"]: + subprocess.Popen(["say", "-v", "Monica", record["es_text"]]) @pyqtSlot() def generate_deck_action(self): - """Placeholder function execution trigger for processing deck compiler passes.""" - QMessageBox.information( - self, - "Deck Compiler Active", - f"Compiling a custom Anki training deck container using the {self.table.rowCount()} visible filtered rows." - ) + """Generates a specialized lightweight .apkg Anki deck matching active filter parameters, + respecting exact user database configuration keys for target folders and naming chains.""" + if not self.filtered_review_pool: + QMessageBox.warning(self, "Export Aborted", "The current matching review deck queue is empty. Cannot compile an empty deck.") + return + + try: + # Load active settings dictionary directly from your database configurations + settings = database.load_all_settings() + # print("--- CURRENT DATABASE SETTINGS ---") + # for key, value in settings.items(): + # print(f"{key}: {value}") + # print("---------------------------------") + # Extract configurations targeting exact database schema names found in settings + target_dir = settings.get("anki_export_directory") + #root_deck_name = settings.get("default_deck_name") + root_deck_name = settings.get("anki_root_deck_name") + sub_deck_hierarchy = settings.get("anki_sub_deck_name") + + # Fallback handling to verify directories exist safely + if not target_dir or not os.path.isdir(str(target_dir)): + target_dir = os.path.expanduser("~/Desktop") + else: + target_dir = str(target_dir) + + # --- Compile Full Namespace Tree Path --- + deck_tree_parts = [] + + if root_deck_name and str(root_deck_name).strip(): + deck_tree_parts.append(str(root_deck_name).strip()) + else: + deck_tree_parts.append("DefaultDeck") # Baseline structural root name fallback + + if sub_deck_hierarchy and str(sub_deck_hierarchy).strip(): + deck_tree_parts.append(str(sub_deck_hierarchy).strip()) + + ## deck_tree_parts.append("Filtered Review Session") + + # Join parts using Anki double-colon syntax (::) + full_deck_namespace = "::".join(deck_tree_parts) + + # Establish absolute output filename file path anchor + filename = "Spanish_Filtered_Review.apkg" + file_path = os.path.join(target_dir, filename) + + # Execute actual compilation algorithm pipeline mapping filtered records cleanly + anki_exporter.compile_anki_package(self.filtered_review_pool, file_path, full_deck_namespace) + + QMessageBox.information( + self, + "Export Complete", + f"Successfully exported Anki package to your configured target directory!\n\n" + f"Full Namespace Tree: {full_deck_namespace}\n" + f"Destination Path: {file_path}" + ) + except Exception as e: + QMessageBox.critical(self, "Compiler Fault Safeguard", f"An exception occurred building your deck container package:\n{str(e)}") @pyqtSlot() def generate_video_action(self): @@ -150,5 +344,5 @@ class ReviewTab(QWidget): QMessageBox.information( self, "Media Generator Active", - f"Initiating background video asset production using the {self.table.rowCount()} visible phrases." + f"Initiating background video asset production using the {len(self.filtered_review_pool)} visible phrases." ) \ No newline at end of file