mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
refactor(settings): unify language picker across app and playback settings
This commit is contained in:
parent
eec51f787c
commit
681a20dd8a
3 changed files with 57 additions and 145 deletions
|
|
@ -436,7 +436,13 @@ const SettingsScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsCard title="GENERAL" isTablet={isTablet}>
|
<SettingsCard title="GENERAL" isTablet={isTablet}>
|
||||||
<LanguageSettingItem isTablet={isTablet} isLast />
|
<LanguageSettingItem
|
||||||
|
title={t('settings.language')}
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={(code) => i18n.changeLanguage(code)}
|
||||||
|
isTablet={isTablet}
|
||||||
|
isLast
|
||||||
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<AppearanceSettingsContent isTablet={isTablet} />
|
<AppearanceSettingsContent isTablet={isTablet} />
|
||||||
</>
|
</>
|
||||||
|
|
@ -703,7 +709,11 @@ const SettingsScreen: React.FC = () => {
|
||||||
(settingsConfig?.categories?.['playback']?.visible !== false)
|
(settingsConfig?.categories?.['playback']?.visible !== false)
|
||||||
) && (
|
) && (
|
||||||
<SettingsCard title="GENERAL">
|
<SettingsCard title="GENERAL">
|
||||||
<LanguageSettingItem />
|
<LanguageSettingItem
|
||||||
|
title={t('settings.language')}
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={(code) => i18n.changeLanguage(code)}
|
||||||
|
/>
|
||||||
{(settingsConfig?.categories?.['content']?.visible !== false) && (
|
{(settingsConfig?.categories?.['content']?.visible !== false) && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={t('settings.content_discovery')}
|
title={t('settings.content_discovery')}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,53 @@
|
||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||||
import { Feather } from '@expo/vector-icons';
|
import { Feather } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { useBottomSheetBackHandler } from '../../hooks/useBottomSheetBackHandler';
|
import { useBottomSheetBackHandler } from '../../hooks/useBottomSheetBackHandler';
|
||||||
import { ChevronRight, SettingItem } from './SettingsComponents';
|
import { ChevronRight, SettingItem } from './SettingsComponents';
|
||||||
import { LOCALES } from '../../constants/locales';
|
import { LOCALES } from '../../constants/locales';
|
||||||
|
|
||||||
const SORTED_LOCALES = [...LOCALES].sort((a, b) => a.name.localeCompare(b.name));
|
export interface LanguageOption {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface LanguageSettingItemProps {
|
interface LanguageSettingItemProps {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (code: string) => void;
|
||||||
|
languages?: LanguageOption[];
|
||||||
isTablet?: boolean;
|
isTablet?: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({ isTablet = false, isLast = false }) => {
|
export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({
|
||||||
const { t, i18n } = useTranslation();
|
title,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
languages,
|
||||||
|
isTablet = false,
|
||||||
|
isLast = false,
|
||||||
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const sheetRef = useRef<BottomSheetModal>(null);
|
const sheetRef = useRef<BottomSheetModal>(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 =
|
const currentLocale =
|
||||||
LOCALES.find(l => l.code === i18n.language) ??
|
sortedLanguages.find(l => l.code === value) ??
|
||||||
LOCALES.find(l => l.code === i18n.language?.split('-')[0]);
|
sortedLanguages.find(l => l.code === value?.split('-')[0]);
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const renderBackdrop = useCallback(
|
||||||
(props: any) => (
|
(props: any) => (
|
||||||
|
|
@ -37,7 +59,7 @@ export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({ isTabl
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={t('settings.language')}
|
title={title}
|
||||||
description={currentLocale?.name}
|
description={currentLocale?.name}
|
||||||
icon="globe"
|
icon="globe"
|
||||||
renderControl={() => <ChevronRight />}
|
renderControl={() => <ChevronRight />}
|
||||||
|
|
@ -61,12 +83,12 @@ export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({ isTabl
|
||||||
backgroundColor: currentTheme.colors.mediumGray,
|
backgroundColor: currentTheme.colors.mediumGray,
|
||||||
width: 40,
|
width: 40,
|
||||||
}}
|
}}
|
||||||
onChange={onChange(sheetRef)}
|
onChange={onSheetChange(sheetRef)}
|
||||||
onDismiss={onDismiss(sheetRef)}
|
onDismiss={onDismiss(sheetRef)}
|
||||||
>
|
>
|
||||||
<View style={[styles.sheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
<View style={[styles.sheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||||
<Text style={[styles.sheetTitle, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.sheetTitle, { color: currentTheme.colors.white }]}>
|
||||||
{t('settings.select_language')}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={() => sheetRef.current?.dismiss()}>
|
<TouchableOpacity onPress={() => sheetRef.current?.dismiss()}>
|
||||||
<Feather name="x" size={24} color={currentTheme.colors.lightGray} />
|
<Feather name="x" size={24} color={currentTheme.colors.lightGray} />
|
||||||
|
|
@ -76,9 +98,8 @@ export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({ isTabl
|
||||||
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
|
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
|
||||||
contentContainerStyle={[styles.sheetContent, { paddingBottom: insets.bottom + 16 }]}
|
contentContainerStyle={[styles.sheetContent, { paddingBottom: insets.bottom + 16 }]}
|
||||||
>
|
>
|
||||||
{SORTED_LOCALES.map(l => {
|
{sortedLanguages.map(l => {
|
||||||
const isSelected = i18n.language === l.code ||
|
const isSelected = value === l.code || value?.split('-')[0] === l.code;
|
||||||
i18n.language?.split('-')[0] === l.code;
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={l.code}
|
key={l.code}
|
||||||
|
|
@ -87,7 +108,7 @@ export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({ isTabl
|
||||||
isSelected && { backgroundColor: currentTheme.colors.primary + '20' },
|
isSelected && { backgroundColor: currentTheme.colors.primary + '20' },
|
||||||
]}
|
]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
i18n.changeLanguage(l.code);
|
onChange(l.code);
|
||||||
sheetRef.current?.dismiss();
|
sheetRef.current?.dismiss();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useSettings } from '../../hooks/useSettings';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
import ScreenHeader from '../../components/common/ScreenHeader';
|
import ScreenHeader from '../../components/common/ScreenHeader';
|
||||||
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
||||||
|
import { LanguageSettingItem } from './LanguageSettingItem';
|
||||||
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
|
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
||||||
|
|
@ -156,31 +157,10 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
||||||
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
|
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bottom sheet refs
|
|
||||||
const audioLanguageSheetRef = useRef<BottomSheetModal>(null);
|
|
||||||
const subtitleLanguageSheetRef = useRef<BottomSheetModal>(null);
|
|
||||||
const subtitleSourceSheetRef = useRef<BottomSheetModal>(null);
|
const subtitleSourceSheetRef = useRef<BottomSheetModal>(null);
|
||||||
|
|
||||||
// Snap points
|
|
||||||
const languageSnapPoints = useMemo(() => ['70%'], []);
|
|
||||||
const sourceSnapPoints = useMemo(() => ['45%'], []);
|
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(() => {
|
const openSubtitleSourceSheet = useCallback(() => {
|
||||||
audioLanguageSheetRef.current?.dismiss();
|
|
||||||
subtitleLanguageSheetRef.current?.dismiss();
|
|
||||||
setTimeout(() => subtitleSourceSheetRef.current?.present(), 100);
|
setTimeout(() => subtitleSourceSheetRef.current?.present(), 100);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -195,11 +175,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
||||||
return itemIds.some(id => isItemVisible(id));
|
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) => {
|
const getSourceLabel = (value: string) => {
|
||||||
if (value === 'internal') return t('settings.options.internal_first');
|
if (value === 'internal') return t('settings.options.internal_first');
|
||||||
if (value === 'external') return t('settings.options.external_first');
|
if (value === 'external') return t('settings.options.external_first');
|
||||||
|
|
@ -220,16 +195,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
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') => {
|
const handleSelectSubtitleSource = (value: 'internal' | 'external' | 'any') => {
|
||||||
updateSetting('subtitleSourcePreference', value);
|
updateSetting('subtitleSourcePreference', value);
|
||||||
subtitleSourceSheetRef.current?.dismiss();
|
subtitleSourceSheetRef.current?.dismiss();
|
||||||
|
|
@ -322,20 +287,18 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
||||||
|
|
||||||
{/* Audio & Subtitle Preferences */}
|
{/* Audio & Subtitle Preferences */}
|
||||||
<SettingsCard title={t('settings.sections.audio_subtitles')} isTablet={isTablet}>
|
<SettingsCard title={t('settings.sections.audio_subtitles')} isTablet={isTablet}>
|
||||||
<SettingItem
|
<LanguageSettingItem
|
||||||
title={t('settings.items.preferred_audio')}
|
title={t('settings.items.preferred_audio')}
|
||||||
description={getLanguageName(settings?.preferredAudioLanguage || 'en')}
|
value={settings?.preferredAudioLanguage || 'en'}
|
||||||
icon="volume-2"
|
onChange={(code) => updateSetting('preferredAudioLanguage', code)}
|
||||||
renderControl={() => <ChevronRight />}
|
languages={AVAILABLE_LANGUAGES}
|
||||||
onPress={openAudioLanguageSheet}
|
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
<SettingItem
|
<LanguageSettingItem
|
||||||
title={t('settings.items.preferred_subtitle')}
|
title={t('settings.items.preferred_subtitle')}
|
||||||
description={getLanguageName(settings?.preferredSubtitleLanguage || 'en')}
|
value={settings?.preferredSubtitleLanguage || 'en'}
|
||||||
icon="type"
|
onChange={(code) => updateSetting('preferredSubtitleLanguage', code)}
|
||||||
renderControl={() => <ChevronRight />}
|
languages={AVAILABLE_LANGUAGES}
|
||||||
onPress={openSubtitleLanguageSheet}
|
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
|
|
@ -411,88 +374,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Audio Language Bottom Sheet */}
|
|
||||||
<BottomSheetModal
|
|
||||||
ref={audioLanguageSheetRef}
|
|
||||||
index={0}
|
|
||||||
snapPoints={languageSnapPoints}
|
|
||||||
enableDynamicSizing={false}
|
|
||||||
enablePanDownToClose={true}
|
|
||||||
backdropComponent={renderBackdrop}
|
|
||||||
backgroundStyle={{ backgroundColor: '#1a1a1a' }}
|
|
||||||
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
|
|
||||||
>
|
|
||||||
<View style={styles.sheetHeader}>
|
|
||||||
<Text style={styles.sheetTitle}>{t('settings.items.preferred_audio')}</Text>
|
|
||||||
</View>
|
|
||||||
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
|
|
||||||
{AVAILABLE_LANGUAGES.map((lang) => {
|
|
||||||
const isSelected = lang.code === (settings?.preferredAudioLanguage || 'en');
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={lang.code}
|
|
||||||
style={[
|
|
||||||
styles.languageItem,
|
|
||||||
isSelected && { backgroundColor: currentTheme.colors.primary + '20' }
|
|
||||||
]}
|
|
||||||
onPress={() => handleSelectAudioLanguage(lang.code)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.languageName, { color: isSelected ? currentTheme.colors.primary : '#fff' }]}>
|
|
||||||
{lang.name}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.languageCode}>
|
|
||||||
{lang.code.toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
{isSelected && (
|
|
||||||
<MaterialIcons name="check" size={20} color={currentTheme.colors.primary} />
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
</BottomSheetModal>
|
|
||||||
|
|
||||||
{/* Subtitle Language Bottom Sheet */}
|
|
||||||
<BottomSheetModal
|
|
||||||
ref={subtitleLanguageSheetRef}
|
|
||||||
index={0}
|
|
||||||
snapPoints={languageSnapPoints}
|
|
||||||
enableDynamicSizing={false}
|
|
||||||
enablePanDownToClose={true}
|
|
||||||
backdropComponent={renderBackdrop}
|
|
||||||
backgroundStyle={{ backgroundColor: '#1a1a1a' }}
|
|
||||||
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
|
|
||||||
>
|
|
||||||
<View style={styles.sheetHeader}>
|
|
||||||
<Text style={styles.sheetTitle}>{t('settings.items.preferred_subtitle')}</Text>
|
|
||||||
</View>
|
|
||||||
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
|
|
||||||
{AVAILABLE_LANGUAGES.map((lang) => {
|
|
||||||
const isSelected = lang.code === (settings?.preferredSubtitleLanguage || 'en');
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={lang.code}
|
|
||||||
style={[
|
|
||||||
styles.languageItem,
|
|
||||||
isSelected && { backgroundColor: currentTheme.colors.primary + '20' }
|
|
||||||
]}
|
|
||||||
onPress={() => handleSelectSubtitleLanguage(lang.code)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.languageName, { color: isSelected ? currentTheme.colors.primary : '#fff' }]}>
|
|
||||||
{lang.name}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.languageCode}>
|
|
||||||
{lang.code.toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
{isSelected && (
|
|
||||||
<MaterialIcons name="check" size={20} color={currentTheme.colors.primary} />
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
</BottomSheetModal>
|
|
||||||
|
|
||||||
{/* Subtitle Source Priority Bottom Sheet */}
|
{/* Subtitle Source Priority Bottom Sheet */}
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
ref={subtitleSourceSheetRef}
|
ref={subtitleSourceSheetRef}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue