Files
ipodderx-rs/web/index.html
rays e05ea62051 Use the original iPodderX icon instead of a placeholder
Recovered from the Internet Archive's capture of ipodderx.com: the icon
was never in either repo, since only the Python engine was open-sourced
and the .icns lived in the Cocoa bundle. Kept as a file in web/ and
embedded as a data URI for the sidebar mark and the favicon.

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

766 lines
60 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>
:root {
--bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --raise:#262b36;
--line:#2b313c; --fg:#e8eaf0; --dim:#98a1b3; --faint:#6b7385;
--accent:#6ea8fe; --accent2:#8b7cf6; --good:#4ade80; --bad:#f87171; --warn:#fbbf24;
--shadow:0 8px 28px rgba(0,0,0,.45);
--r:10px;
}
:root[data-theme="light"] {
--bg:#f6f7f9; --panel:#fff; --panel2:#f0f2f6; --raise:#e6e9ef;
--line:#dde1e9; --fg:#12151c; --dim:#5b6474; --faint:#8b93a4;
--shadow:0 8px 28px rgba(20,25,40,.12);
}
*{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:#0b0e14;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:#0b0e14;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:rgba(74,222,128,.14);color:var(--good)}
.chip.error{background:rgba(248,113,113,.14);color:var(--bad)}
.chip.pending{background:rgba(251,191,36,.13);color:var(--warn)}
.chip.new{background:var(--accent);color:#0b0e14}
.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(0,0,0,.28);
}
#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:#0b0e14}
#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>