import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useLocalSearchParams } from 'expo-router'; import { Settings, Trash2, X } from 'lucide-react-native'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, Modal, Platform, Pressable, ScrollView, StyleSheet, useWindowDimensions, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; import { Icon } from '@/components/ui/icon'; import { Text } from '@/components/ui/text'; import { ScreenHeader } from '@/components/screen-header'; import { ScreenGutter } from '@/constants/layout'; import { useChapterDetail, useDeleteChapter } from '@/features/memoir/hooks'; // Life-Echo reading colors (from HTML reference) const READING_COLORS = { background: '#F8F9FA', backgroundSepia: '#F2E8CF', primary: '#8177A6', onSurface: '#1b1b1f', onSurfaceVariant: '#48454e', divider: 'rgba(141, 140, 144, 0.2)', surfaceContainerHigh: '#e9e7ec', outlineVariant: 'rgba(121, 117, 127, 0.3)', }; type FontSize = 'small' | 'default' | 'large'; type FontFamily = 'serif' | 'sans'; type BackgroundTheme = 'white' | 'sepia'; const FONT_SIZES: Record = { small: 16, default: 20, large: 24, }; const LINE_HEIGHTS: Record = { small: 30, default: 38, large: 44, }; const FONT_FAMILIES: Record = { serif: Platform.select({ ios: 'Georgia', android: 'serif', default: 'serif' }) ?? 'serif', sans: Platform.select({ ios: 'System', android: 'sans-serif', default: 'sans-serif', }) ?? 'sans-serif', }; const BACKGROUND_COLORS: Record = { white: READING_COLORS.background, sepia: READING_COLORS.backgroundSepia, }; function ChapterContent({ sections, coverImageUrl, fontSize, fontFamily, backgroundColor, }: { sections: { content: string; image: { url: string | null } | null }[]; coverImageUrl: string | null; fontSize: FontSize; fontFamily: FontFamily; backgroundColor: BackgroundTheme; }) { const { width } = useWindowDimensions(); const contentWidth = Math.min(width - ScreenGutter * 2, 672); const heroAspectRatio = 4 / 5; const bodySize = FONT_SIZES[fontSize]; const lineHeight = LINE_HEIGHTS[fontSize]; const fontFam = FONT_FAMILIES[fontFamily]; const bgColor = BACKGROUND_COLORS[backgroundColor]; const [heroLoadFailed, setHeroLoadFailed] = useState(false); useEffect(() => { setHeroLoadFailed(false); }, [coverImageUrl]); const hasCoverImage = !!coverImageUrl && !heroLoadFailed; return ( <> {/* Hero Image: 仅在有封面图且加载成功时显示,避免无图或加载失败时大片空白 */} {hasCoverImage && ( Chapter hero setHeroLoadFailed(true)} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> )} {/* Memoir Content */} {(() => { const indent = '\u3000\u3000'; let isFirstParagraph = true; return sections.flatMap((section, i) => { const text = section.content?.trim() ?? ''; const hasImage = !!section.image?.url; if (!text && !hasImage) return []; const paragraphs = text ? text .split(/\n\n+/) .map((p) => p.trim()) .filter(Boolean) : []; const nodes: React.ReactNode[] = []; paragraphs.forEach((para, pIdx) => { const firstChar = para.charAt(0); const rest = para.slice(1); const isLastInSection = pIdx === paragraphs.length - 1 && !hasImage; const mb = isLastInSection ? 20 : 16; if (isFirstParagraph && firstChar) { const firstParaLineHeight = Math.round(lineHeight * 1.35); nodes.push( {firstChar} {rest} , ); isFirstParagraph = false; } else { nodes.push( {indent} {para} , ); } }); if (hasImage) { nodes.push( 0 ? 12 : 0, marginBottom: 20, }} />, ); } return nodes; }); })()} {sections.length > 0 && ( )} ); } function ReadingSettingsModal({ visible, onClose, fontSize, fontFamily, backgroundColor, onFontSizeChange, onFontFamilyChange, onBackgroundChange, t, }: { visible: boolean; onClose: () => void; fontSize: FontSize; fontFamily: FontFamily; backgroundColor: BackgroundTheme; onFontSizeChange: (v: FontSize) => void; onFontFamilyChange: (v: FontFamily) => void; onBackgroundChange: (v: BackgroundTheme) => void; t: (key: string) => string; }) { const insets = useSafeAreaInsets(); return ( {}} > {t('chapterReading.readingSettings')} [ readingSettingsStyles.closeBtn, pressed && { opacity: 0.6 }, ]} accessibilityLabel={t('chapterReading.close')} accessibilityRole="button" > {t('chapterReading.fontSize')} {(['small', 'default', 'large'] as const).map((s) => ( onFontSizeChange(s)} style={({ pressed }) => [ readingSettingsStyles.segItem, fontSize === s && readingSettingsStyles.segItemActive, pressed && { opacity: 0.8 }, ]} > {t( `chapterReading.fontSize${s.charAt(0).toUpperCase() + s.slice(1)}`, )} ))} {t('chapterReading.typography')} {(['serif', 'sans'] as const).map((f) => ( onFontFamilyChange(f)} style={({ pressed }) => [ readingSettingsStyles.segItem, fontFamily === f && readingSettingsStyles.segItemActive, pressed && { opacity: 0.8 }, ]} > {t( `chapterReading.font${f.charAt(0).toUpperCase() + f.slice(1)}`, )} ))} {t('chapterReading.backgroundColor')} {(['white', 'sepia'] as const).map((theme) => ( onBackgroundChange(theme)} style={({ pressed }) => [ readingSettingsStyles.bgOption, { backgroundColor: BACKGROUND_COLORS[theme] }, backgroundColor === theme && readingSettingsStyles.bgOptionActive, pressed && { opacity: 0.9 }, ]} > {t( `chapterReading.bg${theme === 'white' ? 'PureWhite' : 'Sepia'}`, )} ))} ); } const readingSettingsStyles = StyleSheet.create({ backdrop: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.4)', }, sheet: { backgroundColor: '#fff', borderTopLeftRadius: 16, borderTopRightRadius: 16, paddingTop: 8, paddingHorizontal: ScreenGutter, ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: -2 }, shadowOpacity: 0.06, shadowRadius: 12, }, android: { elevation: 12 }, }), }, handle: { alignSelf: 'center', width: 40, height: 4, borderRadius: 2, backgroundColor: 'rgba(0,0,0,0.2)', marginBottom: 16, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, }, title: { fontSize: 17, fontWeight: '700', color: READING_COLORS.onSurface, }, closeBtn: { padding: 8 }, label: { fontSize: 11, fontWeight: '600', letterSpacing: 0.5, color: READING_COLORS.onSurfaceVariant, marginBottom: 10, textTransform: 'uppercase', }, section: { marginBottom: 20 }, segmented: { flexDirection: 'row', backgroundColor: '#eeeef0', borderRadius: 10, padding: 4, }, segItem: { flex: 1, minHeight: 44, justifyContent: 'center', alignItems: 'center', borderRadius: 8, }, segItemActive: { backgroundColor: READING_COLORS.primary, }, segText: { fontSize: 14, fontWeight: '500', color: READING_COLORS.onSurfaceVariant, }, segTextActive: { fontWeight: '700', color: '#fff', }, bgRow: { flexDirection: 'row', gap: 12 }, bgOption: { flex: 1, minHeight: 64, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, borderRadius: 10, borderWidth: 2, borderColor: 'transparent', }, bgOptionActive: { borderColor: READING_COLORS.primary, }, bgSwatch: { width: 28, height: 28, borderRadius: 6, borderWidth: 1, borderColor: 'rgba(0,0,0,0.1)', }, bgLabel: { fontSize: 14, fontWeight: '500', color: READING_COLORS.onSurface, }, bgLabelActive: { fontWeight: '700' }, }); export default function ChapterScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const insets = useSafeAreaInsets(); const { t } = useTranslation('memoir'); const { data: chapter, isLoading } = useChapterDetail(id ?? ''); const deleteChapter = useDeleteChapter(); const [settingsVisible, setSettingsVisible] = useState(false); const [fontSize, setFontSize] = useState('default'); const [fontFamily, setFontFamily] = useState('serif'); const [backgroundColor, setBackgroundColor] = useState('white'); const bgColor = BACKGROUND_COLORS[backgroundColor]; if (isLoading) { return ( ); } if (!chapter) { return ( {t('chapterReading.chapterNotFound')} router.back()} style={({ pressed }) => ({ paddingHorizontal: 20, paddingVertical: 12, borderRadius: 12, backgroundColor: READING_COLORS.primary, opacity: pressed ? 0.8 : 1, })} accessibilityRole="button" accessibilityLabel={t('chapterReading.back')} > {t('chapterReading.back')} ); } const sections = chapter.sections ?? []; const coverImageUrl = chapter.cover_image?.url ?? null; const handleDeletePress = () => { Alert.alert( t('chapterReading.deleteChapter'), t('chapterReading.confirmDeleteMessage'), [ { text: t('chapterReading.cancel'), style: 'cancel' }, { text: t('chapterReading.deleteChapterAction'), style: 'destructive', onPress: () => { deleteChapter.mutate(chapter.id, { onSuccess: () => router.back(), }); }, }, ], ); }; return ( ({ padding: 8, borderRadius: 9999, opacity: pressed ? 0.7 : 1, })} accessibilityLabel={t('chapterReading.deleteChapter')} accessibilityRole="button" > setSettingsVisible(true)} style={({ pressed }) => ({ padding: 8, marginRight: -8, borderRadius: 9999, opacity: pressed ? 0.7 : 1, })} accessibilityLabel={t('chapterReading.settings')} accessibilityRole="button" > } /> {/* Scrollable content */} setSettingsVisible(false)} fontSize={fontSize} fontFamily={fontFamily} backgroundColor={backgroundColor} onFontSizeChange={setFontSize} onFontFamilyChange={setFontFamily} onBackgroundChange={setBackgroundColor} t={t as (key: string) => string} /> ); }