first commit
Build and deploy / build-and-push (push) Successful in 17s
Build and deploy / deploy (push) Successful in 10s

This commit is contained in:
william
2026-08-21 16:23:49 +02:00
committed by william
commit 3f4ebc4527
248 changed files with 10180 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Accès aux écrans du jeu (_screens) et au schéma de base commun au module."""
+21
View File
@@ -0,0 +1,21 @@
import db
from .ensure_schema import ensure_schema
def create_screen(slug, name, is_template=False):
ensure_schema(slug)
conn = db.connect(slug)
if is_template:
order_index = 0
else:
max_order = conn.execute("SELECT MAX(order_index) AS m FROM _screens WHERE is_template = 0").fetchone()["m"]
order_index = (max_order or 0) + 1
conn.execute(
"INSERT INTO _screens (name, order_index, is_template) VALUES (?, ?, ?)",
(name, order_index, 1 if is_template else 0),
)
screen_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
conn.commit()
conn.close()
return screen_id
+8
View File
@@ -0,0 +1,8 @@
import db
def delete_screen(slug, screen_id):
conn = db.connect(slug)
conn.execute("DELETE FROM _screens WHERE id = ?", (screen_id,))
conn.commit()
conn.close()
+124
View File
@@ -0,0 +1,124 @@
import db
def ensure_schema(slug):
"""Crée les tables de ce module si elles n'existent pas encore — les
jeux créés avant l'ajout de cette fonctionnalité n'ont pas besoin d'être
migrés manuellement, la première visite d'un écran de ce jeu suffit."""
conn = db.connect(slug)
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS _screens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
order_index INTEGER NOT NULL DEFAULT 0,
is_template INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS _element_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
icon TEXT NOT NULL DEFAULT '🧩',
default_widget TEXT NOT NULL DEFAULT 'conteneur',
default_content TEXT NOT NULL DEFAULT '',
default_attributes TEXT NOT NULL DEFAULT '{}',
default_style TEXT NOT NULL DEFAULT '{}',
template_screen_id INTEGER REFERENCES _screens(id),
bound_definition_id INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS _screen_elements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
parent_id INTEGER REFERENCES _screen_elements(id) ON DELETE CASCADE,
widget TEXT NOT NULL DEFAULT 'conteneur',
element_type_id INTEGER REFERENCES _element_types(id),
name TEXT,
content TEXT NOT NULL DEFAULT '',
attributes TEXT NOT NULL DEFAULT '{}',
x REAL NOT NULL DEFAULT 10,
y REAL NOT NULL DEFAULT 10,
width REAL NOT NULL DEFAULT 20,
height REAL NOT NULL DEFAULT 10,
z_index INTEGER NOT NULL DEFAULT 1,
style TEXT NOT NULL DEFAULT '{}',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS _actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
element_id INTEGER NOT NULL REFERENCES _screen_elements(id) ON DELETE CASCADE,
trigger_event TEXT NOT NULL,
action_type TEXT NOT NULL,
target_screen_id INTEGER REFERENCES _screens(id),
target_element_id INTEGER REFERENCES _screen_elements(id),
element_property TEXT,
element_value TEXT,
target_definition_id INTEGER,
target_row_id INTEGER,
target_field TEXT,
data_operation TEXT,
data_value TEXT
);
"""
)
cols = {r["name"] for r in conn.execute("PRAGMA table_info(_screen_elements)").fetchall()}
if "widget" not in cols:
conn.execute("ALTER TABLE _screen_elements ADD COLUMN widget TEXT NOT NULL DEFAULT 'conteneur'")
if "attributes" not in cols:
conn.execute("ALTER TABLE _screen_elements ADD COLUMN attributes TEXT NOT NULL DEFAULT '{}'")
if "parent_id" not in cols:
conn.execute("ALTER TABLE _screen_elements ADD COLUMN parent_id INTEGER REFERENCES _screen_elements(id) ON DELETE CASCADE")
if "name" not in cols:
conn.execute("ALTER TABLE _screen_elements ADD COLUMN name TEXT")
type_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_element_types)").fetchall()}
if "default_widget" not in type_cols:
conn.execute("ALTER TABLE _element_types ADD COLUMN default_widget TEXT NOT NULL DEFAULT 'conteneur'")
conn.execute("ALTER TABLE _element_types ADD COLUMN default_content TEXT NOT NULL DEFAULT ''")
conn.execute("ALTER TABLE _element_types ADD COLUMN default_attributes TEXT NOT NULL DEFAULT '{}'")
if "template_screen_id" not in type_cols:
# Un élément de jeu n'est plus un simple widget isolé : c'est un
# conteneur avec un vrai arbre d'éléments imbriqués à l'intérieur,
# construit exactement comme le contenu de n'importe quel conteneur
# sur un écran — ce petit arbre vit dans un écran-modèle CACHÉ
# (voir "is_template" ci-dessous), pointé par cette colonne.
conn.execute("ALTER TABLE _element_types ADD COLUMN template_screen_id INTEGER REFERENCES _screens(id)")
if "bound_definition_id" not in type_cols:
# "Objet lié" optionnel — comme pour le Répéteur de données, rend
# {{nom_du_champ}} disponible dans tout l'arbre de ce modèle.
conn.execute("ALTER TABLE _element_types ADD COLUMN bound_definition_id INTEGER")
screen_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_screens)").fetchall()}
if "is_template" not in screen_cols:
# Un écran-modèle héberge le contenu d'un élément de jeu réutilisable
# (voir template_screen_id ci-dessus) : il n'apparaît jamais dans la
# liste des écrans du jeu, ni dans le mode jouable, ni dans les
# menus "aller à cet écran" — seul son contenu (les éléments qu'il
# contient) est utilisé, jamais l'écran lui-même.
conn.execute("ALTER TABLE _screens ADD COLUMN is_template INTEGER NOT NULL DEFAULT 0")
if "preview_aspect" not in screen_cols:
# Format d'aperçu choisi pour cet écran (Portrait 9/16, Paysage 16/9,
# ou Carré 1/1) — mémorisé par écran pour qu'il reste le même après
# un ajout/suppression d'élément ou un changement d'élément
# sélectionné (ces actions redemandent le panneau au serveur, qui
# doit donc connaître le choix déjà fait, sinon il repart sur la
# valeur par défaut à chaque fois). Sert aussi en mode Jouer, pour
# garder la zone de jeu à la bonne proportion quelle que soit la
# forme réelle de la fenêtre du joueur (voir #playFrame dans
# play.html) — sans ça, les positions/tailles en % se déforment dès
# que la fenêtre du joueur n'a pas la même proportion que celle
# utilisée à la conception.
conn.execute("ALTER TABLE _screens ADD COLUMN preview_aspect TEXT NOT NULL DEFAULT '9/16'")
action_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_actions)").fetchall()}
for col_name, col_def in [
("target_element_id", "INTEGER REFERENCES _screen_elements(id)"),
("element_property", "TEXT"),
("element_value", "TEXT"),
("target_definition_id", "INTEGER"),
("target_row_id", "INTEGER"),
("target_field", "TEXT"),
("data_operation", "TEXT"),
("data_value", "TEXT"),
]:
if col_name not in action_cols:
conn.execute(f"ALTER TABLE _actions ADD COLUMN {col_name} {col_def}")
conn.commit()
conn.close()
+8
View File
@@ -0,0 +1,8 @@
import db
def get_screen(slug, screen_id):
conn = db.connect(slug)
row = conn.execute("SELECT * FROM _screens WHERE id = ?", (screen_id,)).fetchone()
conn.close()
return dict(row) if row else None
+18
View File
@@ -0,0 +1,18 @@
import db
from .ensure_schema import ensure_schema
def list_screens(slug, include_templates=False):
"""Par défaut, n'affiche que les vrais écrans du jeu (pas les
écrans-modèles qui hébergent le contenu des éléments de jeu réutilisables
— voir ensure_schema) : c'est ce qu'on veut partout où on énumère "les
écrans du jeu" (liste, menu "aller à cet écran", mode jouable)."""
ensure_schema(slug)
conn = db.connect(slug)
if include_templates:
rows = conn.execute("SELECT * FROM _screens ORDER BY order_index, id").fetchall()
else:
rows = conn.execute("SELECT * FROM _screens WHERE is_template = 0 ORDER BY order_index, id").fetchall()
conn.close()
return [dict(r) for r in rows]
+21
View File
@@ -0,0 +1,21 @@
import db
from .list_screens import list_screens
def move_screen(slug, screen_id, direction):
"""direction: -1 (monter, l'écran devient plus tôt dans l'enchaînement)
ou +1 (descendre) — échange l'order_index avec l'écran voisin."""
screens_ = list_screens(slug)
idx = next((i for i, s in enumerate(screens_) if s["id"] == screen_id), None)
if idx is None:
return
swap_idx = idx + direction
if swap_idx < 0 or swap_idx >= len(screens_):
return
a, b = screens_[idx], screens_[swap_idx]
conn = db.connect(slug)
conn.execute("UPDATE _screens SET order_index = ? WHERE id = ?", (b["order_index"], a["id"]))
conn.execute("UPDATE _screens SET order_index = ? WHERE id = ?", (a["order_index"], b["id"]))
conn.commit()
conn.close()
+8
View File
@@ -0,0 +1,8 @@
import db
def rename_screen(slug, screen_id, new_name):
conn = db.connect(slug)
conn.execute("UPDATE _screens SET name = ? WHERE id = ?", (new_name, screen_id))
conn.commit()
conn.close()
@@ -0,0 +1,16 @@
import db
def screens_referencing(slug, screen_id):
"""Écrans dont une action cible (aller_a) précisément cet écran — pour
avertir avant suppression plutôt que de casser silencieusement le lien."""
conn = db.connect(slug)
rows = conn.execute(
"""SELECT DISTINCT s.id, s.name FROM _actions a
JOIN _screen_elements e ON e.id = a.element_id
JOIN _screens s ON s.id = e.screen_id
WHERE a.action_type = 'aller_a' AND a.target_screen_id = ?""",
(screen_id,),
).fetchall()
conn.close()
return [dict(r) for r in rows]
+20
View File
@@ -0,0 +1,20 @@
import db
from .ensure_schema import ensure_schema
_ALLOWED = {"9/16", "16/9", "1/1"}
def set_screen_aspect(slug, screen_id, aspect):
"""Mémorise le format d'aperçu choisi (Portrait/Paysage/Carré) pour cet
écran — voir le commentaire sur la colonne preview_aspect dans
ensure_schema.py. Ignore silencieusement une valeur inconnue plutôt que
de planter, au cas où le client enverrait autre chose que les trois
boutons prévus."""
if aspect not in _ALLOWED:
return
ensure_schema(slug)
conn = db.connect(slug)
conn.execute("UPDATE _screens SET preview_aspect = ? WHERE id = ?", (aspect, screen_id))
conn.commit()
conn.close()