feat: système de sons Minecraft configurable par alerte + volume global
- Volume global réglable (slider, défaut 25%) persisté dans localStorage - Catalogue de 10 sons Minecraft officiels (Level Up, XP Orb, Item Pop, Pling, Bell, Toast, Challenge, Coffre, Hit, Click) - Sons téléchargés à la demande depuis le CDN Mojang et cachés en IndexedDB - Chaque alerte de drop a son propre son sélectionnable via dropdown - Bouton ▶ preview par alerte et dans la bibliothèque (auto-download si pas en cache) - Bibliothèque de sons repliable : cards avec preview + statut téléchargé - Lecture via Web Audio API (AudioContext + GainNode) pour contrôle précis du volume - Migration automatique des anciennes alertConfigs sans soundId (défaut: levelup) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -326,6 +326,28 @@
|
|||||||
.timer-card-bar{height:3px;background:var(--border);margin-top:10px;overflow:hidden;}
|
.timer-card-bar{height:3px;background:var(--border);margin-top:10px;overflow:hidden;}
|
||||||
.timer-card-bar-fill{height:100%;transition:width .9s linear;}
|
.timer-card-bar-fill{height:100%;transition:width .9s linear;}
|
||||||
.timer-empty-msg{color:var(--text-dim);font-size:13px;padding:20px 0;}
|
.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);
|
#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);
|
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);}
|
pointer-events:none;display:none;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.5);}
|
||||||
@@ -725,15 +747,34 @@
|
|||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-head-row">
|
<div class="panel-head-row">
|
||||||
<h2>Alertes de drop <span class="muted">— son au drop d'un item</span></h2>
|
<h2>Alertes de drop</h2>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;margin-left:auto;">
|
||||||
|
<span style="font-size:11px;color:var(--text-dim);">Volume</span>
|
||||||
|
<input type="range" id="alertGlobalVolume" min="0" max="100" step="5" value="25" style="width:80px;">
|
||||||
|
<span id="alertGlobalVolumeVal" style="font-size:10px;font-family:var(--font-mono);color:var(--text-dim);min-width:28px;text-align:right;">25%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="timer-add-row">
|
<div class="timer-add-row">
|
||||||
<input type="text" id="alertItemInput" placeholder="Nom de l'item (ex: Diamond)" style="flex:2;" list="alertItemSuggestions" autocomplete="off">
|
<input type="text" id="alertItemInput" placeholder="Nom de l'item (ex: Diamond)" style="flex:2;" list="alertItemSuggestions" autocomplete="off">
|
||||||
<datalist id="alertItemSuggestions"></datalist>
|
<datalist id="alertItemSuggestions"></datalist>
|
||||||
|
<select id="alertSoundSelect" class="alert-cfg-sound"></select>
|
||||||
<button class="btn-ghost" id="alertAddBtn">Ajouter</button>
|
<button class="btn-ghost" id="alertAddBtn">Ajouter</button>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size:11.5px;color:var(--text-dim);margin:8px 0 4px;">Un ping discret est joué à chaque fois que cet item drop.</p>
|
<p style="font-size:11.5px;color:var(--text-dim);margin:6px 0 4px;">Son joué à chaque fois que cet item drop. Volume global s'applique à toutes les alertes.</p>
|
||||||
<div id="alertConfigList" class="timer-config-list"></div>
|
<div id="alertConfigList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head-row" id="soundLibToggle" style="cursor:pointer;user-select:none;">
|
||||||
|
<h2>Bibliothèque de sons Minecraft</h2>
|
||||||
|
<span id="soundLibArrow" style="margin-left:auto;font-size:11px;color:var(--text-dim);">▶ ouvrir</span>
|
||||||
|
</div>
|
||||||
|
<div id="soundLibContent" hidden>
|
||||||
|
<p style="font-size:11.5px;color:var(--text-dim);margin:0 0 8px;">
|
||||||
|
Sons téléchargés depuis le CDN officiel Mojang. Clic ▶ pour prévisualiser, ⬇ pour télécharger.
|
||||||
|
</p>
|
||||||
|
<div class="sound-lib-grid" id="soundLibGrid"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
@@ -1392,7 +1433,7 @@
|
|||||||
if (alertConfigs.length){
|
if (alertConfigs.length){
|
||||||
const pn = mcPlain(it.name || '').toLowerCase();
|
const pn = mcPlain(it.name || '').toLowerCase();
|
||||||
for (const alert of alertConfigs){
|
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 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(){
|
function saveAlertConfigs(){
|
||||||
localStorage.setItem('alertConfigs', JSON.stringify(alertConfigs));
|
localStorage.setItem('alertConfigs', JSON.stringify(alertConfigs));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _alertAudio = null;
|
// Pre-cache levelup.ogg au démarrage (son par défaut)
|
||||||
function playAlertPing(){
|
_getDecodedBuffer('levelup').catch(() => {});
|
||||||
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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// activeTimers : Map<configId, { endsAt: number, durationSec: number, mobName: string }>
|
// activeTimers : Map<configId, { endsAt: number, durationSec: number, mobName: string }>
|
||||||
const activeTimers = new Map();
|
const activeTimers = new Map();
|
||||||
@@ -2307,6 +2461,7 @@
|
|||||||
populateTimerMobSuggestions();
|
populateTimerMobSuggestions();
|
||||||
renderAlertConfigList();
|
renderAlertConfigList();
|
||||||
populateAlertItemSuggestions();
|
populateAlertItemSuggestions();
|
||||||
|
populateAddSoundSelect();
|
||||||
startTimerClock();
|
startTimerClock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2336,16 +2491,35 @@
|
|||||||
populateTimerMobSuggestions();
|
populateTimerMobSuggestions();
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderAlertConfigList(){
|
// ---- Alert config list ----
|
||||||
|
function buildSoundOptions(selectedId){
|
||||||
|
return SOUND_CATALOG.map(s =>
|
||||||
|
'<option value="'+escHtml(s.id)+'"'+(s.id === selectedId ? ' selected' : '')+'>'+escHtml(s.label)+'</option>'
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderAlertConfigList(){
|
||||||
const list = $('alertConfigList');
|
const list = $('alertConfigList');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
if (!alertConfigs.length){ list.innerHTML = ''; return; }
|
if (!alertConfigs.length){ list.innerHTML = ''; return; }
|
||||||
list.innerHTML = alertConfigs.map(cfg =>
|
const cachedIds = new Set();
|
||||||
'<div class="timer-config-row">'+
|
for (const s of SOUND_CATALOG){
|
||||||
'<span class="timer-config-mob">'+escHtml(cfg.itemName)+'</span>'+
|
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 '<option value="'+escHtml(s.id)+'"'+(s.id === sid ? ' selected' : '')+'>'+escHtml(label)+'</option>';
|
||||||
|
}).join('');
|
||||||
|
return '<div class="alert-cfg-row">'+
|
||||||
|
'<span class="alert-cfg-name">'+escHtml(cfg.itemName)+'</span>'+
|
||||||
|
'<select class="alert-cfg-sound" data-alert-sound="'+escHtml(cfg.id)+'">'+opts+'</select>'+
|
||||||
|
'<button class="btn-ghost small" data-preview-alert="'+escHtml(cfg.id)+'" title="Prévisualiser">▶</button>'+
|
||||||
'<button class="btn-ghost small danger" data-del-alert="'+escHtml(cfg.id)+'">✕</button>'+
|
'<button class="btn-ghost small danger" data-del-alert="'+escHtml(cfg.id)+'">✕</button>'+
|
||||||
'</div>'
|
'</div>';
|
||||||
).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateAlertItemSuggestions(){
|
function populateAlertItemSuggestions(){
|
||||||
@@ -2370,25 +2544,129 @@
|
|||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateAddSoundSelect(){
|
||||||
|
const sel = $('alertSoundSelect');
|
||||||
|
if (!sel) return;
|
||||||
|
sel.innerHTML = buildSoundOptions('levelup');
|
||||||
|
}
|
||||||
|
|
||||||
$('alertAddBtn').addEventListener('click', () => {
|
$('alertAddBtn').addEventListener('click', () => {
|
||||||
const itemName = $('alertItemInput').value.trim();
|
const itemName = $('alertItemInput').value.trim();
|
||||||
if (!itemName) return;
|
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();
|
saveAlertConfigs();
|
||||||
$('alertItemInput').value = '';
|
$('alertItemInput').value = '';
|
||||||
renderAlertConfigList();
|
renderAlertConfigList();
|
||||||
populateAlertItemSuggestions();
|
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(); });
|
$('alertItemInput').addEventListener('keydown', e => { if (e.key === 'Enter') $('alertAddBtn').click(); });
|
||||||
|
|
||||||
$('alertConfigList').addEventListener('click', e => {
|
$('alertConfigList').addEventListener('click', async e => {
|
||||||
const id = e.target.dataset.delAlert;
|
const delId = e.target.dataset.delAlert;
|
||||||
if (!id) return;
|
if (delId){
|
||||||
alertConfigs = alertConfigs.filter(c => c.id !== id);
|
alertConfigs = alertConfigs.filter(c => c.id !== delId);
|
||||||
saveAlertConfigs();
|
saveAlertConfigs();
|
||||||
renderAlertConfigList();
|
await renderAlertConfigList();
|
||||||
populateAlertItemSuggestions();
|
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;
|
||||||
|
const cfg = alertConfigs.find(c => c.id === id);
|
||||||
|
if (!cfg) return;
|
||||||
|
cfg.soundId = e.target.value;
|
||||||
|
saveAlertConfigs();
|
||||||
|
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 '<div class="sound-card" data-sound-id="'+escHtml(s.id)+'">'+
|
||||||
|
'<span class="sound-card-name">'+escHtml(s.label)+'</span>'+
|
||||||
|
'<span class="sound-card-desc">'+escHtml(s.desc)+'</span>'+
|
||||||
|
'<div class="sound-card-actions">'+
|
||||||
|
'<button class="btn-ghost small" data-preview-sound="'+escHtml(s.id)+'" title="Prévisualiser">▶ Preview</button>'+
|
||||||
|
(cached
|
||||||
|
? '<span class="sound-card-status">✓ Téléchargé</span>'
|
||||||
|
: '<button class="btn-ghost small" data-dl-sound="'+escHtml(s.id)+'">⬇ Télécharger</button>')+
|
||||||
|
'</div>'+
|
||||||
|
'</div>';
|
||||||
|
}));
|
||||||
|
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();
|
updateTimerTileStatus();
|
||||||
|
|||||||
Reference in New Issue
Block a user