Ajoute "Publier" : exporte un jeu en exécutable Windows autonome
Fonctionnalité mise de côté depuis le tout début de ce chantier
("jouable en toute autonomie"). Nouveau bouton "📦 Publier" dans la barre
de navigation du jeu (après "▶️ Jouer") : construit un zip contenant un
Python portable + Flask embarqués, une copie figée du moteur de rendu
(screens/db/filters + le strict minimum de core/) et des données du jeu
(game.db + uploads/), lancé en double-cliquant sur run.bat — aucune
installation requise, 100% hors-ligne (voir le commit précédent qui a
auto-hébergé polices/animate.css, dernière dépendance CDN de l'app).
Exploration préalable a confirmé que screens/ et db/ sont déjà
totalement découplés de auth/routes/core (seul lien : un try/except
optionnel dans db/connection.py) et que templates/play.html n'appelle
que 4 endpoints "purs" (aucune logique d'auth mélangée dedans) — ce qui
a permis de réutiliser ces routes quasiment telles quelles dans un
mini-serveur Flask séparé plutôt que de les réécrire.
- db/constants.py : PROJECTS_DIR devient surchargeable via
FORGE_PROJECTS_DIR (même schéma que auth/connection.py) — le serveur
joueur autonome pointe ainsi vers son propre dossier "projects/"
embarqué.
- publish/vendor_runtime.py : télécharge (une fois par poste, mis en
cache sous data/publish_vendor/ — déjà ignoré par git) le ZIP Python
embeddable officiel (python.org) et vendore Flask via pip --target ;
fonctions séparées et mockables pour ne jamais déclencher de vrai
téléchargement dans les tests.
- publish/player_app_template.py : mini-Flask autonome, réutilise
core/flask_app.py et core/jinja_filters.py tels quels (SLUG figé en
dur au moment de la publication, csrf_token() factice puisqu'aucune
session n'existe dans cet export). sys.path doit être complété
manuellement au démarrage : le python311._pth de la distribution
embeddable ne référence que le dossier de python.exe lui-même, jamais
celui du script lancé.
- publish/build_package.py : assemble le zip dans un dossier temporaire
(jamais les vrais fichiers de l'app), copié/nettoyé après envoi de la
réponse HTTP (routes/publish/publish_game.py, déjà protégée par la
garde d'accès existante — aucune vérification supplémentaire).
- templates/base.html : bouton + modale (avancement séquentiel, jamais
de suivi serveur réel — le build est rapide) qui déclenche le
téléchargement du zip via un blob, comme la modale des codes de
récupération déjà en place (posée À L'INTÉRIEUR de <main> pour que
pjax.js la remplace et rejoue son script à chaque navigation).
Vérifié pour de vrai (pas seulement via les tests) : zip construit,
extrait, lancé avec le Python embeddable réel — /, /game/test/play,
/game/test/runtime-payload et /static/style.css répondent tous 200.
tests/test_publish.py (nouveau, vendor mocké) : structure du zip,
SLUG correctement substitué, route protégée par la même isolation par
projet que le reste de l'app. 203 tests au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5d6747770c
commit
1289079da5
@@ -50,6 +50,7 @@
|
||||
<nav class="gameNavBar" aria-label="Navigation du jeu">
|
||||
<a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="Écrants, 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">▶️ Jouer</a>
|
||||
<button type="button" class="gameNavLink" onclick="window.forgeOpenPublishModal && window.forgeOpenPublishModal()" title="Exporte le jeu en exécutable Windows autonome (zip)">📦 Publier</button>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -78,6 +79,94 @@
|
||||
</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 "📦 Publier" qui l'ouvre), son
|
||||
script ne serait rejoué qu'au tout premier chargement complet de la
|
||||
page. -->
|
||||
<div class="modal" id="publishModal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="box">
|
||||
<h2 class="title is-4">Publier « {{ game.name }} »</h2>
|
||||
<p class="hint mb-4">Construit un exécutable Windows autonome (Python + serveur embarqués) : dézippé puis lancé en double-cliquant sur <code>run.bat</code>, le jeu se joue hors connexion, sans rien installer.</p>
|
||||
<div id="publishProgress">
|
||||
<p class="hint" id="publishStepText">Préparation du jeu...</p>
|
||||
<div class="passwordStrengthBar" style="margin-top:10px;"><div class="passwordStrengthFill strong" id="publishProgressFill" style="width:100%; animation:forgePublishPulse 1.2s ease-in-out infinite;"></div></div>
|
||||
</div>
|
||||
<div id="publishError" class="notification is-danger" style="display:none;"></div>
|
||||
<div id="publishDone" class="notification is-success" style="display:none;">✅ Téléchargement lancé — dézippe le fichier puis double-clique sur <code>run.bat</code>.</div>
|
||||
<div class="field mt-4" style="display:flex; justify-content:flex-end;">
|
||||
<button type="button" class="button is-small" onclick="window.forgeClosePublishModal()">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes forgePublishPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
var modal = document.getElementById('publishModal');
|
||||
var stepText = document.getElementById('publishStepText');
|
||||
var progressBox = document.getElementById('publishProgress');
|
||||
var errorBox = document.getElementById('publishError');
|
||||
var doneBox = document.getElementById('publishDone');
|
||||
var steps = ["Préparation du jeu...", "Copie des données...", "Compression du dossier..."];
|
||||
var stepTimer = null;
|
||||
|
||||
window.forgeOpenPublishModal = 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 }}.zip";
|
||||
fetch("{{ url_for('publish_game', 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 la publication (" + err.message + "). Réessaie, ou vérifie ta connexion internet (nécessaire uniquement lors de la toute première publication sur ce poste).";
|
||||
});
|
||||
};
|
||||
|
||||
window.forgeClosePublishModal = function () {
|
||||
modal.classList.remove('is-active');
|
||||
if (stepTimer) clearInterval(stepTimer);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user