import React, { useMemo } from 'react'; import { View, Pressable, Platform, ScrollView, Animated, Dimensions } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { LinearGradient } from 'expo-linear-gradient'; import { Link, useRouter, usePathname } from 'expo-router'; import * as Haptics from 'expo-haptics'; import { useTheme } from '~/features/theme/ThemeProvider'; import { useTranslation } from 'react-i18next'; // BlurView import removed import colors from '~/tailwind.config.js'; import Icon from '~/components/atoms/Icon'; import Text from '~/components/atoms/Text'; import HeaderMenu from '~/features/menus/HeaderMenu'; import ManaCounter from '~/features/subscription/ManaCounter'; import { useSettingsStore } from '~/features/settings'; interface RightIconProps { name: string; onPress?: () => void; href?: string; customElement?: React.ReactNode; } interface TagItem { id: string; name: string; color: string; } interface HeaderProps { title?: string; showBackButton?: boolean; rightIcon?: string; onRightIconPress?: () => void; rightIconHref?: string; rightIcons?: RightIconProps[]; className?: string; showMenu?: boolean; onThemeToggle?: () => void; selectedTags?: TagItem[]; onTagRemove?: (tagId: string) => void; isHomePage?: boolean; scrollableTitle?: boolean; showTitle?: boolean; isMemoDetailPage?: boolean; backgroundColor?: string; } /** * Header-Komponente * * Eine wiederverwendbare Header-Komponente für alle Seiten der Anwendung. * Unterstützt einen Titel, einen Zurück-Button und ein optionales rechtes Icon. * * Beispiel: * ```tsx *
*
*
* ``` */ const Header: React.FC = ({ title, showBackButton = false, rightIcon, onRightIconPress, rightIconHref, rightIcons, className = '', showMenu = false, onThemeToggle, selectedTags = [], onTagRemove, isHomePage = false, scrollableTitle = false, showTitle = true, isMemoDetailPage = false, backgroundColor, }) => { const { t } = useTranslation(); const { tw, isDark, themeVariant } = useTheme(); const router = useRouter(); const pathname = usePathname(); const { showManaBadge } = useSettingsStore(); const insets = useSafeAreaInsets(); // Use a fixed top padding for initial render to prevent layout shift // iOS typically has 44-47px status bar, Android varies but usually 0-24px const safeTopPadding = insets.top || (Platform.OS === 'ios' ? 44 : 0); // Generate title text based on selected tags - moved up for animation const getTitleText = () => { if (selectedTags && selectedTags.length > 0) { // Join tag names with ' & ' separator return selectedTags.map((tag) => tag.name).join(' & '); } // Return title directly, don't fallback to 'Memoro' if title is empty string return title === '' ? '' : title || t('app.name', 'Memoro'); }; // Get the actual title text that will be displayed const actualTitleText = getTitleText(); // Track if this is the initial render to skip animation const isInitialRender = React.useRef(true); // Animation for title fade in/out - initialize based on showTitle prop const titleOpacity = React.useRef( new Animated.Value(showTitle && actualTitleText ? 1 : 0) ).current; // Animate title opacity when showTitle or title text changes (skip on initial render) React.useEffect(() => { // Skip animation on initial render to prevent layout shift if (isInitialRender.current) { isInitialRender.current = false; // Set opacity directly without animation const initialOpacity = showTitle && actualTitleText ? 1 : 0; titleOpacity.setValue(initialOpacity); return; } const targetOpacity = showTitle && actualTitleText ? 1 : 0; Animated.timing(titleOpacity, { toValue: targetOpacity, duration: 200, useNativeDriver: true, }).start(); }, [showTitle, actualTitleText, titleOpacity]); const triggerHaptic = async () => { try { await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } catch (error) { console.debug('Haptic feedback error:', error); } }; // Bestimme, ob das Menü-Icon angezeigt werden soll const shouldShowMenu = useMemo(() => { // Wenn rightIcons ein Element mit 'ellipsis-vertical' enthält, zeige das HeaderMenu nicht an // Dies verhindert, dass sowohl das HeaderMenu als auch ein benutzerdefiniertes Menü angezeigt werden const hasCustomMenu = rightIcons?.some( (icon) => icon.name === 'ellipsis-vertical' || icon.customElement ); if (hasCustomMenu) { return false; } // Auf Memo-Detailseiten nicht anzeigen if (pathname && pathname.includes('/(memo)/')) { return false; } // Auf allen anderen Seiten anzeigen, wenn showMenu explizit auf true gesetzt ist return showMenu === true; }, [pathname, showMenu, rightIcons]); // Determine the header background color based on the theme // Use provided backgroundColor or default to menuBackground const headerBackgroundColor = useMemo(() => { if (backgroundColor) { return backgroundColor; } return isDark ? colors.theme.extend.colors.dark[themeVariant].menuBackground : colors.theme.extend.colors[themeVariant].menuBackground; }, [backgroundColor, isDark, themeVariant]); // Blur effect code removed // Bestimme die Textfarbe basierend auf dem Theme const textColor = useMemo(() => (isDark ? '#FFFFFF' : '#000000'), [isDark]); // Handler für den Zurück-Button const handleBackPress = async () => { await triggerHaptic(); router.back(); }; // Rendere die rechten Icons const renderRightIcons = () => { // Container für rechte Icons und Menü const rightContainer = ( {/* Zeige den ManaCounter an, wenn wir auf der Homepage sind und showManaBadge aktiviert ist */} {isHomePage && showManaBadge && ( )} {/* Zeige die konfigurierten Icons an */} {rightIcons && rightIcons.length > 0 && ( {rightIcons.map((icon, index) => { // Wenn ein benutzerdefiniertes Element vorhanden ist, verwende dieses if (icon.customElement) { return ( {icon.customElement} ); } // Ansonsten Standard-Icon-Element erstellen const iconElement = ( { await triggerHaptic(); icon.onPress?.(); }} > ); // Wenn ein href vorhanden ist, verlinke das Icon return icon.href ? ( {iconElement} ) : ( iconElement ); })} )} {/* Einzelnes Icon, wenn keine Icon-Liste vorhanden ist */} {!rightIcons && rightIcon && ( {rightIconHref ? ( { await triggerHaptic(); onRightIconPress?.(); }} > ) : ( { await triggerHaptic(); onRightIconPress?.(); }} > )} )} {/* Zeige das HeaderMenu an, wenn shouldShowMenu true ist */} {shouldShowMenu && ( )} ); return rightContainer; }; // Always use default text color for title const getTitleColor = () => { return textColor; }; return ( 1024 ? 24 : 0, // Match WebContainer's padding } : {} } > {showBackButton && ( )} {scrollableTitle ? ( { // If there are selected tags, allow removing them by clicking on the title if (selectedTags && selectedTags.length > 0 && onTagRemove) { await triggerHaptic(); // Remove all tags one by one selectedTags.forEach((tag) => onTagRemove(tag.id)); } }} > {actualTitleText} {/* Gradient overlay for scrollable title */} ) : ( { // If there are selected tags, allow removing them by clicking on the title if (selectedTags && selectedTags.length > 0 && onTagRemove) { await triggerHaptic(); // Remove all tags one by one selectedTags.forEach((tag) => onTagRemove(tag.id)); } }} > {actualTitleText} )} {renderRightIcons()} ); }; export default Header as React.FC;