Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.99.2",
"@tanstack/react-router": "^1.168.23",
"@tanstack/react-query": "^5.100.8",
"@tanstack/react-router": "^1.169.1",
"@vidstack/react": "1.12.13",
"dashjs": "^5.1.1",
"lucide-react": "^1.8.0",
"lucide-react": "^1.14.0",
"media-icons": "^1.1.5",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"simple-icons": "^16.17.0",
"simple-icons": "^16.18.1",
"zustand": "^5.0.12"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.4",
"@tanstack/router-plugin": "^1.167.22",
"@tanstack/router-plugin": "^1.167.32",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.4",
"typescript": "~6.0.3",
"vite": "^8.0.9"
"vite": "^8.0.10"
}
}
5 changes: 2 additions & 3 deletions apps/web/src/components/format-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ export function FormatSelector() {

const videoOptions = options.filter((o) => o.quality !== null);
const groups = groupByFamily(videoOptions);

if (groups.size <= 1) return null;

const current = activeFamily(videoOptions) ?? "H.264";
const availableOptions = FORMAT_OPTIONS.filter((f) => groups.has(f.value));

if (groups.size <= 1) return null;

function onChange(value: string) {
const best = groups.get(value as "H.264" | "VP9");
if (best) best.select();
Expand Down
14 changes: 13 additions & 1 deletion apps/web/src/components/player-defaults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ type PlayerDefaultsProps = {
defaultSubtitleLanguage?: string;
};

function qualityLabelHeight(label: string): number | null {
const match = label.match(/(\d+)/);
if (!match) return null;
const height = Number(match[1]);
return Number.isFinite(height) ? height : null;
}

export function PlayerDefaults({
defaultQuality,
defaultAudioLanguage,
Expand All @@ -41,7 +48,12 @@ export function PlayerDefaults({

useEffect(() => {
if (!canPlay || qualityApplied.current || !defaultQuality) return;
const match = qualityOptions.find((o) => o.label === defaultQuality);
const defaultHeight = qualityLabelHeight(defaultQuality);
const exactMatch = qualityOptions.find((o) => o.label === defaultQuality);
const heightMatch = qualityOptions.find(
(o) => defaultHeight !== null && o.quality?.height === defaultHeight,
);
const match = exactMatch ?? heightMatch;
if (!match) return;
match.select();
qualityApplied.current = true;
Expand Down
41 changes: 29 additions & 12 deletions apps/web/src/components/quality-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useRef } from "react";
import { activeFamily, codecFamily } from "../lib/quality-utils";
import type { DefaultLayoutIcon, MenuInstance } from "../lib/vidstack";
import {
ClipIcon,
Expand All @@ -11,36 +10,54 @@ import {

const qualityIcon: DefaultLayoutIcon = (props) => <ClipIcon {...props} />;
const MENU_ITEMS_CLASS =
"vds-menu-items overflow-y-auto overscroll-y-contain pr-0.5 [scrollbar-width:thin] [scrollbar-color:var(--color-zinc-500)_transparent] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-surface-soft/80 [&::-webkit-scrollbar-thumb:hover]:bg-surface-soft [&::-webkit-scrollbar-track]:bg-transparent";
"vds-menu-items max-h-[44svh] overflow-y-auto overscroll-y-contain pr-0.5 md:max-h-72 [scrollbar-width:thin] [scrollbar-color:var(--color-zinc-500)_transparent] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-surface-soft/80 [&::-webkit-scrollbar-thumb:hover]:bg-surface-soft [&::-webkit-scrollbar-track]:bg-transparent";

type QualityOption = ReturnType<typeof useVideoQualityOptions>[number];

function qualityValue(option: QualityOption): string {
return String(option.quality?.height ?? option.label);
}

function collectResolutionOptions(options: QualityOption[]): QualityOption[] {
const grouped = new Map<string, QualityOption>();
for (const option of options) {
if (option.quality === null) continue;
const value = qualityValue(option);
const current = grouped.get(value);
if (!current || option.selected) grouped.set(value, option);
}
return [...grouped.values()];
}

export function QualitySelector() {
const menuRef = useRef<MenuInstance>(null);
const options = useVideoQualityOptions({ sort: "descending" });

const videoOptions = options.filter((o) => o.quality !== null);
const family = activeFamily(videoOptions);

const filteredOptions = videoOptions.filter(
(o) => o.quality !== null && codecFamily(o.quality.codec) === family,
);
const filteredOptions = collectResolutionOptions(videoOptions);
const selected = filteredOptions.find((o) => o.selected) ?? filteredOptions[0];
const radioOptions = filteredOptions.map((o) => ({ label: o.label, value: qualityValue(o) }));

if (filteredOptions.length <= 1) return null;
if (filteredOptions.every((o) => (o.quality?.height ?? 0) === 0)) return null;

const current = filteredOptions.find((o) => o.selected)?.label ?? filteredOptions[0]?.label;

const radioOptions = filteredOptions.map((o) => ({ label: o.label, value: o.label }));
if (!selected) return null;
const current = selected.label;

function onChange(value: string) {
filteredOptions.find((o) => o.label === value)?.select();
filteredOptions.find((o) => qualityValue(o) === value)?.select();
menuRef.current?.close();
}

return (
<Menu.Root ref={menuRef} className="vds-quality-menu vds-menu">
<DefaultMenuButton label="Quality" hint={current} Icon={qualityIcon} />
<Menu.Items className={MENU_ITEMS_CLASS}>
<DefaultMenuRadioGroup value={current ?? ""} options={radioOptions} onChange={onChange} />
<DefaultMenuRadioGroup
value={qualityValue(selected)}
options={radioOptions}
onChange={onChange}
/>
</Menu.Items>
</Menu.Root>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/hooks/use-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function useAuth() {
const signOut = useAuthStore((s) => s.setSignedOut);

const role = me?.role ?? null;
const isAuthed = token !== null;
const isAuthed = status === "authenticated" || status === "guest";
const authReady = status !== "loading";
const isGuest = status === "guest";
const publicUsername = me?.publicUsername ?? null;
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import "@vidstack/react/player/styles/default/layouts/video.css";
@import "./styles/theme.css";
@import "./styles/shorts-overrides.css";
@import "./styles/player-menu-overrides.css";

@keyframes card-pop-in {
from {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/quality-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { VideoQualityOption } from "./vidstack";

type CodecFamily = "H.264" | "VP9";

export function codecFamily(codec: string | null): CodecFamily | null {
function codecFamily(codec: string | null): CodecFamily | null {
if (!codec) return null;
if (codec.startsWith("avc1")) return "H.264";
if (codec.startsWith("vp09") || codec === "vp9") return "VP9";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/youtube-import-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type YoutubeTakeoutImportStatus = "pending" | "running" | "completed" | "failed";
type YoutubeTakeoutImportStatus = "pending" | "running" | "completed" | "failed";

export type YoutubeTakeoutImportJob = {
jobId: string;
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { HomeRecommendationsSection } from "../components/home-recommendations-s
import { useAuth } from "../hooks/use-auth";

function HomePage() {
const { isAuthed } = useAuth();
const { authReady, isAuthed } = useAuth();
if (!authReady) {
return (
<div className="min-h-[40vh] flex items-center justify-center">
<p className="text-sm text-fg-muted">Loading session...</p>
</div>
);
}
const title = isAuthed ? "Recommended" : "Trending";

return (
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/styles/player-menu-overrides.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.vds-quality-menu .vds-menu-items[data-submenu] {
align-items: stretch;
justify-content: flex-start;
max-height: min(44svh, 18rem);
overflow-y: auto;
}

@media (pointer: coarse) {
[data-view-type="video"] .vds-menu-items[data-root]:not([data-placement]) {
right: 0.75rem;
bottom: calc(env(safe-area-inset-bottom, 0px) + 0.75rem);
left: 0.75rem;
max-height: min(32svh, 16rem);
}
}
2 changes: 1 addition & 1 deletion apps/web/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export type HomeRecommendationsResponse = {
hasMore: boolean;
};

export type RecommendationOnboardingTopicGroup = {
type RecommendationOnboardingTopicGroup = {
id: string;
label: string;
topics: string[];
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/types/downloader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type DownloaderMode = "video" | "audio";

export type DownloaderJobStatus = "queued" | "running" | "done" | "failed";
type DownloaderJobStatus = "queued" | "running" | "done" | "failed";

export type DownloaderJobStage =
| "queued"
Expand All @@ -12,7 +12,7 @@ export type DownloaderJobStage =
| "cancelled"
| "failed";

export type DownloaderJobOptions = {
type DownloaderJobOptions = {
mode: DownloaderMode;
quality: string;
format: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/types/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { VideoItem } from "./api";

export type SubscriptionNewVideoNotification = {
type SubscriptionNewVideoNotification = {
type: "subscription_new_video";
title: string;
publishedAt?: number | null;
Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
Loading