mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
feat(native): port title page color tinting from web to mobile (#11)
This commit is contained in:
@@ -40,9 +40,11 @@
|
||||
"expo-dev-client": "55.0.16",
|
||||
"expo-device": "55.0.9",
|
||||
"expo-font": "55.0.4",
|
||||
"expo-glass-effect": "~55.0.8",
|
||||
"expo-haptics": "55.0.8",
|
||||
"expo-image": "55.0.6",
|
||||
"expo-image-picker": "55.0.12",
|
||||
"expo-linear-gradient": "~55.0.8",
|
||||
"expo-linking": "55.0.7",
|
||||
"expo-localization": "55.0.8",
|
||||
"expo-network": "55.0.8",
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react-native";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -33,6 +35,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { StarRating } from "@/components/ui/star-rating";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { useTitleTheme } from "@/hooks/use-title-theme";
|
||||
import { orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -143,6 +146,9 @@ export default function TitleDetailScreen() {
|
||||
}, []);
|
||||
|
||||
const title = detail.data?.title;
|
||||
const palette = title?.colorPalette ?? null;
|
||||
useTitleTheme(palette);
|
||||
|
||||
const seasons = detail.data?.seasons ?? [];
|
||||
const cast = detail.data?.cast ?? [];
|
||||
const availability = detail.data?.availability ?? [];
|
||||
@@ -268,9 +274,39 @@ export default function TitleDetailScreen() {
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
{/* Backdrop overlay: glass effect on supported devices, colored Views elsewhere */}
|
||||
{isLiquidGlassAvailable() && palette?.vibrant ? (
|
||||
<GlassView
|
||||
glassEffectStyle="regular"
|
||||
tintColor={palette.vibrant}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Base darkening overlay */}
|
||||
<View
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.45)" }}
|
||||
/>
|
||||
{/* Colored tint from palette */}
|
||||
{palette?.darkMuted && (
|
||||
<View
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: palette.darkMuted, opacity: 0.25 }}
|
||||
/>
|
||||
)}
|
||||
{palette?.vibrant && (
|
||||
<View
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: palette.vibrant, opacity: 0.06 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Bottom fade to background */}
|
||||
<LinearGradient
|
||||
colors={["transparent", "rgba(0,0,0,0.7)"]}
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.55)" }}
|
||||
/>
|
||||
|
||||
{title.trailerVideoKey && (
|
||||
@@ -295,7 +331,14 @@ export default function TitleDetailScreen() {
|
||||
{title.posterPath && (
|
||||
<View
|
||||
className="mr-3 h-[150px] w-[100px] overflow-hidden rounded-lg"
|
||||
style={{ borderCurve: "continuous" }}
|
||||
style={[
|
||||
{ borderCurve: "continuous" },
|
||||
palette?.darkVibrant
|
||||
? {
|
||||
boxShadow: `0 12px 28px -8px ${palette.darkVibrant}80`,
|
||||
}
|
||||
: undefined,
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: title.posterPath }}
|
||||
@@ -342,6 +385,27 @@ export default function TitleDetailScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Ambient color glow below hero */}
|
||||
{(() => {
|
||||
const glowColor =
|
||||
palette?.vibrant ?? palette?.darkMuted ?? palette?.muted;
|
||||
if (!glowColor) return null;
|
||||
return (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
height: 0,
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[`${glowColor}14`, "transparent"]}
|
||||
style={{ height: 300 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Genres */}
|
||||
{title.genres && title.genres.length > 0 && (
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(100)}>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ColorPalette } from "@sofa/api/schemas";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { Uniwind } from "uniwind";
|
||||
|
||||
function hexToRelativeLuminance(hex: string): number {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = Number.parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = Number.parseInt(hex.slice(5, 7), 16) / 255;
|
||||
const toLinear = (c: number) =>
|
||||
c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
||||
}
|
||||
|
||||
const DEFAULTS: Record<string, string> = {
|
||||
"--color-primary": "oklch(0.8 0.14 65)",
|
||||
"--color-ring": "oklch(0.8 0.14 65)",
|
||||
"--color-primary-foreground": "oklch(0.13 0.006 55)",
|
||||
"--color-status-watching": "oklch(0.78 0.14 65)",
|
||||
};
|
||||
|
||||
function applyPalette(vibrant: string): void {
|
||||
const luminance = hexToRelativeLuminance(vibrant);
|
||||
const foreground =
|
||||
luminance > 0.3 ? "oklch(0.13 0.006 55)" : "oklch(0.93 0.015 80)";
|
||||
|
||||
Uniwind.updateCSSVariables("dark", {
|
||||
"--color-primary": vibrant,
|
||||
"--color-ring": vibrant,
|
||||
"--color-primary-foreground": foreground,
|
||||
"--color-status-watching": vibrant,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTitleTheme(palette: ColorPalette | null | undefined): void {
|
||||
const vibrant = palette?.vibrant ?? null;
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (vibrant) {
|
||||
applyPalette(vibrant);
|
||||
}
|
||||
|
||||
return () => {
|
||||
Uniwind.updateCSSVariables("dark", DEFAULTS);
|
||||
};
|
||||
}, [vibrant]),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user