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):