Compare commits
18 Commits
d045237952
...
v1.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
| cdd0ac32de | |||
|
|
d98ec76dc0 | ||
| 3c35e14c3d | |||
|
|
c0deea9318 | ||
| 915e995ab7 | |||
|
|
0e42e6f2a9 | ||
| 39e4bcab6c | |||
|
|
69a1046ff4 | ||
| ceaf459d97 | |||
|
|
d9a5dbafd6 | ||
|
|
86e4853709 | ||
|
|
2adf2475fa | ||
| 9dbba04408 | |||
|
|
64b8352ad3 | ||
|
|
4b739dd194 | ||
|
|
240cdda68f | ||
|
|
ce48e54c03 | ||
|
|
228fd7fd84 |
5
.gitea/workflows/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
docker exec -it gitea-runner bash
|
||||||
|
# 然后在容器里安装 Node.js
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
node -v
|
||||||
116
.gitea/workflows/server-build.yml
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
name: Build and Push Server Docker Image
|
||||||
|
|
||||||
|
# 手动触发 workflow:从哪个分支运行,就打包哪个分支的代码
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1️⃣ Checkout 仓库代码
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# 需要能 push tag(请在仓库 Secrets 配置 RUNNER_TOKEN)
|
||||||
|
token: ${{ secrets.RUNNER_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
# 2️⃣ 自动递增 tag 并推送回 Gitea 仓库(默认按 vX.Y.Z 的 patch +1)
|
||||||
|
- name: Auto bump tag and push to repository
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 配置提交信息(用于创建注释 tag)
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "actions@local"
|
||||||
|
|
||||||
|
# 确保本地有最新 tags
|
||||||
|
git fetch --tags --force
|
||||||
|
|
||||||
|
# 取最新的 semver tag(vX.Y.Z),按版本号排序
|
||||||
|
LATEST_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)"
|
||||||
|
echo "LATEST_TAG=${LATEST_TAG}"
|
||||||
|
|
||||||
|
if [[ -z "${LATEST_TAG}" ]]; then
|
||||||
|
NEXT_TAG="v1.0.0"
|
||||||
|
else
|
||||||
|
if [[ "${LATEST_TAG}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||||
|
MAJOR="${BASH_REMATCH[1]}"
|
||||||
|
MINOR="${BASH_REMATCH[2]}"
|
||||||
|
PATCH="${BASH_REMATCH[3]}"
|
||||||
|
NEXT_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||||
|
else
|
||||||
|
# 如果最新 tag 不符合 vX.Y.Z,回退到 v1.0.0,避免误解析
|
||||||
|
NEXT_TAG="v1.0.0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "NEXT_TAG=${NEXT_TAG}"
|
||||||
|
|
||||||
|
# 如果 tag 已存在则直接复用(避免重复运行失败)
|
||||||
|
if git rev-parse -q --verify "refs/tags/${NEXT_TAG}" >/dev/null; then
|
||||||
|
echo "Tag ${NEXT_TAG} 已存在,跳过创建。"
|
||||||
|
else
|
||||||
|
git tag -a "${NEXT_TAG}" -m "Release ${NEXT_TAG}"
|
||||||
|
git push origin "${NEXT_TAG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 输出给后续步骤使用
|
||||||
|
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||||
|
echo "IMAGE_TAG=${NEXT_TAG}" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
# 兼容部分 Gitea Runner 环境变量命名
|
||||||
|
if [[ -n "${GITEA_ENV:-}" ]]; then
|
||||||
|
echo "IMAGE_TAG=${NEXT_TAG}" >> "$GITEA_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3️⃣ 设置镜像仓库与镜像名称(自建 Registry / Docker Hub 都可)
|
||||||
|
- name: Set image variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# 自建镜像仓库地址(例如:docker.damer.fun 或 registry.example.com)
|
||||||
|
# 如要推送 Docker Hub:可把 REGISTRY 留空并把 IMAGE_REPO 设置为 yourname/xxx
|
||||||
|
REGISTRY="${DOCKER_REGISTRY:-docker.damer.fun}"
|
||||||
|
# 镜像仓库路径(不含 registry)
|
||||||
|
IMAGE_REPO="${DOCKER_IMAGE_REPO:-damer/mindfulness-server}"
|
||||||
|
|
||||||
|
if [[ -n "$REGISTRY" ]]; then
|
||||||
|
IMAGE_NAME="$REGISTRY/$IMAGE_REPO"
|
||||||
|
else
|
||||||
|
IMAGE_NAME="$IMAGE_REPO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||||
|
echo "REGISTRY=$REGISTRY" >> "$GITHUB_ENV"
|
||||||
|
echo "IMAGE_REPO=$IMAGE_REPO" >> "$GITHUB_ENV"
|
||||||
|
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
if [[ -n "${GITEA_ENV:-}" ]]; then
|
||||||
|
echo "REGISTRY=$REGISTRY" >> "$GITEA_ENV"
|
||||||
|
echo "IMAGE_REPO=$IMAGE_REPO" >> "$GITEA_ENV"
|
||||||
|
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITEA_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4️⃣ 登录镜像仓库(自建 Registry)
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: docker.damer.fun
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
# 5️⃣ 构建 Docker 镜像(使用 server/ 作为构建上下文)
|
||||||
|
- name: Build Docker Image
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker build -f server/Dockerfile -t "$IMAGE_NAME:$IMAGE_TAG" server
|
||||||
|
|
||||||
|
# 6️⃣ 推送 Docker 镜像到镜像仓库
|
||||||
|
- name: Push Docker Image
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker push "$IMAGE_NAME:$IMAGE_TAG"
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Hey Mama",
|
"name": "client",
|
||||||
"slug": "hey-mama",
|
"slug": "client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "heymama",
|
"scheme": "client",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"splash": {
|
"splash": {
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.heymama.app"
|
"bundleIdentifier": "com.damer.mindfulness"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react';
|
import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||||
import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated } from 'react-native';
|
import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated, ImageBackground } from 'react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigation, useFocusEffect } from 'expo-router';
|
import { useNavigation, useFocusEffect } from 'expo-router';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -39,6 +39,40 @@ import LikeIcon from '@/assets/images/icon/like_icon.svg';
|
|||||||
|
|
||||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||||
|
|
||||||
|
// 预定义风景图列表
|
||||||
|
const NATURE_IMAGES = [
|
||||||
|
require('@/assets/theme/nature/1.png'),
|
||||||
|
require('@/assets/theme/nature/2.png'),
|
||||||
|
require('@/assets/theme/nature/3.png'),
|
||||||
|
require('@/assets/theme/nature/4.png'),
|
||||||
|
require('@/assets/theme/nature/5.png'),
|
||||||
|
require('@/assets/theme/nature/6.png'),
|
||||||
|
require('@/assets/theme/nature/7.png'),
|
||||||
|
require('@/assets/theme/nature/8.png'),
|
||||||
|
require('@/assets/theme/nature/9.png'),
|
||||||
|
require('@/assets/theme/nature/10.png'),
|
||||||
|
require('@/assets/theme/nature/11.png'),
|
||||||
|
require('@/assets/theme/nature/12.png'),
|
||||||
|
require('@/assets/theme/nature/13.png'),
|
||||||
|
require('@/assets/theme/nature/14.png'),
|
||||||
|
require('@/assets/theme/nature/15.png'),
|
||||||
|
require('@/assets/theme/nature/17.png'),
|
||||||
|
require('@/assets/theme/nature/18.png'),
|
||||||
|
require('@/assets/theme/nature/19.png'),
|
||||||
|
require('@/assets/theme/nature/20.png'),
|
||||||
|
require('@/assets/theme/nature/22.png'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 预定义颜色列表
|
||||||
|
const THEME_COLORS = [
|
||||||
|
'#F7D9BF',
|
||||||
|
'#CBF2D8',
|
||||||
|
'#F5CDDE',
|
||||||
|
'#F2ECCB',
|
||||||
|
'#E2CBF2',
|
||||||
|
'#CBD9F2',
|
||||||
|
];
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
@@ -126,12 +160,26 @@ export default function HomeScreen() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2';
|
const backgroundColor = useMemo(() => {
|
||||||
|
if (themeMode === 'color') {
|
||||||
|
const colorIndex = Math.floor(index / 10) % THEME_COLORS.length;
|
||||||
|
return THEME_COLORS[colorIndex];
|
||||||
|
}
|
||||||
|
return '#F4D6C2'; // 风景模式下的默认底色(图片加载前显示)
|
||||||
|
}, [themeMode, index]);
|
||||||
|
|
||||||
|
// 计算当前应该显示的风景图索引(滑动 10 次切换一张)
|
||||||
|
const natureImageIndex = useMemo(() => {
|
||||||
|
return Math.floor(index / 10) % NATURE_IMAGES.length;
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
|
const currentNatureImage = NATURE_IMAGES[natureImageIndex];
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerShadowVisible: false,
|
headerShadowVisible: false,
|
||||||
headerStyle: { backgroundColor },
|
headerStyle: { backgroundColor: themeMode === 'scenery' ? 'transparent' : backgroundColor },
|
||||||
|
headerTransparent: themeMode === 'scenery',
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<View style={styles.headerRight}>
|
<View style={styles.headerRight}>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
@@ -149,7 +197,7 @@ export default function HomeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}, [backgroundColor, navigation, t]);
|
}, [backgroundColor, themeMode, navigation, t]);
|
||||||
|
|
||||||
const textAnimatedStyle = useAnimatedStyle(() => ({
|
const textAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{ translateY: translateY.value }],
|
transform: [{ translateY: translateY.value }],
|
||||||
@@ -241,10 +289,11 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
// 2. 保存到收藏夹,包含当前背景信息
|
// 2. 保存到收藏夹,包含当前背景信息
|
||||||
await addFavorite({
|
await addFavorite({
|
||||||
id: typeof currentContentId === 'number' ? String(currentContentId) : (item as any).id,
|
favId: String(Date.now()), // 生成唯一 ID
|
||||||
|
id: item.id,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
background: backgroundColor, // 目前存储的是颜色值
|
background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 爱心缩放动画
|
// 3. 爱心缩放动画
|
||||||
@@ -267,8 +316,15 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}>
|
<View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}>
|
||||||
<Animated.View style={[styles.card, textAnimatedStyle]}>
|
{themeMode === 'scenery' && (
|
||||||
<Text style={styles.text}>{item.text}</Text>
|
<ImageBackground
|
||||||
|
source={currentNatureImage}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Animated.View style={[styles.card, textAnimatedStyle, themeMode === 'scenery' && styles.sceneryCard]}>
|
||||||
|
<Text style={[styles.text, themeMode === 'scenery' && styles.sceneryText]}>{item.text}</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
@@ -281,9 +337,13 @@ export default function HomeScreen() {
|
|||||||
style={styles.reactionInner}
|
style={styles.reactionInner}
|
||||||
>
|
>
|
||||||
{likeFilled ? (
|
{likeFilled ? (
|
||||||
<LikeFilledIcon width={35} height={36} />
|
<LikeFilledIcon width={35} height={36} style={{ color: '#EA6969' }} />
|
||||||
) : (
|
) : (
|
||||||
<LikeIcon width={35} height={36} />
|
<LikeIcon
|
||||||
|
width={35}
|
||||||
|
height={36}
|
||||||
|
style={{ color: themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -342,9 +402,15 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
paddingHorizontal: 30,
|
position: 'absolute',
|
||||||
alignItems: 'center',
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 30,
|
||||||
|
zIndex: 5, // 降低层级,防止遮挡底部按钮
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
@@ -353,6 +419,16 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
sceneryCard: {
|
||||||
|
// 风景模式下稍微收窄文案宽度,增加呼吸感
|
||||||
|
paddingHorizontal: 50,
|
||||||
|
},
|
||||||
|
sceneryText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
textShadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
|
textShadowRadius: 4,
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: SCREEN_HEIGHT * 0.16,
|
bottom: SCREEN_HEIGHT * 0.16,
|
||||||
@@ -360,6 +436,7 @@ const styles = StyleSheet.create({
|
|||||||
right: 0,
|
right: 0,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
zIndex: 20, // 提升层级,确保在最顶层可点击
|
||||||
},
|
},
|
||||||
reactionButton: {
|
reactionButton: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -140,14 +140,13 @@ export default function OnboardingScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSkip = () => {
|
const onSkip = async () => {
|
||||||
// 跳过整个 Onboarding:仍生成一个“全跳过”的最小画像,保证下游可用
|
// 跳过整个 Onboarding:仍生成一个“全跳过”的最小画像,保证下游可用
|
||||||
const scoringProfile = buildUserProfileFromQuestionnaire({});
|
const scoringProfile = buildUserProfileFromQuestionnaire({});
|
||||||
void setUserProfileScoring(scoringProfile);
|
await setUserProfileScoring(scoringProfile);
|
||||||
|
|
||||||
// 标记已完成,避免下次启动再次进入 Onboarding
|
// 标记已完成,避免下次启动再次进入 Onboarding
|
||||||
void setOnboardingCompleted(true);
|
await setOnboardingCompleted(true);
|
||||||
|
|
||||||
router.replace('/(app)/home');
|
router.replace('/(app)/home');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
|
|
||||||
import { getOnboardingCompleted, getConsentAccepted, setOnboardingCompleted, setConsentAccepted } from '@/src/storage/appStorage';
|
import { getOnboardingCompleted, getConsentAccepted } from '@/src/storage/appStorage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动分发:根据 consent 和 onboarding 状态跳转
|
* 启动分发:根据 consent 和 onboarding 状态跳转
|
||||||
@@ -14,9 +13,6 @@ export default function Index() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
// 注意:不要在启动时无条件清空存储,否则 Onboarding/画像等数据无法持久化。
|
|
||||||
// 如需调试重置,请在开发期手动清空或自行加调试开关。
|
|
||||||
|
|
||||||
// 1. 检查是否同意协议
|
// 1. 检查是否同意协议
|
||||||
const consentAccepted = await getConsentAccepted();
|
const consentAccepted = await getConsentAccepted();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -26,9 +22,17 @@ export default function Index() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查 Onboarding
|
// 2. 检查 Onboarding 是否已完成
|
||||||
const completed = await getOnboardingCompleted();
|
const completed = await getOnboardingCompleted();
|
||||||
router.replace(completed ? '/(app)/home' : '/(onboarding)/onboarding');
|
if (cancelled) return;
|
||||||
|
|
||||||
|
if (completed) {
|
||||||
|
// 如果已经完成过流程,直接进 Home
|
||||||
|
router.replace('/(app)/home');
|
||||||
|
} else {
|
||||||
|
// 如果是首次进入(或未完成流程),进入 Onboarding
|
||||||
|
router.replace('/(onboarding)/onboarding');
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -45,4 +49,3 @@ export default function Index() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
BIN
client/assets/theme/nature/1.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
client/assets/theme/nature/10.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
client/assets/theme/nature/11.png
Normal file
|
After Width: | Height: | Size: 532 KiB |
BIN
client/assets/theme/nature/12.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
client/assets/theme/nature/13.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
client/assets/theme/nature/14.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
client/assets/theme/nature/15.png
Normal file
|
After Width: | Height: | Size: 693 KiB |
BIN
client/assets/theme/nature/17.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
client/assets/theme/nature/18.png
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
client/assets/theme/nature/19.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
client/assets/theme/nature/2.png
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
client/assets/theme/nature/20.png
Normal file
|
After Width: | Height: | Size: 461 KiB |
BIN
client/assets/theme/nature/22.png
Normal file
|
After Width: | Height: | Size: 606 KiB |
BIN
client/assets/theme/nature/3.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
client/assets/theme/nature/4.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
client/assets/theme/nature/5.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
client/assets/theme/nature/6.png
Normal file
|
After Width: | Height: | Size: 721 KiB |
BIN
client/assets/theme/nature/7.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
client/assets/theme/nature/8.png
Normal file
|
After Width: | Height: | Size: 415 KiB |
BIN
client/assets/theme/nature/9.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
@@ -50,6 +50,29 @@ type Props = {
|
|||||||
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo';
|
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo';
|
||||||
type NavDirection = 'forward' | 'back';
|
type NavDirection = 'forward' | 'back';
|
||||||
|
|
||||||
|
const NATURE_IMAGES = [
|
||||||
|
require('@/assets/theme/nature/1.png'),
|
||||||
|
require('@/assets/theme/nature/2.png'),
|
||||||
|
require('@/assets/theme/nature/3.png'),
|
||||||
|
require('@/assets/theme/nature/4.png'),
|
||||||
|
require('@/assets/theme/nature/5.png'),
|
||||||
|
require('@/assets/theme/nature/6.png'),
|
||||||
|
require('@/assets/theme/nature/7.png'),
|
||||||
|
require('@/assets/theme/nature/8.png'),
|
||||||
|
require('@/assets/theme/nature/9.png'),
|
||||||
|
require('@/assets/theme/nature/10.png'),
|
||||||
|
require('@/assets/theme/nature/11.png'),
|
||||||
|
require('@/assets/theme/nature/12.png'),
|
||||||
|
require('@/assets/theme/nature/13.png'),
|
||||||
|
require('@/assets/theme/nature/14.png'),
|
||||||
|
require('@/assets/theme/nature/15.png'),
|
||||||
|
require('@/assets/theme/nature/17.png'),
|
||||||
|
require('@/assets/theme/nature/18.png'),
|
||||||
|
require('@/assets/theme/nature/19.png'),
|
||||||
|
require('@/assets/theme/nature/20.png'),
|
||||||
|
require('@/assets/theme/nature/22.png'),
|
||||||
|
];
|
||||||
|
|
||||||
export default function ProfileModal({ visible, name: propName, onClose }: Props) {
|
export default function ProfileModal({ visible, name: propName, onClose }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -260,11 +283,11 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
|
|||||||
setFavorites(list);
|
setFavorites(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemove(id: string) {
|
async function handleRemove(favId: string) {
|
||||||
// 1. 调用存储层移除收藏
|
// 1. 调用存储层移除收藏
|
||||||
await removeFavorite(id);
|
await removeFavorite(favId);
|
||||||
// 2. 更新本地状态
|
// 2. 更新本地状态
|
||||||
setFavorites(prev => prev.filter(item => item.id !== id));
|
setFavorites(prev => prev.filter(item => item.favId !== favId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -274,7 +297,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
|
|||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={favorites}
|
data={favorites}
|
||||||
keyExtractor={(it) => it.id}
|
keyExtractor={(it) => it.favId}
|
||||||
contentContainerStyle={styles.favList}
|
contentContainerStyle={styles.favList}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
@@ -290,11 +313,30 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
|
|||||||
<View style={styles.favRight}>
|
<View style={styles.favRight}>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.favThumb,
|
styles.favThumb,
|
||||||
{ backgroundColor: item.background } // 动态同步 Home 页的背景
|
item.themeMode === 'scenery' ? {} : { backgroundColor: item.background }
|
||||||
]}>
|
]}>
|
||||||
<Text style={styles.favThumbText} numberOfLines={4}>{item.text}</Text>
|
{item.themeMode === 'scenery' ? (
|
||||||
|
<View style={StyleSheet.absoluteFill}>
|
||||||
|
<Image
|
||||||
|
source={NATURE_IMAGES[parseInt(item.background)]}
|
||||||
|
style={{
|
||||||
|
width: width * 0.6,
|
||||||
|
height: 800, // 假设原图较高,设置一个较大的高度
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0, // 关键:将图片底部对齐容器底部
|
||||||
|
}}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<Text style={[
|
||||||
|
styles.favThumbText,
|
||||||
|
item.themeMode === 'scenery' && { color: '#FFFFFF', textShadowColor: 'rgba(0,0,0,0.5)', textShadowOffset: {width:0, height:1}, textShadowRadius: 3 }
|
||||||
|
]} numberOfLines={4}>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => handleRemove(item.id)}
|
onPress={() => handleRemove(item.favId)}
|
||||||
style={styles.favRemoveBtn}
|
style={styles.favRemoveBtn}
|
||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
>
|
>
|
||||||
@@ -765,6 +807,7 @@ const styles = StyleSheet.create({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(119, 47, 0, 0.05)',
|
borderColor: 'rgba(119, 47, 0, 0.05)',
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
favThumbText: {
|
favThumbText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
|||||||
@@ -59,5 +59,13 @@ target 'client' do
|
|||||||
:mac_catalyst_enabled => false,
|
:mac_catalyst_enabled => false,
|
||||||
:ccache_enabled => ccache_enabled?(podfile_properties),
|
:ccache_enabled => ccache_enabled?(podfile_properties),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 生成并随归档产物携带 dSYM(用于崩溃符号化与 Upload Symbols Failed 修复)
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |build_config|
|
||||||
|
build_config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf-with-dsym'
|
||||||
|
build_config.build_settings['DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT'] = 'YES'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2397,6 +2397,6 @@ SPEC CHECKSUMS:
|
|||||||
RNWorklets: 9ccdc8112b17af6eee2c85a233891cb80db150ad
|
RNWorklets: 9ccdc8112b17af6eee2c85a233891cb80db150ad
|
||||||
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
||||||
|
|
||||||
PODFILE CHECKSUM: dfe3cc75dee014a0abd367bc9e1bdbab0ba64ee3
|
PODFILE CHECKSUM: 4d5c52f9fa870c1d398cf59e37c149f66700c061
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 56;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
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, ); }; };
|
||||||
|
EB630B302F30BE2700DC761A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EB630B2F2F30BE2700DC761A /* Assets.xcassets */; };
|
||||||
|
EB630B312F30BE2700DC761A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EB630B2F2F30BE2700DC761A /* Assets.xcassets */; };
|
||||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@
|
|||||||
EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
EB3DAF822F2A4B8E00450593 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
EB3DAF822F2A4B8E00450593 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
EB3DAF9A2F2A4D0900450593 /* MindfulnessWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindfulnessWidget.swift; sourceTree = "<group>"; };
|
EB3DAF9A2F2A4D0900450593 /* MindfulnessWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindfulnessWidget.swift; sourceTree = "<group>"; };
|
||||||
|
EB630B2F2F30BE2700DC761A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = client/AppDelegate.swift; sourceTree = "<group>"; };
|
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = client/AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
F11748442D0722820044C1D9 /* client-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "client-Bridging-Header.h"; path = "client/client-Bridging-Header.h"; sourceTree = "<group>"; };
|
F11748442D0722820044C1D9 /* client-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "client-Bridging-Header.h"; path = "client/client-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
@@ -156,6 +159,7 @@
|
|||||||
83CBB9F61A601CBA00E9B192 = {
|
83CBB9F61A601CBA00E9B192 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
EB630B2F2F30BE2700DC761A /* Assets.xcassets */,
|
||||||
13B07FAE1A68108700A75B9A /* client */,
|
13B07FAE1A68108700A75B9A /* client */,
|
||||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */,
|
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */,
|
||||||
@@ -305,6 +309,7 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||||
0BE245B56A79D95AB0A7B4BA /* PrivacyInfo.xcprivacy in Resources */,
|
0BE245B56A79D95AB0A7B4BA /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
|
EB630B312F30BE2700DC761A /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -312,6 +317,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
EB630B302F30BE2700DC761A /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -475,9 +481,11 @@
|
|||||||
baseConfigurationReference = FFF632A94C7A551AAA096858 /* Pods-client.debug.xcconfig */;
|
baseConfigurationReference = FFF632A94C7A551AAA096858 /* Pods-client.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
|
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
|
DEVELOPMENT_TEAM = WS92GPX9H2;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -489,14 +497,14 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
||||||
PRODUCT_NAME = client;
|
PRODUCT_NAME = client;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -505,7 +513,7 @@
|
|||||||
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -515,23 +523,27 @@
|
|||||||
baseConfigurationReference = 3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */;
|
baseConfigurationReference = 3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
|
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = WS92GPX9H2;
|
||||||
|
DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT = YES;
|
||||||
INFOPLIST_FILE = client/Info.plist;
|
INFOPLIST_FILE = client/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
||||||
PRODUCT_NAME = client;
|
PRODUCT_NAME = client;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -539,7 +551,7 @@
|
|||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -676,8 +688,9 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = WS92GPX9H2;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -695,7 +708,7 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client.emotionwidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness.emotionwidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -709,7 +722,7 @@
|
|||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -728,8 +741,9 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = WS92GPX9H2;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -746,7 +760,7 @@
|
|||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client.emotionwidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness.emotionwidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -759,7 +773,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 142 KiB |
@@ -19,7 +19,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.0</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -28,12 +28,12 @@
|
|||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>client</string>
|
<string>client</string>
|
||||||
<string>com.anonymous.client</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<key>RCTNewArchEnabled</key>
|
<key>RCTNewArchEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string></string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
<array>
|
<array>
|
||||||
<string>arm64</string>
|
<string>arm64</string>
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>production</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,54 +1,28 @@
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// V2:纯色背景 + 随机文案小组件(Small/Medium/Large + 点击跳转 Home)
|
// V1:写死文案的小组件(Small/Medium/Large + 点击跳转 Home)
|
||||||
|
|
||||||
struct EmotionProvider: TimelineProvider {
|
struct EmotionProvider: TimelineProvider {
|
||||||
private let quotes = [
|
|
||||||
"你已经很努力了,今天也值得被温柔对待。",
|
|
||||||
"轻轻呼吸,感受当下的每一刻。",
|
|
||||||
"所有的压力,都会在深呼吸中慢慢消散。",
|
|
||||||
"给生活一点留白,给自己一点温柔。",
|
|
||||||
"不要走得太快,等一等落下的灵魂。",
|
|
||||||
"世界虽嘈杂,但你可以拥有一颗宁静的心。",
|
|
||||||
"每一个瞬间,都是生命最好的安排。",
|
|
||||||
"抱抱自己,辛苦了,亲爱的。",
|
|
||||||
"慢一点也没关系,只要你在前行。",
|
|
||||||
"今天,你对自己微笑了吗?",
|
|
||||||
"愿你历经山河,仍觉得人间值得。",
|
|
||||||
"心简单,世界就简单;心平顺,生活就平顺。",
|
|
||||||
"即使生活偶尔晦暗,你也要成为自己的光。",
|
|
||||||
"别让琐事挤走生活的快乐,别让压力消磨奋斗的激情。"
|
|
||||||
]
|
|
||||||
|
|
||||||
func placeholder(in context: Context) -> EmotionEntry {
|
func placeholder(in context: Context) -> EmotionEntry {
|
||||||
EmotionEntry(date: Date(), text: quotes[0])
|
EmotionEntry(date: Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) {
|
func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) {
|
||||||
let entry = EmotionEntry(date: Date(), text: quotes.randomElement() ?? quotes[0])
|
completion(EmotionEntry(date: Date()))
|
||||||
completion(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<EmotionEntry>) -> ()) {
|
func getTimeline(in context: Context, completion: @escaping (Timeline<EmotionEntry>) -> ()) {
|
||||||
var entries: [EmotionEntry] = []
|
// V1:内容写死,不做数据更新;给一个较长的刷新间隔(系统仍可能自行调度)
|
||||||
let currentDate = Date()
|
let entry = EmotionEntry(date: Date())
|
||||||
|
let nextUpdate = Calendar.current.date(byAdding: .day, value: 7, to: Date())
|
||||||
// 生成未来 24 小时的 6 个条目,每 4 小时更换一次随机文案
|
?? Date().addingTimeInterval(60 * 60 * 24 * 7)
|
||||||
for hourOffset in 0..<6 {
|
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
|
||||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset * 4, to: currentDate)!
|
|
||||||
let entry = EmotionEntry(date: entryDate, text: quotes.randomElement() ?? quotes[0])
|
|
||||||
entries.append(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
|
||||||
completion(timeline)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmotionEntry: TimelineEntry {
|
struct EmotionEntry: TimelineEntry {
|
||||||
let date: Date
|
let date: Date
|
||||||
let text: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmotionWidgetView: View {
|
struct EmotionWidgetView: View {
|
||||||
@@ -56,37 +30,160 @@ struct EmotionWidgetView: View {
|
|||||||
@Environment(\.widgetFamily) var family
|
@Environment(\.widgetFamily) var family
|
||||||
|
|
||||||
private let title = "正念"
|
private let title = "正念"
|
||||||
|
private let text = "你已经很努力了,今天也值得被温柔对待。"
|
||||||
private let deepLink = URL(string: "client:///(app)/home")
|
private let deepLink = URL(string: "client:///(app)/home")
|
||||||
|
|
||||||
// 背景色 #F7D9BF
|
|
||||||
private let backgroundColor = Color(red: 247/255, green: 217/255, blue: 191/255)
|
|
||||||
// 文本颜色(深咖色,适合搭配浅橘色背景)
|
|
||||||
private let textColor = Color(red: 74/255, green: 52/255, blue: 40/255)
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center, spacing: 0) {
|
switch family {
|
||||||
Spacer(minLength: 0)
|
case .systemSmall:
|
||||||
|
smallView()
|
||||||
|
case .systemMedium:
|
||||||
|
mediumView()
|
||||||
|
case .systemLarge:
|
||||||
|
largeView()
|
||||||
|
default:
|
||||||
|
smallView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(entry.text)
|
// 统一的“卡片背景”风格(iOS 15 兼容)
|
||||||
.font(.system(size: family == .systemSmall ? 17 : 20, weight: .medium))
|
private func cardBackground(colors: [Color]) -> some View {
|
||||||
.foregroundColor(textColor)
|
ZStack {
|
||||||
.lineSpacing(6)
|
LinearGradient(
|
||||||
.multilineTextAlignment(.center)
|
colors: colors,
|
||||||
.minimumScaleFactor(0.7)
|
startPoint: .topLeading,
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
// 轻微光斑,增加层次
|
||||||
|
RadialGradient(
|
||||||
|
gradient: Gradient(colors: [Color.white.opacity(0.16), Color.white.opacity(0.0)]),
|
||||||
|
center: .topTrailing,
|
||||||
|
startRadius: 10,
|
||||||
|
endRadius: 180
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.14), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.cornerRadius(18)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chip(_ text: String) -> some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.9))
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Color.white.opacity(0.14))
|
||||||
|
.cornerRadius(999)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func smallView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
cardBackground(colors: [
|
||||||
|
Color(red: 0.06, green: 0.08, blue: 0.12),
|
||||||
|
Color(red: 0.13, green: 0.16, blue: 0.22),
|
||||||
|
])
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
chip(title)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text)
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.92))
|
||||||
|
.lineSpacing(2)
|
||||||
|
.lineLimit(4)
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
if family != .systemSmall {
|
Text("点我回到 App")
|
||||||
Text("Hey Mama")
|
.font(.system(size: 11, weight: .medium))
|
||||||
.font(.system(size: 10, weight: .semibold))
|
.foregroundColor(Color.white.opacity(0.65))
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
}
|
||||||
.padding(.bottom, 4)
|
.padding(14)
|
||||||
|
}
|
||||||
|
.widgetURL(deepLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mediumView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
cardBackground(colors: [
|
||||||
|
Color(red: 0.06, green: 0.08, blue: 0.12),
|
||||||
|
Color(red: 0.09, green: 0.11, blue: 0.17),
|
||||||
|
])
|
||||||
|
|
||||||
|
HStack(alignment: .top, spacing: 14) {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
chip(title)
|
||||||
|
Text(text)
|
||||||
|
.font(.system(size: 17, weight: .semibold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.92))
|
||||||
|
.lineSpacing(3)
|
||||||
|
.lineLimit(5)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
|
Text("轻轻呼吸,回到当下")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(Color.white.opacity(0.7))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧装饰区:让版面更饱满
|
||||||
|
VStack(alignment: .trailing, spacing: 8) {
|
||||||
|
Text(entry.date, style: .time)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.8))
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Text("今日")
|
||||||
|
.font(.system(size: 28, weight: .bold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.12))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(family == .systemSmall ? 16 : 24)
|
.padding(16)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity) // 强制撑开容器
|
}
|
||||||
.background(backgroundColor) // 将背景色直接应用到容器上
|
.widgetURL(deepLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func largeView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
cardBackground(colors: [
|
||||||
|
Color(red: 0.06, green: 0.08, blue: 0.12),
|
||||||
|
Color(red: 0.14, green: 0.18, blue: 0.28),
|
||||||
|
])
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
HStack {
|
||||||
|
chip(title)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Text(entry.date, style: .time)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.78))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text)
|
||||||
|
.font(.system(size: 20, weight: .semibold))
|
||||||
|
.foregroundColor(Color.white.opacity(0.92))
|
||||||
|
.lineSpacing(4)
|
||||||
|
.lineLimit(8)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("点我回到 Home")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(Color.white.opacity(0.7))
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Text("🌿")
|
||||||
|
.font(.system(size: 18))
|
||||||
|
.opacity(0.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(18)
|
||||||
|
}
|
||||||
.widgetURL(deepLink)
|
.widgetURL(deepLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,13 +194,7 @@ struct EmotionWidget: Widget {
|
|||||||
|
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
|
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
EmotionWidgetView(entry: entry)
|
EmotionWidgetView(entry: entry)
|
||||||
.containerBackground(Color(red: 247/255, green: 217/255, blue: 191/255), for: .widget)
|
|
||||||
} else {
|
|
||||||
EmotionWidgetView(entry: entry)
|
|
||||||
.background(Color(red: 247/255, green: 217/255, blue: 191/255))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.configurationDisplayName("情绪小组件")
|
.configurationDisplayName("情绪小组件")
|
||||||
.description("一段温柔提醒,陪你回到当下。")
|
.description("一段温柔提醒,陪你回到当下。")
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export async function setReaction(contentId: string, reaction: Reaction): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FavoriteItem = {
|
export type FavoriteItem = {
|
||||||
|
favId: string; // 唯一标识,支持重复点赞同一文案
|
||||||
id: string;
|
id: string;
|
||||||
date: string;
|
date: string;
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
@@ -117,15 +118,15 @@ export async function getFavorites(): Promise<FavoriteItem[]> {
|
|||||||
|
|
||||||
export async function addFavorite(item: FavoriteItem): Promise<void> {
|
export async function addFavorite(item: FavoriteItem): Promise<void> {
|
||||||
const list = await getFavorites();
|
const list = await getFavorites();
|
||||||
if (list.some(i => i.id === item.id)) return;
|
// 允许重复点赞,不再根据 id 去重
|
||||||
const newList = [item, ...list];
|
const newList = [item, ...list];
|
||||||
console.log('Adding to favorites, new list size:', newList.length);
|
console.log('Adding to favorites, new list size:', newList.length);
|
||||||
await setJson(KEY_FAVORITES_ITEMS, newList);
|
await setJson(KEY_FAVORITES_ITEMS, newList);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFavorite(contentId: string): Promise<void> {
|
export async function removeFavorite(favId: string): Promise<void> {
|
||||||
const list = await getFavorites();
|
const list = await getFavorites();
|
||||||
const next = list.filter(item => item.id !== contentId);
|
const next = list.filter(item => item.favId !== favId);
|
||||||
await setJson(KEY_FAVORITES_ITEMS, next);
|
await setJson(KEY_FAVORITES_ITEMS, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
server/.dockerignore
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.venv
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# 本地/测试数据
|
||||||
|
.test.db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# 测试与开发脚本(按需移除)
|
||||||
|
tests/
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Git 元数据
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
27
server/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# 运行时基础环境
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 系统依赖(按需扩展;多数依赖为纯 Python)
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 先复制依赖清单以利用 Docker layer cache
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN python -m pip install -U pip \
|
||||||
|
&& pip install --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
# 复制后端代码与迁移配置
|
||||||
|
COPY app /app/app
|
||||||
|
COPY alembic /app/alembic
|
||||||
|
COPY alembic.ini /app/alembic.ini
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 生产镜像默认不开启 reload
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -46,5 +46,5 @@
|
|||||||
## 6. 风险与注意事项
|
## 6. 风险与注意事项
|
||||||
|
|
||||||
- **Expo Go 不支持**:必须用 Xcode 运行预构建后的 App(或后续 EAS Build)
|
- **Expo Go 不支持**:必须用 Xcode 运行预构建后的 App(或后续 EAS Build)
|
||||||
- **Bundle Identifier**:当前 `client/app.json` 使用 `com.anonymous.client`,仅适合本地验证;上线前需替换为真实 bundle id,并同步证书/签名
|
- **Bundle Identifier**:当前工程已统一为主 App `com.damer.mindfulness`,Widget Extension `com.damer.mindfulness.emotionwidget`;上线前仍需在 Apple Developer / App Store Connect 侧确保 App ID、证书与签名匹配
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ npx expo prebuild -p ios
|
|||||||
- **排查**:
|
- **排查**:
|
||||||
- 桌面搜索不到:通常是没有 Run 安装过主 App,或 Widget target 未加入编译
|
- 桌面搜索不到:通常是没有 Run 安装过主 App,或 Widget target 未加入编译
|
||||||
- 点击不跳转:确认 Widget 里 `widgetURL` 为 `client:///(app)/home`,且 `app.json` 的 `scheme` 为 `client`
|
- 点击不跳转:确认 Widget 里 `widgetURL` 为 `client:///(app)/home`,且 `app.json` 的 `scheme` 为 `client`
|
||||||
- 仍然搜不到:检查 Widget Extension 的 Bundle Identifier 是否有效(本仓库已修正为 `com.anonymous.client.emotionwidget`),然后 Clean + 重新安装 App
|
- 仍然搜不到:检查 Widget Extension 的 Bundle Identifier 是否有效(本仓库已修正为 `com.damer.mindfulness.emotionwidget`),然后 Clean + 重新安装 App
|
||||||
|
|
||||||
## 6. 文档补充(可选但建议)
|
## 6. 文档补充(可选但建议)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
- 安全:真实 IP/账号/密码/Token 不写入仓库,仅提供 `.env.example` 结构
|
- 安全:真实 IP/账号/密码/Token 不写入仓库,仅提供 `.env.example` 结构
|
||||||
- **阶段产物**:
|
- **阶段产物**:
|
||||||
- `spec_kit/Project Bootstrap/spec.md`
|
- `spec_kit/Project Bootstrap/spec.md`
|
||||||
|
- **近期变更**:
|
||||||
|
- 新增后端镜像构建基础:`server/Dockerfile`、`server/.dockerignore`
|
||||||
|
- 新增后端镜像打包与推送工作流:`.gitea/workflows/server-build.yml`(自动递增 semver tag 并推送到 Docker Hub)
|
||||||
|
|
||||||
## Onboarding App Shell
|
## Onboarding App Shell
|
||||||
|
|
||||||
@@ -54,6 +57,10 @@
|
|||||||
- `spec_kit/iOS Widget/spec.md`
|
- `spec_kit/iOS Widget/spec.md`
|
||||||
- `spec_kit/iOS Widget/plan.md`
|
- `spec_kit/iOS Widget/plan.md`
|
||||||
- `spec_kit/iOS Widget/tasks.md`
|
- `spec_kit/iOS Widget/tasks.md`
|
||||||
|
- **近期变更**:
|
||||||
|
- iOS 主 App Bundle ID 已统一为 `com.damer.mindfulness`,Widget Extension 为 `com.damer.mindfulness.emotionwidget`
|
||||||
|
- iOS 构建号已提升到 `2`,并将 `client/ios/client/Info.plist` 改为自动跟随 `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
|
||||||
|
- 推送 entitlements 的 `aps-environment` 已切到 `production`(用于 TestFlight/线上包)
|
||||||
|
|
||||||
## Splash Consent
|
## Splash Consent
|
||||||
|
|
||||||
|
|||||||