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";
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
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() {
const { t } = useTranslation();
@@ -39,6 +43,8 @@ export default function SettingsTV() {
settings.subtitleMode || SubtitlePlaybackMode.Default;
const currentAlignX = settings.mpvSubtitleAlignX ?? "center";
const currentAlignY = settings.mpvSubtitleAlignY ?? "bottom";
const currentTypographyScale =
settings.tvTypographyScale || TVTypographyScale.Default;
// Audio transcoding options
const audioTranscodeModeOptions: TVOptionItem<AudioTranscodeMode>[] = useMemo(
@@ -130,6 +136,33 @@ export default function SettingsTV() {
[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
const audioTranscodeLabel = useMemo(() => {
const option = audioTranscodeModeOptions.find((o) => o.selected);
@@ -151,6 +184,11 @@ export default function SettingsTV() {
return option?.label || "Bottom";
}, [alignYOptions]);
const typographyScaleLabel = useMemo(() => {
const option = typographyScaleOptions.find((o) => o.selected);
return option?.label || t("home.settings.appearance.text_size_default");
}, [typographyScaleOptions, t]);
return (
<View style={{ flex: 1, backgroundColor: "#000000" }}>
<View style={{ flex: 1 }}>
@@ -344,6 +382,18 @@ export default function SettingsTV() {
{/* Appearance Section */}
<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
label={t(
"home.settings.appearance.merge_next_up_continue_watching",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
type ViewProps,
} from "react-native";
import { GlassEffectView } from "react-native-glass-effect-view";
import { TVTypography } from "@/constants/TVTypography";
import { useScaledTVTypography } from "@/constants/TVTypography";
import { Text } from "./common/Text";
interface TagProps {
@@ -25,6 +25,9 @@ export const Tag: React.FC<
textStyle?: StyleProp<TextStyle>;
} & ViewProps
> = ({ 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) {
return (
<View>
@@ -60,7 +63,7 @@ export const Tag: React.FC<
backgroundColor: "rgba(0,0,0,0.3)",
}}
>
<Text style={{ fontSize: TVTypography.callout, color: "#E5E7EB" }}>
<Text style={{ fontSize: typography.callout, color: "#E5E7EB" }}>
{text}
</Text>
</View>

View File

@@ -37,7 +37,7 @@ import {
TVTechnicalDetails,
} from "@/components/tv";
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 useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
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 const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
({ item, itemWithSources }) => {
const typography = useScaledTVTypography();
const [api] = useAtom(apiAtom);
const [_user] = useAtom(userAtom);
const isOffline = useOfflineMode();
@@ -484,7 +485,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
) : (
<Text
style={{
fontSize: TVTypography.display,
fontSize: typography.display,
fontWeight: "bold",
color: "#FFFFFF",
marginBottom: 20,
@@ -500,7 +501,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
<View style={{ marginBottom: 16 }}>
<Text
style={{
fontSize: TVTypography.title,
fontSize: typography.title,
color: "#FFFFFF",
fontWeight: "600",
}}
@@ -509,7 +510,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
</Text>
<Text
style={{
fontSize: TVTypography.body,
fontSize: typography.body,
color: "white",
marginTop: 6,
}}
@@ -554,7 +555,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
>
<Text
style={{
fontSize: TVTypography.body,
fontSize: typography.body,
color: "#E5E7EB",
lineHeight: 32,
}}
@@ -587,7 +588,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
/>
<Text
style={{
fontSize: TVTypography.callout,
fontSize: typography.callout,
fontWeight: "bold",
color: "#000000",
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import React from "react";
import { Animated, Pressable, ScrollView, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { TVTypography } from "@/constants/TVTypography";
import { useScaledTVTypography } from "@/constants/TVTypography";
interface ToggleItem {
id: number;
@@ -21,6 +21,7 @@ const TVToggleChip: React.FC<TVToggleChipProps> = ({
onToggle,
disabled = false,
}) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({
scaleAmount: 1.08,
@@ -57,7 +58,7 @@ const TVToggleChip: React.FC<TVToggleChipProps> = ({
>
<Text
style={{
fontSize: TVTypography.callout,
fontSize: typography.callout,
color: focused ? "#000" : "#fff",
fontWeight: item.selected || focused ? "600" : "400",
}}
@@ -82,13 +83,14 @@ export const TVToggleOptionRow: React.FC<TVToggleOptionRowProps> = ({
onToggle,
disabled = false,
}) => {
const typography = useScaledTVTypography();
if (items.length === 0) return null;
return (
<View style={{ marginBottom: 16 }}>
<Text
style={{
fontSize: TVTypography.callout,
fontSize: typography.callout,
color: "rgba(255,255,255,0.6)",
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 { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
@@ -55,6 +56,7 @@ const TVLibraryRow: React.FC<{
}> = ({ library, isFirst, onPress }) => {
const [api] = useAtom(apiAtom);
const { t } = useTranslation();
const typography = useScaledTVTypography();
const [focused, setFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current;
const opacity = useRef(new Animated.Value(0.7)).current;
@@ -190,7 +192,7 @@ const TVLibraryRow: React.FC<{
<Text
numberOfLines={1}
style={{
fontSize: 32,
fontSize: typography.heading,
fontWeight: "700",
color: "#FFFFFF",
textShadowColor: "rgba(0,0,0,0.8)",
@@ -203,7 +205,7 @@ const TVLibraryRow: React.FC<{
{library.itemCount !== undefined && (
<Text
style={{
fontSize: 18,
fontSize: typography.body,
color: "rgba(255,255,255,0.7)",
marginTop: 4,
textShadowColor: "rgba(0,0,0,0.8)",
@@ -237,6 +239,7 @@ export const TVLibraries: React.FC = () => {
const insets = useSafeAreaInsets();
const router = useRouter();
const { t } = useTranslation();
const typography = useScaledTVTypography();
const { data: userViews, isLoading: viewsLoading } = useQuery({
queryKey: ["user-views", user?.Id],
@@ -360,7 +363,7 @@ export const TVLibraries: React.FC = () => {
alignItems: "center",
}}
>
<Text style={{ fontSize: 20, color: "#737373" }}>
<Text style={{ fontSize: typography.body, color: "#737373" }}>
{t("library.no_libraries_found")}
</Text>
</View>

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Animated, Pressable } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
import { useScaledTVTypography } from "@/constants/TVTypography";
export interface TVSearchBadgeProps {
label: string;
@@ -14,6 +15,7 @@ export const TVSearchBadge: React.FC<TVSearchBadgeProps> = ({
onPress,
hasTVPreferredFocus = false,
}) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08, duration: 150 });
@@ -41,7 +43,7 @@ export const TVSearchBadge: React.FC<TVSearchBadgeProps> = ({
>
<Text
style={{
fontSize: 16,
fontSize: typography.callout,
color: focused ? "#000" : "#fff",
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 { Text } from "@/components/common/Text";
import { TVDiscover } from "@/components/jellyseerr/discover/TVDiscover";
import { TVTypography } from "@/constants/TVTypography";
import { useScaledTVTypography } from "@/constants/TVTypography";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
@@ -27,6 +27,7 @@ const SCALE_PADDING = 20;
// Loading skeleton for TV
const TVLoadingSkeleton: React.FC = () => {
const typography = useScaledTVTypography();
const itemWidth = 210;
return (
<View style={{ overflow: "visible" }}>
@@ -72,7 +73,7 @@ const TVLoadingSkeleton: React.FC = () => {
color: "#262626",
backgroundColor: "#262626",
borderRadius: 6,
fontSize: TVTypography.callout,
fontSize: typography.callout,
}}
numberOfLines={1}
>
@@ -150,6 +151,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
onJellyseerrPersonPress,
discoverSliders,
}) => {
const typography = useScaledTVTypography();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [api] = useAtom(apiAtom);
@@ -308,7 +310,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
<View style={{ alignItems: "center", paddingTop: 40 }}>
<Text
style={{
fontSize: TVTypography.heading,
fontSize: typography.heading,
fontWeight: "bold",
color: "#FFFFFF",
marginBottom: 8,
@@ -318,7 +320,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
</Text>
<Text
style={{
fontSize: TVTypography.body,
fontSize: typography.body,
color: "rgba(255,255,255,0.6)",
}}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
import { TVTypography } from "@/constants/TVTypography";
import { useScaledTVTypography } from "@/constants/TVTypography";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsToggleProps {
@@ -19,6 +19,7 @@ export const TVSettingsToggle: React.FC<TVSettingsToggleProps> = ({
isFirst,
disabled,
}) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } =
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}
</Text>
<View

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
/**
* TV Typography Scale
*
@@ -23,3 +25,29 @@ export const TVTypography = {
} as const;
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",
"hide_remote_session_button": "Hide Remote Session Button",
"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": {
"title": "Network",

View File

@@ -134,6 +134,14 @@ export enum VideoPlayer {
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
// This controls server-side transcoding behavior for audio streams.
// MPV decodes via FFmpeg and supports most formats, but mobile devices
@@ -202,6 +210,7 @@ export type Settings = {
// TV-specific settings
showHomeBackdrop: boolean;
showTVHeroCarousel: boolean;
tvTypographyScale: TVTypographyScale;
// Appearance
hideRemoteSessionButton: boolean;
hideWatchlistsTab: boolean;
@@ -291,6 +300,7 @@ export const defaultValues: Settings = {
// TV-specific settings
showHomeBackdrop: true,
showTVHeroCarousel: true,
tvTypographyScale: TVTypographyScale.Default,
// Appearance
hideRemoteSessionButton: false,
hideWatchlistsTab: false,