Files
ipodderx-rs/web/index.html
rays cbd5e68d13 Derive the colour scheme from the iPodderX icon
Palette sampled from the icon rather than chosen to resemble it: the
silver device ramp, the screen blues, and the amber EQ bars. Dark theme
builds down from the screen navy; light theme uses the device body with
the deeper blue as accent.

Contrast was measured, not assumed: --faint, which carries dates and
sizes, failed AA in both themes and was moved along the icon's own grey
ramp until it passed. Lowest pair anywhere is now 4.54.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
2026-09-10 02:08:59 +00:00

791 lines
61 KiB
HTML
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.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<title>ipx</title>
<link rel="icon" href="data:image/jpeg;base64,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">
<style>
/* Palette taken from the 2004 iPodderX icon: the silver device body, the blue
screen, and the amber EQ bars. Hex values in comments are sampled straight from it. */
:root {
--bg:#0e131b; /* the screen's navy (#314B74), taken right down */
--panel:#151c27;
--panel2:#1c2431;
--raise:#25303f;
--line:#2c3849;
--fg:#f5f5f5; /* #F5F5F5 device highlight */
--dim:#95a0b1; /* #95A0B1 straight from the icon's blue-grey */
--faint:#7a8799; /* lifted from the icon ramp until it clears AA at small sizes */
--accent:#92b2e6; /* #92B2E6 the screen blue */
--accent2:#f49e2c; /* #F49E2C the EQ bars */
--ink:#0e131b; /* text on an accent fill */
--good:#6fbf8b;
--warn:#f49e2c; /* the amber doubles as the pending colour */
--bad:#e2705f;
--shadow:0 8px 28px rgba(6,10,16,.55);
--r:10px;
}
:root[data-theme="light"] {
--bg:#f2f4f7;
--panel:#ffffff; /* #FFFFFF device body */
--panel2:#e9edf3;
--raise:#dde3ec;
--line:#d6d6d6; /* #D6D6D6 device edge */
--fg:#1a1a1a; /* #1A1A1A icon outline */
--dim:#606060; /* #606060 */
--faint:#767676; /* between the icon's #929292 and #606060, to clear AA */
--accent:#2d5391; /* #2D5391 the deep screen blue reads better on white */
--accent2:#b06f10;
--ink:#ffffff;
--good:#2f7d4f;
--warn:#b06f10;
--bad:#b3402f;
--shadow:0 8px 28px rgba(45,83,145,.14);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;background:var(--bg);color:var(--fg);
font:14.5px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
display:grid;grid-template-rows:1fr auto;height:100vh;overflow:hidden;
}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
a{color:var(--accent)}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--raise);border-radius:6px}
::-webkit-scrollbar-track{background:transparent}
/* ---------- shell ---------- */
#shell{display:grid;grid-template-columns:290px 1fr;min-height:0;overflow:hidden}
#sidebar{
background:var(--panel);border-right:1px solid var(--line);
display:flex;flex-direction:column;min-height:0;
}
.brand{display:flex;align-items:center;gap:9px;padding:14px 14px 10px}
.brand .logo{
width:28px;height:28px;border-radius:7px;flex:none;
object-fit:cover;background:#fff;
}
.brand h1{font-size:15px;margin:0;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);flex:1}
.iconbtn{
width:30px;height:30px;border-radius:8px;display:grid;place-items:center;
color:var(--dim);flex:none;
}
.iconbtn:hover{background:var(--raise);color:var(--fg)}
.sidetools{display:flex;gap:6px;padding:0 12px 10px}
.sidetools button{
flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:8px;
padding:6px 8px;font-size:12.5px;color:var(--dim);
}
.sidetools button:hover{border-color:var(--accent);color:var(--fg)}
.searchwrap{padding:0 12px 8px}
input[type=search],input[type=text],input[type=number],select{
width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);
border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px;
}
input:focus,select:focus{outline:0;border-color:var(--accent)}
#feedlist{overflow-y:auto;padding:0 8px 12px;flex:1;min-height:0}
.feed{
display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:9px;
cursor:pointer;margin-bottom:1px;
}
.feed:hover{background:var(--panel2)}
.feed.sel{background:var(--raise)}
.art{
border-radius:7px;object-fit:cover;background:var(--raise);flex:none;
display:grid;place-items:center;color:var(--faint);font-weight:700;overflow:hidden;
}
.feed .art{width:38px;height:38px;font-size:14px}
.feed .txt{min-width:0;flex:1}
.feed .txt b{display:block;font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.feed .txt small{color:var(--faint);font-size:11.5px}
.badge{
background:var(--accent);color:var(--ink);border-radius:20px;padding:1px 7px;
font-size:11px;font-weight:700;flex:none;
}
.badge.zero{background:var(--raise);color:var(--faint)}
/* ---------- main ---------- */
#main{overflow-y:auto;min-height:0;scroll-behavior:smooth}
.wrap{max-width:1000px;margin:0 auto;padding:18px 22px 40px}
.fhead{display:flex;gap:18px;margin-bottom:18px}
.fhead .art{width:118px;height:118px;font-size:34px;box-shadow:var(--shadow)}
.fhead .meta{min-width:0;flex:1;display:flex;flex-direction:column}
.fhead h2{margin:0 0 3px;font-size:23px;line-height:1.2}
.fhead .sub{color:var(--dim);font-size:13px;margin-bottom:8px}
.fhead .sub a{color:var(--dim);text-decoration:none}
.fhead .sub a:hover{color:var(--accent)}
.acts{display:flex;gap:7px;flex-wrap:wrap;margin-top:auto}
.btn{
background:var(--panel2);border:1px solid var(--line);border-radius:8px;
padding:6px 12px;font-size:13px;
}
.btn:hover{border-color:var(--accent)}
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--ink);font-weight:600}
.btn.primary:hover{filter:brightness(1.08)}
.btn.danger:hover{border-color:var(--bad);color:var(--bad)}
.toolbar{
display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap;
position:sticky;top:0;background:var(--bg);padding:6px 0 8px;z-index:3;
}
.tabs{display:flex;gap:2px;background:var(--panel2);border-radius:9px;padding:3px}
.tabs button{padding:5px 12px;border-radius:7px;font-size:13px;color:var(--dim)}
.tabs button.on{background:var(--raise);color:var(--fg);font-weight:600}
.toolbar .grow{flex:1;min-width:150px;max-width:320px}
/* ---------- episodes ---------- */
.ep{
display:flex;gap:12px;padding:11px;border-radius:var(--r);
border:1px solid transparent;margin-bottom:3px;position:relative;
}
.ep:hover{background:var(--panel)}
.ep.playing{background:var(--panel);border-color:var(--accent)}
.ep .art{width:52px;height:52px;font-size:16px;cursor:pointer;position:relative}
.ep .art .ovl{
position:absolute;inset:0;display:grid;place-items:center;
background:rgba(0,0,0,.5);opacity:0;transition:opacity .12s;color:#fff;
}
.ep .art:hover .ovl,.ep.playing .art .ovl{opacity:1}
.ep .body{flex:1;min-width:0}
.ep .t{font-weight:600;font-size:14.5px;cursor:pointer;display:block}
.ep.read .t{color:var(--dim);font-weight:500}
.ep .line{display:flex;gap:9px;align-items:center;flex-wrap:wrap;color:var(--faint);font-size:12px;margin-top:2px}
.dot{width:3px;height:3px;border-radius:50%;background:var(--faint);flex:none}
.chip{
font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;font-weight:700;
padding:1px 6px;border-radius:5px;background:var(--raise);color:var(--dim);
}
.chip.done{background:color-mix(in srgb,var(--good) 16%,transparent);color:var(--good)}
.chip.error{background:color-mix(in srgb,var(--bad) 16%,transparent);color:var(--bad)}
.chip.pending{background:color-mix(in srgb,var(--warn) 16%,transparent);color:var(--warn)}
.chip.new{background:var(--accent);color:var(--ink)}
.ep .rowacts{display:flex;gap:2px;align-items:flex-start;opacity:0;transition:opacity .12s}
.ep:hover .rowacts,.ep.open .rowacts{opacity:1}
.notes{
margin-top:10px;padding:11px 13px;background:var(--panel2);border-radius:9px;
color:var(--dim);font-size:13.5px;overflow-wrap:anywhere;
}
.notes img{max-width:100%;height:auto;border-radius:6px}
.notes p:first-child{margin-top:0}.notes p:last-child{margin-bottom:0}
.dlbar{height:3px;background:var(--raise);border-radius:2px;overflow:hidden;margin-top:7px}
.dlbar i{display:block;height:100%;width:0;background:var(--accent);transition:width .25s}
.empty{color:var(--faint);text-align:center;padding:50px 0}
#more{display:block;width:100%;margin-top:10px}
/* ---------- player ---------- */
#player{
border-top:1px solid var(--line);background:var(--panel);
display:none;grid-template-columns:auto 1fr auto;gap:14px;align-items:center;
padding:9px 16px;box-shadow:0 -6px 24px rgba(6,10,16,.4);
}
#player.on{display:grid}
#pnow{display:flex;gap:11px;align-items:center;min-width:0;width:250px}
#pnow .art{width:44px;height:44px;font-size:13px}
#pnow .txt{min-width:0}
#pnow b{display:block;font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#pnow small{color:var(--faint);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}
#pmid{display:flex;flex-direction:column;gap:3px;min-width:0}
#pbtns{display:flex;gap:5px;align-items:center;justify-content:center}
#pbtns .iconbtn{width:34px;height:34px}
#pplay{
width:40px!important;height:40px!important;background:var(--fg);color:var(--bg);
border-radius:50%;
}
#pplay:hover{background:var(--accent);color:var(--ink)}
#seekrow{display:flex;gap:9px;align-items:center;font-variant-numeric:tabular-nums;font-size:11.5px;color:var(--faint)}
input[type=range]{
-webkit-appearance:none;appearance:none;height:4px;border-radius:3px;flex:1;
background:var(--raise);cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--accent);
}
input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radius:50%;background:var(--accent)}
#pright{display:flex;gap:8px;align-items:center}
#pright select{width:auto;padding:4px 6px;font-size:12px}
#vol{width:78px;flex:none}
/* ---------- overlays ---------- */
#modal{
position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;
place-items:center;z-index:50;padding:20px;
}
#modal.on{display:grid}
.card{
background:var(--panel);border:1px solid var(--line);border-radius:14px;
padding:20px;width:min(460px,100%);box-shadow:var(--shadow);max-height:88vh;overflow:auto;
}
.card h3{margin:0 0 14px;font-size:17px}
.field{display:grid;gap:4px;margin-bottom:12px}
.field label{font-size:12px;color:var(--dim)}
.field .hint{font-size:11.5px;color:var(--faint)}
.check{display:flex;gap:8px;align-items:center;font-size:13.5px;margin-bottom:9px}
.inline{display:flex;gap:6px;align-items:stretch}
.inline input{flex:1;min-width:0}
.inline .btn{white-space:nowrap;flex:none}
.check input{width:16px;height:16px;accent-color:var(--accent)}
.cardacts{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
#toasts{position:fixed;bottom:88px;right:18px;display:flex;flex-direction:column;gap:8px;z-index:60}
.toast{
background:var(--raise);border:1px solid var(--line);border-radius:9px;
padding:9px 13px;font-size:13px;box-shadow:var(--shadow);animation:in .18s;max-width:340px;
}
.toast.bad{border-color:var(--bad);color:var(--bad)}
@keyframes in{from{opacity:0;transform:translateY(6px)}}
#burger{display:none}
@media (max-width:820px){
#shell{grid-template-columns:1fr}
#sidebar{position:fixed;inset:0 auto 0 0;width:280px;z-index:40;transform:translateX(-100%);transition:transform .2s;box-shadow:var(--shadow)}
#sidebar.open{transform:none}
#burger{display:grid}
.fhead .art{width:78px;height:78px;font-size:24px}
.fhead h2{font-size:19px}
#pnow{width:auto;flex:1}
#pnow .txt small,#pright #vol{display:none}
.wrap{padding:14px}
}
</style>
</head>
<body>
<div id="shell">
<aside id="sidebar">
<div class="brand">
<img class="logo" src="data:image/jpeg;base64,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" alt="iPodderX" title="The original iPodderX icon, 2004"><h1>ipx</h1>
<button class="iconbtn" id="theme" title="Toggle theme"></button>
</div>
<div class="sidetools">
<button id="addFeed">+ Feed</button>
<button id="scanAll">Scan all</button>
<button id="opml" title="Import / export OPML">OPML</button>
</div>
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
<div id="feedlist"></div>
</aside>
<div id="main"><div class="wrap" id="content"></div></div>
</div>
<div id="player">
<div id="pnow">
<button class="iconbtn" id="burger" title="Feeds"></button>
<div id="partwrap"></div>
<div class="txt"><b id="ptitle"></b><small id="pfeed"></small></div>
</div>
<div id="pmid">
<div id="pbtns">
<button class="iconbtn" id="pback" title="Back 15s (←)">↺15</button>
<button class="iconbtn" id="pplay" title="Play/pause (space)"></button>
<button class="iconbtn" id="pfwd" title="Forward 30s (→)">30↻</button>
</div>
<div id="seekrow">
<span id="pcur">0:00</span>
<input type="range" id="seek" min="0" max="1000" value="0">
<span id="pdur">0:00</span>
</div>
</div>
<div id="pright">
<select id="rate" title="Speed">
<option value="0.8">0.8×</option><option value="1" selected>1×</option>
<option value="1.25">1.25×</option><option value="1.5">1.5×</option>
<option value="1.75">1.75×</option><option value="2">2×</option><option value="2.5">2.5×</option>
</select>
<input type="range" id="vol" min="0" max="100" value="100" title="Volume">
<button class="iconbtn" id="pclose" title="Close"></button>
</div>
</div>
<div id="modal"><div class="card" id="modalCard"></div></div>
<div id="toasts"></div>
<audio id="audio" preload="metadata"></audio>
<script>
'use strict';
const $ = (s,r=document)=>r.querySelector(s);
const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
const esc = s => (s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
async function api(url,opts){
const r = await fetch(url,{headers:{'Content-Type':'application/json'},...opts});
if(!r.ok) throw new Error(await r.text().catch(()=>r.status)||r.status);
return r.status===204?null:r.json().catch(()=>null);
}
// navigator.clipboard only exists in a secure context. Served over plain HTTP on a LAN
// address it is undefined, so fall back to the old selection-based copy.
async function copyText(text,btn){
const flash=ok=>{
if(!btn) return;
const was=btn.textContent;
btn.textContent=ok?'Copied':'Failed';
setTimeout(()=>btn.textContent=was,1300);
};
try{
if(navigator.clipboard&&window.isSecureContext){
await navigator.clipboard.writeText(text);
}else{
const ta=document.createElement('textarea');
ta.value=text; ta.setAttribute('readonly','');
ta.style.cssText='position:fixed;top:-1000px;opacity:0';
document.body.appendChild(ta);
ta.select(); ta.setSelectionRange(0,ta.value.length);
const ok=document.execCommand('copy');
ta.remove();
if(!ok) throw new Error('copy rejected');
}
flash(true);
}catch(e){
flash(false);
toast('Could not copy automatically — select the URL and copy it manually',true);
}
}
function toast(msg,bad){
const t=document.createElement('div');
t.className='toast'+(bad?' bad':''); t.textContent=msg;
$('#toasts').appendChild(t);
setTimeout(()=>{t.style.opacity=0;t.style.transition='opacity .3s';setTimeout(()=>t.remove(),320)},bad?6000:3200);
}
const clock = s => {
s=Math.max(0,Math.floor(s||0));
const h=Math.floor(s/3600),m=Math.floor(s%3600/60),x=s%60;
return h?`${h}:${String(m).padStart(2,'0')}:${String(x).padStart(2,'0')}`:`${m}:${String(x).padStart(2,'0')}`;
};
const ago = t => {
if(!t) return 'never';
const d=(Date.now()/1000)-t;
if(d<3600) return Math.max(1,Math.round(d/60))+'m ago';
if(d<86400) return Math.round(d/3600)+'h ago';
if(d<2592000) return Math.round(d/86400)+'d ago';
return new Date(t*1000).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'});
};
const dateOf = t => t?new Date(t*1000).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}):'';
const mb = n => n?(n/1048576).toFixed(0)+' MB':'';
const initials = s => (s||'?').replace(/[^A-Za-z0-9 ]/g,'').split(/\s+/).filter(Boolean).slice(0,2).map(w=>w[0]).join('').toUpperCase()||'?';
function artHTML(url,name,cls){
return url
? `<img class="art ${cls||''}" src="${esc(url)}" alt="" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'art ${cls||''}',textContent:'${esc(initials(name))}'}))">`
: `<div class="art ${cls||''}">${esc(initials(name))}</div>`;
}
/* ---------------- state ---------------- */
const S = {
feeds:[], feed:null, entries:[], total:0, offset:0, limit:50,
filter:'unread', q:'', open:new Set(), busy:new Set(),
};
const LIMIT = 50;
/* ---------------- feeds ---------------- */
async function loadFeeds(keepSel){
S.feeds = await api('/api/feeds');
renderFeeds();
if(!keepSel && !S.feed && S.feeds.length) selectFeed(S.feeds[0].id);
}
function renderFeeds(){
const q=$('#feedFilter').value.trim().toLowerCase();
const list=$('#feedlist'); list.innerHTML='';
const shown=S.feeds.filter(f=>!q||(f.title||f.id).toLowerCase().includes(q));
if(!shown.length){ list.innerHTML='<p class="empty" style="padding:20px 8px">No feeds.</p>'; return; }
for(const f of shown){
const el=document.createElement('div');
el.className='feed'+(S.feed===f.id?' sel':'');
el.innerHTML = artHTML(f.image,f.title||f.id)+
`<div class="txt"><b>${esc(f.title||f.id)}</b><small>${f.entries} eps · ${f.downloaded} saved</small></div>`+
`<span class="badge${f.unread?'':' zero'}">${f.unread}</span>`;
el.onclick=()=>{ selectFeed(f.id); $('#sidebar').classList.remove('open'); };
list.appendChild(el);
}
}
function selectFeed(id){
S.feed=id; S.offset=0; S.open.clear(); S.q='';
renderFeeds(); renderFeed(); loadEntries();
}
/* ---------------- feed page ---------------- */
function renderFeed(){
const f=S.feeds.find(x=>x.id===S.feed);
if(!f){ $('#content').innerHTML='<p class="empty">Add a feed to get started.</p>'; return; }
$('#content').innerHTML = `
<div class="fhead">
${artHTML(f.image,f.title||f.id)}
<div class="meta">
<h2>${esc(f.title||f.id)}</h2>
<div class="sub">${f.entries} episodes · ${f.downloaded} downloaded · checked ${ago(f.last_checked)}</div>
${f.last_error?`<div class="sub" style="color:var(--bad)">${esc(f.last_error)}</div>`:''}
<div class="acts">
<button class="btn primary" data-a="scan">Scan now</button>
<button class="btn" data-a="dl">Download latest…</button>
<button class="btn" data-a="read">Mark all read</button>
<button class="btn" data-a="settings">Settings</button>
<button class="btn danger" data-a="rm">Unsubscribe</button>
</div>
</div>
</div>
<div class="toolbar">
<div class="tabs">
${['unread','all','downloaded','flagged'].map(t=>
`<button data-f="${t}" class="${S.filter===t?'on':''}">${t[0].toUpperCase()+t.slice(1)}</button>`).join('')}
</div>
<input type="search" class="grow" id="epSearch" placeholder="Search episodes…" value="${esc(S.q)}">
<span style="color:var(--faint);font-size:12.5px" id="count"></span>
</div>
<div id="eps"></div>`;
$$('#content .acts .btn').forEach(b=>b.onclick=()=>feedAction(b.dataset.a,f));
$$('#content .tabs button').forEach(b=>b.onclick=()=>{S.filter=b.dataset.f;S.offset=0;renderFeed();loadEntries()});
let t; $('#epSearch').oninput=e=>{clearTimeout(t);t=setTimeout(()=>{S.q=e.target.value;S.offset=0;loadEntries()},250)};
}
async function feedAction(a,f){
if(a==='scan'){ toast('Scanning '+(f.title||f.id)+'…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({feed:f.id,force:true})}); }
if(a==='read'){ const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/read-all`,{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); loadEntries(); }
if(a==='rm') removeFeed(f);
if(a==='settings') settingsModal(f);
if(a==='dl') downloadLatestModal(f);
}
/* ---------------- episodes ---------------- */
async function loadEntries(append){
if(!S.feed) return;
const p=new URLSearchParams({offset:S.offset,limit:LIMIT,filter:S.filter});
if(S.q) p.set('q',S.q);
const r=await api(`/api/feeds/${encodeURIComponent(S.feed)}/entries?${p}`);
S.total=r.total;
S.entries = append ? S.entries.concat(r.entries) : r.entries;
renderEntries();
}
function renderEntries(){
const box=$('#eps'); if(!box) return;
const c=$('#count'); if(c) c.textContent=`${S.total} episode${S.total===1?'':'s'}`;
box.innerHTML='';
if(!S.entries.length){
box.innerHTML=`<p class="empty">${S.q?'Nothing matches that search.':'Nothing here yet — try Scan now.'}</p>`;
return;
}
for(const e of S.entries) box.appendChild(epEl(e));
if(S.entries.length < S.total){
const b=document.createElement('button');
b.className='btn'; b.id='more'; b.textContent=`Load more (${S.entries.length} of ${S.total})`;
b.onclick=()=>{S.offset+=LIMIT;loadEntries(true)};
box.appendChild(b);
}
}
function epEl(e){
const enc=e.enclosures[0];
const has=!!(enc&&enc.path);
const el=document.createElement('div');
el.className='ep'+(e.read?' read':'')+(S.open.has(e.guid)?' open':'')+
(player.guid===e.guid?' playing':'');
el.dataset.guid=e.guid;
const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join('');
const left = e.position>10 && e.duration ? `${clock(e.duration-e.position)} left` : (e.duration?clock(e.duration):'');
el.innerHTML=`
${artHTML(e.image||feedArt(),e.title,'')}
<div class="body">
<span class="t">${esc(e.title||'(untitled)')}</span>
<div class="line">
${e.read?'':'<span class="chip new">new</span>'}
${num?`<span>${num}</span><span class="dot"></span>`:''}
<span>${dateOf(e.published)}</span>
${left?'<span class="dot"></span><span>'+left+'</span>':''}
${enc?`<span class="dot"></span><span class="chip ${esc(enc.state)}">${has?'downloaded':esc(enc.state)}</span>`:''}
${enc&&enc.length?`<span>${mb(enc.length)}</span>`:''}
${e.flagged?'<span class="dot"></span><span>★ kept</span>':''}
</div>
${enc&&!has?`<div class="dlbar" data-bar="${enc.id}"><i></i></div>`:''}
${enc&&enc.last_error?`<div class="line" style="color:var(--bad)">${esc(enc.last_error)}</div>`:''}
</div>
<div class="rowacts">
${has?`<button class="iconbtn" data-a="play" title="Play">▶</button>`:
(enc?`<button class="iconbtn" data-a="get" title="Download">⤓</button>`:'')}
<button class="iconbtn" data-a="flag" title="${e.flagged?'Stop keeping':'Keep (never auto-delete)'}">${e.flagged?'★':'☆'}</button>
<button class="iconbtn" data-a="read" title="Mark ${e.read?'unread':'read'}">${e.read?'○':'●'}</button>
${has?`<button class="iconbtn" data-a="del" title="Delete file">🗑</button>`:''}
<button class="iconbtn" data-a="notes" title="Show notes">≡</button>
</div>`;
const art=$('.art',el);
if(art&&has) art.insertAdjacentHTML('beforeend','<span class="ovl">▶</span>');
if(art&&has) art.onclick=()=>play(e);
$('.t',el).onclick=()=>toggleNotes(e,el);
$$('.rowacts .iconbtn',el).forEach(b=>b.onclick=ev=>{ev.stopPropagation();epAction(b.dataset.a,e,el)});
if(S.open.has(e.guid)) showNotes(e,el);
return el;
}
function feedArt(){ const f=S.feeds.find(x=>x.id===S.feed); return f&&f.image; }
function toggleNotes(e,el){
if(S.open.has(e.guid)){ S.open.delete(e.guid); const n=$('.notes',el); if(n)n.remove(); el.classList.remove('open'); }
else { S.open.add(e.guid); showNotes(e,el); el.classList.add('open'); }
}
function showNotes(e,el){
if($('.notes',el)) return;
const d=document.createElement('div');
d.className='notes';
// Sanitized server-side with ammonia before it ever reaches the browser.
d.innerHTML=(e.description&&e.description.trim())||'<em>No show notes.</em>';
if(e.link) d.insertAdjacentHTML('beforeend',`<p><a href="${esc(e.link)}" target="_blank" rel="noopener noreferrer">Open original ↗</a></p>`);
$('.body',el).appendChild(d);
}
async function epAction(a,e,el){
const enc=e.enclosures[0];
const path=`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}`;
try{
if(a==='play') play(e);
if(a==='notes') toggleNotes(e,el);
if(a==='flag'){ e.flagged=!e.flagged; await api(path+'/flags',{method:'POST',body:JSON.stringify({flagged:e.flagged})}); el.replaceWith(epEl(e)); }
if(a==='read'){ e.read=!e.read; await api(path+'/flags',{method:'POST',body:JSON.stringify({read:e.read})}); el.replaceWith(epEl(e)); loadFeeds(true); }
if(a==='get'){
if(!enc) return;
S.busy.add(enc.id);
await api(`/api/enclosures/${enc.id}/download`,{method:'POST'});
toast('Queued: '+(e.title||'episode'));
}
if(a==='del'){
if(!confirm('Delete the downloaded file?\n\nThe episode stays listed and will not be downloaded again automatically.')) return;
await api(`/api/enclosures/${enc.id}`,{method:'DELETE'});
toast('Deleted'); loadEntries(); loadFeeds(true);
}
}catch(err){ toast(err.message,true); }
}
/* ---------------- player ---------------- */
const audio=$('#audio');
const player={guid:null,feed:null,entry:null,saveAt:0,marked:false};
// Marked read when an episode has actually been listened to -- at the end, or past 90%.
// NOT on play: doing that made the episode vanish from the default Unread list the instant
// you pressed play, which looks exactly like it went missing.
function markPlayed(){
if(!player.guid||player.marked) return;
player.marked=true;
const e=player.entry;
if(!e||e.read) return;
e.read=true;
api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`,
{method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)).catch(()=>{});
}
function play(e){
const enc=e.enclosures.find(x=>x.path);
if(!enc){ toast('Not downloaded yet',true); return; }
const resuming = player.guid===e.guid;
if(!resuming){
player.guid=e.guid; player.feed=e.feed_id; player.entry=e;
audio.src=`/media/${enc.id}`;
audio.currentTime=0;
if(e.position>5) audio.addEventListener('loadedmetadata',()=>{audio.currentTime=e.position},{once:true});
$('#partwrap').innerHTML=artHTML(e.image||feedArt(),e.title);
$('#ptitle').textContent=e.title||'(untitled)';
const f=S.feeds.find(x=>x.id===e.feed_id);
$('#pfeed').textContent=f?(f.title||f.id):'';
$('#player').classList.add('on');
mediaSession(e,f);
player.marked=false;
}
audio.play().catch(err=>toast('Playback failed: '+err.message,true));
renderEntries();
}
function mediaSession(e,f){
if(!('mediaSession' in navigator)) return;
navigator.mediaSession.metadata=new MediaMetadata({
title:e.title||'', artist:f?(f.title||f.id):'', album:f?(f.title||''):'',
artwork:(e.image||(f&&f.image))?[{src:e.image||f.image,sizes:'512x512'}]:[],
});
const h={play:()=>audio.play(),pause:()=>audio.pause(),
seekbackward:()=>audio.currentTime-=15,seekforward:()=>audio.currentTime+=30};
for(const k in h){ try{navigator.mediaSession.setActionHandler(k,h[k])}catch{} }
}
audio.addEventListener('timeupdate',()=>{
const d=audio.duration||player.entry?.duration||0;
$('#pcur').textContent=clock(audio.currentTime);
$('#pdur').textContent=clock(d);
if(d) $('#seek').value=String(Math.round(audio.currentTime/d*1000));
// Persist roughly every 10s so a reload resumes where you were.
if(player.guid && audio.currentTime-player.saveAt>10){ savePos(); }
if(d && audio.currentTime/d >= 0.9) markPlayed();
});
function savePos(){
if(!player.guid) return;
player.saveAt=audio.currentTime;
if(player.entry) player.entry.position=Math.floor(audio.currentTime);
navigator.sendBeacon?.(
`/api/entries/${encodeURIComponent(player.feed)}/${encodeURIComponent(player.guid)}/position`,
new Blob([JSON.stringify({secs:Math.floor(audio.currentTime)})],{type:'application/json'}));
}
audio.addEventListener('pause',savePos);
audio.addEventListener('ended',()=>{savePos();markPlayed();$('#pplay').textContent='▶'});
audio.addEventListener('play',()=>$('#pplay').textContent='❚❚');
audio.addEventListener('pause',()=>$('#pplay').textContent='▶');
window.addEventListener('beforeunload',savePos);
$('#pplay').onclick=()=>audio.paused?audio.play():audio.pause();
$('#pback').onclick=()=>audio.currentTime-=15;
$('#pfwd').onclick=()=>audio.currentTime+=30;
$('#seek').oninput=e=>{const d=audio.duration;if(d)audio.currentTime=d*e.target.value/1000};
$('#rate').onchange=e=>{audio.playbackRate=+e.target.value;localStorage.setItem('ipx.rate',e.target.value)};
$('#vol').oninput=e=>{audio.volume=e.target.value/100;localStorage.setItem('ipx.vol',e.target.value)};
$('#pclose').onclick=()=>{savePos();audio.pause();audio.removeAttribute('src');player.guid=null;$('#player').classList.remove('on');renderEntries()};
(function restore(){
const r=localStorage.getItem('ipx.rate'), v=localStorage.getItem('ipx.vol');
if(r){$('#rate').value=r;audio.playbackRate=+r}
if(v){$('#vol').value=v;audio.volume=v/100}
})();
document.addEventListener('keydown',ev=>{
if(/^(INPUT|TEXTAREA|SELECT)$/.test(ev.target.tagName)) return;
if(ev.key===' '&&player.guid){ev.preventDefault();audio.paused?audio.play():audio.pause()}
else if(ev.key==='ArrowLeft'&&player.guid){audio.currentTime-=15}
else if(ev.key==='ArrowRight'&&player.guid){audio.currentTime+=30}
else if(ev.key==='/'){ev.preventDefault();$('#epSearch')?.focus()}
else if(ev.key==='Escape'){closeModal();$('#sidebar').classList.remove('open')}
});
/* ---------------- modals ---------------- */
function openModal(html){ $('#modalCard').innerHTML=html; $('#modal').classList.add('on'); }
function closeModal(){ $('#modal').classList.remove('on'); }
$('#modal').onclick=e=>{ if(e.target.id==='modal') closeModal(); };
$('#addFeed').onclick=()=>{
openModal(`<h3>Add a feed</h3>
<div class="field"><label>Feed URL</label><input type="text" id="nurl" placeholder="https://example.com/rss"></div>
<div class="field"><label>Folder (optional)</label><input type="text" id="nfolder" placeholder="Defaults to the feed title"></div>
<div class="field"><label>Keywords (optional, comma separated)</label>
<input type="text" id="nkw"><span class="hint">Only episodes matching a keyword are downloaded.</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="nsave">Add feed</button></div>`);
$('#nurl').focus();
$('#nsave').onclick=async()=>{
const url=$('#nurl').value.trim(); if(!url) return;
$('#nsave').textContent='Adding…'; $('#nsave').disabled=true;
try{
const r=await api('/api/feeds',{method:'POST',body:JSON.stringify({
url, folder:$('#nfolder').value.trim()||null,
keywords:$('#nkw').value.split(',').map(s=>s.trim()).filter(Boolean)})});
closeModal(); toast(r.existing?`Already subscribed as ${r.id}`:`Added ${r.id}`);
await loadFeeds(true); selectFeed(r.id);
}catch(e){ toast(e.message,true); $('#nsave').textContent='Add feed'; $('#nsave').disabled=false; }
};
};
function settingsModal(f){
openModal(`<h3>${esc(f.title||f.id)}</h3>
<div class="field"><label>Download folder</label>
<input type="text" id="sfolder" value="${esc(f.folder||'')}" placeholder="${esc(f.title||f.id)}"></div>
<div class="field"><label>Keywords</label>
<input type="text" id="skw" value="${esc(f.keywords.join(', '))}">
<span class="hint">Comma separated. Empty takes everything.</span></div>
<div class="field"><label>Max new downloads per scan</label>
<input type="number" id="smax" min="0" value="${f.max_new_per_check??''}">
<span class="hint">Blank means no limit. The rest wait for the next scan.</span></div>
<label class="check"><input type="checkbox" id="sauto" ${f.auto_download?'checked':''}> Download new episodes automatically</label>
<label class="check"><input type="checkbox" id="sexp" ${f.allow_explicit?'checked':''}> Allow episodes marked explicit</label>
<div class="field"><label>Feed URL</label>
<div class="inline">
<input type="text" id="surl" value="${esc(f.url)}" spellcheck="false">
<button type="button" class="btn" id="scopy">Copy</button>
</div>
<span class="hint">Editing this keeps every episode and download — handy when an auth token
in the URL is rotated. The feed is re-checked from scratch on the next scan.</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="ssave">Save</button></div>`);
$('#scopy').onclick=()=>copyText($('#surl').value,$('#scopy'));
$('#ssave').onclick=async()=>{
const max=$('#smax').value;
try{
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({
url:$('#surl').value.trim(),
folder:$('#sfolder').value.trim()||null,
keywords:$('#skw').value.split(',').map(s=>s.trim()).filter(Boolean),
max_new_per_check:max===''?null:Number(max),
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked})});
closeModal(); toast('Saved — applies on the next scan');
await loadFeeds(true); renderFeed(); loadEntries();
}catch(e){ toast(e.message,true); }
};
}
function downloadLatestModal(f){
openModal(`<h3>Download latest episodes</h3>
<div class="field"><label>How many of the newest undownloaded episodes?</label>
<input type="number" id="dcount" min="1" max="100" value="5">
<span class="hint">Queued immediately, ignoring the per-scan limit.</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="dgo">Download</button></div>`);
$('#dgo').onclick=async()=>{
const n=Number($('#dcount').value)||5;
try{
const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/download-latest`,
{method:'POST',body:JSON.stringify({count:n})});
closeModal(); toast(`Queued ${r.queued} episode${r.queued===1?'':'s'}`);
}catch(e){ toast(e.message,true); }
};
}
function removeFeed(f){
openModal(`<h3>Unsubscribe?</h3>
<p style="color:var(--dim)">Removes <b>${esc(f.title||f.id)}</b> from your feeds.
Downloaded files and history are kept, so re-adding it will not pull the back catalogue again.</p>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn danger" id="rgo">Unsubscribe</button></div>`);
$('#rgo').onclick=async()=>{
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'DELETE'});
closeModal(); toast('Unsubscribed'); S.feed=null;
await loadFeeds(); if(!S.feeds.length) renderFeed();
};
}
$('#opml').onclick=()=>{
openModal(`<h3>OPML</h3>
<p style="color:var(--dim);font-size:13.5px">Move subscriptions between podcast apps.</p>
<div class="cardacts" style="justify-content:flex-start">
<a class="btn" href="/api/opml" download="ipx-subscriptions.opml">Export</a>
</div>
<div class="field" style="margin-top:16px"><label>Import: paste OPML</label>
<textarea id="opmlText" rows="6" style="width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:8px;font:12px monospace"></textarea></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Close</button>
<button class="btn primary" id="oimp">Import</button></div>`);
$('#oimp').onclick=async()=>{
try{
const r=await api('/api/opml',{method:'POST',body:JSON.stringify({xml:$('#opmlText').value})});
closeModal(); toast(`Imported ${r.added} feed(s)`); loadFeeds(true);
}catch(e){ toast(e.message,true); }
};
};
$('#scanAll').onclick=async()=>{ toast('Scanning all feeds…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({force:true})}); };
$('#feedFilter').oninput=renderFeeds;
$('#burger').onclick=()=>$('#sidebar').classList.toggle('open');
$('#theme').onclick=()=>{
const cur=document.documentElement.dataset.theme==='light'?'dark':'light';
document.documentElement.dataset.theme=cur; localStorage.setItem('ipx.theme',cur);
};
if(localStorage.getItem('ipx.theme')) document.documentElement.dataset.theme=localStorage.getItem('ipx.theme');
/* ---------------- live events ---------------- */
let sse;
function connect(){
sse=new EventSource('/api/events');
sse.onmessage=m=>{
let ev; try{ ev=JSON.parse(m.data) }catch{ return }
if(ev.ev==='progress'){
const pct=ev.total?ev.done/ev.total*100:0;
$$('.dlbar').forEach(b=>{ /* progress applies to whatever is downloading now */
if(b.dataset.bar) b.firstElementChild.style.width=pct+'%';
});
$('#count') && ($('#count').textContent=`downloading ${ev.file}${pct.toFixed(0)}%`);
}
else if(ev.ev==='download_done'){ toast('Downloaded '+ev.path.split('/').pop()); loadEntries(); loadFeeds(true); }
else if(ev.ev==='download_error'){ toast('Download failed: '+ev.msg,true); loadEntries(); }
else if(ev.ev==='feed_done'){ if(ev.new) toast(`${ev.feed}: ${ev.new} new`); loadFeeds(true); if(ev.feed===S.feed) loadEntries(); }
else if(ev.ev==='feed_error'){ toast(ev.feed+': '+ev.msg,true); loadFeeds(true); }
else if(ev.ev==='scan_done'){ loadFeeds(true); if(S.feed) loadEntries(); }
};
sse.onerror=()=>{ sse.close(); setTimeout(connect,4000); };
}
connect();
loadFeeds();
</script>
</body>
</html>