Files
CDJ900ModsWeb/builder/ui.js
T
Thiemo TizianiandClaude Opus 5.5 9787d88e95 site 1.6.0 (local): four mods stable, the rest experimental; builder ticks all and warns
- Stable: extended BPM, boot text, LED animation, cue colours; every other mod
  experimental (main page, docs, builder).
- Builder: every mod ticked at the start; a red warning across the top: for
  home use, not for clubs or gigs.
- Intro: the emulator links to QEMU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:47 +02:00

152 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The builder page: file, mods with their dependencies, the two
// confirmations, build and download (the work is in builder.js).
(function () {
'use strict';
var MODS = [
{ id: 'extended_bpm', name: 'Extended BPM', by: 'arnoldasr', state: 'stable',
what: 'BPM with two decimals: the tenths as a small digit above, the hundredths below.' },
{ id: 'phasemeter', name: 'Phase meter', by: 'arnoldasr', state: 'experimental',
what: 'Two-line phase meter instead of the four boxes; UTILITY › PHASE METER (was SLIP FLASHING) switches it.' },
{ id: 'advanced_waveforms', name: 'Advanced waveforms', by: 'ours', state: 'experimental',
what: 'The big waveform in RGB colours, the overview as a red-to-white gradient.' },
{ id: 'beatjump', name: 'Beat jump', by: 'ours', state: 'experimental', needs: 'hotcuekeys',
what: 'TRACK SEARCH ◀◀ ▶▶ jumps 1/2/4/8 beats along the beat grid, at once and in phase. UTILITY › BEAT JUMP (was SCREEN SAVER).' },
{ id: 'cuecolours', name: 'Cue colours', by: 'ours', state: 'stable',
what: 'Memory cues in their rekordbox colours; rekordbox hot cues shown as lettered boxes.' },
{ id: 'hotcuekeys', name: 'Hot cue keys', by: 'ours', state: 'experimental', needs: 'cuecolours',
what: 'Hot cues A–C on the BEAT DIVIDE keys: play, gate, set and delete, saved on the USB, loaded in the background.' },
{ id: 'ledsaver', name: 'LED animation', by: 'ours', state: 'stable',
what: 'While the screen saver dims the display, the button LEDs and the jog rings play an animation. UTILITY › LED ANIMATION.' },
{ id: 'keynotation', name: 'Key notation', by: 'ours', state: 'experimental',
what: 'Every key in one notation and in its Camelot wheel colour, whatever rekordbox exported: Camelot, Open Key or classic, also in BROWSE › KEY. UTILITY › KEY NOTATION.' },
{ id: 'credit', name: 'Boot text', by: 'ours', state: 'stable',
what: 'The boot screen says “Pionaarr Mods 1.0” instead of “Ver 1.31”.' }
];
var chosen = {};
MODS.forEach(function (m) { chosen[m.id] = true; }); // every mod ticked at the start
var stock = false, before = null; // the stock switch, and the picks it put away
function setStock(on) {
stock = on;
if (on) {
before = {};
MODS.forEach(function (m) { before[m.id] = chosen[m.id]; chosen[m.id] = false; });
} else if (before) {
MODS.forEach(function (m) { chosen[m.id] = before[m.id]; });
}
byId('stock').setAttribute('aria-pressed', on ? 'true' : 'false');
byId('picks').classList.toggle('collapsed', on); // stock: the mods folded away
byId('pick-note').textContent = on ? 'Stock: the update will have no mods.' : '';
render();
}
var file = null;
function byId(id) { return document.getElementById(id); }
function needsOf(id) { var m = MODS.filter(function (x) { return x.id === id; })[0]; return m && m.needs; }
// A mod ticked ticks what it needs (and what that needs); one unticked
// unticks every mod that needs it, directly or not.
function toggle(id, on) {
chosen[id] = on;
var notes = [];
if (on) {
for (var n = id; needsOf(n); n = needsOf(n)) {
if (!chosen[needsOf(n)]) { chosen[needsOf(n)] = true; notes.push('Ticked ' + needsOf(n) + ' too: ' + n + ' needs it.'); }
}
} else {
var changed = true;
while (changed) {
changed = false;
MODS.forEach(function (m) {
if (m.needs && !chosen[m.needs] && chosen[m.id]) { chosen[m.id] = false; changed = true; notes.push('Unticked ' + m.id + ' too: it needs ' + m.needs + '.'); }
});
}
}
byId('pick-note').textContent = notes.join(' ');
render();
}
function render() {
var box = byId('picks');
box.innerHTML = '';
MODS.forEach(function (m) {
var row = document.createElement('label');
row.className = 'pick' + (chosen[m.id] ? ' on' : '') + (stock ? ' off' : '');
row.innerHTML =
'<input type="checkbox"' + (chosen[m.id] ? ' checked' : '') + '>' +
'<span class="pick-main"><span class="pick-name">' + m.name + ' <code>' + m.id + '</code>' +
(window.CDJ_VERSIONS && window.CDJ_VERSIONS.mods[m.id] ? ' <span class="ver">v' + window.CDJ_VERSIONS.mods[m.id] + '</span>' : '') + '</span>' +
'<span class="pick-what">' + m.what + '</span>' +
'<span class="pick-by">' + (m.by === 'ours' ? 'Pionaarr mod · made with Claude Code' : 'by arnoldasr') +
(m.needs ? ' · needs <code>' + m.needs + '</code>' : '') + '</span></span>' +
'<span class="state ' + m.state + '">' + m.state + '</span>';
row.querySelector('input').disabled = stock;
row.querySelector('input').addEventListener('change', function (e) { toggle(m.id, e.target.checked); });
box.appendChild(row);
});
ready();
}
function ready() {
var why = !file ? 'Choose the update file first.'
: !byId('agree-risk').checked || !byId('agree-ai').checked ? 'Confirm both points above to build.' : '';
byId('build').disabled = !!why;
byId('build-why').textContent = why;
}
function setFile(f) {
file = f;
byId('file-state').textContent = f ? f.name + ' · ' + (f.size / 1048576).toFixed(1) + ' MB (checked when you build)' : 'No file yet';
byId('drop').classList.toggle('has', !!f);
byId('result').hidden = true;
ready();
}
function log(msg, cls) {
var l = byId('log');
l.hidden = false;
var p = document.createElement('div');
if (cls) p.className = cls;
p.textContent = msg;
l.appendChild(p);
l.scrollTop = l.scrollHeight;
}
var url = null;
function run() {
var names = MODS.map(function (m) { return m.id; }).filter(function (id) { return chosen[id]; });
byId('build').disabled = true;
byId('result').hidden = true;
byId('log').innerHTML = '';
log('mods: ' + (names.length ? names.join(', ') : 'none (stock)'));
file.arrayBuffer().then(function (buf) {
return window.CDJBuilder.build(new Uint8Array(buf), names, log);
}).then(function (r) {
log('done: every check passed', 'ok');
if (url) URL.revokeObjectURL(url);
url = URL.createObjectURL(new Blob([r.upd], { type: 'application/octet-stream' }));
byId('download').href = url;
byId('sum').innerHTML = 'SHA-256 of the file <code>' + r.updSha + '</code><br>of the application <code>' + r.appSha + '</code>';
var ul = byId('checks');
ul.innerHTML = '';
r.checks.forEach(function (c) { var li = document.createElement('li'); li.textContent = c; ul.appendChild(li); });
byId('result').hidden = false;
}).catch(function (e) {
log('stopped: ' + e.message, 'bad');
}).then(ready);
}
document.addEventListener('DOMContentLoaded', function () {
render();
byId('file').addEventListener('change', function (e) { setFile(e.target.files[0] || null); });
var drop = byId('drop');
['dragenter', 'dragover'].forEach(function (t) { drop.addEventListener(t, function (e) { e.preventDefault(); drop.classList.add('over'); }); });
['dragleave', 'drop'].forEach(function (t) { drop.addEventListener(t, function (e) { e.preventDefault(); drop.classList.remove('over'); }); });
drop.addEventListener('drop', function (e) { if (e.dataTransfer.files[0]) setFile(e.dataTransfer.files[0]); });
byId('agree-risk').addEventListener('change', ready);
byId('agree-ai').addEventListener('change', ready);
byId('build').addEventListener('click', run);
byId('stock').addEventListener('click', function () { setStock(!stock); });
});
})();