# 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, QFrame, QStackedWidget ) from PyQt6.QtCore import Qt, pyqtSlot import database import anki_exporter class ReviewTab(QWidget): def __init__(self, parent=None): super().__init__(parent) # 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 Main Layout main_layout = QVBoxLayout(self) main_layout.setContentsMargins(30, 20, 30, 20) main_layout.setSpacing(15) # --- SECTION 1: TOP REGION (Source Context & Tags Filters) --- top_container = QWidget() top_layout = QFormLayout(top_container) top_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) top_layout.setSpacing(10) self.txt_review_context = QLineEdit() self.txt_review_context.setPlaceholderText("Filter deck by context (e.g., Camino 2027)...") self.txt_review_context.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_review_context.textChanged.connect(self.handle_live_filter) self.txt_review_tags = QLineEdit() self.txt_review_tags.setPlaceholderText("Filter deck by tags (e.g., verb, greeting)...") self.txt_review_tags.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_review_tags.textChanged.connect(self.handle_live_filter) top_layout.addRow(QLabel("Source Context:"), self.txt_review_context) top_layout.addRow(QLabel("Tags:"), self.txt_review_tags) main_layout.addWidget(top_container) # --- 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) header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) 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() bottom_layout.setSpacing(15) 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:hover { background-color: #219653; } """) self.btn_generate_deck.clicked.connect(self.generate_deck_action) 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:hover { background-color: #1F618D; } """) self.btn_generate_video.clicked.connect(self.generate_video_action) bottom_layout.addWidget(self.btn_generate_deck) bottom_layout.addWidget(self.btn_generate_video) bottom_layout.addStretch() main_layout.addLayout(bottom_layout) # Populate initial states from backend self.reload_review_pool() @pyqtSlot() def reload_review_pool(self): """Fetches consolidated text entries from the database and initializes cache.""" try: self.all_cached_records = database.get_all_translations_explicit() self.handle_live_filter() except Exception as e: print(f"Error initializing flashcard review workspace: {e}") @pyqtSlot() def handle_live_filter(self): """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() if (filter_ctx in val_ctx) and (filter_tag in val_tag): self.filtered_review_pool.append(row) self.table.insertRow(visible_row_index) 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): """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): """Placeholder function execution trigger for media compiler production automation.""" QMessageBox.information( self, "Media Generator Active", f"Initiating background video asset production using the {len(self.filtered_review_pool)} visible phrases." )