|
|
|
|
@@ -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 @@
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
<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>
|
|
|
|
|
<div class="timer-add-row">
|
|
|
|
|
<input type="text" id="alertItemInput" placeholder="Nom de l'item (ex: Diamond)" style="flex:2;" list="alertItemSuggestions" autocomplete="off">
|
|
|
|
|
<div class="timer-add-row" style="flex-wrap:wrap;gap:6px;">
|
|
|
|
|
<input type="text" id="alertItemInput" placeholder="Nom de l'item (ex: Diamond)" style="flex:2;min-width:140px;" list="alertItemSuggestions" autocomplete="off">
|
|
|
|
|
<datalist id="alertItemSuggestions"></datalist>
|
|
|
|
|
<select id="alertSoundSelect" class="alert-cfg-sound"></select>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:5px;">
|
|
|
|
|
<span style="font-size:10px;color:var(--text-dim);">Vol</span>
|
|
|
|
|
<input type="range" id="alertVolumeInput" min="0" max="100" step="1" value="10" style="width:64px;">
|
|
|
|
|
<span id="alertVolumeVal" style="font-size:10px;font-family:var(--font-mono);color:var(--text-dim);min-width:26px;">10%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-ghost" id="alertAddBtn">Ajouter</button>
|
|
|
|
|
</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>
|
|
|
|
|
<div id="alertConfigList" class="timer-config-list"></div>
|
|
|
|
|
<p style="font-size:11.5px;color:var(--text-dim);margin:6px 0 4px;">Chaque alerte a son propre son et volume.</p>
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
@@ -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,21 +2218,127 @@
|
|
|
|
|
// ======================================================================
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function playAlert(cfg){
|
|
|
|
|
try {
|
|
|
|
|
const soundId = cfg ? (cfg.soundId || 'levelup') : 'levelup';
|
|
|
|
|
const vol = Math.max(0, Math.min(1, ((cfg && cfg.volume != null) ? cfg.volume : 10) / 100));
|
|
|
|
|
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 = vol;
|
|
|
|
|
src.start();
|
|
|
|
|
} catch(e){ console.warn('playAlert failed', e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// migration : ajoute soundId et volume si absents
|
|
|
|
|
let alertConfigs = JSON.parse(localStorage.getItem('alertConfigs') || '[]').map(c => ({
|
|
|
|
|
soundId: 'levelup', volume: 10, ...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); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// activeTimers : Map<configId, { endsAt: number, durationSec: number, mobName: string }>
|
|
|
|
|
const activeTimers = new Map();
|
|
|
|
|
let timerClockId = null;
|
|
|
|
|
@@ -2307,6 +2454,7 @@
|
|
|
|
|
populateTimerMobSuggestions();
|
|
|
|
|
renderAlertConfigList();
|
|
|
|
|
populateAlertItemSuggestions();
|
|
|
|
|
populateAddSoundSelect();
|
|
|
|
|
startTimerClock();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2336,16 +2484,38 @@
|
|
|
|
|
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');
|
|
|
|
|
if (!list) return;
|
|
|
|
|
if (!alertConfigs.length){ list.innerHTML = ''; return; }
|
|
|
|
|
list.innerHTML = alertConfigs.map(cfg =>
|
|
|
|
|
'<div class="timer-config-row">'+
|
|
|
|
|
'<span class="timer-config-mob">'+escHtml(cfg.itemName)+'</span>'+
|
|
|
|
|
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 '<option value="'+escHtml(s.id)+'"'+(s.id === sid ? ' selected' : '')+'>'+escHtml(label)+'</option>';
|
|
|
|
|
}).join('');
|
|
|
|
|
const vol = cfg.volume != null ? cfg.volume : 10;
|
|
|
|
|
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>'+
|
|
|
|
|
'<input type="range" min="0" max="100" step="1" value="'+vol+'" data-alert-vol="'+escHtml(cfg.id)+'" style="width:64px;" title="Volume: '+vol+'%">'+
|
|
|
|
|
'<span data-alert-vol-label="'+escHtml(cfg.id)+'" style="font-size:10px;font-family:var(--font-mono);color:var(--text-dim);min-width:26px;">'+vol+'%</span>'+
|
|
|
|
|
'<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>'+
|
|
|
|
|
'</div>'
|
|
|
|
|
).join('');
|
|
|
|
|
'</div>';
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateAlertItemSuggestions(){
|
|
|
|
|
@@ -2370,25 +2540,137 @@
|
|
|
|
|
.join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateAddSoundSelect(){
|
|
|
|
|
const sel = $('alertSoundSelect');
|
|
|
|
|
if (!sel) return;
|
|
|
|
|
sel.innerHTML = buildSoundOptions('levelup');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// slider volume dans le formulaire d'ajout
|
|
|
|
|
$('alertVolumeInput').addEventListener('input', () => {
|
|
|
|
|
$('alertVolumeVal').textContent = $('alertVolumeInput').value + '%';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('alertAddBtn').addEventListener('click', () => {
|
|
|
|
|
const itemName = $('alertItemInput').value.trim();
|
|
|
|
|
if (!itemName) return;
|
|
|
|
|
alertConfigs.push({ id: generateUUID(), itemName, nameLower: itemName.toLowerCase() });
|
|
|
|
|
const soundId = $('alertSoundSelect').value || 'levelup';
|
|
|
|
|
const volume = parseInt($('alertVolumeInput').value, 10);
|
|
|
|
|
alertConfigs.push({ id: generateUUID(), itemName, nameLower: itemName.toLowerCase(), soundId, volume });
|
|
|
|
|
saveAlertConfigs();
|
|
|
|
|
$('alertItemInput').value = '';
|
|
|
|
|
renderAlertConfigList();
|
|
|
|
|
populateAlertItemSuggestions();
|
|
|
|
|
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;
|
|
|
|
|
if (!id) return;
|
|
|
|
|
alertConfigs = alertConfigs.filter(c => c.id !== id);
|
|
|
|
|
$('alertConfigList').addEventListener('click', async e => {
|
|
|
|
|
const delId = e.target.dataset.delAlert;
|
|
|
|
|
if (delId){
|
|
|
|
|
alertConfigs = alertConfigs.filter(c => c.id !== delId);
|
|
|
|
|
saveAlertConfigs();
|
|
|
|
|
renderAlertConfigList();
|
|
|
|
|
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 soundId = e.target.dataset.alertSound;
|
|
|
|
|
if (soundId){
|
|
|
|
|
const cfg = alertConfigs.find(c => c.id === soundId);
|
|
|
|
|
if (!cfg) return;
|
|
|
|
|
cfg.soundId = e.target.value;
|
|
|
|
|
saveAlertConfigs();
|
|
|
|
|
if (cfg.soundId !== 'levelup') isSoundCached(cfg.soundId).then(ok => { if (!ok) downloadSound(cfg.soundId).catch(()=>{}); });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('alertConfigList').addEventListener('input', e => {
|
|
|
|
|
const id = e.target.dataset.alertVol;
|
|
|
|
|
if (!id) return;
|
|
|
|
|
const cfg = alertConfigs.find(c => c.id === id);
|
|
|
|
|
if (!cfg) return;
|
|
|
|
|
cfg.volume = parseInt(e.target.value, 10);
|
|
|
|
|
saveAlertConfigs();
|
|
|
|
|
const label = document.querySelector('[data-alert-vol-label="'+id+'"]');
|
|
|
|
|
if (label) label.textContent = cfg.volume + '%';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- 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();
|
|
|
|
|
|