+
{[compactViews(item.views), relativeTime(item.published)].filter(Boolean).join(" · ")}
- {(live?.error ?? item.error) && (live?.state ?? item.state) === "failed" && (
-
- {live?.error ?? item.error}
-
- )}
-
-
+
+
+ {(live?.error ?? item.error) && (live?.state ?? item.state) === "failed" && (
+
+ {live?.error ?? item.error}
+
+ )}
);
diff --git a/src/components/WatchBar.tsx b/src/components/WatchBar.tsx
new file mode 100644
index 0000000..a2d499f
--- /dev/null
+++ b/src/components/WatchBar.tsx
@@ -0,0 +1,30 @@
+import type { FeedItem } from "../types";
+
+/** Fraction of the video watched, or null if it was never opened. */
+export function watchedFraction(item: FeedItem): number | null {
+ const { position, duration } = item;
+ if (position == null || duration == null || duration <= 0) return null;
+ return Math.min(1, Math.max(0, position / duration));
+}
+
+/**
+ * The red sliver across the bottom of a thumbnail, same idea as YouTube's.
+ * Red rather than the sky accent on purpose: the accent means "the action to
+ * take" or "currently selected", and this is neither.
+ */
+export default function WatchBar({ item }: { item: FeedItem }) {
+ const f = watchedFraction(item);
+ if (f == null || f < 0.01) return null;
+ const nearlyDone = f > 0.97;
+ return (
+
+
+
+ );
+}
diff --git a/src/components/ui.tsx b/src/components/ui.tsx
index 640cea4..7e318ab 100644
--- a/src/components/ui.tsx
+++ b/src/components/ui.tsx
@@ -43,6 +43,16 @@ export const BTN_QUIET =
"text-[11px] text-slate-500 underline underline-offset-2 " +
"hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400";
+/** The only spinning thing in the app; used where a wait has no known length. */
+export function Spinner({ className = "size-4" }: { className?: string }) {
+ return (
+
+ );
+}
+
export function SectionHeading({
step,
children,
diff --git a/src/types.ts b/src/types.ts
index 4a88af1..9b6b246 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -32,6 +32,9 @@ export interface FeedItem {
path: string | null;
pct: number | null;
error: string | null;
+ /** Seconds watched, and the video's length — drives the feed progress bar. */
+ position: number | null;
+ duration: number | null;
}
export interface FeedFilter {
@@ -82,3 +85,5 @@ export interface ImportPreview {
removed_videos: number;
removed_downloads: number;
}
+
+export type Quality = "best" | "compatible";