mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-26 19:26:26 +00:00
feat(tv): add scalable typography with user-configurable text size
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user