Files
ipodderx-rs/web/index.html
rays 8f5e2749ff Sum a subscription's counts from the feeds it holds
An OPML folder has no entries of its own, so its row always showed zero
unread however much was waiting inside. Summed from every feed it holds
-- not just the ones a filter left showing, so the count doesn't move as
you type -- with the badge capped at 999+ so a four-digit number doesn't
eat the title beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
2026-09-10 18:42:42 +00:00

1276 lines
106 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>iPodderX</title>
<link rel="icon" href="data:image/png;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:30px;height:30px;flex:none;object-fit:contain;
}
.brand h1{font-size:16px;margin:0;font-weight:650;letter-spacing:-.01em;color:var(--fg);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)}
.feed.child{margin-left:16px}
.chev{
flex:none;width:18px;height:18px;display:grid;place-items:center;border-radius:4px;
color:var(--faint);font-size:11px;transition:transform .12s;
}
.chev:empty{pointer-events:none}
.chev:not(:empty):hover{background:var(--raise);color:var(--fg)}
.chev.open{transform:rotate(90deg)}
.childlist{display:grid;gap:4px;margin-top:10px}
.childrow{
display:flex;gap:10px;align-items:center;padding:7px 9px;border:1px solid var(--line);
border-radius:9px;cursor:pointer;background:var(--panel);
}
.childrow:hover{border-color:var(--accent)}
.childrow .art{width:32px;height:32px;font-size:12px}
.childrow .txt{flex:1;min-width:0}
.childrow .txt b{display:block;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tag{
font-size:10px;text-transform:uppercase;letter-spacing:.04em;font-weight:700;
padding:1px 5px;border-radius:4px;background:var(--raise);color:var(--warn);flex:none;
}
.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:34px;height:34px;font-size:13px}
.feed .txt{min-width:0;flex:1}
.feed .txt b{display:block;font-weight:600;font-size:13.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.feed .txt small{display:block;color:var(--faint);font-size:11.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.badge{
background:var(--accent);color:var(--ink);border-radius:20px;padding:1px 7px;
font-size:11px;font-weight:700;flex:none;min-width:26px;text-align:center;
}
.badge.zero{background:var(--raise);color:var(--faint)}
/* ---------- main ---------- */
#main{overflow:hidden;min-height:0;display:flex;flex-direction:column}
.wrap{flex:1;min-height:0;display:flex;flex-direction:column}
.wrap.plain{overflow-y:auto;padding:18px 22px 40px;display:block}
#content{display:flex;flex-direction:column;min-height:0;flex:1}
#content>.fhead,#content>.toolbar{padding-left:20px;padding-right:20px;flex:none}
#content>.fhead{padding-top:16px}
/* Three panes, as the original had: feeds beside, items above, the item below. */
#split{display:grid;grid-template-rows:minmax(90px,var(--listh,40%)) 7px 1fr;flex:1;min-height:0}
#list{overflow-y:auto;padding:0 14px 10px}
#grab{cursor:row-resize;background:var(--line)}
#grab:hover{background:var(--accent)}
#detail{overflow-y:auto;padding:16px 20px 28px;background:var(--panel);border-top:1px solid var(--line)}
.dt{font-size:18px;font-weight:650;margin:0 0 5px;line-height:1.3}
.dmeta{color:var(--faint);font-size:12.5px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
.dmeta .btn{padding:3px 9px;font-size:12px}
.dbody{font-size:14.5px;line-height:1.65;overflow-wrap:anywhere;color:var(--fg)}
.dbody img{max-width:100%;height:auto;border-radius:6px}
.dbody a{color:var(--accent)}
.dbody pre{overflow-x:auto;background:var(--panel2);padding:10px;border-radius:8px}
.encbox{
display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;
padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel2);
}
.encbox audio{flex:1;min-width:200px;margin:0}
.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:11px;padding:8px 10px;border-radius:var(--r);align-items:center;
border:1px solid transparent;margin-bottom:2px;position:relative;cursor:pointer;
}
.ep.sel{background:var(--raise);border-color:var(--line)}
.ep .art{width:34px;height:34px;font-size:12px}
.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}
.dlbar{height:3px;background:transparent;border-radius:2px;overflow:hidden;margin-top:7px}
.dlbar.live{background:var(--raise)}
.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.wide{width:min(1000px,100%)}
#logbox{
background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:10px 12px;
height:min(60vh,520px);overflow:auto;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
}
#logbox .l{display:flex;gap:9px;white-space:pre-wrap;overflow-wrap:anywhere}
#logbox time{color:var(--faint);flex:none}
#logbox .lv{flex:none;width:42px;font-weight:700}
#logbox .lv.ERROR{color:var(--bad)} #logbox .lv.WARN{color:var(--warn)}
#logbox .lv.INFO{color:var(--accent)} #logbox .lv.DEBUG,#logbox .lv.TRACE{color:var(--faint)}
#logbox .tg{color:var(--faint);flex:none}
.logbar{display:flex;gap:8px;align-items:center;margin-bottom:9px;flex-wrap:wrap}
.logbar .grow{flex:1;min-width:120px}
.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}
.inline select{flex:none;width:auto}
.inline input[type=number]{flex:none;width:90px}
.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/png;base64,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" alt="iPodderX" title="The original iPodderX icon, 2004"><h1>iPodderX</h1>
<button class="iconbtn" id="prefs" title="Settings"></button>
<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>
<button id="logs" title="Daemon and web log">Log</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:'', sel:null, busy:new Set(),
};
const LIMIT = 50;
/* ---------------- feeds ---------------- */
async function loadFeeds(keepSel){
S.feeds = await api('/api/feeds');
api('/api/settings').then(g=>{globalEvery=g.every_mins;globalMax=g.max_new_per_check}).catch(()=>{});
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; }
// Feeds from a subscribed OPML sit under it, so the group reads as one thing.
const byId=Object.fromEntries(shown.map(f=>[f.id,f]));
const order=[];
for(const f of shown){
if(f.group && byId[f.group]) continue; // drawn under its parent instead
order.push([f,0]);
// A subscription can hold dozens of feeds, so a folder starts closed. Searching
// opens them all, or matches inside a closed folder would be invisible.
if(expanded.has(f.id) || q) for(const c of shown) if(c.group===f.id) order.push([c,1]);
}
for(const [f,depth] of order){
const kids=shown.filter(c=>c.group===f.id).length;
// A subscription holds no entries itself, so its counts are the sum of what is inside --
// taken from every feed it holds, not just the ones a filter left showing.
const mine=S.feeds.filter(c=>c.group===f.id);
const sum=k=>mine.reduce((n,c)=>n+(c[k]||0),0);
const [unread,eps,saved]=mine.length
? [sum('unread'),sum('entries'),sum('downloaded')]
: [f.unread,f.entries,f.downloaded];
const el=document.createElement('div');
el.className='feed'+(S.feed===f.id?' sel':'')+(depth?' child':'')+(kids?' group':'');
const open = kids && (expanded.has(f.id) || q);
el.innerHTML =
`<span class="chev${open?' open':''}"${kids?' title="Show or hide the feeds inside"':''}>${kids?'▶':''}</span>`+
artHTML(f.image,f.title||f.id)+
`<div class="txt"><b>${esc(f.title||f.id)}</b><small>`+
(mine.length?`${mine.length} feed${mine.length===1?'':'s'} · ${saved} saved`
:`${eps} eps · ${saved} saved`)+
`</small></div>`+
(f.orphaned?'<span class="tag" title="No longer listed in the OPML, kept because it has downloads">gone</span>':'')+
`<span class="badge${unread?'':' zero'}" title="${unread} unread">${unread>999?'999+':unread}</span>`;
el.onclick=()=>{ selectFeed(f.id); $('#sidebar').classList.remove('open'); };
if(kids) $('.chev',el).onclick=ev=>{ ev.stopPropagation(); toggleGroup(f.id); };
list.appendChild(el);
}
}
function selectFeed(id){
S.feed=id; S.offset=0; S.sel=null; 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; }
$('#main .wrap').classList.remove('plain');
const kids=S.feeds.filter(c=>c.group===f.id);
if(kids.length){ $('#main .wrap').classList.add('plain'); renderGroup(f,kids); 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)}
· every ${everyText(f.every_mins)}${f.next_check?` · next ${due(f.next_check)}`:''}</div>
${f.last_error?`<div class="sub" style="color:var(--bad)">${esc(f.last_error)}</div>`:''}
${f.orphaned?`<div class="sub" style="color:var(--warn)">This feed is no longer listed in its
OPML subscription. It was kept rather than removed because it has downloaded episodes.</div>`:''}
${f.group?`<div class="sub">From the OPML subscription <b>${esc(f.group)}</b></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>
`;
const pane=document.createElement('div');
pane.id='split';
pane.innerHTML='<div id="list"><div id="eps"></div></div><div id="grab"></div><div id="detail"></div>';
$('#content').appendChild(pane);
pane.style.setProperty('--listh', localStorage.getItem('ipx.listh') || '40%');
dragSplit();
showDetail(null);
$$('#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)};
}
/// An OPML subscription's page lists the feeds inside it rather than episodes, but keeps
/// every action a normal feed has -- it is still an ordinary feed entry underneath.
function renderGroup(f,kids){
const unread=kids.reduce((n,c)=>n+c.unread,0);
const saved=kids.reduce((n,c)=>n+c.downloaded,0);
const gone=kids.filter(c=>c.orphaned).length;
$('#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">OPML subscription · ${kids.length} feed${kids.length===1?'':'s'}
· ${unread} unread · ${saved} downloaded · checked ${ago(f.last_checked)}
· every ${everyText(f.every_mins)}</div>
${f.last_error?`<div class="sub" style="color:var(--bad)">${esc(f.last_error)}</div>`:''}
${gone?`<div class="sub" style="color:var(--warn)">${gone} feed${gone===1?' is':'s are'} no longer
listed in this OPML but kept because ${gone===1?'it has':'they have'} downloads.</div>`:''}
<div class="acts">
<button class="btn primary" data-a="scan">Re-read OPML</button>
<button class="btn" data-a="settings">Settings</button>
<button class="btn danger" data-a="rm">Unsubscribe</button>
</div>
</div>
</div>
<div class="toolbar">
<input type="search" class="grow" id="kidSearch" placeholder="Search these feeds…">
<span style="color:var(--faint);font-size:12.5px">${esc(f.url)}</span>
</div>
<div class="childlist" id="kidlist"></div>`;
$$('#content .acts .btn').forEach(b=>b.onclick=()=>feedAction(b.dataset.a,f));
const draw=()=>{
const q=($('#kidSearch').value||'').trim().toLowerCase();
const box=$('#kidlist'); box.innerHTML='';
const rows=kids.filter(c=>!q||(c.title||c.id).toLowerCase().includes(q));
if(!rows.length){ box.innerHTML='<p class="empty">Nothing matches.</p>'; return; }
for(const c of rows){
const el=document.createElement('div');
el.className='childrow';
el.innerHTML = artHTML(c.image,c.title||c.id)+
`<div class="txt"><b>${esc(c.title||c.id)}</b>`+
`<small class="meta">${c.entries} eps · ${c.downloaded} downloaded`+
(c.last_error?` · <span style="color:var(--bad)">error</span>`:'')+`</small></div>`+
(c.orphaned?'<span class="tag">gone</span>':'')+
`<span class="badge${c.unread?'':' zero'}">${c.unread}</span>`;
el.onclick=()=>selectFeed(c.id);
box.appendChild(el);
}
};
$('#kidSearch').oninput=draw;
draw();
}
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){
// An item may carry several files. The row summarises the one you would act on --
// the playable one, else anything already downloaded, else the first -- and says how
// many others there are; the pane below lists them all.
const enc=e.enclosures.find(isPlayable) || e.enclosures.find(x=>x.path) || e.enclosures[0];
const has=!!(enc&&enc.path);
const playable=isPlayable(enc);
const others=e.enclosures.length-1;
const el=document.createElement('div');
el.className='ep'+(e.read?' read':'')+(S.sel===e.guid?' sel':'')+
(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':(enc.state==='skipped'?kindOf(enc):esc(enc.state))}</span>`:''}
${enc&&enc.length?`<span>${mb(enc.length)}</span>`:''}
${others>0?`<span class="dot"></span><span>+${others} more file${others===1?'':'s'}</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&&enc.state==='error'
?`<div class="line" style="color:var(--bad)">${esc(enc.last_error)}</div>`:''}
</div>
<div class="rowacts">
${playable?`<button class="iconbtn" data-a="play" title="Play">▶</button>`:
(enc&&!has?`<button class="iconbtn" data-a="get" title="Download this ${
enc.state==='skipped'?kindOf(enc):'file'}">⤓</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>`:''}
</div>`;
const art=$('.art',el);
if(art&&playable){
art.insertAdjacentHTML('beforeend','<span class="ovl">▶</span>');
art.onclick=ev=>{ ev.stopPropagation(); play(e); };
}
el.onclick=()=>selectEntry(e);
$$('.rowacts .iconbtn',el).forEach(b=>b.onclick=ev=>{ev.stopPropagation();epAction(b.dataset.a,e,el)});
return el;
}
/// Whether the browser can play it. Having a file is not the same as being playable:
/// blog feeds put article images in enclosures, and an <audio> element pointed at a JPEG
/// is just a broken player.
function isPlayable(x){
if(!x || !x.path) return false;
const m=(x.mime||'').toLowerCase();
if(m.startsWith('audio/')||m.startsWith('video/')) return true;
if(m) return false;
// No declared type: fall back to the file's extension.
return /\.(mp3|m4a|m4b|aac|ogg|oga|opus|flac|wav|mp4|m4v|mov|webm|mkv)$/i
.test((x.path||x.url||'').split('?')[0]);
}
/// What an enclosure is, for a row that is not an episode: "image", "pdf", "document".
function kindOf(enc){
const m=(enc.mime||'').toLowerCase();
if(m.startsWith('image/')) return 'image';
if(m.startsWith('video/')) return 'video';
if(m.startsWith('audio/')) return 'audio';
if(m.includes('pdf')) return 'pdf';
if(m.includes('torrent')) return 'torrent';
const ext=(enc.url||'').split('?')[0].split('.').pop();
return (ext && ext.length<=5) ? ext.toLowerCase() : 'file';
}
function feedArt(){ const f=S.feeds.find(x=>x.id===S.feed); return f&&f.image; }
/// Selecting an item shows it in the pane below, rather than expanding the row.
function selectEntry(e){
S.sel=e.guid;
$$('#eps .ep').forEach(x=>x.classList.toggle('sel', x.dataset.guid===e.guid));
showDetail(e);
const d=$('#detail'); if(d) d.scrollTop=0;
}
/// Drag the divider between the item list and the item text.
function dragSplit(){
const grab=$('#grab'), pane=$('#split');
if(!grab||!pane) return;
const move=ev=>{
const box=pane.getBoundingClientRect();
const pct=Math.min(80,Math.max(12,((ev.clientY-box.top)/box.height)*100));
pane.style.setProperty('--listh',pct.toFixed(1)+'%');
};
const stop=()=>{
document.removeEventListener('mousemove',move);
document.removeEventListener('mouseup',stop);
document.body.style.userSelect='';
try{ localStorage.setItem('ipx.listh', pane.style.getPropertyValue('--listh')); }catch{}
};
grab.onmousedown=ev=>{
ev.preventDefault();
document.body.style.userSelect='none';
document.addEventListener('mousemove',move);
document.addEventListener('mouseup',stop);
};
}
/// The item's text and its enclosures, in the pane below the list.
function showDetail(e){
const box=$('#detail'); if(!box) return;
if(!e){ box.innerHTML='<p class="empty">Pick an episode to read it.</p>'; return; }
const f=S.feeds.find(x=>x.id===e.feed_id);
const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join('');
box.innerHTML=`
<h3 class="dt">${esc(e.title||'(untitled)')}</h3>
<div class="dmeta">
${f?`<span>${esc(f.title||f.id)}</span><span class="dot"></span>`:''}
${num?`<span>${num}</span><span class="dot"></span>`:''}
<span>${dateOf(e.published)}</span>
${e.duration?`<span class="dot"></span><span>${clock(e.duration)}</span>`:''}
<button class="btn" data-a="read">Mark ${e.read?'unread':'read'}</button>
<button class="btn" data-a="flag">${e.flagged?'\u2605 Kept':'\u2606 Keep'}</button>
${e.link?`<a class="btn" href="${esc(e.link)}" target="_blank" rel="noopener noreferrer">Open original \u2197</a>`:''}
</div>
<div class="dbody">${(e.description&&e.description.trim())||'<em>No show notes.</em>'}</div>
${e.enclosures.map(encBox).join('')}`;
// description was sanitized server-side with ammonia before it ever reached here
$$('button[data-a]',box).forEach(b=>
b.onclick=()=>epAction(b.dataset.a,e,null,b.dataset.enc?Number(b.dataset.enc):null));
for(const x of e.enclosures){
const a=$(`#audio-${x.id}`,box);
if(a) a.onplay=()=>play(e);
}
}
/// One enclosure: a player when the file is here, otherwise what it is and a way to get it.
function encBox(x){
const size=x.length?mb(x.length):'';
if(x.path && !isPlayable(x)){
// On disk, but not audio or video: view it, keep it, or remove it -- no player.
return `<div class="encbox">
<span class="chip done">${esc(kindOf(x))}</span>
<span class="meta" style="flex:1">downloaded${size?' \u00b7 '+size:''}</span>
<a class="btn" href="/media/${x.id}" target="_blank" rel="noopener noreferrer">View</a>
<a class="btn" href="/media/${x.id}" download>Save</a>
<button class="btn danger" data-a="del" data-enc="${x.id}">Delete file</button>
</div>`;
}
if(x.path){
return `<div class="encbox">
<audio id="audio-${x.id}" controls preload="none" src="/media/${x.id}"></audio>
<span class="meta">${size}</span>
<a class="btn" href="/media/${x.id}" download>Save</a>
<button class="btn danger" data-a="del" data-enc="${x.id}">Delete file</button>
</div>`;
}
// Nothing on disk. For an image or a PDF you usually just want to look at it, so link
// straight to the publisher's copy in a new tab -- no download, and nothing proxied
// through here, which would make ipx a fetch-anything relay.
const viewable = !isPlayable(x) && x.state !== 'pending';
return `<div class="encbox">
<span class="chip ${esc(x.state)}">${x.state==='skipped'?kindOf(x):esc(x.state)}</span>
<span class="meta" style="flex:1">${esc(kindOf(x))}${size?' \u00b7 '+size:''}</span>
${x.state==='error'&&x.last_error?`<span class="err">${esc(x.last_error)}</span>`:''}
${viewable?`<a class="btn" href="${esc(x.url)}" target="_blank" rel="noopener noreferrer">View</a>`:''}
<button class="btn" data-a="get" data-enc="${x.id}">Download</button>
</div>`;
}
async function epAction(a,e,el,encId){
// From a row we have the element to swap; from the detail pane we do not, so redraw.
const redraw=()=>{ if(el) redraw(); else renderEntries(); showDetail(e); };
const enc=(encId!=null && e.enclosures.find(x=>x.id===encId)) || e.enclosures[0];
const path=`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}`;
try{
if(a==='play') play(e);
if(a==='flag'){ e.flagged=!e.flagged; await api(path+'/flags',{method:'POST',body:JSON.stringify({flagged:e.flagged})}); redraw(); }
if(a==='read'){ e.read=!e.read; await api(path+'/flags',{method:'POST',body:JSON.stringify({read:e.read})}); redraw(); 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(isPlayable);
if(!enc){
toast(e.enclosures.some(x=>x.path) ? 'That file is not audio or video' : '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,wide){
$('#modalCard').innerHTML=html;
$('#modalCard').classList.toggle('wide',!!wide);
$('#modal').classList.add('on');
}
function closeModal(){
$('#modal').classList.remove('on');
if(logTimer){ clearInterval(logTimer); logTimer=null; }
}
/* ---------------- log view ---------------- */
let logTimer=null, logSeq=0, logLines=[], logFilter='', logLevel='', logTab='all';
// Which sources belong to each tab. "daemon" is the control protocol itself: every
// command in and every event out, whatever sent it.
const LOG_TABS={
all: null,
daemon: t=>t==='ipx::io',
scan: t=>t==='ipx::scan',
web: t=>t==='ipx::http',
};
const LEVELS={ERROR:3,WARN:2,INFO:1,DEBUG:0,TRACE:0};
function logsModal(){
logSeq=0; logLines=[];
openModal(`<h3>Log</h3>
<div class="logbar">
<div class="tabs" id="logtabs">
${Object.keys(LOG_TABS).map(t=>
`<button data-t="${t}" class="${logTab===t?'on':''}">${
{all:'All',daemon:'Daemon I/O',scan:'Scans',web:'HTTP'}[t]}</button>`).join('')}
</div>
<select id="loglevel" style="width:auto">
<option value="">All levels</option>
<option value="INFO">Info and above</option>
<option value="WARN">Warnings and errors</option>
<option value="ERROR">Errors only</option>
</select>
<input type="search" id="logq" class="grow" placeholder="Filter…">
<label class="check" style="margin:0"><input type="checkbox" id="logfollow" checked> Follow</label>
<button class="btn" id="logcopy">Copy</button>
<button class="btn" onclick="closeModal()">Close</button>
</div>
<div id="logbox"><p class="empty">Loading…</p></div>
<span class="hint"><b>Daemon I/O</b> is the control protocol itself — every command in and
every event out. <b>Scans</b> is feed and download activity, <b>HTTP</b> is web requests.
The buffer keeps debug detail even when the terminal does not; <b>IPX_UI_LOG</b> changes
what it captures.</span>`, true);
$$('#logtabs button').forEach(b=>b.onclick=()=>{
logTab=b.dataset.t;
$$('#logtabs button').forEach(x=>x.classList.toggle('on',x.dataset.t===logTab));
drawLog();
});
$('#loglevel').onchange=e=>{ logLevel=e.target.value; drawLog(); };
$('#logq').oninput=e=>{ logFilter=e.target.value.toLowerCase(); drawLog(); };
$('#logcopy').onclick=()=>copyText(visibleLog().map(l=>
`${new Date(l.ts*1000).toISOString()} ${l.level} ${l.target} ${l.msg}`).join('\n'), $('#logcopy'));
pollLog();
logTimer=setInterval(pollLog,2000);
}
async function pollLog(){
try{
const r=await api(`/api/logs?after=${logSeq}&limit=500`);
if(r.lines.length){
logLines=logLines.concat(r.lines).slice(-2000);
logSeq=r.latest;
drawLog();
}else if(!logLines.length){ drawLog(); }
}catch(e){
const box=$('#logbox');
if(box) box.innerHTML=`<p class="empty">Lost contact with the daemon: ${esc(e.message)}</p>`;
}
}
function visibleLog(){
const min=logLevel?LEVELS[logLevel]:-1;
const tab=LOG_TABS[logTab];
return logLines.filter(l=>
(!tab || tab(l.target)) &&
(LEVELS[l.level]??1)>=min &&
(!logFilter || (l.msg+' '+l.target).toLowerCase().includes(logFilter)));
}
function drawLog(){
const box=$('#logbox'); if(!box) return;
const follow=$('#logfollow')?.checked;
const rows=visibleLog();
box.innerHTML = rows.length ? rows.map(l=>{
const t=new Date(l.ts*1000).toLocaleTimeString();
if(logTab==='daemon'){
const out=l.msg.startsWith('<-');
return `<div class="l"><time>${t}</time>`+
`<span class="lv" style="color:${out?'var(--good)':'var(--accent)'}">${out?'out':'in'}</span>`+
`<span>${esc(l.msg.replace(/^[<-]+\s*/,''))}</span></div>`;
}
return `<div class="l"><time>${t}</time><span class="lv ${esc(l.level)}">${esc(l.level)}</span>`+
`<span class="tg">${esc(l.target.replace(/^ipx::?/,''))}</span><span>${esc(l.msg)}</span></div>`;
}).join('') : '<p class="empty">Nothing matches.</p>';
if(follow) box.scrollTop=box.scrollHeight;
}
$('#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; }
};
};
let expanded = new Set(JSON.parse(localStorage.getItem('ipx.expanded')||'[]'));
function toggleGroup(id){
expanded.has(id) ? expanded.delete(id) : expanded.add(id);
try{ localStorage.setItem('ipx.expanded', JSON.stringify([...expanded])); }catch{}
renderFeeds();
}
let globalEvery = 60, globalMax = 3;
const UNITS = [['m','minutes'],['h','hours'],['d','days'],['w','weeks']];
const UNIT_MINS = {m:1, h:60, d:1440, w:10080};
/// Largest unit that divides evenly, so 120 reads "2 hours" not "120 minutes".
function splitEvery(m){
if(!m) return {n:1, u:'h'};
for(const u of ['w','d','h']) if(m % UNIT_MINS[u] === 0) return {n:m/UNIT_MINS[u], u};
return {n:m, u:'m'};
}
function unitOptions(sel, firstLabel){
const head = firstLabel
? `<option value=""${sel===''?' selected':''}>${firstLabel}</option>` : '';
return head + UNITS.map(([v,l]) =>
`<option value="${v}"${sel===v?' selected':''}>${l}</option>`).join('');
}
function everyText(m){
if(!m) return '\u2014';
const {n,u} = splitEvery(m);
const name = {m:'min', h:'hour', d:'day', w:'week'}[u];
return n + ' ' + name + (u!=='m' && n!==1 ? 's' : '');
}
function due(ts){
const d = ts - Date.now()/1000;
if(d <= 0) return 'due now';
if(d < 3600) return 'in '+Math.max(1,Math.round(d/60))+'m';
if(d < 86400) return 'in '+Math.round(d/3600)+'h';
return 'in '+Math.round(d/86400)+'d';
}
async function prefsModal(){
const g = await api('/api/settings');
globalEvery = g.every_mins;
const gs = splitEvery(g.every_mins);
openModal(`<h3>Settings</h3>
<div class="field"><label>Check feeds every</label>
<div class="inline">
<input type="number" id="gnum" min="1" max="999" value="${gs.n}">
<select id="gunit">${unitOptions(gs.u)}</select>
</div>
<span class="hint">Applies to every feed that does not set its own. A feed's suggested
interval (its <b>ttl</b>) is still honoured when it asks to be polled less often.</span></div>
<div class="field"><label>Max new downloads per scan, per feed</label>
<input type="number" id="gmax" min="0" max="999" value="${g.max_new_per_check}">
<span class="hint">Applies to any feed that does not set its own — including every feed
inside an OPML subscription. <b>0 means unlimited</b>, which will pull a whole back
catalogue the first time a feed is scanned.</span></div>
<div class="field"><label>Download these media types automatically</label>
<input type="text" id="gtypes" value="${esc((g.media_types||[]).join(', '))}"
placeholder="audio, video">
<span class="hint">Anything else is still listed and can be downloaded by hand — blog feeds
put article images in enclosures, and those are not episodes. Empty takes everything.</span></div>
<div class="field"><label>Disk quota (GB, 0 = unlimited)</label>
<input type="number" id="gquota" min="0" step="0.5" value="${g.max_total_gb}">
<span class="hint">Over this, the oldest played episodes are deleted first. Starred
episodes are never touched.</span></div>
<div class="field"><label>Delete episodes older than (days, 0 = keep)</label>
<input type="number" id="gage" min="0" value="${g.max_age_days}"></div>
<div class="field"><label>Download folder</label>
<span class="hint" style="overflow-wrap:anywhere">${esc(g.download_dir)}</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="gsave">Save</button></div>`);
$('#gsave').onclick=async()=>{
try{
await api('/api/settings',{method:'PATCH',body:JSON.stringify({
schedule:`every ${Math.max(1,Number($('#gnum').value)||1)}${$('#gunit').value}`,
max_new_per_check:Math.max(0,Number($('#gmax').value)||0),
media_types:$('#gtypes').value.split(',').map(t=>t.trim()).filter(Boolean),
max_total_gb:Number($('#gquota').value)||0,
max_age_days:Number($('#gage').value)||0})});
closeModal(); toast('Settings saved');
await loadFeeds(true); if(S.feed){ renderFeed(); loadEntries(); }
}catch(e){ toast(e.message,true); }
};
}
function settingsModal(f){
const fs = splitEvery(f.schedule_mins || globalEvery);
const isGroup = S.feeds.some(c=>c.group===f.id);
openModal(`<h3>${esc(f.title||f.id)}</h3>
${isGroup?`<p class="hint" style="margin:-6px 0 12px">This is an OPML subscription. These
settings apply to it and are inherited by every feed inside it.</p>`:''}
${f.managed?`<p class="hint" style="margin:-6px 0 12px">This feed comes from an OPML
subscription and follows its settings. Saving anything here gives it its own entry in
config.toml, and it stops following the subscription's settings.</p>`:''}
<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>Check schedule</label>
<div class="inline">
<input type="number" id="snum" min="1" max="999" value="${fs.n}" ${f.schedule_mins?'':'disabled'}>
<select id="sunit">${unitOptions(f.schedule_mins?fs.u:'', `Use the default — every ${everyText(globalEvery)}`)}</select>
</div>
<span class="hint">Overrides the global schedule, and the feed's own suggested
interval, for this feed only.</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 follows the global default (${globalMax}). 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'));
// An empty unit means "follow the global default", so the number has nothing to say.
$('#sunit').onchange=()=>{ $('#snum').disabled = !$('#sunit').value; };
$('#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,
schedule:$('#sunit').value
? `every ${Math.max(1,Number($('#snum').value)||1)}${$('#sunit').value}` : 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); }
};
};
function on(sel,ev,fn){
const el=$(sel);
if(!el){ console.error('ipx: no element',sel); return; }
if(typeof fn!=='function'){ console.error('ipx: handler for',sel,'is not a function'); return; }
el[ev]=fn;
}
$('#scanAll').onclick=async()=>{ toast('Scanning all feeds…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({force:true})}); };
on('#prefs','onclick',prefsModal);
on('#logs','onclick',logsModal);
$('#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;
// Only the row actually downloading. Without the enclosure id this used to paint
// every pending bar at once, so adding a feed looked like it was fetching the lot.
const bar=document.querySelector(`.dlbar[data-bar="${ev.enclosure}"] i`);
if(bar){ bar.style.width=pct+'%'; bar.parentElement.classList.add('live'); }
$('#count') && ($('#count').textContent=`downloading ${ev.file}${pct.toFixed(0)}%`);
}
else if(ev.ev==='download_done'){
const bar=document.querySelector(`.dlbar[data-bar="${ev.enclosure}"]`);
if(bar) bar.classList.remove('live');
toast('Downloaded '+ev.path.split('/').pop()); loadEntries(); loadFeeds(true);
}
else if(ev.ev==='download_error'){
const bar=document.querySelector(`.dlbar[data-bar="${ev.enclosure}"]`);
if(bar) bar.classList.remove('live');
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>