Deleting entries during a cleanup left each feed's ETag in place, so the rescan got 304s, skipped parsing, and 57 feeds stayed empty until a publisher happened to change something. A 304 while the feed holds zero entries means the validator has outlived the data, so the daemon drops it and asks again. "not a wanted media type" was jargon, and storing it in last_error painted an ordinary filter decision red. It reads "not audio or video" now, and a reason is only shown as an error when the state actually is one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
1271 lines
106 KiB
HTML
1271 lines
106 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>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:14px}
|
||
.feed.child .art{width:28px;height:28px;font-size:11px}
|
||
.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: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:38px;height:38px;font-size:14px}
|
||
.feed .txt{min-width:0;flex:1}
|
||
.feed .txt b{display:block;font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.feed .txt small{color:var(--faint);font-size:11.5px}
|
||
.badge{
|
||
background:var(--accent);color:var(--ink);border-radius:20px;padding:1px 7px;
|
||
font-size:11px;font-weight:700;flex:none;
|
||
}
|
||
.badge.zero{background:var(--raise);color:var(--faint)}
|
||
|
||
/* ---------- main ---------- */
|
||
#main{overflow: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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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;
|
||
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 =
|
||
(kids?`<span class="chev${open?' open':''}" title="Show or hide the feeds inside">▶</span>`:'')+
|
||
artHTML(f.image,f.title||f.id)+
|
||
`<div class="txt"><b>${esc(f.title||f.id)}</b><small>`+
|
||
(kids?`${kids} feed${kids===1?'':'s'}${f.unread?` · ${f.unread} unread`:''}`
|
||
:`${f.entries} eps · ${f.downloaded} 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${f.unread?'':' zero'}">${f.unread}</span>`;
|
||
el.onclick=()=>{ selectFeed(f.id); $('#sidebar').classList.remove('open'); };
|
||
const chev=$('.chev',el);
|
||
if(chev) chev.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.last_error&&x.state==='error'?`<span class="err">${esc(x.last_error)}</span>`
|
||
:(x.last_error?`<span class="meta">${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>
|