This commit is contained in:
pporcheret
2026-10-30 22:10:03 +01:00
parent 7f26f9b015
commit 90ffa635a0
161 changed files with 19042 additions and 111 deletions
+94
View File
@@ -0,0 +1,94 @@
/* 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();
})();