// 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): OFF, ON or REVERSE. The bar countdown moved clear of the lines.' }, { id: 'advanced_waveforms', name: 'Advanced waveforms', by: 'ours', state: 'experimental', what: 'The big waveform in rekordbox’s RGB colours, the overview as a red-to-white gradient; UTILITY › WAVEFORM: BLUE, RGB or 3BAND (lows, mids, highs).' }, { 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: 'cuenames', name: 'Cue names', by: 'ours', state: 'experimental', needs: 'cuecolours', what: 'The rekordbox comment of the next memory cue or hot cue (“Drop”) in the title bar, in the cue’s colour.' }, { id: 'played', name: 'Played tracks', by: 'ours', state: 'experimental', what: 'Tracks loaded tonight green in BROWSE; with modded players on Pro DJ Link also theirs, and [n] for a deck that has the track loaded now.' }, { id: 'endwarn', name: 'End warning', by: 'ours', state: 'experimental', what: 'The bar under the overview blinks red instead of white in the last 30 seconds.' }, { id: 'quicksearch', name: 'Quick search', by: 'ours', state: 'experimental', what: 'BROWSE held: SEARCH; BROWSE twice: the list the playing track came from, the track picked.' }, { id: 'usbautoload', name: 'USB auto load', by: 'ours', state: 'experimental', what: 'MY SETTINGS on a stick loaded the moment the player finds them. UTILITY › USB AUTO LOAD.' }, { id: 'credit', name: 'Boot text', by: 'ours', state: 'stable', what: 'The boot screen says “Pionaarr Mods 1.8.2” 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 = '' + '' + m.name + ' ' + m.id + '' + (window.CDJ_VERSIONS && window.CDJ_VERSIONS.mods[m.id] ? ' v' + window.CDJ_VERSIONS.mods[m.id] + '' : '') + '' + '' + m.what + '' + '' + (m.by === 'ours' ? 'Pionaarr mod · made with Claude Code' : 'by arnoldasr') + (m.needs ? ' · needs ' + m.needs + '' : '') + '' + '' + m.state + ''; 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 ' + r.updSha + '
of the application ' + r.appSha + ''; 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); }); }); })();