diff --git a/anki_exporter.py b/anki_exporter.py index 96a13d7..1b2c3ef 100644 --- a/anki_exporter.py +++ b/anki_exporter.py @@ -23,6 +23,7 @@ def compile_anki_package(records, output_path, deck_name): Compiles database records into a bidirectional card payload package. Resolves voice models dynamically by gender selection parameters and applies global speed coefficient rates from the active configurations. + Injects sort_order as field 0 for structured card sequencing. """ # Generate deterministic positive 32-bit integers from deck name and model name # to avoid collisions across different decks while keeping imports stable @@ -40,6 +41,7 @@ def compile_anki_package(records, output_path, deck_name): model_id, 'Spanish Bidirectional Multi-Note HTML Model', fields=[ + {'name': 'SortOrder'}, # Field 0: Anki primary sort field {'name': 'EnglishText'}, {'name': 'SpanishText'}, {'name': 'AnkiNotes'}, @@ -85,6 +87,11 @@ def compile_anki_package(records, output_path, deck_name): es_raw = record["es_text"] gender_flag = record.get("gender", "Female") + # Read sort_order from database record (fallback to loop index if unset) + raw_order = record.get("sort_order") + order_val = int(raw_order) if raw_order is not None else (idx + 1) + sort_order_str = f"{order_val:04d}" # Zero-padded integer string for exact sorting + # Sanitize text payloads for TTS engine en_tts_text = parse_text_for_edgetts(en_raw) es_tts_text = parse_text_for_edgetts(es_raw) @@ -123,7 +130,7 @@ def compile_anki_package(records, output_path, deck_name): # Clean sequential matching fields array matching schema mapping above note = genanki.Note( model=anki_model, - fields=[en_raw, es_raw, anki_notes_html, en_audio_field, es_audio_field], + fields=[sort_order_str, en_raw, es_raw, anki_notes_html, en_audio_field, es_audio_field], tags=note_tags ) deck.add_note(note) diff --git a/database.py b/database.py index aabba1a..532dcfe 100644 --- a/database.py +++ b/database.py @@ -69,7 +69,7 @@ def get_connection(): def ensure_database_populated(): - """Initializes tables if running against a new or empty database file.""" + """Initializes tables and migrates schemas if running against a new or existing database file.""" conn = get_connection() cursor = conn.cursor() try: @@ -83,10 +83,17 @@ def ensure_database_populated(): tags TEXT, notes TEXT, anki_notes TEXT DEFAULT '', - gender TEXT DEFAULT 'Female' + gender TEXT DEFAULT 'Female', + sort_order INTEGER DEFAULT 0 ); """) + # Safely migrate existing databases that do not yet have the sort_order column + cursor.execute("PRAGMA table_info(translations);") + columns = [row["name"] for row in cursor.fetchall()] + if "sort_order" not in columns: + cursor.execute("ALTER TABLE translations ADD COLUMN sort_order INTEGER DEFAULT 0;") + # 2. Key-value configuration table cursor.execute(""" CREATE TABLE IF NOT EXISTS settings ( @@ -141,7 +148,7 @@ def save_setting_to_db(key, value): def get_all_translations_explicit(): - """Retrieves all records using explicit, table-qualified column declarations.""" + """Retrieves all records using explicit, table-qualified column declarations sorted by sort_order.""" conn = get_connection() cursor = conn.cursor() try: @@ -154,9 +161,10 @@ def get_all_translations_explicit(): translations.tags, translations.notes, translations.anki_notes, - translations.gender + translations.gender, + translations.sort_order FROM translations - ORDER BY translations.translation_id ASC; + ORDER BY translations.sort_order ASC, translations.translation_id ASC; """) return [dict(row) for row in cursor.fetchall()] finally: @@ -178,7 +186,8 @@ def get_translation_by_id(translation_id): translations.tags, translations.notes, translations.anki_notes, - translations.gender + translations.gender, + translations.sort_order FROM translations WHERE translations.translation_id = ?; """, @@ -199,6 +208,7 @@ def update_translation_record( notes, gender, anki_notes, + sort_order=0, ): """Saves interface edits directly back into the table using named arguments.""" conn = get_connection() @@ -214,7 +224,8 @@ def update_translation_record( tags = :tags, notes = :notes, gender = :gender, - anki_notes = :anki + anki_notes = :anki, + sort_order = :sort_order WHERE translation_id = :id; """, { @@ -226,6 +237,7 @@ def update_translation_record( "notes": notes, "gender": gender, "anki": anki_notes, + "sort_order": sort_order, }, ) conn.commit() @@ -251,7 +263,7 @@ def delete_translation_record(translation_id): def insert_translation_record( - es_text, en_text, source_context, tags, notes, gender, anki_notes + es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order=0 ): """Inserts a new record using named arguments.""" conn = get_connection() @@ -259,8 +271,8 @@ def insert_translation_record( try: cursor.execute( """ - INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes) - VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki); + INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order) + VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki, :sort_order); """, { "en": en_text, @@ -270,6 +282,7 @@ def insert_translation_record( "notes": notes, "anki": anki_notes, "gender": gender, + "sort_order": sort_order, }, ) conn.commit() diff --git a/spanish_trainer-26-06-21.db b/spanish_trainer-26-06-21.db new file mode 100644 index 0000000..1fc4531 Binary files /dev/null and b/spanish_trainer-26-06-21.db differ diff --git a/spanish_trainer-26-08-21.db b/spanish_trainer-26-08-21.db index 1fc4531..d7990c4 100644 Binary files a/spanish_trainer-26-08-21.db and b/spanish_trainer-26-08-21.db differ diff --git a/spanish_trainer.db b/spanish_trainer.db index d7990c4..6a215d3 100644 Binary files a/spanish_trainer.db and b/spanish_trainer.db differ diff --git a/tabs/sandbox_tab.py b/tabs/sandbox_tab.py index 55cb254..5809f7e 100644 --- a/tabs/sandbox_tab.py +++ b/tabs/sandbox_tab.py @@ -6,8 +6,9 @@ import threading import asyncio from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, \ - QHeaderView, QMessageBox, QFormLayout, QDialog, QTextEdit, QRadioButton, QButtonGroup + QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, + QHeaderView, QMessageBox, QFormLayout, QDialog, QTextEdit, + QRadioButton, QButtonGroup, QSpinBox ) from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt import edge_tts @@ -119,6 +120,12 @@ class SandboxTab(QWidget): self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_tags.textChanged.connect(self.apply_live_grid_filter) + # --- Sort Order Field (Added after Tags) --- + self.spn_sort_order = QSpinBox() + self.spn_sort_order.setRange(0, 999999) + self.spn_sort_order.setValue(0) + self.spn_sort_order.setStyleSheet("padding: 6px; font-size: 14px;") + # Modal Editor Row Buttons for Extended Notes editor_buttons_layout = QHBoxLayout() self.btn_edit_notes = QPushButton("📝 Edit Notes Block") @@ -135,6 +142,7 @@ class SandboxTab(QWidget): form_layout.addRow(QLabel("Spanish Translation:"), spanish_widget) form_layout.addRow(QLabel("Source Context:"), self.txt_context) form_layout.addRow(QLabel("Tags:"), self.txt_tags) + form_layout.addRow(QLabel("Sort Order:"), self.spn_sort_order) form_layout.addRow(QLabel("Extended Data Fields:"), editor_buttons_layout) main_layout.addWidget(form_container) @@ -189,8 +197,8 @@ class SandboxTab(QWidget): # --- VIEWPORT GRID TABLE --- self.table = QTableWidget() - self.table.setColumnCount(6) - self.table.setHorizontalHeaderLabels(["ID", "English", "Spanish", "Context", "Tags", "Gender"]) + self.table.setColumnCount(7) + self.table.setHorizontalHeaderLabels(["ID", "English", "Spanish", "Context", "Tags", "Sort Order", "Gender"]) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.cellClicked.connect(self.populate_form_from_grid) @@ -199,6 +207,7 @@ class SandboxTab(QWidget): header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents) main_layout.addWidget(self.table) self.reload_table_display() @@ -238,6 +247,7 @@ class SandboxTab(QWidget): self.txt_spanish.clear() self.txt_context.clear() self.txt_tags.clear() + self.spn_sort_order.setValue(0) self.current_notes_content = "" self.current_anki_notes_content = "" self.rb_female.setChecked(True) @@ -255,6 +265,7 @@ class SandboxTab(QWidget): es_t = self.txt_spanish.text().strip() ctx_t = self.txt_context.text().strip() tag_t = self.txt_tags.text().strip() + sort_val = self.spn_sort_order.value() gender_t = "Male" if self.rb_male.isChecked() else "Female" if not en_t or not es_t: @@ -262,9 +273,17 @@ class SandboxTab(QWidget): return if self.selected_translation_id is None: - database.insert_translation_record(es_t, en_t, ctx_t, tag_t, self.current_notes_content, gender_t, self.current_anki_notes_content) + database.insert_translation_record( + es_t, en_t, ctx_t, tag_t, + self.current_notes_content, gender_t, + self.current_anki_notes_content, sort_val + ) else: - database.update_translation_record(self.selected_translation_id, es_t, en_t, ctx_t, tag_t, self.current_notes_content, gender_t, self.current_anki_notes_content) + database.update_translation_record( + self.selected_translation_id, es_t, en_t, ctx_t, tag_t, + self.current_notes_content, gender_t, + self.current_anki_notes_content, sort_val + ) self.clear_form_fields() self.reload_table_display() @@ -303,6 +322,7 @@ class SandboxTab(QWidget): self.txt_spanish.setText(record["es_text"]) self.txt_context.setText(record.get("source_context", "")) self.txt_tags.setText(record.get("tags", "")) + self.spn_sort_order.setValue(record.get("sort_order", 0)) self.current_notes_content = record.get("notes", "") self.current_anki_notes_content = record.get("anki_notes", "") @@ -343,7 +363,8 @@ class SandboxTab(QWidget): self.table.setItem(visible_row_idx, 2, QTableWidgetItem(r["es_text"])) self.table.setItem(visible_row_idx, 3, QTableWidgetItem(r.get("source_context", ""))) self.table.setItem(visible_row_idx, 4, QTableWidgetItem(r.get("tags", ""))) - self.table.setItem(visible_row_idx, 5, QTableWidgetItem(r.get("gender", "Female"))) + self.table.setItem(visible_row_idx, 5, QTableWidgetItem(str(r.get("sort_order", 0)))) + self.table.setItem(visible_row_idx, 6, QTableWidgetItem(r.get("gender", "Female"))) visible_row_idx += 1 def _async_edge_speech_worker(self, text, voice, rate_modifier):