fixed export of decks & removed our notes from cards
This commit is contained in:
parent
dbb9449efa
commit
aa614391b8
3 changed files with 241 additions and 47 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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."""
|
||||
"""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,
|
||||
"Deck Compiler Active",
|
||||
f"Compiling a custom Anki training deck container using the {self.table.rowCount()} visible filtered rows."
|
||||
"Export Complete",
|
||||
f"Successfully exported Anki package to your configured target directory!\n\n"
|
||||
f"<b>Full Namespace Tree:</b> {full_deck_namespace}\n"
|
||||
f"<b>Destination Path:</b> {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."
|
||||
)
|
||||
Loading…
Reference in a new issue