exl100

Émulateur Exelvision EXL 100 (1984) dans le navigateur : dsp-cpp en WebAssembly, cartouches et cassettes BASIC
git clone https://git.schurger.org/exl100.git
Log | Files | Refs | README

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:
Mtest/cram-panel.mjs | 6+++---
Mweb/shell.html.in | 101++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
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'