Décision produit : seul l'export Web/SCORM (LMS) est pertinent — l'export exécutable Windows autonome (publish/build_package.py, bouton "Publier") et la partie publique par joueur (/jouer/<slug>, routes/public_play/, "Publier en ligne") sont jugés redondants et retirés. Conserve le mécanisme d'état "par joueur" (db/global_vars, db/rows, per_player) : infrastructure générique déjà utilisée par Score/Progression et testée indépendamment de toute route publique (voir tests/test_player_state.py), aucune raison de la retirer. _STATIC_ITEMS/_copy_characters (copie sélective des sprites CraftPix réellement utilisés) migrent de build_package.py vers build_scorm_package.py, seul appelant restant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
187 lines
9.9 KiB
HTML
187 lines
9.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Forge Engine{% endblock %}</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='branding/forge-favicon.svg') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
<script>window.CSRF_TOKEN = {{ csrf_token()|tojson }};</script>
|
|
<script src="{{ url_for('static', filename='csrf_fetch.js') }}"></script>
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="{% block body_class %}{% endblock %}">
|
|
<!-- #pageChrome : tout ce qui est en dehors de <main> mais doit quand même
|
|
changer d'une page à l'autre (barre du jeu comprise) — un seul bloc
|
|
stable pour que pjax.js puisse le remplacer d'un coup à chaque
|
|
navigation (voir swapDocument() dans pjax.js). Avant l'ajout de ce
|
|
conteneur, pjax.js cherchait un <header class="topbar"> qui n'a jamais
|
|
existé (la barre est un <nav>, pas un <header>) : la barre de
|
|
navigation du jeu (ajoutée après pjax.js) ne changeait donc jamais
|
|
pendant la navigation pjax — absente en arrivant sur un jeu tant qu'on
|
|
ne rechargeait pas la page en entier (Ctrl+F5), et inversement
|
|
laissée en place en revenant sur l'accueil (liste des jeux) où elle
|
|
n'a pourtant rien à faire. -->
|
|
<div id="pageChrome">
|
|
<nav class="navbar topbar" role="navigation" aria-label="main navigation">
|
|
<div class="navbar-brand">
|
|
<a href="{{ url_for('index') }}" class="navbar-item brand">
|
|
<img src="{{ url_for('static', filename='branding/forge-logo.svg') }}" alt="" width="32" height="32" style="border-radius:8px;">
|
|
<strong class="forge-gradient-title">Forge Engine</strong>
|
|
</a>
|
|
</div>
|
|
{% if g.get('current_user') %}
|
|
<div class="navbar-end" style="margin-left:auto; display:flex; align-items:center; gap:10px; padding-right:12px;">
|
|
<a href="{{ url_for('profile') }}" class="hint" style="text-decoration:underline;">{{ g.current_user.prenom }} {{ g.current_user.nom }}{{ ' (admin)' if g.current_user.role == 'admin' else '' }}</a>
|
|
<form method="post" action="{{ url_for('logout') }}"><button type="submit" class="button is-small">Déconnexion</button></form>
|
|
</div>
|
|
{% endif %}
|
|
</nav>
|
|
{% if game is defined and game %}
|
|
<!-- Barre toujours visible entre les différentes pages d'UN MÊME jeu.
|
|
Écrans/Éléments de jeu/Variables/Objets n'ont plus leur propre page —
|
|
tout ça vit désormais dans les onglets du tableau de bord (voir
|
|
game_dashboard.html) — donc un seul lien y mène. Remplace le fil
|
|
d'Ariane (retiré, devenu redondant) : n'apparaît que si la route a mis
|
|
un `game` dans le contexte du template (déjà fait par toutes les
|
|
pages d'un jeu) — absente sur l'accueil (liste des jeux). "Jouer" est
|
|
délibérément le DERNIER lien — une action à part (ouvre l'aperçu
|
|
jouable dans un nouvel onglet), pas une page de gestion de plus. -->
|
|
<nav class="gameNavBar" aria-label="Navigation du jeu">
|
|
<!-- Onboarding guidé (voir routes/onboarding/onboarding_new.py) : même
|
|
route pour tous les comptes — routes/games/game_dashboard.py y rend
|
|
une version simplifiée (juste les écrans) pour un type "restreint"
|
|
(quiz/embranchement/rpg), la version complète pour "custom". -->
|
|
<a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="{{ 'Écrans du jeu' if game.onboarding_type in ('quiz', 'embranchement', 'rpg') else 'Écrans, objets, templates, variables' }}">📊 Tableau de bord</a>
|
|
<a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Aperçu jouable du jeu (une seule partie partagée, pour la conception)">▶️ Jouer</a>
|
|
<!-- Phase 1.2 (feuille de route produit) : seul export retenu — paquet
|
|
STATIQUE (aucun Python/Flask embarqué, voir
|
|
publish/build_scorm_package.py), pensé pour un import dans le LMS
|
|
du client (Moodle...). L'export exécutable Windows ("Publier",
|
|
publish/build_package.py) et l'export en ligne (route publique
|
|
/jouer/<slug>, routes/public_play/) ont été retirés, jugés
|
|
redondants avec celui-ci. -->
|
|
<button type="button" class="gameNavLink" onclick="window.forgeOpenScormModal && window.forgeOpenScormModal()" title="Exporte le jeu en paquet Web/SCORM importable dans un LMS">🎓 Exporter (Web/SCORM)</button>
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
<main class="content {% block content_class %}{% endblock %}">
|
|
{% set recovery_codes = pop_recovery_codes() %}
|
|
{% if recovery_codes %}
|
|
<!-- À L'INTÉRIEUR de <main> (et pas à côté de #pageChrome) : pjax.js ne
|
|
remplace jamais que #pageChrome et main.content d'une navigation à
|
|
l'autre (voir swapDocument() dans pjax.js) — posé ailleurs, ce modal
|
|
serait bien présent dans le HTML renvoyé par le serveur juste après
|
|
l'inscription, mais jamais copié dans le DOM réellement affiché
|
|
puisque la redirection qui suit la confirmation de la 2FA passe par
|
|
une navigation pjax (fetch), pas un vrai rechargement de page. -->
|
|
<div class="modal is-active" id="recoveryCodesModal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-content">
|
|
<div class="box">
|
|
<h2 class="title is-4">Tes codes de récupération</h2>
|
|
<p class="hint mb-4">Note ces 10 codes en sécurité (gestionnaire de mots de passe, papier...) : chacun ne fonctionne qu'une seule fois et permet de te connecter si tu perds l'accès à ton application d'authentification. Ils ne seront plus jamais affichés.</p>
|
|
<div class="recoveryCodesGrid">
|
|
{% for code in recovery_codes %}
|
|
<code class="recoveryCode">{{ code }}</code>
|
|
{% endfor %}
|
|
</div>
|
|
<button type="button" class="button primary is-fullwidth mt-4" onclick="document.getElementById('recoveryCodesModal').remove()">J'ai noté mes codes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if game is defined and game %}
|
|
<!-- Même remarque que pour le modal des codes de récupération ci-dessus : à L'INTÉRIEUR
|
|
de <main> pour que pjax.js le remplace (et rejoue son script JS, voir
|
|
runScriptsIn()) à chaque navigation entre pages d'un même jeu — posé
|
|
dans #pageChrome (comme le bouton "🎓 Exporter (Web/SCORM)" qui
|
|
l'ouvre), son script ne serait rejoué qu'au tout premier chargement
|
|
complet de la page. -->
|
|
<style>
|
|
@keyframes forgePublishPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
|
</style>
|
|
<div class="modal" id="scormModal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-content">
|
|
<div class="box">
|
|
<h2 class="title is-4">Exporter « {{ game.name }} » en Web/SCORM</h2>
|
|
<p class="hint mb-4">Construit un paquet SCORM 1.2 (imsmanifest.xml + page web autonome, aucun serveur requis) : importe le .zip obtenu directement dans ton LMS (Moodle...) — le score et le statut de la partie remontent automatiquement.</p>
|
|
<div id="scormProgress">
|
|
<p class="hint" id="scormStepText">Préparation du jeu...</p>
|
|
<div class="passwordStrengthBar" style="margin-top:10px;"><div class="passwordStrengthFill strong" id="scormProgressFill" style="width:100%; animation:forgePublishPulse 1.2s ease-in-out infinite;"></div></div>
|
|
</div>
|
|
<div id="scormError" class="notification is-danger" style="display:none;"></div>
|
|
<div id="scormDone" class="notification is-success" style="display:none;">✅ Téléchargement lancé — importe directement le .zip dans ton LMS.</div>
|
|
<div class="field mt-4" style="display:flex; justify-content:flex-end;">
|
|
<button type="button" class="button is-small" onclick="window.forgeCloseScormModal()">Fermer</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var modal = document.getElementById('scormModal');
|
|
var stepText = document.getElementById('scormStepText');
|
|
var progressBox = document.getElementById('scormProgress');
|
|
var errorBox = document.getElementById('scormError');
|
|
var doneBox = document.getElementById('scormDone');
|
|
var steps = ["Préparation du jeu...", "Génération du paquet hors ligne...", "Écriture du manifeste SCORM..."];
|
|
var stepTimer = null;
|
|
|
|
window.forgeOpenScormModal = function () {
|
|
var stepIndex = 0;
|
|
stepText.textContent = steps[0];
|
|
progressBox.style.display = '';
|
|
errorBox.style.display = 'none';
|
|
doneBox.style.display = 'none';
|
|
modal.classList.add('is-active');
|
|
|
|
stepTimer = setInterval(function () {
|
|
stepIndex = (stepIndex + 1) % steps.length;
|
|
stepText.textContent = steps[stepIndex];
|
|
}, 1200);
|
|
|
|
var filename = "{{ game.slug }}-scorm.zip";
|
|
fetch("{{ url_for('export_scorm', slug=game.slug) }}", { method: 'POST' })
|
|
.then(function (resp) {
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
var disposition = resp.headers.get('Content-Disposition') || '';
|
|
var match = /filename="?([^";]+)"?/.exec(disposition);
|
|
if (match) filename = match[1];
|
|
return resp.blob();
|
|
})
|
|
.then(function (blob) {
|
|
clearInterval(stepTimer);
|
|
progressBox.style.display = 'none';
|
|
doneBox.style.display = '';
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
})
|
|
.catch(function (err) {
|
|
clearInterval(stepTimer);
|
|
progressBox.style.display = 'none';
|
|
errorBox.style.display = '';
|
|
errorBox.textContent = "Échec de l'export (" + err.message + "). Réessaie.";
|
|
});
|
|
};
|
|
|
|
window.forgeCloseScormModal = function () {
|
|
modal.classList.remove('is-active');
|
|
if (stepTimer) clearInterval(stepTimer);
|
|
};
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
<script src="{{ url_for('static', filename='pjax.js') }}"></script>
|
|
</body>
|
|
</html>
|