commit 1e850bb69f183c92f85215c1b54c98df59d11fa9
parent c1ebc5406989d2568924320349ef79851c1f54b7
Author: Jean Schurger <jean@schurger.org>
Date: Thu, 5 Nov 2026 16:30:06 -0500
Bibliothèque : une ligne par programme à plusieurs cassettes, faces en boutons en ligne
Le lien des faces porte un titre de groupe (« Arcade », « Initiation à l'Exelbasic »…) ; la liste n'affiche
plus « Titre (1) », « Titre (2) » mais une ligne avec un bouton par face/cassette (démarre dessus). Le bouton
de la barre reprend le titre seul (il concaténait le badge et « ⏏ insérer »). Recherche sur les titres des
faces.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9
Diffstat:
3 files changed, 40 insertions(+), 13 deletions(-)
diff --git a/test/tape-sides.mjs b/test/tape-sides.mjs
@@ -9,11 +9,11 @@ const page = await open(base + '/', { width: 900, height: 900 });
await page.evaluate('location.reload()');
await sleep(7000);
const row = await page.evaluate(`(() => {
- const b = [...document.querySelectorAll('.pg')].find((b) => b.dataset.type === 'tape' && b.dataset.search.includes(${JSON.stringify(want)}) && b.querySelector('.pg-b'));
+ const b = [...document.querySelectorAll('.pg')].find((b) => b.dataset.type === 'tape' && b.dataset.search.includes(${JSON.stringify(want)}) && b.querySelector('.pg-side'));
if (!b) return null;
- const badge = b.querySelector('.pg-b').textContent;
+ const sides = [...b.querySelectorAll('.pg-side')].map((x) => x.textContent);
b.click();
- return { title: b.querySelector('.pg-t').firstChild.textContent, badge };
+ return { title: b.dataset.title, sides, rows: [...document.querySelectorAll('.pg')].filter((r) => r.dataset.search.includes(${JSON.stringify(want)})).length };
})()`);
console.log('entrée :', JSON.stringify(row));
await sleep(6000);
@@ -21,7 +21,7 @@ const buttons = await page.evaluate("[...document.querySelectorAll('#tape-sides
console.log('boutons de la barre :', JSON.stringify(buttons));
await page.evaluate("document.querySelector('#tape-sides button').click()");
await sleep(500);
-const after = await page.evaluate("({ status: document.getElementById('status').textContent, buttons: [...document.querySelectorAll('#tape-sides button')].map((b) => b.textContent) })");
+const after = await page.evaluate("({ status: document.getElementById('status').textContent, pick: document.getElementById('pick').textContent, buttons: [...document.querySelectorAll('#tape-sides button')].map((b) => b.textContent) })");
console.log('après insertion :', JSON.stringify(after));
console.log(buttons.length && /insérée/.test(after.status) && after.buttons.join() !== buttons.join() ? 'OK' : 'PROBLÈME');
page.close();
diff --git a/tools/get_dcexel.py b/tools/get_dcexel.py
@@ -139,8 +139,14 @@ def link_sides(tapes):
if len(sides) < 2:
continue
sides.sort(key=lambda x: (re.sub(r"\d+", lambda m: m.group(0).zfill(3), x["l"])))
+ # Titre du programme (une seule ligne dans la bibliothèque) : celui de la première face, sans son
+ # numéro (« Arcade (1) » -> « Arcade », « Initiation au Basic 1 » -> « Initiation au Basic »,
+ # « Initiation a l'EXELBASIC - Volume 2 v1.1 » -> « Initiation a l'EXELBASIC »).
+ first = tapes[sides[0]["f"]]["t"]
+ group = re.sub(r"\s*\(\d+\)$|\s*-\s*(Volume|Tome|Cassette|Partie)\s*\d+.*$|\s+\d+$", "", first, flags=re.I).strip()
for sd in sides:
tapes[sd["f"]]["sides"] = sides
+ tapes[sd["f"]]["group"] = group or first
linked += 1
print(f"DCExel : {linked} programmes à plusieurs faces/cassettes reliés", file=sys.stderr)
diff --git a/web/shell.html.in b/web/shell.html.in
@@ -62,6 +62,8 @@
#disk-files tr.run:hover td { background: #2b2d35; color: #fff; }
#disk-files td.go { color: var(--accent); }
.pg-b { color: var(--accent); font-size: .78rem; margin-left: 8px; }
+ .pg-side { margin-left: 6px; background: #1f3f3f; color: var(--accent); border: 1px solid #2b5c5c; border-radius: 6px; font-size: .75rem; padding: 1px 7px; cursor: pointer; }
+ .pg-side:hover { background: #2a5555; color: #fff; }
.pg-ins { margin-left: 8px; background: #2b2d35; color: #ddd; border: 1px solid #40434e; border-radius: 6px; font-size: .75rem; padding: 1px 6px; cursor: pointer; }
.pg-ins:hover { background: #3a3d48; }
#tape-sides { display: inline; }
@@ -496,9 +498,9 @@
function markActive(value) {
let title = '';
for (const el of programsEl.querySelectorAll('.pg')) {
- const on = el.dataset.value === value;
+ const on = el.dataset.value === value || (el.dataset.sides || '').split(' ').includes(value);
el.classList.toggle('active', on);
- if (on && value) title = el.querySelector('.pg-t').textContent;
+ if (on && value) title = el.dataset.title;
}
$('pick').textContent = title ? '📂 ' + title : '📂 Choisir un programme…';
}
@@ -673,14 +675,20 @@
b.dataset.value = value;
b.dataset.cat = cat || '';
b.dataset.type = value.startsWith('cart:') ? 'cart' : value.startsWith('tape:') ? 'tape' : value.startsWith('cram:') ? 'cram' : value.startsWith('disk:') ? 'disk' : '';
- b.dataset.search = (title + ' ' + source + (m && m.sides ? ' ' + m.sides.map((x) => x.l).join(' ') : '')).toLowerCase();
+ b.dataset.search = (title + ' ' + source + (m && m.sides ? ' ' + m.sides.map((x) => x.l).join(' ') + ' ' + m.sides.map((x) => titlesCache[x.f] || '').join(' ') : '')).toLowerCase();
+ b.dataset.title = title;
const t = document.createElement('span'); t.className = 'pg-t'; t.textContent = title;
if (m && m.sides && m.sides.length > 1) {
- // Programme sur plusieurs faces/cassettes : on le dit, la suite s'insère avec les boutons 📼 de la barre.
- const b = document.createElement('span'); b.className = 'pg-b';
- b.textContent = m.sides.length + ' ' + (m.sides.every((x) => /^Face /.test(x.l)) ? 'faces' : 'cassettes') + ' : ' + m.sides.map((x) => x.l).join(' / ');
- b.title = 'Ce programme demande une autre face ou cassette en cours de route : insérez-la alors avec le bouton 📼 de la barre, sans redémarrer.';
- t.appendChild(b);
+ // Programme sur plusieurs faces/cassettes : une seule ligne, chaque face en bouton (démarre dessus) ;
+ // en cours de route, la suite s'insère avec les boutons 📼 de la barre, sans redémarrer.
+ b.dataset.sides = m.sides.map((x) => 'tape:' + x.f).join(' ');
+ for (const sd of m.sides) {
+ const sb = document.createElement('button'); sb.type = 'button'; sb.className = 'pg-side';
+ sb.textContent = sd.l;
+ sb.title = 'Démarrer sur « ' + sd.l + ' » (en cours de programme, insérez-la plutôt avec le bouton 📼 de la barre)';
+ sb.onclick = (e) => { e.stopPropagation(); bootProgram('tape:' + sd.f); };
+ t.appendChild(sb);
+ }
}
if (value.startsWith('tape:')) {
const ins = document.createElement('button'); ins.type = 'button'; ins.className = 'pg-ins'; ins.textContent = '⏏ insérer';
@@ -693,14 +701,27 @@
b.onclick = () => bootProgram(value);
programsEl.appendChild(b);
}
+ const titlesCache = {}; // fichier -> titre, pour la recherche sur les faces
function addGroup(label, path, prefix) {
let titles;
try { titles = JSON.parse(fs().readFile(path, { encoding: 'utf8' })); } catch (e) { return; }
+ for (const [f, m] of Object.entries(titles)) titlesCache[f] = m.t;
const entries = Object.entries(titles).sort((a, b) => a[1].t.localeCompare(b[1].t, 'fr'));
if (!entries.length) return;
const h = document.createElement('div'); h.className = 'pg-group'; h.textContent = label;
programsEl.appendChild(h);
- for (const [file, m] of entries) addRow(prefix + file, m.t, sourceOf(m), m.cat, m);
+ const shown = new Set();
+ for (const [file, m] of entries) {
+ if (m.sides && m.sides.length > 1) {
+ const first = m.sides[0].f;
+ if (shown.has(first)) continue; // les autres faces sont sur la même ligne
+ shown.add(first);
+ const fm = titles[first] || m;
+ addRow(prefix + first, m.group || fm.t, sourceOf(fm), fm.cat, fm);
+ continue;
+ }
+ addRow(prefix + file, m.t, sourceOf(m), m.cat, m);
+ }
}
function populatePrograms() {
programsEl.innerHTML = '';