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;