import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { View, ScrollView, Pressable, ActivityIndicator, Animated } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useTheme } from '~/features/theme/ThemeProvider'; import Input from '~/components/atoms/Input'; import Icon from '~/components/atoms/Icon'; import Text from '~/components/atoms/Text'; import { formatLanguageDisplay } from '~/utils/languageDisplay'; import { useRecordingLanguage } from '~/features/audioRecordingV2'; export interface LanguageItem { code: string; nativeName: string; emoji: string; locale?: string; isExperimental?: boolean; } interface BaseLanguageSelectorProps { languages: Record; selectedLanguages: string[]; onSelect: (languages: string[]) => void; mode?: 'single' | 'multi'; showAutoDetect?: boolean; showExperimentalWarning?: boolean; loading?: boolean; placeholder?: string; searchPlaceholder?: string; height?: number; priorityLanguages?: string[]; onClose?: () => void; autoSelectOnSingle?: boolean; } /** * Unified base component for all language selection needs. * Supports both single and multi-selection modes with consistent UI. */ const BaseLanguageSelector: React.FC = ({ languages, selectedLanguages, onSelect, mode = 'multi', showAutoDetect = false, showExperimentalWarning = false, loading = false, placeholder, searchPlaceholder, height = 450, priorityLanguages = ['de', 'en', 'fr', 'es', 'it'], onClose, autoSelectOnSingle = false, }) => { const { t } = useTranslation(); const { isDark, themeVariant } = useTheme(); const { dialectChangeNotification, clearDialectNotification } = useRecordingLanguage(); // State for search const [searchQuery, setSearchQuery] = useState(''); // Animation value for notification const notificationOpacity = useState(new Animated.Value(0))[0]; const notificationTranslateY = useState(new Animated.Value(-20))[0]; // Get theme colors const getThemeColor = useCallback((colorKey: string) => { try { const colors = require('../../tailwind.config.js'); if (isDark && colors.theme?.extend?.colors?.dark?.[themeVariant]) { return colors.theme.extend.colors.dark[themeVariant][colorKey]; } else if (colors.theme?.extend?.colors?.[themeVariant]) { return colors.theme.extend.colors[themeVariant][colorKey]; } } catch (e) { // Fallback colors } // Fallback colors if (isDark) { return { text: '#FFFFFF', contentBackground: '#1E1E1E', contentBackgroundHover: '#333333', border: '#374151', primary: '#f8d62b', secondary: 'rgba(255, 255, 255, 0.7)', muted: 'rgba(255, 255, 255, 0.5)', }[colorKey] || '#FFFFFF'; } else { return { text: '#000000', contentBackground: '#FFFFFF', contentBackgroundHover: '#F5F5F5', border: '#E5E7EB', primary: '#f8d62b', secondary: 'rgba(0, 0, 0, 0.7)', muted: 'rgba(0, 0, 0, 0.5)', }[colorKey] || '#000000'; } }, [isDark, themeVariant]); const themeColors = (require('../../tailwind.config.js') as any).theme?.extend?.colors as Record; const textColor = getThemeColor('text'); const hoverColor = getThemeColor('contentBackgroundHover'); const borderColor = getThemeColor('border'); const primaryColor = getThemeColor('primary'); const secondaryColor = getThemeColor('secondary'); const mutedColor = getThemeColor('muted'); const contentBackgroundColor = getThemeColor('contentBackground'); // Handle notification animation useEffect(() => { if (dialectChangeNotification) { // Animate in Animated.parallel([ Animated.timing(notificationOpacity, { toValue: 1, duration: 300, useNativeDriver: true, }), Animated.timing(notificationTranslateY, { toValue: 0, duration: 300, useNativeDriver: true, }), ]).start(); } else { // Animate out Animated.parallel([ Animated.timing(notificationOpacity, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(notificationTranslateY, { toValue: -20, duration: 200, useNativeDriver: true, }), ]).start(); } }, [dialectChangeNotification]); // Filter and sort languages const filteredLanguages = useMemo(() => { const entries = Object.entries(languages); // Filter based on search query const filtered = searchQuery.trim() ? entries.filter(([code, language]) => { const query = searchQuery.toLowerCase(); return ( code.toLowerCase().includes(query) || language.nativeName.toLowerCase().includes(query) ); }) : entries; // Sort languages with priority return filtered.sort((a, b) => { // "Auto" always first if enabled if (showAutoDetect) { if (a[0] === 'auto') return -1; if (b[0] === 'auto') return 1; } // Selected languages next (only in multi mode) if (mode === 'multi') { const aSelected = selectedLanguages.includes(a[0]); const bSelected = selectedLanguages.includes(b[0]); if (aSelected && !bSelected) return -1; if (!aSelected && bSelected) return 1; } // Priority languages const aPriority = priorityLanguages.indexOf(a[0]); const bPriority = priorityLanguages.indexOf(b[0]); if (aPriority !== -1 && bPriority !== -1) { return aPriority - bPriority; } if (aPriority !== -1) return -1; if (bPriority !== -1) return 1; // Official languages before experimental (if applicable) if (showExperimentalWarning) { const aExperimental = a[1].isExperimental || false; const bExperimental = b[1].isExperimental || false; if (!aExperimental && bExperimental) return -1; if (aExperimental && !bExperimental) return 1; } // Finally alphabetically by native name return a[1].nativeName.localeCompare(b[1].nativeName); }); }, [searchQuery, languages, selectedLanguages, mode, showAutoDetect, priorityLanguages, showExperimentalWarning]); // Handle language selection const handleLanguagePress = useCallback((languageCode: string) => { if (mode === 'single') { onSelect([languageCode]); if (autoSelectOnSingle && onClose) { onClose(); } } else { // Multi-select mode if (languageCode === 'auto' && showAutoDetect) { // If auto is selected, deselect all others if (selectedLanguages.includes('auto')) { onSelect([]); } else { onSelect(['auto']); } } else { // Remove auto if selecting a specific language const filteredSelection = selectedLanguages.filter(lang => lang !== 'auto'); if (selectedLanguages.includes(languageCode)) { // Deselect onSelect(filteredSelection.filter(lang => lang !== languageCode)); } else { // Select onSelect([...filteredSelection, languageCode]); } } } }, [mode, selectedLanguages, onSelect, showAutoDetect, autoSelectOnSingle, onClose]); // Handle search clear const handleClearSearch = useCallback(() => { setSearchQuery(''); }, []); return ( {/* In-Modal Notification */} {dialectChangeNotification && ( {/* Icon and Title Row */} {t('recording.dialect_switch_title', 'Sprachvariante gewechselt')} {/* Message */} {t('recording.dialect_switch_explanation', { oldDialect: dialectChangeNotification.oldDialect, newDialect: dialectChangeNotification.newDialect, defaultValue: `Die Transkription unterstützt nur eine Sprachvariante gleichzeitig. ${dialectChangeNotification.newDialect} ist jetzt aktiv, ${dialectChangeNotification.oldDialect} wurde deaktiviert.` })} )} {/* Search Field - Unified Design */} 0 ? 44 : 16 }} autoCapitalize="none" autoCorrect={false} editable={!loading} /> {/* Search Icon */} {/* Clear Button */} {searchQuery.length > 0 && ( )} {/* Language List */} {loading ? ( ) : filteredLanguages.length > 0 ? ( filteredLanguages.map(([code, language]) => { const isSelected = selectedLanguages.includes(code); const isExperimental = language.isExperimental || false; return ( handleLanguagePress(code)} accessibilityRole="button" accessibilityLabel={`${language.nativeName} ${isSelected ? t('common.selected', 'ausgewählt') : ''}`} disabled={loading} android_ripple={{ color: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', borderless: false }} > {language.emoji} {formatLanguageDisplay(code, language.nativeName, t)} {isExperimental && showExperimentalWarning && ' ⚠️'} {isExperimental && showExperimentalWarning && ( {t('common.experimental', 'Experimentell')} )} {/* Selection Indicator */} {mode === 'multi' && isSelected && ( )} {mode === 'single' && isSelected && ( )} ); }) ) : ( {t('common.no_results', 'Keine Ergebnisse gefunden')} )} ); }; export default BaseLanguageSelector;