Files
radio-explorer/server/public/assets/admin-CVu6KAFb.js
Marco Mooren e0a60f7b64 Add player functionality with HLS support and API integration
- Implemented a new Player class in player.js to handle audio playback, including HLS support using hls.js.
- Created a shared API module in api.js for making HTTP requests with proper error handling.
- Added DOM utility functions in dom.js for creating and clearing elements.
- Introduced WebSocket connection handling in ws.js for real-time updates.
- Developed a comprehensive CSS stylesheet for styling the application, including a high-contrast theme.
2026-05-10 14:43:00 +02:00

2 lines
9.9 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{a as i,c as g,e}from"./dom-BZgKDOeX.js";const b=document.getElementById("app"),n={user:null,view:"stations",stations:[],users:[],system:null,search:""};async function v(){try{n.user=await i.get("/api/auth/me")}catch{return C()}if(n.user.role!=="admin"){b.innerHTML=`<div class="login"><div><h1>Admin only</h1><p>Signed in as ${n.user.username} (${n.user.role}).</p></div></div>`;return}await u(),m()}async function u(){const a=[i.get("/api/stations?all=1")];n.view==="users"&&a.push(i.get("/api/auth/users")),n.view==="system"&&a.push(i.get("/api/admin/system"));const[t,s,p]=await Promise.all(a);n.stations=t,n.view==="users"&&(n.users=s||[]),n.view==="system"&&(n.system=s||p||null)}function C(){g(b),b.appendChild(e("div",{class:"login"},e("form",{onSubmit:async a=>{a.preventDefault();const t=new FormData(a.target);try{n.user=await i.post("/api/auth/login",{username:t.get("username"),password:t.get("password")}),await v()}catch(s){a.target.querySelector(".err").textContent=s.message}}},e("h1",{},"Admin sign in"),e("input",{name:"username",placeholder:"Username",required:!0}),e("input",{name:"password",type:"password",placeholder:"Password",required:!0}),e("div",{class:"err"}),e("button",{class:"btn primary",type:"submit"},"Sign in"))))}function m(){g(b);const a=e("aside",{class:"side"},e("h1",{},"Online Radio Explorer"),...["stations","import","users","system"].map(s=>e("button",{class:`nav ${n.view===s?"active":""}`,onClick:async()=>{n.view=s,await u(),m()}},k(s))),e("div",{class:"me"},`Signed in as ${n.user.username}`,e("br"),e("a",{href:"#",onClick:async s=>{s.preventDefault(),await i.post("/api/auth/logout"),location.reload()}},"Sign out"))),t=e("main",{class:"main"});n.view==="stations"?S(t):n.view==="import"?I(t):n.view==="users"?$(t):n.view==="system"&&E(t),b.appendChild(e("div",{class:"shell"},a,t))}function k(a){return{stations:"Stations",import:"Import",users:"Users",system:"System"}[a]}function S(a){a.appendChild(e("div",{class:"bar"},e("input",{placeholder:"Search…",value:n.search,onInput:s=>{n.search=s.target.value,w()}}),e("button",{class:"btn primary",onClick:()=>f()},"+ Add station"),e("button",{class:"btn",onClick:async()=>{await i.post("/api/admin/health-check"),alert("Health check finished"),await u(),m()}},"Run health check")));const t=e("div",{id:"tableWrap"});a.appendChild(t),w()}function w(){const a=document.getElementById("tableWrap");if(!a)return;g(a);const t=n.search.toLowerCase(),s=n.stations.filter(o=>!t||o.name.toLowerCase().includes(t)||(o.country||"").toLowerCase().includes(t)||(o.genres||[]).some(c=>c.toLowerCase().includes(t))),p=e("table",{},e("thead",{},e("tr",{},e("th",{},"Name"),e("th",{},"Source"),e("th",{},"Genres"),e("th",{},"Country"),e("th",{},"Enabled"),e("th",{},"Actions"))),e("tbody",{},...s.map(o=>e("tr",{},e("td",{},e("strong",{},o.name),e("br"),e("small",{},o.homepage||"")),e("td",{},o.source),e("td",{},...(o.genres||[]).slice(0,4).map(c=>e("span",{class:"tag"},c))),e("td",{},o.country||""),e("td",{},o.enabled?"✅":"⛔"),e("td",{},e("button",{class:"btn",onClick:()=>f(o.id)},"Edit")," ",e("button",{class:"btn danger",onClick:async()=>{confirm(`Delete ${o.name}?`)&&(await i.del(`/api/stations/${o.id}`),await u(),m())}},"Delete"))))));a.appendChild(p)}async function f(a){const t=a?await i.get(`/api/stations/${a}`):{name:"",genres:[],streams:[],enabled:!0},s=e("dialog"),p=e("div",{class:"streams"});function o(){var l;g(p),p.appendChild(e("div",{style:{fontWeight:600,marginBottom:"6px"}},"Streams")),(l=t.streams)!=null&&l.length||p.appendChild(e("div",{style:{color:"#6b7280"}},"No streams yet."));for(const r of t.streams||[])p.appendChild(e("div",{class:"stream-row"},e("select",{onChange:d=>r.format=d.target.value},...["mp3","aac","hls","m3u","pls","ogg","unknown"].map(d=>e("option",{value:d,selected:r.format===d},d))),e("input",{value:r.url,placeholder:"https://…",onInput:d=>r.url=d.target.value}),e("input",{type:"number",placeholder:"kbps",value:r.bitrate||"",onInput:d=>r.bitrate=Number(d.target.value)||null}),e("input",{value:r.label||"",placeholder:"Label",onInput:d=>r.label=d.target.value}),r.last_status?e("span",{class:`pill ${r.last_status==="up"?"up":"down"}`},r.last_status):e("span"),e("button",{class:"btn danger",type:"button",onClick:()=>{t.streams=t.streams.filter(d=>d!==r),o()}},"×")));p.appendChild(e("button",{class:"btn",type:"button",onClick:()=>{var r;t.streams=[...t.streams||[],{url:"",format:"mp3",priority:((r=t.streams)==null?void 0:r.length)||0}],o()}},"+ Add stream"))}const c=e("form",{method:"dialog",onSubmit:async l=>{l.preventDefault();const r={name:t.name,homepage:t.homepage,country:t.country,genres:t.genres,description:t.description,image_url:t.image_url,enabled:t.enabled};if(a){await i.patch(`/api/stations/${a}`,r);const d=await i.get(`/api/stations/${a}`);for(const y of d.streams||[])await i.del(`/api/stations/${a}/streams/${y.id}`);for(const y of t.streams||[])y.url&&await i.post(`/api/stations/${a}/streams`,y)}else r.streams=(t.streams||[]).filter(d=>d.url),await i.post("/api/stations",r);s.close(),await u(),m()}},e("h2",{},a?"Edit station":"Add station"),e("div",{class:"row"},e("label",{},"Name"),e("input",{value:t.name,onInput:l=>t.name=l.target.value,required:!0})),e("div",{class:"row"},e("label",{},"Homepage"),e("input",{value:t.homepage||"",onInput:l=>t.homepage=l.target.value})),e("div",{class:"row"},e("label",{},"Country"),e("input",{value:t.country||"",maxlength:4,onInput:l=>t.country=l.target.value})),e("div",{class:"row"},e("label",{},"Genres"),e("input",{value:(t.genres||[]).join(", "),onInput:l=>t.genres=l.target.value.split(",").map(r=>r.trim()).filter(Boolean)})),e("div",{class:"row"},e("label",{},"Image URL"),e("input",{value:t.image_url||"",onInput:l=>t.image_url=l.target.value})),e("div",{class:"row col"},e("textarea",{rows:2,placeholder:"Description",onInput:l=>t.description=l.target.value},t.description||"")),e("div",{class:"row"},e("label",{},"Enabled"),e("input",{type:"checkbox",checked:t.enabled,onChange:l=>t.enabled=l.target.checked})),p,e("div",{class:"actions"},e("button",{class:"btn",type:"button",onClick:()=>s.close()},"Cancel"),e("button",{class:"btn primary",type:"submit"},"Save")));o(),s.appendChild(c),document.body.appendChild(s),s.showModal(),s.addEventListener("close",()=>s.remove())}function I(a){let t=[];const s=e("div");a.appendChild(e("h2",{},"Import from Radio-Browser")),a.appendChild(e("div",{class:"bar"},e("input",{id:"rbq",placeholder:"Search by name…"}),e("input",{id:"rbcountry",placeholder:"Country (e.g. NL)",style:{minWidth:"120px"}}),e("input",{id:"rbtag",placeholder:"Tag/genre"}),e("button",{class:"btn primary",onClick:async()=>{const o=new URLSearchParams({q:document.getElementById("rbq").value,country:document.getElementById("rbcountry").value,tag:document.getElementById("rbtag").value});t=await i.get(`/api/stations/sources/radiobrowser/search?${o}`),p()}},"Search"))),a.appendChild(s);function p(){if(g(s),!t.length){s.appendChild(e("p",{},"No results yet."));return}const o=e("table",{},e("thead",{},e("tr",{},e("th",{},"Name"),e("th",{},"Country"),e("th",{},"Tags"),e("th",{},"Stream"),e("th",{},""))),e("tbody",{},...t.map(c=>{var l,r;return e("tr",{},e("td",{},c.name),e("td",{},c.country||""),e("td",{},...(c.genres||[]).slice(0,4).map(d=>e("span",{class:"tag"},d))),e("td",{},e("small",{},(((l=c.streams[0])==null?void 0:l.format)||"")+" "+(((r=c.streams[0])==null?void 0:r.bitrate)||""))),e("td",{},e("button",{class:"btn primary",onClick:async()=>{await i.post("/api/stations/sources/radiobrowser/import",c),alert(`Imported ${c.name}`)}},"Import")))})));s.appendChild(o)}}function $(a){a.appendChild(e("div",{class:"bar"},e("h2",{style:{margin:0,flex:1}},"Users"),e("button",{class:"btn primary",onClick:D},"+ Add user"))),a.appendChild(e("table",{},e("thead",{},e("tr",{},e("th",{},"Username"),e("th",{},"Role"),e("th",{},"Created"),e("th",{},""))),e("tbody",{},...n.users.map(t=>e("tr",{},e("td",{},t.username),e("td",{},t.role),e("td",{},t.created_at),e("td",{},e("button",{class:"btn",onClick:async()=>{const s=prompt(`New password for ${t.username}:`);s&&(await i.patch(`/api/auth/users/${t.id}`,{password:s}),alert("Updated"))}},"Reset PW")," ",e("button",{class:"btn",onClick:async()=>{const s=t.role==="admin"?"user":"admin";await i.patch(`/api/auth/users/${t.id}`,{role:s}),await u(),m()}},"Toggle role")," ",t.id!==n.user.id?e("button",{class:"btn danger",onClick:async()=>{confirm(`Delete ${t.username}?`)&&(await i.del(`/api/auth/users/${t.id}`),await u(),m())}},"Delete"):null))))))}function D(){const a=e("dialog");a.appendChild(e("form",{method:"dialog",onSubmit:async t=>{t.preventDefault();const s=new FormData(t.target);await i.post("/api/auth/users",{username:s.get("username"),password:s.get("password"),role:s.get("role")}),a.close(),await u(),m()}},e("h2",{},"New user"),e("div",{class:"row"},e("label",{},"Username"),e("input",{name:"username",required:!0})),e("div",{class:"row"},e("label",{},"Password"),e("input",{name:"password",type:"password",required:!0})),e("div",{class:"row"},e("label",{},"Role"),e("select",{name:"role"},e("option",{value:"user"},"user"),e("option",{value:"admin"},"admin"))),e("div",{class:"actions"},e("button",{class:"btn",type:"button",onClick:()=>a.close()},"Cancel"),e("button",{class:"btn primary",type:"submit"},"Create")))),document.body.appendChild(a),a.showModal(),a.addEventListener("close",()=>a.remove())}function E(a){const t=n.system||{};a.appendChild(e("h2",{},"System")),a.appendChild(e("div",{class:"system-grid"},h("Stations",t.stations),h("Streams",t.streams),h("Users",t.users),h("Favorites",t.favorites),h("Node",t.node),h("Uptime (s)",t.uptime_s))),a.appendChild(e("div",{class:"bar",style:{marginTop:"16px"}},e("button",{class:"btn",onClick:async()=>{await i.post("/api/admin/health-check"),alert("Health check finished"),await u(),m()}},"Run health check"),e("button",{class:"btn",onClick:async()=>{const s=await i.post("/api/admin/reseed");alert(JSON.stringify(s))}},"Reseed (if empty)")))}function h(a,t){return e("div",{class:"stat"},e("div",{class:"k"},a),e("div",{class:"v"},t??"—"))}v();