From 681a20dd8a386010d00cc29eeaea96bb21052f1e Mon Sep 17 00:00:00 2001 From: YagUber Date: Mon, 13 Apr 2026 19:15:12 -0500 Subject: [PATCH] refactor(settings): unify language picker across app and playback settings --- src/screens/SettingsScreen.tsx | 14 +- src/screens/settings/LanguageSettingItem.tsx | 51 +++++-- .../settings/PlaybackSettingsScreen.tsx | 137 ++---------------- 3 files changed, 57 insertions(+), 145 deletions(-) diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index f87e2d1b..4db1341b 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -436,7 +436,13 @@ const SettingsScreen: React.FC = () => { return ( <> - + i18n.changeLanguage(code)} + isTablet={isTablet} + isLast + /> @@ -703,7 +709,11 @@ const SettingsScreen: React.FC = () => { (settingsConfig?.categories?.['playback']?.visible !== false) ) && ( - + i18n.changeLanguage(code)} + /> {(settingsConfig?.categories?.['content']?.visible !== false) && ( a.name.localeCompare(b.name)); +export interface LanguageOption { + code: string; + name: string; +} interface LanguageSettingItemProps { + title: string; + value: string; + onChange: (code: string) => void; + languages?: LanguageOption[]; isTablet?: boolean; isLast?: boolean; } -export const LanguageSettingItem: React.FC = ({ isTablet = false, isLast = false }) => { - const { t, i18n } = useTranslation(); +export const LanguageSettingItem: React.FC = ({ + title, + value, + onChange, + languages, + isTablet = false, + isLast = false, +}) => { const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); const sheetRef = useRef(null); - const { onChange, onDismiss } = useBottomSheetBackHandler(); + const { onChange: onSheetChange, onDismiss } = useBottomSheetBackHandler(); + + const sortedLanguages = useMemo(() => { + const sorted = [...(languages ?? LOCALES)].sort((a, b) => a.name.localeCompare(b.name)); + const selectedIndex = sorted.findIndex(l => l.code === value || l.code === value?.split('-')[0]); + if (selectedIndex > 0) { + const [selected] = sorted.splice(selectedIndex, 1); + sorted.unshift(selected); + } + return sorted; + }, [languages, value]); const currentLocale = - LOCALES.find(l => l.code === i18n.language) ?? - LOCALES.find(l => l.code === i18n.language?.split('-')[0]); + sortedLanguages.find(l => l.code === value) ?? + sortedLanguages.find(l => l.code === value?.split('-')[0]); const renderBackdrop = useCallback( (props: any) => ( @@ -37,7 +59,7 @@ export const LanguageSettingItem: React.FC = ({ isTabl return ( <> } @@ -61,12 +83,12 @@ export const LanguageSettingItem: React.FC = ({ isTabl backgroundColor: currentTheme.colors.mediumGray, width: 40, }} - onChange={onChange(sheetRef)} + onChange={onSheetChange(sheetRef)} onDismiss={onDismiss(sheetRef)} > - {t('settings.select_language')} + {title} sheetRef.current?.dismiss()}> @@ -76,9 +98,8 @@ export const LanguageSettingItem: React.FC = ({ isTabl style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }} contentContainerStyle={[styles.sheetContent, { paddingBottom: insets.bottom + 16 }]} > - {SORTED_LOCALES.map(l => { - const isSelected = i18n.language === l.code || - i18n.language?.split('-')[0] === l.code; + {sortedLanguages.map(l => { + const isSelected = value === l.code || value?.split('-')[0] === l.code; return ( = ({ isTabl isSelected && { backgroundColor: currentTheme.colors.primary + '20' }, ]} onPress={() => { - i18n.changeLanguage(l.code); + onChange(l.code); sheetRef.current?.dismiss(); }} > diff --git a/src/screens/settings/PlaybackSettingsScreen.tsx b/src/screens/settings/PlaybackSettingsScreen.tsx index ed0abd6b..69895300 100644 --- a/src/screens/settings/PlaybackSettingsScreen.tsx +++ b/src/screens/settings/PlaybackSettingsScreen.tsx @@ -8,6 +8,7 @@ import { useSettings } from '../../hooks/useSettings'; import { RootStackParamList } from '../../navigation/AppNavigator'; import ScreenHeader from '../../components/common/ScreenHeader'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; +import { LanguageSettingItem } from './LanguageSettingItem'; import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; import { MaterialIcons } from '@expo/vector-icons'; import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; @@ -156,31 +157,10 @@ export const PlaybackSettingsContent: React.FC = ( ); - // Bottom sheet refs - const audioLanguageSheetRef = useRef(null); - const subtitleLanguageSheetRef = useRef(null); const subtitleSourceSheetRef = useRef(null); - - // Snap points - const languageSnapPoints = useMemo(() => ['70%'], []); const sourceSnapPoints = useMemo(() => ['45%'], []); - // Handlers to present sheets - ensure only one is open at a time - const openAudioLanguageSheet = useCallback(() => { - subtitleLanguageSheetRef.current?.dismiss(); - subtitleSourceSheetRef.current?.dismiss(); - setTimeout(() => audioLanguageSheetRef.current?.present(), 100); - }, []); - - const openSubtitleLanguageSheet = useCallback(() => { - audioLanguageSheetRef.current?.dismiss(); - subtitleSourceSheetRef.current?.dismiss(); - setTimeout(() => subtitleLanguageSheetRef.current?.present(), 100); - }, []); - const openSubtitleSourceSheet = useCallback(() => { - audioLanguageSheetRef.current?.dismiss(); - subtitleLanguageSheetRef.current?.dismiss(); setTimeout(() => subtitleSourceSheetRef.current?.present(), 100); }, []); @@ -195,11 +175,6 @@ export const PlaybackSettingsContent: React.FC = ( return itemIds.some(id => isItemVisible(id)); }; - const getLanguageName = (code: string) => { - const lang = AVAILABLE_LANGUAGES.find(l => l.code === code); - return lang ? lang.name : code.toUpperCase(); - }; - const getSourceLabel = (value: string) => { if (value === 'internal') return t('settings.options.internal_first'); if (value === 'external') return t('settings.options.external_first'); @@ -220,16 +195,6 @@ export const PlaybackSettingsContent: React.FC = ( [] ); - const handleSelectAudioLanguage = (code: string) => { - updateSetting('preferredAudioLanguage', code); - audioLanguageSheetRef.current?.dismiss(); - }; - - const handleSelectSubtitleLanguage = (code: string) => { - updateSetting('preferredSubtitleLanguage', code); - subtitleLanguageSheetRef.current?.dismiss(); - }; - const handleSelectSubtitleSource = (value: 'internal' | 'external' | 'any') => { updateSetting('subtitleSourcePreference', value); subtitleSourceSheetRef.current?.dismiss(); @@ -322,20 +287,18 @@ export const PlaybackSettingsContent: React.FC = ( {/* Audio & Subtitle Preferences */} - } - onPress={openAudioLanguageSheet} + value={settings?.preferredAudioLanguage || 'en'} + onChange={(code) => updateSetting('preferredAudioLanguage', code)} + languages={AVAILABLE_LANGUAGES} isTablet={isTablet} /> - } - onPress={openSubtitleLanguageSheet} + value={settings?.preferredSubtitleLanguage || 'en'} + onChange={(code) => updateSetting('preferredSubtitleLanguage', code)} + languages={AVAILABLE_LANGUAGES} isTablet={isTablet} /> = ( )} - {/* Audio Language Bottom Sheet */} - - - {t('settings.items.preferred_audio')} - - - {AVAILABLE_LANGUAGES.map((lang) => { - const isSelected = lang.code === (settings?.preferredAudioLanguage || 'en'); - return ( - handleSelectAudioLanguage(lang.code)} - > - - {lang.name} - - - {lang.code.toUpperCase()} - - {isSelected && ( - - )} - - ); - })} - - - - {/* Subtitle Language Bottom Sheet */} - - - {t('settings.items.preferred_subtitle')} - - - {AVAILABLE_LANGUAGES.map((lang) => { - const isSelected = lang.code === (settings?.preferredSubtitleLanguage || 'en'); - return ( - handleSelectSubtitleLanguage(lang.code)} - > - - {lang.name} - - - {lang.code.toUpperCase()} - - {isSelected && ( - - )} - - ); - })} - - - {/* Subtitle Source Priority Bottom Sheet */}