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 @@
from . import games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations
+5
View File
@@ -0,0 +1,5 @@
from . import (
animation_clip_add,
animation_clip_update,
animation_clip_delete,
)
+19
View File
@@ -0,0 +1,19 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/animations/clips/add", methods=["POST"])
def animation_clip_add(slug, screen_id):
"""Ajoute un clip d'animation (Animate.css ou personnalisé) sur la piste
d'un élément, dans le panneau timeline — appelé en AJAX, comme l'ajout
d'un nœud de logique."""
payload = request.get_json(force=True) or {}
element_id = payload.get("element_id")
if not element_id:
return jsonify({"error": "élément manquant"}), 400
fields = {k: v for k, v in payload.items() if k != "element_id"}
clip_id = screens.add_animation_clip(slug, screen_id, int(element_id), **fields)
return jsonify(screens.get_animation_clip(slug, clip_id))
@@ -0,0 +1,11 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/animations/clips/<int:clip_id>/delete", methods=["POST"])
def animation_clip_delete(slug, clip_id):
screens.delete_animation_clip(slug, clip_id)
return jsonify({"ok": True})
@@ -0,0 +1,16 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/animations/clips/<int:clip_id>/update", methods=["POST"])
def animation_clip_update(slug, clip_id):
"""Modifie un clip existant — glisser/redimensionner sur la timeline
n'envoie que start_time/duration, le formulaire d'édition peut envoyer
n'importe quel autre réglage (type, animation Animate.css, images-clés
personnalisées, délai, vitesse, répétitions)."""
payload = request.get_json(force=True) or {}
screens.update_animation_clip(slug, clip_id, **payload)
return jsonify(screens.get_animation_clip(slug, clip_id))
+1
View File
@@ -0,0 +1 @@
from . import element_types, element_type_edit, element_type_delete
@@ -0,0 +1,12 @@
from flask import redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/element-types/<int:element_type_id>/delete", methods=["POST"])
def element_type_delete(slug, element_type_id):
if screens.element_type_usage_count(slug, element_type_id) == 0:
screens.delete_element_type(slug, element_type_id)
return redirect(url_for("element_types", slug=slug))
+16
View File
@@ -0,0 +1,16 @@
from flask import request, redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/element-types/<int:element_type_id>/edit", methods=["POST"])
def element_type_edit(slug, element_type_id):
"""Renomme un élément de jeu et/ou change l'objet auquel il est lié —
son contenu (l'arbre d'éléments) se modifie séparément, via le lien
"✏️ Modifier le contenu" qui ouvre l'écran-modèle dans l'éditeur normal."""
name = request.form.get("name", "").strip() or None
bound_definition_id = request.form.get("bound_definition_id") or None
screens.update_element_type(slug, element_type_id, name=name, bound_definition_id=bound_definition_id)
return redirect(url_for("element_types", slug=slug))
+25
View File
@@ -0,0 +1,25 @@
from flask import render_template, request, redirect, url_for
import db
import screens
from core.flask_app import app
@app.route("/game/<slug>/element-types", methods=["GET", "POST"])
def element_types(slug):
game = db.game_meta(slug)
if request.method == "POST":
name = request.form.get("name", "").strip()
icon = request.form.get("icon", "").strip() or "🧩"
bound_definition_id = request.form.get("bound_definition_id") or None
if name:
screens.create_element_type(slug, name, icon, bound_definition_id)
return redirect(url_for("element_types", slug=slug))
types = screens.list_element_types(slug)
usage = {t["id"]: screens.element_type_usage_count(slug, t["id"]) for t in types}
definitions = db.list_definitions(slug)
return render_template(
"element_types.html", game=game, types=types, usage=usage, widgets=screens.WIDGETS,
definitions=definitions,
)
+8
View File
@@ -0,0 +1,8 @@
from . import (
element_add,
element_add_child,
element_geometry,
element_save,
element_delete,
element_rename,
)
+20
View File
@@ -0,0 +1,20 @@
from flask import request, redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/elements/add", methods=["POST"])
def element_add(slug, screen_id):
widget = request.form.get("widget", "conteneur")
from_catalogue = widget == "__catalogue__"
element_type_id = request.form.get("element_type_id") or None if from_catalogue else None
if from_catalogue and not element_type_id:
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id))
if from_catalogue:
types = {t["id"]: t for t in screens.list_element_types(slug)}
et = types.get(int(element_type_id))
widget = et["default_widget"] if et else "conteneur"
new_id = screens.add_element(slug, screen_id, widget, element_type_id)
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id))
+25
View File
@@ -0,0 +1,25 @@
from flask import request, redirect, url_for, abort
import screens
from core.flask_app import app
@app.route("/game/<slug>/elements/<int:parent_id>/children/add", methods=["POST"])
def element_add_child(slug, parent_id):
"""Ajoute un élément PHYSIQUEMENT à l'intérieur d'un conteneur, d'un
répéteur de données ou d'un groupe de champs déjà posé sur l'écran."""
parent = screens.get_element(slug, parent_id)
if not parent:
abort(404)
widget = request.form.get("widget", "conteneur")
from_catalogue = widget == "__catalogue__"
element_type_id = request.form.get("element_type_id") or None if from_catalogue else None
if from_catalogue and element_type_id:
types = {t["id"]: t for t in screens.list_element_types(slug)}
et = types.get(int(element_type_id))
widget = et["default_widget"] if et else "conteneur"
elif from_catalogue:
return redirect(url_for("screen_edit", slug=slug, screen_id=parent["screen_id"], selected=parent_id))
new_id = screens.add_element(slug, parent["screen_id"], widget, element_type_id, parent_id=parent_id)
return redirect(url_for("screen_edit", slug=slug, screen_id=parent["screen_id"], selected=new_id))
+22
View File
@@ -0,0 +1,22 @@
from flask import redirect, url_for, abort
import screens
from core.flask_app import app
@app.route("/game/<slug>/elements/<int:element_id>/delete", methods=["POST"])
def element_delete(slug, element_id):
element = screens.get_element(slug, element_id)
if not element:
abort(404)
screen_id = element["screen_id"]
parent_id = element.get("parent_id")
screens.delete_element(slug, element_id)
# Resélectionne le conteneur parent (s'il y en a un) après suppression,
# plutôt que de laisser le panneau de droite vide : sinon, supprimer un
# élément fait brusquement "sauter" l'éditeur vers un état sans rien de
# sélectionné.
if parent_id:
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=parent_id))
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id))
+20
View File
@@ -0,0 +1,20 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/elements/<int:element_id>/geometry", methods=["POST"])
def element_geometry(slug, element_id):
"""Appelé en AJAX pendant/à la fin d'un glisser-déposer, d'un
redimensionnement, ou d'une modification des champs Position & taille —
ne touche qu'à la position/taille, aucun rechargement de page nécessaire
pour que ça reste fluide."""
data = request.get_json(force=True)
screens.update_element_geometry(
slug, element_id,
x=float(data["x"]), y=float(data["y"]),
width=float(data["width"]), height=float(data["height"]),
)
return jsonify({"ok": True})
+14
View File
@@ -0,0 +1,14 @@
from flask import request, redirect, url_for, abort
import screens
from core.flask_app import app
@app.route("/game/<slug>/elements/<int:element_id>/rename", methods=["POST"])
def element_rename(slug, element_id):
element = screens.get_element(slug, element_id)
if not element:
abort(404)
screens.rename_element(slug, element_id, request.form.get("name", ""))
return redirect(url_for("screen_edit", slug=slug, screen_id=element["screen_id"], selected=element_id))
+18
View File
@@ -0,0 +1,18 @@
from flask import request, redirect, url_for, abort
import screens
from core.flask_app import app
@app.route("/game/<slug>/elements/<int:element_id>/save", methods=["POST"])
def element_save(slug, element_id):
"""Un seul formulaire pour tout le panneau de propriétés : un champ par
réglage du widget (texte, case à cocher, curseur, couleur...) — c'est
screens.save_element_controls qui sait vers quel attribut ou quelle
propriété CSS chaque réglage doit être traduit."""
element = screens.get_element(slug, element_id)
if not element:
abort(404)
screens.save_element_controls(slug, element_id, request.form)
return redirect(url_for("screen_edit", slug=slug, screen_id=element["screen_id"], selected=element_id))
+9
View File
@@ -0,0 +1,9 @@
from . import (
flow_node_add,
flow_node_move,
flow_node_delete,
flow_edge_add,
flow_edge_delete,
flow_node_run_data,
runtime_payload,
)
+17
View File
@@ -0,0 +1,17 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/flow/edges/add", methods=["POST"])
def flow_edge_add(slug, screen_id):
payload = request.get_json(force=True) or {}
try:
from_node_id = int(payload["from_node_id"])
to_node_id = int(payload["to_node_id"])
except (KeyError, TypeError, ValueError):
return jsonify({"error": "nœuds invalides"}), 400
edge_id = screens.add_flow_edge(slug, screen_id, from_node_id, payload.get("from_port", "out"), to_node_id)
return jsonify({"id": edge_id})
+11
View File
@@ -0,0 +1,11 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/flow/edges/<int:edge_id>/delete", methods=["POST"])
def flow_edge_delete(slug, edge_id):
screens.delete_flow_edge(slug, edge_id)
return jsonify({"ok": True})
+21
View File
@@ -0,0 +1,21 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/flow/nodes/add", methods=["POST"])
def flow_node_add(slug, screen_id):
"""Ajoute un nœud (Déclencheur / Condition / Action) au graphe de
logique de cet écran — appelé en AJAX depuis le panneau "Logique de la
scène", pour que poser un nœud ne recharge jamais toute la page."""
payload = request.get_json(force=True) or {}
node_type = payload.get("node_type")
if node_type not in ("trigger", "condition", "action"):
return jsonify({"error": "type de nœud invalide"}), 400
x = float(payload.get("x", 40))
y = float(payload.get("y", 40))
fields = {k: v for k, v in payload.items() if k not in ("node_type", "x", "y")}
node_id = screens.add_flow_node(slug, screen_id, node_type, x=x, y=y, **fields)
return jsonify(screens.get_flow_node(slug, node_id))
+11
View File
@@ -0,0 +1,11 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/flow/nodes/<int:node_id>/delete", methods=["POST"])
def flow_node_delete(slug, node_id):
screens.delete_flow_node(slug, node_id)
return jsonify({"ok": True})
+12
View File
@@ -0,0 +1,12 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/flow/nodes/<int:node_id>/move", methods=["POST"])
def flow_node_move(slug, node_id):
payload = request.get_json(force=True) or {}
screens.move_flow_node(slug, node_id, float(payload.get("x", 0)), float(payload.get("y", 0)))
return jsonify({"ok": True})
+18
View File
@@ -0,0 +1,18 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/flow/nodes/<int:node_id>/run-data", methods=["POST"])
def flow_node_run_data(slug, node_id):
"""Exécuté côté serveur au moment du clic, en mode jouable, pour un nœud
Action de type "Modifier une donnée d'un objet" — identique au
fonctionnement de l'ancienne action_run_data, mais lit sa config depuis
un nœud du graphe de logique plutôt que depuis la table _actions."""
node = screens.get_flow_node(slug, node_id)
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee":
return jsonify({"ok": False}), 400
ok = screens.apply_data_action(slug, node)
return jsonify({"ok": ok})
+13
View File
@@ -0,0 +1,13 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/runtime-payload")
def runtime_payload(slug):
"""Utilisé par le mode jouable pour se remettre à jour après une action
qui a modifié une donnée d'objet (ex: un Répéteur affichant cet objet
doit refléter la nouvelle valeur sans recharger toute la page)."""
return jsonify(screens.full_game_payload(slug))
+1
View File
@@ -0,0 +1 @@
from . import index, games_new, game_dashboard, game_rename, game_delete
+12
View File
@@ -0,0 +1,12 @@
from flask import render_template
import db
from core.flask_app import app
@app.route("/game/<slug>")
def game_dashboard(slug):
game = db.game_meta(slug)
definitions = db.list_definitions(slug)
return render_template("game_dashboard.html", game=game, definitions=definitions)
+11
View File
@@ -0,0 +1,11 @@
from flask import redirect, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/delete", methods=["POST"])
def game_delete(slug):
db.delete_game(slug)
return redirect(url_for("index"))
+13
View File
@@ -0,0 +1,13 @@
from flask import request, redirect, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/rename", methods=["POST"])
def game_rename(slug):
new_name = request.form.get("name", "").strip()
if new_name:
db.update_game_name(slug, new_name)
return redirect(url_for("game_dashboard", slug=slug))
+14
View File
@@ -0,0 +1,14 @@
from flask import request, redirect, url_for
import db
from core.flask_app import app
@app.route("/games/new", methods=["POST"])
def games_new():
name = request.form.get("name", "").strip()
if not name:
return redirect(url_for("index"))
slug = db.create_game(name)
return redirect(url_for("game_dashboard", slug=slug))
+10
View File
@@ -0,0 +1,10 @@
from flask import render_template
import db
from core.flask_app import app
@app.route("/")
def index():
return render_template("index.html", games=db.list_games())
+1
View File
@@ -0,0 +1 @@
from . import action_add, action_delete, action_run_data
+35
View File
@@ -0,0 +1,35 @@
from flask import request, redirect, url_for, abort
import screens
from core.flask_app import app
@app.route("/game/<slug>/elements/<int:element_id>/actions/add", methods=["POST"])
def action_add(slug, element_id):
element = screens.get_element(slug, element_id)
if not element:
abort(404)
trigger_event = screens._trigger_for(element["widget"], element["attributes"])
action_type = request.form.get("action_type", "rien")
def _int_or_none(key):
raw = request.form.get(key)
try:
return int(raw) if raw not in (None, "") else None
except (TypeError, ValueError):
return None
screens.add_action(
slug, element_id, trigger_event, action_type,
target_screen_id=_int_or_none("target_screen_id"),
target_element_id=_int_or_none("target_element_id"),
element_property=request.form.get("element_property") or None,
element_value=request.form.get("element_value") or None,
target_definition_id=_int_or_none("target_definition_id"),
target_row_id=_int_or_none("target_row_id"),
target_field=request.form.get("target_field") or None,
data_operation=request.form.get("data_operation") or None,
data_value=request.form.get("data_value") or None,
)
return redirect(url_for("screen_edit", slug=slug, screen_id=element["screen_id"], selected=element_id))
+15
View File
@@ -0,0 +1,15 @@
from flask import redirect, url_for, abort
import screens
from core.flask_app import app
@app.route("/game/<slug>/actions/<int:action_id>/delete", methods=["POST"])
def action_delete(slug, action_id):
action = screens.get_action(slug, action_id)
if not action:
abort(404)
element = screens.get_element(slug, action["element_id"])
screens.delete_action(slug, action_id)
return redirect(url_for("screen_edit", slug=slug, screen_id=element["screen_id"], selected=element["id"]))
+17
View File
@@ -0,0 +1,17 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/actions/<int:action_id>/run-data", methods=["POST"])
def action_run_data(slug, action_id):
"""Exécuté côté serveur au moment du clic, en mode jouable, pour une
action "Modifier une donnée d'un objet" — les actions "Modifier un
élément" restent, elles, purement côté client (juste du style CSS)."""
action = screens.get_action(slug, action_id)
if not action or action.get("action_type") != "modifier_donnee":
return jsonify({"ok": False}), 400
ok = screens.apply_data_action(slug, action)
return jsonify({"ok": ok})
+14
View File
@@ -0,0 +1,14 @@
from . import (
parse_field_rows,
object_new,
object_view,
object_edit,
object_field_add,
object_field_edit,
object_field_delete,
object_delete,
data_form,
data_new,
data_edit,
data_delete,
)
+20
View File
@@ -0,0 +1,20 @@
from flask import redirect, url_for, abort
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>/data/<int:row_id>/delete", methods=["POST"])
def data_delete(slug, definition_id, row_id):
definition = db.get_definition(slug, definition_id)
if not definition:
abort(404)
referencing = db.rows_referencing(slug, definition_id, row_id)
if referencing:
# Suppression bloquée : d'autres lignes pointent vers celle-ci via
# une relation. On revient à la liste, qui explique pourquoi via un
# paramètre de requête simple plutôt qu'une session/flash message.
return redirect(url_for("object_view", slug=slug, definition_id=definition_id, blocked_row=row_id))
db.delete_row(slug, definition, row_id)
return redirect(url_for("object_view", slug=slug, definition_id=definition_id))
+17
View File
@@ -0,0 +1,17 @@
from flask import abort
import db
from core.flask_app import app
from .data_form import _data_form
@app.route("/game/<slug>/objects/<int:definition_id>/data/<int:row_id>/edit", methods=["GET", "POST"])
def data_edit(slug, definition_id, row_id):
definition = db.get_definition(slug, definition_id)
if not definition:
abort(404)
row = db.get_row(slug, definition, row_id)
if not row:
abort(404)
return _data_form(slug, definition_id, row=row)
+32
View File
@@ -0,0 +1,32 @@
from flask import render_template, request, redirect, url_for, abort
import db
def _data_form(slug, definition_id, row):
game = db.game_meta(slug)
definition = db.get_definition(slug, definition_id)
if not definition:
abort(404)
# Pour chaque champ de type relation, on prépare les options du <select>
# à partir des lignes déjà enregistrées de l'objet lié.
relation_options = {}
for f in definition["fields"]:
if f["type"] == "relation":
relation_options[f["id"]] = db.relation_options(slug, f["relation_definition_id"])
if request.method == "POST":
if row is None:
db.insert_row(slug, definition, request.form)
else:
db.update_row(slug, definition, row["id"], request.form)
return redirect(url_for("object_view", slug=slug, definition_id=definition_id))
return render_template(
"data_form.html",
game=game,
definition=definition,
relation_options=relation_options,
row=row,
)
+7
View File
@@ -0,0 +1,7 @@
from core.flask_app import app
from .data_form import _data_form
@app.route("/game/<slug>/objects/<int:definition_id>/data/new", methods=["GET", "POST"])
def data_new(slug, definition_id):
return _data_form(slug, definition_id, row=None)
+18
View File
@@ -0,0 +1,18 @@
from flask import redirect, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>/delete", methods=["POST"])
def object_delete(slug, definition_id):
referenced_by = db.definitions_referencing(slug, definition_id)
if referenced_by:
# Suppression bloquée : d'autres objets ont une relation vers
# celui-ci. On revient à l'écran de l'objet, qui affiche déjà cet
# avertissement (referenced_by), plutôt que de casser ces relations
# silencieusement.
return redirect(url_for("object_view", slug=slug, definition_id=definition_id))
db.delete_definition(slug, definition_id)
return redirect(url_for("game_dashboard", slug=slug))
+28
View File
@@ -0,0 +1,28 @@
from flask import render_template, request, redirect, url_for, abort
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>/edit", methods=["GET", "POST"])
def object_edit(slug, definition_id):
game = db.game_meta(slug)
definition = db.get_definition(slug, definition_id)
if not definition:
abort(404)
existing_definitions = [d for d in db.list_definitions(slug) if d["id"] != definition_id]
if request.method == "POST":
new_name = request.form.get("object_name", "").strip()
if new_name and new_name != definition["name"]:
db.rename_definition(slug, definition_id, new_name)
return redirect(url_for("object_edit", slug=slug, definition_id=definition_id))
return render_template(
"object_edit.html",
game=game,
definition=definition,
existing_definitions=existing_definitions,
field_types=db.FIELD_TYPES,
)
+20
View File
@@ -0,0 +1,20 @@
from flask import request, redirect, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>/fields/add", methods=["POST"])
def object_field_add(slug, definition_id):
name = request.form.get("field_name", "").strip()
if name:
db.add_field_to_definition(slug, definition_id, {
"name": name,
"type": request.form.get("field_type", "texte"),
"relation_definition_id": request.form.get("field_relation"),
"required": request.form.get("field_required") == "1",
"min_value": request.form.get("field_min") or None,
"max_value": request.form.get("field_max") or None,
})
return redirect(url_for("object_edit", slug=slug, definition_id=definition_id))
+11
View File
@@ -0,0 +1,11 @@
from flask import redirect, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>/fields/<int:field_id>/delete", methods=["POST"])
def object_field_delete(slug, definition_id, field_id):
db.delete_field(slug, definition_id, field_id)
return redirect(url_for("object_edit", slug=slug, definition_id=definition_id))
+20
View File
@@ -0,0 +1,20 @@
from flask import request, redirect, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>/fields/<int:field_id>/edit", methods=["POST"])
def object_field_edit(slug, definition_id, field_id):
new_name = request.form.get("field_name", "").strip()
if new_name:
db.update_field(
slug, definition_id, field_id,
new_name=new_name,
required=request.form.get("field_required") == "1",
relation_definition_id=request.form.get("field_relation"),
min_value=request.form.get("field_min") or None,
max_value=request.form.get("field_max") or None,
)
return redirect(url_for("object_edit", slug=slug, definition_id=definition_id))
+27
View File
@@ -0,0 +1,27 @@
from flask import render_template, request, redirect, url_for
import db
from core.flask_app import app
from .parse_field_rows import _parse_field_rows
@app.route("/game/<slug>/objects/new", methods=["GET", "POST"])
def object_new(slug):
game = db.game_meta(slug)
existing_definitions = db.list_definitions(slug)
if request.method == "POST":
name = request.form.get("object_name", "").strip()
fields = _parse_field_rows(request.form)
if name and fields:
definition_id = db.create_definition(slug, name, fields)
return redirect(url_for("object_view", slug=slug, definition_id=definition_id))
return render_template(
"object_form.html",
game=game,
existing_definitions=existing_definitions,
field_types=db.FIELD_TYPES,
)
+33
View File
@@ -0,0 +1,33 @@
from flask import render_template, request, abort
import db
from core.flask_app import app
@app.route("/game/<slug>/objects/<int:definition_id>")
def object_view(slug, definition_id):
game = db.game_meta(slug)
definition = db.get_definition(slug, definition_id)
if not definition:
abort(404)
rows = db.list_rows(slug, definition)
# Pour l'affichage : traduit chaque id de relation en un libellé lisible
# (ex. "Salle serveur (#1)") au lieu d'un identifiant numérique brut.
relation_labels = {}
for f in definition["fields"]:
if f["type"] == "relation":
options = db.relation_options(slug, f["relation_definition_id"])
relation_labels[f["id"]] = {opt["id"]: opt["label"] for opt in options}
referenced_by = db.definitions_referencing(slug, definition_id)
blocked_row_id = request.args.get("blocked_row", type=int)
blocked_row_refs = db.rows_referencing(slug, definition_id, blocked_row_id) if blocked_row_id else None
return render_template(
"data_list.html", game=game, definition=definition, rows=rows,
relation_labels=relation_labels, referenced_by=referenced_by,
blocked_row_id=blocked_row_id, blocked_row_refs=blocked_row_refs,
)
+20
View File
@@ -0,0 +1,20 @@
def _parse_field_rows(form):
field_names = form.getlist("field_name[]")
field_types = form.getlist("field_type[]")
field_relations = form.getlist("field_relation[]")
field_required = form.getlist("field_required[]")
field_min = form.getlist("field_min[]")
field_max = form.getlist("field_max[]")
fields = []
for i, fname in enumerate(field_names):
if not fname.strip():
continue
fields.append({
"name": fname.strip(),
"type": field_types[i],
"relation_definition_id": field_relations[i] if i < len(field_relations) else None,
"required": field_required[i] == "1" if i < len(field_required) else False,
"min_value": (field_min[i].strip() or None) if i < len(field_min) else None,
"max_value": (field_max[i].strip() or None) if i < len(field_max) else None,
})
return fields
+1
View File
@@ -0,0 +1 @@
from . import game_play
+18
View File
@@ -0,0 +1,18 @@
import json
from flask import render_template
import db
import screens
from core.flask_app import app
@app.route("/game/<slug>/play")
def game_play(slug):
game = db.game_meta(slug)
payload = screens.full_game_payload(slug)
return render_template(
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
google_fonts_link=screens.GOOGLE_FONTS_LINK,
)
+1
View File
@@ -0,0 +1 @@
from . import screens_list, screens_new, screen_rename, screen_move, screen_delete, screen_edit, screen_set_aspect
+14
View File
@@ -0,0 +1,14 @@
from flask import redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/delete", methods=["POST"])
def screen_delete(slug, screen_id):
referencing = screens.screens_referencing(slug, screen_id)
if referencing:
return redirect(url_for("screens_list", slug=slug, blocked_screen=screen_id))
screens.delete_screen(slug, screen_id)
return redirect(url_for("screens_list", slug=slug))
+90
View File
@@ -0,0 +1,90 @@
import json
from flask import render_template, request, abort
import db
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/edit")
def screen_edit(slug, screen_id):
game = db.game_meta(slug)
screen = screens.get_screen(slug, screen_id)
if not screen:
abort(404)
elements = screens.list_elements(slug, screen_id)
all_screens = screens.list_screens(slug)
element_types = screens.list_element_types(slug)
selected_id = request.args.get("selected", type=int)
selected_controls = None
if selected_id:
selected_el = next((e for e in elements if e["id"] == selected_id), None)
if selected_el:
selected_controls = screens.controls_with_values(slug, selected_el)
children_of = {}
for el in elements:
if el.get("parent_id"):
children_of.setdefault(el["parent_id"], []).append(el)
top_level_elements = [el for el in elements if not el.get("parent_id")]
elements_by_id = {el["id"]: el for el in elements}
definitions = db.list_definitions(slug)
definitions_data = {}
for d in definitions:
full = db.get_definition(slug, d["id"])
rows = db.list_rows(slug, full)
display_field = full["fields"][0]["name"] if full["fields"] else None
display_col = db.slugify(display_field).replace("-", "_") if display_field else None
row_options = [
{
"id": r["id"],
"label": f"{r.get(display_col)} (#{r['id']})" if display_col else f"Ligne #{r['id']}",
}
for r in rows
]
field_options = [{"name": f["name"], "type": f["type"]} for f in full["fields"] if f["type"] != "relation"]
definitions_data[str(d["id"])] = {"rows": row_options, "fields": field_options}
flow_nodes = screens.list_flow_nodes(slug, screen_id)
flow_edges = screens.list_flow_edges(slug, screen_id)
elements_labels = {el["id"]: screens.element_label(el, screens.WIDGETS) for el in elements}
screens_map = {s["id"]: s["name"] for s in all_screens}
animation_clips = screens.list_animation_clips(slug, screen_id)
return render_template(
"screen_edit.html",
game=game, screen=screen, elements=elements, top_level_elements=top_level_elements,
children_of=children_of, container_widgets=screens.CONTAINER_WIDGETS,
elements_by_id=elements_by_id,
all_screens=all_screens,
element_types=element_types, widgets=screens.WIDGETS,
trigger_labels=screens.TRIGGER_LABELS, action_type_labels=screens.ACTION_TYPE_LABELS,
element_action_properties=screens.ELEMENT_ACTION_PROPERTIES,
element_action_property_labels=screens.ELEMENT_ACTION_PROPERTY_LABELS,
element_visibility_values=screens.ELEMENT_VISIBILITY_VALUES,
element_visibility_labels=screens.ELEMENT_VISIBILITY_LABELS,
element_surbrillance_values=screens.ELEMENT_SURBRILLANCE_VALUES,
element_desactive_values=screens.ELEMENT_DESACTIVE_VALUES,
data_operations=screens.DATA_OPERATIONS,
data_operation_labels=screens.DATA_OPERATION_LABELS,
definitions=definitions, definitions_json=json.dumps(definitions_data),
selected_id=selected_id, selected_controls=selected_controls,
google_fonts_link=screens.GOOGLE_FONTS_LINK,
flow_nodes=flow_nodes, flow_edges=flow_edges,
flow_nodes_json=json.dumps(flow_nodes), flow_edges_json=json.dumps(flow_edges),
trigger_events=screens.TRIGGER_EVENTS, condition_operators=screens.CONDITION_OPERATORS,
elements_labels_json=json.dumps(elements_labels), screens_map_json=json.dumps(screens_map),
action_type_labels_json=json.dumps(screens.ACTION_TYPE_LABELS),
element_action_property_labels_json=json.dumps(screens.ELEMENT_ACTION_PROPERTY_LABELS),
element_visibility_labels_json=json.dumps(screens.ELEMENT_VISIBILITY_LABELS),
element_surbrillance_labels_json=json.dumps(screens.ELEMENT_SURBRILLANCE_LABELS),
element_desactive_labels_json=json.dumps(screens.ELEMENT_DESACTIVE_LABELS),
data_operation_labels_json=json.dumps(screens.DATA_OPERATION_LABELS),
condition_operator_labels_json=json.dumps(screens.CONDITION_OPERATOR_LABELS),
trigger_labels_json=json.dumps(screens.TRIGGER_LABELS),
animation_clips=animation_clips, animation_clips_json=json.dumps(animation_clips),
animate_css_catalog=screens.ANIMATE_CSS_CATALOG,
animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS),
)
+12
View File
@@ -0,0 +1,12 @@
from flask import request, redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/move", methods=["POST"])
def screen_move(slug, screen_id):
direction = -1 if request.form.get("direction") == "up" else 1
screens.move_screen(slug, screen_id, direction)
return redirect(url_for("screens_list", slug=slug))
+13
View File
@@ -0,0 +1,13 @@
from flask import request, redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/rename", methods=["POST"])
def screen_rename(slug, screen_id):
new_name = request.form.get("name", "").strip()
if new_name:
screens.rename_screen(slug, screen_id, new_name)
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id))
+17
View File
@@ -0,0 +1,17 @@
from flask import request, jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/<int:screen_id>/aspect", methods=["POST"])
def screen_set_aspect(slug, screen_id):
"""Mémorise le format d'aperçu (Portrait/Paysage/Carré) choisi pour cet
écran, en AJAX — sans recharger la page, pour ne jamais perdre ce choix
au prochain ajout/suppression d'élément ou changement de sélection (qui
redemandent le panneau au serveur)."""
body = request.get_json(silent=True) or {}
aspect = body.get("aspect", "")
screens.set_screen_aspect(slug, screen_id, aspect)
return jsonify({"ok": True})
+12
View File
@@ -0,0 +1,12 @@
from flask import render_template
import db
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens")
def screens_list(slug):
game = db.game_meta(slug)
return render_template("screens_list.html", game=game, screen_list=screens.list_screens(slug))
+14
View File
@@ -0,0 +1,14 @@
from flask import request, redirect, url_for
import screens
from core.flask_app import app
@app.route("/game/<slug>/screens/new", methods=["POST"])
def screens_new(slug):
name = request.form.get("name", "").strip()
if name:
screen_id = screens.create_screen(slug, name)
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id))
return redirect(url_for("screens_list", slug=slug))
+1
View File
@@ -0,0 +1 @@
from . import upload_file, uploaded_file
+25
View File
@@ -0,0 +1,25 @@
import os
import uuid
from flask import request, jsonify, url_for
import db
from core.flask_app import app
@app.route("/game/<slug>/upload", methods=["POST"])
def upload_file(slug):
"""Utilisé par les réglages Image/Vidéo pour héberger un fichier
envoyé par l'utilisateur (au lieu d'une simple adresse URL) — le
fichier est stocké dans le dossier du jeu, et l'adresse renvoyée est
ensuite utilisée exactement comme une URL normale."""
f = request.files.get("file")
if not f or not f.filename:
return jsonify({"error": "Aucun fichier reçu"}), 400
ext = "".join(c for c in os.path.splitext(f.filename)[1].lower() if c.isalnum() or c == ".")[:10]
filename = uuid.uuid4().hex + ext
upload_dir = os.path.join(db.game_dir(slug), "uploads")
os.makedirs(upload_dir, exist_ok=True)
f.save(os.path.join(upload_dir, filename))
return jsonify({"url": url_for("uploaded_file", slug=slug, filename=filename)})
+12
View File
@@ -0,0 +1,12 @@
import os
from flask import send_from_directory
import db
from core.flask_app import app
@app.route("/game/<slug>/uploads/<path:filename>")
def uploaded_file(slug, filename):
return send_from_directory(os.path.join(db.game_dir(slug), "uploads"), filename)