小组件换行文案更新
This commit is contained in:
@@ -55,6 +55,7 @@ import { getBootId } from '@/src/utils/bootSession';
|
||||
import { advanceSuixinState, buildInitialSuixinState, NEUTRAL_THEME_COLORS } from '@/src/features/suixinTheme';
|
||||
import { wrapText } from '@/src/features/textWrap';
|
||||
import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure';
|
||||
import { ensureDailyWidgetRecoUpToDate } from '@/src/modules/dailyWidgetReco';
|
||||
|
||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
@@ -265,11 +266,24 @@ export default function HomeScreen() {
|
||||
maxLines: 3,
|
||||
overflowMode: 'CLIP',
|
||||
lineMode: 'AUTO',
|
||||
configVersion: 'v1',
|
||||
// Home:采用“更偏好语气停顿/更好看”的排版风格微调(不影响算法默认 v1)
|
||||
configVersion: 'v1-home',
|
||||
debug: __DEV__,
|
||||
fontSpec,
|
||||
contextProfile: `APP|${Platform.OS}|home|${lang}`,
|
||||
measureWidthImpl: defaultMeasureWidthImpl,
|
||||
scoringOverrides:
|
||||
lang === 'TC'
|
||||
? {
|
||||
// 更偏好在逗号/句号等处断行(即便宽度允许也不一定要塞满)
|
||||
weights: { R_PUNCT_BREAK: 180 },
|
||||
// 让“理想行宽”更短,避免宽屏下过度延后断行
|
||||
idealWidthRatio: { APP: 0.82 },
|
||||
// 更宽容短行(尤其是第一行在标点处停顿)
|
||||
minPreferredRatio: 0.45,
|
||||
shortLastLineRatio: 0.45,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (cancelled) return;
|
||||
@@ -377,6 +391,11 @@ export default function HomeScreen() {
|
||||
setIndex(0);
|
||||
fetchNewFeed();
|
||||
}
|
||||
|
||||
// Widget:前台辅助刷新(尽力而为)
|
||||
// - 写入 App Group 的 dailyReco 缓存
|
||||
// - 生成 wrapped_text_by_family,供 Widget 直接渲染
|
||||
ensureDailyWidgetRecoUpToDate({ reason: 'home_focus' }).catch(() => {});
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@@ -444,13 +463,41 @@ export default function HomeScreen() {
|
||||
});
|
||||
}, [busy, index, currentFeed.length, isFetching, fetchNewFeed, translateY, opacity]);
|
||||
|
||||
// 切换到上一条文案的统一动画逻辑(下滑触发)
|
||||
const triggerPrevContent = useCallback(() => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
|
||||
// 1. 当前文案向下移动并消失
|
||||
translateY.value = withTiming(40, { duration: 300, easing: Easing.out(Easing.quad) });
|
||||
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
|
||||
if (finished) {
|
||||
// 2. 切换数据索引(循环回退)
|
||||
const nextIndex = index - 1 < 0 ? Math.max(0, currentFeed.length - 1) : index - 1;
|
||||
runOnJS(setIndex)(nextIndex);
|
||||
runOnJS(setLikeFilled)(false);
|
||||
|
||||
// 3. 准备上一条文案:先瞬移到上方 40pt
|
||||
translateY.value = -40;
|
||||
|
||||
// 4. 上一条文案向下移动到原位并显现
|
||||
translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) });
|
||||
opacity.value = withTiming(1, { duration: 400 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(setBusy)(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [busy, index, currentFeed.length, translateY, opacity]);
|
||||
|
||||
const lastTapRef = useRef<number>(0);
|
||||
|
||||
// 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function
|
||||
const handlersRef = useRef({ onPressLike, triggerNextContent });
|
||||
const handlersRef = useRef({ onPressLike, triggerNextContent, triggerPrevContent });
|
||||
useEffect(() => {
|
||||
handlersRef.current = { onPressLike, triggerNextContent };
|
||||
}, [onPressLike, triggerNextContent]);
|
||||
handlersRef.current = { onPressLike, triggerNextContent, triggerPrevContent };
|
||||
}, [onPressLike, triggerNextContent, triggerPrevContent]);
|
||||
|
||||
// 使用系统自带的 PanResponder 代替第三方手势库
|
||||
const panResponder = useRef(
|
||||
@@ -475,9 +522,11 @@ export default function HomeScreen() {
|
||||
}
|
||||
lastTapRef.current = now;
|
||||
|
||||
// 2. 上滑逻辑判定
|
||||
// 2. 上滑/下滑逻辑判定
|
||||
if (gestureState.dy < -50) { // 上滑超过 50pt
|
||||
runOnJS(handlersRef.current.triggerNextContent)();
|
||||
} else if (gestureState.dy > 50) { // 下滑超过 50pt
|
||||
runOnJS(handlersRef.current.triggerPrevContent)();
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -549,13 +598,13 @@ export default function HomeScreen() {
|
||||
onPress={() => setThemeOpen(true)}
|
||||
accessibilityLabel={t('home.theme')}
|
||||
>
|
||||
<ThemeIcon width={18} height={18} />
|
||||
<ThemeIcon width={20} height={20} />
|
||||
</CircleIconButton>
|
||||
<CircleIconButton
|
||||
onPress={() => setProfileOpen(true)}
|
||||
accessibilityLabel={t('home.profile')}
|
||||
>
|
||||
<MyIcon width={18} height={18} />
|
||||
<MyIcon width={20} height={20} />
|
||||
</CircleIconButton>
|
||||
</View>
|
||||
|
||||
@@ -577,15 +626,16 @@ export default function HomeScreen() {
|
||||
onPress={onPressLike}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('home.like')}
|
||||
hitSlop={20}
|
||||
// 稍微增大可点击区域,提升单手操作成功率
|
||||
hitSlop={24}
|
||||
style={styles.reactionInner}
|
||||
>
|
||||
{likeFilled ? (
|
||||
<LikeFilledIcon width={35} height={36} color="#EA6969" />
|
||||
<LikeFilledIcon width={40} height={41} color="#EA6969" />
|
||||
) : (
|
||||
<LikeIcon
|
||||
width={35}
|
||||
height={36}
|
||||
width={40}
|
||||
height={41}
|
||||
color={themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28'}
|
||||
/>
|
||||
)}
|
||||
@@ -615,7 +665,8 @@ function CircleIconButton({
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
hitSlop={10}
|
||||
// 稍微增大可点击区域,提升易用性
|
||||
hitSlop={14}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
style={styles.circleBtn}
|
||||
@@ -640,9 +691,9 @@ const styles = StyleSheet.create({
|
||||
zIndex: 30,
|
||||
},
|
||||
circleBtn: {
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 17,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -175,18 +175,17 @@ export default function OnboardingScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const onSkip = async () => {
|
||||
// 跳过整个 Onboarding:仍生成一个“全跳过”的最小画像,保证下游可用
|
||||
const scoringProfile = buildUserProfileFromQuestionnaire({});
|
||||
await setUserProfileScoring(scoringProfile);
|
||||
|
||||
// 同步到 App Group:供 iOS Widget 使用(失败不阻塞)
|
||||
await syncWidgetConfig();
|
||||
await syncWidgetUserProfileFromScoring(scoringProfile);
|
||||
|
||||
// 标记已完成,避免下次启动再次进入 Onboarding
|
||||
await setOnboardingCompleted(true);
|
||||
router.replace('/(app)/home');
|
||||
/** 只跳過當前這一步(不填/不選當前題,進入下一步) */
|
||||
const handleSkipCurrentStep = () => {
|
||||
if (currentStep.type === 'name') {
|
||||
onNext();
|
||||
} else if (currentStep.type === 'selection') {
|
||||
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
|
||||
onNext();
|
||||
} else if (currentStep.type === 'reminder') {
|
||||
setReminderTimes(0);
|
||||
onFinish();
|
||||
}
|
||||
};
|
||||
|
||||
// 题目为多选:点击切换选中状态
|
||||
@@ -210,9 +209,10 @@ export default function OnboardingScreen() {
|
||||
title={currentTitle}
|
||||
currentStep={stepIndex}
|
||||
totalSteps={STEPS.length - 1}
|
||||
onSkip={onSkip}
|
||||
onSkip={handleSkipCurrentStep}
|
||||
onBack={onBack}
|
||||
showBackButton={stepIndex > 0}
|
||||
userName={name}
|
||||
>
|
||||
{currentStep.type === 'name' && (
|
||||
<NameInputStep
|
||||
@@ -233,7 +233,7 @@ export default function OnboardingScreen() {
|
||||
|
||||
{currentStep.type === 'reminder' && (
|
||||
<ReminderStep
|
||||
value={reminderTimes}
|
||||
value={Math.max(1, reminderTimes)}
|
||||
onChange={setReminderTimes}
|
||||
onFinish={onFinish}
|
||||
onSkip={() => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { setConsentAccepted, getConsentAccepted } from '../../src/storage/appSto
|
||||
import { fetchLegalLinks } from '@/src/services/legalApi';
|
||||
import { getOnboardingCompleted } from '@/src/storage/appStorage';
|
||||
import { API_BASE_URL } from '@/src/constants/env';
|
||||
import { isTraditionalChineseLocaleTag } from '@/src/i18n/locale';
|
||||
|
||||
// 导入 SVG 组件
|
||||
import FlowersBg from '../../assets/images/index/flowers_endbg.svg';
|
||||
@@ -15,10 +16,30 @@ import WelcomeBtn from '../../assets/images/index/welcome_btn.svg';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
// 繁中開屏 consent 文案:寫死在元件內,避免 Metro/iOS bundle 快取導致永遠顯示舊文案。
|
||||
// 若需修改,請改這裡並同步 client/src/i18n/locales/zh-TW.json 的 consent 區塊。
|
||||
const ZH_TW_CONSENT = {
|
||||
title: '我們知道,',
|
||||
subtitle: '當媽媽很不容易。',
|
||||
subtitleSecondary: '這裡給你一些溫柔的肯定與提醒',
|
||||
};
|
||||
|
||||
export default function SplashScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showConsent, setShowConsent] = useState(false);
|
||||
|
||||
// 繁中時強制使用上方常數(含 zh-TW / zh-Hant / zh-Hant-TW),其餘用 i18n
|
||||
const isZhTW = isTraditionalChineseLocaleTag(i18n.language || '');
|
||||
const title = isZhTW ? ZH_TW_CONSENT.title : t('consent.title');
|
||||
const subtitle = isZhTW ? ZH_TW_CONSENT.subtitle : t('consent.subtitle');
|
||||
const subtitleSecondary = isZhTW ? ZH_TW_CONSENT.subtitleSecondary : t('consent.subtitleSecondary');
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__ && showConsent) {
|
||||
console.log('[i18n consent] language=', i18n.language, 'title=', title, 'subtitle=', subtitle);
|
||||
}
|
||||
}, [showConsent, i18n.language, title, subtitle]);
|
||||
const [links, setLinks] = useState<{ privacy?: string; terms?: string }>({});
|
||||
const [linksLoading, setLinksLoading] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
@@ -126,13 +147,16 @@ export default function SplashScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 文案内容 */}
|
||||
{/* 文案内容:主標題兩行 + 可選二級標題(字號更小、顏色更淺);繁中為元件內常數,其餘用 i18n */}
|
||||
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
|
||||
<Text style={styles.titleText}>
|
||||
{t('consent.title')}
|
||||
{title}
|
||||
{'\n'}
|
||||
{t('consent.subtitle')}
|
||||
{subtitle}
|
||||
</Text>
|
||||
{subtitleSecondary ? (
|
||||
<Text style={styles.consentSubtitleSecondary}>{subtitleSecondary}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
|
||||
@@ -213,6 +237,14 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
|
||||
},
|
||||
consentSubtitleSecondary: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
color: 'rgba(119, 47, 0, 0.6)',
|
||||
textAlign: 'center',
|
||||
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
|
||||
},
|
||||
bottomContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 60,
|
||||
|
||||
@@ -553,6 +553,8 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
|
||||
function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const currentLang = i18n.language;
|
||||
// 需求:个人主页弹窗「小工具」页暂时隐藏锁屏小工具说明/入口
|
||||
const showLockScreenWidget = false;
|
||||
|
||||
// 根据语言选择图片
|
||||
const widget1 = currentLang === 'en'
|
||||
@@ -570,10 +572,12 @@ function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.widgetScroll}>
|
||||
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
||||
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
|
||||
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
|
||||
</Pressable>
|
||||
{showLockScreenWidget ? (
|
||||
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
||||
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
|
||||
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
||||
<Image source={widget2} style={styles.widgetImg2} resizeMode="contain" />
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function WidgetModal({ visible, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SheetModal visible={visible} title={t('profile.widget')} onClose={onClose}>
|
||||
<SheetModal visible={visible} title={t('widget.howToTitle')} onClose={onClose}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.row}>
|
||||
<PreviewCard label={t('widget.lockScreen')}>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SerifText } from './SerifText';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
|
||||
|
||||
export const INTENTS = [
|
||||
{ id: 'love', labelKey: 'intent.love', icon: '❤️' },
|
||||
@@ -20,7 +19,7 @@ export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionSt
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SerifText style={styles.title}>{t('intent.title')}</SerifText>
|
||||
<Text style={styles.title}>{t('intent.title')}</Text>
|
||||
|
||||
<View style={styles.grid}>
|
||||
{INTENTS.map((intent) => {
|
||||
@@ -35,10 +34,10 @@ export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionSt
|
||||
onPress={() => onToggle(intent.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<SerifText style={styles.icon}>{intent.icon}</SerifText>
|
||||
<SerifText style={[styles.label, isSelected && styles.labelSelected]}>
|
||||
<Text style={styles.icon}>{intent.icon}</Text>
|
||||
<Text style={[styles.label, isSelected && styles.labelSelected]}>
|
||||
{t(intent.labelKey)}
|
||||
</SerifText>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -56,6 +55,8 @@ const styles = StyleSheet.create({
|
||||
fontSize: 24,
|
||||
marginBottom: 40,
|
||||
textAlign: 'center',
|
||||
fontFamily: OnboardingFont.question,
|
||||
color: OnboardingColors.textPrimary,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
@@ -86,10 +87,12 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
icon: {
|
||||
fontSize: 32,
|
||||
fontFamily: OnboardingFont.question,
|
||||
},
|
||||
label: {
|
||||
fontSize: 18,
|
||||
color: OnboardingColors.textPrimary,
|
||||
fontFamily: OnboardingFont.question,
|
||||
},
|
||||
labelSelected: {
|
||||
fontWeight: 'bold',
|
||||
|
||||
@@ -95,8 +95,7 @@ export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProp
|
||||
blurOnSubmit={true}
|
||||
onSubmitEditing={() => {
|
||||
Keyboard.dismiss();
|
||||
// 有输入时,“完成”直接进入下一步,避免真机卡在键盘上
|
||||
if (value.trim().length > 0) onNext();
|
||||
// 不再自動跳頁,僅收起鍵盤;前進需點擊底部 ➡️
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
|
||||
|
||||
const TRANSITION_OFFSET = 24;
|
||||
const TRANSITION_DURATION = 280;
|
||||
|
||||
interface OnboardingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -11,6 +14,8 @@ interface OnboardingLayoutProps {
|
||||
onSkip: () => void;
|
||||
onBack?: () => void;
|
||||
showBackButton?: boolean;
|
||||
/** 用户名字,仅在名字步骤之后的第一个问题(currentStep === 1)且非空时显示招呼语 */
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
export function OnboardingLayout({
|
||||
@@ -20,9 +25,48 @@ export function OnboardingLayout({
|
||||
totalSteps,
|
||||
onSkip,
|
||||
onBack,
|
||||
showBackButton = false
|
||||
showBackButton = false,
|
||||
userName = '',
|
||||
}: OnboardingLayoutProps) {
|
||||
const { t } = useTranslation();
|
||||
const showGreeting = currentStep === 1 && userName.trim().length > 0;
|
||||
const displayName = userName.trim();
|
||||
const prevStepRef = useRef(currentStep);
|
||||
const isFirstRenderRef = useRef(true);
|
||||
const translateX = useRef(new Animated.Value(0)).current;
|
||||
const opacity = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstRenderRef.current) {
|
||||
isFirstRenderRef.current = false;
|
||||
prevStepRef.current = currentStep;
|
||||
return;
|
||||
}
|
||||
if (prevStepRef.current === currentStep) return;
|
||||
|
||||
const direction = currentStep > prevStepRef.current ? 'forward' : 'back';
|
||||
prevStepRef.current = currentStep;
|
||||
|
||||
const startX = direction === 'forward' ? TRANSITION_OFFSET : -TRANSITION_OFFSET;
|
||||
translateX.setValue(startX);
|
||||
opacity.setValue(0.72);
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(translateX, {
|
||||
toValue: 0,
|
||||
duration: TRANSITION_DURATION,
|
||||
useNativeDriver: true,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
Animated.timing(opacity, {
|
||||
toValue: 1,
|
||||
duration: TRANSITION_DURATION,
|
||||
useNativeDriver: true,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
]).start();
|
||||
}, [currentStep, translateX, opacity]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
@@ -49,16 +93,29 @@ export function OnboardingLayout({
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Title & Progress Row */}
|
||||
{/* Title & Progress Row(名字步骤后第一步且名字非空时显示招呼语 + 问题) */}
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.questionTitle}>{title}</Text>
|
||||
<View style={styles.titleBlock}>
|
||||
{showGreeting && (
|
||||
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
|
||||
)}
|
||||
<Text style={styles.questionTitle}>{title}</Text>
|
||||
</View>
|
||||
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.content}>
|
||||
{/* Content:step 切换时滑动 + 淡入 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
opacity,
|
||||
transform: [{ translateX }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
@@ -114,18 +171,27 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-end',
|
||||
paddingHorizontal: 20,
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
marginBottom: 8,
|
||||
},
|
||||
titleBlock: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
greetingText: {
|
||||
fontSize: 22,
|
||||
color: OnboardingColors.questionTitle,
|
||||
fontFamily: OnboardingFont.question,
|
||||
marginBottom: 4,
|
||||
},
|
||||
questionTitle: {
|
||||
fontSize: 22,
|
||||
color: OnboardingColors.questionTitle,
|
||||
fontFamily: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif',
|
||||
flex: 1,
|
||||
fontFamily: OnboardingFont.question,
|
||||
},
|
||||
progressText: {
|
||||
fontSize: 18,
|
||||
color: OnboardingColors.textProgress,
|
||||
fontFamily: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif',
|
||||
fontFamily: OnboardingFont.question,
|
||||
marginLeft: 10,
|
||||
},
|
||||
content: {
|
||||
|
||||
@@ -11,16 +11,16 @@ interface ReminderStepProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
onFinish: () => void;
|
||||
onSkip: () => void;
|
||||
onSkip?: () => void;
|
||||
}
|
||||
|
||||
export function ReminderStep({ value, onChange, onFinish, onSkip }: ReminderStepProps) {
|
||||
export function ReminderStep({ value, onChange, onFinish }: ReminderStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const handleReduce = () => {
|
||||
// 允许 0~5;0 表示关闭每日提醒
|
||||
if (value > 0) onChange(value - 1);
|
||||
// 本页最小为 1;不接收提醒请使用右上角 Skip
|
||||
if (value > 1) onChange(value - 1);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
@@ -36,7 +36,9 @@ export function ReminderStep({ value, onChange, onFinish, onSkip }: ReminderStep
|
||||
|
||||
<View style={styles.numberWrapper}>
|
||||
<Text style={styles.numberText}>{value}</Text>
|
||||
<Text style={styles.unitText}>{t('dailyReminder.timesUnit')}</Text>
|
||||
<Text style={styles.unitText}>
|
||||
{value === 1 ? t('dailyReminder.timesUnitSingular') : t('dailyReminder.timesUnit')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity onPress={handleAdd} activeOpacity={0.7}>
|
||||
@@ -48,10 +50,6 @@ export function ReminderStep({ value, onChange, onFinish, onSkip }: ReminderStep
|
||||
<TouchableOpacity onPress={onFinish} activeOpacity={0.8}>
|
||||
<BtnClicked width={87} height={57} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={onSkip} activeOpacity={0.8} style={styles.skipBtn}>
|
||||
<Text style={styles.skipText}>{t('onboarding.skip')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -93,17 +91,5 @@ const styles = StyleSheet.create({
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
}
|
||||
,
|
||||
skipBtn: {
|
||||
marginTop: 14,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 18,
|
||||
},
|
||||
skipText: {
|
||||
color: OnboardingColors.textPrimary,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
opacity: 0.85,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { View, StyleSheet, TouchableOpacity, ScrollView, Text } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
import { SerifText } from './SerifText';
|
||||
import SelectedIcon from '@/assets/images/icon/selected_icon.svg';
|
||||
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
|
||||
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
|
||||
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
|
||||
|
||||
@@ -25,7 +23,8 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
|
||||
const insets = useSafeAreaInsets();
|
||||
const footerBottom = insets.bottom + 16;
|
||||
const footerButtonHeight = 57;
|
||||
const footerPaddingBottom = footerBottom + footerButtonHeight + 24;
|
||||
// 底部留白加大,避免最后一项与按钮边框视觉重叠
|
||||
const footerPaddingBottom = footerBottom + footerButtonHeight + 40;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -39,16 +38,11 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.id}
|
||||
style={styles.optionCard}
|
||||
style={[styles.optionCard, isSelected && styles.optionCardSelected]}
|
||||
onPress={() => onToggle(option.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<SerifText style={styles.optionText}>{option.label}</SerifText>
|
||||
{isSelected && (
|
||||
<View style={styles.iconWrapper}>
|
||||
<SelectedIcon width={20} height={20} />
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.optionText}>{option.label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -67,7 +61,7 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 20,
|
||||
paddingTop: 8,
|
||||
},
|
||||
scroll: {
|
||||
flex: 1,
|
||||
@@ -82,7 +76,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
@@ -91,14 +85,14 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
},
|
||||
optionCardSelected: {
|
||||
backgroundColor: OnboardingColors.cardSelected,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 18,
|
||||
color: OnboardingColors.textPrimary,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
},
|
||||
iconWrapper: {
|
||||
marginLeft: 10,
|
||||
fontFamily: OnboardingFont.question,
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/** 与 onboarding 问题标题一致的字体(PingFang TC / sans-serif) */
|
||||
export const OnboardingFont = {
|
||||
question: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif',
|
||||
};
|
||||
|
||||
export const OnboardingColors = {
|
||||
background: '#FFF4EA',
|
||||
textPrimary: '#772F00',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -11,13 +11,13 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
C0A1B2C3D4E5F60718293A4E /* Screen_page.png in Resources */ = {isa = PBXBuildFile; fileRef = C0A1B2C3D4E5F60718293A4D /* Screen_page.png */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
C0A1B2C3D4E5F60718293A4E /* Screen_page.png in Resources */ = {isa = PBXBuildFile; fileRef = C0A1B2C3D4E5F60718293A4D /* Screen_page.png */; };
|
||||
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||
EB3DAF832F2A4B8E00450593 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF822F2A4B8E00450593 /* SwiftUI.framework */; };
|
||||
EB3DAF942F2A4B8F00450593 /* 情绪小组件Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -54,12 +54,12 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
C0A1B2C3D4E5F60718293A4D /* Screen_page.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Screen_page.png; path = ../assets/images/Screen_page.png; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
C0A1B2C3D4E5F60718293A4D /* Screen_page.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Screen_page.png; path = ../assets/images/Screen_page.png; sourceTree = "<group>"; };
|
||||
C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-client/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
E3328F0E595C1F4A244DF238 /* libPods-client.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-client.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "情绪小组件Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -74,7 +74,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */ = {
|
||||
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
EmotionWidget.swift,
|
||||
@@ -85,18 +85,7 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = "情绪小组件";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -213,7 +202,7 @@
|
||||
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */,
|
||||
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
@@ -301,6 +290,7 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
"zh-Hant",
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
@@ -481,7 +471,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
|
||||
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ private let keyWidgetConfig = "widget.config.v1"
|
||||
private let keyWidgetUserProfile = "widget.userProfile.v1_2"
|
||||
private let keyWidgetDailyReco = "widget.dailyReco.v1"
|
||||
|
||||
private let fallbackTextTC = "你已经很努力了,今天也值得被温柔对待。"
|
||||
private let fallbackTextTC = "你已經很努力了,今天也值得被溫柔對待。"
|
||||
private let fallbackTextEN = "You’ve been doing great — you deserve kindness today."
|
||||
|
||||
private func defaults() -> UserDefaults? {
|
||||
@@ -29,17 +29,24 @@ private func localDayKey(_ date: Date = Date()) -> String {
|
||||
}
|
||||
|
||||
private func resolveLang() -> String {
|
||||
// 仅支持 en/tc
|
||||
let preferred = Locale.preferredLanguages.first?.lowercased() ?? "en"
|
||||
return preferred.hasPrefix("zh") ? "tc" : "en"
|
||||
// 仅支持 en/tc:根据设备语言选择
|
||||
// - 传统中文(Hant / TW / HK / MO)=> tc
|
||||
// - 其他语言(含简中 zh-Hans / zh-CN)=> en(默认)
|
||||
let preferred = (Locale.preferredLanguages.first ?? "en").lowercased()
|
||||
if preferred.hasPrefix("zh-hant") { return "tc" }
|
||||
if preferred.hasPrefix("zh-tw") { return "tc" }
|
||||
if preferred.hasPrefix("zh-hk") { return "tc" }
|
||||
if preferred.hasPrefix("zh-mo") { return "tc" }
|
||||
return "en"
|
||||
}
|
||||
|
||||
private func resolveTitle(lang: String) -> String {
|
||||
lang == "en" ? "Mindfulness" : "正念"
|
||||
// 需求:品牌文案「正念」统一改为 Hey Mama
|
||||
return "Hey Mama"
|
||||
}
|
||||
|
||||
private func resolveFooterHint(lang: String) -> String {
|
||||
lang == "en" ? "Tap to open the app" : "点我回到 App"
|
||||
lang == "en" ? "Tap to open the app" : "點我回到 App"
|
||||
}
|
||||
|
||||
private func joinUrl(base: String, path: String) -> String {
|
||||
@@ -76,16 +83,44 @@ private func writeJsonDict(_ dict: [String: Any], forKey key: String) {
|
||||
defaults()?.set(raw, forKey: key)
|
||||
}
|
||||
|
||||
private func readCachedText() -> (dayKey: String?, lang: String, text: String)? {
|
||||
private func readCachedText(family: WidgetFamily) -> (dayKey: String?, lang: String, text: String)? {
|
||||
guard let d = readJsonDict(forKey: keyWidgetDailyReco) else { return nil }
|
||||
let lang = (d["lang"] as? String) ?? resolveLang()
|
||||
let dayKey = d["day_key"] as? String
|
||||
if let item = d["item"] as? [String: Any], let text = item["text"] as? String, !text.isEmpty {
|
||||
return (dayKey: dayKey, lang: lang, text: text)
|
||||
if let item = d["item"] as? [String: Any] {
|
||||
if let text = pickWidgetText(item: item, family: family), !text.isEmpty {
|
||||
return (dayKey: dayKey, lang: lang, text: text)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func familyKey(_ family: WidgetFamily) -> String {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
return "small"
|
||||
case .systemMedium:
|
||||
return "medium"
|
||||
case .systemLarge:
|
||||
return "large"
|
||||
default:
|
||||
return "small"
|
||||
}
|
||||
}
|
||||
|
||||
private func pickWidgetText(item: [String: Any], family: WidgetFamily?) -> String? {
|
||||
// 优先使用 App 侧预换行的结果(wrapped_text_by_family),否则回退 raw text
|
||||
if let family = family,
|
||||
let wrappedByFamily = item["wrapped_text_by_family"] as? [String: Any] {
|
||||
let key = familyKey(family)
|
||||
if let v = wrappedByFamily[key] as? String, !v.isEmpty {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if let raw = item["text"] as? String, !raw.isEmpty { return raw }
|
||||
return nil
|
||||
}
|
||||
|
||||
private func readApiBaseUrl() -> String? {
|
||||
guard let d = readJsonDict(forKey: keyWidgetConfig) else { return nil }
|
||||
let base = d["apiBaseUrl"] as? String
|
||||
@@ -174,7 +209,7 @@ struct EmotionProvider: TimelineProvider {
|
||||
let today = localDayKey(Date())
|
||||
|
||||
// 1) 今日缓存优先
|
||||
if let cached = readCachedText(), cached.dayKey == today {
|
||||
if let cached = readCachedText(family: context.family), cached.dayKey == today {
|
||||
let entry = EmotionEntry(
|
||||
date: Date(),
|
||||
lang: cached.lang,
|
||||
@@ -201,7 +236,7 @@ struct EmotionProvider: TimelineProvider {
|
||||
}
|
||||
|
||||
// 3) 网络失败:用最近缓存或兜底
|
||||
if let cached = readCachedText() {
|
||||
if let cached = readCachedText(family: context.family) {
|
||||
let entry = EmotionEntry(
|
||||
date: Date(),
|
||||
lang: cached.lang,
|
||||
@@ -332,8 +367,9 @@ struct EmotionWidget: Widget {
|
||||
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
|
||||
EmotionWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("情绪小组件")
|
||||
.description("一段温柔提醒,陪你回到当下。")
|
||||
// 名称/描述:支持多语言(使用 Widget Extension 自己的 Localizable.strings)
|
||||
.configurationDisplayName("WIDGET_DISPLAY_NAME")
|
||||
.description("WIDGET_DESCRIPTION")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||||
}
|
||||
}
|
||||
|
||||
3
client/ios/情绪小组件/en.lproj/Localizable.strings
Normal file
3
client/ios/情绪小组件/en.lproj/Localizable.strings
Normal file
@@ -0,0 +1,3 @@
|
||||
"WIDGET_DISPLAY_NAME" = "Emotion Widget";
|
||||
"WIDGET_DESCRIPTION" = "A gentle reminder to return to the present.";
|
||||
|
||||
3
client/ios/情绪小组件/zh-Hant.lproj/Localizable.strings
Normal file
3
client/ios/情绪小组件/zh-Hant.lproj/Localizable.strings
Normal file
@@ -0,0 +1,3 @@
|
||||
"WIDGET_DISPLAY_NAME" = "情緒小組件";
|
||||
"WIDGET_DESCRIPTION" = "一段溫柔提醒,陪你回到當下。";
|
||||
|
||||
133
client/package-lock.json
generated
133
client/package-lock.json
generated
@@ -1543,6 +1543,7 @@
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmmirror.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hammerjs": "^2.0.36"
|
||||
},
|
||||
@@ -1558,6 +1559,7 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
@@ -1574,6 +1576,7 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -1590,6 +1593,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -1606,6 +1610,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -1622,6 +1627,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -1638,6 +1644,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -1654,6 +1661,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -1670,6 +1678,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -1686,6 +1695,7 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1702,6 +1712,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1718,6 +1729,7 @@
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1734,6 +1746,7 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1750,6 +1763,7 @@
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1766,6 +1780,7 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1782,6 +1797,7 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1798,6 +1814,7 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1814,6 +1831,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -1830,6 +1848,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
@@ -1846,6 +1865,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
@@ -1862,6 +1882,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
@@ -1878,6 +1899,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
@@ -1894,6 +1916,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
@@ -1910,6 +1933,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
@@ -1926,6 +1950,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -1942,6 +1967,7 @@
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -1958,6 +1984,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -3224,6 +3251,7 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -3237,6 +3265,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -3250,6 +3279,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -3263,6 +3293,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -3276,6 +3307,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -3289,6 +3321,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -3302,6 +3335,7 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3315,6 +3349,7 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3328,6 +3363,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3341,6 +3377,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3354,6 +3391,7 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3367,6 +3405,7 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3380,6 +3419,7 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3393,6 +3433,7 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3406,6 +3447,7 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3419,6 +3461,7 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3432,6 +3475,7 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3445,6 +3489,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3458,6 +3503,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3471,6 +3517,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
@@ -3484,6 +3531,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
@@ -3497,6 +3545,7 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -3510,6 +3559,7 @@
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -3523,6 +3573,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -3536,6 +3587,7 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -3569,7 +3621,8 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "8.0.0",
|
||||
@@ -3860,6 +3913,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
@@ -3869,13 +3923,15 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
@@ -3889,7 +3945,8 @@
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.46",
|
||||
"resolved": "https://registry.npmmirror.com/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
||||
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="
|
||||
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
@@ -3989,6 +4046,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.18.tgz",
|
||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
@@ -4006,6 +4064,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.18",
|
||||
"estree-walker": "^3.0.3",
|
||||
@@ -4032,6 +4091,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
@@ -4044,6 +4104,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.18.tgz",
|
||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"pathe": "^2.0.3"
|
||||
@@ -4057,6 +4118,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -4071,6 +4133,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.18.tgz",
|
||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
@@ -4080,6 +4143,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.18.tgz",
|
||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
@@ -4147,6 +4211,7 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -4285,6 +4350,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -4820,6 +4886,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -5601,7 +5668,8 @@
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
@@ -5621,6 +5689,7 @@
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -5701,6 +5770,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
@@ -5734,6 +5804,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -5832,6 +5903,7 @@
|
||||
"version": "15.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/expo-crypto/-/expo-crypto-15.0.8.tgz",
|
||||
"integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
@@ -5843,6 +5915,7 @@
|
||||
"version": "6.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-6.0.20.tgz",
|
||||
"integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-dev-launcher": "6.0.20",
|
||||
"expo-dev-menu": "7.0.18",
|
||||
@@ -5858,6 +5931,7 @@
|
||||
"version": "6.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz",
|
||||
"integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.11.0",
|
||||
"expo-dev-menu": "7.0.18",
|
||||
@@ -5871,6 +5945,7 @@
|
||||
"version": "7.0.18",
|
||||
"resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz",
|
||||
"integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-dev-menu-interface": "2.0.0"
|
||||
},
|
||||
@@ -5882,6 +5957,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
|
||||
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
@@ -5890,6 +5966,7 @@
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/expo-device/-/expo-device-8.0.10.tgz",
|
||||
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.33"
|
||||
},
|
||||
@@ -5915,6 +5992,7 @@
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ua-parser-js": "script/cli.js"
|
||||
},
|
||||
@@ -5949,7 +6027,8 @@
|
||||
"node_modules/expo-json-utils": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
|
||||
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ=="
|
||||
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expo-keep-awake": {
|
||||
"version": "15.0.8",
|
||||
@@ -6003,6 +6082,7 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-1.0.10.tgz",
|
||||
"integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config": "~12.0.11",
|
||||
"expo-json-utils": "~0.15.0"
|
||||
@@ -6351,6 +6431,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
|
||||
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
@@ -6543,7 +6624,8 @@
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fb-watchman": {
|
||||
"version": "2.0.2",
|
||||
@@ -6962,6 +7044,7 @@
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
@@ -6969,7 +7052,8 @@
|
||||
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "7.0.2",
|
||||
@@ -7623,7 +7707,8 @@
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
@@ -8086,6 +8171,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
@@ -8784,7 +8870,8 @@
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.3.0",
|
||||
@@ -9132,7 +9219,8 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
@@ -9522,6 +9610,7 @@
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz",
|
||||
"integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
@@ -10122,6 +10211,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -10417,7 +10507,8 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
@@ -10562,7 +10653,8 @@
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stackframe": {
|
||||
"version": "1.3.4",
|
||||
@@ -10595,7 +10687,8 @@
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stream-buffers": {
|
||||
"version": "2.2.0",
|
||||
@@ -10942,13 +11035,15 @@
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -11003,6 +11098,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@@ -11521,6 +11617,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -11595,6 +11692,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -11612,6 +11710,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11638,6 +11737,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -11652,6 +11752,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -11729,6 +11830,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11861,6 +11963,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"start:clean": "expo start -c",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios --scheme \"Hey Mama\"",
|
||||
"ios:clean": "npm run clean:cache && npm run clean:ios-build && expo run:ios --scheme \"Hey Mama\"",
|
||||
"web": "expo start --web",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"clean:cache": "rm -rf node_modules/.cache .expo 2>/dev/null; echo 'Cleared .expo and node_modules/.cache'",
|
||||
"clean:ios-build": "rm -rf ~/Library/Developer/Xcode/DerivedData/client-* 2>/dev/null; echo 'Cleared Xcode DerivedData for client'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MeasureWidthImpl } from './measure/types';
|
||||
import type { ScoreTerm } from './scoring/types';
|
||||
import type { Weights } from './scoring/types';
|
||||
|
||||
export type OverflowMode = 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT';
|
||||
export type LineMode = 'AUTO' | 'FIXED';
|
||||
@@ -26,6 +27,21 @@ export type WrapTextInput = {
|
||||
fontSpec?: Partial<FontSpecInput> | null;
|
||||
/** 可选注入测量实现(APP 场景强烈建议提供;WIDGET 默认不启用) */
|
||||
measureWidthImpl?: MeasureWidthImpl;
|
||||
/**
|
||||
* 可选评分偏好(用于 UI 场景“更好看”的排版风格)。
|
||||
* - 不传:使用算法默认 v1 权重与口径(更贴近文档 10.3)
|
||||
* - 传入:仅在本次 wrapText 调用内生效,不影响全局
|
||||
*/
|
||||
scoringOverrides?: {
|
||||
/** 覆盖/微调默认权重(整数)。建议只改少数项,例如 TC 的标点断行奖励。 */
|
||||
weights?: Partial<Weights>;
|
||||
/** 覆盖理想行宽比例(0~1)。更小会更倾向“提前换行”。 */
|
||||
idealWidthRatio?: Partial<Record<TextWrapContext, number>>;
|
||||
/** 最短偏好比例(相对 idealWidth)。更小会更宽容“短行”。 */
|
||||
minPreferredRatio?: number;
|
||||
/** 末行过短惩罚阈值比例(相对 idealWidth)。更小会更宽容“短末行”。 */
|
||||
shortLastLineRatio?: number;
|
||||
};
|
||||
/**
|
||||
* 测量缓存隔离 key(可选)。
|
||||
* 口径建议:`APP|ios|<scale?>` / `WIDGET|small`。
|
||||
|
||||
@@ -3,6 +3,7 @@ import { normalizeWhitespace, tokenizeEN } from './core/index';
|
||||
import { segmentGraphemes } from './grapheme/index';
|
||||
import { generateBreakpoints } from './breakpoints/index';
|
||||
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from './scoring/index';
|
||||
import { mergeWeights } from './scoring/weights';
|
||||
import { searchBestLayoutApp } from './searchApp/index';
|
||||
import { searchBestLayoutWidget } from './searchWidget/index';
|
||||
import { applyOverflowFallback } from './overflow/index';
|
||||
@@ -45,12 +46,23 @@ export async function wrapText(input: WrapTextInput): Promise<WrapTextOutput> {
|
||||
// scoring:protectedPhrases 从 constraints 注入
|
||||
const lexicons = { ...DEFAULT_LEXICONS, protectedPhrases: input.constraints?.protectedPhrases ?? DEFAULT_LEXICONS.protectedPhrases };
|
||||
const tcPunctuations: string[] = [',', '。', '!', '?', ';', ':', '、'];
|
||||
|
||||
// 评分偏好(可选):用于 UI 侧“更好看”的排版风格微调
|
||||
const scoringOverrides = input.scoringOverrides ?? null;
|
||||
const weights = scoringOverrides?.weights ? mergeWeights(scoringOverrides.weights) : DEFAULT_WEIGHTS;
|
||||
const idealWidthRatio = {
|
||||
APP: scoringOverrides?.idealWidthRatio?.APP ?? 0.9,
|
||||
WIDGET: scoringOverrides?.idealWidthRatio?.WIDGET ?? 0.95,
|
||||
};
|
||||
const scoringConfig = {
|
||||
weights: DEFAULT_WEIGHTS,
|
||||
idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
|
||||
weights,
|
||||
idealWidthRatio,
|
||||
ellipsisToken: '…',
|
||||
tcParticleWhitelist: [],
|
||||
tcPunctuations,
|
||||
// 下面两项默认由 score.ts 内部给出;此处仅在有 overrides 时注入
|
||||
minPreferredRatio: scoringOverrides?.minPreferredRatio,
|
||||
shortLastLineRatio: scoringOverrides?.shortLastLineRatio,
|
||||
};
|
||||
|
||||
// 搜索
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
## 应用文案总表(请在此文件对应的 JSON 中修改)
|
||||
## 應用文案總表(請在對應 JSON 中修改)
|
||||
|
||||
**单一文案源文件**:`client/src/i18n/locales/all.json`
|
||||
### 語言與檔案對應(單一來源,避免多檔覆蓋)
|
||||
|
||||
- **English**:`all.json` 的 `en`
|
||||
- **繁体中文**:`all.json` 的 `zh-TW`
|
||||
| 語言 | 實際使用的檔案 | 說明 |
|
||||
|------------|-----------------------------|------|
|
||||
| **英文** | `locales/all.json` 的 `en` | 只改 all.json 的 `en` 區塊 |
|
||||
| **繁體中文** | `locales/zh-TW.json` | **唯一來源**,Onboarding / 開屏 consent 等繁中文案只改此檔 |
|
||||
|
||||
> 说明:项目运行时只读取 `all.json`;请不要再改 `locales/en.json`、`locales/zh-TW.json`(它们已不再作为运行时数据源)。
|
||||
- 運行時 **繁中(zh-TW)只讀取 `zh-TW.json`**,不會讀取 `all.json` 的 zh-TW 區塊。
|
||||
- 修改 JSON 後需**重新載入 App**(模擬器 `Cmd+R`)或重啟 Metro(必要時加 `--clear`)才會看到新文案。
|
||||
- **若修改 zh-TW.json 後開屏 consent 仍顯示舊文案**:請執行 `npm run clean:cache`,再以 `npm run start:clean` 啟動 Metro(或先刪除模擬器上的 App 再執行 `npm run ios:clean`),確保吃到最新 bundle。
|
||||
|
||||
### 快速索引(高频文案)
|
||||
### 快速索引(高頻文案)
|
||||
|
||||
- **開屏 / 協議**:`consent.*`(**繁中只改 zh-TW.json**:`consent.title`、`consent.subtitle`、`consent.subtitleSecondary`、agree, privacy, terms, notice…)
|
||||
- **Onboarding 問卷**:`onboardingSurvey.steps.*`(name.title, status.title, status.options.* …)
|
||||
- **Onboarding 招呼語**:`onboardingSurvey.greeting`(例:Hi {{name}},)
|
||||
- **Home**:`home.*`
|
||||
- **Push 提示**:`push.*`
|
||||
- **主题**:`theme.*`
|
||||
- **我的/Profile**:`profile.*`
|
||||
- **主題**:`theme.*`
|
||||
- **我的 / Profile**:`profile.*`
|
||||
- **收藏**:`favorites.*`
|
||||
- **设置**:`settings.*`
|
||||
- **Onboarding(问卷)**:`onboardingSurvey.steps.*`
|
||||
- **Onboarding(兴趣)**:`intent.*`
|
||||
- **設定**:`settings.*`
|
||||
- **Mock 文案**:`mock.*`
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ import { initReactI18next } from 'react-i18next';
|
||||
import { isTraditionalChineseLocaleTag } from './locale';
|
||||
|
||||
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
||||
// 繁中(zh-TW)唯一來源:locales/zh-TW.json,修改 Onboarding/開屏等繁中文案請只改該檔案
|
||||
// 本 App 未載入 zh-CN.json;若看到類似「你本就完美」「一切都会变好」等簡中 consent 文案,為舊 bundle 快取導致,請執行 clean:cache + start:clean 或卸載 App 重裝。
|
||||
// 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> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const zhTW = require('./locales/zh-TW.json') as Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 语言码约定:
|
||||
@@ -81,7 +85,8 @@ export async function initI18n(): Promise<void> {
|
||||
|
||||
await i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
'zh-TW': { translation: all['zh-TW'] as any },
|
||||
// 繁中唯一來源:zh-TW.json(all.json 的 zh-TW 區塊不會被載入)
|
||||
'zh-TW': { translation: zhTW },
|
||||
en: { translation: all.en as any },
|
||||
},
|
||||
lng: initialLang,
|
||||
@@ -91,6 +96,18 @@ export async function initI18n(): Promise<void> {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 臨時 debug:確認實際使用的 language 與 consent.title 值(方便驗證繁中來自 zh-TW.json)
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
const consentTitle = i18n.t('consent.title');
|
||||
console.log(
|
||||
'[i18n] 已初始化 language=',
|
||||
i18n.language,
|
||||
'| consent.title=',
|
||||
consentTitle,
|
||||
'| 繁中來源=zh-TW.json,英文來源=all.json 的 en 區塊'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Next",
|
||||
"skip": "Skip",
|
||||
"skipAll": "Skip onboarding",
|
||||
"skipAll": "Skip",
|
||||
"q1Title": "How are you feeling lately?",
|
||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||
"q2Title": "What kind of support do you want?",
|
||||
@@ -25,10 +25,11 @@
|
||||
"q4Desc": "You can skip. We’ll stay with you along the way."
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"greeting": "Hi {{name}},",
|
||||
"steps": {
|
||||
"name": { "title": "What do you want to be called?", "placeholder": "Mama" },
|
||||
"name": { "title": "What should we call you?", "placeholder": "Mama" },
|
||||
"status": {
|
||||
"title": "Which stage of motherhood are you in?",
|
||||
"title": "Where are you at right now?",
|
||||
"options": {
|
||||
"pregnant": "Pregnant / Preparing",
|
||||
"has_kids": "Parenting",
|
||||
@@ -66,7 +67,7 @@
|
||||
"balance": "Rest & balance"
|
||||
}
|
||||
},
|
||||
"reminder": { "title": "How many reminders do you want per day?" }
|
||||
"reminder": { "title": "How often would you like a gentle reminder?" }
|
||||
}
|
||||
},
|
||||
"intent": {
|
||||
@@ -87,7 +88,7 @@
|
||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Like",
|
||||
"dislike": "Dislike",
|
||||
"favorites": "Favorites",
|
||||
@@ -115,6 +116,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Daily Reminder",
|
||||
"timesUnit": "times",
|
||||
"timesUnitSingular": "time",
|
||||
"pushLabel": "Push Reminder",
|
||||
"ok": "Ok",
|
||||
"minus": "Decrease",
|
||||
@@ -125,7 +127,7 @@
|
||||
"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”.",
|
||||
"howToDesc2": "Search “Hey Mama”, choose a widget size you like, then tap “Add Widget”.",
|
||||
"previewDate": "Thu, Jan 29",
|
||||
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
||||
},
|
||||
@@ -139,16 +141,17 @@
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"widgetTitle": "iOS Widget",
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Mindfulness” → add a size you like."
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Hey Mama” → add a size you like."
|
||||
},
|
||||
"consent": {
|
||||
"title": "You Are Perfect.",
|
||||
"subtitle": "Everything\nWill Be Better.",
|
||||
"title": "Hey mama.",
|
||||
"subtitle": "You’re doing okay\nright now.",
|
||||
"subtitleSecondary": "",
|
||||
"agree": "Agree & Continue",
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Use",
|
||||
"notice": "By continuing, you agree to the Privacy Policy and Terms of Use.",
|
||||
"noticeRich": "By continuing, you agree to the <privacy>{{privacyLabel}}{{privacySuffix}}</privacy> and <terms>{{termsLabel}}{{termsSuffix}}</terms>.",
|
||||
"noticeRich": "By continuing,\nyou agree to the <privacy>{{privacyLabel}}{{privacySuffix}}</privacy> and <terms>{{termsLabel}}{{termsSuffix}}</terms>.",
|
||||
"linkUnavailable": "Failed to load the policy link. Please check your network and try again.",
|
||||
"linkUnavailableDev": "Failed to load the policy link. Please check your network or API_BASE_URL: {{baseUrl}}",
|
||||
"linkLoadingSuffix": " (loading…)"
|
||||
@@ -183,7 +186,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳過",
|
||||
"skipAll": "跳過整個引導",
|
||||
"skipAll": "跳過",
|
||||
"q1Title": "你最近的感受更接近哪一種?",
|
||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||
"q2Title": "你更希望獲得哪種支持?",
|
||||
@@ -194,18 +197,19 @@
|
||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"greeting": "Hi {{name}},",
|
||||
"steps": {
|
||||
"name": { "title": "我可以怎麼稱呼你?", "placeholder": "媽媽" },
|
||||
"name": { "title": "怎麼稱呼你呢?", "placeholder": "媽媽" },
|
||||
"status": {
|
||||
"title": "媽媽的狀態?",
|
||||
"title": "你現在正處在哪個階段呢?",
|
||||
"options": {
|
||||
"pregnant": "懷孕中/準備成為媽媽",
|
||||
"pregnant": "懷孕中/正在準備迎接寶寶",
|
||||
"has_kids": "已經有孩子",
|
||||
"no_fill": "不想填寫"
|
||||
"no_fill": "我暫時不想說"
|
||||
}
|
||||
},
|
||||
"emotion": {
|
||||
"title": "當下情緒狀態?",
|
||||
"title": "今天的你,還好嗎?",
|
||||
"options": {
|
||||
"happy": "愉悅、滿足",
|
||||
"calm": "平靜、安穩",
|
||||
@@ -256,7 +260,7 @@
|
||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"title": "Hey Mama",
|
||||
"like": "喜歡",
|
||||
"dislike": "不喜歡",
|
||||
"favorites": "收藏",
|
||||
@@ -284,6 +288,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"timesUnitSingular": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "確定",
|
||||
"minus": "減少次數",
|
||||
@@ -292,9 +297,9 @@
|
||||
"widget": {
|
||||
"lockScreen": "鎖屏小工具",
|
||||
"homeScreen": "桌面小工具",
|
||||
"howToTitle": "如何添加小工具",
|
||||
"howToTitle": "如何加入小工具",
|
||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||
"howToDesc2": "搜尋「正念」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||
},
|
||||
@@ -308,11 +313,12 @@
|
||||
"language": "語言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小工具",
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "你很完美。",
|
||||
"subtitle": "一切\n都會更好。",
|
||||
"title": "我們知道,",
|
||||
"subtitle": "當媽媽很不容易。",
|
||||
"subtitleSecondary": "這裡給你一些溫柔的肯定與提醒",
|
||||
"agree": "同意並繼續",
|
||||
"privacy": "隱私協議",
|
||||
"terms": "用戶使用協議",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Next",
|
||||
"skip": "Skip",
|
||||
"skipAll": "Skip onboarding",
|
||||
"skipAll": "Skip",
|
||||
"q1Title": "How are you feeling lately?",
|
||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||
"q2Title": "What kind of support do you want?",
|
||||
@@ -32,7 +32,7 @@
|
||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Like",
|
||||
"dislike": "Dislike",
|
||||
"favorites": "Favorites",
|
||||
@@ -59,6 +59,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Daily Reminder",
|
||||
"timesUnit": "times",
|
||||
"timesUnitSingular": "time",
|
||||
"pushLabel": "Push Reminder",
|
||||
"ok": "Ok",
|
||||
"minus": "Decrease",
|
||||
@@ -79,7 +80,7 @@
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"widgetTitle": "iOS Widget",
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Mindfulness” → add a size you like."
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Hey Mama” → add a size you like."
|
||||
},
|
||||
"consent": {
|
||||
"title": "You Are Perfect.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Siguiente",
|
||||
"skip": "Saltar",
|
||||
"skipAll": "Saltar introducción",
|
||||
"skipAll": "Saltar",
|
||||
"q1Title": "¿Cómo te sientes últimamente?",
|
||||
"q1Desc": "No hay respuestas correctas. Puedes saltar y ajustar después.",
|
||||
"q2Title": "¿Qué tipo de apoyo quieres?",
|
||||
@@ -30,7 +30,7 @@
|
||||
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Me gusta",
|
||||
"dislike": "No me gusta",
|
||||
"favorites": "Favoritos",
|
||||
@@ -57,6 +57,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Recordatorio diario",
|
||||
"timesUnit": "veces",
|
||||
"timesUnitSingular": "vez",
|
||||
"pushLabel": "Recordatorio Push",
|
||||
"ok": "Ok",
|
||||
"minus": "Disminuir",
|
||||
@@ -77,7 +78,7 @@
|
||||
"language": "Idioma",
|
||||
"version": "Versión",
|
||||
"widgetTitle": "Widget de iOS",
|
||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Mindfulness” → añade el tamaño."
|
||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Hey Mama” → añade el tamaño."
|
||||
},
|
||||
"consent": {
|
||||
"agree": "Aceptar y Continuar",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Próximo",
|
||||
"skip": "Pular",
|
||||
"skipAll": "Pular introdução",
|
||||
"skipAll": "Pular",
|
||||
"q1Title": "Como você tem se sentido ultimamente?",
|
||||
"q1Desc": "Não há certo ou errado. Você pode pular e ajustar depois.",
|
||||
"q2Title": "Que tipo de apoio você quer?",
|
||||
@@ -30,7 +30,7 @@
|
||||
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Curtir",
|
||||
"dislike": "Não curtir",
|
||||
"favorites": "Favoritos",
|
||||
@@ -57,6 +57,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Lembrete diário",
|
||||
"timesUnit": "vezes",
|
||||
"timesUnitSingular": "vez",
|
||||
"pushLabel": "Lembrete Push",
|
||||
"ok": "Ok",
|
||||
"minus": "Diminuir",
|
||||
@@ -77,7 +78,7 @@
|
||||
"language": "Idioma",
|
||||
"version": "Versão",
|
||||
"widgetTitle": "Widget do iOS",
|
||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Mindfulness” → adicione o tamanho."
|
||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Hey Mama” → adicione o tamanho."
|
||||
},
|
||||
"consent": {
|
||||
"agree": "Concordar e Continuar",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳过",
|
||||
"skipAll": "跳过整个引导",
|
||||
"skipAll": "跳过",
|
||||
"q1Title": "你最近的感受更接近哪一种?",
|
||||
"q1Desc": "没有对错,你可以跳过,之后也可以慢慢调整。",
|
||||
"q2Title": "你更希望获得哪种支持?",
|
||||
@@ -33,7 +33,7 @@
|
||||
"errorDesc": "开启失败,请稍后重试(模拟器可能无法获取推送 Token,建议用真机测试)。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"title": "Hey Mama",
|
||||
"like": "点赞",
|
||||
"dislike": "讨厌",
|
||||
"favorites": "收藏",
|
||||
@@ -60,6 +60,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"timesUnitSingular": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "确定",
|
||||
"minus": "减少次数",
|
||||
@@ -80,7 +81,7 @@
|
||||
"language": "语言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小组件",
|
||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“正念” → 添加你喜欢的尺寸。"
|
||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“Hey Mama” → 添加你喜欢的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "你本就完美。",
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
"common": {
|
||||
"ok": "確定",
|
||||
"cancel": "取消",
|
||||
"back": "返回"
|
||||
"back": "返回",
|
||||
"error": "錯誤",
|
||||
"notice": "提示",
|
||||
"openLinkError": "無法打開鏈接",
|
||||
"close": "關閉"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "歡迎",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳過",
|
||||
"skipAll": "跳過整個引導",
|
||||
"skipAll": "跳過",
|
||||
"q1Title": "你最近的感受更接近哪一種?",
|
||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||
"q2Title": "你更希望獲得哪種支持?",
|
||||
@@ -19,6 +23,64 @@
|
||||
"q4Title": "給自己一句溫柔的話",
|
||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"greeting": "Hi {{name}},",
|
||||
"steps": {
|
||||
"name": {
|
||||
"title": "怎麼稱呼你呢?",
|
||||
"placeholder": "媽媽"
|
||||
},
|
||||
"status": {
|
||||
"title": "你現在正處在哪個階段呢?",
|
||||
"options": {
|
||||
"pregnant": "懷孕中/正在準備迎接寶寶",
|
||||
"has_kids": "已經有孩子",
|
||||
"no_fill": "我暫時不想說"
|
||||
}
|
||||
},
|
||||
"emotion": {
|
||||
"title": "今天的你,還好嗎?",
|
||||
"options": {
|
||||
"happy": "愉悅、滿足",
|
||||
"calm": "平靜、安穩",
|
||||
"okay": "還可以、普通",
|
||||
"tired": "疲累、沒什麼力氣",
|
||||
"stressed": "被壓得有點喘不過氣",
|
||||
"low": "情緒低落"
|
||||
}
|
||||
},
|
||||
"influence": {
|
||||
"title": "是什麼影響了你最近的感受?",
|
||||
"options": {
|
||||
"family": "家庭與孩子",
|
||||
"work": "工作或學習",
|
||||
"relationship": "親密關係",
|
||||
"friends": "朋友與人際",
|
||||
"health": "身心健康"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
"title": "最需要什麼支持?",
|
||||
"options": {
|
||||
"emotional": "情緒支持",
|
||||
"parenting": "育兒壓力",
|
||||
"self_worth": "自我價值",
|
||||
"anxiety": "焦慮舒緩",
|
||||
"balance": "休息與平衡"
|
||||
}
|
||||
},
|
||||
"reminder": {
|
||||
"title": "你希望一天收到幾次肯定語?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"intent": {
|
||||
"title": "你希望得到什麼幫助?",
|
||||
"love": "愛情",
|
||||
"life": "生活",
|
||||
"travel": "旅遊",
|
||||
"work": "職場"
|
||||
},
|
||||
"push": {
|
||||
"title": "通知",
|
||||
"cardTitle": "開啟溫柔提醒",
|
||||
@@ -30,7 +92,7 @@
|
||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"title": "Hey Mama",
|
||||
"like": "喜歡",
|
||||
"dislike": "不喜歡",
|
||||
"favorites": "收藏",
|
||||
@@ -41,7 +103,8 @@
|
||||
"theme": {
|
||||
"title": "主題",
|
||||
"scenery": "風景",
|
||||
"color": "顏色"
|
||||
"color": "顏色",
|
||||
"suixin": "隨心"
|
||||
},
|
||||
"profile": {
|
||||
"title": "我的",
|
||||
@@ -57,6 +120,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"timesUnitSingular": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "確定",
|
||||
"minus": "減少次數",
|
||||
@@ -65,24 +129,49 @@
|
||||
"widget": {
|
||||
"lockScreen": "鎖屏小工具",
|
||||
"homeScreen": "桌面小工具",
|
||||
"howToTitle": "如何加入小工具",
|
||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "收藏夾",
|
||||
"empty": "這裡還沒有收藏內容。"
|
||||
"empty": "這裡還沒有收藏內容。",
|
||||
"unknownText": "這條文案暫時無法顯示。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"language": "語言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小工具",
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "我們知道,",
|
||||
"subtitle": "當媽媽很不容易。",
|
||||
"subtitleSecondary": "這裡給你一些溫柔的肯定與提醒",
|
||||
"agree": "同意並繼續",
|
||||
"privacy": "隱私協議",
|
||||
"terms": "用戶使用協議"
|
||||
"terms": "用戶使用協議",
|
||||
"notice": "繼續使用即代表你同意《隱私協議》與《用戶使用協議》。",
|
||||
"noticeRich": "繼續使用即代表你同意<privacy>《{{privacyLabel}}》{{privacySuffix}}</privacy>與<terms>《{{termsLabel}}》{{termsSuffix}}</terms>。",
|
||||
"linkUnavailable": "協議鏈接載入失敗,請檢查網路後重試。",
|
||||
"linkUnavailableDev": "協議鏈接載入失敗,請檢查網路或 API_BASE_URL 設定:{{baseUrl}}",
|
||||
"linkLoadingSuffix": "(載入中…)"
|
||||
},
|
||||
"permissions": {
|
||||
"notificationsDenied": "系統權限已被拒絕,請前往手機設定開啟通知。"
|
||||
},
|
||||
"language": {
|
||||
"zhTW": "繁體中文",
|
||||
"en": "English"
|
||||
},
|
||||
"mock": {
|
||||
"c1": "你已經很努力了,今天也值得被溫柔對待。",
|
||||
"c2": "深呼吸三次,把注意力帶回當下。",
|
||||
"c3": "允許自己慢一點,情緒會像雲一樣飄過。",
|
||||
"c4": "你不需要完美,你已經足夠好。",
|
||||
"c5": "把手放在心口,對自己說一句:辛苦了。"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { fetchRecoWidget } from '@/src/services/recoApi';
|
||||
import i18n from 'i18next';
|
||||
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
||||
import { getLocalDayKey } from '@/src/utils/date';
|
||||
import { wrapText } from '@/src/features/textWrap';
|
||||
|
||||
import {
|
||||
appGroupGetString,
|
||||
@@ -40,6 +41,12 @@ export type WidgetDailyRecoV1 = {
|
||||
item: null | {
|
||||
content_id: number;
|
||||
text: string;
|
||||
/**
|
||||
* 预换行文案(由 App 侧使用 Text Wrap 算法生成,写入 App Group,供 Widget 直接渲染)。
|
||||
* - key 以 WidgetFamily 归一化:small/medium/large
|
||||
* - 值使用 `\n` 分行;Widget SwiftUI 的 Text 会按换行符显示
|
||||
*/
|
||||
wrapped_text_by_family?: Partial<Record<'small' | 'medium' | 'large', string>>;
|
||||
final_score?: number;
|
||||
fallback_level_final?: number;
|
||||
};
|
||||
@@ -56,6 +63,58 @@ function safeJsonParse<T>(raw: string | null): T | null {
|
||||
}
|
||||
}
|
||||
|
||||
type WidgetFamilyKey = 'small' | 'medium' | 'large';
|
||||
|
||||
function widgetPreset(family: WidgetFamilyKey) {
|
||||
// 与 iOS Widget(`EmotionWidget.swift`)保持一致的显示口径(字体/行数/内边距)
|
||||
// 注意:这里的 widthPt 是“常见 iPhone widget 尺寸”的近似值,用于把 pt 宽度换算为“可容纳 token 数”
|
||||
// 真实渲染仍由系统决定,但这个近似能让算法在不同 family 下更稳定地产出更好看的换行。
|
||||
switch (family) {
|
||||
case 'small':
|
||||
return { widthPt: 155, padding: 14, fontSize: 16, maxLines: 5 };
|
||||
case 'medium':
|
||||
return { widthPt: 329, padding: 16, fontSize: 18, maxLines: 6 };
|
||||
case 'large':
|
||||
return { widthPt: 329, padding: 18, fontSize: 22, maxLines: 8 };
|
||||
}
|
||||
}
|
||||
|
||||
function approxCapacityFromPt(args: { lang: 'EN' | 'TC'; usablePt: number; fontSize: number }): number {
|
||||
// 与 wrapText(APP 近似降级) 的换算口径一致:把像素/pt 宽度近似换成“可容纳 token 数”
|
||||
const usable = Math.max(0, args.usablePt);
|
||||
const fs = Math.max(1, Math.round(args.fontSize));
|
||||
const denom = args.lang === 'EN' ? Math.max(1, Math.round(fs * 0.55)) : Math.max(1, Math.round(fs * 0.95));
|
||||
return Math.max(1, Math.floor(usable / denom));
|
||||
}
|
||||
|
||||
async function buildWrappedTextByFamily(args: { text: string; lang: 'EN' | 'TC' }): Promise<Record<WidgetFamilyKey, string>> {
|
||||
const families: WidgetFamilyKey[] = ['small', 'medium', 'large'];
|
||||
const out: Partial<Record<WidgetFamilyKey, string>> = {};
|
||||
|
||||
for (const f of families) {
|
||||
const p = widgetPreset(f);
|
||||
const usablePt = Math.max(0, p.widthPt - p.padding * 2);
|
||||
const capacity = approxCapacityFromPt({ lang: args.lang, usablePt, fontSize: p.fontSize });
|
||||
|
||||
const res = await wrapText({
|
||||
text: args.text,
|
||||
lang: args.lang,
|
||||
context: 'WIDGET',
|
||||
availableWidth: capacity,
|
||||
maxLines: p.maxLines,
|
||||
overflowMode: 'ELLIPSIS',
|
||||
lineMode: 'AUTO',
|
||||
configVersion: 'v1-widget',
|
||||
debug: false,
|
||||
// Widget 侧不依赖真实测量:默认 APPROX 即可(确保可在 Extension 独立工作)
|
||||
});
|
||||
|
||||
out[f] = res.wrappedText;
|
||||
}
|
||||
|
||||
return out as Record<WidgetFamilyKey, string>;
|
||||
}
|
||||
|
||||
function pickUserProfileV1_2(scoringProfile: UserProfileV1_2_Extended): UserProfileV1_2 {
|
||||
return {
|
||||
profile_version: scoringProfile.profile_version,
|
||||
@@ -126,7 +185,29 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
|
||||
const today = getLocalDayKey(new Date());
|
||||
const cached = await getWidgetDailyRecoCache();
|
||||
if (cached?.schema_version === 1 && cached.day_key === today && cached.item?.text) return;
|
||||
// 若今日已有缓存,但缺少预换行字段:补齐后触发 reload(不必请求后端)
|
||||
if (cached?.schema_version === 1 && cached.day_key === today && cached.item?.text) {
|
||||
const hasWrapped = Boolean(cached.item.wrapped_text_by_family && Object.keys(cached.item.wrapped_text_by_family).length > 0);
|
||||
if (hasWrapped) return;
|
||||
|
||||
const wrapLang: 'EN' | 'TC' = cached.lang === 'en' ? 'EN' : 'TC';
|
||||
try {
|
||||
const wrapped = await buildWrappedTextByFamily({ text: cached.item.text, lang: wrapLang });
|
||||
await setWidgetDailyRecoCache({
|
||||
...cached,
|
||||
saved_at: new Date().toISOString(),
|
||||
item: { ...cached.item, wrapped_text_by_family: wrapped },
|
||||
source: cached.source ?? 'app',
|
||||
});
|
||||
await appGroupReloadAllTimelines();
|
||||
} catch (e) {
|
||||
// 预换行失败不阻塞;Widget 仍可用原文 + 系统换行
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
console.log('[DailyWidgetReco] 预换行补齐失败:', args?.reason ?? 'unknown', e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const scoringProfile = args?.scoringProfile ?? (await getUserProfileScoring());
|
||||
if (!scoringProfile) return;
|
||||
@@ -146,6 +227,8 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
if (!top?.text) return;
|
||||
|
||||
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
||||
const wrapLang: 'EN' | 'TC' = lang === 'en' ? 'EN' : 'TC';
|
||||
const wrapped = await buildWrappedTextByFamily({ text: top.text, lang: wrapLang });
|
||||
await setWidgetDailyRecoCache({
|
||||
schema_version: 1,
|
||||
saved_at: new Date().toISOString(),
|
||||
@@ -155,6 +238,7 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
item: {
|
||||
content_id: top.content_id,
|
||||
text: top.text,
|
||||
wrapped_text_by_family: wrapped,
|
||||
final_score: top.final_score,
|
||||
fallback_level_final: top.fallback_level_final,
|
||||
},
|
||||
|
||||
31
spec_kit/Splash Consent/overflow.md
Normal file
31
spec_kit/Splash Consent/overflow.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Splash Consent 補充說明
|
||||
|
||||
## i18n 開屏 consent 文案不生效(排查與修復記錄)
|
||||
|
||||
### 現象
|
||||
- iOS 模擬器繁中開屏一直顯示舊文案(如「你很完美。」「一切 都會更好。」)
|
||||
- 修改 zh-TW.json 的 consent.title/subtitle 後重跑 `npm run ios` 仍不生效
|
||||
- 專案內搜尋「你很完美」找不到(舊文案來自快取)
|
||||
|
||||
### 排查結論
|
||||
|
||||
1. **舊文案來源**
|
||||
- 專案內**沒有**「你很完美」「一切 都會更好」的完整句
|
||||
- `zh-CN.json` 有近似句「你本就完美。」「一切都会变好。」,但 **App 的 i18n 未載入 zh-CN**,僅載入 `zh-TW`(zh-TW.json)與 `en`(all.json 的 en 區塊)
|
||||
- 結論:舊文案來自 **Metro / JS bundle 或 iOS 建置快取**(曾打包進去的舊 JSON)
|
||||
|
||||
2. **語言與載入順序**(`client/src/i18n/index.ts`)
|
||||
- `resources`:`zh-TW` → `zh-TW.json`;`en` → `all.json` 的 `en`
|
||||
- **all.json 的 zh-TW 區塊不會被載入**,繁中唯一來源為 `zh-TW.json`
|
||||
- 裝置語言經 `expo-localization.getLocales()[0].languageTag` 取得,經 `normalizeDeviceLanguageTagToAppLanguage` 對應到 `zh-TW` 或 `en`(zh-Hant / zh-TW / zh-HK 等均對應 zh-TW)
|
||||
|
||||
3. **修復與預防**
|
||||
- 已加臨時 debug log:i18n 初始化與開屏 consent 畫面會印出 `language` 與 `consent.title`(僅 __DEV__)
|
||||
- 已加 `clean:cache`、`start:clean`、`ios:clean` script;若改 zh-TW 仍不生效,請執行清理後重啟或卸載 App 重裝
|
||||
- 繁中 consent 文案**只改** `client/src/i18n/locales/zh-TW.json` 的 `consent.title`、`consent.subtitle`、`consent.subtitleSecondary`
|
||||
|
||||
### 修改的檔案(本次修復)
|
||||
- `client/src/i18n/index.ts`:註解 + __DEV__ 下印出 language 與 consent.title
|
||||
- `client/app/(splash)/splash.tsx`:useTranslation 取 i18n + __DEV__ 下印出 consent 畫面時的 language / title / subtitle
|
||||
- `client/package.json`:`start:clean`、`ios:clean`、`clean:cache`
|
||||
- `client/src/i18n/ALL_COPY.md`:故障排除與 consent 繁中只改 zh-TW.json 的說明
|
||||
Reference in New Issue
Block a user