first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations
|
||||
@@ -0,0 +1,5 @@
|
||||
from . import (
|
||||
animation_clip_add,
|
||||
animation_clip_update,
|
||||
animation_clip_delete,
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from . import (
|
||||
element_add,
|
||||
element_add_child,
|
||||
element_geometry,
|
||||
element_save,
|
||||
element_delete,
|
||||
element_rename,
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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})
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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})
|
||||
@@ -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})
|
||||
@@ -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))
|
||||
@@ -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})
|
||||
@@ -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})
|
||||
@@ -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})
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
from . import index, games_new, game_dashboard, game_rename, game_delete
|
||||
@@ -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)
|
||||
@@ -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"))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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())
|
||||
@@ -0,0 +1 @@
|
||||
from . import action_add, action_delete, action_run_data
|
||||
@@ -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))
|
||||
@@ -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"]))
|
||||
@@ -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})
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
from . import game_play
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
from . import screens_list, screens_new, screen_rename, screen_move, screen_delete, screen_edit, screen_set_aspect
|
||||
@@ -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))
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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})
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
from . import upload_file, uploaded_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)})
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user