feat: add multi-user support for favorites management and room clock synchronization
- Implemented a new API endpoint for retrieving and managing user favorites in /api/users. - Added functionality for admins to edit the shared "main" user's favorites. - Created a one-shot DB smoke test script for verifying multi-user kiosk migrations. - Introduced a RoomClock class for synchronizing server time across clients using WebSocket.
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Radio Admin</title>␍
|
||||
<script type="module" crossorigin src="/assets/admin-GqZPhz-K.js"></script>
|
||||
<script type="module" crossorigin src="/assets/admin-BnGhtAku.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/player-BBOsFRH-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/debug-DBzSAgZo.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/admin-C-qnWY0z.css">
|
||||
</head>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
function x(l,t={}){let e,i=0,r=!1;function m(){const h=location.protocol==="https:"?"wss":"ws",o=new URLSearchParams;t.room&&o.set("room",t.room),t.kind&&o.set("kind",t.kind);const c=o.toString();e=new WebSocket(`${h}://${location.host}/ws${c?"?"+c:""}`),e.addEventListener("open",()=>{var n;i=0,(n=t.onOpen)==null||n.call(t)}),e.addEventListener("message",n=>{try{l(JSON.parse(n.data))}catch{}}),e.addEventListener("close",()=>{var n;(n=t.onClose)==null||n.call(t),!r&&(i=Math.min(i+1,6),setTimeout(m,500*2**i))}),e.addEventListener("error",()=>e.close())}return m(),{send(h){(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify(h))},close(){r=!0,e==null||e.close()},get readyState(){return e==null?void 0:e.readyState}}}const w=1e3,N=5e3,C=5,I=8,v=16;function S(l){if(!l.length)return 0;const t=l.slice().sort((i,r)=>i-r),e=t.length>>1;return t.length%2?t[e]:(t[e-1]+t[e])/2}class M{constructor(){this.offset=0,this.rtt=1/0,this.offsetStd=1/0,this.samples=[],this.synced=!1,this._pending=new Map,this._listeners=new Set,this._timeoutId=null,this._ws=null}attachWs(t){this._ws=t,this.reset();let e=0;const i=()=>{if(e++>=5){this._scheduleNext();return}this._sendPing(),setTimeout(i,150)};i()}detach(){this._timeoutId&&clearTimeout(this._timeoutId),this._timeoutId=null,this._pending.clear(),this._ws=null}reset(){this.samples=[],this.synced=!1,this.offsetStd=1/0,this._pending.clear(),this._timeoutId&&(clearTimeout(this._timeoutId),this._timeoutId=null)}now(){return Date.now()+this.offset}isStable(){return this.synced&&this.samples.length>=I&&this.offsetStd<=C}onUpdate(t){return this._listeners.add(t),()=>this._listeners.delete(t)}handlePong(t){if(this._pending.get(t.t1)==null)return;this._pending.delete(t.t1);const i=Date.now(),r=i-t.t1,m=t.t2-(t.t1+i)/2;this.samples.push({offset:m,rtt:r}),this.samples.length>v&&this.samples.shift();const h=S(this.samples.map(s=>s.rtt)),o=Math.max(h*2,h+10),c=this.samples.filter(s=>s.rtt<=o),n=c.length?c.map(s=>s.offset):this.samples.map(s=>s.offset),_=S(n),p=n.reduce((s,d)=>s+d,0)/n.length,f=n.reduce((s,d)=>s+(d-p)**2,0)/n.length;this.offsetStd=Math.sqrt(f),this.offset=_,this.rtt=r,this.synced=!0;for(const s of this._listeners)s({offset:this.offset,rtt:this.rtt,offsetStd:this.offsetStd,samples:this.samples.length,accepted:c.length,stable:this.isStable()})}_sendPing(){if(!this._ws)return;const t=Date.now();this._pending.set(t,t);for(const e of this._pending.keys())t-e>5e3&&this._pending.delete(e);this._ws.send({type:"clock-ping",t1:t})}_scheduleNext(){this._timeoutId&&clearTimeout(this._timeoutId);const t=this.isStable()?N:w;this._timeoutId=setTimeout(()=>{this._sendPing(),this._scheduleNext()},t)}}const E="oradio.autoplayDismissed";function b(){var l;try{return!!(typeof window<"u"&&((l=window.oradioNative)!=null&&l.isElectron))}catch{return!1}}function L(){if(!b())return!1;try{return localStorage.getItem(E)==="1"}catch{return!1}}let u=null;function T({stationName:l="Radio",subtitle:t="",onStart:e,onCancel:i}={}){if(u)return u.promise;let r,m;const h=new Promise((a,g)=>{r=a,m=g}),o=document.createElement("div");o.className="play-gate-backdrop",o.setAttribute("role","dialog"),o.setAttribute("aria-modal","true"),o.setAttribute("aria-label","Tap to start audio");const c=document.createElement("div");c.className="play-gate-card";const n=document.createElement("h2");n.className="play-gate-title",n.textContent="Tap to start audio",c.appendChild(n);const _=document.createElement("div");if(_.className="play-gate-station",_.textContent=l,c.appendChild(_),t){const a=document.createElement("div");a.className="play-gate-sub",a.textContent=t,c.appendChild(a)}const p=document.createElement("div");p.className="play-gate-row";const f=document.createElement("button");f.className="play-gate-start",f.textContent="▶ Start";const s=document.createElement("button");s.className="play-gate-cancel",s.textContent="Cancel",p.appendChild(f),p.appendChild(s),c.appendChild(p);let d=null;if(b()){const a=document.createElement("label");a.className="play-gate-dismiss",d=document.createElement("input"),d.type="checkbox",d.id="play-gate-dismiss-cb",a.appendChild(d);const g=document.createElement("span");g.textContent=" Don't show again on this device",a.appendChild(g),c.appendChild(a)}o.appendChild(c),document.body.appendChild(o),queueMicrotask(()=>f.focus());function y(){var a;(a=u==null?void 0:u.backdrop)!=null&&a.parentNode&&u.backdrop.parentNode.removeChild(u.backdrop),u=null}function k(){if(d&&d.checked)try{localStorage.setItem(E,"1")}catch{}}return f.addEventListener("click",()=>{k(),y();try{e&&e()}catch{}r()}),s.addEventListener("click",()=>{y();try{i&&i()}catch{}m(new Error("autoplay-cancelled"))}),o.addEventListener("keydown",a=>{a.key==="Escape"&&s.click()}),u={backdrop:o,promise:h},h}export{M as R,L as a,x as c,T as s};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function l(s,n={}){let e,r=0,c=!1;function i(){const a=location.protocol==="https:"?"wss":"ws",o=new URLSearchParams;n.room&&o.set("room",n.room),n.kind&&o.set("kind",n.kind);const d=o.toString();e=new WebSocket(`${a}://${location.host}/ws${d?"?"+d:""}`),e.addEventListener("open",()=>{var t;r=0,(t=n.onOpen)==null||t.call(n)}),e.addEventListener("message",t=>{try{s(JSON.parse(t.data))}catch{}}),e.addEventListener("close",()=>{var t;(t=n.onClose)==null||t.call(n),!c&&(r=Math.min(r+1,6),setTimeout(i,500*2**r))}),e.addEventListener("error",()=>e.close())}return i(),{send(a){(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify(a))},close(){c=!0,e==null||e.close()},get readyState(){return e==null?void 0:e.readyState}}}export{l as c};
|
||||
@@ -5,11 +5,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1080, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<title>Radio Kiosk</title>␍
|
||||
<script type="module" crossorigin src="/assets/kiosk-CzWLja7k.js"></script>
|
||||
<script type="module" crossorigin src="/assets/kiosk-DIx-PLJP.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/player-BBOsFRH-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ws-BM1PmMVd.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/kiosk-PzkUrLf6.css">
|
||||
<link rel="modulepreload" crossorigin href="/assets/debug-DBzSAgZo.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/playGate-C1e0nYli.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/kiosk-DuoYH-tL.css">
|
||||
</head>
|
||||
|
||||
<body class="kiosk">
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Radio Master</title>␍
|
||||
<script type="module" crossorigin src="/assets/master-kSyrThjc.js"></script>
|
||||
<script type="module" crossorigin src="/assets/master-BGIwPPRC.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/player-BBOsFRH-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ws-BM1PmMVd.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/master-CpJfsvtJ.css">
|
||||
<link rel="modulepreload" crossorigin href="/assets/debug-DBzSAgZo.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/playGate-C1e0nYli.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/master-B8Vyo4--.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user