update: autosearch for musics

This commit is contained in:
Nathan FONTEYNE 2026-07-08 14:46:48 +02:00
parent 9426b73940
commit 6d8fe814b0
12 changed files with 380 additions and 12 deletions

View file

@ -10,10 +10,13 @@ function applyTheme(theme) {
updateThemeToggleIcon();
}
const SUN_ICON = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>`;
const MOON_ICON = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`;
function updateThemeToggleIcon() {
const btn = document.getElementById('theme-toggle');
if (!btn) return;
btn.textContent = currentTheme() === 'dark' ? '☀️' : '🌙';
btn.innerHTML = currentTheme() === 'dark' ? SUN_ICON : MOON_ICON;
}
async function initNav(activePage) {

View file

@ -231,12 +231,20 @@ async function onAddSong(e) {
try {
await api.post('/api/songs', { title, artist, notes, youtubeUrl, spotifyUrl });
form.reset();
document.getElementById('song-links-status').textContent = '';
closeAutocompleteDropdown();
setAddSongPanelOpen(false);
await loadSongs();
} catch (err) {
showError(err.message);
}
}
function setAddSongPanelOpen(open) {
document.getElementById('add-song-panel').style.display = open ? 'block' : 'none';
document.getElementById('toggle-add-song').textContent = open ? 'Annuler' : '+ Ajouter un morceau';
}
async function onSaveSong(e) {
e.preventDefault();
const form = e.target;
@ -269,9 +277,125 @@ function showError(message) {
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
}
let autocompleteTimer = null;
let autocompleteCandidates = [];
let autocompleteHighlight = -1;
function candidateItemTemplate(c, index) {
const img = c.artworkUrl
? `<img src="${escapeHtml(c.artworkUrl)}" alt="">`
: `<span class="autocomplete-thumb-placeholder">&#9835;</span>`;
return `
<div class="autocomplete-item${index === autocompleteHighlight ? ' active' : ''}" data-index="${index}">
${img}
<div>
<div>${escapeHtml(c.title)}</div>
<div class="card-subtitle">${escapeHtml(c.artist)}</div>
</div>
</div>
`;
}
function renderAutocompleteDropdown() {
const dropdown = document.getElementById('song-title-dropdown');
if (!autocompleteCandidates.length) {
dropdown.style.display = 'none';
dropdown.innerHTML = '';
return;
}
dropdown.innerHTML = autocompleteCandidates.map(candidateItemTemplate).join('');
dropdown.style.display = 'block';
dropdown.querySelectorAll('.autocomplete-item').forEach((el) => {
el.addEventListener('mousedown', (e) => {
e.preventDefault();
selectCandidate(autocompleteCandidates[parseInt(el.dataset.index, 10)]);
});
});
}
function closeAutocompleteDropdown() {
autocompleteCandidates = [];
autocompleteHighlight = -1;
renderAutocompleteDropdown();
}
async function selectCandidate(candidate) {
document.getElementById('song-title-input').value = candidate.title;
document.getElementById('song-artist-input').value = candidate.artist;
closeAutocompleteDropdown();
const statusEl = document.getElementById('song-links-status');
statusEl.textContent = 'Recherche des liens YouTube / Spotify…';
try {
const links = await api.get(
`/api/music-search/links?title=${encodeURIComponent(candidate.title)}&artist=${encodeURIComponent(candidate.artist)}`
);
const youtubeInput = document.getElementById('song-youtube-input');
const spotifyInput = document.getElementById('song-spotify-input');
if (links.youtubeUrl && !youtubeInput.value) youtubeInput.value = links.youtubeUrl;
if (links.spotifyUrl && !spotifyInput.value) spotifyInput.value = links.spotifyUrl;
if (links.youtubeUrl && links.spotifyUrl) statusEl.textContent = 'Liens YouTube et Spotify trouvés automatiquement.';
else if (links.youtubeUrl) statusEl.textContent = 'Lien YouTube trouvé automatiquement. Aucun lien Spotify trouvé — à saisir manuellement si besoin.';
else if (links.spotifyUrl) statusEl.textContent = 'Lien Spotify trouvé automatiquement. Aucun lien YouTube trouvé — à saisir manuellement si besoin.';
else statusEl.textContent = 'Aucun lien trouvé automatiquement — vous pouvez les saisir manuellement.';
} catch (err) {
statusEl.textContent = '';
}
}
function initTitleAutocomplete() {
const input = document.getElementById('song-title-input');
input.addEventListener('input', () => {
const query = input.value.trim();
clearTimeout(autocompleteTimer);
if (query.length < 2) {
closeAutocompleteDropdown();
return;
}
autocompleteTimer = setTimeout(async () => {
try {
autocompleteCandidates = await api.get(`/api/music-search?q=${encodeURIComponent(query)}`);
autocompleteHighlight = -1;
renderAutocompleteDropdown();
} catch (err) {
closeAutocompleteDropdown();
}
}, 300);
});
input.addEventListener('keydown', (e) => {
if (!autocompleteCandidates.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
autocompleteHighlight = Math.min(autocompleteHighlight + 1, autocompleteCandidates.length - 1);
renderAutocompleteDropdown();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
autocompleteHighlight = Math.max(autocompleteHighlight - 1, 0);
renderAutocompleteDropdown();
} else if (e.key === 'Enter' && autocompleteHighlight >= 0) {
e.preventDefault();
selectCandidate(autocompleteCandidates[autocompleteHighlight]);
} else if (e.key === 'Escape') {
closeAutocompleteDropdown();
}
});
input.addEventListener('blur', () => {
setTimeout(closeAutocompleteDropdown, 150);
});
}
(async function init() {
me = await initNav('repertoire');
await loadInstruments();
document.getElementById('add-song-form').addEventListener('submit', onAddSong);
document.getElementById('toggle-add-song').addEventListener('click', () => {
const isOpen = document.getElementById('add-song-panel').style.display !== 'none';
setAddSongPanelOpen(!isOpen);
});
initTitleAutocomplete();
await loadSongs();
})();

View file

@ -192,12 +192,18 @@ async function onAddSuggestion(e) {
try {
await api.post('/api/suggestions', { title, artist, youtubeUrl, description });
form.reset();
setAddSuggestionPanelOpen(false);
await loadSuggestions();
} catch (err) {
showError(err.message);
}
}
function setAddSuggestionPanelOpen(open) {
document.getElementById('add-suggestion-panel').style.display = open ? 'block' : 'none';
document.getElementById('toggle-add-suggestion').textContent = open ? 'Annuler' : '+ Proposer un morceau';
}
function showError(message) {
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
}
@ -205,5 +211,9 @@ function showError(message) {
(async function init() {
me = await initNav('suggestions');
document.getElementById('add-suggestion-form').addEventListener('submit', onAddSuggestion);
document.getElementById('toggle-add-suggestion').addEventListener('click', () => {
const isOpen = document.getElementById('add-suggestion-panel').style.display !== 'none';
setAddSuggestionPanelOpen(!isOpen);
});
await loadSuggestions();
})();