feat: uniform control height, chrome auto-hide, streaming quality
Every control in the app now shares one height (CONTROL_H), with width still growing to fit its label. The player transport is larger, Back is an icon, and the footer buttons match the prev/next size. Player chrome — transport and edge arrows — fades after 2.6s of inactivity and never hides while paused. The title-bar strip collapses in macOS window fullscreen, where the traffic lights are gone and it was just a blank white bar. Streaming quality is now selectable alongside download quality. A cap is applied by rewriting YouTube's HLS master playlist down to the best variant at or below the chosen height, keeping its audio group. That playlist is served from a small loopback HTTP server: Safari's native HLS is backed by AVFoundation, which cannot read blob: or custom-scheme URLs, so a Blob URL silently fails to play. Settings closes with an icon button and its selects match the shared control height.
This commit is contained in:
@@ -3,10 +3,12 @@ import {
|
||||
checkPrereqs, importTakeoutCsv, pickLibraryFolder, pickTakeoutFile, previewTakeoutImport,
|
||||
} from "../api";
|
||||
import { APPEARANCE_MODES, type Appearance } from "../hooks/useAppearance";
|
||||
import { QUALITIES, type ImportPreview, type Prereqs, type Quality } from "../types";
|
||||
import {
|
||||
QUALITIES, STREAM_QUALITIES, type ImportPreview, type Prereqs, type Quality,
|
||||
} from "../types";
|
||||
import TakeoutGuide from "./TakeoutGuide";
|
||||
import {
|
||||
BTN_CHROME, BTN_PRIMARY, Dialog, HELP, LABEL, SectionHeading, Segmented, SUBPANEL,
|
||||
BTN_PRIMARY, CONTROL_H, Dialog, HELP, ICON_BTN, LABEL, SectionHeading, Segmented, SUBPANEL,
|
||||
} from "./ui";
|
||||
|
||||
interface Props {
|
||||
@@ -16,9 +18,15 @@ interface Props {
|
||||
onAppearance: (a: Appearance) => void;
|
||||
quality: Quality;
|
||||
onQuality: (q: Quality) => void;
|
||||
streamQuality: Quality;
|
||||
onStreamQuality: (q: Quality) => void;
|
||||
onError: (message: string) => void;
|
||||
}
|
||||
|
||||
const SELECT =
|
||||
`w-full ${CONTROL_H} cursor-pointer rounded-lg border border-slate-300 bg-white px-2 ` +
|
||||
"text-[12px] outline-none dark:border-slate-700 dark:bg-slate-800";
|
||||
|
||||
function StatusRow({ label, value }: { label: string; value: string | null }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 border-b border-slate-200 py-2 last:border-b-0 dark:border-slate-800">
|
||||
@@ -35,7 +43,8 @@ function StatusRow({ label, value }: { label: string; value: string | null }) {
|
||||
}
|
||||
|
||||
export default function Settings({
|
||||
onClose, onImported, appearance, onAppearance, quality, onQuality, onError,
|
||||
onClose, onImported, appearance, onAppearance, quality, onQuality,
|
||||
streamQuality, onStreamQuality, onError,
|
||||
}: Props) {
|
||||
const [prereqs, setPrereqs] = useState<Prereqs | null>(null);
|
||||
const [pending, setPending] = useState<{ path: string; preview: ImportPreview } | null>(null);
|
||||
@@ -96,7 +105,17 @@ export default function Settings({
|
||||
>
|
||||
<header className="flex items-center justify-between gap-2 border-b border-slate-200 px-5 py-4 dark:border-slate-800">
|
||||
<h2 className="text-[15px] font-semibold tracking-tight">Settings</h2>
|
||||
<button onClick={onClose} className={`${BTN_CHROME} cursor-pointer`}>Close</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
title="Close settings"
|
||||
aria-label="Close settings"
|
||||
className={`${ICON_BTN} text-slate-500 hover:bg-slate-100 hover:text-slate-900
|
||||
dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white`}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" d="M6 6l12 12M18 6L6 18" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
@@ -135,9 +154,7 @@ export default function Settings({
|
||||
<select
|
||||
value={quality}
|
||||
onChange={(e) => onQuality(e.target.value as Quality)}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-2 py-1.5
|
||||
text-[13px] outline-none cursor-pointer
|
||||
dark:border-slate-700 dark:bg-slate-800"
|
||||
className={SELECT}
|
||||
>
|
||||
{QUALITIES.map((q) => (
|
||||
<option key={q.value} value={q.value}>
|
||||
@@ -146,6 +163,25 @@ export default function Settings({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="mt-2 grid grid-cols-[92px_1fr] items-center gap-2">
|
||||
<span className={LABEL}>Streaming</span>
|
||||
<select
|
||||
value={streamQuality}
|
||||
onChange={(e) => onStreamQuality(e.target.value as Quality)}
|
||||
className={SELECT}
|
||||
>
|
||||
{STREAM_QUALITIES.map((q) => (
|
||||
<option key={q.value} value={q.value}>
|
||||
{q.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p className={`mt-2 ${HELP}`}>
|
||||
Streaming quality applies when you play something you have not downloaded.
|
||||
Best lets the player adapt to your connection; a fixed height pins it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-slate-200 px-5 py-4 dark:border-slate-800">
|
||||
|
||||
Reference in New Issue
Block a user