Compare commits
8 Commits
damer
...
1fbc0aa3f8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fbc0aa3f8 | ||
|
|
b5532df161 | ||
| 5515726465 | |||
|
|
ce018880f4 | ||
|
|
b4ec17fcac | ||
|
|
aa4e1e9947 | ||
| 4578d503e7 | |||
| 66241e5231 |
@@ -55,6 +55,7 @@ import { getBootId } from '@/src/utils/bootSession';
|
|||||||
import { advanceSuixinState, buildInitialSuixinState, NEUTRAL_THEME_COLORS } from '@/src/features/suixinTheme';
|
import { advanceSuixinState, buildInitialSuixinState, NEUTRAL_THEME_COLORS } from '@/src/features/suixinTheme';
|
||||||
import { wrapText } from '@/src/features/textWrap';
|
import { wrapText } from '@/src/features/textWrap';
|
||||||
import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure';
|
import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure';
|
||||||
|
import { ensureDailyWidgetRecoUpToDate } from '@/src/modules/dailyWidgetReco';
|
||||||
|
|
||||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -112,6 +113,18 @@ export default function HomeScreen() {
|
|||||||
const [cardWidth, setCardWidth] = useState<number | null>(null);
|
const [cardWidth, setCardWidth] = useState<number | null>(null);
|
||||||
const [wrappedText, setWrappedText] = useState<string>('');
|
const [wrappedText, setWrappedText] = useState<string>('');
|
||||||
const wrapLogRef = useRef<{ key: string } | null>(null);
|
const wrapLogRef = useRef<{ key: string } | null>(null);
|
||||||
|
const busyRef = useRef(false);
|
||||||
|
const indexRef = useRef(0);
|
||||||
|
const currentFeedRef = useRef<FeedItem[]>([]);
|
||||||
|
const likedIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
const likeInFlightRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
busyRef.current = busy;
|
||||||
|
}, [busy]);
|
||||||
|
useEffect(() => {
|
||||||
|
indexRef.current = index;
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
// 解决语言切换时重复触发拉取/清空导致“文案不停跳动”的问题:
|
// 解决语言切换时重复触发拉取/清空导致“文案不停跳动”的问题:
|
||||||
// 用 ref 持有最新状态,避免 useCallback 依赖 feedItems/isFetching 造成函数 identity 变化 → effect 重复执行
|
// 用 ref 持有最新状态,避免 useCallback 依赖 feedItems/isFetching 造成函数 identity 变化 → effect 重复执行
|
||||||
@@ -182,6 +195,9 @@ export default function HomeScreen() {
|
|||||||
text: t(item.textKey)
|
text: t(item.textKey)
|
||||||
}));
|
}));
|
||||||
}, [feedItems, t]);
|
}, [feedItems, t]);
|
||||||
|
useEffect(() => {
|
||||||
|
currentFeedRef.current = currentFeed;
|
||||||
|
}, [currentFeed]);
|
||||||
|
|
||||||
const item = useMemo(() => {
|
const item = useMemo(() => {
|
||||||
const data = currentFeed[index % currentFeed.length];
|
const data = currentFeed[index % currentFeed.length];
|
||||||
@@ -265,11 +281,24 @@ export default function HomeScreen() {
|
|||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflowMode: 'CLIP',
|
overflowMode: 'CLIP',
|
||||||
lineMode: 'AUTO',
|
lineMode: 'AUTO',
|
||||||
configVersion: 'v1',
|
// Home:采用“更偏好语气停顿/更好看”的排版风格微调(不影响算法默认 v1)
|
||||||
|
configVersion: 'v1-home',
|
||||||
debug: __DEV__,
|
debug: __DEV__,
|
||||||
fontSpec,
|
fontSpec,
|
||||||
contextProfile: `APP|${Platform.OS}|home|${lang}`,
|
contextProfile: `APP|${Platform.OS}|home|${lang}`,
|
||||||
measureWidthImpl: defaultMeasureWidthImpl,
|
measureWidthImpl: defaultMeasureWidthImpl,
|
||||||
|
scoringOverrides:
|
||||||
|
lang === 'TC'
|
||||||
|
? {
|
||||||
|
// 更偏好在逗号/句号等处断行(即便宽度允许也不一定要塞满)
|
||||||
|
weights: { R_PUNCT_BREAK: 180 },
|
||||||
|
// 让“理想行宽”更短,避免宽屏下过度延后断行
|
||||||
|
idealWidthRatio: { APP: 0.82 },
|
||||||
|
// 更宽容短行(尤其是第一行在标点处停顿)
|
||||||
|
minPreferredRatio: 0.45,
|
||||||
|
shortLastLineRatio: 0.45,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -377,6 +406,11 @@ export default function HomeScreen() {
|
|||||||
setIndex(0);
|
setIndex(0);
|
||||||
fetchNewFeed();
|
fetchNewFeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Widget:前台辅助刷新(尽力而为)
|
||||||
|
// - 写入 App Group 的 dailyReco 缓存
|
||||||
|
// - 生成 wrapped_text_by_family,供 Widget 直接渲染
|
||||||
|
ensureDailyWidgetRecoUpToDate({ reason: 'home_focus' }).catch(() => {});
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -411,24 +445,60 @@ export default function HomeScreen() {
|
|||||||
transform: [{ scale: likeScale.value }],
|
transform: [{ scale: likeScale.value }],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const setBusySafe = useCallback((next: boolean) => {
|
||||||
|
busyRef.current = next;
|
||||||
|
setBusy(next);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setLikeInFlight = useCallback((next: boolean) => {
|
||||||
|
likeInFlightRef.current = next;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const syncLikeFilledByIndex = useCallback((nextIndex: number) => {
|
||||||
|
const list = currentFeedRef.current;
|
||||||
|
const len = list.length;
|
||||||
|
if (!len) {
|
||||||
|
setLikeFilled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const safe = ((nextIndex % len) + len) % len;
|
||||||
|
const nextId = String(list[safe]?.content_id);
|
||||||
|
setLikeFilled(likedIdsRef.current.has(nextId));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyIndexChange = useCallback((nextIndex: number) => {
|
||||||
|
indexRef.current = nextIndex;
|
||||||
|
setIndex(nextIndex);
|
||||||
|
syncLikeFilledByIndex(nextIndex);
|
||||||
|
}, [syncLikeFilledByIndex]);
|
||||||
|
|
||||||
|
const maybeFetchNewFeedIfNeeded = useCallback((nextIndex: number) => {
|
||||||
|
const len = currentFeedRef.current.length;
|
||||||
|
if (!len) return;
|
||||||
|
// 当接近当前列表末尾时(例如还剩 5 条)提前拉取
|
||||||
|
if (nextIndex + 5 >= len && !isFetchingRef.current) {
|
||||||
|
fetchNewFeed();
|
||||||
|
}
|
||||||
|
}, [fetchNewFeed]);
|
||||||
|
|
||||||
// 切换到下一条文案的统一动画逻辑
|
// 切换到下一条文案的统一动画逻辑
|
||||||
const triggerNextContent = useCallback(() => {
|
const triggerNextContent = useCallback(() => {
|
||||||
if (busy) return;
|
if (busyRef.current) return;
|
||||||
setBusy(true);
|
setBusySafe(true);
|
||||||
|
|
||||||
|
// 注意:不要在 Reanimated worklet 回调里读取 React ref(例如 indexRef/currentFeedRef),会导致值不更新或异常
|
||||||
|
const nextIndex = indexRef.current + 1;
|
||||||
|
|
||||||
// 1. 当前文案向上移动并消失
|
// 1. 当前文案向上移动并消失
|
||||||
translateY.value = withTiming(-40, { duration: 300, easing: Easing.out(Easing.quad) });
|
translateY.value = withTiming(-40, { duration: 300, easing: Easing.out(Easing.quad) });
|
||||||
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
|
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
// 2. 切换数据索引
|
// 2. 切换数据索引
|
||||||
runOnJS(setIndex)(index + 1);
|
runOnJS(applyIndexChange)(nextIndex);
|
||||||
runOnJS(setLikeFilled)(false);
|
|
||||||
runOnJS(advanceSuixinOnNextContent)();
|
runOnJS(advanceSuixinOnNextContent)();
|
||||||
|
|
||||||
// 检查是否需要拉取新文案(当接近当前列表末尾时,例如还剩 5 条)
|
// 检查是否需要拉取新文案(注意:不要把匿名函数塞进 runOnJS,可能导致原生崩溃)
|
||||||
if (index + 5 >= currentFeed.length && !isFetching) {
|
runOnJS(maybeFetchNewFeedIfNeeded)(nextIndex);
|
||||||
runOnJS(fetchNewFeed)();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 准备下一条文案:先瞬移到下方 40pt
|
// 3. 准备下一条文案:先瞬移到下方 40pt
|
||||||
translateY.value = 40;
|
translateY.value = 40;
|
||||||
@@ -437,20 +507,51 @@ export default function HomeScreen() {
|
|||||||
translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) });
|
translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) });
|
||||||
opacity.value = withTiming(1, { duration: 400 }, (finished) => {
|
opacity.value = withTiming(1, { duration: 400 }, (finished) => {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
runOnJS(setBusy)(false);
|
runOnJS(setBusySafe)(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [busy, index, currentFeed.length, isFetching, fetchNewFeed, translateY, opacity]);
|
}, [applyIndexChange, setBusySafe, translateY, opacity, advanceSuixinOnNextContent, maybeFetchNewFeedIfNeeded]);
|
||||||
|
|
||||||
|
// 切换到上一条文案的统一动画逻辑(下滑触发)
|
||||||
|
const triggerPrevContent = useCallback(() => {
|
||||||
|
if (busyRef.current) return;
|
||||||
|
setBusySafe(true);
|
||||||
|
|
||||||
|
// 注意:同上,不要在 worklet 里读取 React ref
|
||||||
|
const len = currentFeedRef.current.length;
|
||||||
|
const raw = indexRef.current - 1;
|
||||||
|
const nextIndex = len ? ((raw % len) + len) % len : Math.max(0, raw);
|
||||||
|
|
||||||
|
// 1. 当前文案向下移动并消失
|
||||||
|
translateY.value = withTiming(40, { duration: 300, easing: Easing.out(Easing.quad) });
|
||||||
|
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
|
||||||
|
if (finished) {
|
||||||
|
// 2. 切换数据索引(循环回退)
|
||||||
|
runOnJS(applyIndexChange)(nextIndex);
|
||||||
|
|
||||||
|
// 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(setBusySafe)(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [applyIndexChange, setBusySafe, translateY, opacity]);
|
||||||
|
|
||||||
const lastTapRef = useRef<number>(0);
|
const lastTapRef = useRef<number>(0);
|
||||||
|
|
||||||
// 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function
|
// 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function
|
||||||
const handlersRef = useRef({ onPressLike, triggerNextContent });
|
const handlersRef = useRef({ onPressLike, triggerNextContent, triggerPrevContent });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handlersRef.current = { onPressLike, triggerNextContent };
|
handlersRef.current = { onPressLike, triggerNextContent, triggerPrevContent };
|
||||||
}, [onPressLike, triggerNextContent]);
|
}, [onPressLike, triggerNextContent, triggerPrevContent]);
|
||||||
|
|
||||||
// 使用系统自带的 PanResponder 代替第三方手势库
|
// 使用系统自带的 PanResponder 代替第三方手势库
|
||||||
const panResponder = useRef(
|
const panResponder = useRef(
|
||||||
@@ -475,16 +576,32 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
lastTapRef.current = now;
|
lastTapRef.current = now;
|
||||||
|
|
||||||
// 2. 上滑逻辑判定
|
// 2. 上滑/下滑逻辑判定
|
||||||
if (gestureState.dy < -50) { // 上滑超过 50pt
|
if (gestureState.dy < -50) { // 上滑超过 50pt
|
||||||
runOnJS(handlersRef.current.triggerNextContent)();
|
runOnJS(handlersRef.current.triggerNextContent)();
|
||||||
|
} else if (gestureState.dy > 50) { // 下滑超过 50pt
|
||||||
|
runOnJS(handlersRef.current.triggerPrevContent)();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
).current;
|
).current;
|
||||||
|
|
||||||
async function onPressLike() {
|
async function onPressLike() {
|
||||||
if (busy) return;
|
if (busyRef.current) return;
|
||||||
|
if (likeInFlightRef.current) return;
|
||||||
|
|
||||||
|
// 已经喜欢过:不重复写入收藏,直接当作“下一条”
|
||||||
|
if (likeFilled || likedIdsRef.current.has(item.id)) {
|
||||||
|
triggerNextContent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
likeInFlightRef.current = true;
|
||||||
|
|
||||||
|
const likedItemId = item.id;
|
||||||
|
const likedItemText = item.text;
|
||||||
|
// 先记下“已喜欢”,保证回退时能恢复点亮状态(即便异步保存稍后才完成)
|
||||||
|
likedIdsRef.current.add(likedItemId);
|
||||||
setLikeFilled(true);
|
setLikeFilled(true);
|
||||||
|
|
||||||
// 1. 获取当前日期
|
// 1. 获取当前日期
|
||||||
@@ -494,24 +611,33 @@ export default function HomeScreen() {
|
|||||||
// 2. 保存到收藏夹,包含当前背景信息
|
// 2. 保存到收藏夹,包含当前背景信息
|
||||||
const favItem = {
|
const favItem = {
|
||||||
favId: String(Date.now()), // 生成唯一 ID
|
favId: String(Date.now()), // 生成唯一 ID
|
||||||
id: item.id,
|
id: likedItemId,
|
||||||
text: item.text,
|
text: likedItemText,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
|
background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
|
||||||
};
|
};
|
||||||
console.log('Home: Triggering addFavorite', JSON.stringify(favItem));
|
console.log('Home: Triggering addFavorite', JSON.stringify(favItem));
|
||||||
|
try {
|
||||||
await addFavorite(favItem);
|
await addFavorite(favItem);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Home: addFavorite 失败', error);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 记录到后端 Reaction(喜欢)
|
// 3. 记录到后端 Reaction(喜欢)
|
||||||
console.log('Home: Triggering setReaction', item.id);
|
console.log('Home: Triggering setReaction', item.id);
|
||||||
|
try {
|
||||||
await setReaction(item.id, 'like');
|
await setReaction(item.id, 'like');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Home: setReaction 失败', error);
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 爱心缩放动画
|
// 4. 爱心缩放动画
|
||||||
likeScale.value = withSequence(
|
likeScale.value = withSequence(
|
||||||
withTiming(0.8, { duration: 100 }),
|
withTiming(0.8, { duration: 100 }),
|
||||||
withTiming(1.2, { duration: 150 }),
|
withTiming(1.2, { duration: 150 }),
|
||||||
withTiming(1, { duration: 100 }, (finished) => {
|
withTiming(1, { duration: 100 }, (finished) => {
|
||||||
|
runOnJS(setLikeInFlight)(false);
|
||||||
if (finished) {
|
if (finished) {
|
||||||
console.log('Home: Like animation finished, triggering next content');
|
console.log('Home: Like animation finished, triggering next content');
|
||||||
runOnJS(triggerNextContent)();
|
runOnJS(triggerNextContent)();
|
||||||
@@ -549,13 +675,13 @@ export default function HomeScreen() {
|
|||||||
onPress={() => setThemeOpen(true)}
|
onPress={() => setThemeOpen(true)}
|
||||||
accessibilityLabel={t('home.theme')}
|
accessibilityLabel={t('home.theme')}
|
||||||
>
|
>
|
||||||
<ThemeIcon width={18} height={18} />
|
<ThemeIcon width={20} height={20} />
|
||||||
</CircleIconButton>
|
</CircleIconButton>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
onPress={() => setProfileOpen(true)}
|
onPress={() => setProfileOpen(true)}
|
||||||
accessibilityLabel={t('home.profile')}
|
accessibilityLabel={t('home.profile')}
|
||||||
>
|
>
|
||||||
<MyIcon width={18} height={18} />
|
<MyIcon width={20} height={20} />
|
||||||
</CircleIconButton>
|
</CircleIconButton>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -577,15 +703,16 @@ export default function HomeScreen() {
|
|||||||
onPress={onPressLike}
|
onPress={onPressLike}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={t('home.like')}
|
accessibilityLabel={t('home.like')}
|
||||||
hitSlop={20}
|
// 稍微增大可点击区域,提升单手操作成功率
|
||||||
|
hitSlop={24}
|
||||||
style={styles.reactionInner}
|
style={styles.reactionInner}
|
||||||
>
|
>
|
||||||
{likeFilled ? (
|
{likeFilled ? (
|
||||||
<LikeFilledIcon width={35} height={36} color="#EA6969" />
|
<LikeFilledIcon width={40} height={41} color="#EA6969" />
|
||||||
) : (
|
) : (
|
||||||
<LikeIcon
|
<LikeIcon
|
||||||
width={35}
|
width={40}
|
||||||
height={36}
|
height={41}
|
||||||
color={themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28'}
|
color={themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -615,7 +742,8 @@ function CircleIconButton({
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
hitSlop={10}
|
// 稍微增大可点击区域,提升易用性
|
||||||
|
hitSlop={14}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
style={styles.circleBtn}
|
style={styles.circleBtn}
|
||||||
@@ -640,9 +768,9 @@ const styles = StyleSheet.create({
|
|||||||
zIndex: 30,
|
zIndex: 30,
|
||||||
},
|
},
|
||||||
circleBtn: {
|
circleBtn: {
|
||||||
width: 34,
|
width: 40,
|
||||||
height: 34,
|
height: 40,
|
||||||
borderRadius: 17,
|
borderRadius: 20,
|
||||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default function OnboardingScreen() {
|
|||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [selections, setSelections] = useState<Record<string, string[]>>({});
|
const [selections, setSelections] = useState<Record<string, string[]>>({});
|
||||||
const [reminderTimes, setReminderTimes] = useState(3);
|
const [reminderTimes, setReminderTimes] = useState(3);
|
||||||
|
const [finishing, setFinishing] = useState(false);
|
||||||
|
|
||||||
const currentStep = STEPS[stepIndex];
|
const currentStep = STEPS[stepIndex];
|
||||||
const currentTitle = useMemo(() => t(`onboardingSurvey.steps.${currentStep.id}.title`), [t, currentStep.id]);
|
const currentTitle = useMemo(() => t(`onboardingSurvey.steps.${currentStep.id}.title`), [t, currentStep.id]);
|
||||||
@@ -56,6 +57,9 @@ export default function OnboardingScreen() {
|
|||||||
}, [t, currentStep]);
|
}, [t, currentStep]);
|
||||||
|
|
||||||
async function onFinish() {
|
async function onFinish() {
|
||||||
|
if (finishing) return;
|
||||||
|
setFinishing(true);
|
||||||
|
try {
|
||||||
// 用户选择每日次数 > 0:在此页直接触发系统通知权限(已移除单独的 push 引导页)。
|
// 用户选择每日次数 > 0:在此页直接触发系统通知权限(已移除单独的 push 引导页)。
|
||||||
const wantsPush = reminderTimes > 0;
|
const wantsPush = reminderTimes > 0;
|
||||||
|
|
||||||
@@ -159,9 +163,17 @@ export default function OnboardingScreen() {
|
|||||||
} finally {
|
} finally {
|
||||||
router.replace('/(app)/home');
|
router.replace('/(app)/home');
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 极端情况下(例如本地存储/初始化异常)避免卡死在 loading:提示并允许用户重试
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.warn('[OnboardingFinish] 异常:', msg);
|
||||||
|
Alert.alert(t('push.errorTitle'), t('push.errorDesc'));
|
||||||
|
setFinishing(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNext = () => {
|
const onNext = () => {
|
||||||
|
if (finishing) return;
|
||||||
if (stepIndex < STEPS.length - 1) {
|
if (stepIndex < STEPS.length - 1) {
|
||||||
setStepIndex(stepIndex + 1);
|
setStepIndex(stepIndex + 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -170,23 +182,24 @@ export default function OnboardingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onBack = () => {
|
const onBack = () => {
|
||||||
|
if (finishing) return;
|
||||||
if (stepIndex > 0) {
|
if (stepIndex > 0) {
|
||||||
setStepIndex(stepIndex - 1);
|
setStepIndex(stepIndex - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSkip = async () => {
|
/** 只跳過當前這一步(不填/不選當前題,進入下一步) */
|
||||||
// 跳过整个 Onboarding:仍生成一个“全跳过”的最小画像,保证下游可用
|
const handleSkipCurrentStep = () => {
|
||||||
const scoringProfile = buildUserProfileFromQuestionnaire({});
|
if (finishing) return;
|
||||||
await setUserProfileScoring(scoringProfile);
|
if (currentStep.type === 'name') {
|
||||||
|
onNext();
|
||||||
// 同步到 App Group:供 iOS Widget 使用(失败不阻塞)
|
} else if (currentStep.type === 'selection') {
|
||||||
await syncWidgetConfig();
|
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
|
||||||
await syncWidgetUserProfileFromScoring(scoringProfile);
|
onNext();
|
||||||
|
} else if (currentStep.type === 'reminder') {
|
||||||
// 标记已完成,避免下次启动再次进入 Onboarding
|
setReminderTimes(0);
|
||||||
await setOnboardingCompleted(true);
|
onFinish();
|
||||||
router.replace('/(app)/home');
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 题目为多选:点击切换选中状态
|
// 题目为多选:点击切换选中状态
|
||||||
@@ -201,6 +214,7 @@ export default function OnboardingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSkipStep = () => {
|
const handleSkipStep = () => {
|
||||||
|
if (finishing) return;
|
||||||
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
|
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
|
||||||
onNext();
|
onNext();
|
||||||
};
|
};
|
||||||
@@ -210,9 +224,10 @@ export default function OnboardingScreen() {
|
|||||||
title={currentTitle}
|
title={currentTitle}
|
||||||
currentStep={stepIndex}
|
currentStep={stepIndex}
|
||||||
totalSteps={STEPS.length - 1}
|
totalSteps={STEPS.length - 1}
|
||||||
onSkip={onSkip}
|
onSkip={handleSkipCurrentStep}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
showBackButton={stepIndex > 0}
|
showBackButton={stepIndex > 0}
|
||||||
|
userName={name}
|
||||||
>
|
>
|
||||||
{currentStep.type === 'name' && (
|
{currentStep.type === 'name' && (
|
||||||
<NameInputStep
|
<NameInputStep
|
||||||
@@ -233,9 +248,10 @@ export default function OnboardingScreen() {
|
|||||||
|
|
||||||
{currentStep.type === 'reminder' && (
|
{currentStep.type === 'reminder' && (
|
||||||
<ReminderStep
|
<ReminderStep
|
||||||
value={reminderTimes}
|
value={Math.max(1, reminderTimes)}
|
||||||
onChange={setReminderTimes}
|
onChange={setReminderTimes}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
loading={finishing}
|
||||||
onSkip={() => {
|
onSkip={() => {
|
||||||
// 跳过每日提醒:视为 0 次(关闭)
|
// 跳过每日提醒:视为 0 次(关闭)
|
||||||
setReminderTimes(0);
|
setReminderTimes(0);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { setConsentAccepted, getConsentAccepted } from '../../src/storage/appSto
|
|||||||
import { fetchLegalLinks } from '@/src/services/legalApi';
|
import { fetchLegalLinks } from '@/src/services/legalApi';
|
||||||
import { getOnboardingCompleted } from '@/src/storage/appStorage';
|
import { getOnboardingCompleted } from '@/src/storage/appStorage';
|
||||||
import { API_BASE_URL } from '@/src/constants/env';
|
import { API_BASE_URL } from '@/src/constants/env';
|
||||||
|
import { isTraditionalChineseLocaleTag } from '@/src/i18n/locale';
|
||||||
|
|
||||||
// 导入 SVG 组件
|
// 导入 SVG 组件
|
||||||
import FlowersBg from '../../assets/images/index/flowers_endbg.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');
|
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() {
|
export default function SplashScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const [showConsent, setShowConsent] = useState(false);
|
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 [links, setLinks] = useState<{ privacy?: string; terms?: string }>({});
|
||||||
const [linksLoading, setLinksLoading] = useState(false);
|
const [linksLoading, setLinksLoading] = useState(false);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
@@ -126,13 +147,16 @@ export default function SplashScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 文案内容 */}
|
{/* 文案内容:主標題兩行 + 可選二級標題(字號更小、顏色更淺);繁中為元件內常數,其餘用 i18n */}
|
||||||
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
|
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
|
||||||
<Text style={styles.titleText}>
|
<Text style={styles.titleText}>
|
||||||
{t('consent.title')}
|
{title}
|
||||||
{'\n'}
|
{'\n'}
|
||||||
{t('consent.subtitle')}
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
|
{subtitleSecondary ? (
|
||||||
|
<Text style={styles.consentSubtitleSecondary}>{subtitleSecondary}</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
|
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
|
||||||
@@ -213,6 +237,14 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
|
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: {
|
bottomContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 60,
|
bottom: 60,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useColorScheme } from '@/components/useColorScheme';
|
|||||||
import { initI18n } from '@/src/i18n';
|
import { initI18n } from '@/src/i18n';
|
||||||
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromStorage } from '@/src/modules/dailyWidgetReco';
|
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromStorage } from '@/src/modules/dailyWidgetReco';
|
||||||
import { getOrCreateClientUserId } from '@/src/storage/appStorage';
|
import { getOrCreateClientUserId } from '@/src/storage/appStorage';
|
||||||
|
import { ensurePushTokenRegisteredIfPermitted } from '@/src/services/pushApi';
|
||||||
|
|
||||||
// 配置通知处理方式(即使不发送也建议配置,以确保权限接口正常)
|
// 配置通知处理方式(即使不发送也建议配置,以确保权限接口正常)
|
||||||
Notifications.setNotificationHandler({
|
Notifications.setNotificationHandler({
|
||||||
@@ -75,6 +76,17 @@ export default function RootLayout() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 只要系统通知权限已经 granted,就主动上报 Push Token(不依赖用户在“每日提醒”里点确认)
|
||||||
|
ensurePushTokenRegisteredIfPermitted()
|
||||||
|
.then((res) => {
|
||||||
|
if (__DEV__) console.log('[push_token_sync]', res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (__DEV__) console.warn('[push_token_sync] 失败(不阻塞启动)', e);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 字体与 i18n 都准备好后,允许渲染 App(原生 splash 的隐藏交给 onLayout,避免“硬切/闪白”)
|
// 字体与 i18n 都准备好后,允许渲染 App(原生 splash 的隐藏交给 onLayout,避免“硬切/闪白”)
|
||||||
if (loaded && i18nReady) setAppReady(true);
|
if (loaded && i18nReady) setAppReady(true);
|
||||||
@@ -113,7 +125,7 @@ export default function RootLayout() {
|
|||||||
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { opacity: splashOpacity }]}>
|
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { opacity: splashOpacity }]}>
|
||||||
<View style={styles.splashOverlay}>
|
<View style={styles.splashOverlay}>
|
||||||
<Image
|
<Image
|
||||||
source={require('../assets/images/splashScreen.png')}
|
source={require('../assets/images/Screen_page.png')}
|
||||||
style={styles.splashImage}
|
style={styles.splashImage}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 126 KiB |
@@ -41,7 +41,7 @@ import QuestionIcon from '@/assets/images/home/Profile/widget/question_icon.svg'
|
|||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { changeLanguage } from '@/src/i18n';
|
import { changeLanguage } from '@/src/i18n';
|
||||||
import { fetchLegalLinks } from '@/src/services/legalApi';
|
import { fetchLegalLinks } from '@/src/services/legalApi';
|
||||||
import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi';
|
import { ensurePushTokenRegisteredIfPermitted, getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -479,6 +479,13 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
|
|||||||
const next: DailyReminderSettings = { timesPerDay: nextTimes, pushEnabled: nextEnabled };
|
const next: DailyReminderSettings = { timesPerDay: nextTimes, pushEnabled: nextEnabled };
|
||||||
await setDailyReminderSettings(next);
|
await setDailyReminderSettings(next);
|
||||||
|
|
||||||
|
// 若系统权限已授予且用户意愿为开启:兜底同步一次 token(避免“没点开关/没触发 toggle 导致后端无 token”)
|
||||||
|
if (nextEnabled) {
|
||||||
|
ensurePushTokenRegisteredIfPermitted().catch(() => {
|
||||||
|
// ignore:不阻塞保存
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 同步后端偏好(幂等;失败不阻塞)
|
// 同步后端偏好(幂等;失败不阻塞)
|
||||||
try {
|
try {
|
||||||
await setPushPreferences({ enabled: nextEnabled, timesPerDay: nextTimes });
|
await setPushPreferences({ enabled: nextEnabled, timesPerDay: nextTimes });
|
||||||
@@ -553,6 +560,8 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
|
|||||||
function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
|
function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const currentLang = i18n.language;
|
const currentLang = i18n.language;
|
||||||
|
// 需求:个人主页弹窗「小工具」页暂时隐藏锁屏小工具说明/入口
|
||||||
|
const showLockScreenWidget = false;
|
||||||
|
|
||||||
// 根据语言选择图片
|
// 根据语言选择图片
|
||||||
const widget1 = currentLang === 'en'
|
const widget1 = currentLang === 'en'
|
||||||
@@ -570,10 +579,12 @@ function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<View style={styles.widgetScroll}>
|
<View style={styles.widgetScroll}>
|
||||||
|
{showLockScreenWidget ? (
|
||||||
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
||||||
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
|
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
|
||||||
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
|
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
||||||
<Image source={widget2} style={styles.widgetImg2} resizeMode="contain" />
|
<Image source={widget2} style={styles.widgetImg2} resizeMode="contain" />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function WidgetModal({ visible, onClose }: Props) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
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.content}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<PreviewCard label={t('widget.lockScreen')}>
|
<PreviewCard label={t('widget.lockScreen')}>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { SerifText } from './SerifText';
|
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
|
||||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
|
||||||
|
|
||||||
export const INTENTS = [
|
export const INTENTS = [
|
||||||
{ id: 'love', labelKey: 'intent.love', icon: '❤️' },
|
{ id: 'love', labelKey: 'intent.love', icon: '❤️' },
|
||||||
@@ -20,7 +19,7 @@ export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionSt
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<SerifText style={styles.title}>{t('intent.title')}</SerifText>
|
<Text style={styles.title}>{t('intent.title')}</Text>
|
||||||
|
|
||||||
<View style={styles.grid}>
|
<View style={styles.grid}>
|
||||||
{INTENTS.map((intent) => {
|
{INTENTS.map((intent) => {
|
||||||
@@ -35,10 +34,10 @@ export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionSt
|
|||||||
onPress={() => onToggle(intent.id)}
|
onPress={() => onToggle(intent.id)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<SerifText style={styles.icon}>{intent.icon}</SerifText>
|
<Text style={styles.icon}>{intent.icon}</Text>
|
||||||
<SerifText style={[styles.label, isSelected && styles.labelSelected]}>
|
<Text style={[styles.label, isSelected && styles.labelSelected]}>
|
||||||
{t(intent.labelKey)}
|
{t(intent.labelKey)}
|
||||||
</SerifText>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -56,6 +55,8 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
marginBottom: 40,
|
marginBottom: 40,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
fontFamily: OnboardingFont.question,
|
||||||
|
color: OnboardingColors.textPrimary,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -86,10 +87,12 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
|
fontFamily: OnboardingFont.question,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: OnboardingColors.textPrimary,
|
color: OnboardingColors.textPrimary,
|
||||||
|
fontFamily: OnboardingFont.question,
|
||||||
},
|
},
|
||||||
labelSelected: {
|
labelSelected: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
|||||||
@@ -95,8 +95,7 @@ export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProp
|
|||||||
blurOnSubmit={true}
|
blurOnSubmit={true}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
// 有输入时,“完成”直接进入下一步,避免真机卡在键盘上
|
// 不再自動跳頁,僅收起鍵盤;前進需點擊底部 ➡️
|
||||||
if (value.trim().length > 0) onNext();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native';
|
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 {
|
interface OnboardingLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,6 +14,8 @@ interface OnboardingLayoutProps {
|
|||||||
onSkip: () => void;
|
onSkip: () => void;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
|
/** 用户名字,仅在名字步骤之后的第一个问题(currentStep === 1)且非空时显示招呼语 */
|
||||||
|
userName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingLayout({
|
export function OnboardingLayout({
|
||||||
@@ -20,9 +25,48 @@ export function OnboardingLayout({
|
|||||||
totalSteps,
|
totalSteps,
|
||||||
onSkip,
|
onSkip,
|
||||||
onBack,
|
onBack,
|
||||||
showBackButton = false
|
showBackButton = false,
|
||||||
|
userName = '',
|
||||||
}: OnboardingLayoutProps) {
|
}: OnboardingLayoutProps) {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" />
|
<StatusBar barStyle="dark-content" />
|
||||||
@@ -49,16 +93,29 @@ export function OnboardingLayout({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Title & Progress Row */}
|
{/* Title & Progress Row(名字步骤后第一步且名字非空时显示招呼语 + 问题) */}
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
|
<View style={styles.titleBlock}>
|
||||||
|
{showGreeting && (
|
||||||
|
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
|
||||||
|
)}
|
||||||
<Text style={styles.questionTitle}>{title}</Text>
|
<Text style={styles.questionTitle}>{title}</Text>
|
||||||
|
</View>
|
||||||
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
|
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content:step 切换时滑动 + 淡入 */}
|
||||||
<View style={styles.content}>
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.content,
|
||||||
|
{
|
||||||
|
opacity,
|
||||||
|
transform: [{ translateX }],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</Animated.View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -114,18 +171,27 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
marginTop: 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: {
|
questionTitle: {
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
color: OnboardingColors.questionTitle,
|
color: OnboardingColors.questionTitle,
|
||||||
fontFamily: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif',
|
fontFamily: OnboardingFont.question,
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
progressText: {
|
progressText: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: OnboardingColors.textProgress,
|
color: OnboardingColors.textProgress,
|
||||||
fontFamily: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif',
|
fontFamily: OnboardingFont.question,
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet, TouchableOpacity, Text, Platform } from 'react-native';
|
import { View, StyleSheet, TouchableOpacity, Text, Platform, ActivityIndicator } from 'react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||||
import AddIcon from '@/assets/images/icon/add_icon.svg';
|
import AddIcon from '@/assets/images/icon/add_icon.svg';
|
||||||
import ReduceIcon from '@/assets/images/icon/reduce_icon.svg';
|
import ReduceIcon from '@/assets/images/icon/reduce_icon.svg';
|
||||||
@@ -11,16 +12,18 @@ interface ReminderStepProps {
|
|||||||
value: number;
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
onFinish: () => void;
|
onFinish: () => void;
|
||||||
onSkip: () => void;
|
onSkip?: () => void;
|
||||||
|
/** 完成后请求通知权限时的加载态 */
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReminderStep({ value, onChange, onFinish, onSkip }: ReminderStepProps) {
|
export function ReminderStep({ value, onChange, onFinish, loading = false }: ReminderStepProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
const handleReduce = () => {
|
const handleReduce = () => {
|
||||||
// 允许 0~5;0 表示关闭每日提醒
|
// 本页最小为 1;不接收提醒请使用右上角 Skip
|
||||||
if (value > 0) onChange(value - 1);
|
if (value > 1) onChange(value - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@@ -30,27 +33,38 @@ export function ReminderStep({ value, onChange, onFinish, onSkip }: ReminderStep
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.counterContainer}>
|
<View style={styles.counterContainer}>
|
||||||
<TouchableOpacity onPress={handleReduce} activeOpacity={0.7}>
|
<TouchableOpacity onPress={handleReduce} disabled={loading} activeOpacity={0.7}>
|
||||||
<ReduceIcon width={47} height={47} />
|
<ReduceIcon width={47} height={47} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.numberWrapper}>
|
<View style={styles.numberWrapper}>
|
||||||
<Text style={styles.numberText}>{value}</Text>
|
<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>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity onPress={handleAdd} activeOpacity={0.7}>
|
<TouchableOpacity onPress={handleAdd} disabled={loading} activeOpacity={0.7}>
|
||||||
<AddIcon width={47} height={47} />
|
<AddIcon width={47} height={47} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.footer, { bottom: insets.bottom + 16 }]}>
|
<View style={[styles.footer, { bottom: insets.bottom + 16 }]}>
|
||||||
<TouchableOpacity onPress={onFinish} activeOpacity={0.8}>
|
<TouchableOpacity onPress={onFinish} disabled={loading} activeOpacity={0.8}>
|
||||||
|
<View style={styles.finishWrap}>
|
||||||
|
{loading ? (
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#F69F7B', '#F99CC0']}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={[styles.loadingPill, styles.finishDisabled]}
|
||||||
|
>
|
||||||
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||||
|
</LinearGradient>
|
||||||
|
) : (
|
||||||
<BtnClicked width={87} height={57} />
|
<BtnClicked width={87} height={57} />
|
||||||
</TouchableOpacity>
|
)}
|
||||||
|
</View>
|
||||||
<TouchableOpacity onPress={onSkip} activeOpacity={0.8} style={styles.skipBtn}>
|
|
||||||
<Text style={styles.skipText}>{t('onboarding.skip')}</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -93,17 +107,21 @@ const styles = StyleSheet.create({
|
|||||||
footer: {
|
footer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}
|
|
||||||
,
|
|
||||||
skipBtn: {
|
|
||||||
marginTop: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
},
|
},
|
||||||
skipText: {
|
finishWrap: {
|
||||||
color: OnboardingColors.textPrimary,
|
width: 87,
|
||||||
fontSize: 15,
|
height: 57,
|
||||||
fontWeight: '600',
|
alignItems: 'center',
|
||||||
opacity: 0.85,
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
finishDisabled: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
loadingPill: {
|
||||||
|
width: 87,
|
||||||
|
height: 57,
|
||||||
|
borderRadius: 28.5,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React from 'react';
|
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
|
||||||
import { SerifText } from './SerifText';
|
|
||||||
import SelectedIcon from '@/assets/images/icon/selected_icon.svg';
|
|
||||||
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
|
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
|
||||||
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
|
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
|
||||||
|
|
||||||
@@ -25,7 +23,8 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const footerBottom = insets.bottom + 16;
|
const footerBottom = insets.bottom + 16;
|
||||||
const footerButtonHeight = 57;
|
const footerButtonHeight = 57;
|
||||||
const footerPaddingBottom = footerBottom + footerButtonHeight + 24;
|
// 底部留白加大,避免最后一项与按钮边框视觉重叠
|
||||||
|
const footerPaddingBottom = footerBottom + footerButtonHeight + 40;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -39,16 +38,11 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
|
|||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={option.id}
|
key={option.id}
|
||||||
style={styles.optionCard}
|
style={[styles.optionCard, isSelected && styles.optionCardSelected]}
|
||||||
onPress={() => onToggle(option.id)}
|
onPress={() => onToggle(option.id)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<SerifText style={styles.optionText}>{option.label}</SerifText>
|
<Text style={styles.optionText}>{option.label}</Text>
|
||||||
{isSelected && (
|
|
||||||
<View style={styles.iconWrapper}>
|
|
||||||
<SelectedIcon width={20} height={20} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -67,7 +61,7 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingTop: 20,
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
scroll: {
|
scroll: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -82,7 +76,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'center',
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
@@ -91,14 +85,14 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 10,
|
shadowRadius: 10,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
|
optionCardSelected: {
|
||||||
|
backgroundColor: OnboardingColors.cardSelected,
|
||||||
|
},
|
||||||
optionText: {
|
optionText: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: OnboardingColors.textPrimary,
|
color: OnboardingColors.textPrimary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
flex: 1,
|
fontFamily: OnboardingFont.question,
|
||||||
},
|
|
||||||
iconWrapper: {
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
position: 'absolute',
|
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 = {
|
export const OnboardingColors = {
|
||||||
background: '#FFF4EA',
|
background: '#FFF4EA',
|
||||||
textPrimary: '#772F00',
|
textPrimary: '#772F00',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
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 */; };
|
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
|
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
|
||||||
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
|
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
|
C0A1B2C3D4E5F60718293A4E /* Screen_page.png in Resources */ = {isa = PBXBuildFile; fileRef = C0A1B2C3D4E5F60718293A4D /* Screen_page.png */; };
|
||||||
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||||
EB3DAF832F2A4B8E00450593 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF822F2A4B8E00450593 /* SwiftUI.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, ); }; };
|
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>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
|
||||||
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
|
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
|
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */ = {
|
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
EmotionWidget.swift,
|
EmotionWidget.swift,
|
||||||
@@ -85,18 +85,7 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {
|
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */,
|
|
||||||
);
|
|
||||||
explicitFileTypes = {
|
|
||||||
};
|
|
||||||
explicitFolders = (
|
|
||||||
);
|
|
||||||
path = "情绪小组件";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -213,7 +202,7 @@
|
|||||||
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */,
|
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */,
|
||||||
);
|
);
|
||||||
name = "Recovered References";
|
name = "Recovered References";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -301,6 +290,7 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
"zh-Hant",
|
||||||
);
|
);
|
||||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||||
@@ -481,7 +471,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
|
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ private let keyWidgetConfig = "widget.config.v1"
|
|||||||
private let keyWidgetUserProfile = "widget.userProfile.v1_2"
|
private let keyWidgetUserProfile = "widget.userProfile.v1_2"
|
||||||
private let keyWidgetDailyReco = "widget.dailyReco.v1"
|
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 let fallbackTextEN = "You’ve been doing great — you deserve kindness today."
|
||||||
|
|
||||||
private func defaults() -> UserDefaults? {
|
private func defaults() -> UserDefaults? {
|
||||||
@@ -29,17 +29,24 @@ private func localDayKey(_ date: Date = Date()) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func resolveLang() -> String {
|
private func resolveLang() -> String {
|
||||||
// 仅支持 en/tc
|
// 仅支持 en/tc:根据设备语言选择
|
||||||
let preferred = Locale.preferredLanguages.first?.lowercased() ?? "en"
|
// - 传统中文(Hant / TW / HK / MO)=> tc
|
||||||
return preferred.hasPrefix("zh") ? "tc" : "en"
|
// - 其他语言(含简中 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 {
|
private func resolveTitle(lang: String) -> String {
|
||||||
lang == "en" ? "Mindfulness" : "正念"
|
// 需求:品牌文案「正念」统一改为 Hey Mama
|
||||||
|
return "Hey Mama"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveFooterHint(lang: String) -> String {
|
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 {
|
private func joinUrl(base: String, path: String) -> String {
|
||||||
@@ -76,13 +83,41 @@ private func writeJsonDict(_ dict: [String: Any], forKey key: String) {
|
|||||||
defaults()?.set(raw, forKey: key)
|
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 }
|
guard let d = readJsonDict(forKey: keyWidgetDailyReco) else { return nil }
|
||||||
let lang = (d["lang"] as? String) ?? resolveLang()
|
let lang = (d["lang"] as? String) ?? resolveLang()
|
||||||
let dayKey = d["day_key"] as? String
|
let dayKey = d["day_key"] as? String
|
||||||
if let item = d["item"] as? [String: Any], let text = item["text"] as? String, !text.isEmpty {
|
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 (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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +209,7 @@ struct EmotionProvider: TimelineProvider {
|
|||||||
let today = localDayKey(Date())
|
let today = localDayKey(Date())
|
||||||
|
|
||||||
// 1) 今日缓存优先
|
// 1) 今日缓存优先
|
||||||
if let cached = readCachedText(), cached.dayKey == today {
|
if let cached = readCachedText(family: context.family), cached.dayKey == today {
|
||||||
let entry = EmotionEntry(
|
let entry = EmotionEntry(
|
||||||
date: Date(),
|
date: Date(),
|
||||||
lang: cached.lang,
|
lang: cached.lang,
|
||||||
@@ -201,7 +236,7 @@ struct EmotionProvider: TimelineProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3) 网络失败:用最近缓存或兜底
|
// 3) 网络失败:用最近缓存或兜底
|
||||||
if let cached = readCachedText() {
|
if let cached = readCachedText(family: context.family) {
|
||||||
let entry = EmotionEntry(
|
let entry = EmotionEntry(
|
||||||
date: Date(),
|
date: Date(),
|
||||||
lang: cached.lang,
|
lang: cached.lang,
|
||||||
@@ -332,8 +367,9 @@ struct EmotionWidget: Widget {
|
|||||||
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
|
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
|
||||||
EmotionWidgetView(entry: entry)
|
EmotionWidgetView(entry: entry)
|
||||||
}
|
}
|
||||||
.configurationDisplayName("情绪小组件")
|
// 名称/描述:支持多语言(使用 Widget Extension 自己的 Localizable.strings)
|
||||||
.description("一段温柔提醒,陪你回到当下。")
|
.configurationDisplayName("WIDGET_DISPLAY_NAME")
|
||||||
|
.description("WIDGET_DESCRIPTION")
|
||||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
.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",
|
"version": "2.0.17",
|
||||||
"resolved": "https://registry.npmmirror.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
"resolved": "https://registry.npmmirror.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||||
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hammerjs": "^2.0.36"
|
"@types/hammerjs": "^2.0.36"
|
||||||
},
|
},
|
||||||
@@ -1558,6 +1559,7 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"aix"
|
"aix"
|
||||||
@@ -1574,6 +1576,7 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -1590,6 +1593,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -1606,6 +1610,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -1622,6 +1627,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -1638,6 +1644,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -1654,6 +1661,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -1670,6 +1678,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -1686,6 +1695,7 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1702,6 +1712,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1718,6 +1729,7 @@
|
|||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1734,6 +1746,7 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1750,6 +1763,7 @@
|
|||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1766,6 +1780,7 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1782,6 +1797,7 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1798,6 +1814,7 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1814,6 +1831,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1830,6 +1848,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
@@ -1846,6 +1865,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
@@ -1862,6 +1882,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
@@ -1878,6 +1899,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
@@ -1894,6 +1916,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openharmony"
|
"openharmony"
|
||||||
@@ -1910,6 +1933,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"sunos"
|
"sunos"
|
||||||
@@ -1926,6 +1950,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -1942,6 +1967,7 @@
|
|||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -1958,6 +1984,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -3224,6 +3251,7 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -3237,6 +3265,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -3250,6 +3279,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -3263,6 +3293,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -3276,6 +3307,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -3289,6 +3321,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -3302,6 +3335,7 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3315,6 +3349,7 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3328,6 +3363,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3341,6 +3377,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3354,6 +3391,7 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3367,6 +3405,7 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3380,6 +3419,7 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3393,6 +3433,7 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3406,6 +3447,7 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3419,6 +3461,7 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3432,6 +3475,7 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3445,6 +3489,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3458,6 +3503,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -3471,6 +3517,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
@@ -3484,6 +3531,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openharmony"
|
"openharmony"
|
||||||
@@ -3497,6 +3545,7 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -3510,6 +3559,7 @@
|
|||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -3523,6 +3573,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -3536,6 +3587,7 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -3569,7 +3621,8 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -3860,6 +3913,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
|
||||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/deep-eql": "*",
|
"@types/deep-eql": "*",
|
||||||
"assertion-error": "^2.0.1"
|
"assertion-error": "^2.0.1"
|
||||||
@@ -3869,13 +3923,15 @@
|
|||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
@@ -3889,7 +3945,8 @@
|
|||||||
"node_modules/@types/hammerjs": {
|
"node_modules/@types/hammerjs": {
|
||||||
"version": "2.0.46",
|
"version": "2.0.46",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
"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": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
@@ -3989,6 +4046,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.18.tgz",
|
||||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
@@ -4006,6 +4064,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.0.18",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
@@ -4032,6 +4091,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyrainbow": "^3.0.3"
|
"tinyrainbow": "^3.0.3"
|
||||||
},
|
},
|
||||||
@@ -4044,6 +4104,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.18.tgz",
|
||||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.0.18",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
@@ -4057,6 +4118,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.0.18",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
@@ -4071,6 +4133,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.18.tgz",
|
||||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
@@ -4080,6 +4143,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.18.tgz",
|
||||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.0.18",
|
||||||
"tinyrainbow": "^3.0.3"
|
"tinyrainbow": "^3.0.3"
|
||||||
@@ -4147,6 +4211,7 @@
|
|||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -4285,6 +4350,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -4820,6 +4886,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz",
|
"resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz",
|
||||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -5601,7 +5668,8 @@
|
|||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -5621,6 +5689,7 @@
|
|||||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -5701,6 +5770,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -5734,6 +5804,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
@@ -5832,6 +5903,7 @@
|
|||||||
"version": "15.0.8",
|
"version": "15.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-crypto/-/expo-crypto-15.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-crypto/-/expo-crypto-15.0.8.tgz",
|
||||||
"integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
|
"integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.0"
|
"base64-js": "^1.3.0"
|
||||||
},
|
},
|
||||||
@@ -5843,6 +5915,7 @@
|
|||||||
"version": "6.0.20",
|
"version": "6.0.20",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-6.0.20.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-6.0.20.tgz",
|
||||||
"integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==",
|
"integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-dev-launcher": "6.0.20",
|
"expo-dev-launcher": "6.0.20",
|
||||||
"expo-dev-menu": "7.0.18",
|
"expo-dev-menu": "7.0.18",
|
||||||
@@ -5858,6 +5931,7 @@
|
|||||||
"version": "6.0.20",
|
"version": "6.0.20",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz",
|
||||||
"integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==",
|
"integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.11.0",
|
"ajv": "^8.11.0",
|
||||||
"expo-dev-menu": "7.0.18",
|
"expo-dev-menu": "7.0.18",
|
||||||
@@ -5871,6 +5945,7 @@
|
|||||||
"version": "7.0.18",
|
"version": "7.0.18",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz",
|
||||||
"integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==",
|
"integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-dev-menu-interface": "2.0.0"
|
"expo-dev-menu-interface": "2.0.0"
|
||||||
},
|
},
|
||||||
@@ -5882,6 +5957,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
|
||||||
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
|
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
|
||||||
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
@@ -5890,6 +5966,7 @@
|
|||||||
"version": "8.0.10",
|
"version": "8.0.10",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-device/-/expo-device-8.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-device/-/expo-device-8.0.10.tgz",
|
||||||
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
|
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ua-parser-js": "^0.7.33"
|
"ua-parser-js": "^0.7.33"
|
||||||
},
|
},
|
||||||
@@ -5915,6 +5992,7 @@
|
|||||||
"url": "https://github.com/sponsors/faisalman"
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ua-parser-js": "script/cli.js"
|
"ua-parser-js": "script/cli.js"
|
||||||
},
|
},
|
||||||
@@ -5949,7 +6027,8 @@
|
|||||||
"node_modules/expo-json-utils": {
|
"node_modules/expo-json-utils": {
|
||||||
"version": "0.15.0",
|
"version": "0.15.0",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
|
"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": {
|
"node_modules/expo-keep-awake": {
|
||||||
"version": "15.0.8",
|
"version": "15.0.8",
|
||||||
@@ -6003,6 +6082,7 @@
|
|||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-1.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-1.0.10.tgz",
|
||||||
"integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==",
|
"integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config": "~12.0.11",
|
"@expo/config": "~12.0.11",
|
||||||
"expo-json-utils": "~0.15.0"
|
"expo-json-utils": "~0.15.0"
|
||||||
@@ -6351,6 +6431,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
|
||||||
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
|
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
|
||||||
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
@@ -6543,7 +6624,8 @@
|
|||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/fastify"
|
"url": "https://opencollective.com/fastify"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/fb-watchman": {
|
"node_modules/fb-watchman": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
@@ -6962,6 +7044,7 @@
|
|||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
"resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-is": "^16.7.0"
|
"react-is": "^16.7.0"
|
||||||
}
|
}
|
||||||
@@ -6969,7 +7052,8 @@
|
|||||||
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
|
"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": {
|
"node_modules/hosted-git-info": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
@@ -7623,7 +7707,8 @@
|
|||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"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": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
@@ -8086,6 +8171,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
@@ -8784,7 +8870,8 @@
|
|||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/sxzz",
|
"https://github.com/sponsors/sxzz",
|
||||||
"https://opencollective.com/debug"
|
"https://opencollective.com/debug"
|
||||||
]
|
],
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
@@ -9132,7 +9219,8 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
|
||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -9522,6 +9610,7 @@
|
|||||||
"version": "2.30.0",
|
"version": "2.30.0",
|
||||||
"resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz",
|
||||||
"integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==",
|
"integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.17",
|
"@egjs/hammerjs": "^2.0.17",
|
||||||
"hoist-non-react-statics": "^3.3.0",
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
@@ -10122,6 +10211,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz",
|
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz",
|
||||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -10417,7 +10507,8 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
@@ -10562,7 +10653,8 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
|
||||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/stackframe": {
|
"node_modules/stackframe": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
@@ -10595,7 +10687,8 @@
|
|||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
|
||||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/stream-buffers": {
|
"node_modules/stream-buffers": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
@@ -10942,13 +11035,15 @@
|
|||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -11003,6 +11098,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@@ -11521,6 +11617,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -11595,6 +11692,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
},
|
},
|
||||||
@@ -11612,6 +11710,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -11638,6 +11737,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -11652,6 +11752,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.18.tgz",
|
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.18.tgz",
|
||||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.18",
|
"@vitest/expect": "4.0.18",
|
||||||
"@vitest/mocker": "4.0.18",
|
"@vitest/mocker": "4.0.18",
|
||||||
@@ -11729,6 +11830,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -11861,6 +11963,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"siginfo": "^2.0.0",
|
"siginfo": "^2.0.0",
|
||||||
"stackback": "0.0.2"
|
"stackback": "0.0.2"
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
|
"start:clean": "expo start -c",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios --scheme \"Hey Mama\"",
|
"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",
|
"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": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export type AppRuntimeEnv = 'local' | 'dev' | 'prod';
|
|||||||
* 默认回退到 prod(避免误打到 localhost 导致真机“无法发起网络请求”)。
|
* 默认回退到 prod(避免误打到 localhost 导致真机“无法发起网络请求”)。
|
||||||
*/
|
*/
|
||||||
const DEFAULT_RUNTIME_ENV: AppRuntimeEnv =
|
const DEFAULT_RUNTIME_ENV: AppRuntimeEnv =
|
||||||
typeof __DEV__ !== 'undefined' && __DEV__ ? 'local' : 'prod';
|
typeof __DEV__ !== 'undefined' && __DEV__ ? 'local' : 'dev';
|
||||||
|
|
||||||
export const APP_ENV = (getOptionalEnv('EXPO_PUBLIC_ENV', DEFAULT_RUNTIME_ENV) as AppRuntimeEnv) ?? DEFAULT_RUNTIME_ENV;
|
export const APP_ENV = (getOptionalEnv('EXPO_PUBLIC_ENV', DEFAULT_RUNTIME_ENV) as AppRuntimeEnv) ?? DEFAULT_RUNTIME_ENV;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { MeasureWidthImpl } from './measure/types';
|
import type { MeasureWidthImpl } from './measure/types';
|
||||||
import type { ScoreTerm } from './scoring/types';
|
import type { ScoreTerm } from './scoring/types';
|
||||||
|
import type { Weights } from './scoring/types';
|
||||||
|
|
||||||
export type OverflowMode = 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT';
|
export type OverflowMode = 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT';
|
||||||
export type LineMode = 'AUTO' | 'FIXED';
|
export type LineMode = 'AUTO' | 'FIXED';
|
||||||
@@ -26,6 +27,21 @@ export type WrapTextInput = {
|
|||||||
fontSpec?: Partial<FontSpecInput> | null;
|
fontSpec?: Partial<FontSpecInput> | null;
|
||||||
/** 可选注入测量实现(APP 场景强烈建议提供;WIDGET 默认不启用) */
|
/** 可选注入测量实现(APP 场景强烈建议提供;WIDGET 默认不启用) */
|
||||||
measureWidthImpl?: MeasureWidthImpl;
|
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(可选)。
|
* 测量缓存隔离 key(可选)。
|
||||||
* 口径建议:`APP|ios|<scale?>` / `WIDGET|small`。
|
* 口径建议:`APP|ios|<scale?>` / `WIDGET|small`。
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { normalizeWhitespace, tokenizeEN } from './core/index';
|
|||||||
import { segmentGraphemes } from './grapheme/index';
|
import { segmentGraphemes } from './grapheme/index';
|
||||||
import { generateBreakpoints } from './breakpoints/index';
|
import { generateBreakpoints } from './breakpoints/index';
|
||||||
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from './scoring/index';
|
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from './scoring/index';
|
||||||
|
import { mergeWeights } from './scoring/weights';
|
||||||
import { searchBestLayoutApp } from './searchApp/index';
|
import { searchBestLayoutApp } from './searchApp/index';
|
||||||
import { searchBestLayoutWidget } from './searchWidget/index';
|
import { searchBestLayoutWidget } from './searchWidget/index';
|
||||||
import { applyOverflowFallback } from './overflow/index';
|
import { applyOverflowFallback } from './overflow/index';
|
||||||
@@ -45,12 +46,23 @@ export async function wrapText(input: WrapTextInput): Promise<WrapTextOutput> {
|
|||||||
// scoring:protectedPhrases 从 constraints 注入
|
// scoring:protectedPhrases 从 constraints 注入
|
||||||
const lexicons = { ...DEFAULT_LEXICONS, protectedPhrases: input.constraints?.protectedPhrases ?? DEFAULT_LEXICONS.protectedPhrases };
|
const lexicons = { ...DEFAULT_LEXICONS, protectedPhrases: input.constraints?.protectedPhrases ?? DEFAULT_LEXICONS.protectedPhrases };
|
||||||
const tcPunctuations: string[] = [',', '。', '!', '?', ';', ':', '、'];
|
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 = {
|
const scoringConfig = {
|
||||||
weights: DEFAULT_WEIGHTS,
|
weights,
|
||||||
idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
|
idealWidthRatio,
|
||||||
ellipsisToken: '…',
|
ellipsisToken: '…',
|
||||||
tcParticleWhitelist: [],
|
tcParticleWhitelist: [],
|
||||||
tcPunctuations,
|
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.*`
|
- **Home**:`home.*`
|
||||||
- **Push 提示**:`push.*`
|
- **Push 提示**:`push.*`
|
||||||
- **主题**:`theme.*`
|
- **主題**:`theme.*`
|
||||||
- **我的/Profile**:`profile.*`
|
- **我的 / Profile**:`profile.*`
|
||||||
- **收藏**:`favorites.*`
|
- **收藏**:`favorites.*`
|
||||||
- **设置**:`settings.*`
|
- **設定**:`settings.*`
|
||||||
- **Onboarding(问卷)**:`onboardingSurvey.steps.*`
|
|
||||||
- **Onboarding(兴趣)**:`intent.*`
|
|
||||||
- **Mock 文案**:`mock.*`
|
- **Mock 文案**:`mock.*`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import { initReactI18next } from 'react-i18next';
|
|||||||
import { isTraditionalChineseLocaleTag } from './locale';
|
import { isTraditionalChineseLocaleTag } from './locale';
|
||||||
|
|
||||||
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
// 用 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
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const all = require('./locales/all.json') as { en: Record<string, unknown>; 'zh-TW': Record<string, unknown> };
|
const all = require('./locales/all.json') as { en: Record<string, unknown>; 'zh-TW': Record<string, unknown> };
|
||||||
|
// 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({
|
await i18n.use(initReactI18next).init({
|
||||||
resources: {
|
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 },
|
en: { translation: all.en as any },
|
||||||
},
|
},
|
||||||
lng: initialLang,
|
lng: initialLang,
|
||||||
@@ -91,6 +96,18 @@ export async function initI18n(): Promise<void> {
|
|||||||
escapeValue: false,
|
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}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"skipAll": "Skip onboarding",
|
"skipAll": "Skip",
|
||||||
"q1Title": "How are you feeling lately?",
|
"q1Title": "How are you feeling lately?",
|
||||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||||
"q2Title": "What kind of support do you want?",
|
"q2Title": "What kind of support do you want?",
|
||||||
@@ -25,10 +25,11 @@
|
|||||||
"q4Desc": "You can skip. We’ll stay with you along the way."
|
"q4Desc": "You can skip. We’ll stay with you along the way."
|
||||||
},
|
},
|
||||||
"onboardingSurvey": {
|
"onboardingSurvey": {
|
||||||
|
"greeting": "Hi {{name}},",
|
||||||
"steps": {
|
"steps": {
|
||||||
"name": { "title": "What do you want to be called?", "placeholder": "Mama" },
|
"name": { "title": "What should we call you?", "placeholder": "Mama" },
|
||||||
"status": {
|
"status": {
|
||||||
"title": "Which stage of motherhood are you in?",
|
"title": "Where are you at right now?",
|
||||||
"options": {
|
"options": {
|
||||||
"pregnant": "Pregnant / Preparing",
|
"pregnant": "Pregnant / Preparing",
|
||||||
"has_kids": "Parenting",
|
"has_kids": "Parenting",
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
"balance": "Rest & balance"
|
"balance": "Rest & balance"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reminder": { "title": "How many reminders do you want per day?" }
|
"reminder": { "title": "How often would you like a gentle reminder?" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"intent": {
|
"intent": {
|
||||||
@@ -87,7 +88,7 @@
|
|||||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Mindfulness",
|
"title": "Hey Mama",
|
||||||
"like": "Like",
|
"like": "Like",
|
||||||
"dislike": "Dislike",
|
"dislike": "Dislike",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
@@ -115,6 +116,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "Daily Reminder",
|
"title": "Daily Reminder",
|
||||||
"timesUnit": "times",
|
"timesUnit": "times",
|
||||||
|
"timesUnitSingular": "time",
|
||||||
"pushLabel": "Push Reminder",
|
"pushLabel": "Push Reminder",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"minus": "Decrease",
|
"minus": "Decrease",
|
||||||
@@ -125,7 +127,7 @@
|
|||||||
"homeScreen": "Home Screen Widget",
|
"homeScreen": "Home Screen Widget",
|
||||||
"howToTitle": "How to add the 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.",
|
"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",
|
"previewDate": "Thu, Jan 29",
|
||||||
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
||||||
},
|
},
|
||||||
@@ -139,16 +141,17 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"widgetTitle": "iOS Widget",
|
"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": {
|
"consent": {
|
||||||
"title": "You Are Perfect.",
|
"title": "Hey mama.",
|
||||||
"subtitle": "Everything\nWill Be Better.",
|
"subtitle": "You’re doing okay\nright now.",
|
||||||
|
"subtitleSecondary": "",
|
||||||
"agree": "Agree & Continue",
|
"agree": "Agree & Continue",
|
||||||
"privacy": "Privacy Policy",
|
"privacy": "Privacy Policy",
|
||||||
"terms": "Terms of Use",
|
"terms": "Terms of Use",
|
||||||
"notice": "By continuing, you agree to the Privacy Policy and Terms of Use.",
|
"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.",
|
"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}}",
|
"linkUnavailableDev": "Failed to load the policy link. Please check your network or API_BASE_URL: {{baseUrl}}",
|
||||||
"linkLoadingSuffix": " (loading…)"
|
"linkLoadingSuffix": " (loading…)"
|
||||||
@@ -183,7 +186,7 @@
|
|||||||
"progress": "{{current}}/{{total}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"skip": "跳過",
|
"skip": "跳過",
|
||||||
"skipAll": "跳過整個引導",
|
"skipAll": "跳過",
|
||||||
"q1Title": "你最近的感受更接近哪一種?",
|
"q1Title": "你最近的感受更接近哪一種?",
|
||||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||||
"q2Title": "你更希望獲得哪種支持?",
|
"q2Title": "你更希望獲得哪種支持?",
|
||||||
@@ -194,18 +197,19 @@
|
|||||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
||||||
},
|
},
|
||||||
"onboardingSurvey": {
|
"onboardingSurvey": {
|
||||||
|
"greeting": "Hi {{name}},",
|
||||||
"steps": {
|
"steps": {
|
||||||
"name": { "title": "我可以怎麼稱呼你?", "placeholder": "媽媽" },
|
"name": { "title": "怎麼稱呼你呢?", "placeholder": "媽媽" },
|
||||||
"status": {
|
"status": {
|
||||||
"title": "媽媽的狀態?",
|
"title": "你現在正處在哪個階段呢?",
|
||||||
"options": {
|
"options": {
|
||||||
"pregnant": "懷孕中/準備成為媽媽",
|
"pregnant": "懷孕中/正在準備迎接寶寶",
|
||||||
"has_kids": "已經有孩子",
|
"has_kids": "已經有孩子",
|
||||||
"no_fill": "不想填寫"
|
"no_fill": "我暫時不想說"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"emotion": {
|
"emotion": {
|
||||||
"title": "當下情緒狀態?",
|
"title": "今天的你,還好嗎?",
|
||||||
"options": {
|
"options": {
|
||||||
"happy": "愉悅、滿足",
|
"happy": "愉悅、滿足",
|
||||||
"calm": "平靜、安穩",
|
"calm": "平靜、安穩",
|
||||||
@@ -256,7 +260,7 @@
|
|||||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "正念",
|
"title": "Hey Mama",
|
||||||
"like": "喜歡",
|
"like": "喜歡",
|
||||||
"dislike": "不喜歡",
|
"dislike": "不喜歡",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
@@ -284,6 +288,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "每日提醒",
|
"title": "每日提醒",
|
||||||
"timesUnit": "次",
|
"timesUnit": "次",
|
||||||
|
"timesUnitSingular": "次",
|
||||||
"pushLabel": "推送提醒",
|
"pushLabel": "推送提醒",
|
||||||
"ok": "確定",
|
"ok": "確定",
|
||||||
"minus": "減少次數",
|
"minus": "減少次數",
|
||||||
@@ -292,9 +297,9 @@
|
|||||||
"widget": {
|
"widget": {
|
||||||
"lockScreen": "鎖屏小工具",
|
"lockScreen": "鎖屏小工具",
|
||||||
"homeScreen": "桌面小工具",
|
"homeScreen": "桌面小工具",
|
||||||
"howToTitle": "如何添加小工具",
|
"howToTitle": "如何加入小工具",
|
||||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||||
"howToDesc2": "搜尋「正念」,選擇喜歡的尺寸,點「加入小工具」。",
|
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||||
},
|
},
|
||||||
@@ -308,11 +313,12 @@
|
|||||||
"language": "語言",
|
"language": "語言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小工具",
|
"widgetTitle": "iOS 小工具",
|
||||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "你很完美。",
|
"title": "我們知道,",
|
||||||
"subtitle": "一切\n都會更好。",
|
"subtitle": "當媽媽很不容易。",
|
||||||
|
"subtitleSecondary": "這裡給你一些溫柔的肯定與提醒",
|
||||||
"agree": "同意並繼續",
|
"agree": "同意並繼續",
|
||||||
"privacy": "隱私協議",
|
"privacy": "隱私協議",
|
||||||
"terms": "用戶使用協議",
|
"terms": "用戶使用協議",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"progress": "{{current}}/{{total}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"skipAll": "Skip onboarding",
|
"skipAll": "Skip",
|
||||||
"q1Title": "How are you feeling lately?",
|
"q1Title": "How are you feeling lately?",
|
||||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||||
"q2Title": "What kind of support do you want?",
|
"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."
|
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Mindfulness",
|
"title": "Hey Mama",
|
||||||
"like": "Like",
|
"like": "Like",
|
||||||
"dislike": "Dislike",
|
"dislike": "Dislike",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "Daily Reminder",
|
"title": "Daily Reminder",
|
||||||
"timesUnit": "times",
|
"timesUnit": "times",
|
||||||
|
"timesUnitSingular": "time",
|
||||||
"pushLabel": "Push Reminder",
|
"pushLabel": "Push Reminder",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"minus": "Decrease",
|
"minus": "Decrease",
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"widgetTitle": "iOS Widget",
|
"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": {
|
"consent": {
|
||||||
"title": "You Are Perfect.",
|
"title": "You Are Perfect.",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"progress": "{{current}}/{{total}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
"skip": "Saltar",
|
"skip": "Saltar",
|
||||||
"skipAll": "Saltar introducción",
|
"skipAll": "Saltar",
|
||||||
"q1Title": "¿Cómo te sientes últimamente?",
|
"q1Title": "¿Cómo te sientes últimamente?",
|
||||||
"q1Desc": "No hay respuestas correctas. Puedes saltar y ajustar después.",
|
"q1Desc": "No hay respuestas correctas. Puedes saltar y ajustar después.",
|
||||||
"q2Title": "¿Qué tipo de apoyo quieres?",
|
"q2Title": "¿Qué tipo de apoyo quieres?",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Mindfulness",
|
"title": "Hey Mama",
|
||||||
"like": "Me gusta",
|
"like": "Me gusta",
|
||||||
"dislike": "No me gusta",
|
"dislike": "No me gusta",
|
||||||
"favorites": "Favoritos",
|
"favorites": "Favoritos",
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "Recordatorio diario",
|
"title": "Recordatorio diario",
|
||||||
"timesUnit": "veces",
|
"timesUnit": "veces",
|
||||||
|
"timesUnitSingular": "vez",
|
||||||
"pushLabel": "Recordatorio Push",
|
"pushLabel": "Recordatorio Push",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"minus": "Disminuir",
|
"minus": "Disminuir",
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"version": "Versión",
|
"version": "Versión",
|
||||||
"widgetTitle": "Widget de iOS",
|
"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": {
|
"consent": {
|
||||||
"agree": "Aceptar y Continuar",
|
"agree": "Aceptar y Continuar",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"progress": "{{current}}/{{total}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "Próximo",
|
"next": "Próximo",
|
||||||
"skip": "Pular",
|
"skip": "Pular",
|
||||||
"skipAll": "Pular introdução",
|
"skipAll": "Pular",
|
||||||
"q1Title": "Como você tem se sentido ultimamente?",
|
"q1Title": "Como você tem se sentido ultimamente?",
|
||||||
"q1Desc": "Não há certo ou errado. Você pode pular e ajustar depois.",
|
"q1Desc": "Não há certo ou errado. Você pode pular e ajustar depois.",
|
||||||
"q2Title": "Que tipo de apoio você quer?",
|
"q2Title": "Que tipo de apoio você quer?",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Mindfulness",
|
"title": "Hey Mama",
|
||||||
"like": "Curtir",
|
"like": "Curtir",
|
||||||
"dislike": "Não curtir",
|
"dislike": "Não curtir",
|
||||||
"favorites": "Favoritos",
|
"favorites": "Favoritos",
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "Lembrete diário",
|
"title": "Lembrete diário",
|
||||||
"timesUnit": "vezes",
|
"timesUnit": "vezes",
|
||||||
|
"timesUnitSingular": "vez",
|
||||||
"pushLabel": "Lembrete Push",
|
"pushLabel": "Lembrete Push",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"minus": "Diminuir",
|
"minus": "Diminuir",
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"version": "Versão",
|
"version": "Versão",
|
||||||
"widgetTitle": "Widget do iOS",
|
"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": {
|
"consent": {
|
||||||
"agree": "Concordar e Continuar",
|
"agree": "Concordar e Continuar",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"progress": "{{current}}/{{total}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"skip": "跳过",
|
"skip": "跳过",
|
||||||
"skipAll": "跳过整个引导",
|
"skipAll": "跳过",
|
||||||
"q1Title": "你最近的感受更接近哪一种?",
|
"q1Title": "你最近的感受更接近哪一种?",
|
||||||
"q1Desc": "没有对错,你可以跳过,之后也可以慢慢调整。",
|
"q1Desc": "没有对错,你可以跳过,之后也可以慢慢调整。",
|
||||||
"q2Title": "你更希望获得哪种支持?",
|
"q2Title": "你更希望获得哪种支持?",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"errorDesc": "开启失败,请稍后重试(模拟器可能无法获取推送 Token,建议用真机测试)。"
|
"errorDesc": "开启失败,请稍后重试(模拟器可能无法获取推送 Token,建议用真机测试)。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "正念",
|
"title": "Hey Mama",
|
||||||
"like": "点赞",
|
"like": "点赞",
|
||||||
"dislike": "讨厌",
|
"dislike": "讨厌",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "每日提醒",
|
"title": "每日提醒",
|
||||||
"timesUnit": "次",
|
"timesUnit": "次",
|
||||||
|
"timesUnitSingular": "次",
|
||||||
"pushLabel": "推送提醒",
|
"pushLabel": "推送提醒",
|
||||||
"ok": "确定",
|
"ok": "确定",
|
||||||
"minus": "减少次数",
|
"minus": "减少次数",
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
"language": "语言",
|
"language": "语言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小组件",
|
"widgetTitle": "iOS 小组件",
|
||||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“正念” → 添加你喜欢的尺寸。"
|
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“Hey Mama” → 添加你喜欢的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "你本就完美。",
|
"title": "你本就完美。",
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"ok": "確定",
|
"ok": "確定",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"back": "返回"
|
"back": "返回",
|
||||||
|
"error": "錯誤",
|
||||||
|
"notice": "提示",
|
||||||
|
"openLinkError": "無法打開鏈接",
|
||||||
|
"close": "關閉"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"title": "歡迎",
|
"title": "歡迎",
|
||||||
"progress": "{{current}}/{{total}}",
|
"progress": "{{current}}/{{total}}",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"skip": "跳過",
|
"skip": "跳過",
|
||||||
"skipAll": "跳過整個引導",
|
"skipAll": "跳過",
|
||||||
"q1Title": "你最近的感受更接近哪一種?",
|
"q1Title": "你最近的感受更接近哪一種?",
|
||||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||||
"q2Title": "你更希望獲得哪種支持?",
|
"q2Title": "你更希望獲得哪種支持?",
|
||||||
@@ -19,6 +23,64 @@
|
|||||||
"q4Title": "給自己一句溫柔的話",
|
"q4Title": "給自己一句溫柔的話",
|
||||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
"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": {
|
"push": {
|
||||||
"title": "通知",
|
"title": "通知",
|
||||||
"cardTitle": "開啟溫柔提醒",
|
"cardTitle": "開啟溫柔提醒",
|
||||||
@@ -30,7 +92,7 @@
|
|||||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "正念",
|
"title": "Hey Mama",
|
||||||
"like": "喜歡",
|
"like": "喜歡",
|
||||||
"dislike": "不喜歡",
|
"dislike": "不喜歡",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
@@ -41,7 +103,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"title": "主題",
|
"title": "主題",
|
||||||
"scenery": "風景",
|
"scenery": "風景",
|
||||||
"color": "顏色"
|
"color": "顏色",
|
||||||
|
"suixin": "隨心"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "我的",
|
"title": "我的",
|
||||||
@@ -57,6 +120,7 @@
|
|||||||
"dailyReminder": {
|
"dailyReminder": {
|
||||||
"title": "每日提醒",
|
"title": "每日提醒",
|
||||||
"timesUnit": "次",
|
"timesUnit": "次",
|
||||||
|
"timesUnitSingular": "次",
|
||||||
"pushLabel": "推送提醒",
|
"pushLabel": "推送提醒",
|
||||||
"ok": "確定",
|
"ok": "確定",
|
||||||
"minus": "減少次數",
|
"minus": "減少次數",
|
||||||
@@ -65,24 +129,49 @@
|
|||||||
"widget": {
|
"widget": {
|
||||||
"lockScreen": "鎖屏小工具",
|
"lockScreen": "鎖屏小工具",
|
||||||
"homeScreen": "桌面小工具",
|
"homeScreen": "桌面小工具",
|
||||||
|
"howToTitle": "如何加入小工具",
|
||||||
|
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||||
|
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "收藏夾",
|
"title": "收藏夾",
|
||||||
"empty": "這裡還沒有收藏內容。"
|
"empty": "這裡還沒有收藏內容。",
|
||||||
|
"unknownText": "這條文案暫時無法顯示。"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小工具",
|
"widgetTitle": "iOS 小工具",
|
||||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
|
"title": "我們知道,",
|
||||||
|
"subtitle": "當媽媽很不容易。",
|
||||||
|
"subtitleSecondary": "這裡給你一些溫柔的肯定與提醒",
|
||||||
"agree": "同意並繼續",
|
"agree": "同意並繼續",
|
||||||
"privacy": "隱私協議",
|
"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 i18n from 'i18next';
|
||||||
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
||||||
import { getLocalDayKey } from '@/src/utils/date';
|
import { getLocalDayKey } from '@/src/utils/date';
|
||||||
|
import { wrapText } from '@/src/features/textWrap';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
appGroupGetString,
|
appGroupGetString,
|
||||||
@@ -40,6 +41,12 @@ export type WidgetDailyRecoV1 = {
|
|||||||
item: null | {
|
item: null | {
|
||||||
content_id: number;
|
content_id: number;
|
||||||
text: string;
|
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;
|
final_score?: number;
|
||||||
fallback_level_final?: 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 {
|
function pickUserProfileV1_2(scoringProfile: UserProfileV1_2_Extended): UserProfileV1_2 {
|
||||||
return {
|
return {
|
||||||
profile_version: scoringProfile.profile_version,
|
profile_version: scoringProfile.profile_version,
|
||||||
@@ -126,7 +185,29 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
|||||||
|
|
||||||
const today = getLocalDayKey(new Date());
|
const today = getLocalDayKey(new Date());
|
||||||
const cached = await getWidgetDailyRecoCache();
|
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());
|
const scoringProfile = args?.scoringProfile ?? (await getUserProfileScoring());
|
||||||
if (!scoringProfile) return;
|
if (!scoringProfile) return;
|
||||||
@@ -146,6 +227,8 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
|||||||
if (!top?.text) return;
|
if (!top?.text) return;
|
||||||
|
|
||||||
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
||||||
|
const wrapLang: 'EN' | 'TC' = lang === 'en' ? 'EN' : 'TC';
|
||||||
|
const wrapped = await buildWrappedTextByFamily({ text: top.text, lang: wrapLang });
|
||||||
await setWidgetDailyRecoCache({
|
await setWidgetDailyRecoCache({
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
saved_at: new Date().toISOString(),
|
saved_at: new Date().toISOString(),
|
||||||
@@ -155,6 +238,7 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
|||||||
item: {
|
item: {
|
||||||
content_id: top.content_id,
|
content_id: top.content_id,
|
||||||
text: top.text,
|
text: top.text,
|
||||||
|
wrapped_text_by_family: wrapped,
|
||||||
final_score: top.final_score,
|
final_score: top.final_score,
|
||||||
fallback_level_final: top.fallback_level_final,
|
fallback_level_final: top.fallback_level_final,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { Platform } from 'react-native';
|
|||||||
|
|
||||||
import { httpJson } from '../utils/http';
|
import { httpJson } from '../utils/http';
|
||||||
import { APP_ENV } from '../constants/env';
|
import { APP_ENV } from '../constants/env';
|
||||||
import { getDailyReminderSettings, getOrCreateClientUserId, getUserProfileScoring } from '../storage/appStorage';
|
import {
|
||||||
|
getDailyReminderSettings,
|
||||||
|
getLastRegisteredPushToken,
|
||||||
|
getOrCreateClientUserId,
|
||||||
|
getUserProfileScoring,
|
||||||
|
setLastRegisteredPushToken,
|
||||||
|
} from '../storage/appStorage';
|
||||||
import type { UserProfileScoring } from '../storage/appStorage';
|
import type { UserProfileScoring } from '../storage/appStorage';
|
||||||
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||||
|
|
||||||
@@ -154,6 +160,22 @@ export async function registerPushToken(args: { pushToken: string }): Promise<vo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ensurePushTokenRegisteredIfPermitted(): Promise<{ ok: boolean; reason: string }> {
|
||||||
|
// 只要系统权限已授权,就应尽早把 token 写入后端(不依赖用户在“每日提醒”里点确认)
|
||||||
|
const settings = await Notifications.getPermissionsAsync();
|
||||||
|
if (settings.status !== 'granted') return { ok: false, reason: 'permission_not_granted' };
|
||||||
|
|
||||||
|
const token = await getExpoPushTokenOrThrow();
|
||||||
|
|
||||||
|
// 简单去重:token 未变化则不重复上报(减少网络与日志噪音)
|
||||||
|
const last = await getLastRegisteredPushToken().catch(() => null);
|
||||||
|
if (last && String(last) === String(token)) return { ok: true, reason: 'already_registered' };
|
||||||
|
|
||||||
|
await registerPushToken({ pushToken: token });
|
||||||
|
await setLastRegisteredPushToken(token);
|
||||||
|
return { ok: true, reason: 'registered' };
|
||||||
|
}
|
||||||
|
|
||||||
export async function setPushPreferences(args: { enabled: boolean; timesPerDay: number }): Promise<PushPreferencesResponse> {
|
export async function setPushPreferences(args: { enabled: boolean; timesPerDay: number }): Promise<PushPreferencesResponse> {
|
||||||
const clientUserId = await getOrCreateClientUserId();
|
const clientUserId = await getOrCreateClientUserId();
|
||||||
const tz = pickTimezone();
|
const tz = pickTimezone();
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const KEY_RECO_FEED_HISTORY = 'reco.feedHistory';
|
|||||||
const KEY_UI_THEME_MODE = 'ui.theme.mode';
|
const KEY_UI_THEME_MODE = 'ui.theme.mode';
|
||||||
const KEY_UI_THEME_SUIXIN_STATE = 'ui.theme.suixin.state';
|
const KEY_UI_THEME_SUIXIN_STATE = 'ui.theme.suixin.state';
|
||||||
const KEY_DAILY_REMINDER_SETTINGS = 'dailyReminder.settings';
|
const KEY_DAILY_REMINDER_SETTINGS = 'dailyReminder.settings';
|
||||||
|
const KEY_PUSH_LAST_REGISTERED_TOKEN = 'push.lastRegisteredToken';
|
||||||
|
const KEY_PUSH_LAST_REGISTERED_AT = 'push.lastRegisteredAt';
|
||||||
|
|
||||||
export type PushPromptState = 'enabled' | 'skipped' | 'unknown';
|
export type PushPromptState = 'enabled' | 'skipped' | 'unknown';
|
||||||
export type Reaction = 'like' | 'dislike';
|
export type Reaction = 'like' | 'dislike';
|
||||||
@@ -69,6 +71,16 @@ export type DailyReminderSettings = {
|
|||||||
pushEnabled: boolean;
|
pushEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function getLastRegisteredPushToken(): Promise<string | null> {
|
||||||
|
const raw = await AsyncStorage.getItem(KEY_PUSH_LAST_REGISTERED_TOKEN);
|
||||||
|
return raw ? String(raw) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setLastRegisteredPushToken(token: string): Promise<void> {
|
||||||
|
await AsyncStorage.setItem(KEY_PUSH_LAST_REGISTERED_TOKEN, String(token));
|
||||||
|
await AsyncStorage.setItem(KEY_PUSH_LAST_REGISTERED_AT, new Date().toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
export type RecoFeedCacheItem = {
|
export type RecoFeedCacheItem = {
|
||||||
content_id: number;
|
content_id: number;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -188,7 +200,7 @@ export async function setReaction(contentId: string, reaction: Reaction): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FavoriteItem = {
|
export type FavoriteItem = {
|
||||||
favId: string; // 唯一标识,支持重复点赞同一文案
|
favId: string; // 唯一标识
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
/**
|
||||||
* 收藏时的文案快照(强烈建议写入,避免后续 cache 覆盖导致无法还原文案)
|
* 收藏时的文案快照(强烈建议写入,避免后续 cache 覆盖导致无法还原文案)
|
||||||
@@ -200,13 +212,28 @@ export type FavoriteItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getFavorites(): Promise<FavoriteItem[]> {
|
export async function getFavorites(): Promise<FavoriteItem[]> {
|
||||||
return await getJson<FavoriteItem[]>(KEY_FAVORITES_ITEMS, []);
|
const raw = await getJson<FavoriteItem[]>(KEY_FAVORITES_ITEMS, []);
|
||||||
|
// 去重:同一条文案只保留最新的一条,防止重复点击喜欢导致弹窗重复展示
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const deduped: FavoriteItem[] = [];
|
||||||
|
for (const item of raw) {
|
||||||
|
const key = String(item.id);
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
deduped.push(item);
|
||||||
|
}
|
||||||
|
// 若发现历史数据有重复,顺便写回清理后的版本
|
||||||
|
if (deduped.length !== raw.length) {
|
||||||
|
await setJson(KEY_FAVORITES_ITEMS, deduped);
|
||||||
|
}
|
||||||
|
return deduped;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFavorite(item: FavoriteItem): Promise<void> {
|
export async function addFavorite(item: FavoriteItem): Promise<void> {
|
||||||
const list = await getFavorites();
|
const list = await getFavorites();
|
||||||
// 允许重复点赞,不再根据 id 去重
|
// 幂等写入:同一条文案只保留一条(最新),避免重复点击喜欢产生重复项
|
||||||
const newList = [item, ...list];
|
const filtered = list.filter(x => String(x.id) !== String(item.id));
|
||||||
|
const newList = [item, ...filtered];
|
||||||
console.log('Adding to favorites:', JSON.stringify(item));
|
console.log('Adding to favorites:', JSON.stringify(item));
|
||||||
await setJson(KEY_FAVORITES_ITEMS, newList);
|
await setJson(KEY_FAVORITES_ITEMS, newList);
|
||||||
}
|
}
|
||||||
|
|||||||
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 的說明
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
- **已完成编码(阶段性)**:
|
- **已完成编码(阶段性)**:
|
||||||
- 客户端:新增 `client_user_id`(UUID v4)生成与持久化;每日提醒次数范围修正为 **0~5**(0 表示关闭)
|
- 客户端:新增 `client_user_id`(UUID v4)生成与持久化;每日提醒次数范围修正为 **0~5**(0 表示关闭)
|
||||||
- 客户端:Onboarding 结束页(每日提醒)在用户选择次数 > 0 时**直接触发系统权限申请**;授权后获取 Expo Push Token 并调用后端 `register/preferences`(移除单独的 push 引导页)
|
- 客户端:Onboarding 结束页(每日提醒)在用户选择次数 > 0 时**直接触发系统权限申请**;授权后获取 Expo Push Token 并调用后端 `register/preferences`(移除单独的 push 引导页)
|
||||||
|
- 客户端:Onboarding 问卷完成后“开通推送权限”流程增加 **loading 态**(完成按钮转圈 + 全页禁用交互,避免重复触发/重复上报)
|
||||||
- 客户端:个人主页“每日提醒”弹窗移除测试模式强制无权限逻辑,改为真实读取系统权限;并在开关/点击 OK 时同步后端偏好
|
- 客户端:个人主页“每日提醒”弹窗移除测试模式强制无权限逻辑,改为真实读取系统权限;并在开关/点击 OK 时同步后端偏好
|
||||||
- 客户端:新增推送接口封装 `client/src/services/pushApi.ts`(token 获取、register/preferences/get、自动上报时区与 locale,并携带用户画像供后端 Push 模板使用)
|
- 客户端:新增推送接口封装 `client/src/services/pushApi.ts`(token 获取、register/preferences/get、自动上报时区与 locale,并携带用户画像供后端 Push 模板使用)
|
||||||
- 后端:新增 Push 数据模型 + Alembic 迁移(`push_tokens` / `push_preferences` / `push_send_log`)
|
- 后端:新增 Push 数据模型 + Alembic 迁移(`push_tokens` / `push_preferences` / `push_send_log`)
|
||||||
@@ -89,6 +90,8 @@
|
|||||||
- 推送 entitlements 的 `aps-environment` 已切到 `production`(用于 TestFlight/线上包)
|
- 推送 entitlements 的 `aps-environment` 已切到 `production`(用于 TestFlight/线上包)
|
||||||
- 清理未接入编译的 WidgetKit 骨架残留:移除磁盘上的 `client/ios/MindfulnessWidget/` 文件,并从 `client/ios/client.xcodeproj/project.pbxproj` 删除对应工程引用(避免 Xcode 显示幽灵文件)
|
- 清理未接入编译的 WidgetKit 骨架残留:移除磁盘上的 `client/ios/MindfulnessWidget/` 文件,并从 `client/ios/client.xcodeproj/project.pbxproj` 删除对应工程引用(避免 Xcode 显示幽灵文件)
|
||||||
- 修复 Xcode Archive 偶发显示 “Generic Xcode Archive”:在共享 scheme `Hey Mama` 的 Archive Post-actions 自动补齐 `.xcarchive/Info.plist` 的 `ApplicationProperties`,并在缺失时补齐 `Name`/`SchemeName` + 自检提示(根治 Organizer 无法识别主 App、无法分发/上传 TestFlight 的问题)
|
- 修复 Xcode Archive 偶发显示 “Generic Xcode Archive”:在共享 scheme `Hey Mama` 的 Archive Post-actions 自动补齐 `.xcarchive/Info.plist` 的 `ApplicationProperties`,并在缺失时补齐 `Name`/`SchemeName` + 自检提示(根治 Organizer 无法识别主 App、无法分发/上传 TestFlight 的问题)
|
||||||
|
- Widget 名称与描述支持多语言(TC/EN,默认 EN):Widget Extension 增加 `Localizable.strings`(`en.lproj` / `zh-Hant.lproj`),`EmotionWidget.swift` 使用本地化 key 作为 `.configurationDisplayName/.description`
|
||||||
|
- 个人主页弹窗:小工具入口**暂时隐藏**锁屏小工具说明;桌面小工具引导弹窗标题(繁中/TC)更新为“**如何加入小工具**”(并统一弹窗标题使用该文案);品牌文案“正念”改为 **Hey Mama**(含引导搜索词与 Widget 标题)
|
||||||
|
|
||||||
## Text Wrap
|
## Text Wrap
|
||||||
|
|
||||||
@@ -169,6 +172,10 @@
|
|||||||
- `spec_kit/Text Wrap/modules/integration/tasks.md`
|
- `spec_kit/Text Wrap/modules/integration/tasks.md`
|
||||||
- **接入情况**:
|
- **接入情况**:
|
||||||
- Home(APP):已在 `client/app/(app)/home.tsx` 接入 `wrapText()` 渲染 `wrappedText`(含 `\n`)
|
- Home(APP):已在 `client/app/(app)/home.tsx` 接入 `wrapText()` 渲染 `wrappedText`(含 `\n`)
|
||||||
|
- iOS Widget(WidgetKit):App 侧在写入 `widget.dailyReco.v1` 缓存时,额外生成 `wrapped_text_by_family`(small/medium/large)并写入 App Group;Widget 侧按 `WidgetFamily` 优先读取该字段渲染(保证换行一致且无需在 Extension 内跑 JS)
|
||||||
|
- **近期变更**:
|
||||||
|
- Widget 接入:`client/src/modules/dailyWidgetReco/index.ts` 生成 `wrapped_text_by_family`;`client/ios/情绪小组件/EmotionWidget.swift` 按 family 读取;`client/app/(app)/home.tsx` 前台触发一次“尽力而为”的补齐/刷新
|
||||||
|
- Home 排版风格微调:支持 `scoringOverrides`,在 Home 里对 TC 做“更偏好标点停顿/更好看”的权重与理想宽度微调(不影响默认 v1)
|
||||||
|
|
||||||
## Splash Consent
|
## Splash Consent
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user