commit ece27b18ee7401946a74e083d011685175ac3fb6
parent 75f1acb0e01eaadf6d41cd2aeb0210d68eb1c237
Author: Jean Schurger <jean@schurger.org>
Date: Thu, 5 Nov 2026 11:57:56 -0500
Page : regrouper toutes les fonctions Exelmémoire dans une modale
La barre n'a plus qu'un bouton « 💾 Exelmémoire » ; la case à cocher, l'explorateur
du volume, l'import/export .cram et le bouton Vider passent dans une modale dédiée
(brancher/débrancher avec une note selon la machine, résumé du volume, liste des
fichiers, Importer/Exporter/Vider). L'import de la modale utilise la même détection
que la zone de dépôt (.cram 16 Ko, image 64 Ko, .bkp). Explorateur rafraîchi à
l'ouverture et à chaque sauvegarde. Test cram-panel.mjs mis à jour (ouvre la modale).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9
Diffstat:
2 files changed, 59 insertions(+), 48 deletions(-)
diff --git a/test/cram-panel.mjs b/test/cram-panel.mjs
@@ -13,13 +13,13 @@ await page.evaluate(vol ? `localStorage.setItem(${JSON.stringify(key)}, ${JSON.s
: `localStorage.removeItem(${JSON.stringify(key)})`);
await page.evaluate('location.reload()');
await sleep(8000);
+await page.evaluate("document.getElementById('cram-open').click()");
+await sleep(400);
const info = await page.evaluate(`(() => {
- const p = document.getElementById('cram-panel');
const rows = [...document.querySelectorAll('#cram-files tbody tr')].map((tr) => [...tr.children].map((td) => td.textContent).join(' | '));
- return { hidden: p.hidden, summary: document.getElementById('cram-summary').textContent, rows, note: document.getElementById('cram-empty').textContent };
+ return { open: document.getElementById('cram-modal').open, summary: document.getElementById('cram-summary').textContent, rows, note: document.getElementById('cram-empty').textContent };
})()`);
console.log(JSON.stringify(info, null, 1));
-await page.evaluate("document.getElementById('cram-panel').open = true");
await page.screenshot(shot);
console.log('Capture :', shot);
page.close();
diff --git a/web/shell.html.in b/web/shell.html.in
@@ -50,14 +50,15 @@
.toolbar .drop { cursor: pointer; border: 1px dashed var(--muted); border-radius: 8px; padding: 6px 12px;
color: var(--fg, #ddd); transition: border-color .15s, background .15s; }
.toolbar .drop:hover, .toolbar .drop.over { border-color: #6cf; background: rgba(102, 204, 255, .08); }
- .cram-panel { max-width: 900px; margin: 0 auto 12px; font-size: .9rem; color: var(--muted); }
- .cram-panel summary { cursor: pointer; color: #ddd; }
- .cram-body { padding: 8px 4px 0; }
- .cram-panel table { border-collapse: collapse; width: 100%; font-family: ui-monospace, monospace; font-size: .85rem; }
- .cram-panel th, .cram-panel td { text-align: left; padding: 3px 8px; border-bottom: 1px solid #2a2c33; }
- .cram-panel td.num { text-align: right; }
+ .cram-body { padding: 12px 16px 16px; font-size: .9rem; color: var(--muted); }
+ .cram-toggle { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; margin: 0 0 10px; }
+ .cram-toggle label { color: #ddd; cursor: pointer; }
+ .cram-sum { color: #ddd; margin: 6px 0; }
+ #cram-modal table { border-collapse: collapse; width: 100%; font-family: ui-monospace, monospace; font-size: .85rem; }
+ #cram-modal th, #cram-modal td { text-align: left; padding: 3px 8px; border-bottom: 1px solid #2a2c33; }
+ #cram-modal td.num { text-align: right; }
.cram-note { font-size: .82rem; margin: 6px 0; }
- .cram-actions { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0 0; }
+ .cram-actions { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 10px 0; }
.toolbar select, .toolbar input[type=file], .toolbar button {
background: #2b2d35; color: #fff; border: 1px solid #40434e; border-radius: 6px;
padding: 7px 10px; font: inherit; font-size: .92rem; cursor: pointer; }
@@ -136,14 +137,7 @@
<option value="exeltel">EXELTEL (64 Ko)</option>
</select>
</label>
- <label id="exelmem-label" class="machine-sel" title="Extension Exelmémoire : 16 Ko de mémoire sauvegardée et son gestionnaire de fichiers CROS64. Sur l'EXL 100 elle démarre à la place du logo ; sur l'EXELTEL, menu Périphériques → Exelmémoire. Le contenu est conservé dans ce navigateur.">
- <input type="checkbox" id="exelmem"> Exelmémoire
- </label>
- <span id="cram-io" class="machine-sel" hidden>
- <button id="cram-import" type="button" class="pick-btn" title="Charger une image .cram (format DCExel : 16 Ko brut, ou 64 Ko pour l'EXELTEL) dans l'Exelmémoire">📥 .cram</button>
- <button id="cram-export" type="button" class="pick-btn" title="Télécharger le contenu de l'Exelmémoire en .cram (format DCExel)">📤 .cram</button>
- <input type="file" id="cram-file" accept=".cram,.bin,application/octet-stream" hidden>
- </span>
+ <button id="cram-open" type="button" class="pick-btn" title="Extension Exelmémoire : 16 Ko de mémoire sauvegardée et son gestionnaire de fichiers CROS64 — brancher/débrancher, contenu du volume, import/export, vider.">💾 Exelmémoire</button>
<label class="hidden">Machine
<select id="mtype">
<option value="all">All</option><option value="arcade">Arcade</option>
@@ -168,18 +162,32 @@
<details id="more" class="hidden"><summary>options</summary></details>
</div>
- <!-- Explorateur de l'Exelmémoire : contenu du volume (16 Ko) lu côté page, mis à jour à chaque sauvegarde. -->
- <details id="cram-panel" class="cram-panel" hidden>
- <summary id="cram-summary">Exelmémoire</summary>
+ <!-- Modale Exelmémoire : brancher/débrancher, contenu du volume (lu côté page, à jour à chaque
+ sauvegarde), import/export .cram, vider. -->
+ <dialog id="cram-modal" class="modal">
+ <div class="modal-head">
+ <strong>Exelmémoire</strong>
+ <button id="cram-modal-close" type="button" class="x" aria-label="Fermer">✕</button>
+ </div>
<div class="cram-body">
+ <p class="cram-toggle">
+ <label><input type="checkbox" id="exelmem"> <b>Brancher l'Exelmémoire</b></label>
+ <span id="exelmem-note" class="cram-note"></span>
+ </p>
+ <p id="cram-summary" class="cram-sum">Exelmémoire</p>
<table id="cram-files"><thead><tr><th>Fichier</th><th>Ext</th><th>Taille</th><th>Enreg.</th><th>Drap.</th><th>Commentaire</th></tr></thead><tbody></tbody></table>
<p id="cram-empty" class="cram-note"></p>
<p class="cram-actions">
+ <button id="cram-import" type="button" class="pick-btn" title="Charger une image .cram (format DCExel : 16 Ko brut, ou 64 Ko pour l'EXELTEL) ou une sauvegarde .bkp dans l'Exelmémoire">📥 Importer</button>
+ <button id="cram-export" type="button" class="pick-btn" title="Télécharger le contenu de l'Exelmémoire en .cram (format DCExel)">📤 Exporter</button>
<button id="cram-clear" type="button" class="pick-btn" title="Repartir d'un volume vierge (le contenu actuel est perdu, exportez-le d'abord si besoin)">🗑 Vider</button>
- <span class="cram-note">Export / import : boutons « .cram » de la barre. Format : CROS64, enregistrements en descendant depuis $BFD0.</span>
+ <input type="file" id="cram-file" accept=".cram,.bkp,.bin,application/octet-stream" hidden>
</p>
+ <p class="cram-note">Le contenu est conservé dans ce navigateur (comme un fichier <code>.cram</code> de DCExel).
+ Sur l'EXL 100, branchée, elle démarre à la place du logo (gestionnaire CROS) ; sur l'EXELTEL, menu
+ <i>Périphériques → Exelmémoire</i>. Format : CROS64, enregistrements en descendant depuis $BFD0.</p>
</div>
- </details>
+ </dialog>
<dialog id="modal" class="modal">
<div class="modal-head">
@@ -590,16 +598,22 @@
}
exelmemBox.checked = exelmemEnabled();
exelmemBox.disabled = onExeltel;
+ $('exelmem-note').textContent = onExeltel
+ ? 'Toujours branchée sur l’EXELTEL (menu Périphériques → Exelmémoire).'
+ : 'Branchée, l’EXL 100 démarre sur le gestionnaire CROS au lieu du logo. Changer recharge la page.';
exelmemBox.onchange = () => {
try { localStorage.setItem('exl100.exelmem', exelmemBox.checked ? '1' : '0'); } catch (e) {}
const u = new URL(location.href);
u.searchParams.delete('exelmem');
location.href = u.toString();
};
- // Import / export du volume au format .cram de DCExel : 16 Ko bruts ($8000-$BFFF) pour l'EXL 100 ;
- // pour l'EXELTEL DCExel écrit une image de 64 Ko de tout l'espace d'adresses (le volume en $8000-$BFFF).
- const cramIo = $('cram-io');
- cramIo.hidden = !exelmemEnabled();
+ // Modale Exelmémoire : un seul bouton de la barre regroupe brancher/débrancher, contenu du volume,
+ // import/export et vider. Import / export au format .cram de DCExel : 16 Ko bruts ($8000-$BFFF) pour
+ // l'EXL 100 ; pour l'EXELTEL DCExel écrit une image de 64 Ko de tout l'espace d'adresses.
+ const cramModal = $('cram-modal');
+ $('cram-open').onclick = () => { cramRefresh(); cramModal.showModal(); };
+ $('cram-modal-close').onclick = () => cramModal.close();
+ cramModal.addEventListener('click', (e) => { if (e.target === cramModal) cramModal.close(); });
function cramBytes() {
const s = atob(Module.UTF8ToString(Module.ccall('dsp_web_cram_save', 'number', [], [])));
const a = new Uint8Array(s.length);
@@ -717,20 +731,26 @@
return { name, formatted, free, files };
}
function cramRefresh(b64) {
- const panel = $('cram-panel');
+ const present = (() => { try { return Module.ccall('dsp_web_cram_present', 'number', [], []); } catch (e) { return 0; } })();
+ const io = [$('cram-export'), $('cram-clear')];
+ const tb = $('cram-files').querySelector('tbody');
+ tb.innerHTML = '';
+ if (!present) {
+ $('cram-summary').textContent = onExeltel ? 'Exelmémoire indisponible.' : 'Exelmémoire débranchée — cochez ci-dessus pour l’activer.';
+ $('cram-files').hidden = true; $('cram-empty').textContent = '';
+ for (const b of io) b.disabled = true;
+ return;
+ }
+ for (const b of io) b.disabled = false;
try {
- if (!Module.ccall('dsp_web_cram_present', 'number', [], [])) { panel.hidden = true; return; }
if (!b64) b64 = Module.UTF8ToString(Module.ccall('dsp_web_cram_save', 'number', [], []));
const s = atob(b64), v = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) v[i] = s.charCodeAt(i);
const info = cramParse(v);
- panel.hidden = false;
- $('cram-summary').textContent = 'Exelmémoire — ' + (info.formatted
- ? 'volume « ' + (info.name || 'sans nom') + ' », ' + info.files.length + ' fichier' + (info.files.length > 1 ? 's' : '') +
+ $('cram-summary').textContent = info.formatted
+ ? 'Volume « ' + (info.name || 'sans nom') + ' » — ' + info.files.length + ' fichier' + (info.files.length > 1 ? 's' : '') +
', ' + info.free.toLocaleString('fr-FR') + ' octets libres'
- : 'volume non formaté (CROS : Format / FMT)');
- const tb = $('cram-files').querySelector('tbody');
- tb.innerHTML = '';
+ : 'Volume non formaté (gestionnaire CROS : Format / FMT).';
for (const f of info.files) {
const tr = document.createElement('tr');
for (const [val, cls] of [[f.name, ''], [f.ext, ''], [f.size, 'num'], [f.rclen, 'num'],
@@ -740,9 +760,9 @@
tb.appendChild(tr);
}
$('cram-files').hidden = !info.files.length;
- $('cram-empty').textContent = info.formatted ? (info.files.length ? '' : 'Aucun fichier.') :
+ $('cram-empty').textContent = info.formatted ? (info.files.length ? '' : 'Volume formaté, aucun fichier.') :
'Le volume doit être formaté avant usage (menu CROS64 : Format ; CROS 16K : FMT).';
- } catch (e) { panel.hidden = true; }
+ } catch (e) { $('cram-summary').textContent = 'Exelmémoire (contenu illisible).'; }
}
$('cram-clear').onclick = () => {
if (!confirm('Vider l’Exelmémoire ? Le contenu actuel sera perdu (exportez-le d’abord si besoin).')) return;
@@ -750,17 +770,8 @@
location.reload();
};
$('cram-import').onclick = () => $('cram-file').click();
- $('cram-file').onchange = async () => {
- const file = $('cram-file').files[0];
- if (!file) return;
- let data = new Uint8Array(await file.arrayBuffer());
- if (data.length === 65536) data = data.subarray(0x8000, 0xc000);
- if (data.length !== 16384) { statusEl.textContent = 'Image .cram inattendue (' + data.length + ' octets ; 16384 ou 65536)'; return; }
- let s = '';
- for (let i = 0; i < data.length; i += 4096) s += String.fromCharCode.apply(null, data.subarray(i, i + 4096));
- try { localStorage.setItem(cramKey, btoa(s)); } catch (e) {}
- location.reload(); // restaurée au lancement, avant que le système ne la lise
- };
+ // Même détection que le dépôt (gère .cram 16 Ko, image 64 Ko DCExel et sauvegarde .bkp webmo).
+ $('cram-file').onchange = () => { if ($('cram-file').files[0]) openMedia($('cram-file').files[0]); };
machineSel.onchange = () => {
location.href = machineSel.value === 'exeltel'
? location.pathname + '?game=exeltel&rom=/roms/exeltel.zip'