235 lines
7.4 KiB
JavaScript
235 lines
7.4 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
const views = document.querySelectorAll('.view');
|
|
const filterSection = document.querySelector('.filter-section');
|
|
const themeSelect = document.getElementById('theme-select');
|
|
const themeLink = document.getElementById('theme-stylesheet');
|
|
const btnExport = document.getElementById('btn-export');
|
|
const btnApply = document.getElementById('btn-apply');
|
|
const btnSave = document.getElementById('btn-save');
|
|
const btnReset = document.getElementById('btn-reset');
|
|
const presetSelect = document.getElementById('preset-select');
|
|
const tuningGrid = document.getElementById('tuning-grid');
|
|
const fretsInput = document.getElementById('frets-input');
|
|
const fingersInput = document.getElementById('fingers-input');
|
|
const loading = document.getElementById('chord-loading');
|
|
|
|
const allNotes = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
|
|
|
// --- Audio ---
|
|
const noteToSemitone = {C:0,'C#':1,D:2,'D#':3,E:4,F:5,'F#':6,G:7,'G#':8,A:9,'A#':10,B:11};
|
|
const standardMIDI = [40, 45, 50, 55, 59, 64];
|
|
|
|
function tuningNamesToMIDI(names) {
|
|
return names.map((n, i) => {
|
|
const pc = noteToSemitone[n];
|
|
if (pc === undefined) return standardMIDI[i] || 40;
|
|
const std = standardMIDI[i] || 40;
|
|
for (let m = std - 6; m <= std + 6; m++) {
|
|
if (((m % 12) + 12) % 12 === pc) return m;
|
|
}
|
|
return std;
|
|
});
|
|
}
|
|
|
|
window.currentTuningMIDI = standardMIDI.slice();
|
|
|
|
let polySynth = null;
|
|
window.playChord = function(midiNotes) {
|
|
Tone.start();
|
|
if (!polySynth) {
|
|
polySynth = new Tone.PolySynth(Tone.Synth, {maxPolyphony: 8}).toDestination();
|
|
polySynth.set({
|
|
oscillator: {type: 'triangle'},
|
|
envelope: {attack: 0.01, decay: 0.3, sustain: 0.4, release: 1.0}
|
|
});
|
|
}
|
|
polySynth.releaseAll();
|
|
const now = Tone.now();
|
|
midiNotes.forEach((m, i) => {
|
|
const freq = 440 * Math.pow(2, (m - 69) / 12);
|
|
polySynth.triggerAttackRelease(freq, '1.5s', now + i * 0.03);
|
|
});
|
|
};
|
|
|
|
const presets = {
|
|
'Standard': ['E','A','D','G','B','E'],
|
|
'Drop D': ['D','A','D','G','B','E'],
|
|
'DADGAD': ['D','A','D','G','A','D'],
|
|
'Open G': ['D','G','B','D','G','B'],
|
|
'Open D': ['D','A','D','F#','A','D'],
|
|
'Open C': ['C','G','C','G','C','E'],
|
|
'Half Step Down': ['D#','G#','C#','F#','A#','D#'],
|
|
'Full Step Down': ['D','G','C','F','A','D'],
|
|
'Custom': null
|
|
};
|
|
|
|
const shapesSection = document.getElementById('shapes-section');
|
|
|
|
let currentConfig = null;
|
|
let chordsLoaded = false;
|
|
let shapesInited = false;
|
|
|
|
// --- Navigation ---
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
const target = link.dataset.view;
|
|
navLinks.forEach(l => l.classList.remove('active'));
|
|
views.forEach(v => v.classList.remove('active'));
|
|
link.classList.add('active');
|
|
document.getElementById('view-' + target).classList.add('active');
|
|
filterSection.style.display = target === 'chords' ? '' : 'none';
|
|
shapesSection.style.display = target === 'shapes' ? '' : 'none';
|
|
|
|
if (target === 'chords' && !chordsLoaded) {
|
|
loadChords();
|
|
}
|
|
if (target === 'shapes' && !shapesInited) {
|
|
shapesInited = true;
|
|
if (window.initShapeExplorer) window.initShapeExplorer();
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Theme ---
|
|
themeSelect.addEventListener('change', () => {
|
|
themeLink.href = 'chords-' + themeSelect.value + '.css';
|
|
});
|
|
|
|
// --- PDF Export ---
|
|
btnExport.addEventListener('click', () => window.print());
|
|
|
|
// --- Config Panel ---
|
|
function populatePresets() {
|
|
presetSelect.innerHTML = '';
|
|
for (const name of Object.keys(presets)) {
|
|
const opt = document.createElement('option');
|
|
opt.value = name;
|
|
opt.textContent = name;
|
|
presetSelect.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
function buildTuningGrid(tuning) {
|
|
tuningGrid.innerHTML = '';
|
|
tuning.forEach((note, i) => {
|
|
const sel = document.createElement('select');
|
|
allNotes.forEach(n => {
|
|
const opt = document.createElement('option');
|
|
opt.value = n;
|
|
opt.textContent = n;
|
|
if (n === note) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
});
|
|
sel.addEventListener('change', () => {
|
|
presetSelect.value = 'Custom';
|
|
});
|
|
tuningGrid.appendChild(sel);
|
|
});
|
|
}
|
|
|
|
function readTuningFromGrid() {
|
|
return Array.from(tuningGrid.querySelectorAll('select')).map(s => s.value);
|
|
}
|
|
|
|
function syncConfigPanel(cfg) {
|
|
fretsInput.value = cfg.frets;
|
|
fingersInput.value = cfg.max_fingers;
|
|
buildTuningGrid(cfg.tuning);
|
|
|
|
let matched = false;
|
|
for (const [name, notes] of Object.entries(presets)) {
|
|
if (notes && notes.length === cfg.tuning.length &&
|
|
notes.every((n, i) => n === cfg.tuning[i])) {
|
|
presetSelect.value = name;
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!matched) presetSelect.value = 'Custom';
|
|
}
|
|
|
|
presetSelect.addEventListener('change', () => {
|
|
const notes = presets[presetSelect.value];
|
|
if (notes) {
|
|
buildTuningGrid(notes);
|
|
}
|
|
});
|
|
|
|
btnApply.addEventListener('click', () => {
|
|
if (!window.go) return;
|
|
const cfg = {
|
|
instrument: currentConfig ? currentConfig.instrument : 'guitar',
|
|
tuning: readTuningFromGrid(),
|
|
frets: parseInt(fretsInput.value) || 4,
|
|
max_fingers: parseInt(fingersInput.value) || 4
|
|
};
|
|
loading.style.display = '';
|
|
loading.textContent = 'Regenerating chords...';
|
|
|
|
window.go.main.App.UpdateConfig(cfg).then(chords => {
|
|
currentConfig = cfg;
|
|
window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning);
|
|
loading.style.display = 'none';
|
|
chordsLoaded = true;
|
|
if (window.buildChordCards) {
|
|
window.buildChordCards(chords || [], cfg.frets, cfg.tuning.length);
|
|
}
|
|
}).catch(err => {
|
|
loading.textContent = 'Error: ' + err;
|
|
});
|
|
});
|
|
|
|
btnSave.addEventListener('click', () => {
|
|
if (!window.go) return;
|
|
window.go.main.App.SaveConfig().then(() => {
|
|
btnSave.textContent = 'Saved';
|
|
setTimeout(() => { btnSave.textContent = 'Save'; }, 1500);
|
|
}).catch(err => {
|
|
btnSave.textContent = 'Error';
|
|
setTimeout(() => { btnSave.textContent = 'Save'; }, 1500);
|
|
});
|
|
});
|
|
|
|
btnReset.addEventListener('click', () => {
|
|
if (!window.go) return;
|
|
window.go.main.App.ResetConfig().then(cfg => {
|
|
currentConfig = cfg;
|
|
window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning);
|
|
syncConfigPanel(cfg);
|
|
loadChords();
|
|
});
|
|
});
|
|
|
|
// --- Load Chords ---
|
|
function loadChords() {
|
|
if (!window.go) {
|
|
loading.textContent = 'Wails runtime not available.';
|
|
return;
|
|
}
|
|
loading.style.display = '';
|
|
loading.textContent = 'Loading chord fingerings...';
|
|
|
|
window.go.main.App.FindChordFingerings().then(chords => {
|
|
loading.style.display = 'none';
|
|
chordsLoaded = true;
|
|
if (window.buildChordCards) {
|
|
window.buildChordCards(chords || [], currentConfig.frets, currentConfig.tuning.length);
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Init ---
|
|
populatePresets();
|
|
|
|
if (window.go && window.go.main && window.go.main.App) {
|
|
window.go.main.App.GetConfig().then(cfg => {
|
|
currentConfig = cfg;
|
|
window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning);
|
|
syncConfigPanel(cfg);
|
|
});
|
|
} else {
|
|
loading.textContent = 'Wails runtime not available.';
|
|
}
|
|
});
|