Files
radio-explorer/server/public/assets/master-kSyrThjc.js
Marco Mooren b86dcfbb8d Add master display UI with audio output management and styling
- Implement main.js for the master display functionality, including WebSocket connection, audio output management, and state handling.
- Create style.css for the master display's visual design, ensuring a cohesive look and feel with a dark theme and responsive layout.
- Integrate device management with a fallback for non-Electron environments, allowing users to select audio outputs.
- Add features for managing favorites, including toggling favorites and filtering by genre.
- Enhance user experience with a responsive favorites grid and drag-to-scroll functionality.
2026-05-11 17:55:09 +02:00

2 lines
11 KiB
JavaScript
Raw 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.

import"./modulepreload-polyfill-B5Qt9EMX.js";import{a as v,P as E,c as C,e as s}from"./player-BBOsFRH-.js";import{c as D}from"./ws-BM1PmMVd.js";const F=[{id:"default",label:"System default",kind:"speakers"},{id:"speakers-internal",label:"Built-in speakers",kind:"speakers"},{id:"headphones-jack",label:"Headphones (3.5mm)",kind:"headphones"},{id:"hdmi-tv",label:"HDMI Living-room TV",kind:"hdmi"},{id:"bt-marshall",label:"Bluetooth Marshall Stanmore",kind:"bluetooth"},{id:"usb-audient",label:"USB Audient EVO 4",kind:"usb"}],f=document.getElementById("app"),t={user:null,rooms:[],roomSlug:null,room:null,peers:[],devices:{list:[],current:"default"},np:{stationId:null,station:null,playing:!1,loading:!1,volume:.7,error:null},voteStats:null,favorites:[],favGenre:"",showOutputs:!1,session:null},c=window.oradioNative||null;let p=null,u=null;async function I(){var a,o;try{t.user=await v.get("/api/auth/me")}catch{return x()}const n=new URLSearchParams(location.search).get("room");try{t.rooms=await v.get("/api/rooms")}catch{t.rooms=[]}t.roomSlug=n||t.rooms[0]&&t.rooms[0].slug||`u-${t.user.id}`,c!=null&&c.listOutputs?(t.devices.list=await c.listOutputs(),t.devices.current=await c.getCurrent()||((a=t.devices.list[0])==null?void 0:a.id),(o=c.onCurrentChanged)==null||o.call(c,d=>{t.devices.current=d,S(),i()})):(t.devices.list=F,t.devices.current="default"),u=new E({onState:d=>{Object.assign(t.np,d),b(),i()}});try{t.favorites=await v.get("/api/me/favorites")}catch{t.favorites=[]}L(),i()}typeof window<"u"&&(window.addEventListener("pagehide",()=>h({beacon:!0})),window.addEventListener("beforeunload",()=>h({beacon:!0})));function L(){if(p)try{p.close()}catch{}p=D(P,{room:t.roomSlug,kind:"display",onOpen:()=>S()})}function P(e){var n;if(!(!e||!e.type))switch(e.type){case"hello":{t.room=e.room,t.peers=e.peers||[],(n=e.you)!=null&&n.kind&&e.you.kind!=="display"&&(t.np.error=`This room already has a display (${B(e.peers)} active). You were joined as ${e.you.kind}.`);const a=e.state;a!=null&&a.station_id&&a.station&&a.station_id!==t.np.stationId&&w(a.station,{silent:!0}),typeof(a==null?void 0:a.volume)=="number"&&u.setVolume(a.volume),i();return}case"presence":t.peers=e.peers||[],i();return;case"command":_(e);return;case"vote":case"plays":e.stationId===t.np.stationId&&(t.voteStats={...t.voteStats||{},...e.stats||{}},e.type==="plays"&&(t.voteStats.plays=e.plays),i());return;default:return}}function _(e){switch(e.action){case"play":{const n=Number(e.stationId);if(!Number.isFinite(n))return;v.get(`/api/stations/${n}`).then(a=>w(a)).catch(()=>{});return}case"pause":u.togglePause();return;case"stop":u.stop(),h(),t.np.playing=!1,t.np.stationId=null,b(),i();return;case"volume":typeof e.value=="number"&&u.setVolume(e.value);return;case"setSink":$(String(e.deviceId||""));return;default:return}}async function w(e,{silent:n}={}){if(e&&(h(),t.np.station=e,t.np.stationId=e.id,t.voteStats={up:e.up||0,down:e.down||0,plays:e.plays||0,score:e.score||0},i(),await u.play(e),!n))try{const a=await v.post(`/api/stations/${e.id}/play`);t.np.stationId===e.id?(t.session={id:a.sessionId,stationId:e.id,startedAt:Date.now()},t.voteStats={...t.voteStats,...a}):a.sessionId&&v.post(`/api/stations/${e.id}/play/end`,{sessionId:a.sessionId,duration_ms:0}).catch(()=>{})}catch{}}function h({beacon:e=!1}={}){const n=t.session;if(!n||!n.id)return;t.session=null;const a={sessionId:n.id,duration_ms:Math.max(0,Date.now()-n.startedAt)},o=`/api/stations/${n.stationId}/play/end`;if(e&&typeof navigator<"u"&&navigator.sendBeacon)try{navigator.sendBeacon(o,new Blob([JSON.stringify(a)],{type:"application/json"}));return}catch{}v.post(o,a).catch(()=>{})}function b(){if(!p||!t.np.stationId){p==null||p.send({type:"state",stationId:t.np.stationId,playing:!!t.np.playing,volume:t.np.volume});return}p.send({type:"state",stationId:t.np.stationId,playing:!!t.np.playing,volume:t.np.volume})}function S(){p==null||p.send({type:"devices",list:t.devices.list,current:t.devices.current})}async function $(e){var n;if(e){if(c!=null&&c.setOutput)try{await c.setOutput(e)}catch(a){console.warn("[master] setOutput failed",a);return}if(t.devices.current=e,(n=u==null?void 0:u.audio)!=null&&n.setSinkId&&/^[a-f0-9]{16,}$/.test(e))try{await u.audio.setSinkId(e)}catch{}S(),t.showOutputs=!1,i()}}function B(e){return(e||[]).filter(n=>n.kind==="display").length}function O(e){return!!e&&t.favorites.some(n=>n.id===e)}async function M(e){if(!e)return;const n=O(e);try{n?await v.del(`/api/me/favorites/${e}`):await v.put(`/api/me/favorites/${e}`,{position:t.favorites.length}),t.favorites=await v.get("/api/me/favorites"),i()}catch(a){console.warn("[master] toggleFavorite failed",a)}}function A(){const e=new Map;for(const n of t.favorites)for(const a of n.genres||[])e.set(a,(e.get(a)||0)+1);return[...e.entries()].sort((n,a)=>a[1]-n[1]||n[0].localeCompare(a[0])).map(([n,a])=>({genre:n,count:a}))}function G(){return t.favGenre?t.favorites.filter(e=>(e.genres||[]).includes(t.favGenre)):t.favorites}function i(){var l;const e=((l=f.querySelector(".favs-grid"))==null?void 0:l.scrollLeft)??0;C(f);const n=t.np,a=n.station,o=(a==null?void 0:a.image_display_url)||(a==null?void 0:a.image_url)||null,d=O(a==null?void 0:a.id),g=s("div",{class:"master"},s("header",{class:"topbar"},s("h1",{},"◉ MASTER"),s("div",{class:"pill"},s("span",{},"Room:"),s("select",{onChange:r=>{t.roomSlug=r.target.value,history.replaceState(null,"",`?room=${encodeURIComponent(t.roomSlug)}`),L()}},...t.rooms.map(r=>s("option",{value:r.slug,selected:r.slug===t.roomSlug},r.name)))),s("div",{class:"pill peers"},`${t.peers.length} peer${t.peers.length===1?"":"s"}`),n.error?s("div",{class:"err-banner"},n.error):null,s("div",{class:"grow"}),s("button",{class:"pill out-btn"+(t.showOutputs?" active":""),title:"Audio output",onClick:()=>{t.showOutputs=!t.showOutputs,i()}},"🔊 ",V()),s("div",{class:"pill"},c?"native":"browser"),s("div",{class:"pill"},t.user.username)),s("section",{class:"stage"},s("div",{class:"np"},s("div",{class:"art"+(o?"":" empty")},o?s("img",{class:"art-img",src:o,alt:"",referrerpolicy:"no-referrer",onError:r=>{const y=r.target.parentNode;r.target.remove(),y&&y.classList.add("empty")}}):null),s("div",{class:"meta"},s("div",{class:"tiny"},n.loading?"Loading…":n.playing?"Now playing":a?"Paused":"Idle"),s("div",{class:"title-row"},s("h2",{},(a==null?void 0:a.name)||"—"),a?s("button",{class:"fav-toggle"+(d?" on":""),title:d?"Remove favorite":"Add favorite",onClick:()=>M(a.id)},d?"★":"☆"):null),s("div",{class:"genres"},...((a==null?void 0:a.genres)||[]).slice(0,6).map(r=>s("span",{class:"tag"},r))),t.voteStats?s("div",{class:"stats"},s("span",{},"▲ ",s("b",{},String(t.voteStats.up||0))),s("span",{},"▼ ",s("b",{},String(t.voteStats.down||0))),s("span",{},"▶ ",s("b",{},String(t.voteStats.plays||0)))):null,a!=null&&a.country?s("div",{class:"stats"},s("span",{},a.country)):null,s("div",{class:"transport"},s("button",{class:"ctrl primary",title:"Play / pause",disabled:!a,onClick:()=>u.togglePause()},n.playing?"❚❚":"▶"),s("button",{class:"ctrl",title:"Stop",disabled:!a,onClick:()=>{u.stop(),h(),t.np.playing=!1,b(),i()}},"■"),s("div",{class:"vol"},s("span",{class:"vol-icon"},"🔊"),s("input",{type:"range",min:0,max:1,step:.01,value:n.volume,onInput:r=>u.setVolume(Number(r.target.value))}),s("span",{class:"val"},Math.round(n.volume*100)+"%"))),s("div",{class:"peer-line"},s("span",{class:"peer-line-label"},"In room:"),...t.peers.length?t.peers.map(r=>{var y;return s("span",{class:"peer role-"+r.kind},s("span",{class:"role-tag"},r.kind),s("span",{},((y=r.user)==null?void 0:y.username)||"?"))}):[s("span",{class:"peer"},"Just you.")])))),s("section",{class:"stations-bar"},q()),t.showOutputs?T():null);f.appendChild(g);const m=f.querySelector(".favs-grid");m&&(e&&(m.scrollLeft=e),N(m))}function N(e){if(e.dataset.dragBound==="1")return;e.dataset.dragBound="1";let n=!1,a=!1,o=0,d=0,g=-1;e.addEventListener("pointerdown",l=>{l.pointerType==="mouse"&&l.button!==0||(n=!0,a=!1,o=l.clientX,d=e.scrollLeft,g=l.pointerId)}),e.addEventListener("pointermove",l=>{if(!n)return;const r=l.clientX-o;if(!a&&Math.abs(r)>5){a=!0;try{e.setPointerCapture(g)}catch{}e.classList.add("dragging")}a&&(e.scrollLeft=d-r,l.preventDefault())});const m=()=>{if(n=!1,a){const l=r=>{r.stopPropagation(),r.preventDefault()};e.addEventListener("click",l,{capture:!0,once:!0}),setTimeout(()=>e.removeEventListener("click",l,!0),0)}a=!1,e.classList.remove("dragging");try{e.releasePointerCapture(g)}catch{}};e.addEventListener("pointerup",m),e.addEventListener("pointercancel",m),e.addEventListener("pointerleave",()=>{n&&!a&&(n=!1)})}function k(e){const n=f.querySelector(".favs-grid");if(!n)return;const a=Math.max(160,Math.round(n.clientWidth*.8));n.scrollBy({left:e*a,behavior:"smooth"})}function q(){const e=A(),n=G();return s("div",{class:"card favs-card"},s("div",{class:"favs-header"},s("h3",{},`Favorites (${n.length}${t.favGenre?`/${t.favorites.length}`:""})`),e.length?s("select",{class:"genre-filter",title:"Filter by genre",onChange:a=>{t.favGenre=a.target.value,i()}},s("option",{value:""},"All genres"),...e.map(({genre:a,count:o})=>s("option",{value:a,selected:t.favGenre===a},`${a} (${o})`))):null,s("button",{class:"favs-nav",title:"Scroll left",onClick:()=>k(-1)},""),s("button",{class:"favs-nav",title:"Scroll right",onClick:()=>k(1)},"")),s("div",{class:"favs-grid"},...n.length?n.map(a=>{const o=a.image_display_url||a.image_url,d=t.np.stationId===a.id;return s("button",{class:"fav-tile"+(d?" active":""),title:a.name,onClick:()=>w(a)},s("div",{class:"fav-art"+(o?"":" empty"),style:o?{backgroundImage:`url("${o}")`}:{}}),s("div",{class:"fav-name"},a.name))}):[s("div",{class:"favs-empty"},t.favGenre?"No favorites in this genre.":"No favorites yet. Star a station to add it.")]))}function T(){return s("div",{class:"out-popover-wrap",onClick:e=>{e.target===e.currentTarget&&(t.showOutputs=!1,i())}},s("div",{class:"out-popover card"},s("div",{class:"out-popover-head"},s("h3",{},"Audio output"),s("button",{class:"close",title:"Close",onClick:()=>{t.showOutputs=!1,i()}},"×")),s("div",{class:"device-list"},...t.devices.list.map(e=>s("button",{class:"device"+(e.id===t.devices.current?" active":""),onClick:()=>{$(e.id)}},s("span",{class:"dot"}),s("span",{class:"name"},e.label),s("span",{class:"kind"},e.kind))))))}function V(){const e=t.devices.list.find(n=>n.id===t.devices.current);return e?e.label:"—"}function x(){C(f),f.appendChild(s("div",{class:"login"},s("form",{onSubmit:async e=>{e.preventDefault();const n=new FormData(e.target);try{t.user=await v.post("/api/auth/login",{username:n.get("username"),password:n.get("password")}),await I()}catch(a){e.target.querySelector(".err").textContent=a.message}}},s("h1",{},"Master sign in"),s("input",{name:"username",placeholder:"Username",required:!0}),s("input",{name:"password",type:"password",placeholder:"Password",required:!0}),s("div",{class:"err"}),s("button",{type:"submit"},"Sign in"))))}I();