273 lines
7.5 KiB
TypeScript
273 lines
7.5 KiB
TypeScript
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
|
import { StyleSheet, View } from 'react-native';
|
|
import { Pressable } from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useNavigation } from 'expo-router';
|
|
import Animated, {
|
|
Easing,
|
|
runOnJS,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withSequence,
|
|
withTiming,
|
|
} from 'react-native-reanimated';
|
|
|
|
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
|
import {
|
|
addFavorite,
|
|
getThemeMode,
|
|
getUserProfile,
|
|
setReaction,
|
|
setThemeMode,
|
|
type ThemeMode,
|
|
} from '@/src/storage/appStorage';
|
|
|
|
import ProfileModal from '@/components/home/ProfileModal';
|
|
import ThemeModal from '@/components/home/ThemeModal';
|
|
|
|
import ThemeIcon from '@/assets/images/home/theme.svg';
|
|
import MyIcon from '@/assets/images/home/my.svg';
|
|
import LikeOutlineIcon from '@/assets/images/home/like.svg';
|
|
import LikeFilledIcon from '@/assets/images/home/like_filled.svg';
|
|
import HateIcon from '@/assets/images/home/hate.svg';
|
|
|
|
export default function HomeScreen() {
|
|
const { t } = useTranslation();
|
|
const navigation = useNavigation();
|
|
const [index, setIndex] = useState(0);
|
|
const [themeMode, setThemeModeState] = useState<ThemeMode>('scenery');
|
|
const [themeOpen, setThemeOpen] = useState(false);
|
|
const [profileOpen, setProfileOpen] = useState(false);
|
|
const [profileName, setProfileName] = useState<string | undefined>(undefined);
|
|
const [busy, setBusy] = useState(false);
|
|
const [likeFilled, setLikeFilled] = useState(false);
|
|
|
|
const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
const mode = await getThemeMode();
|
|
const profile = await getUserProfile();
|
|
if (cancelled) return;
|
|
setThemeModeState(mode);
|
|
setProfileName(profile.name);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2';
|
|
|
|
useLayoutEffect(() => {
|
|
navigation.setOptions({
|
|
headerShadowVisible: false,
|
|
headerStyle: { backgroundColor },
|
|
headerRight: () => (
|
|
<View style={styles.headerRight}>
|
|
<CircleIconButton
|
|
onPress={() => setThemeOpen(true)}
|
|
accessibilityLabel={t('home.theme')}
|
|
>
|
|
<ThemeIcon width={18} height={18} />
|
|
</CircleIconButton>
|
|
<CircleIconButton
|
|
onPress={() => setProfileOpen(true)}
|
|
accessibilityLabel={t('home.profile')}
|
|
>
|
|
<MyIcon width={18} height={18} />
|
|
</CircleIconButton>
|
|
</View>
|
|
),
|
|
});
|
|
}, [backgroundColor, navigation, t]);
|
|
|
|
const likeScale = useSharedValue(1);
|
|
const hateScale = useSharedValue(1);
|
|
|
|
const likeAnimatedStyle = useAnimatedStyle(() => ({
|
|
transform: [{ scale: likeScale.value }],
|
|
}));
|
|
|
|
const hateAnimatedStyle = useAnimatedStyle(() => ({
|
|
transform: [{ scale: hateScale.value }],
|
|
}));
|
|
|
|
function playLikeAnimationAndThen(next: () => void) {
|
|
setLikeFilled(true);
|
|
likeScale.value = withSequence(
|
|
withTiming(0.92, { duration: 90, easing: Easing.out(Easing.cubic) }),
|
|
withTiming(1.14, { duration: 120, easing: Easing.out(Easing.cubic) }),
|
|
withTiming(1, { duration: 140, easing: Easing.out(Easing.cubic) }, (finished) => {
|
|
if (finished) runOnJS(next)();
|
|
})
|
|
);
|
|
}
|
|
|
|
function playHateAnimationAndThen(next: () => void) {
|
|
hateScale.value = withSequence(
|
|
withTiming(0.92, { duration: 90, easing: Easing.out(Easing.cubic) }),
|
|
withTiming(1.06, { duration: 110, easing: Easing.out(Easing.cubic) }),
|
|
withTiming(1, { duration: 120, easing: Easing.out(Easing.cubic) }, (finished) => {
|
|
if (finished) runOnJS(next)();
|
|
})
|
|
);
|
|
}
|
|
|
|
function onPressLike() {
|
|
if (busy) return;
|
|
setBusy(true);
|
|
playLikeAnimationAndThen(async () => {
|
|
await setReaction(item.id, 'like');
|
|
await addFavorite(item.id);
|
|
setIndex((i) => i + 1);
|
|
setTimeout(() => setLikeFilled(false), 220);
|
|
setBusy(false);
|
|
});
|
|
}
|
|
|
|
function onPressHate() {
|
|
if (busy) return;
|
|
setBusy(true);
|
|
playHateAnimationAndThen(async () => {
|
|
await setReaction(item.id, 'dislike');
|
|
setIndex((i) => i + 1);
|
|
setBusy(false);
|
|
});
|
|
}
|
|
|
|
async function onSelectTheme(next: ThemeMode) {
|
|
setThemeModeState(next);
|
|
await setThemeMode(next);
|
|
setThemeOpen(false);
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor }]}>
|
|
<View style={styles.card}>
|
|
<Animated.Text style={styles.text}>{item.text}</Animated.Text>
|
|
</View>
|
|
|
|
<View style={styles.actions}>
|
|
<Animated.View style={[styles.reactionButton, hateAnimatedStyle]}>
|
|
<Pressable
|
|
onPress={onPressHate}
|
|
onPressIn={() => (hateScale.value = withTiming(0.92, { duration: 80 }))}
|
|
onPressOut={() => (hateScale.value = withTiming(1, { duration: 120 }))}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={t('home.dislike')}
|
|
hitSlop={10}
|
|
style={styles.reactionInner}
|
|
>
|
|
<HateIcon width={30} height={30} />
|
|
</Pressable>
|
|
</Animated.View>
|
|
|
|
<Animated.View style={[styles.reactionButton, likeAnimatedStyle]}>
|
|
<Pressable
|
|
onPress={onPressLike}
|
|
onPressIn={() => (likeScale.value = withTiming(0.92, { duration: 80 }))}
|
|
onPressOut={() => (likeScale.value = withTiming(1, { duration: 120 }))}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={t('home.like')}
|
|
hitSlop={10}
|
|
style={styles.reactionInner}
|
|
>
|
|
{likeFilled ? (
|
|
<LikeFilledIcon width={30} height={30} />
|
|
) : (
|
|
<LikeOutlineIcon width={30} height={30} />
|
|
)}
|
|
</Pressable>
|
|
</Animated.View>
|
|
</View>
|
|
|
|
<ThemeModal visible={themeOpen} mode={themeMode} onSelect={onSelectTheme} onClose={() => setThemeOpen(false)} />
|
|
<ProfileModal
|
|
visible={profileOpen}
|
|
name={profileName}
|
|
onClose={() => setProfileOpen(false)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function CircleIconButton({
|
|
onPress,
|
|
accessibilityLabel,
|
|
children,
|
|
}: {
|
|
onPress: () => void;
|
|
accessibilityLabel: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
hitSlop={10}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={accessibilityLabel}
|
|
style={styles.circleBtn}
|
|
>
|
|
{children}
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
padding: 20,
|
|
justifyContent: 'center',
|
|
gap: 16,
|
|
},
|
|
headerRight: {
|
|
flexDirection: 'row',
|
|
gap: 10,
|
|
paddingRight: 10,
|
|
},
|
|
circleBtn: {
|
|
width: 34,
|
|
height: 34,
|
|
borderRadius: 17,
|
|
backgroundColor: 'rgba(255,255,255,0.75)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
card: {
|
|
borderRadius: 16,
|
|
padding: 20,
|
|
backgroundColor: 'rgba(255,255,255,0.65)',
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: '#E5E7EB',
|
|
},
|
|
text: {
|
|
fontSize: 20,
|
|
lineHeight: 28,
|
|
color: '#5E2A28',
|
|
fontWeight: '700',
|
|
},
|
|
actions: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
justifyContent: 'center',
|
|
},
|
|
reactionButton: {
|
|
width: 58,
|
|
height: 58,
|
|
borderRadius: 29,
|
|
backgroundColor: 'rgba(255,255,255,0.75)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
reactionInner: {
|
|
width: 58,
|
|
height: 58,
|
|
borderRadius: 29,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
});
|
|
|