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
766 lines
60 KiB
HTML
766 lines
60 KiB
HTML
<!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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|