mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-24 09:34:43 +01:00
refactor
This commit is contained in:
@@ -1,464 +1,24 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Animated, Pressable, ScrollView, TextInput, View } from "react-native";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import type { TVOptionItem } from "@/components/tv";
|
||||
import { useTVFocusAnimation } from "@/components/tv";
|
||||
import {
|
||||
TVLogoutButton,
|
||||
TVSectionHeader,
|
||||
TVSettingsOptionButton,
|
||||
TVSettingsRow,
|
||||
TVSettingsStepper,
|
||||
TVSettingsTextInput,
|
||||
TVSettingsToggle,
|
||||
} from "@/components/tv";
|
||||
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { AudioTranscodeMode, useSettings } from "@/utils/atoms/settings";
|
||||
|
||||
// TV-optimized focusable row component
|
||||
const TVSettingsRow: React.FC<{
|
||||
label: string;
|
||||
value: string;
|
||||
onPress?: () => void;
|
||||
isFirst?: boolean;
|
||||
showChevron?: boolean;
|
||||
disabled?: boolean;
|
||||
}> = ({ label, value, onPress, isFirst, showChevron = true, disabled }) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.02 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst && !disabled}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#9CA3AF",
|
||||
marginRight: showChevron ? 12 : 0,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
{showChevron && (
|
||||
<Ionicons name='chevron-forward' size={20} color='#6B7280' />
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
// TV-optimized toggle row component
|
||||
const TVSettingsToggle: React.FC<{
|
||||
label: string;
|
||||
value: boolean;
|
||||
onToggle: (value: boolean) => void;
|
||||
isFirst?: boolean;
|
||||
disabled?: boolean;
|
||||
}> = ({ label, value, onToggle, isFirst, disabled }) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.02 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => onToggle(!value)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst && !disabled}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
|
||||
<View
|
||||
style={{
|
||||
width: 56,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: value ? "#34C759" : "#4B5563",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 2,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: "#FFFFFF",
|
||||
alignSelf: value ? "flex-end" : "flex-start",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
// TV-optimized stepper row component
|
||||
const TVSettingsStepper: React.FC<{
|
||||
label: string;
|
||||
value: number;
|
||||
onDecrease: () => void;
|
||||
onIncrease: () => void;
|
||||
formatValue?: (value: number) => string;
|
||||
isFirst?: boolean;
|
||||
disabled?: boolean;
|
||||
}> = ({
|
||||
label,
|
||||
value,
|
||||
onDecrease,
|
||||
onIncrease,
|
||||
formatValue,
|
||||
isFirst,
|
||||
disabled,
|
||||
}) => {
|
||||
const labelAnim = useTVFocusAnimation({ scaleAmount: 1.02 });
|
||||
const minusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
|
||||
const plusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
|
||||
|
||||
const displayValue = formatValue ? formatValue(value) : String(value);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor:
|
||||
labelAnim.focused || minusAnim.focused || plusAnim.focused
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onFocus={labelAnim.handleFocus}
|
||||
onBlur={labelAnim.handleBlur}
|
||||
hasTVPreferredFocus={isFirst && !disabled}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View style={labelAnim.animatedStyle}>
|
||||
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Pressable
|
||||
onPress={onDecrease}
|
||||
onFocus={minusAnim.handleFocus}
|
||||
onBlur={minusAnim.handleBlur}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
minusAnim.animatedStyle,
|
||||
{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: minusAnim.focused ? "#FFFFFF" : "#4B5563",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name='remove'
|
||||
size={24}
|
||||
color={minusAnim.focused ? "#000000" : "#FFFFFF"}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#FFFFFF",
|
||||
minWidth: 60,
|
||||
textAlign: "center",
|
||||
marginHorizontal: 16,
|
||||
}}
|
||||
>
|
||||
{displayValue}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={onIncrease}
|
||||
onFocus={plusAnim.handleFocus}
|
||||
onBlur={plusAnim.handleBlur}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
plusAnim.animatedStyle,
|
||||
{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: plusAnim.focused ? "#FFFFFF" : "#4B5563",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name='add'
|
||||
size={24}
|
||||
color={plusAnim.focused ? "#000000" : "#FFFFFF"}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// TV Settings Option Button - displays current value and opens bottom sheet
|
||||
const TVSettingsOptionButton: React.FC<{
|
||||
label: string;
|
||||
value: string;
|
||||
onPress: () => void;
|
||||
isFirst?: boolean;
|
||||
disabled?: boolean;
|
||||
}> = ({ label, value, onPress, isFirst, disabled }) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.02 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst && !disabled}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#9CA3AF",
|
||||
marginRight: 12,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
<Ionicons name='chevron-forward' size={20} color='#6B7280' />
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
// TV-optimized text input component
|
||||
const TVSettingsTextInput: React.FC<{
|
||||
label: string;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onBlur?: () => void;
|
||||
secureTextEntry?: boolean;
|
||||
disabled?: boolean;
|
||||
}> = ({
|
||||
label,
|
||||
value,
|
||||
placeholder,
|
||||
onChangeText,
|
||||
onBlur,
|
||||
secureTextEntry,
|
||||
disabled,
|
||||
}) => {
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.02 });
|
||||
|
||||
const handleInputBlur = () => {
|
||||
handleBlur();
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => inputRef.current?.focus()}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleInputBlur}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={{ fontSize: 16, color: "#9CA3AF", marginBottom: 8 }}>
|
||||
{label}
|
||||
</Text>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor='#6B7280'
|
||||
onChangeText={onChangeText}
|
||||
onBlur={handleInputBlur}
|
||||
secureTextEntry={secureTextEntry}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#FFFFFF",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderWidth: focused ? 2 : 1,
|
||||
borderColor: focused ? "#FFFFFF" : "#4B5563",
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
// Section header component
|
||||
const SectionHeader: React.FC<{ title: string }> = ({ title }) => (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#9CA3AF",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
marginTop: 32,
|
||||
marginBottom: 16,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// Logout button component
|
||||
const TVLogoutButton: React.FC<{ onPress: () => void; disabled?: boolean }> = ({
|
||||
onPress,
|
||||
disabled,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.05 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
shadowColor: "#ef4444",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: focused ? 0.6 : 0,
|
||||
shadowRadius: focused ? 20 : 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: focused ? "#ef4444" : "rgba(239, 68, 68, 0.8)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 48,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
color: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
{t("home.settings.log_out_button")}
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default function SettingsTV() {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -616,7 +176,7 @@ export default function SettingsTV() {
|
||||
</Text>
|
||||
|
||||
{/* Audio Section */}
|
||||
<SectionHeader title={t("home.settings.audio.audio_title")} />
|
||||
<TVSectionHeader title={t("home.settings.audio.audio_title")} />
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.audio.transcode_mode.title")}
|
||||
value={audioTranscodeLabel}
|
||||
@@ -632,7 +192,9 @@ export default function SettingsTV() {
|
||||
/>
|
||||
|
||||
{/* Subtitles Section */}
|
||||
<SectionHeader title={t("home.settings.subtitles.subtitle_title")} />
|
||||
<TVSectionHeader
|
||||
title={t("home.settings.subtitles.subtitle_title")}
|
||||
/>
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.subtitles.subtitle_mode")}
|
||||
value={subtitleModeLabel}
|
||||
@@ -666,7 +228,7 @@ export default function SettingsTV() {
|
||||
/>
|
||||
|
||||
{/* MPV Subtitles Section */}
|
||||
<SectionHeader title='MPV Subtitle Settings' />
|
||||
<TVSectionHeader title='MPV Subtitle Settings' />
|
||||
<TVSettingsStepper
|
||||
label='Subtitle Scale'
|
||||
value={settings.mpvSubtitleScale ?? 1.0}
|
||||
@@ -738,7 +300,7 @@ export default function SettingsTV() {
|
||||
/>
|
||||
|
||||
{/* OpenSubtitles Section */}
|
||||
<SectionHeader
|
||||
<TVSectionHeader
|
||||
title={
|
||||
t("home.settings.subtitles.opensubtitles_title") ||
|
||||
"OpenSubtitles"
|
||||
@@ -781,7 +343,7 @@ export default function SettingsTV() {
|
||||
</Text>
|
||||
|
||||
{/* Appearance Section */}
|
||||
<SectionHeader title={t("home.settings.appearance.title")} />
|
||||
<TVSectionHeader title={t("home.settings.appearance.title")} />
|
||||
<TVSettingsToggle
|
||||
label={t(
|
||||
"home.settings.appearance.merge_next_up_continue_watching",
|
||||
@@ -798,7 +360,9 @@ export default function SettingsTV() {
|
||||
/>
|
||||
|
||||
{/* User Section */}
|
||||
<SectionHeader title={t("home.settings.user_info.user_info_title")} />
|
||||
<TVSectionHeader
|
||||
title={t("home.settings.user_info.user_info_title")}
|
||||
/>
|
||||
<TVSettingsRow
|
||||
label={t("home.settings.user_info.user")}
|
||||
value={user?.Name || "-"}
|
||||
|
||||
Reference in New Issue
Block a user