feat: channel failures, updater, and a tidier Settings

Failing channels are visible instead of buried in a toast count. Each
refresh records its outcome per channel, the sidebar marks the failures
and counts them in its heading, and selecting one explains why above its
videos. Yours turn out to be three channels returning HTTP 404 — removed
or renamed on YouTube.

Video lengths now fill for what is on screen. Filling the newest across
all subscriptions meant a channel's videos stayed blank forever, since
the global newest always won the queue.

yt-dlp can be updated from Settings, which also says whether it is
current. It breaks whenever YouTube changes something, so it lands in
app data and takes precedence over the bundled copy; ffmpeg is stable
and ships with each release, so it is shown but not updated.

Also: 'Downloaded only' is now 'Local'; Hide Shorts moved to Settings;
the seven-step Takeout guide moved behind a button, since it dominated
the panel; the player names the height it is actually streaming, which
changes as an adaptive stream switches rendition; the player's delete is
an icon; and the window minimum drops to 1080 now that the control row
has one fewer button, while still never wrapping.
This commit is contained in:
vincent
2026-08-29 15:17:15 +02:00
parent 923fd3273f
commit 48acaa359f
13 changed files with 416 additions and 74 deletions
+22 -2
View File
@@ -19,6 +19,8 @@ export default function Sidebar({
channels, activeChannel, onSelect, onOpenSettings, totalVideos, totalDownloaded,
onHide, floating, titleBarInset,
}: Props) {
const failing = channels.filter((c) => c.last_error).length;
const row =
"flex w-full cursor-pointer items-center justify-between gap-2 rounded-lg px-2 py-1.5 " +
"text-[13px] transition-colors";
@@ -88,7 +90,17 @@ export default function Sidebar({
</button>
{channels.length > 0 && (
<h2 className={`${HEADING} px-2 pb-1 pt-4`}>Channels</h2>
<h2 className={`${HEADING} flex items-center justify-between px-2 pb-1 pt-4`}>
<span>Channels</span>
{failing > 0 && (
<span
title={`${failing} channel${failing === 1 ? "" : "s"} failed to refresh`}
className="font-mono text-[10px] normal-case tracking-normal text-red-500"
>
{failing} failing
</span>
)}
</h2>
)}
<ul className="space-y-0.5">
@@ -99,7 +111,15 @@ export default function Sidebar({
title={c.title}
className={`${row} ${activeChannel === c.id ? active : inactive}`}
>
<span className="truncate">{c.title}</span>
<span className="flex min-w-0 items-center gap-1.5">
{c.last_error && (
<span
title={`Last refresh failed: ${c.last_error}`}
className="size-1.5 shrink-0 rounded-full bg-red-500"
/>
)}
<span className="truncate">{c.title}</span>
</span>
<span className="shrink-0 font-mono text-[11px] tabular-nums opacity-70">
{c.downloaded_count > 0 && `${c.downloaded_count}/`}
{c.video_count}