mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-23 05:11:34 +00:00
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>
360 lines
19 KiB
HTML
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>
|