feat: 4K downloads, watch progress, player controls, new icon

Window chrome: titleBarStyle Overlay (Transparent left the native bar in
place, white in dark mode, with a dead strip beneath it). The app now
paints that strip itself, so it matches the page background.

Player: prev/next through the feed, a full-screen button, a spinner
while a stream resolves, Open on YouTube on downloaded videos too, and
the description collapsed behind a disclosure.

Downloads default to Best, which reaches real 4K — above 1080p YouTube
serves VP9/AV1, verified to play natively in WKWebView here. Audio stays
pinned to AAC because Opus in MP4 would be silent. A Compatible setting
keeps the old 1080p H.264 behaviour. Files are now named
'<ISO date> - <title> [<id>].mp4'.

Watch progress is recorded and drawn under thumbnails like YouTube's,
and reopening a video resumes where it left off.

Tiles clamp every text line to a fixed height so they share a baseline,
and the search field no longer clips its placeholder.

Fixes a Picture-in-Picture leak: WebKit kept a detached video playing
after the player closed, so a second video could play over the first
with no way to stop it. Every exit path now tears the element down.
This commit is contained in:
vincent
2026-08-29 04:03:45 +02:00
parent d0bad64d7c
commit 5b09acd28d
69 changed files with 582 additions and 89 deletions
+25 -2
View File
@@ -3,7 +3,7 @@ import {
checkPrereqs, importTakeoutCsv, pickLibraryFolder, pickTakeoutFile, previewTakeoutImport,
} from "../api";
import { APPEARANCE_MODES, type Appearance } from "../hooks/useAppearance";
import type { ImportPreview, Prereqs } from "../types";
import type { ImportPreview, Prereqs, Quality } from "../types";
import TakeoutGuide from "./TakeoutGuide";
import {
BTN_CHROME, BTN_PRIMARY, Dialog, HELP, LABEL, SectionHeading, Segmented, SUBPANEL,
@@ -14,6 +14,8 @@ interface Props {
onImported: (count: number) => void;
appearance: Appearance;
onAppearance: (a: Appearance) => void;
quality: Quality;
onQuality: (q: Quality) => void;
onError: (message: string) => void;
}
@@ -33,7 +35,7 @@ function StatusRow({ label, value }: { label: string; value: string | null }) {
}
export default function Settings({
onClose, onImported, appearance, onAppearance, onError,
onClose, onImported, appearance, onAppearance, quality, onQuality, onError,
}: Props) {
const [prereqs, setPrereqs] = useState<Prereqs | null>(null);
const [pending, setPending] = useState<{ path: string; preview: ImportPreview } | null>(null);
@@ -121,6 +123,27 @@ export default function Settings({
</button>
</section>
<section className="border-b border-slate-200 px-5 py-4 dark:border-slate-800">
<SectionHeading>Download quality</SectionHeading>
<p className={`mt-1.5 ${HELP}`}>
<b>Best</b> takes the highest resolution available, up to 4K above 1080p
that means VP9 or AV1, which your Mac decodes but older ones may not, and
the files are several times larger. <b>Compatible</b> caps at 1080p H.264,
which plays anywhere. Audio is AAC either way.
</p>
<div className="mt-2 flex items-center justify-between gap-3">
<span className={LABEL}>Quality</span>
<Segmented
value={quality}
onChange={onQuality}
options={[
{ value: "best", label: "Best (4K)" },
{ value: "compatible", label: "Compatible" },
]}
/>
</div>
</section>
<section className="border-b border-slate-200 px-5 py-4 dark:border-slate-800">
<SectionHeading>Appearance</SectionHeading>
<div className="mt-2 flex items-center justify-between gap-3">