feat(tv): add scalable typography with user-configurable text size

This commit is contained in:
Fredrik Burmester
2026-01-25 22:55:44 +01:00
parent 0c6c20f563
commit 875a017e8c
59 changed files with 712 additions and 494 deletions

View File

@@ -17,7 +17,11 @@ import {
} from "@/components/tv"; } from "@/components/tv";
import { useTVOptionModal } from "@/hooks/useTVOptionModal"; import { useTVOptionModal } from "@/hooks/useTVOptionModal";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { AudioTranscodeMode, useSettings } from "@/utils/atoms/settings"; import {
AudioTranscodeMode,
TVTypographyScale,
useSettings,
} from "@/utils/atoms/settings";
export default function SettingsTV() { export default function SettingsTV() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -39,6 +43,8 @@ export default function SettingsTV() {
settings.subtitleMode || SubtitlePlaybackMode.Default; settings.subtitleMode || SubtitlePlaybackMode.Default;
const currentAlignX = settings.mpvSubtitleAlignX ?? "center"; const currentAlignX = settings.mpvSubtitleAlignX ?? "center";
const currentAlignY = settings.mpvSubtitleAlignY ?? "bottom"; const currentAlignY = settings.mpvSubtitleAlignY ?? "bottom";
const currentTypographyScale =
settings.tvTypographyScale || TVTypographyScale.Default;
// Audio transcoding options // Audio transcoding options
const audioTranscodeModeOptions: TVOptionItem<AudioTranscodeMode>[] = useMemo( const audioTranscodeModeOptions: TVOptionItem<AudioTranscodeMode>[] = useMemo(
@@ -130,6 +136,33 @@ export default function SettingsTV() {
[currentAlignY], [currentAlignY],
); );
// Typography scale options
const typographyScaleOptions: TVOptionItem<TVTypographyScale>[] = useMemo(
() => [
{
label: t("home.settings.appearance.text_size_small"),
value: TVTypographyScale.Small,
selected: currentTypographyScale === TVTypographyScale.Small,
},
{
label: t("home.settings.appearance.text_size_default"),
value: TVTypographyScale.Default,
selected: currentTypographyScale === TVTypographyScale.Default,
},
{
label: t("home.settings.appearance.text_size_large"),
value: TVTypographyScale.Large,
selected: currentTypographyScale === TVTypographyScale.Large,
},
{
label: t("home.settings.appearance.text_size_extra_large"),
value: TVTypographyScale.ExtraLarge,
selected: currentTypographyScale === TVTypographyScale.ExtraLarge,
},
],
[t, currentTypographyScale],
);
// Get display labels for option buttons // Get display labels for option buttons
const audioTranscodeLabel = useMemo(() => { const audioTranscodeLabel = useMemo(() => {
const option = audioTranscodeModeOptions.find((o) => o.selected); const option = audioTranscodeModeOptions.find((o) => o.selected);
@@ -151,6 +184,11 @@ export default function SettingsTV() {
return option?.label || "Bottom"; return option?.label || "Bottom";
}, [alignYOptions]); }, [alignYOptions]);
const typographyScaleLabel = useMemo(() => {
const option = typographyScaleOptions.find((o) => o.selected);
return option?.label || t("home.settings.appearance.text_size_default");
}, [typographyScaleOptions, t]);
return ( return (
<View style={{ flex: 1, backgroundColor: "#000000" }}> <View style={{ flex: 1, backgroundColor: "#000000" }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@@ -344,6 +382,18 @@ export default function SettingsTV() {
{/* Appearance Section */} {/* Appearance Section */}
<TVSectionHeader title={t("home.settings.appearance.title")} /> <TVSectionHeader title={t("home.settings.appearance.title")} />
<TVSettingsOptionButton
label={t("home.settings.appearance.text_size")}
value={typographyScaleLabel}
onPress={() =>
showOptions({
title: t("home.settings.appearance.text_size"),
options: typographyScaleOptions,
onSelect: (value) =>
updateSettings({ tvTypographyScale: value }),
})
}
/>
<TVSettingsToggle <TVSettingsToggle
label={t( label={t(
"home.settings.appearance.merge_next_up_continue_watching", "home.settings.appearance.merge_next_up_continue_watching",

View File

@@ -42,6 +42,7 @@ import {
TVFocusablePoster, TVFocusablePoster,
TVItemCardText, TVItemCardText,
} from "@/components/tv"; } from "@/components/tv";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import { useTVOptionModal } from "@/hooks/useTVOptionModal"; import { useTVOptionModal } from "@/hooks/useTVOptionModal";
@@ -83,6 +84,7 @@ const Page = () => {
}; };
const { libraryId } = searchParams; const { libraryId } = searchParams;
const typography = useScaledTVTypography();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { width: screenWidth } = useWindowDimensions(); const { width: screenWidth } = useWindowDimensions();
@@ -947,7 +949,7 @@ const Page = () => {
paddingTop: 100, paddingTop: 100,
}} }}
> >
<Text style={{ fontSize: 20, color: "#737373" }}> <Text style={{ fontSize: typography.body, color: "#737373" }}>
{t("library.no_results")} {t("library.no_results")}
</Text> </Text>
</View> </View>

View File

@@ -29,7 +29,7 @@ import MoviePoster, {
} from "@/components/posters/MoviePoster.tv"; } from "@/components/posters/MoviePoster.tv";
import SeriesPoster from "@/components/posters/SeriesPoster.tv"; import SeriesPoster from "@/components/posters/SeriesPoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import { import {
@@ -46,17 +46,22 @@ import { userAtom } from "@/providers/JellyfinProvider";
const TV_ITEM_GAP = 20; const TV_ITEM_GAP = 20;
const TV_HORIZONTAL_PADDING = 60; const TV_HORIZONTAL_PADDING = 60;
const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => ( type Typography = ReturnType<typeof useScaledTVTypography>;
const TVItemCardText: React.FC<{
item: BaseItemDto;
typography: Typography;
}> = ({ item, typography }) => (
<View style={{ marginTop: 12 }}> <View style={{ marginTop: 12 }}>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout - 2, fontSize: typography.callout - 2,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -67,6 +72,7 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => (
); );
export default function WatchlistDetailScreen() { export default function WatchlistDetailScreen() {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const navigation = useNavigation(); const navigation = useNavigation();
@@ -212,11 +218,11 @@ export default function WatchlistDetailScreen() {
<SeriesPoster item={item} /> <SeriesPoster item={item} />
)} )}
</TVFocusablePoster> </TVFocusablePoster>
<TVItemCardText item={item} /> <TVItemCardText item={item} typography={typography} />
</View> </View>
); );
}, },
[router], [router, typography],
); );
const renderItem = useCallback( const renderItem = useCallback(
@@ -356,7 +362,7 @@ export default function WatchlistDetailScreen() {
{watchlist.description && ( {watchlist.description && (
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "#9CA3AF", color: "#9CA3AF",
marginBottom: 16, marginBottom: 16,
textAlign: "center", textAlign: "center",
@@ -376,9 +382,7 @@ export default function WatchlistDetailScreen() {
style={{ flexDirection: "row", alignItems: "center", gap: 8 }} style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
> >
<Ionicons name='film-outline' size={20} color='#9ca3af' /> <Ionicons name='film-outline' size={20} color='#9ca3af' />
<Text <Text style={{ fontSize: typography.callout, color: "#9CA3AF" }}>
style={{ fontSize: TVTypography.callout, color: "#9CA3AF" }}
>
{items?.length ?? 0}{" "} {items?.length ?? 0}{" "}
{(items?.length ?? 0) === 1 {(items?.length ?? 0) === 1
? t("watchlists.item") ? t("watchlists.item")
@@ -395,18 +399,14 @@ export default function WatchlistDetailScreen() {
size={20} size={20}
color='#9ca3af' color='#9ca3af'
/> />
<Text <Text style={{ fontSize: typography.callout, color: "#9CA3AF" }}>
style={{ fontSize: TVTypography.callout, color: "#9CA3AF" }}
>
{watchlist.isPublic {watchlist.isPublic
? t("watchlists.public") ? t("watchlists.public")
: t("watchlists.private")} : t("watchlists.private")}
</Text> </Text>
</View> </View>
{!isOwner && ( {!isOwner && (
<Text <Text style={{ fontSize: typography.callout, color: "#737373" }}>
style={{ fontSize: TVTypography.callout, color: "#737373" }}
>
{t("watchlists.by_owner")} {t("watchlists.by_owner")}
</Text> </Text>
)} )}
@@ -426,7 +426,7 @@ export default function WatchlistDetailScreen() {
<Ionicons name='film-outline' size={48} color='#4b5563' /> <Ionicons name='film-outline' size={48} color='#4b5563' />
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "#9CA3AF", color: "#9CA3AF",
textAlign: "center", textAlign: "center",
marginTop: 16, marginTop: 16,

View File

@@ -17,7 +17,7 @@ import { TVRequestOptionRow } from "@/components/jellyseerr/tv/TVRequestOptionRo
import { TVToggleOptionRow } from "@/components/jellyseerr/tv/TVToggleOptionRow"; import { TVToggleOptionRow } from "@/components/jellyseerr/tv/TVToggleOptionRow";
import { TVButton, TVOptionSelector } from "@/components/tv"; import { TVButton, TVOptionSelector } from "@/components/tv";
import type { TVOptionItem } from "@/components/tv/TVOptionSelector"; import type { TVOptionItem } from "@/components/tv/TVOptionSelector";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { tvRequestModalAtom } from "@/utils/atoms/tvRequestModal"; import { tvRequestModalAtom } from "@/utils/atoms/tvRequestModal";
@@ -30,6 +30,7 @@ import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/
import { store } from "@/utils/store"; import { store } from "@/utils/store";
export default function TVRequestModalPage() { export default function TVRequestModalPage() {
const typography = useScaledTVTypography();
const router = useRouter(); const router = useRouter();
const modalState = useAtomValue(tvRequestModalAtom); const modalState = useAtomValue(tvRequestModalAtom);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -336,8 +337,12 @@ export default function TVRequestModalPage() {
trapFocusRight trapFocusRight
style={styles.content} style={styles.content}
> >
<Text style={styles.heading}>{t("jellyseerr.advanced")}</Text> <Text style={[styles.heading, { fontSize: typography.heading }]}>
<Text style={styles.subtitle}>{modalState.title}</Text> {t("jellyseerr.advanced")}
</Text>
<Text style={[styles.subtitle, { fontSize: typography.callout }]}>
{modalState.title}
</Text>
{isDataLoaded && isReady ? ( {isDataLoaded && isReady ? (
<ScrollView <ScrollView
@@ -390,7 +395,12 @@ export default function TVRequestModalPage() {
color='#FFFFFF' color='#FFFFFF'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
/> />
<Text style={styles.buttonText}> <Text
style={[
styles.buttonText,
{ fontSize: typography.callout },
]}
>
{t("jellyseerr.request_button")} {t("jellyseerr.request_button")}
</Text> </Text>
</TVButton> </TVButton>
@@ -451,13 +461,11 @@ const styles = StyleSheet.create({
overflow: "visible", overflow: "visible",
}, },
heading: { heading: {
fontSize: TVTypography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 8, marginBottom: 8,
}, },
subtitle: { subtitle: {
fontSize: TVTypography.callout,
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
marginBottom: 24, marginBottom: 24,
}, },
@@ -482,7 +490,6 @@ const styles = StyleSheet.create({
marginTop: 24, marginTop: 24,
}, },
buttonText: { buttonText: {
fontSize: TVTypography.callout,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
}, },

View File

@@ -16,7 +16,7 @@ import {
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVButton } from "@/components/tv"; import { TVButton } from "@/components/tv";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useTVRequestModal } from "@/hooks/useTVRequestModal"; import { useTVRequestModal } from "@/hooks/useTVRequestModal";
@@ -162,6 +162,7 @@ const TVSeasonToggleCard: React.FC<TVSeasonToggleCardProps> = ({
}; };
export default function TVSeasonSelectModalPage() { export default function TVSeasonSelectModalPage() {
const typography = useScaledTVTypography();
const router = useRouter(); const router = useRouter();
const modalState = useAtomValue(tvSeasonSelectModalAtom); const modalState = useAtomValue(tvSeasonSelectModalAtom);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -305,8 +306,12 @@ export default function TVSeasonSelectModalPage() {
trapFocusRight trapFocusRight
style={styles.content} style={styles.content}
> >
<Text style={styles.heading}>{t("jellyseerr.select_seasons")}</Text> <Text style={[styles.heading, { fontSize: typography.heading }]}>
<Text style={styles.subtitle}>{modalState.title}</Text> {t("jellyseerr.select_seasons")}
</Text>
<Text style={[styles.subtitle, { fontSize: typography.callout }]}>
{modalState.title}
</Text>
{/* Season cards horizontal scroll */} {/* Season cards horizontal scroll */}
<ScrollView <ScrollView
@@ -343,7 +348,9 @@ export default function TVSeasonSelectModalPage() {
color='#FFFFFF' color='#FFFFFF'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
/> />
<Text style={styles.buttonText}> <Text
style={[styles.buttonText, { fontSize: typography.callout }]}
>
{t("jellyseerr.request_selected")} {t("jellyseerr.request_selected")}
{selectedSeasons.size > 0 && ` (${selectedSeasons.size})`} {selectedSeasons.size > 0 && ` (${selectedSeasons.size})`}
</Text> </Text>
@@ -377,13 +384,11 @@ const styles = StyleSheet.create({
overflow: "visible", overflow: "visible",
}, },
heading: { heading: {
fontSize: TVTypography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 8, marginBottom: 8,
}, },
subtitle: { subtitle: {
fontSize: TVTypography.callout,
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
marginBottom: 24, marginBottom: 24,
}, },
@@ -413,7 +418,6 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
seasonTitle: { seasonTitle: {
fontSize: TVTypography.callout,
fontWeight: "600", fontWeight: "600",
marginBottom: 4, marginBottom: 4,
}, },
@@ -436,7 +440,6 @@ const styles = StyleSheet.create({
marginTop: 24, marginTop: 24,
}, },
buttonText: { buttonText: {
fontSize: TVTypography.callout,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
}, },

View File

@@ -12,12 +12,13 @@ import {
} from "react-native"; } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVCancelButton, TVOptionCard } from "@/components/tv"; import { TVCancelButton, TVOptionCard } from "@/components/tv";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { tvSeriesSeasonModalAtom } from "@/utils/atoms/tvSeriesSeasonModal"; import { tvSeriesSeasonModalAtom } from "@/utils/atoms/tvSeriesSeasonModal";
import { store } from "@/utils/store"; import { store } from "@/utils/store";
export default function TVSeriesSeasonModalPage() { export default function TVSeriesSeasonModalPage() {
const typography = useScaledTVTypography();
const router = useRouter(); const router = useRouter();
const modalState = useAtomValue(tvSeriesSeasonModalAtom); const modalState = useAtomValue(tvSeriesSeasonModalAtom);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -103,7 +104,9 @@ export default function TVSeriesSeasonModalPage() {
trapFocusRight trapFocusRight
style={styles.content} style={styles.content}
> >
<Text style={styles.title}>{t("item_card.select_season")}</Text> <Text style={[styles.title, { fontSize: typography.callout }]}>
{t("item_card.select_season")}
</Text>
{isReady && ( {isReady && (
<ScrollView <ScrollView
@@ -164,7 +167,6 @@ const styles = StyleSheet.create({
overflow: "visible", overflow: "visible",
}, },
title: { title: {
fontSize: TVTypography.callout,
fontWeight: "500", fontWeight: "500",
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
marginBottom: 16, marginBottom: 16,

View File

@@ -1,7 +1,7 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Platform, StyleSheet, View, type ViewProps } from "react-native"; import { Platform, StyleSheet, View, type ViewProps } from "react-native";
import { GlassEffectView } from "react-native-glass-effect-view"; import { GlassEffectView } from "react-native-glass-effect-view";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
interface Props extends ViewProps { interface Props extends ViewProps {
@@ -16,6 +16,8 @@ export const Badge: React.FC<Props> = ({
variant = "purple", variant = "purple",
...props ...props
}) => { }) => {
const typography = useScaledTVTypography();
const content = ( const content = (
<View style={styles.content}> <View style={styles.content}>
{iconLeft && <View style={styles.iconLeft}>{iconLeft}</View>} {iconLeft && <View style={styles.iconLeft}>{iconLeft}</View>}
@@ -69,7 +71,7 @@ export const Badge: React.FC<Props> = ({
{iconLeft && <View style={{ marginRight: 8 }}>{iconLeft}</View>} {iconLeft && <View style={{ marginRight: 8 }}>{iconLeft}</View>}
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#E5E7EB", color: "#E5E7EB",
}} }}
> >

View File

@@ -10,7 +10,7 @@ import {
type ViewProps, type ViewProps,
} from "react-native"; } from "react-native";
import { GlassEffectView } from "react-native-glass-effect-view"; import { GlassEffectView } from "react-native-glass-effect-view";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
interface TagProps { interface TagProps {
@@ -25,6 +25,9 @@ export const Tag: React.FC<
textStyle?: StyleProp<TextStyle>; textStyle?: StyleProp<TextStyle>;
} & ViewProps } & ViewProps
> = ({ text, textClass, textStyle, ...props }) => { > = ({ text, textClass, textStyle, ...props }) => {
// Hook must be called at the top level, before any conditional returns
const typography = useScaledTVTypography();
if (Platform.OS === "ios" && !Platform.isTV) { if (Platform.OS === "ios" && !Platform.isTV) {
return ( return (
<View> <View>
@@ -60,7 +63,7 @@ export const Tag: React.FC<
backgroundColor: "rgba(0,0,0,0.3)", backgroundColor: "rgba(0,0,0,0.3)",
}} }}
> >
<Text style={{ fontSize: TVTypography.callout, color: "#E5E7EB" }}> <Text style={{ fontSize: typography.callout, color: "#E5E7EB" }}>
{text} {text}
</Text> </Text>
</View> </View>

View File

@@ -37,7 +37,7 @@ import {
TVTechnicalDetails, TVTechnicalDetails,
} from "@/components/tv"; } from "@/components/tv";
import type { Track } from "@/components/video-player/controls/types"; import type { Track } from "@/components/video-player/controls/types";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn"; import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
@@ -69,6 +69,7 @@ interface ItemContentTVProps {
// Export as both ItemContentTV (for direct requires) and ItemContent (for platform-resolved imports) // Export as both ItemContentTV (for direct requires) and ItemContent (for platform-resolved imports)
export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo( export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
({ item, itemWithSources }) => { ({ item, itemWithSources }) => {
const typography = useScaledTVTypography();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [_user] = useAtom(userAtom); const [_user] = useAtom(userAtom);
const isOffline = useOfflineMode(); const isOffline = useOfflineMode();
@@ -484,7 +485,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
) : ( ) : (
<Text <Text
style={{ style={{
fontSize: TVTypography.display, fontSize: typography.display,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 20, marginBottom: 20,
@@ -500,7 +501,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
<View style={{ marginBottom: 16 }}> <View style={{ marginBottom: 16 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.title, fontSize: typography.title,
color: "#FFFFFF", color: "#FFFFFF",
fontWeight: "600", fontWeight: "600",
}} }}
@@ -509,7 +510,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "white", color: "white",
marginTop: 6, marginTop: 6,
}} }}
@@ -554,7 +555,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "#E5E7EB", color: "#E5E7EB",
lineHeight: 32, lineHeight: 32,
}} }}
@@ -587,7 +588,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "bold", fontWeight: "bold",
color: "#000000", color: "#000000",
}} }}

View File

@@ -11,7 +11,7 @@ import heart from "@/assets/icons/heart.fill.png";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { InfiniteScrollingCollectionList } from "@/components/home/InfiniteScrollingCollectionList.tv"; import { InfiniteScrollingCollectionList } from "@/components/home/InfiniteScrollingCollectionList.tv";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
const HORIZONTAL_PADDING = 60; const HORIZONTAL_PADDING = 60;
@@ -28,6 +28,7 @@ type FavoriteTypes =
type EmptyState = Record<FavoriteTypes, boolean>; type EmptyState = Record<FavoriteTypes, boolean>;
export const Favorites = () => { export const Favorites = () => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
@@ -148,7 +149,7 @@ export const Favorites = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
marginBottom: 8, marginBottom: 8,
color: "#FFFFFF", color: "#FFFFFF",
@@ -160,7 +161,7 @@ export const Favorites = () => {
style={{ style={{
textAlign: "center", textAlign: "center",
opacity: 0.7, opacity: 0.7,
fontSize: TVTypography.body, fontSize: typography.body,
color: "#FFFFFF", color: "#FFFFFF",
}} }}
> >

View File

@@ -32,7 +32,7 @@ import { StreamystatsPromotedWatchlists } from "@/components/home/StreamystatsPr
import { StreamystatsRecommendations } from "@/components/home/StreamystatsRecommendations.tv"; import { StreamystatsRecommendations } from "@/components/home/StreamystatsRecommendations.tv";
import { TVHeroCarousel } from "@/components/home/TVHeroCarousel"; import { TVHeroCarousel } from "@/components/home/TVHeroCarousel";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useNetworkStatus } from "@/hooks/useNetworkStatus"; import { useNetworkStatus } from "@/hooks/useNetworkStatus";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
@@ -62,6 +62,7 @@ type Section = InfiniteScrollingCollectionListSection;
const BACKDROP_DEBOUNCE_MS = 300; const BACKDROP_DEBOUNCE_MS = 300;
export const Home = () => { export const Home = () => {
const typography = useScaledTVTypography();
const _router = useRouter(); const _router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
@@ -579,7 +580,7 @@ export const Home = () => {
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
marginBottom: 8, marginBottom: 8,
color: "#FFFFFF", color: "#FFFFFF",
@@ -591,7 +592,7 @@ export const Home = () => {
style={{ style={{
textAlign: "center", textAlign: "center",
opacity: 0.7, opacity: 0.7,
fontSize: TVTypography.body, fontSize: typography.body,
color: "#FFFFFF", color: "#FFFFFF",
}} }}
> >
@@ -632,7 +633,7 @@ export const Home = () => {
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
marginBottom: 8, marginBottom: 8,
color: "#FFFFFF", color: "#FFFFFF",
@@ -644,7 +645,7 @@ export const Home = () => {
style={{ style={{
textAlign: "center", textAlign: "center",
opacity: 0.7, opacity: 0.7,
fontSize: TVTypography.body, fontSize: typography.body,
color: "#FFFFFF", color: "#FFFFFF",
}} }}
> >

View File

@@ -20,7 +20,7 @@ import MoviePoster, {
TV_POSTER_WIDTH, TV_POSTER_WIDTH,
} from "@/components/posters/MoviePoster.tv"; } from "@/components/posters/MoviePoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters"; import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
import ContinueWatchingPoster, { import ContinueWatchingPoster, {
@@ -48,22 +48,27 @@ interface Props extends ViewProps {
parentId?: string; parentId?: string;
} }
type Typography = ReturnType<typeof useScaledTVTypography>;
// TV-specific ItemCardText with larger fonts // TV-specific ItemCardText with larger fonts
const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => { const TVItemCardText: React.FC<{
item: BaseItemDto;
typography: Typography;
}> = ({ item, typography }) => {
return ( return (
<View style={{ marginTop: 12, flexDirection: "column" }}> <View style={{ marginTop: 12, flexDirection: "column" }}>
{item.Type === "Episode" ? ( {item.Type === "Episode" ? (
<> <>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -77,13 +82,13 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<> <>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -103,7 +108,8 @@ const TVSeeAllCard: React.FC<{
disabled?: boolean; disabled?: boolean;
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
}> = ({ onPress, orientation, disabled, onFocus, onBlur }) => { typography: Typography;
}> = ({ onPress, orientation, disabled, onFocus, onBlur, typography }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const width = const width =
orientation === "horizontal" ? TV_LANDSCAPE_WIDTH : TV_POSTER_WIDTH; orientation === "horizontal" ? TV_LANDSCAPE_WIDTH : TV_POSTER_WIDTH;
@@ -137,7 +143,7 @@ const TVSeeAllCard: React.FC<{
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#FFFFFF", color: "#FFFFFF",
fontWeight: "600", fontWeight: "600",
}} }}
@@ -165,6 +171,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
parentId, parentId,
...props ...props
}) => { }) => {
const typography = useScaledTVTypography();
const effectivePageSize = Math.max(1, pageSize); const effectivePageSize = Math.max(1, pageSize);
const hasCalledOnLoaded = useRef(false); const hasCalledOnLoaded = useRef(false);
const router = useRouter(); const router = useRouter();
@@ -343,7 +350,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
> >
{renderPoster()} {renderPoster()}
</TVFocusablePoster> </TVFocusablePoster>
<TVItemCardText item={item} /> <TVItemCardText item={item} typography={typography} />
</View> </View>
); );
}, },
@@ -354,6 +361,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
handleItemPress, handleItemPress,
handleItemFocus, handleItemFocus,
handleItemBlur, handleItemBlur,
typography,
], ],
); );
@@ -365,7 +373,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
{/* Section Header */} {/* Section Header */}
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "700", fontWeight: "700",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 20, marginBottom: 20,
@@ -380,7 +388,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
<Text <Text
style={{ style={{
color: "#737373", color: "#737373",
fontSize: TVTypography.callout, fontSize: typography.callout,
marginLeft: SCALE_PADDING, marginLeft: SCALE_PADDING,
}} }}
> >
@@ -421,7 +429,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
color: "#262626", color: "#262626",
backgroundColor: "#262626", backgroundColor: "#262626",
borderRadius: 6, borderRadius: 6,
fontSize: TVTypography.callout, fontSize: typography.callout,
}} }}
numberOfLines={1} numberOfLines={1}
> >
@@ -478,6 +486,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
disabled={disabled} disabled={disabled}
onFocus={handleSeeAllFocus} onFocus={handleSeeAllFocus}
onBlur={handleItemBlur} onBlur={handleItemBlur}
typography={typography}
/> />
)} )}
</View> </View>

View File

@@ -16,7 +16,7 @@ import MoviePoster, {
} from "@/components/posters/MoviePoster.tv"; } from "@/components/posters/MoviePoster.tv";
import SeriesPoster from "@/components/posters/SeriesPoster.tv"; import SeriesPoster from "@/components/posters/SeriesPoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -26,18 +26,23 @@ import type { StreamystatsWatchlist } from "@/utils/streamystats/types";
const ITEM_GAP = 16; const ITEM_GAP = 16;
const SCALE_PADDING = 20; const SCALE_PADDING = 20;
const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => { type Typography = ReturnType<typeof useScaledTVTypography>;
const TVItemCardText: React.FC<{
item: BaseItemDto;
typography: Typography;
}> = ({ item, typography }) => {
return ( return (
<View style={{ marginTop: 12, flexDirection: "column" }}> <View style={{ marginTop: 12, flexDirection: "column" }}>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -60,6 +65,7 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
onItemFocus, onItemFocus,
...props ...props
}) => { }) => {
const typography = useScaledTVTypography();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const { settings } = useSettings(); const { settings } = useSettings();
@@ -142,11 +148,11 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
{item.Type === "Movie" && <MoviePoster item={item} />} {item.Type === "Movie" && <MoviePoster item={item} />}
{item.Type === "Series" && <SeriesPoster item={item} />} {item.Type === "Series" && <SeriesPoster item={item} />}
</TVFocusablePoster> </TVFocusablePoster>
<TVItemCardText item={item} /> <TVItemCardText item={item} typography={typography} />
</View> </View>
); );
}, },
[handleItemPress, onItemFocus], [handleItemPress, onItemFocus, typography],
); );
if (!isLoading && (!items || items.length === 0)) return null; if (!isLoading && (!items || items.length === 0)) return null;
@@ -155,7 +161,7 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
<View style={{ overflow: "visible" }} {...props}> <View style={{ overflow: "visible" }} {...props}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "700", fontWeight: "700",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 20, marginBottom: 20,

View File

@@ -16,7 +16,7 @@ import MoviePoster, {
} from "@/components/posters/MoviePoster.tv"; } from "@/components/posters/MoviePoster.tv";
import SeriesPoster from "@/components/posters/SeriesPoster.tv"; import SeriesPoster from "@/components/posters/SeriesPoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -26,6 +26,8 @@ import type { StreamystatsRecommendationsIdsResponse } from "@/utils/streamystat
const ITEM_GAP = 16; const ITEM_GAP = 16;
const SCALE_PADDING = 20; const SCALE_PADDING = 20;
type Typography = ReturnType<typeof useScaledTVTypography>;
interface Props extends ViewProps { interface Props extends ViewProps {
title: string; title: string;
type: "Movie" | "Series"; type: "Movie" | "Series";
@@ -34,18 +36,21 @@ interface Props extends ViewProps {
onItemFocus?: (item: BaseItemDto) => void; onItemFocus?: (item: BaseItemDto) => void;
} }
const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => { const TVItemCardText: React.FC<{
item: BaseItemDto;
typography: Typography;
}> = ({ item, typography }) => {
return ( return (
<View style={{ marginTop: 12, flexDirection: "column" }}> <View style={{ marginTop: 12, flexDirection: "column" }}>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -64,6 +69,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
onItemFocus, onItemFocus,
...props ...props
}) => { }) => {
const typography = useScaledTVTypography();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const { settings } = useSettings(); const { settings } = useSettings();
@@ -203,11 +209,11 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
{item.Type === "Movie" && <MoviePoster item={item} />} {item.Type === "Movie" && <MoviePoster item={item} />}
{item.Type === "Series" && <SeriesPoster item={item} />} {item.Type === "Series" && <SeriesPoster item={item} />}
</TVFocusablePoster> </TVFocusablePoster>
<TVItemCardText item={item} /> <TVItemCardText item={item} typography={typography} />
</View> </View>
); );
}, },
[handleItemPress, onItemFocus], [handleItemPress, onItemFocus, typography],
); );
if (!streamyStatsEnabled) return null; if (!streamyStatsEnabled) return null;
@@ -218,7 +224,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
<View style={{ overflow: "visible" }} {...props}> <View style={{ overflow: "visible" }} {...props}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "700", fontWeight: "700",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 20, marginBottom: 20,

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
import { Animated, FlatList, Pressable, View } from "react-native"; import { Animated, FlatList, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { import {
type DiscoverEndpoint, type DiscoverEndpoint,
@@ -33,6 +33,7 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
item, item,
isFirstItem = false, isFirstItem = false,
}) => { }) => {
const typography = useScaledTVTypography();
const router = useRouter(); const router = useRouter();
const { jellyseerrApi, getTitle, getYear } = useJellyseerr(); const { jellyseerrApi, getTitle, getYear } = useJellyseerr();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
@@ -130,7 +131,7 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
</View> </View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#fff", color: "#fff",
fontWeight: "600", fontWeight: "600",
marginTop: 12, marginTop: 12,
@@ -142,7 +143,7 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
{year && ( {year && (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -164,6 +165,7 @@ export const TVDiscoverSlide: React.FC<TVDiscoverSlideProps> = ({
slide, slide,
isFirstSlide = false, isFirstSlide = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
@@ -232,7 +234,7 @@ export const TVDiscoverSlide: React.FC<TVDiscoverSlideProps> = ({
<View style={{ marginBottom: 24 }}> <View style={{ marginBottom: 24 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,

View File

@@ -22,7 +22,7 @@ import { Loader } from "@/components/Loader";
import { JellyserrRatings } from "@/components/Ratings"; import { JellyserrRatings } from "@/components/Ratings";
import { TVButton } from "@/components/tv"; import { TVButton } from "@/components/tv";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useTVRequestModal } from "@/hooks/useTVRequestModal"; import { useTVRequestModal } from "@/hooks/useTVRequestModal";
@@ -68,6 +68,7 @@ const TVCastCard: React.FC<TVCastCardProps> = ({
onPress, onPress,
refSetter, refSetter,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08 }); useTVFocusAnimation({ scaleAmount: 1.08 });
@@ -128,7 +129,7 @@ const TVCastCard: React.FC<TVCastCardProps> = ({
</View> </View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#fff" : "rgba(255,255,255,0.9)", color: focused ? "#fff" : "rgba(255,255,255,0.9)",
fontWeight: "600", fontWeight: "600",
textAlign: "center", textAlign: "center",
@@ -158,6 +159,7 @@ const TVCastCard: React.FC<TVCastCardProps> = ({
}; };
export const TVJellyseerrPage: React.FC = () => { export const TVJellyseerrPage: React.FC = () => {
const typography = useScaledTVTypography();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const params = useLocalSearchParams(); const params = useLocalSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -552,7 +554,7 @@ export const TVJellyseerrPage: React.FC = () => {
{/* Title */} {/* Title */}
<Text <Text
style={{ style={{
fontSize: TVTypography.display, fontSize: typography.display,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginTop: 8, marginTop: 8,
@@ -566,7 +568,7 @@ export const TVJellyseerrPage: React.FC = () => {
{/* Year */} {/* Year */}
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "rgba(255,255,255,0.7)", color: "rgba(255,255,255,0.7)",
marginBottom: 16, marginBottom: 16,
}} }}
@@ -601,7 +603,7 @@ export const TVJellyseerrPage: React.FC = () => {
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "#E5E7EB", color: "#E5E7EB",
lineHeight: 32, lineHeight: 32,
}} }}
@@ -636,7 +638,7 @@ export const TVJellyseerrPage: React.FC = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "bold", fontWeight: "bold",
color: "#000000", color: "#000000",
}} }}
@@ -663,7 +665,7 @@ export const TVJellyseerrPage: React.FC = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
}} }}
@@ -698,7 +700,7 @@ export const TVJellyseerrPage: React.FC = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
}} }}
@@ -732,7 +734,7 @@ export const TVJellyseerrPage: React.FC = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
}} }}
@@ -757,7 +759,7 @@ export const TVJellyseerrPage: React.FC = () => {
<Ionicons name='person-outline' size={18} color='#9CA3AF' /> <Ionicons name='person-outline' size={18} color='#9CA3AF' />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginLeft: 8, marginLeft: 8,
}} }}
@@ -776,7 +778,7 @@ export const TVJellyseerrPage: React.FC = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
}} }}
@@ -794,7 +796,7 @@ export const TVJellyseerrPage: React.FC = () => {
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
}} }}
@@ -813,7 +815,7 @@ export const TVJellyseerrPage: React.FC = () => {
<View style={{ marginTop: 24 }}> <View style={{ marginTop: 24 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,

View File

@@ -20,7 +20,7 @@ import {
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVButton, TVOptionSelector } from "@/components/tv"; import { TVButton, TVOptionSelector } from "@/components/tv";
import type { TVOptionItem } from "@/components/tv/TVOptionSelector"; import type { TVOptionItem } from "@/components/tv/TVOptionSelector";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import type { import type {
QualityProfile, QualityProfile,
@@ -51,6 +51,7 @@ export const TVRequestModal: React.FC<TVRequestModalProps> = ({
onClose, onClose,
onRequested, onRequested,
}) => { }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
@@ -389,7 +390,7 @@ export const TVRequestModal: React.FC<TVRequestModalProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 8, marginBottom: 8,
@@ -399,7 +400,7 @@ export const TVRequestModal: React.FC<TVRequestModalProps> = ({
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
marginBottom: 24, marginBottom: 24,
}} }}
@@ -473,7 +474,7 @@ export const TVRequestModal: React.FC<TVRequestModalProps> = ({
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
}} }}

View File

@@ -3,7 +3,7 @@ import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
interface TVRequestOptionRowProps { interface TVRequestOptionRowProps {
label: string; label: string;
@@ -20,6 +20,7 @@ export const TVRequestOptionRow: React.FC<TVRequestOptionRowProps> = ({
hasTVPreferredFocus = false, hasTVPreferredFocus = false,
disabled = false, disabled = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ useTVFocusAnimation({
scaleAmount: 1.02, scaleAmount: 1.02,
@@ -56,7 +57,7 @@ export const TVRequestOptionRow: React.FC<TVRequestOptionRowProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
}} }}
> >
@@ -65,7 +66,7 @@ export const TVRequestOptionRow: React.FC<TVRequestOptionRowProps> = ({
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}> <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: focused ? "#FFFFFF" : "rgba(255,255,255,0.9)", color: focused ? "#FFFFFF" : "rgba(255,255,255,0.9)",
fontWeight: "500", fontWeight: "500",
}} }}

View File

@@ -2,7 +2,7 @@ import React from "react";
import { Animated, Pressable, ScrollView, View } from "react-native"; import { Animated, Pressable, ScrollView, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
interface ToggleItem { interface ToggleItem {
id: number; id: number;
@@ -21,6 +21,7 @@ const TVToggleChip: React.FC<TVToggleChipProps> = ({
onToggle, onToggle,
disabled = false, disabled = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ useTVFocusAnimation({
scaleAmount: 1.08, scaleAmount: 1.08,
@@ -57,7 +58,7 @@ const TVToggleChip: React.FC<TVToggleChipProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#000" : "#fff", color: focused ? "#000" : "#fff",
fontWeight: item.selected || focused ? "600" : "400", fontWeight: item.selected || focused ? "600" : "400",
}} }}
@@ -82,13 +83,14 @@ export const TVToggleOptionRow: React.FC<TVToggleOptionRowProps> = ({
onToggle, onToggle,
disabled = false, disabled = false,
}) => { }) => {
const typography = useScaledTVTypography();
if (items.length === 0) return null; if (items.length === 0) return null;
return ( return (
<View style={{ marginBottom: 16 }}> <View style={{ marginBottom: 16 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
marginBottom: 10, marginBottom: 10,
}} }}

View File

@@ -15,6 +15,7 @@ import { Animated, Easing, FlatList, Pressable, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -55,6 +56,7 @@ const TVLibraryRow: React.FC<{
}> = ({ library, isFirst, onPress }) => { }> = ({ library, isFirst, onPress }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const { t } = useTranslation(); const { t } = useTranslation();
const typography = useScaledTVTypography();
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current; const scale = useRef(new Animated.Value(1)).current;
const opacity = useRef(new Animated.Value(0.7)).current; const opacity = useRef(new Animated.Value(0.7)).current;
@@ -190,7 +192,7 @@ const TVLibraryRow: React.FC<{
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ style={{
fontSize: 32, fontSize: typography.heading,
fontWeight: "700", fontWeight: "700",
color: "#FFFFFF", color: "#FFFFFF",
textShadowColor: "rgba(0,0,0,0.8)", textShadowColor: "rgba(0,0,0,0.8)",
@@ -203,7 +205,7 @@ const TVLibraryRow: React.FC<{
{library.itemCount !== undefined && ( {library.itemCount !== undefined && (
<Text <Text
style={{ style={{
fontSize: 18, fontSize: typography.body,
color: "rgba(255,255,255,0.7)", color: "rgba(255,255,255,0.7)",
marginTop: 4, marginTop: 4,
textShadowColor: "rgba(0,0,0,0.8)", textShadowColor: "rgba(0,0,0,0.8)",
@@ -237,6 +239,7 @@ export const TVLibraries: React.FC = () => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const typography = useScaledTVTypography();
const { data: userViews, isLoading: viewsLoading } = useQuery({ const { data: userViews, isLoading: viewsLoading } = useQuery({
queryKey: ["user-views", user?.Id], queryKey: ["user-views", user?.Id],
@@ -360,7 +363,7 @@ export const TVLibraries: React.FC = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Text style={{ fontSize: 20, color: "#737373" }}> <Text style={{ fontSize: typography.body, color: "#737373" }}>
{t("library.no_libraries_found")} {t("library.no_libraries_found")}
</Text> </Text>
</View> </View>

View File

@@ -12,6 +12,7 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useScaledTVTypography } from "@/constants/TVTypography";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
@@ -44,6 +45,7 @@ export const TVLibraryCard: React.FC<Props> = ({ library }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { t } = useTranslation(); const { t } = useTranslation();
const typography = useScaledTVTypography();
const url = useMemo( const url = useMemo(
() => () =>
@@ -148,7 +150,7 @@ export const TVLibraryCard: React.FC<Props> = ({ library }) => {
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ style={{
fontSize: 22, fontSize: typography.body,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
marginTop: 12, marginTop: 12,
@@ -160,7 +162,7 @@ export const TVLibraryCard: React.FC<Props> = ({ library }) => {
{itemsCount !== undefined && ( {itemsCount !== undefined && (
<Text <Text
style={{ style={{
fontSize: 14, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 4, marginTop: 4,
}} }}

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { Animated, FlatList, Pressable, View } from "react-native"; import { Animated, FlatList, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media"; import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import type { import type {
@@ -27,6 +27,7 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
onPress, onPress,
isFirstItem = false, isFirstItem = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { jellyseerrApi, getTitle, getYear } = useJellyseerr(); const { jellyseerrApi, getTitle, getYear } = useJellyseerr();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 }); useTVFocusAnimation({ scaleAmount: 1.05 });
@@ -113,7 +114,7 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
</View> </View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#fff", color: "#fff",
fontWeight: "600", fontWeight: "600",
marginTop: 12, marginTop: 12,
@@ -125,7 +126,7 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
{year && ( {year && (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -147,6 +148,7 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
item, item,
onPress, onPress,
}) => { }) => {
const typography = useScaledTVTypography();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08 }); useTVFocusAnimation({ scaleAmount: 1.08 });
@@ -202,7 +204,7 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
</View> </View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#fff" : "rgba(255,255,255,0.9)", color: focused ? "#fff" : "rgba(255,255,255,0.9)",
fontWeight: "600", fontWeight: "600",
marginTop: 12, marginTop: 12,
@@ -230,13 +232,14 @@ const TVJellyseerrMovieSection: React.FC<TVJellyseerrMovieSectionProps> = ({
isFirstSection = false, isFirstSection = false,
onItemPress, onItemPress,
}) => { }) => {
const typography = useScaledTVTypography();
if (!items || items.length === 0) return null; if (!items || items.length === 0) return null;
return ( return (
<View style={{ marginBottom: 24 }}> <View style={{ marginBottom: 24 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,
@@ -281,13 +284,14 @@ const TVJellyseerrTvSection: React.FC<TVJellyseerrTvSectionProps> = ({
isFirstSection = false, isFirstSection = false,
onItemPress, onItemPress,
}) => { }) => {
const typography = useScaledTVTypography();
if (!items || items.length === 0) return null; if (!items || items.length === 0) return null;
return ( return (
<View style={{ marginBottom: 24 }}> <View style={{ marginBottom: 24 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,
@@ -332,13 +336,14 @@ const TVJellyseerrPersonSection: React.FC<TVJellyseerrPersonSectionProps> = ({
isFirstSection: _isFirstSection = false, isFirstSection: _isFirstSection = false,
onItemPress, onItemPress,
}) => { }) => {
const typography = useScaledTVTypography();
if (!items || items.length === 0) return null; if (!items || items.length === 0) return null;
return ( return (
<View style={{ marginBottom: 24 }}> <View style={{ marginBottom: 24 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Animated, Pressable } from "react-native"; import { Animated, Pressable } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVSearchBadgeProps { export interface TVSearchBadgeProps {
label: string; label: string;
@@ -14,6 +15,7 @@ export const TVSearchBadge: React.FC<TVSearchBadgeProps> = ({
onPress, onPress,
hasTVPreferredFocus = false, hasTVPreferredFocus = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08, duration: 150 }); useTVFocusAnimation({ scaleAmount: 1.08, duration: 150 });
@@ -41,7 +43,7 @@ export const TVSearchBadge: React.FC<TVSearchBadgeProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: 16, fontSize: typography.callout,
color: focused ? "#000" : "#fff", color: focused ? "#000" : "#fff",
fontWeight: focused ? "600" : "400", fontWeight: focused ? "600" : "400",
}} }}

View File

@@ -7,7 +7,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Input } from "@/components/common/Input"; import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVDiscover } from "@/components/jellyseerr/discover/TVDiscover"; import { TVDiscover } from "@/components/jellyseerr/discover/TVDiscover";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
@@ -27,6 +27,7 @@ const SCALE_PADDING = 20;
// Loading skeleton for TV // Loading skeleton for TV
const TVLoadingSkeleton: React.FC = () => { const TVLoadingSkeleton: React.FC = () => {
const typography = useScaledTVTypography();
const itemWidth = 210; const itemWidth = 210;
return ( return (
<View style={{ overflow: "visible" }}> <View style={{ overflow: "visible" }}>
@@ -72,7 +73,7 @@ const TVLoadingSkeleton: React.FC = () => {
color: "#262626", color: "#262626",
backgroundColor: "#262626", backgroundColor: "#262626",
borderRadius: 6, borderRadius: 6,
fontSize: TVTypography.callout, fontSize: typography.callout,
}} }}
numberOfLines={1} numberOfLines={1}
> >
@@ -150,6 +151,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
onJellyseerrPersonPress, onJellyseerrPersonPress,
discoverSliders, discoverSliders,
}) => { }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
@@ -308,7 +310,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
<View style={{ alignItems: "center", paddingTop: 40 }}> <View style={{ alignItems: "center", paddingTop: 40 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 8, marginBottom: 8,
@@ -318,7 +320,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
}} }}
> >

View File

@@ -11,27 +11,28 @@ import MoviePoster, {
} from "@/components/posters/MoviePoster.tv"; } from "@/components/posters/MoviePoster.tv";
import SeriesPoster from "@/components/posters/SeriesPoster.tv"; import SeriesPoster from "@/components/posters/SeriesPoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
const ITEM_GAP = 16; const ITEM_GAP = 16;
const SCALE_PADDING = 20; const SCALE_PADDING = 20;
// TV-specific ItemCardText with larger fonts // TV-specific ItemCardText with larger fonts
const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => { const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
const typography = useScaledTVTypography();
return ( return (
<View style={{ marginTop: 12, flexDirection: "column" }}> <View style={{ marginTop: 12, flexDirection: "column" }}>
{item.Type === "Episode" ? ( {item.Type === "Episode" ? (
<> <>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -45,7 +46,7 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#FFFFFF", color: "#FFFFFF",
textAlign: "center", textAlign: "center",
}} }}
@@ -56,14 +57,14 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<> <>
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -75,14 +76,14 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<> <>
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -94,13 +95,13 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<> <>
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -111,7 +112,7 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
) : item.Type === "Person" ? ( ) : item.Type === "Person" ? (
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
@@ -119,13 +120,13 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<> <>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} style={{ fontSize: typography.callout, color: "#FFFFFF" }}
> >
{item.Name} {item.Name}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginTop: 2, marginTop: 2,
}} }}
@@ -158,6 +159,7 @@ export const TVSearchSection: React.FC<TVSearchSectionProps> = ({
imageUrlGetter, imageUrlGetter,
...props ...props
}) => { }) => {
const typography = useScaledTVTypography();
const flatListRef = useRef<FlatList<BaseItemDto>>(null); const flatListRef = useRef<FlatList<BaseItemDto>>(null);
const [focusedCount, setFocusedCount] = useState(0); const [focusedCount, setFocusedCount] = useState(0);
const prevFocusedCount = useRef(0); const prevFocusedCount = useRef(0);
@@ -358,7 +360,7 @@ export const TVSearchSection: React.FC<TVSearchSectionProps> = ({
{/* Section Header */} {/* Section Header */}
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "700", fontWeight: "700",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 20, marginBottom: 20,

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { useScaledTVTypography } from "@/constants/TVTypography";
type SearchType = "Library" | "Discover"; type SearchType = "Library" | "Discover";
@@ -20,6 +21,7 @@ const TVSearchTabBadge: React.FC<TVSearchTabBadgeProps> = ({
hasTVPreferredFocus = false, hasTVPreferredFocus = false,
disabled = false, disabled = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08, duration: 150 }); useTVFocusAnimation({ scaleAmount: 1.08, duration: 150 });
@@ -61,7 +63,7 @@ const TVSearchTabBadge: React.FC<TVSearchTabBadgeProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: 16, fontSize: typography.callout,
color: getTextColor(), color: getTextColor(),
fontWeight: isSelected || focused ? "600" : "400", fontWeight: isSelected || focused ? "600" : "400",
}} }}

View File

@@ -7,7 +7,7 @@ import { ProgressBar } from "@/components/common/ProgressBar";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { WatchedIndicator } from "@/components/WatchedIndicator"; import { WatchedIndicator } from "@/components/WatchedIndicator";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { runtimeTicksToMinutes } from "@/utils/time"; import { runtimeTicksToMinutes } from "@/utils/time";
@@ -33,6 +33,7 @@ export const TVEpisodeCard: React.FC<TVEpisodeCardProps> = ({
onBlur, onBlur,
refSetter, refSetter,
}) => { }) => {
const typography = useScaledTVTypography();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const thumbnailUrl = useMemo(() => { const thumbnailUrl = useMemo(() => {
@@ -112,7 +113,7 @@ export const TVEpisodeCard: React.FC<TVEpisodeCardProps> = ({
{episodeLabel && ( {episodeLabel && (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
fontWeight: "500", fontWeight: "500",
}} }}
@@ -122,14 +123,10 @@ export const TVEpisodeCard: React.FC<TVEpisodeCardProps> = ({
)} )}
{duration && ( {duration && (
<> <>
<Text <Text style={{ color: "#6B7280", fontSize: typography.callout }}>
style={{ color: "#6B7280", fontSize: TVTypography.callout }}
>
</Text> </Text>
<Text <Text style={{ fontSize: typography.callout, color: "#9CA3AF" }}>
style={{ fontSize: TVTypography.callout, color: "#9CA3AF" }}
>
{duration} {duration}
</Text> </Text>
</> </>
@@ -138,7 +135,7 @@ export const TVEpisodeCard: React.FC<TVEpisodeCardProps> = ({
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#FFFFFF", color: "#FFFFFF",
marginTop: 4, marginTop: 4,
fontWeight: "500", fontWeight: "500",

View File

@@ -8,7 +8,7 @@ import { Dimensions, View } from "react-native";
import { Badge } from "@/components/Badge"; import { Badge } from "@/components/Badge";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { GenreTags } from "@/components/GenreTags"; import { GenreTags } from "@/components/GenreTags";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
@@ -19,6 +19,7 @@ interface TVSeriesHeaderProps {
} }
export const TVSeriesHeader: React.FC<TVSeriesHeaderProps> = ({ item }) => { export const TVSeriesHeader: React.FC<TVSeriesHeaderProps> = ({ item }) => {
const typography = useScaledTVTypography();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const logoUrl = useMemo(() => { const logoUrl = useMemo(() => {
@@ -58,7 +59,7 @@ export const TVSeriesHeader: React.FC<TVSeriesHeaderProps> = ({ item }) => {
) : ( ) : (
<Text <Text
style={{ style={{
fontSize: TVTypography.display, fontSize: typography.display,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,
@@ -80,7 +81,7 @@ export const TVSeriesHeader: React.FC<TVSeriesHeaderProps> = ({ item }) => {
}} }}
> >
{yearString && ( {yearString && (
<Text style={{ color: "white", fontSize: TVTypography.body }}> <Text style={{ color: "white", fontSize: typography.body }}>
{yearString} {yearString}
</Text> </Text>
)} )}
@@ -123,7 +124,7 @@ export const TVSeriesHeader: React.FC<TVSeriesHeaderProps> = ({ item }) => {
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "#E5E7EB", color: "#E5E7EB",
lineHeight: 32, lineHeight: 32,
}} }}

View File

@@ -29,7 +29,7 @@ import { getItemNavigation } from "@/components/common/TouchableItemRouter";
import { seasonIndexAtom } from "@/components/series/SeasonPicker"; import { seasonIndexAtom } from "@/components/series/SeasonPicker";
import { TVEpisodeCard } from "@/components/series/TVEpisodeCard"; import { TVEpisodeCard } from "@/components/series/TVEpisodeCard";
import { TVSeriesHeader } from "@/components/series/TVSeriesHeader"; import { TVSeriesHeader } from "@/components/series/TVSeriesHeader";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useTVSeriesSeasonModal } from "@/hooks/useTVSeriesSeasonModal"; import { useTVSeriesSeasonModal } from "@/hooks/useTVSeriesSeasonModal";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
@@ -142,6 +142,7 @@ const TVSeasonButton: React.FC<{
onPress: () => void; onPress: () => void;
disabled?: boolean; disabled?: boolean;
}> = ({ seasonName, onPress, disabled = false }) => { }> = ({ seasonName, onPress, disabled = false }) => {
const typography = useScaledTVTypography();
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current; const scale = useRef(new Animated.Value(1)).current;
@@ -190,7 +191,7 @@ const TVSeasonButton: React.FC<{
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: focused ? "#000" : "#FFFFFF", color: focused ? "#000" : "#FFFFFF",
fontWeight: "bold", fontWeight: "bold",
}} }}
@@ -213,6 +214,7 @@ export const TVSeriesPage: React.FC<TVSeriesPageProps> = ({
allEpisodes = [], allEpisodes = [],
isLoading: _isLoading, isLoading: _isLoading,
}) => { }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
@@ -567,7 +569,7 @@ export const TVSeriesPage: React.FC<TVSeriesPageProps> = ({
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
fontWeight: "bold", fontWeight: "bold",
color: "#000000", color: "#000000",
}} }}
@@ -591,7 +593,7 @@ export const TVSeriesPage: React.FC<TVSeriesPageProps> = ({
<View style={{ marginTop: 40, overflow: "visible" }}> <View style={{ marginTop: 40, overflow: "visible" }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,
@@ -646,7 +648,7 @@ export const TVSeriesPage: React.FC<TVSeriesPageProps> = ({
<Text <Text
style={{ style={{
color: "#737373", color: "#737373",
fontSize: TVTypography.callout, fontSize: typography.callout,
marginLeft: SCALE_PADDING, marginLeft: SCALE_PADDING,
}} }}
> >

View File

@@ -3,7 +3,7 @@ import { Image } from "expo-image";
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVActorCardProps { export interface TVActorCardProps {
@@ -19,6 +19,7 @@ export interface TVActorCardProps {
export const TVActorCard = React.forwardRef<View, TVActorCardProps>( export const TVActorCard = React.forwardRef<View, TVActorCardProps>(
({ person, apiBasePath, onPress, hasTVPreferredFocus }, ref) => { ({ person, apiBasePath, onPress, hasTVPreferredFocus }, ref) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08 }); useTVFocusAnimation({ scaleAmount: 1.08 });
@@ -84,7 +85,7 @@ export const TVActorCard = React.forwardRef<View, TVActorCardProps>(
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
fontWeight: "600", fontWeight: "600",
color: focused ? "#fff" : "rgba(255,255,255,0.9)", color: focused ? "#fff" : "rgba(255,255,255,0.9)",
textAlign: "center", textAlign: "center",
@@ -98,7 +99,7 @@ export const TVActorCard = React.forwardRef<View, TVActorCardProps>(
{person.Role && ( {person.Role && (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused color: focused
? "rgba(255,255,255,0.8)" ? "rgba(255,255,255,0.8)"
: "rgba(255,255,255,0.5)", : "rgba(255,255,255,0.5)",

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable } from "react-native"; import { Animated, Pressable } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVCancelButtonProps { export interface TVCancelButtonProps {
@@ -16,6 +16,7 @@ export const TVCancelButton: React.FC<TVCancelButtonProps> = ({
label = "Cancel", label = "Cancel",
disabled = false, disabled = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05, duration: 120 }); useTVFocusAnimation({ scaleAmount: 1.05, duration: 120 });
@@ -48,7 +49,7 @@ export const TVCancelButton: React.FC<TVCancelButtonProps> = ({
/> />
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#000" : "rgba(255,255,255,0.8)", color: focused ? "#000" : "rgba(255,255,255,0.8)",
fontWeight: "500", fontWeight: "500",
}} }}

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVCastCrewTextProps { export interface TVCastCrewTextProps {
director?: BaseItemPerson | null; director?: BaseItemPerson | null;
@@ -14,6 +14,7 @@ export interface TVCastCrewTextProps {
export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo( export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo(
({ director, cast, hideCast = false }) => { ({ director, cast, hideCast = false }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
if (!director && (!cast || cast.length === 0)) { if (!director && (!cast || cast.length === 0)) {
@@ -24,7 +25,7 @@ export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo(
<View style={{ marginBottom: 32 }}> <View style={{ marginBottom: 32 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 16, marginBottom: 16,
@@ -37,7 +38,7 @@ export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo(
<View> <View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#6B7280", color: "#6B7280",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
@@ -46,7 +47,7 @@ export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo(
> >
{t("item_card.director")} {t("item_card.director")}
</Text> </Text>
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{director.Name} {director.Name}
</Text> </Text>
</View> </View>
@@ -55,7 +56,7 @@ export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo(
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#6B7280", color: "#6B7280",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
@@ -64,7 +65,7 @@ export const TVCastCrewText: React.FC<TVCastCrewTextProps> = React.memo(
> >
{t("item_card.cast")} {t("item_card.cast")}
</Text> </Text>
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{cast.map((c) => c.Name).join(", ")} {cast.map((c) => c.Name).join(", ")}
</Text> </Text>
</View> </View>

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollView, TVFocusGuideView, View } from "react-native"; import { ScrollView, TVFocusGuideView, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { TVActorCard } from "./TVActorCard"; import { TVActorCard } from "./TVActorCard";
export interface TVCastSectionProps { export interface TVCastSectionProps {
@@ -24,6 +24,7 @@ export const TVCastSection: React.FC<TVCastSectionProps> = React.memo(
firstActorRefSetter, firstActorRefSetter,
upwardFocusDestination, upwardFocusDestination,
}) => { }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
if (cast.length === 0) { if (cast.length === 0) {
@@ -34,7 +35,7 @@ export const TVCastSection: React.FC<TVCastSectionProps> = React.memo(
<View style={{ marginBottom: 40 }}> <View style={{ marginBottom: 40 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 24, marginBottom: 24,

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVFilterButtonProps { export interface TVFilterButtonProps {
@@ -21,6 +21,7 @@ export const TVFilterButton: React.FC<TVFilterButtonProps> = ({
disabled = false, disabled = false,
hasActiveFilter = false, hasActiveFilter = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.04, duration: 120 }); useTVFocusAnimation({ scaleAmount: 1.04, duration: 120 });
@@ -54,7 +55,7 @@ export const TVFilterButton: React.FC<TVFilterButtonProps> = ({
{label ? ( {label ? (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#444" : "#bbb", color: focused ? "#444" : "#bbb",
}} }}
> >
@@ -63,7 +64,7 @@ export const TVFilterButton: React.FC<TVFilterButtonProps> = ({
) : null} ) : null}
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#000" : "#FFFFFF", color: focused ? "#000" : "#FFFFFF",
fontWeight: "500", fontWeight: "500",
}} }}

View File

@@ -2,28 +2,32 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import React from "react"; import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVItemCardTextProps { export interface TVItemCardTextProps {
item: BaseItemDto; item: BaseItemDto;
} }
export const TVItemCardText: React.FC<TVItemCardTextProps> = ({ item }) => ( export const TVItemCardText: React.FC<TVItemCardTextProps> = ({ item }) => {
<View style={{ marginTop: 12 }}> const typography = useScaledTVTypography();
<Text
numberOfLines={1} return (
style={{ fontSize: TVTypography.callout, color: "#FFFFFF" }} <View style={{ marginTop: 12 }}>
> <Text
{item.Name} numberOfLines={1}
</Text> style={{ fontSize: typography.callout, color: "#FFFFFF" }}
<Text >
style={{ {item.Name}
fontSize: TVTypography.callout - 2, </Text>
color: "#9CA3AF", <Text
marginTop: 2, style={{
}} fontSize: typography.callout - 2,
> color: "#9CA3AF",
{item.ProductionYear} marginTop: 2,
</Text> }}
</View> >
); {item.ProductionYear}
</Text>
</View>
);
};

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable, StyleSheet, View } from "react-native"; import { Animated, Pressable, StyleSheet, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVLanguageCardProps { export interface TVLanguageCardProps {
@@ -15,6 +15,8 @@ export interface TVLanguageCardProps {
export const TVLanguageCard = React.forwardRef<View, TVLanguageCardProps>( export const TVLanguageCard = React.forwardRef<View, TVLanguageCardProps>(
({ code, name, selected, hasTVPreferredFocus, onPress }, ref) => { ({ code, name, selected, hasTVPreferredFocus, onPress }, ref) => {
const typography = useScaledTVTypography();
const styles = createStyles(typography);
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 }); useTVFocusAnimation({ scaleAmount: 1.05 });
@@ -72,26 +74,27 @@ export const TVLanguageCard = React.forwardRef<View, TVLanguageCardProps>(
}, },
); );
const styles = StyleSheet.create({ const createStyles = (typography: ReturnType<typeof useScaledTVTypography>) =>
languageCard: { StyleSheet.create({
width: 120, languageCard: {
height: 60, width: 120,
borderRadius: 12, height: 60,
justifyContent: "center", borderRadius: 12,
alignItems: "center", justifyContent: "center",
paddingHorizontal: 12, alignItems: "center",
}, paddingHorizontal: 12,
languageCardText: { },
fontSize: TVTypography.callout, languageCardText: {
fontWeight: "500", fontSize: typography.callout,
}, fontWeight: "500",
languageCardCode: { },
fontSize: TVTypography.callout, languageCardCode: {
marginTop: 2, fontSize: typography.callout,
}, marginTop: 2,
checkmark: { },
position: "absolute", checkmark: {
top: 8, position: "absolute",
right: 8, top: 8,
}, right: 8,
}); },
});

View File

@@ -3,7 +3,7 @@ import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import { Badge } from "@/components/Badge"; import { Badge } from "@/components/Badge";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVMetadataBadgesProps { export interface TVMetadataBadgesProps {
year?: number | null; year?: number | null;
@@ -14,6 +14,8 @@ export interface TVMetadataBadgesProps {
export const TVMetadataBadges: React.FC<TVMetadataBadgesProps> = React.memo( export const TVMetadataBadges: React.FC<TVMetadataBadgesProps> = React.memo(
({ year, duration, officialRating, communityRating }) => { ({ year, duration, officialRating, communityRating }) => {
const typography = useScaledTVTypography();
return ( return (
<View <View
style={{ style={{
@@ -25,12 +27,12 @@ export const TVMetadataBadges: React.FC<TVMetadataBadgesProps> = React.memo(
}} }}
> >
{year != null && ( {year != null && (
<Text style={{ color: "white", fontSize: TVTypography.body }}> <Text style={{ color: "white", fontSize: typography.body }}>
{year} {year}
</Text> </Text>
)} )}
{duration && ( {duration && (
<Text style={{ color: "white", fontSize: TVTypography.body }}> <Text style={{ color: "white", fontSize: typography.body }}>
{duration} {duration}
</Text> </Text>
)} )}

View File

@@ -1,7 +1,7 @@
import type { Api } from "@jellyfin/sdk"; import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { type FC, useEffect, useRef } from "react"; import { type FC, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Image, StyleSheet, View } from "react-native"; import { Image, StyleSheet, View } from "react-native";
import Animated, { import Animated, {
@@ -13,7 +13,7 @@ import Animated, {
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
export interface TVNextEpisodeCountdownProps { export interface TVNextEpisodeCountdownProps {
@@ -31,6 +31,7 @@ export const TVNextEpisodeCountdown: FC<TVNextEpisodeCountdownProps> = ({
isPlaying, isPlaying,
onFinish, onFinish,
}) => { }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const progress = useSharedValue(0); const progress = useSharedValue(0);
const onFinishRef = useRef(onFinish); const onFinishRef = useRef(onFinish);
@@ -69,6 +70,8 @@ export const TVNextEpisodeCountdown: FC<TVNextEpisodeCountdownProps> = ({
width: `${progress.value * 100}%`, width: `${progress.value * 100}%`,
})); }));
const styles = useMemo(() => createStyles(typography), [typography]);
if (!show) return null; if (!show) return null;
return ( return (
@@ -105,57 +108,58 @@ export const TVNextEpisodeCountdown: FC<TVNextEpisodeCountdownProps> = ({
); );
}; };
const styles = StyleSheet.create({ const createStyles = (typography: ReturnType<typeof useScaledTVTypography>) =>
container: { StyleSheet.create({
position: "absolute", container: {
bottom: 180, position: "absolute",
right: 80, bottom: 180,
zIndex: 100, right: 80,
}, zIndex: 100,
blur: { },
borderRadius: 16, blur: {
overflow: "hidden", borderRadius: 16,
}, overflow: "hidden",
innerContainer: { },
flexDirection: "row", innerContainer: {
alignItems: "stretch", flexDirection: "row",
}, alignItems: "stretch",
thumbnail: { },
width: 180, thumbnail: {
backgroundColor: "rgba(0,0,0,0.3)", width: 180,
}, backgroundColor: "rgba(0,0,0,0.3)",
content: { },
padding: 16, content: {
justifyContent: "center", padding: 16,
width: 280, justifyContent: "center",
}, width: 280,
label: { },
fontSize: TVTypography.callout, label: {
color: "rgba(255,255,255,0.5)", fontSize: typography.callout,
textTransform: "uppercase", color: "rgba(255,255,255,0.5)",
letterSpacing: 1, textTransform: "uppercase",
marginBottom: 4, letterSpacing: 1,
}, marginBottom: 4,
seriesName: { },
fontSize: TVTypography.callout, seriesName: {
color: "rgba(255,255,255,0.7)", fontSize: typography.callout,
marginBottom: 2, color: "rgba(255,255,255,0.7)",
}, marginBottom: 2,
episodeInfo: { },
fontSize: TVTypography.body, episodeInfo: {
color: "#fff", fontSize: typography.body,
fontWeight: "600", color: "#fff",
marginBottom: 12, fontWeight: "600",
}, marginBottom: 12,
progressContainer: { },
height: 4, progressContainer: {
backgroundColor: "rgba(255,255,255,0.2)", height: 4,
borderRadius: 2, backgroundColor: "rgba(255,255,255,0.2)",
overflow: "hidden", borderRadius: 2,
}, overflow: "hidden",
progressBar: { },
height: "100%", progressBar: {
backgroundColor: "#fff", height: "100%",
borderRadius: 2, backgroundColor: "#fff",
}, borderRadius: 2,
}); },
});

View File

@@ -2,7 +2,7 @@ import { BlurView } from "expo-blur";
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVOptionButtonProps { export interface TVOptionButtonProps {
@@ -14,6 +14,7 @@ export interface TVOptionButtonProps {
export const TVOptionButton = React.forwardRef<View, TVOptionButtonProps>( export const TVOptionButton = React.forwardRef<View, TVOptionButtonProps>(
({ label, value, onPress, hasTVPreferredFocus }, ref) => { ({ label, value, onPress, hasTVPreferredFocus }, ref) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02, duration: 120 }); useTVFocusAnimation({ scaleAmount: 1.02, duration: 120 });
@@ -50,7 +51,7 @@ export const TVOptionButton = React.forwardRef<View, TVOptionButtonProps>(
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#444", color: "#444",
}} }}
> >
@@ -58,7 +59,7 @@ export const TVOptionButton = React.forwardRef<View, TVOptionButtonProps>(
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#000", color: "#000",
fontWeight: "500", fontWeight: "500",
}} }}
@@ -88,7 +89,7 @@ export const TVOptionButton = React.forwardRef<View, TVOptionButtonProps>(
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#bbb", color: "#bbb",
}} }}
> >
@@ -96,7 +97,7 @@ export const TVOptionButton = React.forwardRef<View, TVOptionButtonProps>(
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#E5E7EB", color: "#E5E7EB",
fontWeight: "500", fontWeight: "500",
}} }}

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVOptionCardProps { export interface TVOptionCardProps {
@@ -28,6 +28,7 @@ export const TVOptionCard = React.forwardRef<View, TVOptionCardProps>(
}, },
ref, ref,
) => { ) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 }); useTVFocusAnimation({ scaleAmount: 1.05 });
@@ -59,7 +60,7 @@ export const TVOptionCard = React.forwardRef<View, TVOptionCardProps>(
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#000" : "#fff", color: focused ? "#000" : "#fff",
fontWeight: focused || selected ? "600" : "400", fontWeight: focused || selected ? "600" : "400",
textAlign: "center", textAlign: "center",
@@ -71,7 +72,7 @@ export const TVOptionCard = React.forwardRef<View, TVOptionCardProps>(
{sublabel && ( {sublabel && (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)", color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)",
textAlign: "center", textAlign: "center",
marginTop: 2, marginTop: 2,

View File

@@ -9,7 +9,7 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { TVCancelButton } from "./TVCancelButton"; import { TVCancelButton } from "./TVCancelButton";
import { TVOptionCard } from "./TVOptionCard"; import { TVOptionCard } from "./TVOptionCard";
@@ -41,6 +41,7 @@ export const TVOptionSelector = <T,>({
cardWidth = 160, cardWidth = 160,
cardHeight = 75, cardHeight = 75,
}: TVOptionSelectorProps<T>) => { }: TVOptionSelectorProps<T>) => {
const typography = useScaledTVTypography();
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const firstCardRef = useRef<View>(null); const firstCardRef = useRef<View>(null);
@@ -91,6 +92,8 @@ export const TVOptionSelector = <T,>({
} }
}, [isReady]); }, [isReady]);
const styles = useMemo(() => createStyles(typography), [typography]);
if (!visible) return null; if (!visible) return null;
return ( return (
@@ -151,50 +154,51 @@ export const TVOptionSelector = <T,>({
); );
}; };
const styles = StyleSheet.create({ const createStyles = (typography: ReturnType<typeof useScaledTVTypography>) =>
overlay: { StyleSheet.create({
position: "absolute", overlay: {
top: 0, position: "absolute",
left: 0, top: 0,
right: 0, left: 0,
bottom: 0, right: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)", bottom: 0,
justifyContent: "flex-end", backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 1000, justifyContent: "flex-end",
}, zIndex: 1000,
sheetContainer: { },
width: "100%", sheetContainer: {
}, width: "100%",
blurContainer: { },
borderTopLeftRadius: 24, blurContainer: {
borderTopRightRadius: 24, borderTopLeftRadius: 24,
overflow: "hidden", borderTopRightRadius: 24,
}, overflow: "hidden",
content: { },
paddingTop: 24, content: {
paddingBottom: 50, paddingTop: 24,
overflow: "visible", paddingBottom: 50,
}, overflow: "visible",
title: { },
fontSize: TVTypography.callout, title: {
fontWeight: "500", fontSize: typography.callout,
color: "rgba(255,255,255,0.6)", fontWeight: "500",
marginBottom: 16, color: "rgba(255,255,255,0.6)",
paddingHorizontal: 48, marginBottom: 16,
textTransform: "uppercase", paddingHorizontal: 48,
letterSpacing: 1, textTransform: "uppercase",
}, letterSpacing: 1,
scrollView: { },
overflow: "visible", scrollView: {
}, overflow: "visible",
scrollContent: { },
paddingHorizontal: 48, scrollContent: {
paddingVertical: 20, paddingHorizontal: 48,
gap: 12, paddingVertical: 20,
}, gap: 12,
cancelButtonContainer: { },
marginTop: 16, cancelButtonContainer: {
paddingHorizontal: 48, marginTop: 16,
alignItems: "flex-start", paddingHorizontal: 48,
}, alignItems: "flex-start",
}); },
});

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { TVSeriesSeasonCard } from "./TVSeriesSeasonCard"; import { TVSeriesSeasonCard } from "./TVSeriesSeasonCard";
export interface TVSeriesNavigationProps { export interface TVSeriesNavigationProps {
@@ -16,6 +16,7 @@ export interface TVSeriesNavigationProps {
export const TVSeriesNavigation: React.FC<TVSeriesNavigationProps> = React.memo( export const TVSeriesNavigation: React.FC<TVSeriesNavigationProps> = React.memo(
({ item, seriesImageUrl, seasonImageUrl, onSeriesPress, onSeasonPress }) => { ({ item, seriesImageUrl, seasonImageUrl, onSeriesPress, onSeasonPress }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
// Only show for episodes with a series // Only show for episodes with a series
@@ -27,7 +28,7 @@ export const TVSeriesNavigation: React.FC<TVSeriesNavigationProps> = React.memo(
<View style={{ marginBottom: 32 }}> <View style={{ marginBottom: 32 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 24, marginBottom: 24,

View File

@@ -3,7 +3,7 @@ import { Image } from "expo-image";
import React from "react"; import React from "react";
import { Animated, Platform, Pressable, View } from "react-native"; import { Animated, Platform, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { import {
GlassPosterView, GlassPosterView,
isGlassEffectAvailable, isGlassEffectAvailable,
@@ -25,6 +25,7 @@ export const TVSeriesSeasonCard: React.FC<TVSeriesSeasonCardProps> = ({
onPress, onPress,
hasTVPreferredFocus, hasTVPreferredFocus,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 }); useTVFocusAnimation({ scaleAmount: 1.05 });
@@ -104,7 +105,7 @@ export const TVSeriesSeasonCard: React.FC<TVSeriesSeasonCardProps> = ({
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
fontWeight: "600", fontWeight: "600",
color: focused ? "#fff" : "rgba(255,255,255,0.9)", color: focused ? "#fff" : "rgba(255,255,255,0.9)",
textAlign: "center", textAlign: "center",
@@ -118,7 +119,7 @@ export const TVSeriesSeasonCard: React.FC<TVSeriesSeasonCardProps> = ({
{subtitle && ( {subtitle && (
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused color: focused
? "rgba(255,255,255,0.8)" ? "rgba(255,255,255,0.8)"
: "rgba(255,255,255,0.5)", : "rgba(255,255,255,0.5)",

View File

@@ -8,7 +8,7 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import type { SubtitleSearchResult } from "@/hooks/useRemoteSubtitles"; import type { SubtitleSearchResult } from "@/hooks/useRemoteSubtitles";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
@@ -23,6 +23,8 @@ export const TVSubtitleResultCard = React.forwardRef<
View, View,
TVSubtitleResultCardProps TVSubtitleResultCardProps
>(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => { >(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => {
const typography = useScaledTVTypography();
const styles = createStyles(typography);
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.03 }); useTVFocusAnimation({ scaleAmount: 1.03 });
@@ -197,72 +199,73 @@ export const TVSubtitleResultCard = React.forwardRef<
); );
}); });
const styles = StyleSheet.create({ const createStyles = (typography: ReturnType<typeof useScaledTVTypography>) =>
resultCard: { StyleSheet.create({
width: 220, resultCard: {
minHeight: 120, width: 220,
borderRadius: 14, minHeight: 120,
padding: 14, borderRadius: 14,
borderWidth: 1, padding: 14,
}, borderWidth: 1,
providerBadge: { },
alignSelf: "flex-start", providerBadge: {
paddingHorizontal: 8, alignSelf: "flex-start",
paddingVertical: 3, paddingHorizontal: 8,
borderRadius: 6, paddingVertical: 3,
marginBottom: 8, borderRadius: 6,
}, marginBottom: 8,
providerText: { },
fontSize: TVTypography.callout, providerText: {
fontWeight: "600", fontSize: typography.callout,
textTransform: "uppercase", fontWeight: "600",
letterSpacing: 0.5, textTransform: "uppercase",
}, letterSpacing: 0.5,
resultName: { },
fontSize: TVTypography.callout, resultName: {
fontWeight: "500", fontSize: typography.callout,
marginBottom: 8, fontWeight: "500",
lineHeight: 18, marginBottom: 8,
}, lineHeight: 18,
resultMeta: { },
flexDirection: "row", resultMeta: {
alignItems: "center", flexDirection: "row",
gap: 12, alignItems: "center",
marginBottom: 8, gap: 12,
}, marginBottom: 8,
resultMetaText: { },
fontSize: TVTypography.callout, resultMetaText: {
}, fontSize: typography.callout,
ratingContainer: { },
flexDirection: "row", ratingContainer: {
alignItems: "center", flexDirection: "row",
gap: 3, alignItems: "center",
}, gap: 3,
downloadCountContainer: { },
flexDirection: "row", downloadCountContainer: {
alignItems: "center", flexDirection: "row",
gap: 3, alignItems: "center",
}, gap: 3,
flagsContainer: { },
flexDirection: "row", flagsContainer: {
gap: 6, flexDirection: "row",
flexWrap: "wrap", gap: 6,
}, flexWrap: "wrap",
flag: { },
paddingHorizontal: 6, flag: {
paddingVertical: 2, paddingHorizontal: 6,
borderRadius: 4, paddingVertical: 2,
}, borderRadius: 4,
flagText: { },
fontSize: TVTypography.callout, flagText: {
fontWeight: "600", fontSize: typography.callout,
color: "#fff", fontWeight: "600",
}, color: "#fff",
downloadingOverlay: { },
...StyleSheet.absoluteFillObject, downloadingOverlay: {
backgroundColor: "rgba(0,0,0,0.5)", ...StyleSheet.absoluteFillObject,
borderRadius: 14, backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "center", borderRadius: 14,
alignItems: "center", justifyContent: "center",
}, alignItems: "center",
}); },
});

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Animated, Pressable } from "react-native"; import { Animated, Pressable } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVTabButtonProps { export interface TVTabButtonProps {
@@ -21,6 +21,7 @@ export const TVTabButton: React.FC<TVTabButtonProps> = ({
switchOnFocus = false, switchOnFocus = false,
disabled = false, disabled = false,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ useTVFocusAnimation({
scaleAmount: 1.05, scaleAmount: 1.05,
@@ -56,7 +57,7 @@ export const TVTabButton: React.FC<TVTabButtonProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: focused ? "#000" : "#fff", color: focused ? "#000" : "#fff",
fontWeight: focused || active ? "600" : "400", fontWeight: focused || active ? "600" : "400",
}} }}

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVTechnicalDetailsProps { export interface TVTechnicalDetailsProps {
mediaStreams: MediaStream[]; mediaStreams: MediaStream[];
@@ -11,6 +11,7 @@ export interface TVTechnicalDetailsProps {
export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo( export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo(
({ mediaStreams }) => { ({ mediaStreams }) => {
const typography = useScaledTVTypography();
const { t } = useTranslation(); const { t } = useTranslation();
const videoStream = mediaStreams.find((s) => s.Type === "Video"); const videoStream = mediaStreams.find((s) => s.Type === "Video");
@@ -24,7 +25,7 @@ export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo(
<View style={{ marginBottom: 32 }}> <View style={{ marginBottom: 32 }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.heading, fontSize: typography.heading,
fontWeight: "600", fontWeight: "600",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 20, marginBottom: 20,
@@ -37,7 +38,7 @@ export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo(
<View> <View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#6B7280", color: "#6B7280",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
@@ -46,7 +47,7 @@ export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo(
> >
Video Video
</Text> </Text>
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{videoStream.DisplayTitle || {videoStream.DisplayTitle ||
`${videoStream.Codec?.toUpperCase()} ${videoStream.Width}x${videoStream.Height}`} `${videoStream.Codec?.toUpperCase()} ${videoStream.Width}x${videoStream.Height}`}
</Text> </Text>
@@ -56,7 +57,7 @@ export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo(
<View> <View>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#6B7280", color: "#6B7280",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
@@ -65,7 +66,7 @@ export const TVTechnicalDetails: React.FC<TVTechnicalDetailsProps> = React.memo(
> >
Audio Audio
</Text> </Text>
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{audioStream.DisplayTitle || {audioStream.DisplayTitle ||
`${audioStream.Codec?.toUpperCase()} ${audioStream.Channels}ch`} `${audioStream.Codec?.toUpperCase()} ${audioStream.Channels}ch`}
</Text> </Text>

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable, StyleSheet, View } from "react-native"; import { Animated, Pressable, StyleSheet, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
export interface TVTrackCardProps { export interface TVTrackCardProps {
@@ -15,6 +15,8 @@ export interface TVTrackCardProps {
export const TVTrackCard = React.forwardRef<View, TVTrackCardProps>( export const TVTrackCard = React.forwardRef<View, TVTrackCardProps>(
({ label, sublabel, selected, hasTVPreferredFocus, onPress }, ref) => { ({ label, sublabel, selected, hasTVPreferredFocus, onPress }, ref) => {
const typography = useScaledTVTypography();
const styles = createStyles(typography);
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 }); useTVFocusAnimation({ scaleAmount: 1.05 });
@@ -77,26 +79,27 @@ export const TVTrackCard = React.forwardRef<View, TVTrackCardProps>(
}, },
); );
const styles = StyleSheet.create({ const createStyles = (typography: ReturnType<typeof useScaledTVTypography>) =>
trackCard: { StyleSheet.create({
width: 180, trackCard: {
height: 80, width: 180,
borderRadius: 14, height: 80,
justifyContent: "center", borderRadius: 14,
alignItems: "center", justifyContent: "center",
paddingHorizontal: 12, alignItems: "center",
}, paddingHorizontal: 12,
trackCardText: { },
fontSize: TVTypography.callout, trackCardText: {
textAlign: "center", fontSize: typography.callout,
}, textAlign: "center",
trackCardSublabel: { },
fontSize: TVTypography.callout, trackCardSublabel: {
marginTop: 2, fontSize: typography.callout,
}, marginTop: 2,
checkmark: { },
position: "absolute", checkmark: {
top: 8, position: "absolute",
right: 8, top: 8,
}, right: 8,
}); },
});

View File

@@ -2,7 +2,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVLogoutButtonProps { export interface TVLogoutButtonProps {
@@ -15,6 +15,7 @@ export const TVLogoutButton: React.FC<TVLogoutButtonProps> = ({
disabled, disabled,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 }); useTVFocusAnimation({ scaleAmount: 1.05 });
@@ -49,7 +50,7 @@ export const TVLogoutButton: React.FC<TVLogoutButtonProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
}} }}

View File

@@ -1,24 +1,28 @@
import React from "react"; import React from "react";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVSectionHeaderProps { export interface TVSectionHeaderProps {
title: string; title: string;
} }
export const TVSectionHeader: React.FC<TVSectionHeaderProps> = ({ title }) => ( export const TVSectionHeader: React.FC<TVSectionHeaderProps> = ({ title }) => {
<Text const typography = useScaledTVTypography();
style={{
fontSize: TVTypography.callout, return (
fontWeight: "600", <Text
color: "#9CA3AF", style={{
textTransform: "uppercase", fontSize: typography.callout,
letterSpacing: 1, fontWeight: "600",
marginTop: 32, color: "#9CA3AF",
marginBottom: 16, textTransform: "uppercase",
marginLeft: 8, letterSpacing: 1,
}} marginTop: 32,
> marginBottom: 16,
{title} marginLeft: 8,
</Text> }}
); >
{title}
</Text>
);
};

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsOptionButtonProps { export interface TVSettingsOptionButtonProps {
@@ -20,6 +20,7 @@ export const TVSettingsOptionButton: React.FC<TVSettingsOptionButtonProps> = ({
isFirst, isFirst,
disabled, disabled,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 }); useTVFocusAnimation({ scaleAmount: 1.02 });
@@ -49,13 +50,13 @@ export const TVSettingsOptionButton: React.FC<TVSettingsOptionButtonProps> = ({
}, },
]} ]}
> >
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{label} {label}
</Text> </Text>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginRight: 12, marginRight: 12,
}} }}

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsRowProps { export interface TVSettingsRowProps {
@@ -22,6 +22,7 @@ export const TVSettingsRow: React.FC<TVSettingsRowProps> = ({
showChevron = true, showChevron = true,
disabled, disabled,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 }); useTVFocusAnimation({ scaleAmount: 1.02 });
@@ -51,13 +52,13 @@ export const TVSettingsRow: React.FC<TVSettingsRowProps> = ({
}, },
]} ]}
> >
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{label} {label}
</Text> </Text>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginRight: showChevron ? 12 : 0, marginRight: showChevron ? 12 : 0,
}} }}

View File

@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsStepperProps { export interface TVSettingsStepperProps {
@@ -24,6 +24,7 @@ export const TVSettingsStepper: React.FC<TVSettingsStepperProps> = ({
isFirst, isFirst,
disabled, disabled,
}) => { }) => {
const typography = useScaledTVTypography();
const labelAnim = useTVFocusAnimation({ scaleAmount: 1.02 }); const labelAnim = useTVFocusAnimation({ scaleAmount: 1.02 });
const minusAnim = useTVFocusAnimation({ scaleAmount: 1.1 }); const minusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
const plusAnim = useTVFocusAnimation({ scaleAmount: 1.1 }); const plusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
@@ -54,7 +55,7 @@ export const TVSettingsStepper: React.FC<TVSettingsStepperProps> = ({
focusable={!disabled} focusable={!disabled}
> >
<Animated.View style={labelAnim.animatedStyle}> <Animated.View style={labelAnim.animatedStyle}>
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{label} {label}
</Text> </Text>
</Animated.View> </Animated.View>
@@ -89,7 +90,7 @@ export const TVSettingsStepper: React.FC<TVSettingsStepperProps> = ({
</Pressable> </Pressable>
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#FFFFFF", color: "#FFFFFF",
minWidth: 60, minWidth: 60,
textAlign: "center", textAlign: "center",

View File

@@ -1,7 +1,7 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { Animated, Pressable, TextInput } from "react-native"; import { Animated, Pressable, TextInput } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsTextInputProps { export interface TVSettingsTextInputProps {
@@ -23,6 +23,7 @@ export const TVSettingsTextInput: React.FC<TVSettingsTextInputProps> = ({
secureTextEntry, secureTextEntry,
disabled, disabled,
}) => { }) => {
const typography = useScaledTVTypography();
const inputRef = useRef<TextInput>(null); const inputRef = useRef<TextInput>(null);
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 }); useTVFocusAnimation({ scaleAmount: 1.02 });
@@ -56,7 +57,7 @@ export const TVSettingsTextInput: React.FC<TVSettingsTextInputProps> = ({
> >
<Text <Text
style={{ style={{
fontSize: TVTypography.callout, fontSize: typography.callout,
color: "#9CA3AF", color: "#9CA3AF",
marginBottom: 8, marginBottom: 8,
}} }}
@@ -74,7 +75,7 @@ export const TVSettingsTextInput: React.FC<TVSettingsTextInputProps> = ({
autoCapitalize='none' autoCapitalize='none'
autoCorrect={false} autoCorrect={false}
style={{ style={{
fontSize: TVTypography.body, fontSize: typography.body,
color: "#FFFFFF", color: "#FFFFFF",
backgroundColor: "rgba(255, 255, 255, 0.05)", backgroundColor: "rgba(255, 255, 255, 0.05)",
borderRadius: 8, borderRadius: 8,

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Animated, Pressable, View } from "react-native"; import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation"; import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsToggleProps { export interface TVSettingsToggleProps {
@@ -19,6 +19,7 @@ export const TVSettingsToggle: React.FC<TVSettingsToggleProps> = ({
isFirst, isFirst,
disabled, disabled,
}) => { }) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } = const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 }); useTVFocusAnimation({ scaleAmount: 1.02 });
@@ -48,7 +49,7 @@ export const TVSettingsToggle: React.FC<TVSettingsToggleProps> = ({
}, },
]} ]}
> >
<Text style={{ fontSize: TVTypography.body, color: "#FFFFFF" }}> <Text style={{ fontSize: typography.body, color: "#FFFFFF" }}>
{label} {label}
</Text> </Text>
<View <View

View File

@@ -31,7 +31,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVControlButton, TVNextEpisodeCountdown } from "@/components/tv"; import { TVControlButton, TVNextEpisodeCountdown } from "@/components/tv";
import { TVFocusableProgressBar } from "@/components/tv/TVFocusableProgressBar"; import { TVFocusableProgressBar } from "@/components/tv/TVFocusableProgressBar";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useTrickplay } from "@/hooks/useTrickplay"; import { useTrickplay } from "@/hooks/useTrickplay";
@@ -206,6 +206,7 @@ export const Controls: FC<Props> = ({
playMethod, playMethod,
transcodeReasons, transcodeReasons,
}) => { }) => {
const typography = useScaledTVTypography();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { width: screenWidth } = useWindowDimensions(); const { width: screenWidth } = useWindowDimensions();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -973,14 +974,16 @@ export const Controls: FC<Props> = ({
</View> </View>
<View style={styles.timeContainer}> <View style={styles.timeContainer}>
<Text style={styles.timeText}> <Text style={[styles.timeText, { fontSize: typography.body }]}>
{formatTimeString(currentTime, "ms")} {formatTimeString(currentTime, "ms")}
</Text> </Text>
<View style={styles.timeRight}> <View style={styles.timeRight}>
<Text style={styles.timeText}> <Text style={[styles.timeText, { fontSize: typography.body }]}>
-{formatTimeString(remainingTime, "ms")} -{formatTimeString(remainingTime, "ms")}
</Text> </Text>
<Text style={styles.endsAtText}> <Text
style={[styles.endsAtText, { fontSize: typography.callout }]}
>
{t("player.ends_at")} {getFinishTime()} {t("player.ends_at")} {getFinishTime()}
</Text> </Text>
</View> </View>
@@ -1006,12 +1009,18 @@ export const Controls: FC<Props> = ({
<View style={styles.metadataContainer}> <View style={styles.metadataContainer}>
{item?.Type === "Episode" && ( {item?.Type === "Episode" && (
<Text <Text
style={styles.subtitleText} style={[styles.subtitleText, { fontSize: typography.body }]}
>{`${item.SeriesName} - ${item.SeasonName} Episode ${item.IndexNumber}`}</Text> >{`${item.SeriesName} - ${item.SeasonName} Episode ${item.IndexNumber}`}</Text>
)} )}
<Text style={styles.titleText}>{item?.Name}</Text> <Text style={[styles.titleText, { fontSize: typography.heading }]}>
{item?.Name}
</Text>
{item?.Type === "Movie" && ( {item?.Type === "Movie" && (
<Text style={styles.subtitleText}>{item?.ProductionYear}</Text> <Text
style={[styles.subtitleText, { fontSize: typography.body }]}
>
{item?.ProductionYear}
</Text>
)} )}
</View> </View>
@@ -1110,14 +1119,16 @@ export const Controls: FC<Props> = ({
</TVFocusGuideView> </TVFocusGuideView>
<View style={styles.timeContainer}> <View style={styles.timeContainer}>
<Text style={styles.timeText}> <Text style={[styles.timeText, { fontSize: typography.body }]}>
{formatTimeString(currentTime, "ms")} {formatTimeString(currentTime, "ms")}
</Text> </Text>
<View style={styles.timeRight}> <View style={styles.timeRight}>
<Text style={styles.timeText}> <Text style={[styles.timeText, { fontSize: typography.body }]}>
-{formatTimeString(remainingTime, "ms")} -{formatTimeString(remainingTime, "ms")}
</Text> </Text>
<Text style={styles.endsAtText}> <Text
style={[styles.endsAtText, { fontSize: typography.callout }]}
>
{t("player.ends_at")} {getFinishTime()} {t("player.ends_at")} {getFinishTime()}
</Text> </Text>
</View> </View>
@@ -1151,11 +1162,9 @@ const styles = StyleSheet.create({
}, },
subtitleText: { subtitleText: {
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
fontSize: TVTypography.body,
}, },
titleText: { titleText: {
color: "#fff", color: "#fff",
fontSize: TVTypography.heading,
fontWeight: "bold", fontWeight: "bold",
}, },
controlButtonsRow: { controlButtonsRow: {
@@ -1218,7 +1227,6 @@ const styles = StyleSheet.create({
}, },
timeText: { timeText: {
color: "rgba(255,255,255,0.7)", color: "rgba(255,255,255,0.7)",
fontSize: TVTypography.body,
}, },
timeRight: { timeRight: {
flexDirection: "column", flexDirection: "column",
@@ -1226,7 +1234,6 @@ const styles = StyleSheet.create({
}, },
endsAtText: { endsAtText: {
color: "rgba(255,255,255,0.5)", color: "rgba(255,255,255,0.5)",
fontSize: TVTypography.callout,
marginTop: 2, marginTop: 2,
}, },
// Minimal seek bar styles // Minimal seek bar styles

View File

@@ -15,7 +15,7 @@ import Animated, {
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { TVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import type { TechnicalInfo } from "@/modules/mpv-player"; import type { TechnicalInfo } from "@/modules/mpv-player";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { HEADER_LAYOUT } from "./constants"; import { HEADER_LAYOUT } from "./constants";
@@ -183,6 +183,7 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
currentSubtitleIndex, currentSubtitleIndex,
currentAudioIndex, currentAudioIndex,
}) => { }) => {
const typography = useScaledTVTypography();
const { settings } = useSettings(); const { settings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [info, setInfo] = useState<TechnicalInfo | null>(null); const [info, setInfo] = useState<TechnicalInfo | null>(null);
@@ -277,8 +278,15 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
: HEADER_LAYOUT.CONTAINER_PADDING + 20, : HEADER_LAYOUT.CONTAINER_PADDING + 20,
}; };
const textStyle = Platform.isTV ? styles.infoTextTV : styles.infoText; const textStyle = Platform.isTV
const reasonStyle = Platform.isTV ? styles.reasonTextTV : styles.reasonText; ? [
styles.infoTextTV,
{ fontSize: typography.body, lineHeight: typography.body * 1.5 },
]
: styles.infoText;
const reasonStyle = Platform.isTV
? [styles.reasonTextTV, { fontSize: typography.callout }]
: styles.reasonText;
const boxStyle = Platform.isTV ? styles.infoBoxTV : styles.infoBox; const boxStyle = Platform.isTV ? styles.infoBoxTV : styles.infoBox;
return ( return (
@@ -383,9 +391,7 @@ const styles = StyleSheet.create({
}, },
infoTextTV: { infoTextTV: {
color: "white", color: "white",
fontSize: TVTypography.body,
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace", fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
lineHeight: TVTypography.body * 1.5,
}, },
warningText: { warningText: {
color: "#ff9800", color: "#ff9800",
@@ -396,6 +402,5 @@ const styles = StyleSheet.create({
}, },
reasonTextTV: { reasonTextTV: {
color: "#fbbf24", color: "#fbbf24",
fontSize: TVTypography.callout,
}, },
}); });

View File

@@ -1,3 +1,5 @@
import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
/** /**
* TV Typography Scale * TV Typography Scale
* *
@@ -23,3 +25,29 @@ export const TVTypography = {
} as const; } as const;
export type TVTypographyKey = keyof typeof TVTypography; export type TVTypographyKey = keyof typeof TVTypography;
const scaleMultipliers: Record<TVTypographyScale, number> = {
[TVTypographyScale.Small]: 0.85,
[TVTypographyScale.Default]: 1.0,
[TVTypographyScale.Large]: 1.15,
[TVTypographyScale.ExtraLarge]: 1.3,
};
/**
* Hook that returns scaled TV typography values based on user settings.
* Use this instead of the static TVTypography constant for dynamic scaling.
*/
export const useScaledTVTypography = () => {
const { settings } = useSettings();
const scale =
scaleMultipliers[settings.tvTypographyScale] ??
scaleMultipliers[TVTypographyScale.Default];
return {
display: Math.round(TVTypography.display * scale),
title: Math.round(TVTypography.title * scale),
heading: Math.round(TVTypography.heading * scale),
body: Math.round(TVTypography.body * scale),
callout: Math.round(TVTypography.callout * scale),
};
};

View File

@@ -126,7 +126,12 @@
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button",
"show_home_backdrop": "Dynamic Home Backdrop", "show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel" "show_hero_carousel": "Hero Carousel",
"text_size": "Text Size",
"text_size_small": "Small",
"text_size_default": "Default",
"text_size_large": "Large",
"text_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",

View File

@@ -134,6 +134,14 @@ export enum VideoPlayer {
MPV = 0, MPV = 0,
} }
// TV Typography scale presets
export enum TVTypographyScale {
Small = "small",
Default = "default",
Large = "large",
ExtraLarge = "extraLarge",
}
// Audio transcoding mode - controls how surround audio is handled // Audio transcoding mode - controls how surround audio is handled
// This controls server-side transcoding behavior for audio streams. // This controls server-side transcoding behavior for audio streams.
// MPV decodes via FFmpeg and supports most formats, but mobile devices // MPV decodes via FFmpeg and supports most formats, but mobile devices
@@ -202,6 +210,7 @@ export type Settings = {
// TV-specific settings // TV-specific settings
showHomeBackdrop: boolean; showHomeBackdrop: boolean;
showTVHeroCarousel: boolean; showTVHeroCarousel: boolean;
tvTypographyScale: TVTypographyScale;
// Appearance // Appearance
hideRemoteSessionButton: boolean; hideRemoteSessionButton: boolean;
hideWatchlistsTab: boolean; hideWatchlistsTab: boolean;
@@ -291,6 +300,7 @@ export const defaultValues: Settings = {
// TV-specific settings // TV-specific settings
showHomeBackdrop: true, showHomeBackdrop: true,
showTVHeroCarousel: true, showTVHeroCarousel: true,
tvTypographyScale: TVTypographyScale.Default,
// Appearance // Appearance
hideRemoteSessionButton: false, hideRemoteSessionButton: false,
hideWatchlistsTab: false, hideWatchlistsTab: false,