Files
life-echo/app-ios/app/(tabs)/memoir.tsx
penghanyuan 748f252c2f add ios app
2026-01-31 21:20:50 +01:00

373 lines
9.9 KiB
TypeScript

import { ChapterContent } from '@/components/memoir/ChapterContent';
import { ChapterItem } from '@/components/memoir/ChapterItem';
import { FullMemoirContent } from '@/components/memoir/FullMemoirContent';
import { AppColors } from '@/constants/theme';
import { Chapter, mockMemoir } from '@/data/mockData';
import { Ionicons } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { SafeAreaView } from 'react-native-safe-area-context';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
type ViewType = 'toc' | 'chapter' | 'fullRead';
export default function MemoirScreen() {
const [currentView, setCurrentView] = useState<ViewType>('toc');
const [selectedChapter, setSelectedChapter] = useState<Chapter | null>(null);
const [showMoreMenu, setShowMoreMenu] = useState(false);
// Animation values
const tocTranslateX = useSharedValue(0);
const readingTranslateX = useSharedValue(SCREEN_WIDTH);
const isReading = currentView === 'chapter' || currentView === 'fullRead';
useEffect(() => {
if (isReading) {
// Slide reading view in from right
tocTranslateX.value = withTiming(-SCREEN_WIDTH * 0.3, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
readingTranslateX.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
} else {
// Slide reading view out to right
tocTranslateX.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
readingTranslateX.value = withTiming(SCREEN_WIDTH, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
}
}, [isReading]);
const tocAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: tocTranslateX.value }],
}));
const readingAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: readingTranslateX.value }],
}));
const handleChapterPress = (chapter: Chapter) => {
setSelectedChapter(chapter);
setCurrentView('chapter');
};
const handleBackToToc = () => {
setCurrentView('toc');
setTimeout(() => {
setSelectedChapter(null);
}, 300);
};
const handleReadAll = () => {
setCurrentView('fullRead');
setSelectedChapter(null);
};
const handleExportPdf = () => {
setShowMoreMenu(false);
console.log('Export PDF pressed');
};
const handleShare = () => {
setShowMoreMenu(false);
console.log('Share pressed');
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.contentWrapper}>
{/* TOC View */}
<Animated.View style={[styles.tocView, tocAnimatedStyle]}>
<View style={styles.purpleHeader}>
<Text style={styles.purpleHeaderTitle}></Text>
</View>
<ScrollView
style={styles.tocContainer}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.tocContent}
>
{/* Book Info */}
<View style={styles.bookHeader}>
<Text style={styles.bookTitle}>{mockMemoir.title}</Text>
<Text style={styles.bookSubtitle}>{mockMemoir.subtitle}</Text>
<Text style={styles.bookUpdate}> {mockMemoir.updatedAt}</Text>
</View>
{/* Chapter List */}
{mockMemoir.chapters.map((chapter) => (
<ChapterItem
key={chapter.id}
chapter={chapter}
onPress={() => handleChapterPress(chapter)}
/>
))}
</ScrollView>
{/* Read All Button */}
{mockMemoir.chapters.length > 0 && (
<View style={styles.floatingActions}>
<TouchableOpacity
style={styles.readAllButton}
onPress={handleReadAll}
activeOpacity={0.8}
>
<Ionicons name="book" size={18} color={AppColors.white} />
<Text style={styles.readAllButtonText}></Text>
</TouchableOpacity>
</View>
)}
</Animated.View>
{/* Reading View */}
<Animated.View style={[styles.readingView, readingAnimatedStyle]}>
<View style={styles.readingHeader}>
<TouchableOpacity
style={styles.backButton}
onPress={handleBackToToc}
activeOpacity={0.7}
>
<Ionicons name="chevron-back" size={24} color={AppColors.white} />
<Text style={styles.backButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.moreButton}
onPress={() => setShowMoreMenu(true)}
activeOpacity={0.7}
>
<Ionicons name="ellipsis-horizontal" size={22} color={AppColors.white} />
</TouchableOpacity>
</View>
<View style={styles.readingContainer}>
{currentView === 'fullRead' ? (
<FullMemoirContent memoir={mockMemoir} />
) : selectedChapter ? (
<ChapterContent chapter={selectedChapter} />
) : null}
</View>
</Animated.View>
</View>
{/* More Menu Modal */}
<Modal
visible={showMoreMenu}
transparent
animationType="fade"
onRequestClose={() => setShowMoreMenu(false)}
>
<Pressable
style={styles.modalOverlay}
onPress={() => setShowMoreMenu(false)}
>
<View style={styles.menuContainer}>
<TouchableOpacity
style={styles.menuItem}
onPress={handleExportPdf}
activeOpacity={0.7}
>
<Ionicons name="download-outline" size={20} color={AppColors.deepPurple} />
<Text style={styles.menuItemText}> PDF</Text>
</TouchableOpacity>
<View style={styles.menuDivider} />
<TouchableOpacity
style={styles.menuItem}
onPress={handleShare}
activeOpacity={0.7}
>
<Ionicons name="share-social-outline" size={20} color={AppColors.deepPurple} />
<Text style={styles.menuItemText}></Text>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.mediumPurple,
},
contentWrapper: {
flex: 1,
position: 'relative',
overflow: 'hidden',
},
tocView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
readingView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: AppColors.mediumPurple,
},
purpleHeader: {
height: 62,
paddingHorizontal: 20,
justifyContent: 'center',
backgroundColor: AppColors.mediumPurple,
},
purpleHeaderTitle: {
fontSize: 32,
fontWeight: '600',
color: AppColors.white,
},
readingHeader: {
height: 62,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: AppColors.mediumPurple,
paddingHorizontal: 16,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
},
backButtonText: {
fontSize: 16,
color: AppColors.white,
marginLeft: 4,
},
moreButton: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
},
tocContainer: {
flex: 1,
backgroundColor: AppColors.cream,
},
tocContent: {
padding: 20,
paddingBottom: 120,
},
bookHeader: {
alignItems: 'center',
marginBottom: 24,
paddingVertical: 20,
backgroundColor: AppColors.white,
borderRadius: 16,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 2,
},
bookTitle: {
fontSize: 24,
fontWeight: '600',
color: AppColors.deepPurple,
marginBottom: 6,
},
bookSubtitle: {
fontSize: 13,
color: AppColors.slatePurple,
},
bookUpdate: {
fontSize: 12,
color: AppColors.mediumPurple,
marginTop: 6,
},
readingContainer: {
flex: 1,
backgroundColor: AppColors.cream,
},
floatingActions: {
position: 'absolute',
bottom: Platform.OS === 'ios' ? 100 : 80,
left: 0,
right: 0,
alignItems: 'center',
paddingHorizontal: 20,
},
readAllButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 25,
backgroundColor: AppColors.mediumPurple,
gap: 8,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 12,
elevation: 4,
},
readAllButtonText: {
fontSize: 16,
fontWeight: '600',
color: AppColors.white,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'flex-start',
alignItems: 'flex-end',
paddingTop: Platform.OS === 'ios' ? 110 : 90,
paddingRight: 20,
},
menuContainer: {
backgroundColor: AppColors.white,
borderRadius: 12,
paddingVertical: 8,
minWidth: 160,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 16,
elevation: 8,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
gap: 12,
},
menuItemText: {
fontSize: 15,
color: AppColors.deepPurple,
},
menuDivider: {
height: 1,
backgroundColor: 'rgba(32, 0, 40, 0.08)',
marginHorizontal: 16,
},
});