PlaybackSettings IntroDB Localization Patch

This commit is contained in:
cyberalby2 2026-03-06 19:55:44 +01:00
parent 46e5b469dd
commit e3a09164e6
3 changed files with 759 additions and 604 deletions

View file

@ -697,7 +697,8 @@
"media": "MEDIA",
"notifications": "NOTIFICATIONS",
"testing": "TESTING",
"danger_zone": "DANGER ZONE"
"danger_zone": "DANGER ZONE",
"introdb_contribution": "INTRODB CONTRIBUTION"
},
"items": {
"legal": "Legal & Disclaimer",
@ -758,7 +759,12 @@
"reset_campaigns": "Reset Campaigns",
"reset_campaigns_desc": "Clear campaign impressions",
"clear_all_data": "Clear All Data",
"clear_all_data_desc": "Reset all settings and cached data"
"clear_all_data_desc": "Reset all settings and cached data",
"enable_intro_submission": "Enable Intro Submission",
"enable_intro_submission_desc": "Contribute timestamps to the community",
"introdb_api_key": "INTRODB API KEY",
"introdb_key_placeholder": "Enter your API Key",
"api_key_cleared": "API Key Cleared"
},
"options": {
"horizontal": "Horizontal",
@ -906,10 +912,10 @@
"confirm_remove_msg": "Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.",
"success_removed": "API key removed successfully",
"error_remove": "Failed to remove API key",
"model":"Model",
"using":"Using",
"free_routing":"(free automatic routing)",
"paid_model":"Use a custom OpenRouter model ID (useful for paid plans)."
"model": "Model",
"using": "Using",
"free_routing": "(free automatic routing)",
"paid_model": "Use a custom OpenRouter model ID (useful for paid plans)."
},
"catalog_settings": {
"title": "Catalogs",

View file

@ -696,7 +696,8 @@
"media": "MEDIA",
"notifications": "NOTIFICHE",
"testing": "TEST",
"danger_zone": "ZONA PERICOLOSA"
"danger_zone": "ZONA PERICOLOSA",
"introdb_contribution": "CONTRIBUTI INTRODB"
},
"items": {
"legal": "Note Legali & Disclaimer",
@ -757,7 +758,12 @@
"reset_campaigns": "Ripristina Campagne",
"reset_campaigns_desc": "Cancella le impressioni delle campagne",
"clear_all_data": "Cancella tutti i dati",
"clear_all_data_desc": "Ripristina tutte le impostazioni e i dati memorizzati"
"clear_all_data_desc": "Ripristina tutte le impostazioni e i dati memorizzati",
"enable_intro_submission":"Abilita contributi IntroDB",
"enable_intro_submission_desc":"Contribuisci con i tuoi timestamp",
"introdb_api_key":"CHIAVE API INTRODB",
"introdb_key_placeholder":"Inserisci la tua chiave API",
"api_key_cleared":"Chiave API rimossa"
},
"options": {
"horizontal": "Orizzontale",

View file

@ -1,5 +1,22 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput, ActivityIndicator } from 'react-native';
import React, {
useState,
useCallback,
useMemo,
useRef,
useEffect,
} from 'react';
import {
View,
StyleSheet,
ScrollView,
StatusBar,
Platform,
Text,
TouchableOpacity,
Dimensions,
TextInput,
ActivityIndicator,
} from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -7,10 +24,19 @@ import { useTheme } from '../../contexts/ThemeContext';
import { useSettings } from '../../hooks/useSettings';
import { RootStackParamList } from '../../navigation/AppNavigator';
import ScreenHeader from '../../components/common/ScreenHeader';
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
import {
SettingsCard,
SettingItem,
CustomSwitch,
ChevronRight,
} from './SettingsComponents';
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
import { MaterialIcons } from '@expo/vector-icons';
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import {
BottomSheetModal,
BottomSheetScrollView,
BottomSheetBackdrop,
} from '@gorhom/bottom-sheet';
import { useTranslation } from 'react-i18next';
import { SvgXml } from 'react-native-svg';
import { toastService } from '../../services/toastService';
@ -66,9 +92,21 @@ const AVAILABLE_LANGUAGES = [
];
const SUBTITLE_SOURCE_OPTIONS = [
{ value: 'internal', label: 'Internal First', description: 'Prefer embedded subtitles, then external' },
{ value: 'external', label: 'External First', description: 'Prefer addon subtitles, then embedded' },
{ value: 'any', label: 'Any Available', description: 'Use first available subtitle track' },
{
value: 'internal',
label: 'Internal First',
description: 'Prefer embedded subtitles, then external',
},
{
value: 'external',
label: 'External First',
description: 'Prefer addon subtitles, then embedded',
},
{
value: 'any',
label: 'Any Available',
description: 'Use first available subtitle track',
},
];
// Props for the reusable content component
@ -80,7 +118,9 @@ interface PlaybackSettingsContentProps {
* Reusable PlaybackSettingsContent component
* Can be used inline (tablets) or wrapped in a screen (mobile)
*/
export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = ({ isTablet = false }) => {
export const PlaybackSettingsContent: React.FC<
PlaybackSettingsContentProps
> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const { settings, updateSetting } = useSettings();
@ -107,7 +147,9 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
const handleApiKeySubmit = async () => {
if (!apiKeyInput.trim()) {
updateSetting('introDbApiKey', '');
toastService.success(t('settings.items.api_key_cleared', { defaultValue: 'API Key Cleared' }));
toastService.success(
t('settings.items.api_key_cleared'),
);
return;
}
@ -119,9 +161,13 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
if (isValid) {
updateSetting('introDbApiKey', apiKeyInput);
toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' }));
toastService.success(
t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' }),
);
} else {
toastService.error(t('settings.items.api_key_invalid', { defaultValue: 'Invalid API Key' }));
toastService.error(
t('settings.items.api_key_invalid', { defaultValue: 'Invalid API Key' }),
);
}
};
@ -135,8 +181,14 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
// Map known classes from the IntroDB logo to equivalent inline attributes
xml = xml.replace(/class="cls-4"/g, 'fill="url(#linear-gradient)"');
xml = xml.replace(/class="cls-3"/g, 'fill="#141414" opacity=".38"');
xml = xml.replace(/class="cls-1"/g, 'fill="url(#linear-gradient-2)" opacity=".53"');
xml = xml.replace(/class="cls-2"/g, 'fill="url(#linear-gradient-3)" opacity=".53"');
xml = xml.replace(
/class="cls-1"/g,
'fill="url(#linear-gradient-2)" opacity=".53"',
);
xml = xml.replace(
/class="cls-2"/g,
'fill="url(#linear-gradient-3)" opacity=".53"',
);
// Remove the <style> block to avoid unsupported CSS
xml = xml.replace(/<style>[\s\S]*?<\/style>/, '');
if (!cancelled) setIntroDbLogoXml(xml);
@ -153,7 +205,11 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
const introDbLogoIcon = introDbLogoXml ? (
<SvgXml xml={introDbLogoXml} width={28} height={18} />
) : (
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
<MaterialIcons
name="skip-next"
size={18}
color={currentTheme.colors.primary}
/>
);
// Bottom sheet refs
@ -192,11 +248,11 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
};
const hasVisibleItems = (itemIds: string[]) => {
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);
const lang = AVAILABLE_LANGUAGES.find((l) => l.code === code);
return lang ? lang.name : code.toUpperCase();
};
@ -217,7 +273,7 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
opacity={0.5}
/>
),
[]
[],
);
const handleSelectAudioLanguage = (code: string) => {
@ -230,7 +286,9 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
subtitleLanguageSheetRef.current?.dismiss();
};
const handleSelectSubtitleSource = (value: 'internal' | 'external' | 'any') => {
const handleSelectSubtitleSource = (
value: 'internal' | 'external' | 'any',
) => {
updateSetting('subtitleSourcePreference', value);
subtitleSourceSheetRef.current?.dismiss();
};
@ -238,13 +296,22 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
return (
<>
{hasVisibleItems(['video_player']) && (
<SettingsCard title={t('settings.sections.video_player')} isTablet={isTablet}>
<SettingsCard
title={t('settings.sections.video_player')}
isTablet={isTablet}
>
{isItemVisible('video_player') && (
<SettingItem
title={t('settings.items.video_player')}
description={Platform.OS === 'ios'
? (settings?.preferredPlayer === 'internal' ? t('settings.items.built_in') : settings?.preferredPlayer?.toUpperCase() || t('settings.items.built_in'))
: (settings?.useExternalPlayer ? t('settings.items.external') : t('settings.items.built_in'))
description={
Platform.OS === 'ios'
? settings?.preferredPlayer === 'internal'
? t('settings.items.built_in')
: settings?.preferredPlayer?.toUpperCase() ||
t('settings.items.built_in')
: settings?.useExternalPlayer
? t('settings.items.external')
: t('settings.items.built_in')
}
icon="play-circle"
renderControl={() => <ChevronRight />}
@ -256,10 +323,17 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
</SettingsCard>
)}
<SettingsCard title={t('player.section_playback', { defaultValue: 'Playback' })} isTablet={isTablet}>
<SettingsCard
title={t('player.section_playback', { defaultValue: 'Playback' })}
isTablet={isTablet}
>
<SettingItem
title={t('player.skip_intro_settings_title', { defaultValue: 'Skip Intro' })}
description={t('player.powered_by_introdb', { defaultValue: 'Powered by IntroDB' })}
title={t('player.skip_intro_settings_title', {
defaultValue: 'Skip Intro',
})}
description={t('player.powered_by_introdb', {
defaultValue: 'Powered by IntroDB',
})}
customIcon={introDbLogoIcon}
renderControl={() => (
<CustomSwitch
@ -273,10 +347,13 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
</SettingsCard>
{/* IntroDB Contribution Section */}
<SettingsCard title={t('settings.sections.introdb_contribution', { defaultValue: 'IntroDB Contribution' })} isTablet={isTablet}>
<SettingsCard
title={t('settings.sections.introdb_contribution')}
isTablet={isTablet}
>
<SettingItem
title={t('settings.items.enable_intro_submission', { defaultValue: 'Enable Intro Submission' })}
description={t('settings.items.enable_intro_submission_desc', { defaultValue: 'Contribute timestamps to the community' })}
title={t('settings.items.enable_intro_submission')}
description={t('settings.items.enable_intro_submission_desc')}
icon="flag"
renderControl={() => (
<CustomSwitch
@ -291,14 +368,17 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
{settings?.introSubmitEnabled && (
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>
{t('settings.items.introdb_api_key', { defaultValue: 'INTRODB API KEY' })}
{t('settings.items.introdb_api_key')}
</Text>
<View style={styles.apiKeyRow}>
<TextInput
style={[styles.input, { flex: 1, marginRight: 10, color: currentTheme.colors.highEmphasis }]}
style={[
styles.input,
{ flex: 1, marginRight: 10, color: currentTheme.colors.highEmphasis },
]}
value={apiKeyInput}
onChangeText={setApiKeyInput}
placeholder="Enter your API key"
placeholder={t('settings.items.introdb_key_placeholder')}
placeholderTextColor={currentTheme.colors.mediumEmphasis}
autoCapitalize="none"
autoCorrect={false}
@ -321,7 +401,10 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
</SettingsCard>
{/* Audio & Subtitle Preferences */}
<SettingsCard title={t('settings.sections.audio_subtitles')} isTablet={isTablet}>
<SettingsCard
title={t('settings.sections.audio_subtitles')}
isTablet={isTablet}
>
<SettingItem
title={t('settings.items.preferred_audio')}
description={getLanguageName(settings?.preferredAudioLanguage || 'en')}
@ -340,7 +423,9 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
/>
<SettingItem
title={t('settings.items.subtitle_source')}
description={getSourceLabel(settings?.subtitleSourcePreference || 'internal')}
description={getSourceLabel(
settings?.subtitleSourcePreference || 'internal',
)}
icon="layers"
renderControl={() => <ChevronRight />}
onPress={openSubtitleSourceSheet}
@ -353,7 +438,9 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
renderControl={() => (
<CustomSwitch
value={settings?.enableSubtitleAutoSelect ?? true}
onValueChange={(value) => updateSetting('enableSubtitleAutoSelect', value)}
onValueChange={(value) =>
updateSetting('enableSubtitleAutoSelect', value)
}
/>
)}
isLast
@ -396,7 +483,10 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
)}
{hasVisibleItems(['notifications']) && (
<SettingsCard title={t('settings.sections.notifications')} isTablet={isTablet}>
<SettingsCard
title={t('settings.sections.notifications')}
isTablet={isTablet}
>
{isItemVisible('notifications') && (
<SettingItem
title={t('settings.items.notifications')}
@ -423,28 +513,38 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
>
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{t('settings.items.preferred_audio')}</Text>
<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');
const isSelected =
lang.code === (settings?.preferredAudioLanguage || 'en');
return (
<TouchableOpacity
key={lang.code}
style={[
styles.languageItem,
isSelected && { backgroundColor: currentTheme.colors.primary + '20' }
isSelected && { backgroundColor: currentTheme.colors.primary + '20' },
]}
onPress={() => handleSelectAudioLanguage(lang.code)}
>
<Text style={[styles.languageName, { color: isSelected ? currentTheme.colors.primary : '#fff' }]}>
<Text
style={[
styles.languageName,
{ color: isSelected ? currentTheme.colors.primary : '#fff' },
]}
>
{lang.name}
</Text>
<Text style={styles.languageCode}>
{lang.code.toUpperCase()}
</Text>
<Text style={styles.languageCode}>{lang.code.toUpperCase()}</Text>
{isSelected && (
<MaterialIcons name="check" size={20} color={currentTheme.colors.primary} />
<MaterialIcons
name="check"
size={20}
color={currentTheme.colors.primary}
/>
)}
</TouchableOpacity>
);
@ -464,28 +564,38 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
>
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{t('settings.items.preferred_subtitle')}</Text>
<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');
const isSelected =
lang.code === (settings?.preferredSubtitleLanguage || 'en');
return (
<TouchableOpacity
key={lang.code}
style={[
styles.languageItem,
isSelected && { backgroundColor: currentTheme.colors.primary + '20' }
isSelected && { backgroundColor: currentTheme.colors.primary + '20' },
]}
onPress={() => handleSelectSubtitleLanguage(lang.code)}
>
<Text style={[styles.languageName, { color: isSelected ? currentTheme.colors.primary : '#fff' }]}>
<Text
style={[
styles.languageName,
{ color: isSelected ? currentTheme.colors.primary : '#fff' },
]}
>
{lang.name}
</Text>
<Text style={styles.languageCode}>
{lang.code.toUpperCase()}
</Text>
<Text style={styles.languageCode}>{lang.code.toUpperCase()}</Text>
{isSelected && (
<MaterialIcons name="check" size={20} color={currentTheme.colors.primary} />
<MaterialIcons
name="check"
size={20}
color={currentTheme.colors.primary}
/>
)}
</TouchableOpacity>
);
@ -505,32 +615,53 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
>
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{t('settings.items.subtitle_source')}</Text>
<Text style={styles.sheetTitle}>
{t('settings.items.subtitle_source')}
</Text>
</View>
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
{SUBTITLE_SOURCE_OPTIONS.map((option) => {
const isSelected = option.value === (settings?.subtitleSourcePreference || 'internal');
const isSelected =
option.value === (settings?.subtitleSourcePreference || 'internal');
return (
<TouchableOpacity
key={option.value}
style={[
styles.sourceItem,
isSelected && { backgroundColor: currentTheme.colors.primary + '20', borderColor: currentTheme.colors.primary }
isSelected && {
backgroundColor: currentTheme.colors.primary + '20',
borderColor: currentTheme.colors.primary,
},
]}
onPress={() => handleSelectSubtitleSource(option.value as 'internal' | 'external' | 'any')}
onPress={() =>
handleSelectSubtitleSource(
option.value as 'internal' | 'external' | 'any',
)
}
>
<View style={styles.sourceItemContent}>
<Text style={[styles.sourceLabel, { color: isSelected ? currentTheme.colors.primary : '#fff' }]}>
<Text
style={[
styles.sourceLabel,
{ color: isSelected ? currentTheme.colors.primary : '#fff' },
]}
>
{getSourceLabel(option.value)}
</Text>
<Text style={styles.sourceDescription}>
{option.value === 'internal' && t('settings.options.internal_first_desc')}
{option.value === 'external' && t('settings.options.external_first_desc')}
{option.value === 'internal' &&
t('settings.options.internal_first_desc')}
{option.value === 'external' &&
t('settings.options.external_first_desc')}
{option.value === 'any' && t('settings.options.any_available_desc')}
</Text>
</View>
{isSelected && (
<MaterialIcons name="check" size={20} color={currentTheme.colors.primary} />
<MaterialIcons
name="check"
size={20}
color={currentTheme.colors.primary}
/>
)}
</TouchableOpacity>
);
@ -553,14 +684,26 @@ const PlaybackSettingsScreen: React.FC = () => {
const screenIsTablet = width >= 768;
return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<View
style={[
styles.container,
{ backgroundColor: currentTheme.colors.darkBackground },
]}
>
<StatusBar barStyle="light-content" />
<ScreenHeader title={t('settings.playback')} showBackButton onBackPress={() => navigation.goBack()} />
<ScreenHeader
title={t('settings.playback')}
showBackButton
onBackPress={() => navigation.goBack()}
/>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: insets.bottom + 24 },
]}
>
<PlaybackSettingsContent isTablet={screenIsTablet} />
</ScrollView>