Repository navigation
Expand file tree
/
Copy pathclient.mjs
More file actions
80 lines (77 loc) · 19.5 KB
/
Copy pathclient.mjs
File metadata and controls
80 lines (77 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import {installTwoFinger} from './two-finger.mjs';
import {createPlaybackSync} from './playback-sync.mjs';
import {setButtonIcon} from './icons.mjs';import {createDeskThing} from '@deskthing/client';import {createVisuals,modes} from './visuals.mjs';import {createLyricOverlay} from './lyric-overlay.mjs';import {lyricIndex,playbackPosition} from './lyrics-sync.mjs';
const api=createDeskThing(),$=id=>document.getElementById(id),visuals=createVisuals($('scope'),$('gpu'));let tab='visualizer',mode=0,visualFull=false,menuOpen=false,capturing=false,playing=null,sequence=0,visible=true,overlay=false,audioWanted=true,videoId=null,transportPending=false,transportTimer=null,requests=new Map(),runSpeed=1;
api.overrideKeys(['wheel']);
for(const [id,icon]of [['previousTrack','previous'],['togglePlayback','play'],['nextTrack','next'],['menuToggle','more'],['fullscreen','expand'],['restoreControls','collapse']])setButtonIcon($(id),icon);
const playbackSync=createPlaybackSync({api,visuals});
createLyricOverlay({api,visuals,root:$('visualizer').querySelector('.canvas-wrap'),active:()=>tab==='visualizer'&&visible&&!document.hidden&&!overlay});
function replace(root,...items){root.textContent='';root.append(...items)}
const tabs=[['visualizer','Visuals'],['lyrics','Lyrics'],['queue','Up Next'],['recent','Recent'],['devices','Devices']];
const node=(tag,text,cls)=>{const n=document.createElement(tag);if(text!=null)n.textContent=text;if(cls)n.className=cls;return n};
function request(section,value){const id=++sequence;requests.set(section,id);api.send({type:'music-request',payload:{section,value,id}});if(section!=='playing')$('status').textContent='Loading…';}
function capture(on){capturing=on;visuals.setEnabled(on);api.send({type:'visualizer',payload:{active:on}});$('capture').textContent=on?'Pause visuals':'Resume visuals';$('capture').classList.toggle('active',on);if(!on)$('captureStatus').textContent='Audio capture stopped.';}
function setMenu(open){if(open){$('effectPicker').hidden=false;$('visualAdjust').hidden=true}menuOpen=open;visuals.setInputBlocked?.(open);$('musicMenu').hidden=!open;$('menuToggle').setAttribute('aria-expanded',String(open))}
$('openAdjust').onclick=()=>{$('skylineSensitivity').parentElement.hidden=mode>=6;$('backdropReactionLabel').textContent='EQ skyline';$('skylineSensitivity').setAttribute('aria-label',mode===6?'Frequency wave sensitivity':'EQ skyline sensitivity');$('motionSpeedLabel').textContent=mode===7?'DJ pace':mode===6?'Combat pace':'Running speed';$('runnerSpeed').setAttribute('aria-label',mode===7?'DJ pace':mode===6?'Combat pace':'Running speed');$('runnerAdjust').hidden=(mode<5||mode>7);$('generalAdjust').hidden=(mode>=5&&mode<=7);$('effectPicker').hidden=true;$('visualAdjust').hidden=false};$('closeAdjust').onclick=()=>{$('effectPicker').hidden=false;$('visualAdjust').hidden=true};
$('menuToggle').onclick=()=>setMenu(!menuOpen);$('closeMenu').onclick=()=>setMenu(false);$('musicMenu').onclick=e=>{if(e.target===$('musicMenu'))setMenu(false)};
let lyricLines=[],lyricTrack=null,activeLyric=-2,followLyrics=true,playingReceivedAt=Date.now(),lastPlayingPoll=0;
function followLive(){followLyrics=true;$('followLyrics').hidden=true;activeLyric=-2;tickLyrics()}
function manualLyrics(){if(tab==='lyrics'&&lyricLines.length){followLyrics=false;$('followLyrics').hidden=false}}
function tickLyrics(){if(tab!=='lyrics'||!visible||document.hidden||overlay||!lyricLines.length||lyricTrack!==playing?.track?.id)return;const index=lyricIndex(lyricLines,playbackPosition(playing,playingReceivedAt,Date.now()));if(index===activeLyric)return;activeLyric=index;Array.from($('lyricText').children).forEach((line,i)=>{line.classList.toggle('current',i===index);line.setAttribute('aria-current',i===index?'true':'false')});const current=$('lyricText').children[index];if(current&&followLyrics){const section=$('lyrics'),top=Math.max(0,current.offsetTop-section.clientHeight*.42+current.offsetHeight/2);section.scrollTo({top,behavior:'smooth'})}}
function renderLyrics(data){if(data.trackId&&playing?.track?.id&&data.trackId!==playing.track.id){request('lyrics');return}lyricTrack=data.trackId||playing?.track?.id;lyricLines=Array.isArray(data.lines)?data.lines:[];activeLyric=-2;followLyrics=true;$('followLyrics').hidden=true;if(lyricLines.length){replace($('lyricText'),...lyricLines.map(line=>node('p',line.text||'♪','lyric-line')));$('lyricMode').textContent='Live lyrics · '+data.source}else{$('lyricText').textContent=data.text;$('lyricMode').textContent='Plain lyrics · '+data.source}$('status').textContent='';tickLyrics()}
$('followLyrics').onclick=followLive;$('lyrics').addEventListener('touchstart',e=>{if(!e.target.closest('button,[data-character-picker]'))manualLyrics()},{passive:true});
function show(id){setMenu(false);if(id!=='visualizer')setFull(false);tab=id;capture(id==='visualizer'&&audioWanted&&visible&&!document.hidden&&!overlay);for(const [key]of tabs)$(key).hidden=id!==key;for(const b of $('tabs').children){b.classList.toggle('active',b.dataset.tab===id);b.setAttribute('aria-selected',String(b.dataset.tab===id))}if(['queue','recent','devices','lyrics'].includes(id))request(id);if(id==='lyrics'){request('playing');activeLyric=-2;tickLyrics()}if(id==='video')request('playing');$('status').textContent='';}
for(const [id,label]of tabs){const b=node('button',label);b.dataset.tab=id;b.setAttribute('role','tab');b.onclick=()=>show(id);$('tabs').append(b)}
function setMode(i){mode=(i+modes.length)%modes.length;visuals.setMode(mode);try{setRunSpeed(Number(localStorage.getItem(paceKey()))||1,false)}catch{setRunSpeed(1,false)}document.body.classList.toggle('beat-runner',(mode>=5&&mode<=7));try{localStorage.setItem('musicthing-selected-effect',modes[mode]);localStorage.setItem('musicthing-spotify-preview','visualizer')}catch{}Array.from($('modes').children).forEach((b,i)=>b.classList.toggle('active',i===mode));$('modeHint').textContent='Swipe / tap picture / turn dial: effect · Full screen: tap to show controls';}
visuals.setModeRequest?.(setMode);
modes.forEach((name,i)=>{const b=node('button',name);b.onclick=()=>{setMode(i);setMenu(false)};$('modes').append(b)});$('scope').onclick=$('gpu').onclick=e=>{if((mode>=5&&mode<=7)){visuals.cycleRunner();return}if(visualFull){setFull(false);return}setMode(mode+1)};let initialMode=0;try{const saved=modes.indexOf(localStorage.getItem('musicthing-selected-effect'));if(saved>=0)initialMode=saved}catch{}setMode(initialMode);
let sensitivity=1;try{const saved=Number(localStorage.getItem('musicthing-sensitivity-v2'));if(saved>=.5&&saved<=4)sensitivity=saved}catch{}
function setSensitivity(value,persist=true){sensitivity=Math.max(.5,Math.min(4,Number(value)||1));visuals.setSensitivity(sensitivity);$('sensitivity').value=sensitivity;$('sensitivityValue').textContent=sensitivity.toFixed(1)+'×';if(persist)try{localStorage.setItem('musicthing-sensitivity-v2',String(sensitivity))}catch{}}
function paceKey(){return mode===6?'musicthing-fighter-speed':mode===7?'musicthing-dj-speed':'musicthing-run-speed'}
function setRunSpeed(value,persist=true){runSpeed=Math.max(.5,Math.min(2.5,value));visuals.setRunnerSpeed(runSpeed);$('runnerSpeed').value=runSpeed;$('runnerSpeedValue').textContent=runSpeed.toFixed(1)+'×';if(persist&&mode>=5&&mode<=7)try{localStorage.setItem(paceKey(),String(runSpeed))}catch{}}setRunSpeed(runSpeed);
let skylineGain=1;try{skylineGain=Number(localStorage.getItem('musicthing-skyline-sensitivity'))||1}catch{}
function setSkyline(value,persist=true){skylineGain=Math.max(.1,Math.min(2,Number(value)||1));visuals.setSkylineSensitivity(skylineGain);$('skylineSensitivity').value=skylineGain;$('skylineSensitivityValue').textContent=skylineGain.toFixed(1)+'×';if(persist)try{localStorage.setItem('musicthing-skyline-sensitivity',String(skylineGain))}catch{}}setSkyline(skylineGain);
$('runnerSpeed').oninput=e=>setRunSpeed(Number(e.target.value));$('skylineSensitivity').oninput=e=>setSkyline(e.target.value);
$('sensitivity').oninput=e=>setSensitivity(e.target.value);setSensitivity(sensitivity);
$('back').onclick=()=>{if(menuOpen){setMenu(false);return}if(tab!=='visualizer'){show('visualizer');return}capture(false);api.triggerAction({id:'music-dismiss',source:'server'})};
function setFull(full){setMenu(false);visualFull=full;document.body.classList.toggle('visual-full',full);$('scope').setAttribute('aria-label',full?'Tap to show visualizer controls':'Tap to change visualizer');$('gpu').setAttribute('aria-label',full?'Tap to show visualizer controls':'Tap to change visualizer')}
$('fullscreen').onclick=()=>setFull(true);$('restoreControls').onclick=()=>setFull(false);
$('capture').onclick=()=>{audioWanted=!audioWanted;capture(audioWanted)};
function transportState(){for(const id of ['previousTrack','togglePlayback','nextTrack'])$(id).disabled=transportPending||!playing?.device;setButtonIcon($('togglePlayback'),playing?.playing?'pause':'play');$('togglePlayback').setAttribute('aria-label',playing?.playing?'Pause Spotify':'Play Spotify');$('playbackDevice').textContent=playing?.device||'Open Spotify and choose a speaker';}
function playbackControl(command){if(transportPending||!playing?.device)return;if(command==='pause'||command==='resume')playbackSync.set(command==='resume');transportPending=true;transportState();request('transport',command);clearTimeout(transportTimer);transportTimer=setTimeout(()=>{transportPending=false;transportState();$('status').textContent='Playback response timed out. Check Spotify and try again.'},14000)}
$('previousTrack').onclick=()=>playbackControl('previous');$('nextTrack').onclick=()=>playbackControl('next');$('togglePlayback').onclick=()=>playbackControl(playing?.playing?'pause':'resume');
function embed(id){if(!/^[A-Za-z0-9]{22}$/.test(id||'')){$('status').textContent='Start a Spotify track first.';return}videoId=id;replace($('embed'),node('p','Connecting the Spotify player through your Mac…','hint'));request('video-ready');}
function mountEmbed(){const id=videoId;if(!id)return;const frame=node('iframe');frame.src='https://open.spotify.com/embed/track/'+id;frame.title='Spotify official player';frame.allow='autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture';frame.setAttribute('allowfullscreen','');replace($('embed'),frame);$('status').textContent='Official Spotify player · video availability depends on the track and browser.';}
$('loadVideo').onclick=()=>{if(playing?.track?.id)embed(playing.track.id);else{$('status').textContent='Start a song in Spotify, then tap Load current again.';request('playing')}};$('videoExample').onclick=()=>embed('0Lr4kGOYn9l83EjuK6cZFQ');$('stopVideo').onclick=()=>replace($('embed'));$('refreshLyrics').onclick=()=>request('lyrics');
function list(section,items){const root=$(section==='devices'?'deviceList':section+'List');replace(root);if(!items.length){root.append(node('p',section==='devices'?'Open Spotify on a phone, computer or speaker to make it available.':'Nothing here yet.'));return}items.forEach(item=>{const row=node('div',null,'item'),text=node('div',null,'text');text.append(node('strong',item.title||item.name),node('small',item.artist||(item.active?'Selected speaker':item.type)));row.append(text);if(section==='devices'){const b=node('button',item.active?'Selected':'Use speaker');b.disabled=item.restricted||item.active;b.onclick=()=>request('transfer',item.id);row.append(b)}else{const play=node('button','Play'),add=node('button','+ Queue');play.onclick=()=>request('play',item.uri);add.onclick=()=>request('enqueue',item.uri);row.append(play,add)}root.append(row)})}
api.on('music-result',message=>{const m=message.payload;if(requests.get(m.section)!==m.id)return;if(m.section==='transport'){clearTimeout(transportTimer);transportPending=false;transportState();if(!m.error){request('playing');setTimeout(()=>request('playing'),600)}}if(m.error){$('status').textContent=m.error;if(m.section==='video-ready')replace($('embed'),node('p',m.error,'hint'));if(m.section==='lyrics'){lyricLines=[];$('lyricText').textContent=m.error;$('lyricMode').textContent='Lyrics unavailable';$('followLyrics').hidden=true}if(['queue','recent','devices'].includes(m.section)){const root=$(m.section==='devices'?'deviceList':m.section+'List');replace(root,node('p',m.error,'list-error'))}return}if(m.section==='playing'){const previous=playing?.track?.id;playing=m.data;playingReceivedAt=Date.now();transportState();if(tab==='lyrics'&&previous!==playing.track?.id){lyricLines=[];$('lyricText').textContent='Loading lyrics…';request('lyrics')}tickLyrics();$('track').textContent=playing.track?playing.track.title+' · '+playing.track.artist:'Start a song in Spotify';}else if(m.section==='video-ready'){mountEmbed();return}else if(m.section==='lyrics'){renderLyrics(m.data);return}else if(['queue','recent','devices'].includes(m.section))list(m.section,m.data);else if(m.section==='transfer')list('devices',m.data);if(m.section!=='playing')$('status').textContent=['play','enqueue','transfer','transport'].includes(m.section)?'':'More: choose a page · Turn dial to scroll';});
api.on('visualizer-frame',message=>{if(!capturing)return;const frame=message.payload;if(frame.status==='audio'){visuals.setFrame(frame);$('captureStatus').textContent=frame.bands.some(v=>v>.05)?'● Live audio · '+modes[mode]:'Listening · Choose this Mac as Spotify’s speaker'}else{$('captureStatus').textContent=frame.message||'System audio connected.';if(frame.status==='permission'||frame.status==='error'){setFull(false);capture(false);$('captureStatus').textContent=frame.message;}}});
function syncCapture(){capture(tab==='visualizer'&&audioWanted&&visible&&!document.hidden&&!overlay)}
api.on('client_status',m=>{announceMusicOptions();visible=(m.request||m.payload?.status)!=='closed';syncCapture()});document.addEventListener('visibilitychange',()=>{syncCapture()});window.addEventListener('pagehide',()=>capture(false));
window.addEventListener('message',e=>{if(e.source!==window.parent||e.data?.source!=='deskthing')return;if(e.data.type==='music-options-probe'){announceMusicOptions();return}if(e.data.type==='music-options'){setMenu(true);return}if(e.data.type!=='overlay-priority')return;overlay=!!e.data.payload?.open;syncCapture()});
let gesture=null,suppressClickUntil=0,lastDial=0,sensitivityTimer=null;
function showSensitivity(){const running=(mode>=5&&mode<=7),value=mode===8?visuals.getHyperZoom():running?runSpeed:sensitivity;clearTimeout(sensitivityTimer);$('gestureHudLabel').textContent=running?(mode===7?'DJ pace':mode===6?'Combat pace':'Running speed'):mode===8?'Zoom':'Sensitivity';$('sensitivityHud').classList.toggle('show',true);$('sensitivityHudValue').textContent=value.toFixed(1)+'×';$('sensitivityHudFill').style.height=((value-.5)/(running?2:3.5)*100)+'%';}
function fadeSensitivity(){clearTimeout(sensitivityTimer);sensitivityTimer=setTimeout(()=>$('sensitivityHud').classList.toggle('show',false),900)}
if(typeof installTwoFinger==='function')installTwoFinger({root:$('visualizer'),active:()=>mode>=5&&mode<=7&&!menuOpen&&!overlay,getGain:()=>visuals.getBackdropGain(),setGain:v=>visuals.setBackdropGain(v),cyclePalette:d=>visuals.cycleBackdropPalette(d),cancelSingle:()=>{gesture=null}});
document.addEventListener('touchstart',e=>{if(menuOpen||overlay){gesture=null;return}if(e.target.closest&&e.target.closest('button,[data-character-picker]')){gesture=null;return}const t=e.touches[0];const onVisual=e.target===$('scope')||e.target===$('gpu')||!!e.target?.closest?.('.canvas-wrap');gesture=t&&t.clientY>40?{x:t.clientX,y:t.clientY,axis:null,zoom:mode===8?visuals.getHyperZoom():1,sensitivity,runSpeed,onVisual:tab==='visualizer'&&onVisual}:null},{passive:true});
document.addEventListener('touchmove',e=>{if(!gesture||overlay)return;const t=e.touches[0];if(!t)return;const dx=t.clientX-gesture.x,dy=t.clientY-gesture.y;
if(!gesture.axis&&Math.max(Math.abs(dx),Math.abs(dy))>14){if(Math.abs(dy)>Math.abs(dx)*1.3&&gesture.onVisual)gesture.axis='sensitivity';else if(Math.abs(dx)>Math.abs(dy)*1.3)gesture.axis='effect'}
if(gesture.axis==='sensitivity'){e.preventDefault();suppressClickUntil=Date.now()+500;if(mode===8)visuals.setHyperZoom(gesture.zoom-dy/180);else if((mode>=5&&mode<=7))setRunSpeed(Math.round((gesture.runSpeed-dy/150)*10)/10,false);else setSensitivity(Math.round((gesture.sensitivity-dy/90)*10)/10,false);showSensitivity()}else if(gesture.axis==='effect')e.preventDefault();
},{passive:false});
document.addEventListener('touchend',e=>{if(!gesture||overlay)return;const g=gesture;gesture=null;if(g.axis==='sensitivity'){e.preventDefault();suppressClickUntil=Date.now()+500;if((mode>=5&&mode<=7))setRunSpeed(runSpeed);else if(mode!==8)setSensitivity(sensitivity);fadeSensitivity();return}const t=e.changedTouches[0],dx=t.clientX-g.x,dy=t.clientY-g.y;if(Math.abs(dx)>65&&Math.abs(dx)>Math.abs(dy)*1.4){e.preventDefault();suppressClickUntil=Date.now()+400;if(tab==='visualizer'&&g.onVisual)setMode(mode+(dx<0?1:-1))}},{passive:false});
document.addEventListener('touchcancel',()=>{if(gesture?.axis==='sensitivity'){if((mode>=5&&mode<=7))setRunSpeed(runSpeed);else if(mode!==8)setSensitivity(sensitivity);fadeSensitivity()}gesture=null});
document.addEventListener('click',e=>{if(Date.now()<suppressClickUntil){e.preventDefault();e.stopImmediatePropagation()}},true);
window.addEventListener('wheel',e=>{if(overlay||!visible||menuOpen)return;e.preventDefault();e.stopImmediatePropagation();const delta=Math.abs(e.deltaX)>Math.abs(e.deltaY)?e.deltaX:e.deltaY;if(!delta)return;if(tab==='visualizer'){if(Date.now()-lastDial>120){setMode(mode+(delta>0?1:-1));lastDial=Date.now()}}else{if(tab==='lyrics')manualLyrics();$(tab).scrollTop+=delta>0?100:-100}},{passive:false});
for(const section of ['queue','recent','devices']){const b=node('button','Refresh','refresh');b.onclick=()=>request(section);$(section).insertBefore(b,$(section).firstChild)}
setInterval(()=>{if(capturing&&visible&&!document.hidden)api.send({type:'visualizer',payload:{active:true}})},2000);setInterval(()=>{if(visible&&!document.hidden&&Date.now()-lastPlayingPoll>=(tab==='lyrics'?2500:10000)){lastPlayingPoll=Date.now();request('playing')}},1000);setInterval(tickLyrics,100);
show('visualizer');request('playing');
async function checkPlaybackSupport(){
const video=document.createElement('video'),result={mp4:video.canPlayType('video/mp4; codecs="avc1.42E01E"'),webm:video.canPlayType('video/webm; codecs="vp8"'),encryptedMedia:typeof navigator.requestMediaKeySystemAccess==='function',widevine:false};
if(result.encryptedMedia){try{await navigator.requestMediaKeySystemAccess('com.widevine.alpha',[{initDataTypes:['cenc'],videoCapabilities:[{contentType:'video/mp4; codecs="avc1.42E01E"'}]}]);result.widevine=true}catch{}}
api.send({type:'capabilities',payload:result});
if(!result.widevine){const note=node('p','This browser lacks the protected-playback support Spotify needs. The player can appear, but full video may not play.','hint');note.style.color='#ff72b8';$('video').append(note);}
}
function announceMusicOptions(){window.parent.postMessage?.({source:'musicthing',type:'music-options-ready'},'*')}setTimeout(announceMusicOptions,100);
window.addEventListener('storage',e=>{if(e.key==='musicthing-selected-effect'){const i=modes.indexOf(localStorage.getItem(e.key));if(i>=0)setMode(i)}else if(e.key==='musicthing-sensitivity-v2')setSensitivity(Number(localStorage.getItem(e.key)),false);else if(e.key===paceKey())setRunSpeed(Number(localStorage.getItem(e.key)),false);else if(e.key==='musicthing-skyline-sensitivity')setSkyline(localStorage.getItem(e.key),false);else if(e.key==='musicthing-visuals-paused'){audioWanted=localStorage.getItem(e.key)!=='true';syncCapture()}else if(e.key==='musicthing-control-page')openControlPage()});
function openControlPage(){try{const data=JSON.parse(localStorage.getItem('musicthing-control-page'));if(data&&Date.now()-data.time<10000&&tabs.some(([id])=>id===data.page))show(data.page)}catch{}}
setTimeout(openControlPage,150);