chore: vendor client into main repo

This commit is contained in:
吕新雨
2026-01-28 22:54:21 +08:00
parent 7d743e78ea
commit 6a598f0a98
79 changed files with 12952 additions and 1 deletions

View File

@@ -0,0 +1,50 @@
import { Stack } from 'expo-router';
import { Pressable, Text } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'expo-router';
export default function AppLayout() {
const router = useRouter();
const { t } = useTranslation();
return (
<Stack
screenOptions={{
headerTitleAlign: 'center',
}}
>
<Stack.Screen
name="home"
options={{
title: t('home.title'),
headerRight: () => (
<Pressable
onPress={() => router.push('/(app)/settings')}
hitSlop={10}
>
<Text>{t('home.settings')}</Text>
</Pressable>
),
headerLeft: () => (
<Pressable onPress={() => router.push('/(app)/favorites')} hitSlop={10}>
<Text>{t('home.favorites')}</Text>
</Pressable>
),
}}
/>
<Stack.Screen
name="favorites"
options={{
title: t('favorites.title'),
}}
/>
<Stack.Screen
name="settings"
options={{
title: t('settings.title'),
}}
/>
</Stack>
);
}

View File

@@ -0,0 +1,61 @@
import { useEffect, useMemo, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import { MOCK_CONTENT } from '@/src/constants/mockContent';
import { getFavorites } from '@/src/storage/appStorage';
export default function FavoritesScreen() {
const { t } = useTranslation();
const [ids, setIds] = useState<string[]>([]);
useEffect(() => {
let cancelled = false;
(async () => {
const list = await getFavorites();
if (!cancelled) setIds(list);
})();
return () => {
cancelled = true;
};
}, []);
const items = useMemo(() => {
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c]));
return ids.map((id) => map.get(id)).filter(Boolean) as { id: string; text: string }[];
}, [ids]);
return (
<View style={styles.container}>
{items.length === 0 ? (
<Text style={styles.empty}>{t('favorites.empty')}</Text>
) : (
<FlatList
data={items}
keyExtractor={(it) => it.id}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.text}>{item.text}</Text>
</View>
)}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
empty: { color: '#6B7280', fontSize: 16, textAlign: 'center', marginTop: 40 },
list: { gap: 12, paddingBottom: 24 },
row: {
borderRadius: 14,
padding: 16,
backgroundColor: '#F9FAFB',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#E5E7EB',
},
text: { color: '#111827', fontSize: 16, lineHeight: 22 },
});

76
client/app/(app)/home.tsx Normal file
View File

@@ -0,0 +1,76 @@
import { useMemo, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import { MOCK_CONTENT } from '@/src/constants/mockContent';
import { addFavorite, setReaction } from '@/src/storage/appStorage';
export default function HomeScreen() {
const { t } = useTranslation();
const [index, setIndex] = useState(0);
const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]);
async function onLike() {
await setReaction(item.id, 'like');
await addFavorite(item.id);
setIndex((i) => i + 1);
}
async function onDislike() {
await setReaction(item.id, 'dislike');
setIndex((i) => i + 1);
}
return (
<View style={styles.container}>
<View style={styles.card}>
<Text style={styles.text}>{item.text}</Text>
</View>
<View style={styles.actions}>
<Pressable style={[styles.button, styles.dislike]} onPress={onDislike}>
<Text style={styles.buttonText}>{t('home.dislike')}</Text>
</Pressable>
<Pressable style={[styles.button, styles.like]} onPress={onLike}>
<Text style={styles.buttonText}>{t('home.like')}</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
gap: 16,
},
card: {
borderRadius: 16,
padding: 20,
backgroundColor: '#F6F7FB',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#E5E7EB',
},
text: {
fontSize: 20,
lineHeight: 28,
color: '#111827',
},
actions: {
flexDirection: 'row',
gap: 12,
},
button: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: 'center',
},
like: { backgroundColor: '#16A34A' },
dislike: { backgroundColor: '#EF4444' },
buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

View File

@@ -0,0 +1,50 @@
import Constants from 'expo-constants';
import { StyleSheet, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
export default function SettingsScreen() {
const { t } = useTranslation();
const version =
Constants.expoConfig?.version ??
// 兜底:部分环境下 expoConfig 可能为空
'unknown';
return (
<View style={styles.container}>
<View style={styles.section}>
<Text style={styles.label}>{t('settings.version')}</Text>
<Text style={styles.value}>{version}</Text>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>{t('settings.widgetTitle')}</Text>
<Text style={styles.cardText}>{t('settings.widgetDesc')}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, gap: 16 },
section: {
borderRadius: 14,
padding: 16,
backgroundColor: '#FFFFFF',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#E5E7EB',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
label: { color: '#374151', fontSize: 16 },
value: { color: '#111827', fontSize: 16, fontWeight: '600' },
card: {
borderRadius: 16,
padding: 16,
backgroundColor: '#111827',
},
cardTitle: { color: 'white', fontSize: 16, fontWeight: '700', marginBottom: 8 },
cardText: { color: '#E5E7EB', fontSize: 14, lineHeight: 20 },
});