sort order added to deck

This commit is contained in:
stephen 2026-08-28 10:27:21 +10:00
parent 2c0c63036d
commit ed1c6b9d4e
6 changed files with 59 additions and 18 deletions

View file

@ -23,6 +23,7 @@ def compile_anki_package(records, output_path, deck_name):
Compiles database records into a bidirectional card payload package. Compiles database records into a bidirectional card payload package.
Resolves voice models dynamically by gender selection parameters and applies Resolves voice models dynamically by gender selection parameters and applies
global speed coefficient rates from the active configurations. 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 # Generate deterministic positive 32-bit integers from deck name and model name
# to avoid collisions across different decks while keeping imports stable # 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, model_id,
'Spanish Bidirectional Multi-Note HTML Model', 'Spanish Bidirectional Multi-Note HTML Model',
fields=[ fields=[
{'name': 'SortOrder'}, # Field 0: Anki primary sort field
{'name': 'EnglishText'}, {'name': 'EnglishText'},
{'name': 'SpanishText'}, {'name': 'SpanishText'},
{'name': 'AnkiNotes'}, {'name': 'AnkiNotes'},
@ -85,6 +87,11 @@ def compile_anki_package(records, output_path, deck_name):
es_raw = record["es_text"] es_raw = record["es_text"]
gender_flag = record.get("gender", "Female") 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 # Sanitize text payloads for TTS engine
en_tts_text = parse_text_for_edgetts(en_raw) en_tts_text = parse_text_for_edgetts(en_raw)
es_tts_text = parse_text_for_edgetts(es_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 # Clean sequential matching fields array matching schema mapping above
note = genanki.Note( note = genanki.Note(
model=anki_model, 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 tags=note_tags
) )
deck.add_note(note) deck.add_note(note)

View file

@ -69,7 +69,7 @@ def get_connection():
def ensure_database_populated(): 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() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
try: try:
@ -83,10 +83,17 @@ def ensure_database_populated():
tags TEXT, tags TEXT,
notes TEXT, notes TEXT,
anki_notes TEXT DEFAULT '', 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 # 2. Key-value configuration table
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
@ -141,7 +148,7 @@ def save_setting_to_db(key, value):
def get_all_translations_explicit(): 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() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
try: try:
@ -154,9 +161,10 @@ def get_all_translations_explicit():
translations.tags, translations.tags,
translations.notes, translations.notes,
translations.anki_notes, translations.anki_notes,
translations.gender translations.gender,
translations.sort_order
FROM translations 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()] return [dict(row) for row in cursor.fetchall()]
finally: finally:
@ -178,7 +186,8 @@ def get_translation_by_id(translation_id):
translations.tags, translations.tags,
translations.notes, translations.notes,
translations.anki_notes, translations.anki_notes,
translations.gender translations.gender,
translations.sort_order
FROM translations FROM translations
WHERE translations.translation_id = ?; WHERE translations.translation_id = ?;
""", """,
@ -199,6 +208,7 @@ def update_translation_record(
notes, notes,
gender, gender,
anki_notes, anki_notes,
sort_order=0,
): ):
"""Saves interface edits directly back into the table using named arguments.""" """Saves interface edits directly back into the table using named arguments."""
conn = get_connection() conn = get_connection()
@ -214,7 +224,8 @@ def update_translation_record(
tags = :tags, tags = :tags,
notes = :notes, notes = :notes,
gender = :gender, gender = :gender,
anki_notes = :anki anki_notes = :anki,
sort_order = :sort_order
WHERE translation_id = :id; WHERE translation_id = :id;
""", """,
{ {
@ -226,6 +237,7 @@ def update_translation_record(
"notes": notes, "notes": notes,
"gender": gender, "gender": gender,
"anki": anki_notes, "anki": anki_notes,
"sort_order": sort_order,
}, },
) )
conn.commit() conn.commit()
@ -251,7 +263,7 @@ def delete_translation_record(translation_id):
def insert_translation_record( 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.""" """Inserts a new record using named arguments."""
conn = get_connection() conn = get_connection()
@ -259,8 +271,8 @@ def insert_translation_record(
try: try:
cursor.execute( cursor.execute(
""" """
INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes) INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order)
VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki); VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki, :sort_order);
""", """,
{ {
"en": en_text, "en": en_text,
@ -270,6 +282,7 @@ def insert_translation_record(
"notes": notes, "notes": notes,
"anki": anki_notes, "anki": anki_notes,
"gender": gender, "gender": gender,
"sort_order": sort_order,
}, },
) )
conn.commit() conn.commit()

BIN
spanish_trainer-26-06-21.db Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -6,8 +6,9 @@ import threading
import asyncio import asyncio
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, \ QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
QHeaderView, QMessageBox, QFormLayout, QDialog, QTextEdit, QRadioButton, QButtonGroup QHeaderView, QMessageBox, QFormLayout, QDialog, QTextEdit,
QRadioButton, QButtonGroup, QSpinBox
) )
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt
import edge_tts import edge_tts
@ -119,6 +120,12 @@ class SandboxTab(QWidget):
self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;")
self.txt_tags.textChanged.connect(self.apply_live_grid_filter) 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 # Modal Editor Row Buttons for Extended Notes
editor_buttons_layout = QHBoxLayout() editor_buttons_layout = QHBoxLayout()
self.btn_edit_notes = QPushButton("📝 Edit Notes Block") self.btn_edit_notes = QPushButton("📝 Edit Notes Block")
@ -135,6 +142,7 @@ class SandboxTab(QWidget):
form_layout.addRow(QLabel("<b>Spanish Translation:</b>"), spanish_widget) form_layout.addRow(QLabel("<b>Spanish Translation:</b>"), spanish_widget)
form_layout.addRow(QLabel("<b>Source Context:</b>"), self.txt_context) form_layout.addRow(QLabel("<b>Source Context:</b>"), self.txt_context)
form_layout.addRow(QLabel("<b>Tags:</b>"), self.txt_tags) form_layout.addRow(QLabel("<b>Tags:</b>"), self.txt_tags)
form_layout.addRow(QLabel("<b>Sort Order:</b>"), self.spn_sort_order)
form_layout.addRow(QLabel("<b>Extended Data Fields:</b>"), editor_buttons_layout) form_layout.addRow(QLabel("<b>Extended Data Fields:</b>"), editor_buttons_layout)
main_layout.addWidget(form_container) main_layout.addWidget(form_container)
@ -189,8 +197,8 @@ class SandboxTab(QWidget):
# --- VIEWPORT GRID TABLE --- # --- VIEWPORT GRID TABLE ---
self.table = QTableWidget() self.table = QTableWidget()
self.table.setColumnCount(6) self.table.setColumnCount(7)
self.table.setHorizontalHeaderLabels(["ID", "English", "Spanish", "Context", "Tags", "Gender"]) self.table.setHorizontalHeaderLabels(["ID", "English", "Spanish", "Context", "Tags", "Sort Order", "Gender"])
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.cellClicked.connect(self.populate_form_from_grid) self.table.cellClicked.connect(self.populate_form_from_grid)
@ -199,6 +207,7 @@ class SandboxTab(QWidget):
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
main_layout.addWidget(self.table) main_layout.addWidget(self.table)
self.reload_table_display() self.reload_table_display()
@ -238,6 +247,7 @@ class SandboxTab(QWidget):
self.txt_spanish.clear() self.txt_spanish.clear()
self.txt_context.clear() self.txt_context.clear()
self.txt_tags.clear() self.txt_tags.clear()
self.spn_sort_order.setValue(0)
self.current_notes_content = "" self.current_notes_content = ""
self.current_anki_notes_content = "" self.current_anki_notes_content = ""
self.rb_female.setChecked(True) self.rb_female.setChecked(True)
@ -255,6 +265,7 @@ class SandboxTab(QWidget):
es_t = self.txt_spanish.text().strip() es_t = self.txt_spanish.text().strip()
ctx_t = self.txt_context.text().strip() ctx_t = self.txt_context.text().strip()
tag_t = self.txt_tags.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" gender_t = "Male" if self.rb_male.isChecked() else "Female"
if not en_t or not es_t: if not en_t or not es_t:
@ -262,9 +273,17 @@ class SandboxTab(QWidget):
return return
if self.selected_translation_id is None: 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: 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.clear_form_fields()
self.reload_table_display() self.reload_table_display()
@ -303,6 +322,7 @@ class SandboxTab(QWidget):
self.txt_spanish.setText(record["es_text"]) self.txt_spanish.setText(record["es_text"])
self.txt_context.setText(record.get("source_context", "")) self.txt_context.setText(record.get("source_context", ""))
self.txt_tags.setText(record.get("tags", "")) 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_notes_content = record.get("notes", "")
self.current_anki_notes_content = record.get("anki_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, 2, QTableWidgetItem(r["es_text"]))
self.table.setItem(visible_row_idx, 3, QTableWidgetItem(r.get("source_context", ""))) 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, 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 visible_row_idx += 1
def _async_edge_speech_worker(self, text, voice, rate_modifier): def _async_edge_speech_worker(self, text, voice, rate_modifier):