feedBack/plugins/tuner/settings.html
ChrisBeWithYou 6bfd92aa06
Fix tuner auto-open flash: opt-in + persist (issue E, stage 1/3) (#655)
The tuner self-closes on song:play; autoplay fires it right after a song
switch, so an auto-opened tuner flashed shut ~1s later. An arrangement
switch (which never arms autoplay) instead persisted — the opposite
tester reports, and not the mic.

- New opt-in setting autoOpenOnTuningChange (tuner Settings, default OFF)
- An auto-opened tuner persists: it ignores the autoplay song:play, stray
  outside-clicks, and same-screen re-emits, closing only via the new
  in-panel x / Skip buttons or leaving the song. A manual open keeps the
  classic click-away / play-to-close behaviour.
- Adds the panel's first in-box close (x + contextual Skip).
- All in the tuner plugin; no core app.js changes.

Default (opt-in vs opt-out) is teed up for Byron to flip one boolean.
Staged follow-ups: E1.5 = instrument-coverage smart prompting + badge
cue; E2 = holdAutoplay gate.

Tests: tests/js/tuner_auto_open.test.js (opt-in gate, persist mode,
play/click-proofing).


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:24:39 +02:00

360 lines
19 KiB
HTML

<div class="space-y-6 py-2">
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
<div>
<h3 class="text-sm font-medium text-gray-200">Floating Button</h3>
<p class="text-[11px] text-gray-500">Show the tuner button on the main interface.</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="tuner-show-floating" class="sr-only peer" onchange="window._tunerToggleFloating(this.checked)">
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-accent"></div>
</label>
</div>
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
<div>
<h3 class="text-sm font-medium text-gray-200">Auto-open on tuning change</h3>
<p class="text-[11px] text-gray-500">When a song (or arrangement) needs a different tuning, pop the tuner open automatically. It stays open until you Skip or close it.</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="tuner-auto-open" class="sr-only peer" onchange="window._tunerToggleAutoOpen(this.checked)">
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-accent"></div>
</label>
</div>
<script>
if (window.feedBackDesktop && window.feedBackDesktop.isDesktop) {
document.currentScript.insertAdjacentHTML('beforebegin', `
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
<div>
<h3 class="text-sm font-medium text-gray-200">Force Browser Audio</h3>
<p class="text-[11px] text-gray-500">Use the browser microphone pipeline instead of the desktop audio engine.</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="tuner-force-browser-audio" class="sr-only peer" onchange="window._tunerToggleBrowserAudio(this.checked)">
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-accent"></div>
</label>
</div>`);
}
</script>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-3">Tuning Visibility</h3>
<p class="text-xs text-gray-500 mb-4">Toggle which built-in tunings appear in the tuner menu.</p>
<div id="tuner-visibility-list" class="space-y-2 pr-2">
<!-- Populated by JS -->
</div>
</div>
<div class="pt-4 border-t border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Custom Tunings</h3>
<div id="tuner-custom-list" class="space-y-2 mb-4">
<!-- Populated by JS -->
</div>
<div class="bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
<p class="text-[11px] font-bold text-gray-500 uppercase tracking-wider mb-3">Add Custom Tuning</p>
<div class="space-y-3">
<input type="text" id="tuner-new-name" placeholder="Tuning Name (e.g. DADGAD)"
class="w-full bg-dark-800 border-none rounded-lg px-3 py-2 text-xs text-white focus:ring-1 focus:ring-accent/30 outline-none">
<select id="tuner-new-instrument"
class="w-full bg-dark-800 border-none rounded-lg px-3 py-2 text-xs text-white focus:ring-1 focus:ring-accent/30 outline-none">
<optgroup label="Guitar">
<option value="guitar-6">Guitar 6-string</option>
<option value="guitar-7">Guitar 7-string</option>
<option value="guitar-8">Guitar 8-string</option>
</optgroup>
<optgroup label="Bass">
<option value="bass-4">Bass 4-string</option>
<option value="bass-5">Bass 5-string</option>
</optgroup>
</select>
<div class="flex flex-col gap-1">
<label class="text-[10px] text-gray-500 ml-1">Notes or Frequencies (e.g. E2, A2, D3, G3 or 82.41, 110.0...)</label>
<input type="text" id="tuner-new-freqs" placeholder="E2, A2, D3, G3, B3, E4"
class="w-full bg-dark-800 border-none rounded-lg px-3 py-2 text-xs text-white focus:ring-1 focus:ring-accent/30 outline-none font-mono">
</div>
<button onclick="window._tunerAddCustom()"
class="w-full bg-accent/20 hover:bg-accent/30 text-accent text-xs font-bold py-2 rounded-lg transition-colors border border-accent/20">
Add Tuning
</button>
</div>
</div>
</div>
<p class="text-[11px] text-gray-600 italic">Settings are saved automatically.</p>
<script>
(function() {
let config = { customTunings: {}, disabledTunings: [], defaultTunings: {}, showFloatingButton: true, audioInputMode: 'auto' };
let expandedGroups = [];
var _INSTRUMENT_CAPTIONS = {
"guitar-6": "Guitar",
"guitar-7": "Guitar 7-string",
"guitar-8": "Guitar 8-string",
"bass-4": "Bass 4-string",
"bass-5": "Bass 5-string"
};
var _instrumentLabels = {
"guitar-6": "Guitar 6-string",
"guitar-7": "Guitar 7-string",
"guitar-8": "Guitar 8-string",
"bass-4": "Bass 4-string",
"bass-5": "Bass 5-string"
};
async function load() {
try {
const resp = await fetch('/api/plugins/tuner/config');
config = await resp.json();
const floatingToggle = document.getElementById('tuner-show-floating');
if (floatingToggle) floatingToggle.checked = config.showFloatingButton !== false;
const browserAudioToggle = document.getElementById('tuner-force-browser-audio');
if (browserAudioToggle) browserAudioToggle.checked = config.audioInputMode === 'browser';
const autoOpenToggle = document.getElementById('tuner-auto-open');
if (autoOpenToggle) autoOpenToggle.checked = config.autoOpenOnTuningChange === true;
render();
} catch (e) { console.error('Tuner settings: load failed', e); }
}
window._tunerToggleFloating = (enabled) => {
config.showFloatingButton = enabled;
save();
};
window._tunerToggleBrowserAudio = (forceBrowser) => {
config.audioInputMode = forceBrowser ? 'browser' : 'auto';
save();
};
window._tunerToggleAutoOpen = (enabled) => {
config.autoOpenOnTuningChange = enabled;
save();
};
async function save(opts) {
try {
await fetch('/api/plugins/tuner/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (window._tunerReloadConfig) window._tunerReloadConfig();
if (opts && opts.tuningsChanged) window.feedBack?.emit('tunings:updated');
} catch (e) { console.error('Tuner settings: save failed', e); }
}
function render() {
const visList = document.getElementById('tuner-visibility-list');
visList.innerHTML = '';
const defaultTunings = config.defaultTunings || {};
Object.keys(defaultTunings).forEach(groupName => {
const group = defaultTunings[groupName];
const groupTunings = Object.keys(group);
const instrument = groupName;
// Compound keys for all tunings in this group
const compoundKeys = groupTunings.map(n => instrument + ':' + n);
const groupWrapper = document.createElement('div');
groupWrapper.className = 'mb-4';
const header = document.createElement('div');
header.className = 'flex items-center justify-between p-2 mt-2 bg-dark-900/80 rounded-t-lg border-x border-t border-gray-800/50 cursor-pointer hover:bg-dark-900 transition-colors';
const left = document.createElement('div');
left.className = 'flex items-center gap-2';
const chevron = document.createElement('span');
chevron.innerHTML = '<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>';
chevron.className = 'text-gray-500 transition-transform duration-200 rotate-0';
const groupLabel = document.createElement('span');
groupLabel.className = 'text-[11px] font-bold text-gray-400 uppercase tracking-wider';
groupLabel.textContent = _INSTRUMENT_CAPTIONS[groupName] || groupName;
left.appendChild(chevron);
left.appendChild(groupLabel);
const groupToggle = document.createElement('input');
groupToggle.type = 'checkbox';
const allEnabled = compoundKeys.every(k => !config.disabledTunings.includes(k));
const someEnabled = compoundKeys.some(k => !config.disabledTunings.includes(k));
groupToggle.checked = allEnabled;
groupToggle.indeterminate = someEnabled && !allEnabled;
groupToggle.className = 'accent-accent';
groupToggle.onclick = (e) => e.stopPropagation();
groupToggle.onchange = () => {
if (groupToggle.checked) {
config.disabledTunings = config.disabledTunings.filter(k => !compoundKeys.includes(k));
} else {
compoundKeys.forEach(k => {
if (!config.disabledTunings.includes(k)) config.disabledTunings.push(k);
});
}
save();
render();
};
header.appendChild(left);
header.appendChild(groupToggle);
groupWrapper.appendChild(header);
const groupContainer = document.createElement('div');
groupContainer.className = 'border-x border-b border-gray-800/50 rounded-b-lg overflow-hidden';
const isExpanded = expandedGroups.includes(groupName);
if (!isExpanded) {
groupContainer.classList.add('hidden');
chevron.classList.remove('rotate-90');
} else {
chevron.classList.add('rotate-90');
}
header.onclick = () => {
const idx = expandedGroups.indexOf(groupName);
if (idx === -1) {
expandedGroups.push(groupName);
} else {
expandedGroups.splice(idx, 1);
}
render();
};
groupTunings.forEach((name, idx) => {
const compoundKey = instrument + ':' + name;
const div = document.createElement('div');
div.className = `flex items-center justify-between p-2 bg-dark-800/30 hover:bg-dark-800/50 transition-colors ${idx === 0 ? 'border-t-0' : 'border-t border-gray-800/20'}`;
const label = document.createElement('span');
label.className = 'text-xs text-gray-300';
label.textContent = name;
const toggle = document.createElement('input');
toggle.type = 'checkbox';
toggle.checked = !config.disabledTunings.includes(compoundKey);
toggle.className = 'accent-accent';
toggle.onchange = () => {
if (toggle.checked) {
config.disabledTunings = config.disabledTunings.filter(k => k !== compoundKey);
} else {
if (!config.disabledTunings.includes(compoundKey)) config.disabledTunings.push(compoundKey);
}
save();
render();
};
div.appendChild(label);
div.appendChild(toggle);
groupContainer.appendChild(div);
});
groupWrapper.appendChild(groupContainer);
visList.appendChild(groupWrapper);
});
const customList = document.getElementById('tuner-custom-list');
customList.innerHTML = '';
const customNames = Object.keys(config.customTunings);
if (customNames.length === 0) {
customList.innerHTML = '<p class="text-[10px] text-gray-600 italic ml-1">No custom tunings added.</p>';
} else {
customNames.forEach(name => {
const val = config.customTunings[name];
const strings = Array.isArray(val) ? val : (val.strings || []);
const instrument = Array.isArray(val) ? 'guitar-6' : (val.instrument || 'guitar-6');
const instrLabel = _instrumentLabels[instrument] || instrument;
const div = document.createElement('div');
div.className = 'flex items-center justify-between p-2 bg-dark-800/30 rounded-lg border border-gray-800/50';
const info = document.createElement('div');
info.className = 'flex flex-col';
const nEl = document.createElement('span');
nEl.className = 'text-xs text-white font-medium';
nEl.textContent = name + ' · ' + instrLabel;
const fEl = document.createElement('span');
fEl.className = 'text-[10px] text-gray-500 font-mono';
fEl.textContent = strings.join(', ');
info.appendChild(nEl);
info.appendChild(fEl);
const del = document.createElement('button');
del.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>';
del.className = 'text-gray-600 hover:text-red-400 transition-colors p-1';
del.onclick = () => {
delete config.customTunings[name];
save({ tuningsChanged: true });
render();
};
div.appendChild(info);
div.appendChild(del);
customList.appendChild(div);
});
}
}
function noteToFreq(note) {
const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const res = note.toUpperCase().match(/^([A-G]#?)(\d)$/);
if (!res) return null;
const name = res[1];
const octave = parseInt(res[2]);
const semitones = notes.indexOf(name) + (octave + 1) * 12;
return 440 * Math.pow(2, (semitones - 69) / 12);
}
window._tunerAddCustom = () => {
const nameInput = document.getElementById('tuner-new-name');
const instrInput = document.getElementById('tuner-new-instrument');
const freqsInput = document.getElementById('tuner-new-freqs');
const name = nameInput.value.trim();
const instrument = instrInput.value;
const inputStr = freqsInput.value.trim();
let errP = document.getElementById('tuner-add-error');
if (!errP) {
errP = document.createElement('p');
errP.id = 'tuner-add-error';
errP.className = 'text-[10px] text-red-400 mt-1 ml-1 hidden';
document.getElementById('tuner-new-freqs').parentElement.parentElement.appendChild(errP);
}
const showErr = (msg) => { errP.textContent = msg; errP.classList.remove('hidden'); };
const clearErr = () => errP.classList.add('hidden');
if (!name || !inputStr) return showErr('Enter a name and notes/frequencies.');
const freqs = inputStr.split(',').map(s => {
s = s.trim();
const f = parseFloat(s);
if (!isNaN(f)) return Math.round(f * 100) / 100;
const nf = noteToFreq(s);
return nf !== null ? Math.round(nf * 100) / 100 : null;
}).filter(f => f !== null && !isNaN(f));
if (freqs.length === 0) return showErr('Invalid notes or frequencies — use E2, A2 or Hz values.');
clearErr();
config.customTunings[name] = { instrument, strings: freqs };
save({ tuningsChanged: true });
render();
nameInput.value = '';
instrInput.value = 'guitar-6';
freqsInput.value = '';
};
load();
})();
</script>
</div>