function initAdmin(){ var previewCanvas = document.getElementById('previewCanvas'); var renderer = BLS.Renderer(previewCanvas); var chan = BLS.createChannel('bls-session'); var state = BLS.loadState(); renderer.setState(state); var shapeControls = document.getElementById('shapeControls'); var directionControls = document.getElementById('directionControls'); var easingControls = document.getElementById('easingControls'); var shapeColor = document.getElementById('shapeColor'); var bgColor = document.getElementById('bgColor'); var shapePreset = document.getElementById('shapePreset'); var bgPreset = document.getElementById('bgPreset'); var speed = document.getElementById('speed'); var speedLabel = document.getElementById('speedLabel'); var size = document.getElementById('size'); var sizeLabel = document.getElementById('sizeLabel'); var yPercent = document.getElementById('yPercent'); var yPercentLabel = document.getElementById('yPercentLabel'); var yReset = document.getElementById('yReset'); var glowEnable = document.getElementById('glowEnable'); var glowIntensity = document.getElementById('glowIntensity'); var glowIntensityLabel = document.getElementById('glowIntensityLabel'); var glowRate = document.getElementById('glowRate'); var glowRateLabel = document.getElementById('glowRateLabel'); var rampEnable = document.getElementById('rampEnable'); var rampSeconds = document.getElementById('rampSeconds'); var rampLabel = document.getElementById('rampLabel'); var edgePause = document.getElementById('edgePause'); var edgePauseLabel = document.getElementById('edgePauseLabel'); var wiggleEnable = document.getElementById('wiggleEnable'); var wiggleAmplitude = document.getElementById('wiggleAmplitude'); var wiggleLabel = document.getElementById('wiggleLabel'); var presetName = document.getElementById('presetName'); var presetList = document.getElementById('presetList'); var savePresetBtn = document.getElementById('savePreset'); var applyPresetBtn = document.getElementById('applyPreset'); var deletePresetBtn = document.getElementById('deletePreset'); var exportPresetsBtn = document.getElementById('exportPresets'); var importPresetsBtn = document.getElementById('importPresets'); var timerMinutes = document.getElementById('timerMinutes'); var timerDisplay = document.getElementById('timerDisplay'); var statusText = document.getElementById('statusText'); var elapsedText = document.getElementById('elapsedText'); var startBtn = document.getElementById('startSession'); var endBtn = document.getElementById('endSession'); var openPatient = document.getElementById('openPatient'); var audioSelect = document.getElementById('audioSelect'); var pickAudioFolder = document.getElementById('pickAudioFolder'); var audioEnable = document.getElementById('audioEnable'); var audioMode = document.getElementById('audioMode'); var programControls = document.getElementById('programmaticControls'); var fileControls = document.getElementById('fileControls'); var programPreset = document.getElementById('programPreset'); var syncPan = document.getElementById('syncPan'); var panRate = document.getElementById('panRate'); var panRateLabel = document.getElementById('panRateLabel'); var cueRate = document.getElementById('cueRate'); var cueRateLabel = document.getElementById('cueRateLabel'); var volume = document.getElementById('volume'); var volumeLabel = document.getElementById('volumeLabel'); var testSound = document.getElementById('testSound'); var connectHID = document.getElementById('connectHID'); var deviceStatus = document.getElementById('deviceStatus'); var knobSens = document.getElementById('knobSens'); var knobSensLabel = document.getElementById('knobSensLabel'); var hidDevice = null; var audioFiles = []; var patientName = document.getElementById('patientName'); var savePatientBtn = document.getElementById('savePatient'); var sessionCountEl = document.getElementById('sessionCount'); var lastSessionInfoEl = document.getElementById('lastSessionInfo'); var clearSessionsBtn = document.getElementById('clearSessions'); var deletePatientBtn = document.getElementById('deletePatient'); function setActive(group, dataKey, stateKey){ var btns = group.querySelectorAll('button'); for(var i=0;i'+n+'' }).join(''); var sel = document.getElementById('patientList'); if(sel){ sel.innerHTML = html } } function loadPatients(){ try{ var m = JSON.parse(localStorage.getItem('bls_patients')||'null'); return m&&typeof m==='object'? m : {} }catch(e){ return {} } } function savePatients(m){ try{ localStorage.setItem('bls_patients', JSON.stringify(m)) }catch(e){} } function getPatient(name){ var n = (name||'').trim()||'Unnamed'; var m = loadPatients(); if(!m[n]) m[n] = { name:n, sessions:[], count:0, last:null }; return m[n] } function setPatient(p){ var m = loadPatients(); m[p.name] = p; savePatients(m) } function deletePatient(name){ var n = (name||'').trim()||'Unnamed'; var m = loadPatients(); if(m[n]){ delete m[n]; savePatients(m) } } function refreshPatientUI(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); sessionCountEl.textContent = String(p.count||0); if(p.last){ var d = new Date(p.last.end); var dur = Math.round((p.last.durationSec||0)); lastSessionInfoEl.textContent = d.toLocaleString()+' • '+dur+'s' } else { lastSessionInfoEl.textContent = '—' } var host = document.getElementById('sessionHistory'); if(host){ if(!p.sessions || !p.sessions.length){ host.innerHTML = 'No sessions yet' } else { var rows = p.sessions.slice().sort(function(a,b){ return a.start - b.start }).map(function(s, idx){ var start = new Date(s.start).toLocaleString(); var end = new Date(s.end).toLocaleString(); var dur = Math.round(s.durationSec||0); return '
'+String(idx+1)+'. '+start+' → '+end+' • '+dur+'s
' }); host.innerHTML = rows.join('') } } } function recordSession(){ var n = (patientName.value||'').trim()||'Unnamed'; var st = BLS.loadState(); if(!st.startedAt) return; var end = Date.now(); var durationSec = Math.floor((end - st.startedAt)/1000); var p = getPatient(n); p.sessions.push({ start: st.startedAt, end: end, durationSec: durationSec }); p.count = (p.count||0)+1; p.last = { start: st.startedAt, end: end, durationSec: durationSec }; setPatient(p); refreshPatientUI() } function refreshPresetList(){ var names = BLS.listPresets(); var html=''; for(var i=0;i'+n+'' } presetList.innerHTML = html } function applyState(next, broadcast){ state = BLS.mergeState(state, next||{}); renderer.setState(state); if(state.running){ renderer.start() } else { renderer.stop() } if(broadcast){ chan.send(state) } } shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.shape; if(!d) return; applyState({shape:d}, true); setActive(shapeControls,'shape','shape'); }); directionControls.addEventListener('click', function(e){ var d = e.target.dataset.direction; if(!d) return; applyState({direction:d}, true); setActive(directionControls,'direction','direction'); }); easingControls.addEventListener('click', function(e){ var d = e.target.dataset.easing; if(!d) return; applyState({easingMode:d}, true); setActive(easingControls,'easing','easingMode'); }); shapeColor.addEventListener('input', function(){ applyState({shapeColor:shapeColor.value}, true); }); bgColor.addEventListener('input', function(){ applyState({bgColor:bgColor.value}, true); }); shapePreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; shapeColor.value = c; applyState({shapeColor:c}, true); }); bgPreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; bgColor.value = c; applyState({bgColor:c}, true); }); speed.addEventListener('input', function(){ updateSpeedLabel(); var m = (BLS.loadState().speedMultiplier||1); applyState({speed:parseInt(speed.value,10)*m}, true); }); size.addEventListener('input', function(){ updateSizeLabel(); applyState({size:parseInt(size.value,10)}, true); }); yPercent.addEventListener('input', function(){ updateYPercentLabel(); applyState({yPercent:parseInt(yPercent.value,10)}, true); }); yReset.addEventListener('click', function(){ yPercent.value=50; updateYPercentLabel(); applyState({yPercent:50}, true); }); glowEnable.addEventListener('change', function(){ applyState({glowEnabled: glowEnable.checked}, true) }); glowIntensity.addEventListener('input', function(){ updateGlowIntensityLabel(); var f = parseInt(glowIntensity.value,10)/100; applyState({glowIntensity:f}, true) }); glowRate.addEventListener('input', function(){ updateGlowRateLabel(); applyState({glowRate: parseFloat(glowRate.value)}, true) }); rampEnable.addEventListener('change', function(){ var st = BLS.loadState(); var next = {rampEnabled: rampEnable.checked}; if(!rampEnable.checked){ next.easingMode = 'linear' } if(st.running && rampEnable.checked){ next.startedAt = Date.now() } applyState(next, true) }); rampSeconds.addEventListener('input', function(){ updateRampLabel(); var st = BLS.loadState(); var next = {rampSeconds: parseInt(rampSeconds.value,10)}; if(st.running && st.rampEnabled){ next.startedAt = Date.now() } applyState(next, true) }); edgePause.addEventListener('input', function(){ updateEdgePauseLabel(); applyState({edgePauseMs: parseInt(edgePause.value,10)}, true) }); wiggleEnable.addEventListener('change', function(){ applyState({wiggleEnabled: wiggleEnable.checked}, true) }); wiggleAmplitude.addEventListener('input', function(){ updateWiggleLabel(); applyState({wiggleAmplitude: parseInt(wiggleAmplitude.value,10)}, true) }); timerMinutes.addEventListener('input', function(){ updateTimerLabel(); }); savePresetBtn.addEventListener('click', function(){ var n = (presetName.value||'').trim(); if(!n) return; BLS.savePreset(n, BLS.loadState()); refreshPresetList() }) applyPresetBtn.addEventListener('click', function(){ var n = presetList.value; if(!n) return; var preset = BLS.loadPreset(n); if(!preset) return; applyState(preset, true); setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode') }) deletePresetBtn.addEventListener('click', function(){ var n = presetList.value; if(!n) return; BLS.deletePreset(n); refreshPresetList() }) exportPresetsBtn.addEventListener('click', async function(){ var data = BLS.exportPresets(); if(window.showSaveFilePicker){ try{ var handle = await window.showSaveFilePicker({ suggestedName: 'bls-presets.json', types:[{ description:'JSON', accept:{ 'application/json':['.json'] } }] }); var w = await handle.createWritable(); await w.write(data); await w.close() }catch(e){} } else { var a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([data], {type:'application/json'})); a.download = 'bls-presets.json'; a.click(); URL.revokeObjectURL(a.href) } }) importPresetsBtn.addEventListener('click', async function(){ if(window.showOpenFilePicker){ try{ var files = await window.showOpenFilePicker({ multiple:false, types:[{ description:'JSON', accept:{ 'application/json':['.json'] } }] }); if(files && files.length){ var file = await files[0].getFile(); var text = await file.text(); BLS.importPresets(text); refreshPresetList() } }catch(e){} } else { var input = document.createElement('input'); input.type='file'; input.accept='.json,application/json'; input.addEventListener('change', function(){ var f = input.files && input.files[0]; if(!f) return; var reader = new FileReader(); reader.onload = function(){ BLS.importPresets(reader.result); refreshPresetList() }; reader.readAsText(f) }); input.click() } }) async function listAudioFromHandle(handle){ audioFiles=[]; if(!handle) return; try{ for await (const [name, h] of handle.entries()){ if(h.kind==='file' && /\.(mp3|wav)$/i.test(name)){ audioFiles.push({name:name, handle:h}) } } }catch(e){} audioSelect.innerHTML = '' + audioFiles.map(function(f){ return '' }).join('') } async function pickFolder(){ if(window.showDirectoryPicker){ try{ var dir = await window.showDirectoryPicker(); await listAudioFromHandle(dir); }catch(e){} } else { var input = document.createElement('input'); input.type='file'; input.webkitdirectory=true; input.multiple=true; input.addEventListener('change', function(){ audioFiles = Array.from(input.files).filter(function(f){ return /\.(mp3|wav)$/i.test(f.name) }).map(function(f){ return {name:f.name, file:f} }); audioSelect.innerHTML = '' + audioFiles.map(function(f){ return '' }).join(''); }); input.click(); } } pickAudioFolder.addEventListener('click', pickFolder); audioSelect.addEventListener('change', function(){ var name = audioSelect.value || null; applyState({audioName:name}, true) }); audioEnable.addEventListener('change', function(){ applyState({audioEnabled: audioEnable.checked}, true) }); audioMode.addEventListener('change', function(){ refreshAudioMode(); applyState({audioMode: audioMode.value}, true) }); programPreset.addEventListener('change', function(){ applyState({programPreset: programPreset.value}, true) }); syncPan.addEventListener('change', function(){ applyState({syncPan: syncPan.checked}, true) }); panRate.addEventListener('input', function(){ updatePanRateLabel(); applyState({panRate: parseFloat(panRate.value)}, true) }); cueRate.addEventListener('input', function(){ updateCueRateLabel(); applyState({cueRate: parseFloat(cueRate.value)}, true) }); volume.addEventListener('input', function(){ updateVolumeLabel(); applyState({volume: parseInt(volume.value,10)/100}, true) }); testSound.addEventListener('click', function(){ try{ var st = BLS.loadState(); if(st.audioMode==='programmatic'){ AudioEngine.setVolume(st.volume||0.6); var pan = 0; if(st.programPreset==='click'){ AudioEngine.playClick(pan) } else if(st.programPreset==='ping'){ AudioEngine.playPing(pan) } else if(st.programPreset==='woodblock'){ AudioEngine.playWoodblock(pan) } else if(st.programPreset==='bell'){ AudioEngine.playBell(pan) } else if(st.programPreset==='bass'){ AudioEngine.playBass(pan) } else if(st.programPreset==='kick'){ AudioEngine.playKick(pan) } else if(st.programPreset==='snare'){ AudioEngine.playSnare(pan) } else if(st.programPreset==='hihat'){ AudioEngine.playHiHat(pan) } else if(st.programPreset==='sweep'){ AudioEngine.playSweep(pan) } else if(st.programPreset==='zap'){ AudioEngine.playZap(pan) } else if(st.programPreset==='bubble'){ AudioEngine.playBubble(pan) } else if(st.programPreset==='pink'){ AudioEngine.startPinkNoisePan(st.panRate||0.5); setTimeout(function(){ AudioEngine.stopNoise() }, 800) } else if(st.programPreset==='hybrid'){ AudioEngine.startHybrid(st.cueRate||1.2); setTimeout(function(){ AudioEngine.stopNoise() }, 1000) } } }catch(e){} }); knobSens.addEventListener('input', function(){ updateKnobSensLabel() }); if(clearSessionsBtn){ clearSessionsBtn.addEventListener('click', function(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); p.sessions = []; p.count = 0; p.last = null; setPatient(p); refreshPatientUI() }) } if(deletePatientBtn){ deletePatientBtn.addEventListener('click', function(){ var n = (patientName.value||'').trim()||'Unnamed'; deletePatient(n); patientName.value=''; refreshPatientList(); refreshPatientUI() }) } var loadPatientBtn = document.getElementById('loadPatient'); if(loadPatientBtn){ loadPatientBtn.addEventListener('click', function(){ var sel = document.getElementById('patientList'); if(!sel) return; var n = sel.value; if(!n) return; patientName.value = n; refreshPatientUI() }) } var speedBoost = document.getElementById('speedBoost'); if(speedBoost){ speedBoost.addEventListener('click', function(e){ var m = e.target.dataset.mult; if(!m) return; var mult = parseInt(m,10)||1; var btns = speedBoost.querySelectorAll('button'); for(var i=0;i127) b0 = b0-256 }catch(_){ b0 = 0 } } var sens = parseInt(knobSens.value,10)||1; var delta = b0 * sens; if(delta!==0){ var current = parseInt(speed.value,10)||250; var next = Math.max(parseInt(speed.min,10), Math.min(parseInt(speed.max,10), current + delta)); speed.value = String(next); updateSpeedLabel(); applyState({speed: next}, true) } }) }catch(e){ deviceStatus.textContent = 'Error connecting' } } connectHID.addEventListener('click', connectHIDDevice); var tickId = null; function startTimer(){ if(tickId) clearInterval(tickId); var durationSec = Math.max(0, (parseInt(timerMinutes.value||'0',10)||0)*60); var startedAt = Date.now(); applyState({running:true, durationSec:durationSec, startedAt:startedAt}, true); statusText.textContent = 'Running'; tickId = setInterval(function(){ var st = BLS.loadState(); if(!st.running){ clearInterval(tickId); tickId=null; return } var elapsed = Math.floor((Date.now()-st.startedAt)/1000); var rem = Math.max(0, st.durationSec - elapsed); var em = Math.floor(elapsed/60), es = elapsed%60; elapsedText.textContent = String(em).padStart(2,'0')+':'+String(es).padStart(2,'0'); var rm = Math.floor(rem/60); timerDisplay.textContent = String(rm).padStart(2,'0')+':'+String(rem%60).padStart(2,'0'); if(rem<=0){ stopTimer() } }, 250); } function stopTimer(){ if(tickId){ clearInterval(tickId); tickId=null } recordSession(); applyState({running:false}, true); statusText.textContent = 'Idle'; refreshPatientUI() } startBtn.addEventListener('click', startTimer); endBtn.addEventListener('click', stopTimer); openPatient.addEventListener('click', function(){ window.open('patient.html','bls-patient'); }); renderer.resize(360,220); updateSpeedLabel(); updateSizeLabel(); updateYPercentLabel(); updateTimerLabel(); updateGlowIntensityLabel(); updateGlowRateLabel(); updateRampLabel(); updateEdgePauseLabel(); updateWiggleLabel(); updatePanRateLabel(); updateCueRateLabel(); updateVolumeLabel(); statusText.textContent = state.running? 'Running':'Idle'; setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode'); audioEnable.checked = !!state.audioEnabled; if(state.audioName){ var opt = document.createElement('option'); opt.value = state.audioName; opt.textContent = state.audioName; audioSelect.appendChild(opt); audioSelect.value = state.audioName } glowEnable.checked = !!state.glowEnabled; glowIntensity.value = Math.round((state.glowIntensity||1)*100); rampEnable.checked = !!state.rampEnabled; rampSeconds.value = parseInt(state.rampSeconds||10,10); edgePause.value = parseInt(state.edgePauseMs||0,10); wiggleEnable.checked = !!state.wiggleEnabled; wiggleAmplitude.value = parseInt(state.wiggleAmplitude||0,10); audioMode.value = state.audioMode || 'programmatic'; programPreset.value = state.programPreset || 'pink'; syncPan.checked = !!state.syncPan; panRate.value = String(state.panRate||0.5); cueRate.value = String(state.cueRate||1.2); volume.value = String(Math.round((state.volume||0.6)*100)); glowRate.value = String(state.glowRate||0.5); refreshAudioMode(); updateKnobSensLabel(); yPercent.value = parseInt(state.yPercent||50,10); updateYPercentLabel(); refreshPresetList(); refreshPatientList(); refreshPatientUI(); } function waitForBLS(){ if(window.BLS){ initAdmin() } else { setTimeout(waitForBLS, 50) } } if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', waitForBLS) } else { waitForBLS() }