Skip to content

Commit c85ed1f

Browse files
committed
privacy policy and error fix
1 parent 53eacd3 commit c85ed1f

13 files changed

Lines changed: 487 additions & 114 deletions

File tree

app/(tabs)/index.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useDevConnectionConfig } from '@/hooks/useDevConnectionConfig';
88
import { hasUserStoredConfig } from '@/services/DevConnectionStorage';
99
import { sessionStore } from '@/utils/sessionStore';
1010
import { useColorScheme } from '@/hooks/use-color-scheme';
11+
import { changeLanguage, SUPPORTED_LANGUAGES } from '@/i18n';
1112

1213
type ConnectionStatus = 'online' | 'offline';
1314

@@ -58,7 +59,7 @@ export default function HomeScreen() {
5859
const colorScheme = useColorScheme();
5960
const isDark = colorScheme === 'dark';
6061
const theme = isDark ? DARK : LIGHT;
61-
const { t } = useTranslation();
62+
const { t, i18n } = useTranslation();
6263

6364
const isFocused = useIsFocused();
6465
const [isConnected, setIsConnected] = useState(sessionStore.isConnected);
@@ -111,6 +112,33 @@ export default function HomeScreen() {
111112
</View>
112113
</View>
113114

115+
{/* 语言选择 */}
116+
<View style={styles.section}>
117+
<Text style={[styles.sectionTitle, { color: theme.sectionTitle }]}>🌐 {t('settings.sections.language')}</Text>
118+
<View style={styles.langRow}>
119+
{SUPPORTED_LANGUAGES.map((lang) => (
120+
<TouchableOpacity
121+
key={lang.code}
122+
style={[
123+
styles.langButton,
124+
{ backgroundColor: theme.actionBtn, borderColor: theme.actionBorder },
125+
i18n.language === lang.code && { backgroundColor: theme.titleColor, borderColor: theme.titleColor },
126+
]}
127+
onPress={() => changeLanguage(lang.code)}
128+
activeOpacity={0.8}
129+
>
130+
<Text style={[
131+
styles.langText,
132+
{ color: theme.textPrimary },
133+
i18n.language === lang.code && { color: '#fff', fontWeight: 'bold' },
134+
]}>
135+
{lang.name}
136+
</Text>
137+
</TouchableOpacity>
138+
))}
139+
</View>
140+
</View>
141+
114142
{/* 服务器配置 */}
115143
<View style={styles.section}>
116144
<Text style={[styles.sectionTitle, { color: theme.sectionTitle }]}>{t('home.serverConnection')}</Text>
@@ -211,6 +239,23 @@ const styles = StyleSheet.create({
211239
fontSize: 14,
212240
fontWeight: '600',
213241
},
242+
langRow: {
243+
flexDirection: 'row',
244+
flexWrap: 'wrap',
245+
gap: 8,
246+
},
247+
langButton: {
248+
width: '31%',
249+
paddingHorizontal: 8,
250+
paddingVertical: 8,
251+
borderRadius: 10,
252+
borderWidth: 1,
253+
alignItems: 'center',
254+
},
255+
langText: {
256+
fontSize: 13,
257+
textAlign: 'center',
258+
},
214259
configCard: {
215260
borderRadius: 16,
216261
padding: 16,

app/_layout.tsx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,59 @@
1+
import AsyncStorage from '@react-native-async-storage/async-storage';
12
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
23
import { Stack } from 'expo-router';
34
import { StatusBar } from 'expo-status-bar';
45
import 'react-native-reanimated';
56
import { I18nextProvider } from 'react-i18next';
67
import { GestureHandlerRootView } from 'react-native-gesture-handler';
7-
import { useEffect } from 'react';
8+
import { useEffect, useState } from 'react';
89

910
import { initI18n, i18n } from '@/i18n';
1011
import { useColorScheme } from '@/hooks/use-color-scheme';
12+
import PrivacyConsentModal, { PRIVACY_AGREED_KEY } from '@/components/PrivacyConsentModal';
1113

1214
export const unstable_settings = {
1315
anchor: '(tabs)',
1416
};
1517

18+
type PrivacyStatus = 'checking' | 'pending' | 'agreed';
19+
1620
export default function RootLayout() {
1721
const colorScheme = useColorScheme();
22+
const [privacyStatus, setPrivacyStatus] = useState<PrivacyStatus>('checking');
1823

19-
// Initialize i18n on mount
2024
useEffect(() => {
21-
initI18n().catch(err => console.error('Failed to initialize i18n:', err));
25+
const init = async () => {
26+
await initI18n().catch(err => console.error('Failed to initialize i18n:', err));
27+
const agreed = await AsyncStorage.getItem(PRIVACY_AGREED_KEY);
28+
setPrivacyStatus(agreed === 'true' ? 'agreed' : 'pending');
29+
};
30+
init();
2231
}, []);
2332

33+
// Keep splash screen visible while checking
34+
if (privacyStatus === 'checking') {
35+
return null;
36+
}
37+
2438
return (
2539
<GestureHandlerRootView style={{ flex: 1 }}>
2640
<I18nextProvider i18n={i18n}>
27-
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
28-
<Stack>
29-
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
30-
{/* <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} /> */}
31-
<Stack.Screen name="audio-test" options={{ title: '音频测试' }} />
32-
<Stack.Screen name="audio-debug" options={{ title: '🎤 音频诊断' }} />
33-
<Stack.Screen name="qr-scanner" options={{ title: '扫码(Dev)' }} />
34-
<Stack.Screen name="request-lab" options={{ title: 'Request/组件实验室' }} />
35-
<Stack.Screen name="webapp" options={{ title: 'WebApp(对齐 frontend/src/web/App.tsx)' }} />
36-
</Stack>
37-
<StatusBar style="auto" />
38-
</ThemeProvider>
41+
{privacyStatus === 'pending' ? (
42+
<PrivacyConsentModal onAgree={() => setPrivacyStatus('agreed')} />
43+
) : (
44+
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
45+
<Stack>
46+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
47+
{/* <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} /> */}
48+
<Stack.Screen name="audio-test" options={{ title: '音频测试' }} />
49+
<Stack.Screen name="audio-debug" options={{ title: '🎤 音频诊断' }} />
50+
<Stack.Screen name="qr-scanner" options={{ title: '扫码(Dev)' }} />
51+
<Stack.Screen name="request-lab" options={{ title: 'Request/组件实验室' }} />
52+
<Stack.Screen name="webapp" options={{ title: 'WebApp(对齐 frontend/src/web/App.tsx)' }} />
53+
</Stack>
54+
<StatusBar style="auto" />
55+
</ThemeProvider>
56+
)}
3957
</I18nextProvider>
4058
</GestureHandlerRootView>
4159
);

app/settings.tsx

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import QRCode from 'react-native-qrcode-svg';
2424
import { useDevConnectionConfig } from '@/hooks/useDevConnectionConfig';
2525
import { createConfigApiClient, type CoreConfig, type ApiProvider } from '@/services/api/config';
2626
import { useColorScheme } from '@/hooks/use-color-scheme';
27-
import { changeLanguage, SUPPORTED_LANGUAGES } from '@/i18n';
28-
2927
// Icons as text
3028
const Icons = {
3129
back: '←',
@@ -86,9 +84,10 @@ const DARK = {
8684

8785
export default function SettingsScreen() {
8886
const router = useRouter();
89-
const { config } = useDevConnectionConfig();
87+
const { config, isLoaded } = useDevConnectionConfig();
9088
const apiBase = `http://${config.host}:${config.port}`;
91-
const { t, i18n } = useTranslation();
89+
const p2pToken = config.p2p?.token;
90+
const { t } = useTranslation();
9291

9392
const colorScheme = useColorScheme();
9493
const isDark = colorScheme === 'dark';
@@ -124,7 +123,7 @@ export default function SettingsScreen() {
124123
setLoading(true);
125124
setError(null);
126125

127-
const client = createConfigApiClient(apiBase);
126+
const client = createConfigApiClient(apiBase, p2pToken);
128127
const [configData, providersData] = await Promise.all([
129128
client.getCoreConfig(),
130129
client.getApiProviders(),
@@ -142,9 +141,10 @@ export default function SettingsScreen() {
142141
}, [apiBase]);
143142

144143
useEffect(() => {
144+
if (!isLoaded) return;
145145
loadConfig();
146146
loadP2PConfig();
147-
}, [loadConfig, loadP2PConfig]);
147+
}, [isLoaded, loadConfig, loadP2PConfig]);
148148

149149
// Save config
150150
const handleSave = useCallback(async () => {
@@ -153,7 +153,7 @@ export default function SettingsScreen() {
153153
setError(null);
154154
setSuccess(null);
155155

156-
const client = createConfigApiClient(apiBase);
156+
const client = createConfigApiClient(apiBase, p2pToken);
157157
const result = await client.updateCoreConfig(coreConfig);
158158

159159
if (result.success) {
@@ -221,45 +221,6 @@ export default function SettingsScreen() {
221221
style={styles.content}
222222
refreshControl={<RefreshControl refreshing={loading} onRefresh={loadConfig} />}
223223
>
224-
{/* Language Section */}
225-
<View style={styles.section}>
226-
<View style={styles.sectionHeader}>
227-
<Text style={styles.sectionIcon}>🌐</Text>
228-
<Text style={[styles.sectionTitle, { color: theme.sectionTitle }]}>{t('settings.sections.language')}</Text>
229-
</View>
230-
<View style={[styles.card, { backgroundColor: theme.card }]}>
231-
<View style={styles.field}>
232-
<Text style={[styles.label, { color: theme.textLabel }]}>{t('settings.language.select')}</Text>
233-
<View style={styles.pickerContainer}>
234-
{SUPPORTED_LANGUAGES.map((lang) => (
235-
<TouchableOpacity
236-
key={lang.code}
237-
style={[
238-
styles.pickerOption,
239-
{ backgroundColor: theme.pickerOptionBg, borderColor: theme.pickerOptionBorder },
240-
i18n.language === lang.code && { backgroundColor: theme.accent, borderColor: theme.accent },
241-
]}
242-
onPress={() => changeLanguage(lang.code)}
243-
>
244-
<Text
245-
style={[
246-
styles.pickerOptionText,
247-
{ color: theme.textPrimary },
248-
i18n.language === lang.code && { color: theme.accentText, fontWeight: 'bold' },
249-
]}
250-
>
251-
{t(`settings.language.languages.${lang.code}`)}
252-
</Text>
253-
{i18n.language === lang.code && (
254-
<Text style={styles.checkmark}>{Icons.check}</Text>
255-
)}
256-
</TouchableOpacity>
257-
))}
258-
</View>
259-
</View>
260-
</View>
261-
</View>
262-
263224
{/* Core API Section */}
264225
<View style={styles.section}>
265226
<View style={styles.sectionHeader}>
@@ -428,7 +389,27 @@ export default function SettingsScreen() {
428389
<Text style={styles.sectionIcon}>📱</Text>
429390
<Text style={[styles.sectionTitle, { color: theme.sectionTitle }]}>{t('settings.sections.p2p')}</Text>
430391
</View>
431-
<View style={[styles.card, { backgroundColor: theme.card, alignItems: 'center' }]}>
392+
<View style={[styles.card, { backgroundColor: theme.card }]}>
393+
{/* 说明文字 */}
394+
<Text style={[styles.qrDesc, { color: theme.textLabel }]}>
395+
{t('settings.p2p.desc')}
396+
</Text>
397+
398+
{/* 步骤 */}
399+
<View style={[styles.qrSteps, { borderColor: theme.inputBorder }]}>
400+
{(['step1', 'step2', 'step3'] as const).map((key, i) => (
401+
<View key={key} style={styles.qrStepRow}>
402+
<View style={[styles.qrStepNum, { backgroundColor: theme.accent }]}>
403+
<Text style={styles.qrStepNumText}>{i + 1}</Text>
404+
</View>
405+
<Text style={[styles.qrStepText, { color: theme.textLabel }]}>
406+
{t(`settings.p2p.${key}`)}
407+
</Text>
408+
</View>
409+
))}
410+
</View>
411+
412+
{/* 二维码 */}
432413
<View style={styles.qrContainer}>
433414
<QRCode
434415
value={JSON.stringify(p2pConfig)}
@@ -437,13 +418,19 @@ export default function SettingsScreen() {
437418
backgroundColor={isDark ? '#1a1a2e' : '#f0f8ff'}
438419
/>
439420
</View>
440-
<Text style={[styles.qrHint, { color: theme.textMuted }]}>
441-
Scan with N.E.K.O. RN App to connect
442-
</Text>
443-
<View style={styles.p2pInfo}>
444-
<Text style={[styles.p2pInfoText, { color: theme.textMuted }]}>
445-
UDP: {p2pConfig.port} | TCP: {p2pConfig.tcp_port}
446-
</Text>
421+
422+
{/* 连接参数 */}
423+
<View style={[styles.qrInfoBlock, { borderColor: theme.inputBorder }]}>
424+
{[
425+
{ label: t('settings.p2p.lanIp'), value: p2pConfig.lan_ip || '--' },
426+
{ label: t('settings.p2p.port'), value: String(p2pConfig.port || '--') },
427+
{ label: t('settings.p2p.token'), value: t('settings.p2p.tokenHidden') },
428+
].map(({ label, value }) => (
429+
<View key={label} style={[styles.qrInfoRow, { borderBottomColor: theme.inputBorder }]}>
430+
<Text style={[styles.qrInfoLabel, { color: theme.textMuted }]}>{label}</Text>
431+
<Text style={[styles.qrInfoValue, { color: theme.textLabel }]}>{value}</Text>
432+
</View>
433+
))}
447434
</View>
448435
</View>
449436
</View>

0 commit comments

Comments
 (0)