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

View file

@ -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()

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
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("<b>Spanish Translation:</b>"), spanish_widget)
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>Sort Order:</b>"), self.spn_sort_order)
form_layout.addRow(QLabel("<b>Extended Data Fields:</b>"), 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):