diff --git a/renderer/index.html b/renderer/index.html index 6ce4b5b..a85e100 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -326,6 +326,28 @@ .timer-card-bar{height:3px;background:var(--border);margin-top:10px;overflow:hidden;} .timer-card-bar-fill{height:100%;transition:width .9s linear;} .timer-empty-msg{color:var(--text-dim);font-size:13px;padding:20px 0;} + + /* --- Volume slider --- */ + input[type=range]{-webkit-appearance:none;height:3px;background:var(--border);outline:none;cursor:pointer;flex:1;} + input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;background:var(--moss);cursor:pointer;} + input[type=range]::-moz-range-thumb{width:11px;height:11px;background:var(--moss);cursor:pointer;border:none;} + + /* --- Alert config rows --- */ + .alert-cfg-row{display:flex;align-items:center;gap:8px;padding:7px 0;border-bottom:1px solid var(--border);} + .alert-cfg-row:last-child{border-bottom:none;} + .alert-cfg-name{flex:1;font-size:12px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} + .alert-cfg-sound{font-size:11px;background:var(--bg);border:1px solid var(--border);color:var(--text); + padding:2px 5px;cursor:pointer;max-width:140px;} + + /* --- Sound library --- */ + .sound-lib-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:8px;margin-top:10px;} + .sound-card{background:var(--bg);border:1px solid var(--border);padding:10px 12px; + display:flex;flex-direction:column;gap:5px;} + .sound-card-name{font-size:12px;font-weight:600;} + .sound-card-desc{font-size:11px;color:var(--text-dim);} + .sound-card-actions{display:flex;gap:6px;align-items:center;margin-top:2px;} + .sound-card-status{font-size:10px;font-family:var(--font-mono);color:var(--moss);margin-left:auto;} + #chartTip{position:fixed;z-index:1001;background:var(--bg-panel);border:1px solid var(--border); padding:4px 10px;font-family:var(--font-mono);font-size:11px;color:var(--text); pointer-events:none;display:none;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.5);} @@ -725,15 +747,34 @@
-

Alertes de drop — son au drop d'un item

+

Alertes de drop

+
+ Volume + + 25% +
+
-

Un ping discret est joué à chaque fois que cet item drop.

-
+

Son joué à chaque fois que cet item drop. Volume global s'applique à toutes les alertes.

+
+
+ +
+
+

Bibliothèque de sons Minecraft

+ ▶ ouvrir +
+
@@ -1392,7 +1433,7 @@ if (alertConfigs.length){ const pn = mcPlain(it.name || '').toLowerCase(); for (const alert of alertConfigs){ - if (pn && alert.nameLower && pn.includes(alert.nameLower)){ playAlertPing(); break; } + if (pn && alert.nameLower && pn.includes(alert.nameLower)){ playAlert(alert); break; } } } } @@ -2177,20 +2218,133 @@ // ====================================================================== let timerConfigs = JSON.parse(localStorage.getItem('timerConfigs') || '[]'); - let alertConfigs = JSON.parse(localStorage.getItem('alertConfigs') || '[]'); + // ---- Sound catalog ---- + const SOUND_CATALOG = [ + { id:'levelup', label:'Level Up XP', desc:'Montée de niveau', hash:'19034765ba8ba5389b35804ab213537ab5cf706f' }, + { id:'orb', label:'XP Orb', desc:'Ramassage d\'orbe XP', hash:'8a04a60d5c28fc60df472a877ca57f37eabc78d7' }, + { id:'pop', label:'Item Pop', desc:'Ramassage d\'item', hash:'d6ae1c04d0a7376a33d1df12e1b8057cfbab6bc2' }, + { id:'pling', label:'Pling', desc:'Note de bloc Pling', hash:'774ae41e86f0b62a5cc961d1bc2e3d0aef9d229c' }, + { id:'bell', label:'Bell', desc:'Note de bloc Bell', hash:'a1e833dec61595dc79d0c672fcd7838579ca4b14' }, + { id:'toast', label:'Toast Avancement', desc:'Notification avancement', hash:'506f2fdb1b7530df66134aa04c71e66513df0c93' }, + { id:'challenge', label:'Défi Terminé', desc:'Challenge complété', hash:'bd01dff39a7bd1e0e7f6847f7aee981f157e4f94' }, + { id:'chestopen', label:'Coffre', desc:'Ouverture de coffre', hash:'186d5d9481d59cc99bc4be1b5fbb98d0ef877b8e' }, + { id:'hit', label:'Hit', desc:'Coup réussi', hash:'57f50076e7b91b12595a17cf0d38303f979f862b' }, + { id:'click', label:'Click', desc:'Clic de bouton', hash:'3455ca942556c7d5eac7dd5e458e7fb3bad564c9' }, + ]; + + // ---- IndexedDB cache pour sons téléchargés ---- + let _soundDb = null; + + function openSoundDb(){ + if (_soundDb) return Promise.resolve(_soundDb); + return new Promise((res, rej) => { + const req = indexedDB.open('FarmTrackerSounds', 1); + req.onupgradeneeded = e => e.target.result.createObjectStore('sounds', { keyPath: 'id' }); + req.onsuccess = e => { _soundDb = e.target.result; res(_soundDb); }; + req.onerror = e => rej(e.target.error); + }); + } + + async function isSoundCached(soundId){ + try { + const db = await openSoundDb(); + return new Promise(res => { + const req = db.transaction('sounds','readonly').objectStore('sounds').get(soundId); + req.onsuccess = () => res(!!req.result); + req.onerror = () => res(false); + }); + } catch { return false; } + } + + async function getSoundBytes(soundId){ + const db = await openSoundDb(); + return new Promise((res, rej) => { + const req = db.transaction('sounds','readonly').objectStore('sounds').get(soundId); + req.onsuccess = () => res(req.result ? req.result.buffer : null); + req.onerror = () => rej(req.error); + }); + } + + async function saveSoundBytes(soundId, buffer){ + const db = await openSoundDb(); + return new Promise((res, rej) => { + const tx = db.transaction('sounds','readwrite'); + tx.objectStore('sounds').put({ id: soundId, buffer }); + tx.oncomplete = res; + tx.onerror = () => rej(tx.error); + }); + } + + async function downloadSound(soundId, onProgress){ + const entry = SOUND_CATALOG.find(s => s.id === soundId); + if (!entry) throw new Error('Unknown sound: ' + soundId); + const prefix = entry.hash.slice(0, 2); + const url = `https://resources.download.minecraft.net/${prefix}/${entry.hash}`; + const resp = await fetch(url); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const buffer = await resp.arrayBuffer(); + await saveSoundBytes(soundId, buffer); + _audioBufferCache.delete(soundId); + return buffer; + } + + // ---- Web Audio playback ---- + const _audioBufferCache = new Map(); + let _audioCtx = null; + + function getAudioCtx(){ + if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + return _audioCtx; + } + + async function _getDecodedBuffer(soundId){ + if (_audioBufferCache.has(soundId)) return _audioBufferCache.get(soundId); + let bytes = await getSoundBytes(soundId); + if (!bytes && soundId === 'levelup'){ + // fallback : fichier bundlé dans renderer/ + const r = await fetch('levelup.ogg'); + bytes = await r.arrayBuffer(); + await saveSoundBytes('levelup', bytes); + } + if (!bytes) return null; + const ctx = getAudioCtx(); + const decoded = await ctx.decodeAudioData(bytes.slice(0)); + _audioBufferCache.set(soundId, decoded); + return decoded; + } + + function getAlertVolume(){ + return parseInt(localStorage.getItem('alertGlobalVolume') || '25', 10) / 100; + } + + async function playAlert(cfg){ + try { + const soundId = cfg ? (cfg.soundId || 'levelup') : 'levelup'; + const vol = getAlertVolume(); + const buf = await _getDecodedBuffer(soundId); + if (!buf){ console.warn('Sound not cached:', soundId); return; } + const ctx = getAudioCtx(); + const src = ctx.createBufferSource(); + const gain = ctx.createGain(); + src.buffer = buf; + src.connect(gain); + gain.connect(ctx.destination); + gain.gain.value = Math.max(0, Math.min(1, vol)); + src.start(); + } catch(e){ console.warn('playAlert failed', e); } + } + + // migration ancien format (sans soundId) + let alertConfigs = JSON.parse(localStorage.getItem('alertConfigs') || '[]').map(c => ({ + soundId: 'levelup', ...c + })); function saveAlertConfigs(){ localStorage.setItem('alertConfigs', JSON.stringify(alertConfigs)); } - let _alertAudio = null; - function playAlertPing(){ - try { - if (!_alertAudio) _alertAudio = new Audio('levelup.ogg'); - _alertAudio.currentTime = 0; - _alertAudio.play().catch(e => console.warn('Alert ping failed', e)); - } catch(e){ console.warn('Alert ping failed', e); } - } + // Pre-cache levelup.ogg au démarrage (son par défaut) + _getDecodedBuffer('levelup').catch(() => {}); // activeTimers : Map const activeTimers = new Map(); @@ -2307,6 +2461,7 @@ populateTimerMobSuggestions(); renderAlertConfigList(); populateAlertItemSuggestions(); + populateAddSoundSelect(); startTimerClock(); } @@ -2336,16 +2491,35 @@ populateTimerMobSuggestions(); }); - function renderAlertConfigList(){ + // ---- Alert config list ---- + function buildSoundOptions(selectedId){ + return SOUND_CATALOG.map(s => + '' + ).join(''); + } + + async function renderAlertConfigList(){ const list = $('alertConfigList'); if (!list) return; if (!alertConfigs.length){ list.innerHTML = ''; return; } - list.innerHTML = alertConfigs.map(cfg => - '
'+ - ''+escHtml(cfg.itemName)+''+ - ''+ - '
' - ).join(''); + const cachedIds = new Set(); + for (const s of SOUND_CATALOG){ + if (await isSoundCached(s.id)) cachedIds.add(s.id); + } + list.innerHTML = alertConfigs.map(cfg => { + const sid = cfg.soundId || 'levelup'; + const opts = SOUND_CATALOG.map(s => { + const notCached = !cachedIds.has(s.id) && s.id !== 'levelup'; + const label = s.label + (notCached ? ' (⬇)' : ''); + return ''; + }).join(''); + return '
'+ + ''+escHtml(cfg.itemName)+''+ + ''+ + ''+ + ''+ + '
'; + }).join(''); } function populateAlertItemSuggestions(){ @@ -2370,25 +2544,129 @@ .join(''); } + function populateAddSoundSelect(){ + const sel = $('alertSoundSelect'); + if (!sel) return; + sel.innerHTML = buildSoundOptions('levelup'); + } + $('alertAddBtn').addEventListener('click', () => { const itemName = $('alertItemInput').value.trim(); if (!itemName) return; - alertConfigs.push({ id: generateUUID(), itemName, nameLower: itemName.toLowerCase() }); + const soundId = $('alertSoundSelect').value || 'levelup'; + alertConfigs.push({ id: generateUUID(), itemName, nameLower: itemName.toLowerCase(), soundId }); saveAlertConfigs(); $('alertItemInput').value = ''; renderAlertConfigList(); populateAlertItemSuggestions(); + // auto-download si son pas encore en cache + if (soundId !== 'levelup') isSoundCached(soundId).then(ok => { if (!ok) downloadSound(soundId).catch(()=>{}); }); }); $('alertItemInput').addEventListener('keydown', e => { if (e.key === 'Enter') $('alertAddBtn').click(); }); - $('alertConfigList').addEventListener('click', e => { - const id = e.target.dataset.delAlert; + $('alertConfigList').addEventListener('click', async e => { + const delId = e.target.dataset.delAlert; + if (delId){ + alertConfigs = alertConfigs.filter(c => c.id !== delId); + saveAlertConfigs(); + await renderAlertConfigList(); + populateAlertItemSuggestions(); + return; + } + const prevId = e.target.dataset.previewAlert; + if (prevId){ + const cfg = alertConfigs.find(c => c.id === prevId); + if (cfg){ + const soundId = cfg.soundId || 'levelup'; + const cached = await isSoundCached(soundId) || soundId === 'levelup'; + if (!cached){ + e.target.textContent = '⬇'; + await downloadSound(soundId).catch(err => console.warn('Download failed', err)); + await renderAlertConfigList(); + renderSoundLibrary(); + } + playAlert(cfg); + } + } + }); + + $('alertConfigList').addEventListener('change', e => { + const id = e.target.dataset.alertSound; if (!id) return; - alertConfigs = alertConfigs.filter(c => c.id !== id); + const cfg = alertConfigs.find(c => c.id === id); + if (!cfg) return; + cfg.soundId = e.target.value; saveAlertConfigs(); - renderAlertConfigList(); - populateAlertItemSuggestions(); + if (cfg.soundId !== 'levelup') isSoundCached(cfg.soundId).then(ok => { if (!ok) downloadSound(cfg.soundId).catch(()=>{}); }); + }); + + // ---- Volume global ---- + const _volSlider = $('alertGlobalVolume'); + const _volLabel = $('alertGlobalVolumeVal'); + const _savedVol = localStorage.getItem('alertGlobalVolume') || '25'; + if (_volSlider){ _volSlider.value = _savedVol; _volLabel.textContent = _savedVol + '%'; } + _volSlider && _volSlider.addEventListener('input', () => { + localStorage.setItem('alertGlobalVolume', _volSlider.value); + _volLabel.textContent = _volSlider.value + '%'; + }); + + // ---- Sound library ---- + $('soundLibToggle').addEventListener('click', () => { + const content = $('soundLibContent'); + const arrow = $('soundLibArrow'); + const open = content.hidden; + content.hidden = !open; + arrow.textContent = open ? '▼ fermer' : '▶ ouvrir'; + if (open) renderSoundLibrary(); + }); + + async function renderSoundLibrary(){ + const grid = $('soundLibGrid'); + if (!grid) return; + const cards = await Promise.all(SOUND_CATALOG.map(async s => { + const cached = await isSoundCached(s.id) || s.id === 'levelup'; + return '
'+ + ''+escHtml(s.label)+''+ + ''+escHtml(s.desc)+''+ + '
'+ + ''+ + (cached + ? '✓ Téléchargé' + : '')+ + '
'+ + '
'; + })); + grid.innerHTML = cards.join(''); + } + + $('soundLibGrid').addEventListener('click', async e => { + const dlId = e.target.dataset.dlSound; + if (dlId){ + e.target.textContent = '…'; + e.target.disabled = true; + try { + await downloadSound(dlId); + await renderSoundLibrary(); + await renderAlertConfigList(); + populateAddSoundSelect(); + } catch(err){ + e.target.textContent = '✗ Erreur'; + console.warn('Sound download failed', err); + } + return; + } + const prevId = e.target.dataset.previewSound; + if (prevId){ + const cached = await isSoundCached(prevId) || prevId === 'levelup'; + if (!cached){ + e.target.textContent = '⬇…'; + await downloadSound(prevId).catch(err => console.warn('dl failed', err)); + await renderSoundLibrary(); + await renderAlertConfigList(); + } + playAlert({ soundId: prevId }); + } }); updateTimerTileStatus();