chore: more scaling fixes and selection improve

Fixed the scaling in the direct player controls to use the scaleTV
settings
Fixed 2 items in settings not being selectable (added style:flex:1)

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
Lance Chant
2026-05-25 15:12:44 +02:00
parent 6b0f8b833f
commit 5ede3f30d0
4 changed files with 216 additions and 147 deletions

View File

@@ -1,6 +1,6 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
Animated, Animated,
Easing, Easing,
@@ -11,13 +11,17 @@ import {
} from "react-native"; } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVOptionCard } from "@/components/tv"; import { TVOptionCard } from "@/components/tv";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useTVBackPress } from "@/hooks/useTVBackPress";
import { tvOptionModalAtom } from "@/utils/atoms/tvOptionModal"; import { tvOptionModalAtom } from "@/utils/atoms/tvOptionModal";
import { scaleSize } from "@/utils/scaleSize";
import { store } from "@/utils/store"; import { store } from "@/utils/store";
export default function TVOptionModal() { export default function TVOptionModal() {
const router = useRouter(); const router = useRouter();
const modalState = useAtomValue(tvOptionModalAtom); const modalState = useAtomValue(tvOptionModalAtom);
const typography = useScaledTVTypography();
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const firstCardRef = useRef<View>(null); const firstCardRef = useRef<View>(null);
@@ -76,12 +80,25 @@ export default function TVOptionModal() {
router.back(); router.back();
}; };
const handleClose = useCallback(() => {
store.set(tvOptionModalAtom, null);
router.back();
}, [router]);
// Intercept back/menu press to close the modal instead of the player
useTVBackPress(() => {
handleClose();
return true;
}, [handleClose]);
// If no modal state, just go back (shouldn't happen in normal usage) // If no modal state, just go back (shouldn't happen in normal usage)
if (!modalState) { if (!modalState) {
return null; return null;
} }
const { title, options, cardWidth = 160, cardHeight = 75 } = modalState; const { title, options } = modalState;
const scaledCardWidth = scaleSize(160);
const scaledCardHeight = scaleSize(75);
return ( return (
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}> <Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
@@ -100,7 +117,9 @@ export default function TVOptionModal() {
trapFocusRight trapFocusRight
style={styles.content} style={styles.content}
> >
<Text style={styles.title}>{title}</Text> <Text style={[styles.title, { fontSize: typography.callout }]}>
{title}
</Text>
{isReady && ( {isReady && (
<ScrollView <ScrollView
horizontal horizontal
@@ -119,8 +138,8 @@ export default function TVOptionModal() {
selected={option.selected} selected={option.selected}
hasTVPreferredFocus={index === initialSelectedIndex} hasTVPreferredFocus={index === initialSelectedIndex}
onPress={() => handleSelect(option.value)} onPress={() => handleSelect(option.value)}
width={cardWidth} width={scaledCardWidth}
height={cardHeight} height={scaledCardHeight}
/> />
))} ))}
</ScrollView> </ScrollView>
@@ -142,21 +161,20 @@ const styles = StyleSheet.create({
width: "100%", width: "100%",
}, },
blurContainer: { blurContainer: {
borderTopLeftRadius: 24, borderTopLeftRadius: scaleSize(24),
borderTopRightRadius: 24, borderTopRightRadius: scaleSize(24),
overflow: "hidden", overflow: "hidden",
}, },
content: { content: {
paddingTop: 24, paddingTop: scaleSize(24),
paddingBottom: 50, paddingBottom: scaleSize(50),
overflow: "visible", overflow: "visible",
}, },
title: { title: {
fontSize: 18,
fontWeight: "500", fontWeight: "500",
color: "rgba(255,255,255,0.6)", color: "rgba(255,255,255,0.6)",
marginBottom: 16, marginBottom: scaleSize(16),
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
}, },
@@ -164,8 +182,8 @@ const styles = StyleSheet.create({
overflow: "visible", overflow: "visible",
}, },
scrollContent: { scrollContent: {
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
paddingVertical: 20, paddingVertical: scaleSize(20),
gap: 12, gap: scaleSize(12),
}, },
}); });

View File

@@ -22,14 +22,17 @@ import {
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TVTabButton, useTVFocusAnimation } from "@/components/tv"; import { TVTabButton, useTVFocusAnimation } from "@/components/tv";
import type { Track } from "@/components/video-player/controls/types"; import type { Track } from "@/components/video-player/controls/types";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { import {
type SubtitleSearchResult, type SubtitleSearchResult,
useRemoteSubtitles, useRemoteSubtitles,
} from "@/hooks/useRemoteSubtitles"; } from "@/hooks/useRemoteSubtitles";
import { useTVBackPress } from "@/hooks/useTVBackPress";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { tvSubtitleModalAtom } from "@/utils/atoms/tvSubtitleModal"; import { tvSubtitleModalAtom } from "@/utils/atoms/tvSubtitleModal";
import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api"; import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api";
import { scaleSize } from "@/utils/scaleSize";
import { store } from "@/utils/store"; import { store } from "@/utils/store";
type TabType = "tracks" | "download" | "settings"; type TabType = "tracks" | "download" | "settings";
@@ -72,10 +75,10 @@ const TVTrackCard = React.forwardRef<
<Text <Text
style={[ style={[
styles.trackCardText, styles.trackCardText,
{ color: focused ? "#000" : "#fff" }, { color: focused ? "#000" : "#fff", fontSize: scaleSize(16) },
(focused || selected) && { fontWeight: "600" }, (focused || selected) && { fontWeight: "600" },
]} ]}
numberOfLines={2} numberOfLines={3}
> >
{label} {label}
</Text> </Text>
@@ -83,7 +86,10 @@ const TVTrackCard = React.forwardRef<
<Text <Text
style={[ style={[
styles.trackCardSublabel, styles.trackCardSublabel,
{ color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)" }, {
color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)",
fontSize: scaleSize(12),
},
]} ]}
numberOfLines={1} numberOfLines={1}
> >
@@ -94,7 +100,7 @@ const TVTrackCard = React.forwardRef<
<View style={styles.checkmark}> <View style={styles.checkmark}>
<Ionicons <Ionicons
name='checkmark' name='checkmark'
size={16} size={scaleSize(16)}
color='rgba(255,255,255,0.8)' color='rgba(255,255,255,0.8)'
/> />
</View> </View>
@@ -142,7 +148,7 @@ const LanguageCard = React.forwardRef<
<Text <Text
style={[ style={[
styles.languageCardText, styles.languageCardText,
{ color: focused ? "#000" : "#fff" }, { color: focused ? "#000" : "#fff", fontSize: scaleSize(15) },
(focused || selected) && { fontWeight: "600" }, (focused || selected) && { fontWeight: "600" },
]} ]}
numberOfLines={1} numberOfLines={1}
@@ -152,7 +158,10 @@ const LanguageCard = React.forwardRef<
<Text <Text
style={[ style={[
styles.languageCardCode, styles.languageCardCode,
{ color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)" }, {
color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)",
fontSize: scaleSize(11),
},
]} ]}
> >
{code.toUpperCase()} {code.toUpperCase()}
@@ -161,7 +170,7 @@ const LanguageCard = React.forwardRef<
<View style={styles.checkmark}> <View style={styles.checkmark}>
<Ionicons <Ionicons
name='checkmark' name='checkmark'
size={16} size={scaleSize(16)}
color='rgba(255,255,255,0.8)' color='rgba(255,255,255,0.8)'
/> />
</View> </View>
@@ -219,7 +228,10 @@ const SubtitleResultCard = React.forwardRef<
<Text <Text
style={[ style={[
styles.providerText, styles.providerText,
{ color: focused ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.7)" }, {
color: focused ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.7)",
fontSize: scaleSize(11),
},
]} ]}
> >
{result.providerName} {result.providerName}
@@ -228,7 +240,10 @@ const SubtitleResultCard = React.forwardRef<
{/* Name */} {/* Name */}
<Text <Text
style={[styles.resultName, { color: focused ? "#000" : "#fff" }]} style={[
styles.resultName,
{ color: focused ? "#000" : "#fff", fontSize: scaleSize(14) },
]}
numberOfLines={2} numberOfLines={2}
> >
{result.name} {result.name}
@@ -240,7 +255,10 @@ const SubtitleResultCard = React.forwardRef<
<Text <Text
style={[ style={[
styles.resultMetaText, styles.resultMetaText,
{ color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)" }, {
color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)",
fontSize: scaleSize(12),
},
]} ]}
> >
{result.format?.toUpperCase()} {result.format?.toUpperCase()}
@@ -252,7 +270,7 @@ const SubtitleResultCard = React.forwardRef<
<View style={styles.ratingContainer}> <View style={styles.ratingContainer}>
<Ionicons <Ionicons
name='star' name='star'
size={12} size={scaleSize(12)}
color={focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)"} color={focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)"}
/> />
<Text <Text
@@ -262,6 +280,7 @@ const SubtitleResultCard = React.forwardRef<
color: focused color: focused
? "rgba(0,0,0,0.6)" ? "rgba(0,0,0,0.6)"
: "rgba(255,255,255,0.5)", : "rgba(255,255,255,0.5)",
fontSize: scaleSize(12),
}, },
]} ]}
> >
@@ -275,7 +294,7 @@ const SubtitleResultCard = React.forwardRef<
<View style={styles.downloadCountContainer}> <View style={styles.downloadCountContainer}>
<Ionicons <Ionicons
name='download-outline' name='download-outline'
size={12} size={scaleSize(12)}
color={focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)"} color={focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)"}
/> />
<Text <Text
@@ -285,6 +304,7 @@ const SubtitleResultCard = React.forwardRef<
color: focused color: focused
? "rgba(0,0,0,0.6)" ? "rgba(0,0,0,0.6)"
: "rgba(255,255,255,0.5)", : "rgba(255,255,255,0.5)",
fontSize: scaleSize(12),
}, },
]} ]}
> >
@@ -307,7 +327,9 @@ const SubtitleResultCard = React.forwardRef<
}, },
]} ]}
> >
<Text style={styles.flagText}>Hash Match</Text> <Text style={[styles.flagText, { fontSize: scaleSize(10) }]}>
Hash Match
</Text>
</View> </View>
)} )}
{result.hearingImpaired && ( {result.hearingImpaired && (
@@ -323,7 +345,7 @@ const SubtitleResultCard = React.forwardRef<
> >
<Ionicons <Ionicons
name='ear-outline' name='ear-outline'
size={12} size={scaleSize(12)}
color={focused ? "#000" : "#fff"} color={focused ? "#000" : "#fff"}
/> />
</View> </View>
@@ -339,7 +361,9 @@ const SubtitleResultCard = React.forwardRef<
}, },
]} ]}
> >
<Text style={styles.flagText}>AI</Text> <Text style={[styles.flagText, { fontSize: scaleSize(10) }]}>
AI
</Text>
</View> </View>
)} )}
</View> </View>
@@ -389,7 +413,7 @@ const TVStepperButton: React.FC<{
> >
<Ionicons <Ionicons
name={icon} name={icon}
size={28} size={scaleSize(28)}
color={focused ? "#000" : disabled ? "rgba(255,255,255,0.4)" : "#fff"} color={focused ? "#000" : disabled ? "rgba(255,255,255,0.4)" : "#fff"}
/> />
</Animated.View> </Animated.View>
@@ -485,7 +509,7 @@ const TVAlignmentCard: React.FC<{
<Text <Text
style={[ style={[
styles.alignmentCardText, styles.alignmentCardText,
{ color: focused ? "#000" : "#fff" }, { color: focused ? "#000" : "#fff", fontSize: scaleSize(15) },
(focused || selected) && { fontWeight: "600" }, (focused || selected) && { fontWeight: "600" },
]} ]}
> >
@@ -495,7 +519,7 @@ const TVAlignmentCard: React.FC<{
<View style={styles.alignmentCheckmark}> <View style={styles.alignmentCheckmark}>
<Ionicons <Ionicons
name='checkmark' name='checkmark'
size={14} size={scaleSize(14)}
color='rgba(255,255,255,0.8)' color='rgba(255,255,255,0.8)'
/> />
</View> </View>
@@ -510,6 +534,7 @@ export default function TVSubtitleModal() {
const { t } = useTranslation(); const { t } = useTranslation();
const modalState = useAtomValue(tvSubtitleModalAtom); const modalState = useAtomValue(tvSubtitleModalAtom);
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const typography = useScaledTVTypography();
const [activeTab, setActiveTab] = useState<TabType>("tracks"); const [activeTab, setActiveTab] = useState<TabType>("tracks");
const [selectedLanguage, setSelectedLanguage] = useState("eng"); const [selectedLanguage, setSelectedLanguage] = useState("eng");
@@ -604,6 +629,12 @@ export default function TVSubtitleModal() {
router.back(); router.back();
}, [router]); }, [router]);
// Intercept back/menu press to close the modal instead of the player
useTVBackPress(() => {
handleClose();
return true;
}, [handleClose]);
const handleLanguageSelect = useCallback( const handleLanguageSelect = useCallback(
(code: string) => { (code: string) => {
setSelectedLanguage(code); setSelectedLanguage(code);
@@ -745,7 +776,7 @@ export default function TVSubtitleModal() {
> >
{/* Header with tabs */} {/* Header with tabs */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}> <Text style={[styles.title, { fontSize: typography.heading }]}>
{t("item_card.subtitles.label") || "Subtitles"} {t("item_card.subtitles.label") || "Subtitles"}
</Text> </Text>
@@ -802,7 +833,9 @@ export default function TVSubtitleModal() {
<> <>
{/* Language Selector */} {/* Language Selector */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}> <Text
style={[styles.sectionTitle, { fontSize: scaleSize(14) }]}
>
{t("player.language") || "Language"} {t("player.language") || "Language"}
</Text> </Text>
<ScrollView <ScrollView
@@ -829,7 +862,9 @@ export default function TVSubtitleModal() {
{/* Results Section */} {/* Results Section */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}> <Text
style={[styles.sectionTitle, { fontSize: scaleSize(14) }]}
>
{t("player.results") || "Results"} {t("player.results") || "Results"}
{searchResults && ` (${searchResults.length})`} {searchResults && ` (${searchResults.length})`}
</Text> </Text>
@@ -846,13 +881,17 @@ export default function TVSubtitleModal() {
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
<Ionicons <Ionicons
name='alert-circle-outline' name='alert-circle-outline'
size={32} size={scaleSize(32)}
color='rgba(255,100,100,0.8)' color='rgba(255,100,100,0.8)'
/> />
<Text style={styles.errorText}> <Text
style={[styles.errorText, { fontSize: scaleSize(16) }]}
>
{t("player.search_failed") || "Search failed"} {t("player.search_failed") || "Search failed"}
</Text> </Text>
<Text style={styles.errorHint}> <Text
style={[styles.errorHint, { fontSize: scaleSize(13) }]}
>
{!hasOpenSubtitlesApiKey {!hasOpenSubtitlesApiKey
? t("player.no_subtitle_provider") || ? t("player.no_subtitle_provider") ||
"No subtitle provider configured on server" "No subtitle provider configured on server"
@@ -869,10 +908,15 @@ export default function TVSubtitleModal() {
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<Ionicons <Ionicons
name='document-text-outline' name='document-text-outline'
size={32} size={scaleSize(32)}
color='rgba(255,255,255,0.4)' color='rgba(255,255,255,0.4)'
/> />
<Text style={styles.emptyText}> <Text
style={[
styles.emptyText,
{ fontSize: scaleSize(14) },
]}
>
{t("player.no_subtitles_found") || {t("player.no_subtitles_found") ||
"No subtitles found"} "No subtitles found"}
</Text> </Text>
@@ -907,10 +951,15 @@ export default function TVSubtitleModal() {
<View style={styles.apiKeyHint}> <View style={styles.apiKeyHint}>
<Ionicons <Ionicons
name='information-circle-outline' name='information-circle-outline'
size={16} size={scaleSize(16)}
color='rgba(255,255,255,0.4)' color='rgba(255,255,255,0.4)'
/> />
<Text style={styles.apiKeyHintText}> <Text
style={[
styles.apiKeyHintText,
{ fontSize: scaleSize(12) },
]}
>
{t("player.add_opensubtitles_key_hint") || {t("player.add_opensubtitles_key_hint") ||
"Add OpenSubtitles API key in settings for client-side fallback"} "Add OpenSubtitles API key in settings for client-side fallback"}
</Text> </Text>
@@ -942,7 +991,12 @@ export default function TVSubtitleModal() {
}} }}
hasTVPreferredFocus={true} hasTVPreferredFocus={true}
/> />
<Text style={styles.settingLabel}> <Text
style={[
styles.settingLabel,
{ fontSize: typography.callout },
]}
>
{t("home.settings.subtitles.mpv_subtitle_scale") || {t("home.settings.subtitles.mpv_subtitle_scale") ||
"Subtitle Scale"} "Subtitle Scale"}
</Text> </Text>
@@ -960,7 +1014,12 @@ export default function TVSubtitleModal() {
updateSettings({ mpvSubtitleMarginY: newValue }); updateSettings({ mpvSubtitleMarginY: newValue });
}} }}
/> />
<Text style={styles.settingLabel}> <Text
style={[
styles.settingLabel,
{ fontSize: typography.callout },
]}
>
{t("home.settings.subtitles.mpv_subtitle_margin_y") || {t("home.settings.subtitles.mpv_subtitle_margin_y") ||
"Vertical Margin"} "Vertical Margin"}
</Text> </Text>
@@ -984,7 +1043,12 @@ export default function TVSubtitleModal() {
/> />
))} ))}
</View> </View>
<Text style={styles.settingLabel}> <Text
style={[
styles.settingLabel,
{ fontSize: typography.callout },
]}
>
{t("home.settings.subtitles.mpv_subtitle_align_x") || {t("home.settings.subtitles.mpv_subtitle_align_x") ||
"Horizontal Align"} "Horizontal Align"}
</Text> </Text>
@@ -1008,7 +1072,12 @@ export default function TVSubtitleModal() {
/> />
))} ))}
</View> </View>
<Text style={styles.settingLabel}> <Text
style={[
styles.settingLabel,
{ fontSize: typography.callout },
]}
>
{t("home.settings.subtitles.mpv_subtitle_align_y") || {t("home.settings.subtitles.mpv_subtitle_align_y") ||
"Vertical Align"} "Vertical Align"}
</Text> </Text>
@@ -1033,218 +1102,201 @@ const styles = StyleSheet.create({
maxHeight: "70%", maxHeight: "70%",
}, },
blurContainer: { blurContainer: {
borderTopLeftRadius: 24, borderTopLeftRadius: scaleSize(24),
borderTopRightRadius: 24, borderTopRightRadius: scaleSize(24),
overflow: "hidden", overflow: "hidden",
}, },
content: { content: {
paddingTop: 24, paddingTop: scaleSize(24),
paddingBottom: 48, paddingBottom: scaleSize(48),
}, },
header: { header: {
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
marginBottom: 20, marginBottom: scaleSize(20),
}, },
title: { title: {
fontSize: 24,
fontWeight: "600", fontWeight: "600",
color: "#fff", color: "#fff",
marginBottom: 16, marginBottom: scaleSize(16),
}, },
tabRow: { tabRow: {
flexDirection: "row", flexDirection: "row",
gap: 24, gap: scaleSize(24),
}, },
section: { section: {
marginBottom: 20, marginBottom: scaleSize(20),
}, },
sectionTitle: { sectionTitle: {
fontSize: 14,
fontWeight: "500", fontWeight: "500",
color: "rgba(255,255,255,0.5)", color: "rgba(255,255,255,0.5)",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
marginBottom: 12, marginBottom: scaleSize(12),
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
}, },
tracksScroll: { tracksScroll: {
overflow: "visible", overflow: "visible",
}, },
tracksScrollContent: { tracksScrollContent: {
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
paddingVertical: 8, paddingVertical: scaleSize(8),
gap: 12, gap: scaleSize(12),
}, },
trackCard: { trackCard: {
width: 180, width: scaleSize(180),
height: 80, height: scaleSize(80),
borderRadius: 14, borderRadius: scaleSize(14),
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
paddingHorizontal: 12, paddingHorizontal: scaleSize(12),
}, },
trackCardText: { trackCardText: {
fontSize: 16,
textAlign: "center", textAlign: "center",
}, },
trackCardSublabel: { trackCardSublabel: {
fontSize: 12, marginTop: scaleSize(2),
marginTop: 2,
}, },
checkmark: { checkmark: {
position: "absolute", position: "absolute",
top: 8, top: scaleSize(8),
right: 8, right: scaleSize(8),
}, },
languageScroll: { languageScroll: {
overflow: "visible", overflow: "visible",
}, },
languageScrollContent: { languageScrollContent: {
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
paddingVertical: 8, paddingVertical: scaleSize(8),
gap: 10, gap: scaleSize(10),
}, },
languageCard: { languageCard: {
width: 120, width: scaleSize(120),
height: 60, height: scaleSize(60),
borderRadius: 12, borderRadius: scaleSize(12),
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
paddingHorizontal: 12, paddingHorizontal: scaleSize(12),
}, },
languageCardText: { languageCardText: {
fontSize: 15,
fontWeight: "500", fontWeight: "500",
}, },
languageCardCode: { languageCardCode: {
fontSize: 11, marginTop: scaleSize(2),
marginTop: 2,
}, },
resultsScroll: { resultsScroll: {
overflow: "visible", overflow: "visible",
}, },
resultsScrollContent: { resultsScrollContent: {
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
paddingVertical: 8, paddingVertical: scaleSize(8),
gap: 12, gap: scaleSize(12),
}, },
resultCard: { resultCard: {
width: 220, width: scaleSize(220),
height: 130, height: scaleSize(130),
borderRadius: 14, borderRadius: scaleSize(14),
padding: 14, padding: scaleSize(14),
borderWidth: 1, borderWidth: 1,
overflow: "hidden", overflow: "hidden",
}, },
providerBadge: { providerBadge: {
alignSelf: "flex-start", alignSelf: "flex-start",
paddingHorizontal: 8, paddingHorizontal: scaleSize(8),
paddingVertical: 3, paddingVertical: scaleSize(3),
borderRadius: 6, borderRadius: scaleSize(6),
marginBottom: 8, marginBottom: scaleSize(8),
}, },
providerText: { providerText: {
fontSize: 11,
fontWeight: "600", fontWeight: "600",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 0.5, letterSpacing: 0.5,
}, },
resultName: { resultName: {
fontSize: 14,
fontWeight: "500", fontWeight: "500",
marginBottom: 8, marginBottom: scaleSize(8),
lineHeight: 18, lineHeight: scaleSize(18),
}, },
resultMeta: { resultMeta: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 12, gap: scaleSize(12),
marginBottom: 8, marginBottom: scaleSize(8),
},
resultMetaText: {
fontSize: 12,
}, },
resultMetaText: {},
ratingContainer: { ratingContainer: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 3, gap: scaleSize(3),
}, },
downloadCountContainer: { downloadCountContainer: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 3, gap: scaleSize(3),
}, },
flagsContainer: { flagsContainer: {
flexDirection: "row", flexDirection: "row",
gap: 6, gap: scaleSize(6),
flexWrap: "wrap", flexWrap: "wrap",
}, },
flag: { flag: {
paddingHorizontal: 6, paddingHorizontal: scaleSize(6),
paddingVertical: 2, paddingVertical: scaleSize(2),
borderRadius: 4, borderRadius: scaleSize(4),
}, },
flagText: { flagText: {
fontSize: 10,
fontWeight: "600", fontWeight: "600",
color: "#fff", color: "#fff",
}, },
downloadingOverlay: { downloadingOverlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0,0,0,0.5)", backgroundColor: "rgba(0,0,0,0.5)",
borderRadius: 14, borderRadius: scaleSize(14),
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}, },
loadingContainer: { loadingContainer: {
paddingVertical: 20, paddingVertical: scaleSize(20),
alignItems: "center", alignItems: "center",
}, },
errorContainer: { errorContainer: {
paddingVertical: 40, paddingVertical: scaleSize(40),
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
alignItems: "center", alignItems: "center",
}, },
errorText: { errorText: {
color: "rgba(255,100,100,0.9)", color: "rgba(255,100,100,0.9)",
marginTop: 8, marginTop: scaleSize(8),
fontSize: 16,
fontWeight: "500", fontWeight: "500",
}, },
errorHint: { errorHint: {
color: "rgba(255,255,255,0.5)", color: "rgba(255,255,255,0.5)",
marginTop: 4, marginTop: scaleSize(4),
fontSize: 13,
textAlign: "center", textAlign: "center",
}, },
emptyContainer: { emptyContainer: {
paddingVertical: 40, paddingVertical: scaleSize(40),
alignItems: "center", alignItems: "center",
}, },
emptyText: { emptyText: {
color: "rgba(255,255,255,0.5)", color: "rgba(255,255,255,0.5)",
marginTop: 8, marginTop: scaleSize(8),
fontSize: 14,
}, },
apiKeyHint: { apiKeyHint: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 8, gap: scaleSize(8),
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
paddingTop: 8, paddingTop: scaleSize(8),
},
apiKeyHintText: {
color: "rgba(255,255,255,0.4)",
fontSize: 12,
}, },
apiKeyHintText: {},
// Settings tab styles // Settings tab styles
settingsScroll: { settingsScroll: {
maxHeight: 300, maxHeight: scaleSize(300),
}, },
settingsScrollContent: { settingsScrollContent: {
paddingHorizontal: 48, paddingHorizontal: scaleSize(48),
paddingVertical: 8, paddingVertical: scaleSize(8),
gap: 24, gap: scaleSize(24),
}, },
settingRow: { settingRow: {
flexDirection: "row", flexDirection: "row",
@@ -1252,49 +1304,47 @@ const styles = StyleSheet.create({
justifyContent: "space-between", justifyContent: "space-between",
}, },
settingLabel: { settingLabel: {
fontSize: 18,
fontWeight: "500", fontWeight: "500",
color: "#fff", color: "#fff",
}, },
sizeControlContainer: { sizeControlContainer: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 16, gap: scaleSize(16),
}, },
stepperButton: { stepperButton: {
width: 56, width: scaleSize(56),
height: 56, height: scaleSize(56),
borderRadius: 14, borderRadius: scaleSize(14),
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}, },
sizeValueContainer: { sizeValueContainer: {
width: 80, width: scaleSize(80),
alignItems: "center", alignItems: "center",
}, },
sizeValueText: { sizeValueText: {
fontSize: 24,
fontWeight: "600", fontWeight: "600",
color: "#fff", color: "#fff",
fontSize: scaleSize(24),
}, },
alignmentRow: { alignmentRow: {
flexDirection: "row", flexDirection: "row",
gap: 10, gap: scaleSize(10),
}, },
alignmentCard: { alignmentCard: {
paddingHorizontal: 20, paddingHorizontal: scaleSize(20),
paddingVertical: 14, paddingVertical: scaleSize(14),
borderRadius: 12, borderRadius: scaleSize(12),
minWidth: 90, minWidth: scaleSize(90),
alignItems: "center", alignItems: "center",
}, },
alignmentCardText: { alignmentCardText: {
fontSize: 15,
textTransform: "capitalize", textTransform: "capitalize",
}, },
alignmentCheckmark: { alignmentCheckmark: {
position: "absolute", position: "absolute",
top: 6, top: scaleSize(6),
right: 6, right: scaleSize(6),
}, },
}); });

View File

@@ -68,7 +68,7 @@ export const TVOptionCard = React.forwardRef<View, TVOptionCardProps>(
fontWeight: focused || selected ? "600" : "400", fontWeight: focused || selected ? "600" : "400",
textAlign: "center", textAlign: "center",
}} }}
numberOfLines={2} numberOfLines={4}
> >
{label} {label}
</Text> </Text>

View File

@@ -49,6 +49,7 @@ export const TVSettingsStepper: React.FC<TVSettingsStepperProps> = ({
}} }}
> >
<Pressable <Pressable
style={{ flex: 1 }}
onFocus={labelAnim.handleFocus} onFocus={labelAnim.handleFocus}
onBlur={labelAnim.handleBlur} onBlur={labelAnim.handleBlur}
hasTVPreferredFocus={isFirst && !disabled} hasTVPreferredFocus={isFirst && !disabled}