APP-PUSH和纯色小组件
@@ -9,9 +9,9 @@
|
|||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/images/splash-icon.png",
|
"image": "./assets/images/splashScreen.png",
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#EAD2BA"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false,
|
||||||
|
"package": "com.damer.mindfulness"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '@/src/storage/appStorage';
|
} from '@/src/storage/appStorage';
|
||||||
|
|
||||||
import { fetchRecoFeed } from '@/src/services/recoApi';
|
import { fetchRecoFeed } from '@/src/services/recoApi';
|
||||||
|
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
|
||||||
|
|
||||||
import ProfileModal from '@/components/home/ProfileModal';
|
import ProfileModal from '@/components/home/ProfileModal';
|
||||||
import ThemeModal from '@/components/home/ThemeModal';
|
import ThemeModal from '@/components/home/ThemeModal';
|
||||||
@@ -78,7 +79,7 @@ type FeedItem = { content_id: string; text: string };
|
|||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const isEnglish = i18n.language?.startsWith('en');
|
const isEnglish = i18n.language?.startsWith('en');
|
||||||
const recoLang: 'en' | 'tc' = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
const recoLang: 'en' | 'tc' = toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const [themeMode, setThemeModeState] = useState<ThemeMode>('scenery');
|
const [themeMode, setThemeModeState] = useState<ThemeMode>('scenery');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SelectionStep } from '@/components/onboarding/SelectionStep';
|
|||||||
import { ReminderStep } from '@/components/onboarding/ReminderStep';
|
import { ReminderStep } from '@/components/onboarding/ReminderStep';
|
||||||
import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnaireAnswers } from '@/src/features/userProfileScoring';
|
import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnaireAnswers } from '@/src/features/userProfileScoring';
|
||||||
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
|
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
|
||||||
|
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
|
||||||
import { fetchRecoFeed } from '@/src/services/recoApi';
|
import { fetchRecoFeed } from '@/src/services/recoApi';
|
||||||
import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi';
|
import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi';
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +31,7 @@ type Step =
|
|||||||
const STEPS: Step[] = [
|
const STEPS: Step[] = [
|
||||||
{ id: 'name', type: 'name' },
|
{ id: 'name', type: 'name' },
|
||||||
{ id: 'status', type: 'selection', optionIds: ['pregnant', 'has_kids', 'no_fill'] },
|
{ id: 'status', type: 'selection', optionIds: ['pregnant', 'has_kids', 'no_fill'] },
|
||||||
{ id: 'emotion', type: 'selection', optionIds: ['happy', 'calm', 'stressed', 'low'] },
|
{ id: 'emotion', type: 'selection', optionIds: ['happy', 'calm', 'okay', 'tired', 'stressed', 'low'] },
|
||||||
{ id: 'influence', type: 'selection', optionIds: ['family', 'work', 'relationship', 'friends', 'health'] },
|
{ id: 'influence', type: 'selection', optionIds: ['family', 'work', 'relationship', 'friends', 'health'] },
|
||||||
{ id: 'support', type: 'selection', optionIds: ['emotional', 'parenting', 'self_worth', 'anxiety', 'balance'] },
|
{ id: 'support', type: 'selection', optionIds: ['emotional', 'parenting', 'self_worth', 'anxiety', 'balance'] },
|
||||||
{ id: 'reminder', type: 'reminder' },
|
{ id: 'reminder', type: 'reminder' },
|
||||||
@@ -74,7 +75,7 @@ export default function OnboardingScreen() {
|
|||||||
|
|
||||||
// Onboarding 结束后预拉取一次 Feed 文案(失败不阻塞进入首页)
|
// Onboarding 结束后预拉取一次 Feed 文案(失败不阻塞进入首页)
|
||||||
try {
|
try {
|
||||||
const lang = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
const { items, meta } = await fetchRecoFeed({
|
const { items, meta } = await fetchRecoFeed({
|
||||||
k: 30,
|
k: 30,
|
||||||
user_profile: {
|
user_profile: {
|
||||||
|
|||||||
@@ -88,16 +88,22 @@ export default function SplashScreen() {
|
|||||||
{/* 文案内容 */}
|
{/* 文案内容 */}
|
||||||
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
|
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
|
||||||
<Text style={styles.titleText}>
|
<Text style={styles.titleText}>
|
||||||
You Are Perfect.{"\n"}
|
{t('consent.title')}
|
||||||
Everything{"\n"}
|
{'\n'}
|
||||||
Will Be Better.
|
{t('consent.subtitle')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
|
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
|
||||||
{showConsent && (
|
{showConsent && (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity onPress={handleAgree} activeOpacity={0.8} style={styles.buttonWrapper}>
|
<TouchableOpacity
|
||||||
|
onPress={handleAgree}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
style={styles.buttonWrapper}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('consent.agree')}
|
||||||
|
>
|
||||||
<WelcomeBtn width={87} height={57} />
|
<WelcomeBtn width={87} height={57} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -116,6 +122,8 @@ export default function SplashScreen() {
|
|||||||
<Text style={styles.linkText}>{t('consent.terms')}</Text>
|
<Text style={styles.linkText}>{t('consent.terms')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.noticeText}>{t('consent.notice')}</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -168,6 +176,14 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
noticeText: {
|
||||||
|
marginTop: 10,
|
||||||
|
paddingHorizontal: 28,
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'rgba(119, 47, 0, 0.45)',
|
||||||
|
},
|
||||||
linkText: {
|
linkText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: 'rgba(119, 47, 0, 0.5)', // 使用半透明的文字颜色
|
color: 'rgba(119, 47, 0, 0.5)', // 使用半透明的文字颜色
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useFonts } from 'expo-font';
|
|||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
import { AppState } from 'react-native';
|
import { Animated, AppState, Image, StyleSheet, View } from 'react-native';
|
||||||
|
|
||||||
import { useColorScheme } from '@/components/useColorScheme';
|
import { useColorScheme } from '@/components/useColorScheme';
|
||||||
import { initI18n } from '@/src/i18n';
|
import { initI18n } from '@/src/i18n';
|
||||||
@@ -41,6 +41,10 @@ export default function RootLayout() {
|
|||||||
...FontAwesome.font,
|
...FontAwesome.font,
|
||||||
});
|
});
|
||||||
const [i18nReady, setI18nReady] = useState(false);
|
const [i18nReady, setI18nReady] = useState(false);
|
||||||
|
const [appReady, setAppReady] = useState(false);
|
||||||
|
const [splashOverlayVisible, setSplashOverlayVisible] = useState(true);
|
||||||
|
const splashOpacity = useRef(new Animated.Value(1)).current;
|
||||||
|
const hasHiddenNativeSplashRef = useRef(false);
|
||||||
|
|
||||||
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,17 +72,52 @@ export default function RootLayout() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 等字体与 i18n 都准备好后再隐藏启动页,避免文案闪烁
|
// 字体与 i18n 都准备好后,允许渲染 App(原生 splash 的隐藏交给 onLayout,避免“硬切/闪白”)
|
||||||
if (loaded && i18nReady) {
|
if (loaded && i18nReady) setAppReady(true);
|
||||||
SplashScreen.hideAsync();
|
|
||||||
}
|
|
||||||
}, [loaded, i18nReady]);
|
}, [loaded, i18nReady]);
|
||||||
|
|
||||||
|
const onLayoutRootView = useCallback(() => {
|
||||||
|
if (!appReady) return;
|
||||||
|
if (hasHiddenNativeSplashRef.current) return;
|
||||||
|
hasHiddenNativeSplashRef.current = true;
|
||||||
|
|
||||||
|
// 先隐藏原生 splash,再把同款覆盖层淡出,视觉上实现平滑过渡
|
||||||
|
void SplashScreen.hideAsync().finally(() => {
|
||||||
|
Animated.timing(splashOpacity, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 380,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start(({ finished }) => {
|
||||||
|
if (finished) setSplashOverlayVisible(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [appReady, splashOpacity]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (!appReady) return null;
|
||||||
|
return <RootLayoutNav />;
|
||||||
|
}, [appReady]);
|
||||||
|
|
||||||
if (!loaded || !i18nReady) {
|
if (!loaded || !i18nReady) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <RootLayoutNav />;
|
return (
|
||||||
|
<View style={styles.root} onLayout={onLayoutRootView}>
|
||||||
|
{content}
|
||||||
|
{splashOverlayVisible && (
|
||||||
|
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { opacity: splashOpacity }]}>
|
||||||
|
<View style={styles.splashOverlay}>
|
||||||
|
<Image
|
||||||
|
source={require('../assets/images/splashScreen.png')}
|
||||||
|
style={styles.splashImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RootLayoutNav() {
|
function RootLayoutNav() {
|
||||||
@@ -118,3 +157,20 @@ function RootLayoutNav() {
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
splashOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
// 与 app.json 的 expo.splash.backgroundColor 保持一致
|
||||||
|
backgroundColor: '#EAD2BA',
|
||||||
|
},
|
||||||
|
splashImage: {
|
||||||
|
width: '80%',
|
||||||
|
height: '80%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
BIN
client/assets/images/splashScreen.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
@@ -162,23 +162,13 @@ export default function ProfileModal({ visible, name: propName, onClose }: Props
|
|||||||
const duration = 220;
|
const duration = 220;
|
||||||
const easing = Easing.out(Easing.cubic);
|
const easing = Easing.out(Easing.cubic);
|
||||||
|
|
||||||
// 进入二级页:从右侧滑入;返回:从左侧滑入
|
// 需求:去掉左右滑动的切页动效,改为纯淡入淡出
|
||||||
const entering =
|
const entering = FadeIn.duration(duration).easing(easing);
|
||||||
navDirection === 'forward'
|
const exiting = FadeOut.duration(duration).easing(easing);
|
||||||
? SlideInRight.duration(duration).easing(easing)
|
|
||||||
: SlideInLeft.duration(duration).easing(easing);
|
|
||||||
|
|
||||||
// 离开:进入二级页时旧页面向左滑出;返回时旧页面向右滑出
|
|
||||||
const exiting =
|
|
||||||
navDirection === 'forward'
|
|
||||||
? SlideOutLeft.duration(duration).easing(easing)
|
|
||||||
: SlideOutRight.duration(duration).easing(easing);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entering,
|
entering,
|
||||||
exiting,
|
exiting,
|
||||||
fadeIn: FadeIn.duration(duration).easing(easing),
|
|
||||||
fadeOut: FadeOut.duration(duration).easing(easing),
|
|
||||||
};
|
};
|
||||||
}, [navDirection]);
|
}, [navDirection]);
|
||||||
|
|
||||||
@@ -196,11 +186,6 @@ export default function ProfileModal({ visible, name: propName, onClose }: Props
|
|||||||
exiting={transition.exiting}
|
exiting={transition.exiting}
|
||||||
style={!isRoot ? { flex: 1 } : undefined}
|
style={!isRoot ? { flex: 1 } : undefined}
|
||||||
>
|
>
|
||||||
<Animated.View
|
|
||||||
entering={transition.fadeIn}
|
|
||||||
exiting={transition.fadeOut}
|
|
||||||
style={!isRoot ? { flex: 1 } : undefined}
|
|
||||||
>
|
|
||||||
{page === 'root' ? (
|
{page === 'root' ? (
|
||||||
<RootPage
|
<RootPage
|
||||||
name={currentName}
|
name={currentName}
|
||||||
@@ -222,7 +207,6 @@ export default function ProfileModal({ visible, name: propName, onClose }: Props
|
|||||||
) : (
|
) : (
|
||||||
<WidgetPage onOpenHowTo={() => go('widgetHowTo', 'forward')} />
|
<WidgetPage onOpenHowTo={() => go('widgetHowTo', 'forward')} />
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</SheetModal>
|
</SheetModal>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { View, StyleSheet, TextInput, Platform, Animated, TouchableOpacity, Dimensions, Text } from 'react-native';
|
import { View, StyleSheet, TextInput, Platform, Animated, TouchableOpacity, Dimensions, Text } from 'react-native';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||||
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
|
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
|
||||||
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
|
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
|
||||||
@@ -14,6 +15,7 @@ interface NameInputStepProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProps) {
|
export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const blinkAnim = useRef(new Animated.Value(1)).current;
|
const blinkAnim = useRef(new Animated.Value(1)).current;
|
||||||
const hasInput = value.trim().length > 0;
|
const hasInput = value.trim().length > 0;
|
||||||
@@ -46,7 +48,7 @@ export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProp
|
|||||||
(!isFocused && !hasInput) && { color: OnboardingColors.textSecondary }
|
(!isFocused && !hasInput) && { color: OnboardingColors.textSecondary }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{hasInput ? value : (isFocused ? "" : "Mama")}
|
{hasInput ? value : isFocused ? '' : t('onboardingSurvey.steps.name.placeholder')}
|
||||||
</Text>
|
</Text>
|
||||||
{isFocused && (
|
{isFocused && (
|
||||||
<Animated.View style={[styles.cursorWrapper, { opacity: blinkAnim, marginLeft: 2 }]}>
|
<Animated.View style={[styles.cursorWrapper, { opacity: blinkAnim, marginLeft: 2 }]}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native';
|
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||||
|
|
||||||
interface OnboardingLayoutProps {
|
interface OnboardingLayoutProps {
|
||||||
@@ -21,6 +22,7 @@ export function OnboardingLayout({
|
|||||||
onBack,
|
onBack,
|
||||||
showBackButton = false
|
showBackButton = false
|
||||||
}: OnboardingLayoutProps) {
|
}: OnboardingLayoutProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" />
|
<StatusBar barStyle="dark-content" />
|
||||||
@@ -39,7 +41,7 @@ export function OnboardingLayout({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity onPress={onSkip} style={styles.skipButton}>
|
<TouchableOpacity onPress={onSkip} style={styles.skipButton}>
|
||||||
<Text style={styles.skipText}>skip</Text>
|
<Text style={styles.skipText}>{t('onboarding.skipAll')}</Text>
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/icon/skip_icon.png')}
|
source={require('@/assets/images/icon/skip_icon.png')}
|
||||||
style={styles.skipIcon}
|
style={styles.skipIcon}
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ PODS:
|
|||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoCrypto (15.0.8):
|
- ExpoCrypto (15.0.8):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- ExpoDevice (8.0.10):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoFileSystem (19.0.21):
|
- ExpoFileSystem (19.0.21):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoFont (14.0.11):
|
- ExpoFont (14.0.11):
|
||||||
@@ -2250,6 +2252,7 @@ DEPENDENCIES:
|
|||||||
- "expo-dev-menu-interface (from `../node_modules/.pnpm/expo-dev-menu-interface@2.0.0_expo@54.0.32/node_modules/expo-dev-menu-interface/ios`)"
|
- "expo-dev-menu-interface (from `../node_modules/.pnpm/expo-dev-menu-interface@2.0.0_expo@54.0.32/node_modules/expo-dev-menu-interface/ios`)"
|
||||||
- "ExpoAsset (from `../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios`)"
|
- "ExpoAsset (from `../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios`)"
|
||||||
- "ExpoCrypto (from `../node_modules/.pnpm/expo-crypto@15.0.8_expo@54.0.32/node_modules/expo-crypto/ios`)"
|
- "ExpoCrypto (from `../node_modules/.pnpm/expo-crypto@15.0.8_expo@54.0.32/node_modules/expo-crypto/ios`)"
|
||||||
|
- "ExpoDevice (from `../node_modules/.pnpm/expo-device@8.0.10_expo@54.0.32/node_modules/expo-device/ios`)"
|
||||||
- "ExpoFileSystem (from `../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios`)"
|
- "ExpoFileSystem (from `../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios`)"
|
||||||
- "ExpoFont (from `../node_modules/.pnpm/expo-font@14.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-font/ios`)"
|
- "ExpoFont (from `../node_modules/.pnpm/expo-font@14.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-font/ios`)"
|
||||||
- "ExpoHead (from `../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.13_expo_mxedi6ntnfsoyp6zijog4pvdsy/node_modules/expo-router/ios`)"
|
- "ExpoHead (from `../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.13_expo_mxedi6ntnfsoyp6zijog4pvdsy/node_modules/expo-router/ios`)"
|
||||||
@@ -2362,6 +2365,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios"
|
:path: "../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios"
|
||||||
ExpoCrypto:
|
ExpoCrypto:
|
||||||
:path: "../node_modules/.pnpm/expo-crypto@15.0.8_expo@54.0.32/node_modules/expo-crypto/ios"
|
:path: "../node_modules/.pnpm/expo-crypto@15.0.8_expo@54.0.32/node_modules/expo-crypto/ios"
|
||||||
|
ExpoDevice:
|
||||||
|
:path: "../node_modules/.pnpm/expo-device@8.0.10_expo@54.0.32/node_modules/expo-device/ios"
|
||||||
ExpoFileSystem:
|
ExpoFileSystem:
|
||||||
:path: "../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios"
|
:path: "../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios"
|
||||||
ExpoFont:
|
ExpoFont:
|
||||||
@@ -2547,6 +2552,7 @@ SPEC CHECKSUMS:
|
|||||||
expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533
|
expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533
|
||||||
ExpoAsset: d999f3bbd998a750f3b74cb913229848901b926b
|
ExpoAsset: d999f3bbd998a750f3b74cb913229848901b926b
|
||||||
ExpoCrypto: 4d23a9ff67c25e2ed23ca792d81e58817a7ea1b9
|
ExpoCrypto: 4d23a9ff67c25e2ed23ca792d81e58817a7ea1b9
|
||||||
|
ExpoDevice: 0773c782b055558ca9b40b74aa4a8133a66cd0d2
|
||||||
ExpoFileSystem: aefcd337b94b874f88752ebefc52813b84992fad
|
ExpoFileSystem: aefcd337b94b874f88752ebefc52813b84992fad
|
||||||
ExpoFont: c625dbd97ed57e9089b172b2a7bb99003d074664
|
ExpoFont: c625dbd97ed57e9089b172b2a7bb99003d074664
|
||||||
ExpoHead: b691a2ed7ab02ed820b6c6468941832d34969c29
|
ExpoHead: b691a2ed7ab02ed820b6c6468941832d34969c29
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */; };
|
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
|
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
|
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||||
|
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
|
||||||
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
|
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
|
||||||
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
|
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
|
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */ = {
|
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
EmotionWidget.swift,
|
EmotionWidget.swift,
|
||||||
@@ -83,18 +83,7 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {
|
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */,
|
|
||||||
);
|
|
||||||
explicitFileTypes = {
|
|
||||||
};
|
|
||||||
explicitFolders = (
|
|
||||||
);
|
|
||||||
path = "情绪小组件";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -210,7 +199,7 @@
|
|||||||
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */,
|
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */,
|
||||||
);
|
);
|
||||||
name = "Recovered References";
|
name = "Recovered References";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -382,6 +371,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
@@ -397,6 +387,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
@@ -475,7 +466,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
|
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -712,6 +703,7 @@
|
|||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "情绪小组件ExtensionRelease.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 4;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"color": {
|
"color": {
|
||||||
"components": {
|
"components": {
|
||||||
"alpha": "1.000",
|
"alpha": "1.000",
|
||||||
"blue": "1.00000000000000",
|
"blue": "0.729411764705882",
|
||||||
"green": "1.00000000000000",
|
"green": "0.823529411764706",
|
||||||
"red": "1.00000000000000"
|
"red": "0.917647058823529"
|
||||||
},
|
},
|
||||||
"color-space": "srgb"
|
"color-space": "srgb"
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 123 KiB |
@@ -22,6 +22,14 @@
|
|||||||
<string>CA92.1</string>
|
<string>CA92.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>35F9.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
@@ -31,14 +39,6 @@
|
|||||||
<string>85F4.1</string>
|
<string>85F4.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>35F9.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<array/>
|
<array/>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<namedColor name="SplashScreenBackground">
|
<namedColor name="SplashScreenBackground">
|
||||||
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
|
<color alpha="1.000" blue="0.729411764705882" green="0.823529411764706" red="0.917647058823529" customColorSpace="sRGB" colorSpace="custom"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
// - 部分环境下仅 `import React` 可能无法在 Swift 中解析到 RCTBridge 等类型
|
// - 部分环境下仅 `import React` 可能无法在 Swift 中解析到 RCTBridge 等类型
|
||||||
// - 通过 Bridging Header 显式引入需要的 React 头文件,保证 AppDelegate.swift 可编译
|
// - 通过 Bridging Header 显式引入需要的 React 头文件,保证 AppDelegate.swift 可编译
|
||||||
#import <React/RCTBridge.h>
|
#import <React/RCTBridge.h>
|
||||||
|
#import <React/RCTBridgeModule.h>
|
||||||
#import <React/RCTBundleURLProvider.h>
|
#import <React/RCTBundleURLProvider.h>
|
||||||
#import <React/RCTLinkingManager.h>
|
#import <React/RCTLinkingManager.h>
|
||||||
|
|||||||
@@ -237,48 +237,22 @@ struct EmotionWidgetView: View {
|
|||||||
var entry: EmotionProvider.Entry
|
var entry: EmotionProvider.Entry
|
||||||
@Environment(\.widgetFamily) var family
|
@Environment(\.widgetFamily) var family
|
||||||
private let deepLink = URL(string: "client:///(app)/home")
|
private let deepLink = URL(string: "client:///(app)/home")
|
||||||
|
private let widgetBackgroundColor = Color(red: 1.0, green: 250.0 / 255.0, blue: 229.0 / 255.0) // #FFFAE5
|
||||||
|
private let widgetTextColor = Color(red: 98.0 / 255.0, green: 59.0 / 255.0, blue: 59.0 / 255.0) // #623B3B
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
// 只显示一句话(不显示标题/提示/时间等装饰元素)
|
||||||
cardBackground(colors: [
|
Text(entry.text)
|
||||||
Color(red: 0.06, green: 0.08, blue: 0.12),
|
.font(fontForFamily())
|
||||||
Color(red: 0.14, green: 0.18, blue: 0.28),
|
.foregroundColor(widgetTextColor)
|
||||||
])
|
.multilineTextAlignment(.leading)
|
||||||
|
.lineSpacing(lineSpacingForFamily())
|
||||||
// 只显示一句话(不显示标题/提示/时间等装饰元素)
|
.lineLimit(lineLimitForFamily())
|
||||||
Text(entry.text)
|
.minimumScaleFactor(0.78)
|
||||||
.font(fontForFamily())
|
.padding(paddingForFamily())
|
||||||
.foregroundColor(Color.white.opacity(0.92))
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
.multilineTextAlignment(.leading)
|
.widgetSolidBackground(widgetBackgroundColor)
|
||||||
.lineSpacing(lineSpacingForFamily())
|
.widgetURL(deepLink)
|
||||||
.lineLimit(lineLimitForFamily())
|
|
||||||
.minimumScaleFactor(0.78)
|
|
||||||
.padding(paddingForFamily())
|
|
||||||
}
|
|
||||||
.widgetURL(deepLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一的“卡片背景”风格(iOS 15 兼容)
|
|
||||||
private func cardBackground(colors: [Color]) -> some View {
|
|
||||||
ZStack {
|
|
||||||
LinearGradient(
|
|
||||||
colors: colors,
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
// 轻微光斑,增加层次
|
|
||||||
RadialGradient(
|
|
||||||
gradient: Gradient(colors: [Color.white.opacity(0.16), Color.white.opacity(0.0)]),
|
|
||||||
center: .topTrailing,
|
|
||||||
startRadius: 10,
|
|
||||||
endRadius: 180
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
||||||
.stroke(Color.white.opacity(0.14), lineWidth: 1)
|
|
||||||
)
|
|
||||||
.cornerRadius(18)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fontForFamily() -> Font {
|
private func fontForFamily() -> Font {
|
||||||
@@ -330,6 +304,26 @@ struct EmotionWidgetView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct WidgetSolidBackgroundModifier: ViewModifier {
|
||||||
|
let color: Color
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if #available(iOSApplicationExtension 17.0, *) {
|
||||||
|
content.containerBackground(for: .widget) { color }
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
.background(color)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
func widgetSolidBackground(_ color: Color) -> some View {
|
||||||
|
modifier(WidgetSolidBackgroundModifier(color: color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct EmotionWidget: Widget {
|
struct EmotionWidget: Widget {
|
||||||
let kind: String = "EmotionWidget"
|
let kind: String = "EmotionWidget"
|
||||||
|
|||||||
@@ -33,9 +33,13 @@ function mapMomStage(raw: string | undefined): QuestionnaireAnswersV1_2['mom_sta
|
|||||||
|
|
||||||
function mapEmotion(raw: string | undefined): QuestionnaireAnswersV1_2['emotion'] {
|
function mapEmotion(raw: string | undefined): QuestionnaireAnswersV1_2['emotion'] {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
// UI 当前选项:happy/calm/stressed/low
|
// UI 选项:
|
||||||
|
// - happy/calm/stressed/low:历史选项(仍保留兼容)
|
||||||
|
// - okay/tired:新增选项
|
||||||
if (raw === 'happy') return 'joyful';
|
if (raw === 'happy') return 'joyful';
|
||||||
if (raw === 'calm') return 'calm';
|
if (raw === 'calm') return 'calm';
|
||||||
|
if (raw === 'okay') return 'neutral';
|
||||||
|
if (raw === 'tired') return 'tired';
|
||||||
if (raw === 'stressed') return 'overwhelmed';
|
if (raw === 'stressed') return 'overwhelmed';
|
||||||
if (raw === 'low') return 'low';
|
if (raw === 'low') return 'low';
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import * as Localization from 'expo-localization';
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import { isTraditionalChineseLocaleTag } from './locale';
|
||||||
|
|
||||||
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const all = require('./locales/all.json') as { en: Record<string, unknown>; 'zh-TW': Record<string, unknown> };
|
const all = require('./locales/all.json') as { en: Record<string, unknown>; 'zh-TW': Record<string, unknown> };
|
||||||
@@ -29,13 +31,8 @@ function isSupportedLanguage(lang: string): lang is AppLanguage {
|
|||||||
function normalizeDeviceLanguageTagToAppLanguage(languageTag: string): AppLanguage {
|
function normalizeDeviceLanguageTagToAppLanguage(languageTag: string): AppLanguage {
|
||||||
const tag = languageTag.toLowerCase();
|
const tag = languageTag.toLowerCase();
|
||||||
|
|
||||||
// 中文:当前仅支持繁体中文(zh-TW)
|
|
||||||
if (tag.startsWith('zh')) {
|
|
||||||
return 'zh-TW';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他语言:按前缀匹配(当前仅支持英文)
|
|
||||||
if (tag.startsWith('en')) return 'en';
|
if (tag.startsWith('en')) return 'en';
|
||||||
|
if (isTraditionalChineseLocaleTag(tag)) return 'zh-TW';
|
||||||
|
|
||||||
return DEFAULT_FALLBACK_LANGUAGE;
|
return DEFAULT_FALLBACK_LANGUAGE;
|
||||||
}
|
}
|
||||||
|
|||||||
35
client/src/i18n/locale.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export type BackendLocale = 'en' | 'tc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个 BCP-47 language tag 是否应视为「繁体中文」(TC)。
|
||||||
|
*
|
||||||
|
* 规则(面向当前产品约束:只支持 EN/TC,默认 EN):
|
||||||
|
* - 仅在语言为中文(zh)且脚本为 Hant 或地区为 TW/HK/MO 时,判定为 TC
|
||||||
|
* - 兼容后端/历史写法:明确包含 tc 也视为 TC
|
||||||
|
* - 其他情况一律视为 EN
|
||||||
|
*/
|
||||||
|
export function isTraditionalChineseLocaleTag(languageTag: string): boolean {
|
||||||
|
const tag = (languageTag || '').trim().toLowerCase();
|
||||||
|
if (!tag) return false;
|
||||||
|
|
||||||
|
// 兼容:有些链路可能直接传 tc
|
||||||
|
const parts = tag.split(/[-_]/g).filter(Boolean);
|
||||||
|
if (parts.includes('tc')) return true;
|
||||||
|
|
||||||
|
const lang = parts[0];
|
||||||
|
if (lang !== 'zh') return false;
|
||||||
|
|
||||||
|
// 脚本:zh-Hant / zh-Hant-TW / zh-Hant-HK ...
|
||||||
|
if (tag.includes('hant') || parts.includes('hant')) return true;
|
||||||
|
|
||||||
|
// 地区:zh-TW / zh-HK / zh-MO
|
||||||
|
if (parts.includes('tw') || parts.includes('hk') || parts.includes('mo')) return true;
|
||||||
|
|
||||||
|
// 其他中文(如 zh / zh-CN / zh-Hans)不属于 TC → 回退 EN
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBackendLocaleFromLanguageTag(languageTag: string | null | undefined): BackendLocale {
|
||||||
|
return isTraditionalChineseLocaleTag(languageTag ?? '') ? 'tc' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,39 +25,41 @@
|
|||||||
},
|
},
|
||||||
"onboardingSurvey": {
|
"onboardingSurvey": {
|
||||||
"steps": {
|
"steps": {
|
||||||
"name": { "title": "What should I call you?" },
|
"name": { "title": "What do you want to be called?", "placeholder": "Mama" },
|
||||||
"status": {
|
"status": {
|
||||||
"title": "Your current stage?",
|
"title": "Which stage of motherhood are you in?",
|
||||||
"options": {
|
"options": {
|
||||||
"pregnant": "Pregnant / preparing for motherhood",
|
"pregnant": "Pregnant / Preparing",
|
||||||
"has_kids": "Already have kids",
|
"has_kids": "Parenting",
|
||||||
"no_fill": "Prefer not to say"
|
"no_fill": "Prefer not to say"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"emotion": {
|
"emotion": {
|
||||||
"title": "How are you feeling right now?",
|
"title": "How are you feeling right now?",
|
||||||
"options": {
|
"options": {
|
||||||
"happy": "Happy / satisfied",
|
"happy": "Joyful",
|
||||||
"calm": "Calm / grounded",
|
"calm": "Calm",
|
||||||
"stressed": "Stressed / overwhelmed",
|
"okay": "Okay",
|
||||||
"low": "Down / low mood"
|
"tired": "Tired",
|
||||||
|
"stressed": "Overwhelmed",
|
||||||
|
"low": "Low"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"influence": {
|
"influence": {
|
||||||
"title": "What has been affecting you lately?",
|
"title": "What’s been influencing how you feel?",
|
||||||
"options": {
|
"options": {
|
||||||
"family": "Family & kids",
|
"family": "Family",
|
||||||
"work": "Work or study",
|
"work": "Work or study",
|
||||||
"relationship": "Intimate relationship",
|
"relationship": "Relationship",
|
||||||
"friends": "Friends & social life",
|
"friends": "Friends",
|
||||||
"health": "Mental & physical health"
|
"health": "Health"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
"title": "What support do you need most?",
|
"title": "What kind of support do you need most right now?",
|
||||||
"options": {
|
"options": {
|
||||||
"emotional": "Emotional support",
|
"emotional": "Emotional support",
|
||||||
"parenting": "Parenting stress",
|
"parenting": "Parenting pressure",
|
||||||
"self_worth": "Self-worth",
|
"self_worth": "Self-worth",
|
||||||
"anxiety": "Anxiety relief",
|
"anxiety": "Anxiety relief",
|
||||||
"balance": "Rest & balance"
|
"balance": "Rest & balance"
|
||||||
@@ -119,6 +121,9 @@
|
|||||||
"widget": {
|
"widget": {
|
||||||
"lockScreen": "Lock Screen Widget",
|
"lockScreen": "Lock Screen Widget",
|
||||||
"homeScreen": "Home Screen Widget",
|
"homeScreen": "Home Screen Widget",
|
||||||
|
"howToTitle": "How to add the widget",
|
||||||
|
"howToDesc1": "Long-press on the Home Screen until the apps jiggle, then tap “+” in the top-left corner.",
|
||||||
|
"howToDesc2": "Search “Mindfulness”, choose a widget size you like, then tap “Add Widget”.",
|
||||||
"previewDate": "Thu, Jan 29",
|
"previewDate": "Thu, Jan 29",
|
||||||
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
||||||
},
|
},
|
||||||
@@ -136,10 +141,11 @@
|
|||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "You Are Perfect.",
|
"title": "You Are Perfect.",
|
||||||
"subtitle": "Everything Will Be Better.",
|
"subtitle": "Everything\nWill Be Better.",
|
||||||
"agree": "Agree & Continue",
|
"agree": "Agree & Continue",
|
||||||
"privacy": "Privacy Policy",
|
"privacy": "Privacy Policy",
|
||||||
"terms": "Terms of Use"
|
"terms": "Terms of Use",
|
||||||
|
"notice": "By continuing, you agree to the Privacy Policy and Terms of Use."
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"notificationsDenied": "Notifications are denied. Please enable them in Settings."
|
"notificationsDenied": "Notifications are denied. Please enable them in Settings."
|
||||||
@@ -180,7 +186,7 @@
|
|||||||
},
|
},
|
||||||
"onboardingSurvey": {
|
"onboardingSurvey": {
|
||||||
"steps": {
|
"steps": {
|
||||||
"name": { "title": "我可以怎麼稱呼你?" },
|
"name": { "title": "我可以怎麼稱呼你?", "placeholder": "媽媽" },
|
||||||
"status": {
|
"status": {
|
||||||
"title": "媽媽的狀態?",
|
"title": "媽媽的狀態?",
|
||||||
"options": {
|
"options": {
|
||||||
@@ -194,6 +200,8 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"happy": "愉悅、滿足",
|
"happy": "愉悅、滿足",
|
||||||
"calm": "平靜、安穩",
|
"calm": "平靜、安穩",
|
||||||
|
"okay": "還可以、普通",
|
||||||
|
"tired": "疲累、沒什麼力氣",
|
||||||
"stressed": "被壓得有點喘不過氣",
|
"stressed": "被壓得有點喘不過氣",
|
||||||
"low": "情緒低落"
|
"low": "情緒低落"
|
||||||
}
|
}
|
||||||
@@ -274,6 +282,9 @@
|
|||||||
"widget": {
|
"widget": {
|
||||||
"lockScreen": "鎖屏小工具",
|
"lockScreen": "鎖屏小工具",
|
||||||
"homeScreen": "桌面小工具",
|
"homeScreen": "桌面小工具",
|
||||||
|
"howToTitle": "如何添加小工具",
|
||||||
|
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||||
|
"howToDesc2": "搜尋「正念」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||||
},
|
},
|
||||||
@@ -290,9 +301,12 @@
|
|||||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
|
"title": "你很完美。",
|
||||||
|
"subtitle": "一切\n都會更好。",
|
||||||
"agree": "同意並繼續",
|
"agree": "同意並繼續",
|
||||||
"privacy": "隱私協議",
|
"privacy": "隱私協議",
|
||||||
"terms": "用戶使用協議"
|
"terms": "用戶使用協議",
|
||||||
|
"notice": "繼續使用即代表你同意《隱私協議》與《用戶使用協議》。"
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"notificationsDenied": "系統權限已被拒絕,請前往手機設定開啟通知。"
|
"notificationsDenied": "系統權限已被拒絕,請前往手機設定開啟通知。"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { API_BASE_URL } from '@/src/constants/env';
|
import { API_BASE_URL } from '@/src/constants/env';
|
||||||
import type { UserProfileV1_2, UserProfileV1_2_Extended } from '@/src/features/userProfileScoring/types';
|
import type { UserProfileV1_2, UserProfileV1_2_Extended } from '@/src/features/userProfileScoring/types';
|
||||||
|
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
|
||||||
import { fetchRecoWidget } from '@/src/services/recoApi';
|
import { fetchRecoWidget } from '@/src/services/recoApi';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
||||||
@@ -144,7 +145,7 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
|||||||
const top = items?.[0];
|
const top = items?.[0];
|
||||||
if (!top?.text) return;
|
if (!top?.text) return;
|
||||||
|
|
||||||
const lang = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
await setWidgetDailyRecoCache({
|
await setWidgetDailyRecoCache({
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
saved_at: new Date().toISOString(),
|
saved_at: new Date().toISOString(),
|
||||||
|
|||||||
@@ -20,10 +20,15 @@ describe('legalApi.buildAcceptLanguage', () => {
|
|||||||
expect(buildAcceptLanguage()).toBe('en');
|
expect(buildAcceptLanguage()).toBe('en');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('任意 zh* 归一为 tc', () => {
|
it('简中/其他中文不支持时回退为 en', () => {
|
||||||
setLang('zh-CN');
|
setLang('zh-CN');
|
||||||
expect(buildAcceptLanguage()).toBe('tc');
|
expect(buildAcceptLanguage()).toBe('en');
|
||||||
|
|
||||||
|
setLang('zh');
|
||||||
|
expect(buildAcceptLanguage()).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('繁体中文归一为 tc', () => {
|
||||||
setLang('zh-TW');
|
setLang('zh-TW');
|
||||||
expect(buildAcceptLanguage()).toBe('tc');
|
expect(buildAcceptLanguage()).toBe('tc');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
import { httpJson } from '../utils/http';
|
import { httpJson } from '../utils/http';
|
||||||
|
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||||
|
|
||||||
export type LegalLinks = {
|
export type LegalLinks = {
|
||||||
privacyPolicyUrl: string;
|
privacyPolicyUrl: string;
|
||||||
@@ -9,17 +10,8 @@ export type LegalLinks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildAcceptLanguage(): 'en' | 'tc' {
|
export function buildAcceptLanguage(): 'en' | 'tc' {
|
||||||
const lang = (i18n.language || '').trim();
|
// 当前多语言仅支持 EN / TC;其他语言统一回退到 en
|
||||||
const lower = lang.toLowerCase();
|
return toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
|
|
||||||
// 当前多语言仅支持 EN / TC(与 reco 链路一致);其他语言统一回退到 en
|
|
||||||
if (lower.startsWith('zh')) {
|
|
||||||
return 'tc';
|
|
||||||
}
|
|
||||||
if (lower.includes('tc') || lower.includes('hant') || lower.includes('hk') || lower.includes('mo') || lower.includes('tw')) {
|
|
||||||
return 'tc';
|
|
||||||
}
|
|
||||||
return 'en';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchLegalLinks(): Promise<LegalLinks> {
|
export async function fetchLegalLinks(): Promise<LegalLinks> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { httpJson } from '../utils/http';
|
|||||||
import { APP_ENV } from '../constants/env';
|
import { APP_ENV } from '../constants/env';
|
||||||
import { getDailyReminderSettings, getOrCreateClientUserId, getUserProfileScoring } from '../storage/appStorage';
|
import { getDailyReminderSettings, getOrCreateClientUserId, getUserProfileScoring } from '../storage/appStorage';
|
||||||
import type { UserProfileScoring } from '../storage/appStorage';
|
import type { UserProfileScoring } from '../storage/appStorage';
|
||||||
|
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||||
|
|
||||||
export type PushEnv = 'dev' | 'prod';
|
export type PushEnv = 'dev' | 'prod';
|
||||||
|
|
||||||
@@ -44,11 +45,7 @@ export type PushPreferencesResponse = PushPreferencesRequest & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildAcceptLanguage(): 'en' | 'tc' {
|
export function buildAcceptLanguage(): 'en' | 'tc' {
|
||||||
const lang = (i18n.language || '').trim();
|
return toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
const lower = lang.toLowerCase();
|
|
||||||
if (lower.startsWith('zh')) return 'tc';
|
|
||||||
if (lower.includes('tc') || lower.includes('hant') || lower.includes('hk') || lower.includes('mo') || lower.includes('tw')) return 'tc';
|
|
||||||
return 'en';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPushEnv(appEnv: typeof APP_ENV): PushEnv {
|
function toPushEnv(appEnv: typeof APP_ENV): PushEnv {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
import type { UserProfileV1_2 } from '../features/userProfileScoring';
|
import type { UserProfileV1_2 } from '../features/userProfileScoring';
|
||||||
|
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||||
import { httpJson } from '../utils/http';
|
import { httpJson } from '../utils/http';
|
||||||
|
|
||||||
export type RecommendedItem = {
|
export type RecommendedItem = {
|
||||||
@@ -27,7 +28,7 @@ export type RecoRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchRecoFeed(req: RecoRequest): Promise<RecoEngineResult> {
|
export async function fetchRecoFeed(req: RecoRequest): Promise<RecoEngineResult> {
|
||||||
const acceptLanguage = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
const acceptLanguage = toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
// 让后端做 locale 选择(目前后端只区分 en/tc)
|
// 让后端做 locale 选择(目前后端只区分 en/tc)
|
||||||
@@ -52,7 +53,7 @@ export async function fetchRecoFeed(req: RecoRequest): Promise<RecoEngineResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchRecoWidget(req: RecoRequest): Promise<RecoEngineResult> {
|
export async function fetchRecoWidget(req: RecoRequest): Promise<RecoEngineResult> {
|
||||||
const acceptLanguage = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
const acceptLanguage = toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
// 让后端做 locale 选择(目前后端只区分 en/tc)
|
// 让后端做 locale 选择(目前后端只区分 en/tc)
|
||||||
|
|||||||