Compare commits
14 Commits
damer
...
decc7f9564
| Author | SHA1 | Date | |
|---|---|---|---|
| decc7f9564 | |||
| 173cee75d5 | |||
|
|
076bd5636f | ||
|
|
154f347ddb | ||
|
|
dec3ac82e1 | ||
|
|
e552e22de9 | ||
|
|
1fbc0aa3f8 | ||
|
|
b5532df161 | ||
| 5515726465 | |||
|
|
ce018880f4 | ||
|
|
b4ec17fcac | ||
|
|
aa4e1e9947 | ||
| 4578d503e7 | |||
| 66241e5231 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Hey Mama",
|
"name": "Dear Mama",
|
||||||
"slug": "client",
|
"slug": "client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
|
|||||||
@@ -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));
|
||||||
await addFavorite(favItem);
|
try {
|
||||||
|
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);
|
||||||
await setReaction(item.id, 'like');
|
try {
|
||||||
|
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',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnai
|
|||||||
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
|
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
|
||||||
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
|
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
|
||||||
import { fetchRecoFeed } from '@/src/services/recoApi';
|
import { fetchRecoFeed } from '@/src/services/recoApi';
|
||||||
import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi';
|
import { ensurePushTokenRegisteredIfPermitted, setPushPreferences } from '@/src/services/pushApi';
|
||||||
import {
|
import {
|
||||||
recordRecoFeedServed,
|
recordRecoFeedServed,
|
||||||
setOnboardingCompleted,
|
setOnboardingCompleted,
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -124,7 +128,8 @@ export default function OnboardingScreen() {
|
|||||||
await setPushPromptState('unknown');
|
await setPushPromptState('unknown');
|
||||||
try {
|
try {
|
||||||
const { status } = await Notifications.requestPermissionsAsync();
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
// iOS 可能出现 provisional(临时授权),也应视为“已授权”
|
||||||
|
if (status !== 'granted' && status !== ('provisional' as any)) {
|
||||||
await setPushPromptState('skipped');
|
await setPushPromptState('skipped');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -136,10 +141,8 @@ export default function OnboardingScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) 获取 Expo Push Token(失败才认为“推送开启失败”)
|
// 1) 上报 token 到后端(幂等;失败才认为“推送开启失败”)
|
||||||
const expoPushToken = await getExpoPushTokenOrThrow();
|
await ensurePushTokenRegisteredIfPermitted();
|
||||||
// 2) 上报 token 到后端(幂等;失败才认为“推送开启失败”)
|
|
||||||
await registerPushToken({ pushToken: expoPushToken });
|
|
||||||
|
|
||||||
// 3) 上报推送偏好(幂等)
|
// 3) 上报推送偏好(幂等)
|
||||||
// 注意:这一步失败时,后端仍可能已成功接收 token。
|
// 注意:这一步失败时,后端仍可能已成功接收 token。
|
||||||
@@ -159,9 +162,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 +181,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 +213,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 +223,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 +247,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,28 @@ 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(() => {
|
||||||
|
// 兜底:当用户在系统弹窗/系统设置里变更权限后,App 回到前台时再同步一次 token
|
||||||
|
const sub = AppState.addEventListener('change', (state) => {
|
||||||
|
if (state !== 'active') return;
|
||||||
|
ensurePushTokenRegisteredIfPermitted().catch(() => {
|
||||||
|
// ignore:不阻塞
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => sub.remove();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 字体与 i18n 都准备好后,允许渲染 App(原生 splash 的隐藏交给 onLayout,避免“硬切/闪白”)
|
// 字体与 i18n 都准备好后,允许渲染 App(原生 splash 的隐藏交给 onLayout,避免“硬切/闪白”)
|
||||||
if (loaded && i18nReady) setAppReady(true);
|
if (loaded && i18nReady) setAppReady(true);
|
||||||
@@ -113,7 +136,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, setPushPreferences } from '@/src/services/pushApi';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -430,14 +430,13 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
|
|||||||
// 调试:打印状态
|
// 调试:打印状态
|
||||||
console.log('Push Permission Status:', status);
|
console.log('Push Permission Status:', status);
|
||||||
|
|
||||||
if (status === 'granted') {
|
if (status === 'granted' || (status as any) === 'provisional') {
|
||||||
setPushEnabled(true);
|
setPushEnabled(true);
|
||||||
setHasSystemPermission(true);
|
setHasSystemPermission(true);
|
||||||
|
|
||||||
// 获取 token 并上报后端(幂等)
|
// 获取 token 并上报后端(幂等)
|
||||||
try {
|
try {
|
||||||
const expoPushToken = await getExpoPushTokenOrThrow();
|
await ensurePushTokenRegisteredIfPermitted();
|
||||||
await registerPushToken({ pushToken: expoPushToken });
|
|
||||||
// 偏好同步失败不应被用户感知为“开启失败”
|
// 偏好同步失败不应被用户感知为“开启失败”
|
||||||
// (常见现象:后端已接收 token,但偏好接口短暂失败/超时)
|
// (常见现象:后端已接收 token,但偏好接口短暂失败/超时)
|
||||||
try {
|
try {
|
||||||
@@ -479,6 +478,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 +559,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 +578,12 @@ function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<View style={styles.widgetScroll}>
|
<View style={styles.widgetScroll}>
|
||||||
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
{showLockScreenWidget ? (
|
||||||
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
|
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
|
||||||
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
|
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
|
||||||
</Pressable>
|
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
|
||||||
|
</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}>
|
||||||
<Text style={styles.questionTitle}>{title}</Text>
|
<View style={styles.titleBlock}>
|
||||||
|
{showGreeting && (
|
||||||
|
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
|
||||||
|
)}
|
||||||
|
<Text style={styles.questionTitle}>{title}</Text>
|
||||||
|
</View>
|
||||||
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
|
<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}>
|
||||||
<BtnClicked width={87} height={57} />
|
<View style={styles.finishWrap}>
|
||||||
</TouchableOpacity>
|
{loading ? (
|
||||||
|
<LinearGradient
|
||||||
<TouchableOpacity onPress={onSkip} activeOpacity={0.8} style={styles.skipBtn}>
|
colors={['#F69F7B', '#F99CC0']}
|
||||||
<Text style={styles.skipText}>{t('onboarding.skip')}</Text>
|
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} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</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',
|
||||||
|
|||||||
@@ -2540,30 +2540,30 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
EXApplication: 13420f8139864183f8a04fd6099077bdf8cfb186
|
EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f
|
||||||
EXConstants: 3feb66fd1d94202fc1f0946d74e029d8b224b60e
|
EXConstants: fce59a631a06c4151602843667f7cfe35f81e271
|
||||||
EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd
|
EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd
|
||||||
EXManifests: 83ef0844fcf06d6099b12a7bdbd7d36fc0e1dd16
|
EXManifests: a8d97683e5c7a3b026ffbd58559c64dc655b747b
|
||||||
EXNotifications: 2a3feb7af6194828d9aafda72f63a9a03866230a
|
EXNotifications: 9eec98712cc814ceff916d876cb53859003b0597
|
||||||
Expo: b8d64eb9a496ebe8c71e3dae7eeb7f394b146b80
|
Expo: 4e503a041c59c4e34c8be262a135848ad5cd3710
|
||||||
expo-dev-client: 12ef7d5b14d93e309922acea78dcd851db583a87
|
expo-dev-client: 425ee077d6754a98cfe3a2e2410d29b440b24c9d
|
||||||
expo-dev-launcher: 47994056008ffdc30a6a5e328a375b3e30a8db05
|
expo-dev-launcher: a4f4cdef064ab1fb8621e5b8c7c457cd6e9568c3
|
||||||
expo-dev-menu: ea4fb803ace52e60d7cd8060c7cd379612a140b2
|
expo-dev-menu: 05b18812110c175814c6af0d09dd658abcc5e00d
|
||||||
expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533
|
expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533
|
||||||
ExpoAsset: d999f3bbd998a750f3b74cb913229848901b926b
|
ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18
|
||||||
ExpoCrypto: 4d23a9ff67c25e2ed23ca792d81e58817a7ea1b9
|
ExpoCrypto: b6105ebaa15d6b38a811e71e43b52cd934945322
|
||||||
ExpoDevice: 0773c782b055558ca9b40b74aa4a8133a66cd0d2
|
ExpoDevice: 6327c3c200816795708885adf540d26ecab83d1a
|
||||||
ExpoFileSystem: aefcd337b94b874f88752ebefc52813b84992fad
|
ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063
|
||||||
ExpoFont: c625dbd97ed57e9089b172b2a7bb99003d074664
|
ExpoFont: f543ce20a228dd702813668b1a07b46f51878d47
|
||||||
ExpoHead: b691a2ed7ab02ed820b6c6468941832d34969c29
|
ExpoHead: 4425246bc93411f0fe7f6945f95f698e91db8780
|
||||||
ExpoKeepAwake: 44bf6715bc1d2ddb17afe19d927cd039cda123f0
|
ExpoKeepAwake: 55f75eca6499bb9e4231ebad6f3e9cb8f99c0296
|
||||||
ExpoLinearGradient: 814a21fc4056c3cf606e4f19e31e47074c5b5a86
|
ExpoLinearGradient: 809102bdb979f590083af49f7fa4805cd931bd58
|
||||||
ExpoLinking: ebf543fd411d56375cb4eee07f6ab4e31c7ad959
|
ExpoLinking: 8f0aaf69aa56f832913030503b6263dc6f647f37
|
||||||
ExpoLocalization: 6ac6f326210f0a3141ef6f58ab8f8f4ed003b485
|
ExpoLocalization: d9168d5300a5b03e5e78b986124d11fb6ec3ebbd
|
||||||
ExpoModulesCore: 77496909fd3c800f97f7f2007dd26aeac4bb3798
|
ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583
|
||||||
ExpoSplashScreen: 72fbc6dd9d6404dd9d0725a56c9ac1383bc0b14f
|
ExpoSplashScreen: bc3cffefca2716e5f22350ca109badd7e50ec14d
|
||||||
ExpoWebBrowser: 88b116cd378d9609c776c0903fe4070fca461588
|
ExpoWebBrowser: 17b064c621789e41d4816c95c93f429b84971f52
|
||||||
EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b
|
EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734
|
||||||
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
|
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
|
||||||
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
|
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
|
||||||
RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990
|
RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990
|
||||||
@@ -2571,72 +2571,72 @@ SPEC CHECKSUMS:
|
|||||||
RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c
|
RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c
|
||||||
React: 914f8695f9bf38e6418228c2ffb70021e559f92f
|
React: 914f8695f9bf38e6418228c2ffb70021e559f92f
|
||||||
React-callinvoker: 1c0808402aee0c6d4a0d8e7220ce6547af9fba71
|
React-callinvoker: 1c0808402aee0c6d4a0d8e7220ce6547af9fba71
|
||||||
React-Core: 4ae98f9e8135b8ddbd7c98730afb6fdae883db90
|
React-Core: c61410ef0ca6055e204a963992e363227e0fd1c5
|
||||||
React-Core-prebuilt: 8f4cca589c14e8cf8fc6db4587ef1c2056b5c151
|
React-Core-prebuilt: 02f0ad625ddd47463c009c2d0c5dd35c0d982599
|
||||||
React-CoreModules: e878a90bb19b8f3851818af997dbae3b3b0a27ac
|
React-CoreModules: 1f6d1744b5f9f2ec684a4bb5ced25370f87e5382
|
||||||
React-cxxreact: 28af9844f6dc87be1385ab521fbfb3746f19563c
|
React-cxxreact: 3af79478e8187b63ffc22b794cd42d3fc1f1f2da
|
||||||
React-debug: 6328c2228e268846161f10082e80dc69eac2e90a
|
React-debug: 6328c2228e268846161f10082e80dc69eac2e90a
|
||||||
React-defaultsnativemodule: afc9d809ec75780f39464a6949c07987fbea488c
|
React-defaultsnativemodule: d635ef36d755321e5d6fc065bd166b2c5a0e9833
|
||||||
React-domnativemodule: 91a233260411d41f27f67aa1358b7f9f0bfd101d
|
React-domnativemodule: dd28f6d96cd21236e020be2eff6fe0b7d4ec3b66
|
||||||
React-Fabric: 21f349b5e93f305a3c38c885902683a9c79cf983
|
React-Fabric: 2e32c3fdbb1fbcf5fde54607e3abe453c6652ce2
|
||||||
React-FabricComponents: 47ac634cc9ecc64b30a9997192f510eebe4177e4
|
React-FabricComponents: 5ed0cdb81f6b91656cb4d3be432feaa28a58071a
|
||||||
React-FabricImage: 21873acd6d4a51a0b97c133141051c7acb11cc86
|
React-FabricImage: 2bc714f818cb24e454f5d3961864373271b2faf8
|
||||||
React-featureflags: 653f469f0c3c9dc271d610373e3b6e66a9fd847d
|
React-featureflags: 847642f41fa71ad4eec5e0351badebcad4fe6171
|
||||||
React-featureflagsnativemodule: c91a8a3880e0f4838286402241ead47db43aed28
|
React-featureflagsnativemodule: c868a544b2c626fa337bcbd364b1befe749f0d3f
|
||||||
React-graphics: b4bdb0f635b8048c652a5d2b73eb8b1ddd950f24
|
React-graphics: 192ec701def5b3f2a07db2814dfba5a44986cff6
|
||||||
React-hermes: fcfad3b917400f49026f3232561e039c9d1c34bf
|
React-hermes: e875778b496c86d07ab2ccaa36a9505d248a254b
|
||||||
React-idlecallbacksnativemodule: 8cb83207e39f8179ac1d344b6177c6ab3ccebcdc
|
React-idlecallbacksnativemodule: 4d57965cdf82c14ee3b337189836cd8491632b76
|
||||||
React-ImageManager: 396128004783fc510e629124dce682d38d1088e7
|
React-ImageManager: bd0b99e370b13de82c9cd15f0f08144ff3de079e
|
||||||
React-jserrorhandler: b58b788d788cdbf8bda7db74a88ebfcffc8a0795
|
React-jserrorhandler: a2fdef4cbcfdcdf3fa9f5d1f7190f7fd4535248d
|
||||||
React-jsi: d2c3f8555175371c02da6dfe7ed1b64b55a9d6c0
|
React-jsi: 89d43d1e7d4d0663f8ba67e0b39eb4e4672c27de
|
||||||
React-jsiexecutor: ba537434eb45ee018b590ed7d29ee233fddb8669
|
React-jsiexecutor: abe4874aaab90dfee5dec480680220b2f8af07e3
|
||||||
React-jsinspector: f21b6654baf96cb9f71748844a32468a5f73ad51
|
React-jsinspector: a0b3e051aef842b0b2be2353790ae2b2a5a65a8f
|
||||||
React-jsinspectorcdp: 3f8be4830694c3c1c39442e50f8db877966d43f0
|
React-jsinspectorcdp: 6346013b2247c6263fbf5199adf4a8751e53bd89
|
||||||
React-jsinspectornetwork: 70e41469565712ad60e11d9c8b8f999b9f7f61eb
|
React-jsinspectornetwork: 26281aa50d49fc1ec93abf981d934698fa95714f
|
||||||
React-jsinspectortracing: eccf9bfa4ec7f130d514f215cfb2222dc3c0e270
|
React-jsinspectortracing: 55eedf6d57540507570259a778663b90060bbd6e
|
||||||
React-jsitooling: b376a695f5a507627f7934748533b24eed1751ca
|
React-jsitooling: 0e001113fa56d8498aa8ac28437ac0d36348e51a
|
||||||
React-jsitracing: 5c8c3273dda2d95191cc0612fb5e71c4d9018d2a
|
React-jsitracing: b713793eb8a5bbc4d86a84e9d9e5023c0f58cbaf
|
||||||
React-logger: c3e2f8a2e284341205f61eef3d4677ab5a309dfd
|
React-logger: 50fdb9a8236da90c0b1072da5c32ee03aeb5bf28
|
||||||
React-Mapbuffer: 603c18db65844bb81dbe62fee8fcc976eaeb7108
|
React-Mapbuffer: 9050ee10c19f4f7fca8963d0211b2854d624973e
|
||||||
React-microtasksnativemodule: d77e0c426fce34c23227394c96ca1033b30c813c
|
React-microtasksnativemodule: f775db9e991c6f3b8ccbc02bfcde22770f96e23b
|
||||||
react-native-safe-area-context: 53f796cb6c814661bbe99fbdfd0585d07b996cdd
|
react-native-safe-area-context: 37e680fc4cace3c0030ee46e8987d24f5d3bdab2
|
||||||
React-NativeModulesApple: 1664340b8750d64e0ef3907c5e53d9481f74bcbd
|
React-NativeModulesApple: 8969913947d5b576de4ed371a939455a8daf28aa
|
||||||
React-oscompat: ce47230ed20185e91de62d8c6d139ae61763d09c
|
React-oscompat: ce47230ed20185e91de62d8c6d139ae61763d09c
|
||||||
React-perflogger: b1af3cfb3f095f819b2814910000392a8e17ba9f
|
React-perflogger: 02b010e665772c7dcb859d85d44c1bfc5ac7c0e4
|
||||||
React-performancetimeline: f9ec65b77bcadbc7bd8b47a6f4b4b697da7b1490
|
React-performancetimeline: 130db956b5a83aa4fb41ddf5ae68da89f3fb1526
|
||||||
React-RCTActionSheet: 0b14875b3963e9124a5a29a45bd1b22df8803916
|
React-RCTActionSheet: 0b14875b3963e9124a5a29a45bd1b22df8803916
|
||||||
React-RCTAnimation: 60f6eca214a62b9673f64db6df3830cee902b5af
|
React-RCTAnimation: a7b90fd2af7bb9c084428867445a1481a8cb112e
|
||||||
React-RCTAppDelegate: 37734b39bac108af30a0fd9d3e1149ec68b82c28
|
React-RCTAppDelegate: 3262bedd01263f140ec62b7989f4355f57cec016
|
||||||
React-RCTBlob: 83fbcbd57755caf021787324aac2fe9b028cc264
|
React-RCTBlob: c17531368702f1ebed5d0ada75a7cf5915072a53
|
||||||
React-RCTFabric: a05cb1df484008db3753c8b4a71e4c6d9f1e43a6
|
React-RCTFabric: 6409edd8cfdc3133b6cc75636d3b858fdb1d11ea
|
||||||
React-RCTFBReactNativeSpec: d58d7ae9447020bbbac651e3b0674422aba18266
|
React-RCTFBReactNativeSpec: c004b27b4fa3bd85878ad2cf53de3bbec85da797
|
||||||
React-RCTImage: 47aba3be7c6c64f956b7918ab933769602406aac
|
React-RCTImage: c68078a120d0123f4f07a5ac77bea3bb10242f32
|
||||||
React-RCTLinking: 2dbaa4df2e4523f68baa07936bd8efdfa34d5f31
|
React-RCTLinking: cf8f9391fe7fe471f96da3a5f0435235eca18c5b
|
||||||
React-RCTNetwork: 1fca7455f9dedf7de2b95bec438da06680f3b000
|
React-RCTNetwork: ca31f7c879355760c2d9832a06ee35f517938a20
|
||||||
React-RCTRuntime: 17819dd1dfc8613efaf4cbb9d8686baae4a83e5b
|
React-RCTRuntime: a6cf4a1e42754fc87f493e538f2ac6b820e45418
|
||||||
React-RCTSettings: 01bf91c856862354d3d2f642ccb82f3697a4284a
|
React-RCTSettings: e0e140b2ff4bf86d34e9637f6316848fc00be035
|
||||||
React-RCTText: cb576a3797dcb64933613c522296a07eaafc0461
|
React-RCTText: 75915bace6f7877c03a840cc7b6c622fb62bfa6b
|
||||||
React-RCTVibration: 560af8c086741f3525b8456a482cdbe27f9d098e
|
React-RCTVibration: 25f26b85e5e432bb3c256f8b384f9269e9529f25
|
||||||
React-rendererconsistency: 2dac03f448ff337235fd5820b10f81633328870d
|
React-rendererconsistency: 2dac03f448ff337235fd5820b10f81633328870d
|
||||||
React-renderercss: c5c6b7a15948dd28facca39a18ac269073718490
|
React-renderercss: 477da167bb96b5ac86d30c5d295412fb853f5453
|
||||||
React-rendererdebug: 3c9d5e1634273f5a24d84cc5669f290ce0bdc812
|
React-rendererdebug: 2a1798c6f3ef5f22d466df24c33653edbabb5b89
|
||||||
React-RuntimeApple: 887637d1e12ea8262df7d32bc100467df2302613
|
React-RuntimeApple: 28cf4d8eb18432f6a21abbed7d801ab7f6b6f0b4
|
||||||
React-RuntimeCore: 91f779835dc4f8f84777fe5dd24f1a22f96454e4
|
React-RuntimeCore: 41bf0fd56a00de5660f222415af49879fa49c4f0
|
||||||
React-runtimeexecutor: 8bb6b738f37b0ada4a6269e6f8ab1133dea0285c
|
React-runtimeexecutor: 1afb774dde3011348e8334be69d2f57a359ea43e
|
||||||
React-RuntimeHermes: 4cb93de9fa8b1cc753d200dbe61a01b9ec5f5562
|
React-RuntimeHermes: f3b158ea40e8212b1a723a68b4315e7a495c5fc6
|
||||||
React-runtimescheduler: 83dc28f530bfbd2fce84ed13aa7feebdc24e5af7
|
React-runtimescheduler: 3e1e2bec7300bae512533107d8e54c6e5c63fe0f
|
||||||
React-timing: 03c7217455d2bff459b27a3811be25796b600f47
|
React-timing: 6fa9883de2e41791e5dc4ec404e5e37f3f50e801
|
||||||
React-utils: 6d46795ae0444ec8a5d9a5f201157b286bf5250a
|
React-utils: 6e2035b53d087927768649a11a26c4e092448e34
|
||||||
ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee
|
ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79
|
||||||
ReactCodegen: 4c44b74b77fc41ae25b9e2c7e9bd6e2bc772c23f
|
ReactCodegen: 7d4593f7591f002d137fe40cef3f6c11f13c88cc
|
||||||
ReactCommon: e6e232202a447d353e5531f2be82f50f47cbaa9a
|
ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8
|
||||||
ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a
|
ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a
|
||||||
RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4
|
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
|
||||||
RNGestureHandler: 40c2d1c168e54715fe52e0fb16cb38c54611e4f3
|
RNGestureHandler: e0d0bce5599f6120b7adf90c38d2805e2935795f
|
||||||
RNReanimated: 43f611f1c85c90e0273df7399bf1536f8e2bd125
|
RNReanimated: e5c702a3e24cc1c68b2de67671713f35461678f4
|
||||||
RNScreens: dd61bc3a3e6f6901ad833efa411917d44827cf51
|
RNScreens: d8d6f1792f6e7ac12b0190d33d8d390efc0c1845
|
||||||
RNSVG: 2825ee146e0f6a16221e852299943e4cceef4528
|
RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34
|
||||||
RNWorklets: 28ee7370ca8da356fcc914e3e68b97e9752196d2
|
RNWorklets: 76fce72926e28e304afb44f0da23b2d24f2c1fa0
|
||||||
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
||||||
|
|
||||||
PODFILE CHECKSUM: c2c3838f0b2a579fef2350bff2ecaa005e27145d
|
PODFILE CHECKSUM: c2c3838f0b2a579fef2350bff2ecaa005e27145d
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 56;
|
||||||
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, ); }; };
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
13B07F961A680F5B00A75B9A /* HeyMama.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HeyMama.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
13B07F961A680F5B00A75B9A /* DearMama.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DearMama.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = client/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = client/Images.xcassets; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
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; };
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
13B07F961A680F5B00A75B9A /* HeyMama.app */,
|
13B07F961A680F5B00A75B9A /* DearMama.app */,
|
||||||
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */,
|
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
);
|
);
|
||||||
name = client;
|
name = client;
|
||||||
productName = client;
|
productName = client;
|
||||||
productReference = 13B07F961A680F5B00A75B9A /* HeyMama.app */;
|
productReference = 13B07F961A680F5B00A75B9A /* DearMama.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */ = {
|
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */ = {
|
||||||
@@ -301,6 +301,7 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
"zh-Hant",
|
||||||
);
|
);
|
||||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||||
@@ -526,7 +527,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
||||||
PRODUCT_NAME = HeyMama;
|
PRODUCT_NAME = DearMama;
|
||||||
SKIP_INSTALL = NO;
|
SKIP_INSTALL = NO;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -567,7 +568,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
||||||
PRODUCT_NAME = HeyMama;
|
PRODUCT_NAME = DearMama;
|
||||||
SKIP_INSTALL = NO;
|
SKIP_INSTALL = NO;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
customArchiveName = "Hey Mama"
|
customArchiveName = "Dear Mama"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
<PostActions>
|
<PostActions>
|
||||||
<ExecutionAction
|
<ExecutionAction
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Hey Mama</string>
|
<string>Dear Mama</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ if [[ -z "$APP_PLIST" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
APP_DIR="$(/usr/bin/dirname "$APP_PLIST")"
|
APP_DIR="$(/usr/bin/dirname "$APP_PLIST")"
|
||||||
APP_NAME="$(/usr/bin/basename "$APP_DIR")" # 例如 HeyMama.app
|
APP_NAME="$(/usr/bin/basename "$APP_DIR")" # 例如 DearMama.app
|
||||||
APP_REL_PATH="Applications/$APP_NAME"
|
APP_REL_PATH="Applications/$APP_NAME"
|
||||||
|
|
||||||
bundle_id="$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "$APP_PLIST" 2>/dev/null || true)"
|
bundle_id="$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "$APP_PLIST" 2>/dev/null || true)"
|
||||||
|
|||||||
@@ -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" : "正念"
|
// 需求:品牌文案统一为 Dear Mama
|
||||||
|
return "Dear 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,16 +83,44 @@ 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] {
|
||||||
return (dayKey: dayKey, lang: lang, text: text)
|
if let text = pickWidgetText(item: item, family: family), !text.isEmpty {
|
||||||
|
return (dayKey: dayKey, lang: lang, text: text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func familyKey(_ family: WidgetFamily) -> String {
|
||||||
|
switch family {
|
||||||
|
case .systemSmall:
|
||||||
|
return "small"
|
||||||
|
case .systemMedium:
|
||||||
|
return "medium"
|
||||||
|
case .systemLarge:
|
||||||
|
return "large"
|
||||||
|
default:
|
||||||
|
return "small"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pickWidgetText(item: [String: Any], family: WidgetFamily?) -> String? {
|
||||||
|
// 优先使用 App 侧预换行的结果(wrapped_text_by_family),否则回退 raw text
|
||||||
|
if let family = family,
|
||||||
|
let wrappedByFamily = item["wrapped_text_by_family"] as? [String: Any] {
|
||||||
|
let key = familyKey(family)
|
||||||
|
if let v = wrappedByFamily[key] as? String, !v.isEmpty {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let raw = item["text"] as? String, !raw.isEmpty { return raw }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private func readApiBaseUrl() -> String? {
|
private func readApiBaseUrl() -> String? {
|
||||||
guard let d = readJsonDict(forKey: keyWidgetConfig) else { return nil }
|
guard let d = readJsonDict(forKey: keyWidgetConfig) else { return nil }
|
||||||
let base = d["apiBaseUrl"] as? String
|
let base = d["apiBaseUrl"] as? String
|
||||||
@@ -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" = "一段溫柔提醒,陪你回到當下。";
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Metro 配置:支持 import 本地 .svg 为 React 组件
|
// Metro 配置:支持 import 本地 .svg 为 React 组件
|
||||||
// 说明:Expo SDK 54 + react-native-svg-transformer 的常见配置方式
|
// 说明:Expo SDK 54 + react-native-svg-transformer 的常见配置方式
|
||||||
|
const path = require('path');
|
||||||
const { getDefaultConfig } = require('expo/metro-config');
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
|
||||||
/** @type {import('expo/metro-config').MetroConfig} */
|
/** @type {import('expo/metro-config').MetroConfig} */
|
||||||
@@ -14,6 +15,10 @@ config.resolver = {
|
|||||||
...config.resolver,
|
...config.resolver,
|
||||||
assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
|
assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
|
||||||
sourceExts: [...config.resolver.sourceExts, 'svg'],
|
sourceExts: [...config.resolver.sourceExts, 'svg'],
|
||||||
|
// 确保 react-native-text-size 从项目 node_modules 解析(避免 Metro 解析不到)
|
||||||
|
extraNodeModules: {
|
||||||
|
'react-native-text-size': path.resolve(__dirname, 'node_modules/react-native-text-size'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
134
client/package-lock.json
generated
134
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",
|
||||||
@@ -9630,6 +9719,7 @@
|
|||||||
"version": "4.0.0-rc.1",
|
"version": "4.0.0-rc.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-native-text-size/-/react-native-text-size-4.0.0-rc.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-native-text-size/-/react-native-text-size-4.0.0-rc.1.tgz",
|
||||||
"integrity": "sha512-CysqjU2jK6Yc+a+kEI222pUyTY2ywcU2HqbFqf1KHymW6OPTdvBBHqbEJKL0QiLhQaFYDbqicM+h990s9TP00g==",
|
"integrity": "sha512-CysqjU2jK6Yc+a+kEI222pUyTY2ywcU2HqbFqf1KHymW6OPTdvBBHqbEJKL0QiLhQaFYDbqicM+h990s9TP00g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react-native": ">=0.59.0"
|
"react-native": ">=0.59.0"
|
||||||
}
|
}
|
||||||
@@ -10122,6 +10212,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 +10508,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 +10654,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 +10688,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 +11036,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 +11099,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 +11618,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 +11693,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 +11711,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 +11738,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 +11753,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 +11831,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 +11964,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 \"Dear Mama\"",
|
||||||
|
"ios:clean": "npm run clean:cache && npm run clean:ios-build && expo run:ios --scheme \"Dear 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;
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ type TextSizeMeasureParams = {
|
|||||||
|
|
||||||
type TextSizeMeasureResult = { width: number };
|
type TextSizeMeasureResult = { width: number };
|
||||||
|
|
||||||
async function loadReactNativeTextSize(): Promise<{
|
function loadReactNativeTextSize(): {
|
||||||
measure: (params: TextSizeMeasureParams) => Promise<TextSizeMeasureResult>;
|
measure: (params: TextSizeMeasureParams) => Promise<TextSizeMeasureResult>;
|
||||||
}> {
|
} {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 使用 require 确保 Metro 能解析并打包该原生模块(动态 import 在某些环境下无法被正确解析)
|
||||||
const mod: any = await import('react-native-text-size');
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
|
||||||
|
const mod: any = require('react-native-text-size');
|
||||||
return mod?.default ?? mod;
|
return mod?.default ?? mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ function toTextSizeFontSpecs(fontSpec: FontSpec): Pick<TextSizeMeasureParams, 'f
|
|||||||
* - usePreciseWidth=true,取更精确的宽度(开销更大,但对本算法更稳定)
|
* - usePreciseWidth=true,取更精确的宽度(开销更大,但对本算法更稳定)
|
||||||
*/
|
*/
|
||||||
export const defaultMeasureWidthImpl: MeasureWidthImpl = async ({ text, fontSpec }) => {
|
export const defaultMeasureWidthImpl: MeasureWidthImpl = async ({ text, fontSpec }) => {
|
||||||
const TextSize = await loadReactNativeTextSize();
|
const TextSize = loadReactNativeTextSize();
|
||||||
if (!TextSize || typeof TextSize.measure !== 'function') {
|
if (!TextSize || typeof TextSize.measure !== 'function') {
|
||||||
// 典型原因:在 Expo Go 中运行,或没有使用包含该原生模块的 Development Build。
|
// 典型原因:在 Expo Go 中运行,或没有使用包含该原生模块的 Development Build。
|
||||||
// 这里抛出更明确的错误,方便上层捕获并在 meta.reason=MEASURE_FAILED 时看到根因。
|
// 这里抛出更明确的错误,方便上层捕获并在 meta.reason=MEASURE_FAILED 时看到根因。
|
||||||
|
|||||||
@@ -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": "Dear 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 “Dear 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 “Dear Mama” → add a size you like."
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "You Are Perfect.",
|
"title": "Dear 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": "Dear 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": "搜尋「Dear Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||||
},
|
},
|
||||||
@@ -308,11 +313,12 @@
|
|||||||
"language": "語言",
|
"language": "語言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小工具",
|
"widgetTitle": "iOS 小工具",
|
||||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Dear 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": "Dear 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 “Dear 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": "Dear 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 “Dear 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": "Dear 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 “Dear 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": "Dear 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": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“Dear 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": "Dear 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": "搜尋「Dear 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": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Dear 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,
|
||||||
|
getLastRegisteredPushPayload,
|
||||||
|
getOrCreateClientUserId,
|
||||||
|
getUserProfileScoring,
|
||||||
|
setLastRegisteredPushPayload,
|
||||||
|
} 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,40 @@ export async function registerPushToken(args: { pushToken: string }): Promise<vo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSystemNotificationPermissionGranted(status: unknown): boolean {
|
||||||
|
// iOS 可能出现 provisional(临时授权),在 Push 场景也应视为“可获取 token 并上报”
|
||||||
|
return status === 'granted' || status === 'provisional';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensurePushTokenRegisteredIfPermitted(): Promise<{ ok: boolean; reason: string }> {
|
||||||
|
// 只要系统权限已授权,就应尽早把 token 写入后端(不依赖用户在“每日提醒”里点确认)
|
||||||
|
const settings = await Notifications.getPermissionsAsync();
|
||||||
|
if (!isSystemNotificationPermissionGranted(settings.status)) return { ok: false, reason: 'permission_not_granted' };
|
||||||
|
|
||||||
|
const clientUserId = await getOrCreateClientUserId();
|
||||||
|
const token = await getExpoPushTokenOrThrow();
|
||||||
|
const env = toPushEnv(APP_ENV);
|
||||||
|
const appId = pickAppId();
|
||||||
|
|
||||||
|
// 去重规则(更严格):
|
||||||
|
// - 只有当 token + client_user_id + env + app_id 全都一致时才跳过
|
||||||
|
// - 避免出现“client_user_id 变化但 token 没变,导致后端绑定不更新”的问题
|
||||||
|
const last = await getLastRegisteredPushPayload().catch(() => null);
|
||||||
|
if (
|
||||||
|
last &&
|
||||||
|
last.pushToken === token &&
|
||||||
|
last.clientUserId === clientUserId &&
|
||||||
|
last.env === env &&
|
||||||
|
last.appId === appId
|
||||||
|
) {
|
||||||
|
return { ok: true, reason: 'already_registered' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await registerPushToken({ pushToken: token });
|
||||||
|
await setLastRegisteredPushPayload({ pushToken: token, clientUserId, env, appId });
|
||||||
|
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,9 @@ 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'; // 旧:仅 token(保留兼容读取)
|
||||||
|
const KEY_PUSH_LAST_REGISTERED_AT = 'push.lastRegisteredAt'; // 旧:时间(保留兼容)
|
||||||
|
const KEY_PUSH_LAST_REGISTERED_PAYLOAD = 'push.lastRegisteredPayload'; // 新:token+client_user_id+env+app_id
|
||||||
|
|
||||||
export type PushPromptState = 'enabled' | 'skipped' | 'unknown';
|
export type PushPromptState = 'enabled' | 'skipped' | 'unknown';
|
||||||
export type Reaction = 'like' | 'dislike';
|
export type Reaction = 'like' | 'dislike';
|
||||||
@@ -69,6 +72,46 @@ 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 LastRegisteredPushPayload = {
|
||||||
|
pushToken: string;
|
||||||
|
clientUserId: string;
|
||||||
|
env: 'dev' | 'prod';
|
||||||
|
appId: string;
|
||||||
|
savedAt: string; // ISO8601
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getLastRegisteredPushPayload(): Promise<LastRegisteredPushPayload | null> {
|
||||||
|
const raw = await AsyncStorage.getItem(KEY_PUSH_LAST_REGISTERED_PAYLOAD);
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(raw) as Partial<LastRegisteredPushPayload>;
|
||||||
|
if (!obj || typeof obj !== 'object') return null;
|
||||||
|
if (!obj.pushToken || !obj.clientUserId || !obj.env || !obj.appId || !obj.savedAt) return null;
|
||||||
|
if (obj.env !== 'dev' && obj.env !== 'prod') return null;
|
||||||
|
return obj as LastRegisteredPushPayload;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setLastRegisteredPushPayload(payload: Omit<LastRegisteredPushPayload, 'savedAt'>): Promise<void> {
|
||||||
|
const savedAt = new Date().toISOString();
|
||||||
|
const full: LastRegisteredPushPayload = { ...payload, savedAt };
|
||||||
|
await AsyncStorage.setItem(KEY_PUSH_LAST_REGISTERED_PAYLOAD, JSON.stringify(full));
|
||||||
|
// 同时写入旧 key,便于兼容老逻辑/快速排查
|
||||||
|
await setLastRegisteredPushToken(payload.pushToken);
|
||||||
|
}
|
||||||
|
|
||||||
export type RecoFeedCacheItem = {
|
export type RecoFeedCacheItem = {
|
||||||
content_id: number;
|
content_id: number;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -188,7 +231,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 +243,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ async def get_privacy_policy(request: Request) -> HTMLResponse:
|
|||||||
accept_language = request.headers.get("accept-language")
|
accept_language = request.headers.get("accept-language")
|
||||||
lang = _resolve_lang(accept_language)
|
lang = _resolve_lang(accept_language)
|
||||||
content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang)
|
content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang)
|
||||||
title = "Hey Mama | Privacy Policy" if resolved == "en" else "Hey Mama|隱私權政策"
|
title = "Dear Mama | Privacy Policy" if resolved == "en" else "Dear Mama|隱私權政策"
|
||||||
page = render_as_simple_html(title=title, content=content)
|
page = render_as_simple_html(title=title, content=content)
|
||||||
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ async def get_terms_of_use(request: Request) -> HTMLResponse:
|
|||||||
accept_language = request.headers.get("accept-language")
|
accept_language = request.headers.get("accept-language")
|
||||||
lang = _resolve_lang(accept_language)
|
lang = _resolve_lang(accept_language)
|
||||||
content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang)
|
content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang)
|
||||||
title = "Hey Mama – Terms of Use" if resolved == "en" else "Hey Mama 使用條款"
|
title = "Dear Mama – Terms of Use" if resolved == "en" else "Dear Mama 使用條款"
|
||||||
page = render_as_simple_html(title=title, content=content)
|
page = render_as_simple_html(title=title, content=content)
|
||||||
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import httpx
|
|||||||
import redis
|
import redis
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.api.limits import rate_limit_push_by_ip
|
from app.api.limits import rate_limit_push_by_ip
|
||||||
@@ -194,9 +194,10 @@ async def put_preferences(req: PushPreferencesRequest, db: AsyncSession = Depend
|
|||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# 返回更新时间(从 ORM 读取到的可能不包含 server_onupdate,这里用 now 兜底)
|
# 返回更新时间:
|
||||||
updated_at = getattr(pref, "updated_at", None)
|
# - 某些运行环境/驱动组合下,commit 后访问 ORM 字段可能触发隐式 IO,导致 async 下报 MissingGreenlet。
|
||||||
updated_at_iso = updated_at.isoformat() if isinstance(updated_at, datetime) else None
|
# - 这里直接用当前时间兜底(字段本身为可选,仅用于前端展示)。
|
||||||
|
updated_at_iso = datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
return PushPreferencesResponse(
|
return PushPreferencesResponse(
|
||||||
client_user_id=req.client_user_id,
|
client_user_id=req.client_user_id,
|
||||||
@@ -256,7 +257,7 @@ async def test_push(
|
|||||||
_ = UserProfileV1_2.model_validate(pref.user_profile_json)
|
_ = UserProfileV1_2.model_validate(pref.user_profile_json)
|
||||||
|
|
||||||
# V1:先发固定测试文案;后续在定时任务中替换为推荐模块的 push 场景模板
|
# V1:先发固定测试文案;后续在定时任务中替换为推荐模块的 push 场景模板
|
||||||
title = req.title or "Hey Mama"
|
title = req.title or "Dear Mama"
|
||||||
body = req.body or "这是一条测试推送(dev)。"
|
body = req.body or "这是一条测试推送(dev)。"
|
||||||
|
|
||||||
expo_res = await _send_expo_push(to=token.push_token, title=title, body=body, data={"client_user_id": req.client_user_id})
|
expo_res = await _send_expo_push(to=token.push_token, title=title, body=body, data={"client_user_id": req.client_user_id})
|
||||||
@@ -296,6 +297,7 @@ async def scheduler_health(db: AsyncSession = Depends(get_db)) -> dict[str, Any]
|
|||||||
"worker": {"ok": False, "worker_count": 0},
|
"worker": {"ok": False, "worker_count": 0},
|
||||||
"beat": {"ok": False, "last_heartbeat_at": None, "age_seconds": None},
|
"beat": {"ok": False, "last_heartbeat_at": None, "age_seconds": None},
|
||||||
"db": {"ok": False, "push_send_log_latest_created_at": None},
|
"db": {"ok": False, "push_send_log_latest_created_at": None},
|
||||||
|
"db_push_tokens": {"ok": False, "count": None, "latest": None},
|
||||||
"now_utc": datetime.now(timezone.utc).isoformat(),
|
"now_utc": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,5 +344,36 @@ async def scheduler_health(db: AsyncSession = Depends(get_db)) -> dict[str, Any]
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
out["db"]["error"] = f"{type(e).__name__}: {e}"
|
out["db"]["error"] = f"{type(e).__name__}: {e}"
|
||||||
|
|
||||||
|
# 4) DB:查询 push_tokens 计数与最近一条(用于确认 /v1/push/register 是否真正落库)
|
||||||
|
try:
|
||||||
|
qcount = select(func.count()).select_from(PushToken)
|
||||||
|
rcount = await db.execute(qcount)
|
||||||
|
cnt = int(rcount.scalar_one() or 0)
|
||||||
|
|
||||||
|
qlatest = select(PushToken).order_by(PushToken.last_seen_at.desc()).limit(1)
|
||||||
|
rlatest = await db.execute(qlatest)
|
||||||
|
t = rlatest.scalar_one_or_none()
|
||||||
|
|
||||||
|
latest_obj = None
|
||||||
|
if t is not None:
|
||||||
|
tok = str(t.push_token or "")
|
||||||
|
masked = tok[:10] + "***" + tok[-6:] if len(tok) > 20 else (tok[:6] + "***" if tok else "")
|
||||||
|
latest_obj = {
|
||||||
|
"id": int(getattr(t, "id", 0) or 0),
|
||||||
|
"client_user_id": str(t.client_user_id),
|
||||||
|
"env": str(t.env),
|
||||||
|
"app_id": str(t.app_id),
|
||||||
|
"platform": str(t.platform),
|
||||||
|
"is_active": bool(t.is_active),
|
||||||
|
"last_seen_at": t.last_seen_at.isoformat() if t.last_seen_at else None,
|
||||||
|
"push_token_masked": masked,
|
||||||
|
}
|
||||||
|
|
||||||
|
out["db_push_tokens"]["ok"] = True
|
||||||
|
out["db_push_tokens"]["count"] = cnt
|
||||||
|
out["db_push_tokens"]["latest"] = latest_obj
|
||||||
|
except Exception as e:
|
||||||
|
out["db_push_tokens"]["error"] = f"{type(e).__name__}: {e}"
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ ResolvedLang = Literal["en", "tc"]
|
|||||||
# 协议原文(直接来自仓库中的 Markdown 文档)。
|
# 协议原文(直接来自仓库中的 Markdown 文档)。
|
||||||
# 说明:为了保证“点击一定有内容”,这里在后端内置了一份可展示的协议文本。
|
# 说明:为了保证“点击一定有内容”,这里在后端内置了一份可展示的协议文本。
|
||||||
# 线上若你们有官网/静态站点托管页面,可通过环境变量 LEGAL_* 覆盖为外部链接。
|
# 线上若你们有官网/静态站点托管页面,可通过环境变量 LEGAL_* 覆盖为外部链接。
|
||||||
PRIVACY_POLICY_MD = """Hey Mama | Privacy Policy
|
PRIVACY_POLICY_MD = """Dear Mama | Privacy Policy
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
|
|
||||||
1. Introduction
|
1. Introduction
|
||||||
Welcome to Hey Mama (“the App,” “we,” “us”).
|
Welcome to Dear Mama (“the App,” “we,” “us”).
|
||||||
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
||||||
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
||||||
|
|
||||||
2. Data Controller and Scope
|
2. Data Controller and Scope
|
||||||
The App is operated and maintained by the Hey Mama team.
|
The App is operated and maintained by the Dear Mama team.
|
||||||
This Privacy Policy applies to information processing activities related to your use of the App.
|
This Privacy Policy applies to information processing activities related to your use of the App.
|
||||||
|
|
||||||
3. Information We Collect
|
3. Information We Collect
|
||||||
3.1 Information You Provide
|
3.1 Information You Provide
|
||||||
Hey Mama does not require account registration and does not require you to provide personally identifiable information.
|
Dear Mama does not require account registration and does not require you to provide personally identifiable information.
|
||||||
During your use of the App, you may optionally provide or generate the following information:
|
During your use of the App, you may optionally provide or generate the following information:
|
||||||
- Reminder settings (e.g., reminder frequency)
|
- Reminder settings (e.g., reminder frequency)
|
||||||
- Text content you view, save as favorites, or create within the App (if available)
|
- Text content you view, save as favorites, or create within the App (if available)
|
||||||
@@ -59,7 +59,7 @@ We do not:
|
|||||||
- Use your data for third-party advertising purposes
|
- Use your data for third-party advertising purposes
|
||||||
|
|
||||||
7. Third-Party Services
|
7. Third-Party Services
|
||||||
Hey Mama currently does not integrate third-party advertising or marketing services.
|
Dear Mama currently does not integrate third-party advertising or marketing services.
|
||||||
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
||||||
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ If we later integrate third-party analytics or technical services, we will updat
|
|||||||
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
||||||
|
|
||||||
9. Minors
|
9. Minors
|
||||||
Hey Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
Dear Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
||||||
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
||||||
|
|
||||||
10. Changes to This Privacy Policy
|
10. Changes to This Privacy Policy
|
||||||
@@ -75,17 +75,17 @@ We may update this Privacy Policy from time to time. The updated version will be
|
|||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama|隱私權政策
|
Dear Mama|隱私權政策
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
|
|
||||||
一、前言
|
一、前言
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Hey Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Dear Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
||||||
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
||||||
|
|
||||||
二、我們收集的資訊
|
二、我們收集的資訊
|
||||||
1. 使用者主動提供的資訊
|
1. 使用者主動提供的資訊
|
||||||
Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
Dear Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
||||||
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
||||||
- 提醒設定(例如提醒頻率)
|
- 提醒設定(例如提醒頻率)
|
||||||
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
||||||
@@ -99,7 +99,7 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
||||||
|
|
||||||
三、推送通知
|
三、推送通知
|
||||||
在取得您同意後,Hey Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
在取得您同意後,Dear Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
||||||
- 推送內容僅包含一般文字資訊
|
- 推送內容僅包含一般文字資訊
|
||||||
- 不包含任何敏感個人資料
|
- 不包含任何敏感個人資料
|
||||||
- 您可隨時於裝置系統設定中關閉通知功能
|
- 您可隨時於裝置系統設定中關閉通知功能
|
||||||
@@ -117,14 +117,14 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
- 將資料用於第三方廣告投放
|
- 將資料用於第三方廣告投放
|
||||||
|
|
||||||
六、第三方服務
|
六、第三方服務
|
||||||
目前 Hey Mama 未整合第三方廣告或行銷服務。
|
目前 Dear Mama 未整合第三方廣告或行銷服務。
|
||||||
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
||||||
|
|
||||||
七、資料保存與安全
|
七、資料保存與安全
|
||||||
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
||||||
|
|
||||||
八、未成年人說明
|
八、未成年人說明
|
||||||
Hey Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
Dear Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
||||||
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
||||||
|
|
||||||
九、隱私權政策的變更
|
九、隱私權政策的變更
|
||||||
@@ -132,17 +132,17 @@ Hey Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的
|
|||||||
更新後的版本將公布於 App 內或相關頁面,您於政策更新後繼續使用本 App,即視為同意更新內容。
|
更新後的版本將公布於 App 內或相關頁面,您於政策更新後繼續使用本 App,即視為同意更新內容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TERMS_OF_USE_MD = """Hey Mama – Terms of Use
|
TERMS_OF_USE_MD = """Dear Mama – Terms of Use
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
Welcome to Hey Mama (“the App,” “we,” or “us”).
|
Welcome to Dear Mama (“the App,” “we,” or “us”).
|
||||||
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
||||||
|
|
||||||
1. Intended Audience
|
1. Intended Audience
|
||||||
Hey Mama is intended for adults only.
|
Dear Mama is intended for adults only.
|
||||||
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
||||||
|
|
||||||
2. Services Provided
|
2. Services Provided
|
||||||
Hey Mama provides text-based content and features, including but not limited to:
|
Dear Mama provides text-based content and features, including but not limited to:
|
||||||
- Daily affirmations and mindfulness text
|
- Daily affirmations and mindfulness text
|
||||||
- User-configured reminders and push notifications
|
- User-configured reminders and push notifications
|
||||||
- Home screen widgets displaying affirmation text
|
- Home screen widgets displaying affirmation text
|
||||||
@@ -184,17 +184,17 @@ Updated versions will be made available within the App or related pages. Continu
|
|||||||
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama 使用條款
|
Dear Mama 使用條款
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
||||||
|
|
||||||
1. 服務對象與使用資格
|
1. 服務對象與使用資格
|
||||||
Hey Mama 僅供成年人使用(intended for adults)。
|
Dear Mama 僅供成年人使用(intended for adults)。
|
||||||
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
||||||
|
|
||||||
2. 服務內容
|
2. 服務內容
|
||||||
Hey Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
Dear Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
||||||
- 每日肯定語與正念文字內容
|
- 每日肯定語與正念文字內容
|
||||||
- 使用者設定的提醒與推送通知
|
- 使用者設定的提醒與推送通知
|
||||||
- 桌面小組件顯示肯定語文字
|
- 桌面小組件顯示肯定語文字
|
||||||
|
|||||||
BIN
server/celerybeat-schedule-shm
Normal file
BIN
server/celerybeat-schedule-shm
Normal file
Binary file not shown.
BIN
server/celerybeat-schedule-wal
Normal file
BIN
server/celerybeat-schedule-wal
Normal file
Binary file not shown.
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`)
|
||||||
@@ -88,7 +89,9 @@
|
|||||||
- iOS 构建号已提升到 `2`,并将 `client/ios/client/Info.plist` 改为自动跟随 `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
|
- iOS 构建号已提升到 `2`,并将 `client/ios/client/Info.plist` 改为自动跟随 `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
|
||||||
- 推送 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 `Dear 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)更新为“**如何加入小工具**”(并统一弹窗标题使用该文案);品牌文案改为 **Dear 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
Hey Mama – Terms of Use
|
Dear Mama – Terms of Use
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
Welcome to Hey Mama (“the App,” “we,” or “us”).
|
Welcome to Dear Mama (“the App,” “we,” or “us”).
|
||||||
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
||||||
|
|
||||||
1. Intended Audience
|
1. Intended Audience
|
||||||
Hey Mama is intended for adults only.
|
Dear Mama is intended for adults only.
|
||||||
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
||||||
|
|
||||||
2. Services Provided
|
2. Services Provided
|
||||||
Hey Mama provides text-based content and features, including but not limited to:
|
Dear Mama provides text-based content and features, including but not limited to:
|
||||||
- Daily affirmations and mindfulness text
|
- Daily affirmations and mindfulness text
|
||||||
- User-configured reminders and push notifications
|
- User-configured reminders and push notifications
|
||||||
- Home screen widgets displaying affirmation text
|
- Home screen widgets displaying affirmation text
|
||||||
@@ -50,17 +50,17 @@ Updated versions will be made available within the App or related pages. Continu
|
|||||||
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama 使用條款
|
Dear Mama 使用條款
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
||||||
|
|
||||||
1. 服務對象與使用資格
|
1. 服務對象與使用資格
|
||||||
Hey Mama 僅供成年人使用(intended for adults)。
|
Dear Mama 僅供成年人使用(intended for adults)。
|
||||||
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
||||||
|
|
||||||
2. 服務內容
|
2. 服務內容
|
||||||
Hey Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
Dear Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
||||||
- 每日肯定語與正念文字內容
|
- 每日肯定語與正念文字內容
|
||||||
- 使用者設定的提醒與推送通知
|
- 使用者設定的提醒與推送通知
|
||||||
- 桌面小組件顯示肯定語文字
|
- 桌面小組件顯示肯定語文字
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
Hey Mama | Privacy Policy
|
Dear Mama | Privacy Policy
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
|
|
||||||
1. Introduction
|
1. Introduction
|
||||||
Welcome to Hey Mama (“the App,” “we,” “us”).
|
Welcome to Dear Mama (“the App,” “we,” “us”).
|
||||||
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
||||||
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
||||||
|
|
||||||
2. Data Controller and Scope
|
2. Data Controller and Scope
|
||||||
The App is operated and maintained by the Hey Mama team.
|
The App is operated and maintained by the Dear Mama team.
|
||||||
This Privacy Policy applies to information processing activities related to your use of the App.
|
This Privacy Policy applies to information processing activities related to your use of the App.
|
||||||
|
|
||||||
3. Information We Collect
|
3. Information We Collect
|
||||||
3.1 Information You Provide
|
3.1 Information You Provide
|
||||||
Hey Mama does not require account registration and does not require you to provide personally identifiable information.
|
Dear Mama does not require account registration and does not require you to provide personally identifiable information.
|
||||||
During your use of the App, you may optionally provide or generate the following information:
|
During your use of the App, you may optionally provide or generate the following information:
|
||||||
- Reminder settings (e.g., reminder frequency)
|
- Reminder settings (e.g., reminder frequency)
|
||||||
- Text content you view, save as favorites, or create within the App (if available)
|
- Text content you view, save as favorites, or create within the App (if available)
|
||||||
@@ -46,7 +46,7 @@ We do not:
|
|||||||
- Use your data for third-party advertising purposes
|
- Use your data for third-party advertising purposes
|
||||||
|
|
||||||
7. Third-Party Services
|
7. Third-Party Services
|
||||||
Hey Mama currently does not integrate third-party advertising or marketing services.
|
Dear Mama currently does not integrate third-party advertising or marketing services.
|
||||||
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
||||||
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ If we later integrate third-party analytics or technical services, we will updat
|
|||||||
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
||||||
|
|
||||||
9. Minors
|
9. Minors
|
||||||
Hey Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
Dear Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
||||||
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
||||||
|
|
||||||
10. Changes to This Privacy Policy
|
10. Changes to This Privacy Policy
|
||||||
@@ -62,17 +62,17 @@ We may update this Privacy Policy from time to time. The updated version will be
|
|||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama|隱私權政策
|
Dear Mama|隱私權政策
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
|
|
||||||
一、前言
|
一、前言
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Hey Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Dear Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
||||||
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
||||||
|
|
||||||
二、我們收集的資訊
|
二、我們收集的資訊
|
||||||
1. 使用者主動提供的資訊
|
1. 使用者主動提供的資訊
|
||||||
Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
Dear Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
||||||
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
||||||
- 提醒設定(例如提醒頻率)
|
- 提醒設定(例如提醒頻率)
|
||||||
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
||||||
@@ -86,7 +86,7 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
||||||
|
|
||||||
三、推送通知
|
三、推送通知
|
||||||
在取得您同意後,Hey Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
在取得您同意後,Dear Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
||||||
- 推送內容僅包含一般文字資訊
|
- 推送內容僅包含一般文字資訊
|
||||||
- 不包含任何敏感個人資料
|
- 不包含任何敏感個人資料
|
||||||
- 您可隨時於裝置系統設定中關閉通知功能
|
- 您可隨時於裝置系統設定中關閉通知功能
|
||||||
@@ -104,14 +104,14 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
- 將資料用於第三方廣告投放
|
- 將資料用於第三方廣告投放
|
||||||
|
|
||||||
六、第三方服務
|
六、第三方服務
|
||||||
目前 Hey Mama 未整合第三方廣告或行銷服務。
|
目前 Dear Mama 未整合第三方廣告或行銷服務。
|
||||||
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
||||||
|
|
||||||
七、資料保存與安全
|
七、資料保存與安全
|
||||||
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
||||||
|
|
||||||
八、未成年人說明
|
八、未成年人說明
|
||||||
Hey Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
Dear Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
||||||
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
||||||
|
|
||||||
九、隱私權政策的變更
|
九、隱私權政策的變更
|
||||||
|
|||||||
Reference in New Issue
Block a user