Files
2026-10-30 22:10:03 +01:00

95 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Dona V6: Spotify Connect — live player, REST-only polling; voice/text keep Hermes. */
(() => {
'use strict';
const $ = id => document.getElementById(id);
const state = { data: null, expanded: false, loaded: false, busy: false, timestamp: 0 };
const icons = {play:'▶',pause:'Ⅱ',prev:'⏮',next:'⏭',music:'♫'};
function node(tag,cls,txt){const e=document.createElement(tag); if(cls)e.className=cls;if(txt)e.textContent=txt;return e;}
function fmt(ms){const seconds=Math.floor(Math.max(0,ms)/1000);return Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0');}
function progress(){
const d=state.data;if(!d||!d.available)return 0;
return Math.min(d.duration_ms||0, Math.max(0,(d.progress_ms||0)+(d.is_playing?Date.now()-state.timestamp:0)));
}
function init(){
const main=$('alfredMainV1');if(!main){setTimeout(init,180);return;}
if($('donaSpotifyPanel'))return;
const head=main.querySelector('.av-head-actions');
if(head){const open=node('button','av-round dona-music-open',icons.music);open.type='button';open.title='Lecteur Spotify';open.setAttribute('aria-label','Ouvrir le lecteur Spotify');open.id='donaSpotifyOpen';head.prepend(open);open.addEventListener('click',()=>toggle(true));}
const mini=node('button','dona-spotify-mini');mini.id='donaSpotifyMini';mini.type='button';mini.hidden=true;mini.innerHTML='<span class="dona-mini-note">♫</span><span id="donaMiniTitle">Spotify</span><span id="donaMiniArtist"></span><span class="dona-mini-arrow">⌃</span>';
mini.addEventListener('click',()=>toggle(true));document.body.append(mini);
const overlay=node('div','dona-spotify-overlay');overlay.id='donaSpotifyOverlay';overlay.hidden=true;
overlay.innerHTML=`<section id="donaSpotifyPanel" role="dialog" aria-modal="true" aria-label="Lecteur Spotify" class="dona-spotify-panel">
<div class="dona-spotify-head"><span>♫ &nbsp;SPOTIFY CONNECT</span><button type="button" id="donaSpotifyClose" aria-label="Fermer le lecteur">×</button></div>
<div class="dona-spotify-art"><img id="donaSpotifyCover" alt="Pochette de l’album" hidden><span id="donaSpotifyFallback">♫</span></div>
<div class="dona-spotify-track"><strong id="donaSpotifyTitle">Aucune musique en cours</strong><span id="donaSpotifyArtist">Lance un morceau sur un appareil Spotify.</span><small id="donaSpotifyAlbum"></small></div>
<div class="dona-spotify-progress"><input id="donaSpotifySeek" type="range" min="0" max="100" value="0" step="1000" aria-label="Avancer dans le morceau"><div class="dona-spotify-timing"><span id="donaSpotifyElapsed">0:00</span><span id="donaSpotifyDuration">0:00</span></div></div>
<div class="dona-spotify-actions"><button type="button" data-saction="previous" aria-label="Morceau précédent">⏮</button><button type="button" data-saction="toggle" id="donaSpotifyPause" class="dona-spotify-primary" aria-label="Lecture ou pause">▶</button><button type="button" data-saction="next" aria-label="Morceau suivant">⏭</button></div>
<div class="dona-spotify-settings"><label for="donaSpotifyDevice">Sortie audio</label><select id="donaSpotifyDevice" aria-label="Appareil Spotify"></select><label for="donaSpotifyVolume">Volume <span id="donaSpotifyVolumeNum">—</span></label><input type="range" min="0" max="100" id="donaSpotifyVolume" aria-label="Volume Spotify"></div>
<p class="dona-spotify-hint">Tu peux aussi dire « Dona, mets en pause », « morceau suivant » ou écrire ta demande dans le champ principal.</p>
<p id="donaSpotifyMessage" class="dona-spotify-message" role="status" aria-live="polite"></p>
</section>`;
overlay.addEventListener('click',e=>{if(e.target===overlay)toggle(false);});document.body.append(overlay);
$('donaSpotifyClose').addEventListener('click',()=>toggle(false));
overlay.querySelectorAll('[data-saction]').forEach(b=>b.addEventListener('click',()=>{
const action=b.dataset.saction==='toggle'?(state.data?.is_playing?'pause':'play'):b.dataset.saction;
send(action);
}));
$('donaSpotifySeek').addEventListener('change',e=>send('seek',{position_ms:Number(e.target.value)}));
$('donaSpotifyVolume').addEventListener('change',e=>send('volume',{volume_percent:Number(e.target.value)}));
$('donaSpotifyDevice').addEventListener('change',e=>{if(e.target.value)send('transfer',{device_id:e.target.value});});
window.addEventListener('keydown',e=>{if(e.key==='Escape'&&state.expanded)toggle(false);});
setInterval(updateBar,1000);
setInterval(()=>{if(!document.hidden)load();},12000);
document.addEventListener('visibilitychange',()=>{if(!document.hidden)load();});
load();
}
function toggle(flag){state.expanded=flag;const overlay=$('donaSpotifyOverlay');overlay.hidden=!flag;document.body.classList.toggle('dona-spotify-open',flag);if(flag)load();}
function message(txt){const m=$('donaSpotifyMessage');if(m)m.textContent=txt;}
function updateBar(){
const d=state.data;if(!d)return;
$('donaSpotifyElapsed').textContent=fmt(progress());$('donaSpotifyDuration').textContent=fmt(d.duration_ms||0);
const seek=$('donaSpotifySeek');if(document.activeElement!==seek){seek.max=Math.max(1,d.duration_ms||1);seek.value=Math.min(seek.max,progress());seek.disabled=!d.available;}
}
function redraw(){
const d=state.data;if(!d)return;
const active=Boolean(d.available);
$('donaSpotifyMini').hidden=!active;
$('donaMiniTitle').textContent=d.title||'Spotify';$('donaMiniArtist').textContent=d.artist||'';
$('donaSpotifyTitle').textContent=d.title||(d.devices?.length?'Aucune lecture active':'Aucun appareil connecté');
$('donaSpotifyArtist').textContent=d.artist||(d.devices?.length?'Choisis un appareil et lance un morceau.':'Ouvre Spotify sur un appareil.');
$('donaSpotifyAlbum').textContent=d.album||'';
const cover=$('donaSpotifyCover');cover.hidden=!d.cover;if(d.cover)cover.src=d.cover;else cover.removeAttribute('src');
$('donaSpotifyFallback').hidden=!!d.cover;
$('donaSpotifyPause').textContent=d.is_playing?icons.pause:icons.play;
$('donaSpotifyPause').setAttribute('aria-label',d.is_playing?'Mettre en pause':'Reprendre la lecture');
const select=$('donaSpotifyDevice');const chosen=select.value;
select.replaceChildren();
const option=node('option','',d.device_name||'Choisir un appareil');option.value='';select.append(option);
(d.devices||[]).forEach(x=>{const o=node('option','',x.name||x.id);o.value=x.id;select.append(o);});
select.value=(d.devices||[]).some(x=>x.id===d.device_id)?d.device_id:(chosen&&[...select.options].some(x=>x.value===chosen)?chosen:'');
select.disabled=!d.devices?.length;
const volume=$('donaSpotifyVolume');if(document.activeElement!==volume && Number.isFinite(d.volume_percent))volume.value=d.volume_percent;
$('donaSpotifyVolumeNum').textContent=Number.isFinite(d.volume_percent)?d.volume_percent+' %':'—';
updateBar();
}
async function load(){
if(state.busy)return;
state.busy=true;
try{
const r=await fetch('/api/dona/spotify/state',{cache:'no-store',credentials:'same-origin'});
const d=await r.json();if(!r.ok||!d.ok)throw Error(d.error||'Spotify non disponible');
state.data=d;state.timestamp=Date.now();state.loaded=true;redraw();message('');
}catch(e){if(state.expanded||!state.loaded)message(e.message||'Spotify indisponible');}
finally{state.busy=false;}
}
async function send(action,extra={}){
message('Commande en cours…');
try{
const r=await fetch('/api/dona/spotify/control',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({action,...extra})});
const data=await r.json();if(!r.ok||!data.ok)throw Error(data.error||'Commande refusée');
message('');await new Promise(resolve=>setTimeout(resolve,450));await load();
}catch(e){message(e.message||'Commande indisponible');}
}
init();
})();