95 lines
7.8 KiB
JavaScript
95 lines
7.8 KiB
JavaScript
/* 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>♫ 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();
|
||
})();
|