87 Commits

Author SHA1 Message Date
吕新雨
43e33eb991 fix:更新协议隐私问题 2026-02-25 17:42:34 +08:00
吕新雨
7fcc64f6e3 fix:更新技术支持网址 2026-02-25 17:13:29 +08:00
吕新雨
9d6829eceb fix:修复错误 2026-02-24 10:51:32 +08:00
吕新雨
4838bcef4b fix:更新图片 2026-02-24 10:44:36 +08:00
吕新雨
0fcf85a081 更新任务生成 2026-02-13 22:46:01 +08:00
吕新雨
62fcc4bfce fix:每日推荐修复 2026-02-12 13:54:34 +08:00
吕新雨
eef5210c99 fix:更新定时任务push 2026-02-11 13:50:02 +08:00
吕新雨
402cbf90eb 修复:定时任务 2026-02-11 11:14:51 +08:00
decc7f9564 Merge pull request 'chore: App 名稱 Hey Mama → Dear Mama + 相關修復' (#22) from Lei-0210 into main
Reviewed-on: #22
2026-02-10 09:24:25 +00:00
173cee75d5 Merge pull request 'damer-0210' (#21) from damer-0210 into main
Reviewed-on: #21
2026-02-10 09:24:00 +00:00
吕新雨
076bd5636f fix:更新APP-PUSH 2026-02-10 17:23:38 +08:00
吕新雨
154f347ddb 注册token排查 2026-02-10 16:57:36 +08:00
吕新雨
dec3ac82e1 fix:后端错误 2026-02-10 16:47:18 +08:00
雷汀岚
e552e22de9 chore: App 名稱 Hey Mama → Dear Mama + 相關修復
- 品牌與顯示:app.json、Info.plist、package scheme、iOS 產物 DearMama.app
- 協議與條款:隱私協議/使用條款全文、設計文檔、server legal_docs + legal API、push 預設 title
- 多語言:all.json / zh-TW / zh-CN / en / es / pt 的 consent、widget 標題與引導文案
- 小工具:EmotionWidget.swift 品牌文案、Dear Mama.xcscheme
- CocoaPods:project.pbxproj objectVersion 70→56 以通過 pod install
- Metro:react-native-text-size 用 require + extraNodeModules 解析
- textWrap:measureWidthImpl 改為 require 載入 react-native-text-size

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 16:16:02 +08:00
吕新雨
1fbc0aa3f8 fix:重复点击 2026-02-10 15:06:50 +08:00
吕新雨
b5532df161 小组件换行文案更新 2026-02-10 13:35:32 +08:00
5515726465 Merge pull request '更新换行算法和APP-PUSH' (#19) from damer into main
Reviewed-on: #19
2026-02-10 03:40:00 +00:00
吕新雨
ee2d9f44ea 更新换行算法和APP-PUSH 2026-02-10 11:39:33 +08:00
雷汀岚
ce018880f4 fix(i18n): 繁中開屏 consent 文案不生效 - 寫死繁中文案、清理腳本、文件
- splash: 繁中 consent 使用元件內 ZH_TW_CONSENT,避免 bundle 快取
- splash: 使用 isTraditionalChineseLocaleTag 判斷繁中
- i18n: 註解與 __DEV__ debug log
- package: clean:cache, start:clean, ios:clean, clean:ios-build
- ALL_COPY: 故障排除與 consent 只改 zh-TW.json 說明
- spec_kit: Splash Consent overflow 記錄排查結論

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:05:17 +08:00
雷汀岚
b4ec17fcac onboarding: 转场动画、名字步取消自动跳页、标题个性化招呼语、Skip/提醒步等文案与交互优化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 17:33:53 +08:00
雷汀岚
aa4e1e9947 onboarding: 选项字体与问题一致、底部间距、提醒页文案与移除底部 Skip
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 17:11:13 +08:00
4578d503e7 Merge pull request '增加定时服务的健康检测' (#18) from damer into main
Reviewed-on: #18
2026-02-09 07:40:02 +00:00
吕新雨
f03d36b5e9 增加定时服务的健康检测 2026-02-09 15:39:46 +08:00
66241e5231 Merge pull request 'damer' (#17) from damer into main
Reviewed-on: #17
2026-02-09 06:49:41 +00:00
吕新雨
e980bd4e4d fix:更新容器启动 2026-02-09 14:47:17 +08:00
吕新雨
0b8bbebf6a fix:增加定时推动 2026-02-09 11:52:11 +08:00
吕新雨
1e1e49ea57 fix:同意隐私 2026-02-05 16:33:58 +08:00
吕新雨
8e71503169 fix:修复 2026-02-05 16:06:49 +08:00
吕新雨
2b67a571bb f 2026-02-05 02:02:14 +08:00
吕新雨
8f84f25616 IOS小组件/文案 2026-02-05 01:49:29 +08:00
吕新雨
4c03fce720 APP-PUSH和纯色小组件 2026-02-05 01:14:13 +08:00
吕新雨
c1c2c6197d fix:小组件- PUSH 2026-02-03 17:43:58 +08:00
d742b398ef Merge pull request 'docs: 更新隐私协议与用户使用协议' (#16) from lei into main
Reviewed-on: #16
2026-02-03 05:51:54 +00:00
吕新雨
d525c6b939 fix:test打包 2026-02-03 13:50:58 +08:00
吕新雨
c37bb2e2a5 fix:打包 2026-02-03 11:19:25 +08:00
吕新雨
4c450db9b2 fix:环境变量 2026-02-03 02:36:42 +08:00
8c02c5b9a0 Merge pull request 'feat(client): i18n copy + reco lang refresh + home polish' (#15) from Hao into main
Reviewed-on: #15
2026-02-02 18:13:13 +00:00
吕新雨
90cbde1bac fix:部署 2026-02-03 02:04:48 +08:00
吕新雨
bb7a4c7361 fix:main.py 2026-02-03 01:44:25 +08:00
吕新雨
17d19f9172 更新蓝v 2026-02-03 01:36:35 +08:00
吕新雨
b35b71470d upload 2026-02-03 01:25:47 +08:00
吕新雨
ccdc6350a1 最后一班 2026-02-03 01:22:25 +08:00
吕新雨
7fea799087 最后一班 2026-02-03 01:21:10 +08:00
吕新雨
e6b64b39f2 最后一班 2026-02-03 01:18:47 +08:00
吕新雨
a457186f46 放在宿主机 2026-02-03 01:07:06 +08:00
吕新雨
2d5b8b094f fix:upload 2026-02-03 00:44:00 +08:00
吕新雨
ef7c4e774d end 2026-02-03 00:41:38 +08:00
吕新雨
cc36301638 u 2026-02-03 00:36:48 +08:00
吕新雨
5dd632d4d3 fix:更新 2026-02-03 00:26:13 +08:00
吕新雨
4d70f16b69 更新部署工作了 2026-02-03 00:23:48 +08:00
吕新雨
2ddee6b8f8 fix:更新runner 2026-02-02 23:02:49 +08:00
吕新雨
aa530b0ce3 fix:更新runner 2026-02-02 22:58:42 +08:00
cdd0ac32de Merge pull request 'fix:更新自建仓库' (#13) from damer into main
Reviewed-on: #13
2026-02-02 14:50:28 +00:00
吕新雨
d98ec76dc0 fix:更新自建仓库 2026-02-02 22:50:11 +08:00
3c35e14c3d Merge pull request 'damer' (#12) from damer into main
Reviewed-on: #12
2026-02-02 14:47:30 +00:00
吕新雨
c0deea9318 fix:更新自建仓库 2026-02-02 22:47:09 +08:00
915e995ab7 Merge pull request 'fix:更新' (#11) from damer into main
Reviewed-on: #11
2026-02-02 14:26:16 +00:00
吕新雨
0e42e6f2a9 fix:更新 2026-02-02 22:25:48 +08:00
39e4bcab6c Merge pull request 'fix:更新server' (#10) from damer into main
Reviewed-on: #10
2026-02-02 14:22:49 +00:00
吕新雨
69a1046ff4 fix:更新server 2026-02-02 22:22:20 +08:00
ceaf459d97 Merge pull request 'damer' (#9) from damer into main
Reviewed-on: #9
2026-02-02 14:01:03 +00:00
吕新雨
d9a5dbafd6 fix:新增工作流 2026-02-02 21:59:15 +08:00
1
996999453a feat(client): i18n copy + reco lang refresh + home polish 2026-02-02 20:11:09 +08:00
雷汀岚
3f91e734fa docs: 更新隐私协议与用户使用协议 2026-02-02 18:58:12 +08:00
吕新雨
86e4853709 Merge branch 'main' into damer 2026-02-02 18:37:44 +08:00
吕新雨
2adf2475fa fix:git忽略文件 2026-02-02 18:37:00 +08:00
Hao
9dbba04408 Merge pull request 'Hao' (#8) from Hao into main
Reviewed-on: #8
2026-02-02 09:33:41 +00:00
1
64b8352ad3 fix:并 2026-02-02 17:33:20 +08:00
1
4b739dd194 feat: 完善启动分发逻辑,优化小组件背景适配与点赞图标交互 2026-02-02 17:17:10 +08:00
吕新雨
240cdda68f Merge branch 'main' into damer 2026-02-02 16:54:51 +08:00
吕新雨
ce48e54c03 增加git忽略 2026-02-02 16:53:26 +08:00
吕新雨
d045237952 chore: 清理 Python 运行产物并忽略
- 从仓库移除 __pycache__/ 与 *.pyc 等运行产物,避免 git pull 覆盖未跟踪文件导致合并失败
- 在 .gitignore 增加 Python 运行产物忽略规则
2026-02-02 16:52:22 +08:00
1
228fd7fd84 feat: 首页支持风景图与纯色背景随滑动自动切换,优化点赞图标样式与交互 2026-02-02 16:50:38 +08:00
3587a24115 Merge pull request '新功能:个性化推荐算法' (#7) from damer into main
Reviewed-on: #7
2026-02-02 08:48:11 +00:00
吕新雨
6dc4e2b943 新功能:个性化推荐算法 2026-02-02 16:47:37 +08:00
吕新雨
936094211b Merge remote-tracking branch 'origin/main' 2026-02-02 11:27:00 +08:00
吕新雨
be38d817d5 Merge branch 'damer' 2026-02-02 11:23:53 +08:00
吕新雨
58d17fc39f fix:更新算法 2026-02-02 11:22:35 +08:00
Hao
502a6ac500 Merge pull request 'Hao' (#5) from Hao into main
Reviewed-on: #5
2026-02-02 02:51:14 +00:00
f49cbb7186 Merge pull request 'fix:文明' (#4) from damer into main
Reviewed-on: #4
2026-02-02 02:49:27 +00:00
吕新雨
814b96edb6 fix:文明 2026-02-02 10:47:24 +08:00
1
868c5cac40 feat: 更新 App 名称为 Hey Mama,优化 iOS 情绪小组件样式与背景铺满 2026-01-31 19:46:34 +08:00
1
e675cbbbfb feat: 完善个人中心与小组件功能,优化 Onboarding 交互与手势体验 2026-01-31 19:02:54 +08:00
Hao
c4c7d7d251 Merge pull request 'main' (#3) from main into Hao
Reviewed-on: #3
2026-01-30 10:16:42 +00:00
Hao
7e4074d457 Merge pull request 'Hao' (#2) from Hao into main
Reviewed-on: #2
2026-01-30 10:15:45 +00:00
1
a9ea31b235 fix: resolve Expo SDK 54 compatibility issues and React 19 JSX runtime errors 2026-01-30 18:09:56 +08:00
1
0c71494815 改页 2026-01-30 16:34:40 +08:00
376 changed files with 33010 additions and 1524 deletions

View File

@@ -1,4 +1,69 @@
请开始完成编码
客户端请按照标准的RN架构目录写代码
客户端API请求 统一使用utlis中封装的请求
客户端的架构目录参考
project-root
├── android/ # Android 原生工程
├── ios/ # iOS 原生工程
├── src/ # 业务代码主目录 ⭐⭐⭐
│ ├── app.tsx # App 入口(注册 Provider / Navigation
│ ├── navigation/ # 路由导航
│ │ ├── index.tsx
│ │ ├── RootNavigator.tsx
│ │ └── types.ts
│ ├── screens/ # 页面Screen 级别)
│ │ ├── Home/
│ │ │ ├── index.tsx
│ │ │ ├── styles.ts
│ │ │ └── hooks.ts
│ │ └── Profile/
│ ├── components/ # 通用 UI 组件(无业务)
│ │ ├── Button/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── Empty/
│ ├── modules/ # 业务模块(强烈推荐)
│ │ ├── user/
│ │ │ ├── api.ts
│ │ │ ├── model.ts
│ │ │ ├── store.ts
│ │ │ └── index.ts
│ │ └── emotion/
│ ├── services/ # 跨模块服务(网络、存储等)
│ │ ├── http.ts # axios/fetch 封装
│ │ ├── storage.ts # AsyncStorage 封装
│ │ └── logger.ts
│ ├── store/ # 全局状态Redux / Zustand / Jotai
│ │ ├── index.ts
│ │ └── middleware.ts
│ ├── hooks/ # 全局通用 hooks
│ │ ├── useTheme.ts
│ │ └── useDebounce.ts
│ ├── utils/ # 工具函数
│ │ ├── date.ts
│ │ └── uuid.ts
│ ├── constants/ # 常量
│ │ ├── colors.ts
│ │ ├── env.ts
│ │ └── storageKeys.ts
│ ├── assets/ # 静态资源
│ │ ├── images/
│ │ ├── icons/
│ │ └── fonts/
│ ├── theme/ # 主题系统
│ │ ├── index.ts
│ │ └── dark.ts
│ └── types/ # 全局 TS 类型
│ └── index.d.ts
├── __tests__/ # 测试
├── .env # 环境变量
├── babel.config.js
├── metro.config.js
├── tsconfig.json
├── package.json
└── index.js # RN 启动入口
后端请按照标准的python FastAPI 架构目录写代码
现在多语言仅支持 EN / TC
整个task.md执行完毕后需要在对应的overview.md标记并且说明变更的文件名

View File

@@ -1,4 +1,4 @@
当前有一个很大的 spec.md大需求规范需要按业务逻辑拆分成多个子模块规范。
当前有一个很大的 spec.md大需求规范需要按业务逻辑合理拆分成多个子模块规范。
请按以下规则拆分:
@@ -9,6 +9,6 @@
- 输入/输出定义
- 验收标准(可验证)
3. 拆分后输出一个 `modules/` 目录结构列表,并为每个模块生成对应 spec 内容。
4. 保留大 spec.md 的高层背景/总览到 overview 部分。
4. 保留大 spec.md 的高层背景/总览到 overview 部分,并标明各个模块的实现顺序
5. 子模块之间按逻辑关系关联。
6. 不生成 plan.md 或 tasks.md仅拆出子模块 spec。

View File

@@ -2,3 +2,4 @@
根据对应的plan.md 生成task.md
任务清单详细可执行
执行完要标记
整个task.md执行完毕后需要在对应的overview.md标记

View File

@@ -0,0 +1 @@
使用测试工具完成集成测试,并给我一份简单的测试报告

View File

@@ -28,4 +28,5 @@ modules/ 可嵌套 modules/,每层都独立规范。
输出时根据这个结构生成内容时,请保持文件职责清晰。
简短记录项目的该层每个spec的内容 每次编码完成后更新overview.md
可以通过nvm 切换node版本
在对数据库操作中,禁止执行破坏性操作,如果必须请让我同意,并回复:允许操作数据库

View 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

View File

@@ -0,0 +1,104 @@
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 tagvX.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: |
# 直接写死:推送到自建仓库
IMAGE_NAME="docker.damer.fun/damer/mindfulness-server"
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITHUB_ENV"
fi
if [[ -n "${GITEA_ENV:-}" ]]; then
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITEA_ENV"
fi
# 4⃣ 登录镜像仓库(自建 Registry / Docker Hub
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
# 与 IMAGE_NAME 的 registry 保持一致
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"

View File

@@ -0,0 +1,497 @@
name: Deploy Server (SSH + Nginx 蓝绿)
# 手动触发选择部署环境dev/pro并输入要部署的镜像 tag
on:
workflow_dispatch:
inputs:
env:
description: '部署环境'
type: choice
required: true
options:
- dev
- pro
tag:
description: '要部署的镜像 Tag例如v1.2.7'
required: true
# 同一环境同一时间只允许一个部署在跑,避免互相覆盖
concurrency:
group: deploy-server-${{ github.event.inputs.env }}
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
env:
# 统一在 env 中注入变量,减少 runner 差异带来的兼容性问题
DEPLOY_ENV: ${{ github.event.inputs.env }}
DEPLOY_TAG: ${{ github.event.inputs.tag }}
# 镜像名(优先 vars.DOCKER_IMAGE未配置则步骤里兜底
DOCKER_IMAGE: ${{ vars.DOCKER_IMAGE }}
# 可选:明确 registry私有仓库用。未配置会从 DOCKER_IMAGE 推断
DOCKER_REGISTRY: ${{ vars.DOCKER_REGISTRY }}
# SSHdev 用 secretspro 用 vars按你现有用法
SSH_HOST: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_HOST || vars.PRO_SSH_HOST }}
SSH_USER: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_USER || vars.PRO_SSH_USER }}
SSH_PORT: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_PORT || vars.PRO_SSH_PORT }}
SSH_KEY: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_KEY || secrets.PRO_SSH_KEY }}
# 推荐把私钥做成 base64单行存到 Secrets避免多行变量丢换行
SSH_KEY_B64: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_KEY_B64 || secrets.PRO_SSH_KEY_B64 }}
# Nginx upstream 配置(建议放到 vars
NGINX_UPSTREAM_FILE: ${{ vars.NGINX_UPSTREAM_FILE }}
NGINX_UPSTREAM_NAME: ${{ vars.NGINX_UPSTREAM_NAME }}
# 蓝绿端口(宿主机端口,建议放到 vars未配置则脚本内有默认值
BLUE_PORT: ${{ vars.BLUE_PORT }}
GREEN_PORT: ${{ vars.GREEN_PORT }}
# 容器内监听端口FastAPI 常用 8000未配置则默认 8000
CONTAINER_PORT: ${{ vars.CONTAINER_PORT }}
# 健康检查路径(未配置则默认 /health如果你没有 health 接口,可改为 /docs 或 /
HEALTHCHECK_PATH: ${{ vars.HEALTHCHECK_PATH }}
# 定时服务健康检查路径(检查 Redis/Worker/Beat未配置则默认 /v1/push/scheduler/health
SCHEDULER_HEALTHCHECK_PATH: ${{ vars.SCHEDULER_HEALTHCHECK_PATH }}
# 可选:远端 env 文件路径(例如 /opt/mindfulness-server/.env.prod存在则 docker run --env-file
REMOTE_ENV_FILE: ${{ vars.REMOTE_ENV_FILE }}
steps:
- name: 配置 SSH Key密钥登陆
shell: bash
run: |
set -euo pipefail
# 镜像名兜底(与 .gitea/workflows/server-build.yml 的默认一致)
if [[ -z "${DOCKER_IMAGE:-}" ]]; then
DOCKER_IMAGE="docker.damer.fun/damer/mindfulness-server"
fi
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> "$GITHUB_ENV"
fi
if [[ -n "${GITEA_ENV:-}" ]]; then
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> "$GITEA_ENV"
fi
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 从 Secrets 写入私钥(推荐使用 *_SSH_KEY_B64
SSH_KEY_PATH="${HOME}/.ssh/id_rsa"
if [[ -n "${SSH_KEY_B64:-}" ]]; then
# 兼容两种常见误配置:
# 1) *_SSH_KEY_B64 里其实粘贴的是“原始私钥”(包含 -----BEGIN ... PRIVATE KEY-----
# 2) base64 值在粘贴/保存过程中混入空白或其他无关字符
if printf '%s' "${SSH_KEY_B64}" | grep -qE 'BEGIN[[:space:]].*PRIVATE[[:space:]]KEY'; then
echo "提示:检测到 *_SSH_KEY_B64 看起来是原始私钥内容,将按原始私钥写入(建议你改用真正的 base64 单行值)。"
echo "SSH_KEY_B64 字符数(原始):${#SSH_KEY_B64}"
printf '%s' "${SSH_KEY_B64}" | tr -d '\r' > "${SSH_KEY_PATH}"
else
CLEAN_B64="$(printf '%s' "${SSH_KEY_B64}" | tr -d '\r\n\t ')"
echo "SSH_KEY_B64 字符数(原始/清理后):${#SSH_KEY_B64}/${#CLEAN_B64}"
if ! printf '%s' "${CLEAN_B64}" | base64 -d -i | tr -d '\r' > "${SSH_KEY_PATH}"; then
echo "私钥 base64 解码失败:"
echo "- 请确认你在 Gitea Secrets 配置的 *_SSH_KEY_B64 是“单行 base64 字符串”,不要带引号/前后空格。"
echo "- 推荐用仓库里的脚本生成并复制node scripts/ssh-key-to-b64.mjs ~/.ssh/你的私钥 --clipboard"
exit 1
fi
fi
else
echo "使用 SSH_KEY原始私钥写入。SSH_KEY 字符数:${#SSH_KEY}"
printf '%s' "${SSH_KEY}" | tr -d '\r' > "${SSH_KEY_PATH}"
fi
chmod 600 "${SSH_KEY_PATH}"
# 自检(不输出私钥内容,仅输出元信息,方便定位是否写入了错误文件/被截断)
echo "SSH_KEY_PATH=${SSH_KEY_PATH}"
KEY_BYTES="$(wc -c < "${SSH_KEY_PATH}" | tr -d ' ')"
KEY_LINES="$(wc -l < "${SSH_KEY_PATH}" | tr -d ' ')"
echo "SSH key 字节数:${KEY_BYTES}"
echo "SSH key 行数:${KEY_LINES}"
echo "SSH key 首行:$(head -n 1 "${SSH_KEY_PATH}" | tr -d '\r')"
echo "SSH key 末行:$(tail -n 1 "${SSH_KEY_PATH}" | tr -d '\r')"
if command -v stat >/dev/null 2>&1; then
# Ubuntu runner: stat -c不同系统做兼容
if stat -c '%a %U %G %n' "${SSH_KEY_PATH}" >/dev/null 2>&1; then
echo "SSH key 权限:$(stat -c '%a %U %G %n' "${SSH_KEY_PATH}")"
else
echo "SSH key 权限:$(stat -f '%Lp %Su %Sg %N' "${SSH_KEY_PATH}" 2>/dev/null || true)"
fi
fi
if command -v file >/dev/null 2>&1; then
echo "file(1) 识别:$(file "${SSH_KEY_PATH}")"
fi
if command -v sha256sum >/dev/null 2>&1; then
echo "SSH key sha256(前12位)$(sha256sum "${SSH_KEY_PATH}" | awk '{print substr($1,1,12)}')"
fi
echo "ssh 版本:$(ssh -V 2>&1 || true)"
echo "ssh-keygen 版本:$(ssh-keygen -V 2>&1 || true)"
echo "openssl 版本:$(openssl version 2>&1 || true)"
# 进一步校验OpenSSH 私钥中间的 base64 块是否可解码(不输出内容)
if grep -q '^-----BEGIN OPENSSH PRIVATE KEY-----$' "${SSH_KEY_PATH}" && grep -q '^-----END OPENSSH PRIVATE KEY-----$' "${SSH_KEY_PATH}"; then
# 注意:不要用变量名 in是 awk 关键字,部分实现会报语法错)
OPENSSH_B64_LEN="$(
awk '
BEGIN{in_block=0; n=0}
/^-----BEGIN OPENSSH PRIVATE KEY-----$/{in_block=1; next}
/^-----END OPENSSH PRIVATE KEY-----$/{in_block=0; exit}
in_block==1{gsub(/\r/,""); n+=length($0)}
END{print n}
' "${SSH_KEY_PATH}" 2>/dev/null || true
)"
if [[ -n "${OPENSSH_B64_LEN:-}" ]]; then
echo "OpenSSH base64 块字符数(合计):${OPENSSH_B64_LEN}"
fi
# 该校验仅用于提示,不应阻断部署流程
if ! awk '
BEGIN{in_block=0}
/^-----BEGIN OPENSSH PRIVATE KEY-----$/{in_block=1; next}
/^-----END OPENSSH PRIVATE KEY-----$/{in_block=0; exit}
in_block==1{gsub(/\r/,""); print}
' "${SSH_KEY_PATH}" 2>/dev/null | tr -d '\n' | base64 -d >/dev/null 2>&1; then
echo "提示OpenSSH 私钥的 base64 块无法解码(疑似内容被截断/损坏),将继续执行 ssh-keygen 校验定位。"
fi
fi
# 额外信息ssh-keygen -lf 的报错(不输出私钥内容)
if ! ssh-keygen -lf "${SSH_KEY_PATH}" >/dev/null 2>~/.ssh/ssh_key_fingerprint.err; then
echo "ssh-keygen -lf 报错:"
cat ~/.ssh/ssh_key_fingerprint.err || true
fi
# 快速校验私钥是否可解析(不会输出私钥内容)
if ! ssh-keygen -y -f "${SSH_KEY_PATH}" >/dev/null 2>~/.ssh/ssh_key_check.err; then
echo "SSH 私钥无法解析。下面是 ssh-keygen 的报错(不包含私钥内容):"
cat ~/.ssh/ssh_key_check.err || true
echo
echo "常见原因:"
echo "- 你填的是公钥(.pub不是私钥"
echo "- 私钥带口令CI 无法交互输入 passphrase"
echo "- 内容复制丢换行/被截断/不是正确的 base64"
echo
echo "推荐:生成一把“无口令”的部署专用私钥,并用 base64 存储到 Secrets*_SSH_KEY_B64。"
exit 1
fi
SSH_PORT="${SSH_PORT:-22}"
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "SSH_PORT=${SSH_PORT}" >> "$GITHUB_ENV"
fi
if [[ -n "${GITEA_ENV:-}" ]]; then
echo "SSH_PORT=${SSH_PORT}" >> "$GITEA_ENV"
fi
# 预写 known_hosts避免交互
ssh-keyscan -p "${SSH_PORT}" -H "${SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null
- name: 验证 SSH 连接(快速定位公钥/用户/端口问题)
shell: bash
run: |
set -euo pipefail
SSH_PORT="${SSH_PORT:-22}"
ssh -p "${SSH_PORT}" \
-i ~/.ssh/id_rsa \
-o StrictHostKeyChecking=yes \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
"${SSH_USER}@${SSH_HOST}" 'echo "SSH_OK $(whoami)@$(hostname)"'
- name: 远程登录 Docker Registry如需私有镜像
shell: bash
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
run: |
set -euo pipefail
# 如果镜像是公开的,可不配 DOCKER_USERNAME/DOCKER_TOKEN此步骤会自动跳过
if [[ -z "${DOCKER_USERNAME:-}" || -z "${DOCKER_TOKEN:-}" ]]; then
echo "未配置 DOCKER_USERNAME/DOCKER_TOKEN跳过远程 docker login。"
exit 0
fi
SSH_PORT="${SSH_PORT:-22}"
# 推断 registry优先使用 DOCKER_REGISTRY否则从 DOCKER_IMAGE 的第一个段推断
REG="${DOCKER_REGISTRY:-}"
if [[ -z "${REG}" ]]; then
FIRST_SEG="${DOCKER_IMAGE%%/*}"
if [[ "${FIRST_SEG}" == *.* || "${FIRST_SEG}" == *:* || "${FIRST_SEG}" == "localhost" ]]; then
REG="${FIRST_SEG}"
fi
fi
if [[ -z "${REG}" ]]; then
echo "无法推断 registry看起来像 Docker Hub 公有镜像),跳过 docker login。"
exit 0
fi
ssh -p "${SSH_PORT}" -i ~/.ssh/id_rsa -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes \
"${SSH_USER}@${SSH_HOST}" bash -s -- "${DOCKER_TOKEN}" "${DOCKER_USERNAME}" "${REG}" <<'REMOTE'
set -euo pipefail
TOKEN="$1"
USERNAME="$2"
REGISTRY="$3"
SUDO=""
if [[ "$(id -u)" -ne 0 ]] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
printf '%s' "$TOKEN" | ${SUDO} docker login "${REGISTRY}" -u "$USERNAME" --password-stdin
REMOTE
- name: SSH 部署Nginx 蓝绿切换)
shell: bash
run: |
set -euo pipefail
SSH_PORT="${SSH_PORT:-22}"
NGINX_UPSTREAM_NAME="${NGINX_UPSTREAM_NAME:-mindfulness_backend}"
NGINX_UPSTREAM_FILE="${NGINX_UPSTREAM_FILE:-/etc/nginx/conf.d/api.damer.fun.conf}"
BLUE_PORT="${BLUE_PORT:-8001}"
GREEN_PORT="${GREEN_PORT:-8002}"
CONTAINER_PORT="${CONTAINER_PORT:-8000}"
HEALTHCHECK_PATH="${HEALTHCHECK_PATH:-/health}"
SCHEDULER_HEALTHCHECK_PATH="${SCHEDULER_HEALTHCHECK_PATH:-/v1/push/scheduler/health}"
echo "准备部署:${DOCKER_IMAGE}:${DEPLOY_TAG} -> ${DEPLOY_ENV} (${SSH_USER}@${SSH_HOST}:${SSH_PORT})"
ssh -p "${SSH_PORT}" -i ~/.ssh/id_rsa -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes "${SSH_USER}@${SSH_HOST}" bash -s -- \
"${DOCKER_IMAGE}" "${DEPLOY_TAG}" "${NGINX_UPSTREAM_FILE}" "${NGINX_UPSTREAM_NAME}" "${BLUE_PORT}" "${GREEN_PORT}" "${CONTAINER_PORT}" "${HEALTHCHECK_PATH}" "${SCHEDULER_HEALTHCHECK_PATH}" "${REMOTE_ENV_FILE:-}" <<'REMOTE'
set -euo pipefail
IMAGE="$1"
TAG="$2"
UPSTREAM_FILE="$3"
UPSTREAM_NAME="$4"
BLUE_PORT="$5"
GREEN_PORT="$6"
CONTAINER_PORT="$7"
HEALTHCHECK_PATH="$8"
SCHEDULER_HEALTHCHECK_PATH="$9"
REMOTE_ENV_FILE="${10}"
APP_DIR="/opt/mindfulness-server"
ACTIVE_FILE="${APP_DIR}/active_color"
mkdir -p "${APP_DIR}"
# 判断 sudo如果非 root 且存在 sudo则使用 sudo
SUDO=""
if [[ "$(id -u)" -ne 0 ]] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
ACTIVE_COLOR="blue"
if [[ -f "${ACTIVE_FILE}" ]]; then
ACTIVE_COLOR="$(cat "${ACTIVE_FILE}" || echo blue)"
fi
if [[ "${ACTIVE_COLOR}" == "blue" ]]; then
NEW_COLOR="green"
NEW_PORT="${GREEN_PORT}"
OLD_COLOR="blue"
OLD_PORT="${BLUE_PORT}"
else
NEW_COLOR="blue"
NEW_PORT="${BLUE_PORT}"
OLD_COLOR="green"
OLD_PORT="${GREEN_PORT}"
fi
echo "当前在线:${ACTIVE_COLOR}${OLD_PORT}),准备发布:${NEW_COLOR}${NEW_PORT}"
# 拉取镜像
${SUDO} docker pull "${IMAGE}:${TAG}"
# 启动新颜色容器API + Worker + Beat
API_NAME="mindfulness-server-api-${NEW_COLOR}"
WORKER_NAME="mindfulness-server-worker-${NEW_COLOR}"
BEAT_NAME="mindfulness-server-beat-${NEW_COLOR}"
# 兼容旧命名:之前可能只有一个 mindfulness-server-blue/green
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" >/dev/null 2>&1 || true
${SUDO} docker rm -f "${API_NAME}" >/dev/null 2>&1 || true
${SUDO} docker rm -f "${WORKER_NAME}" >/dev/null 2>&1 || true
${SUDO} docker rm -f "${BEAT_NAME}" >/dev/null 2>&1 || true
ENV_FILE_ARGS=()
if [[ -n "${REMOTE_ENV_FILE}" && -f "${REMOTE_ENV_FILE}" ]]; then
ENV_FILE_ARGS=(--env-file "${REMOTE_ENV_FILE}")
echo "将使用远端 env 文件:${REMOTE_ENV_FILE}"
elif [[ -n "${REMOTE_ENV_FILE}" ]]; then
echo "提示REMOTE_ENV_FILE 已配置但文件不存在:${REMOTE_ENV_FILE}(将忽略 env-file"
fi
# API对外暴露端口仅该容器参与蓝绿切流
${SUDO} docker run -d \
--name "${API_NAME}" \
--restart=always \
-p "${NEW_PORT}:${CONTAINER_PORT}" \
"${ENV_FILE_ARGS[@]}" \
-e START_API=1 -e START_WORKER=0 -e START_BEAT=0 \
"${IMAGE}:${TAG}"
# Worker处理异步/ETA 任务,不暴露端口)
${SUDO} docker run -d \
--name "${WORKER_NAME}" \
--restart=always \
"${ENV_FILE_ARGS[@]}" \
-e START_API=0 -e START_WORKER=1 -e START_BEAT=0 \
"${IMAGE}:${TAG}"
# Beat定时调度不暴露端口
${SUDO} docker run -d \
--name "${BEAT_NAME}" \
--restart=always \
"${ENV_FILE_ARGS[@]}" \
-e START_API=0 -e START_WORKER=0 -e START_BEAT=1 \
"${IMAGE}:${TAG}"
# 健康检查
if [[ "${HEALTHCHECK_PATH}" != /* ]]; then
HEALTHCHECK_PATH="/${HEALTHCHECK_PATH}"
fi
HEALTH_URL="http://127.0.0.1:${NEW_PORT}${HEALTHCHECK_PATH}"
echo "健康检查:${HEALTH_URL}"
for i in $(seq 1 30); do
if command -v curl >/dev/null 2>&1; then
if curl -fsS "${HEALTH_URL}" >/dev/null; then
echo "健康检查通过"
break
fi
elif command -v wget >/dev/null 2>&1; then
if wget -q -O /dev/null "${HEALTH_URL}"; then
echo "健康检查通过"
break
fi
else
echo "远端缺少 curl/wget跳过 HTTP 健康检查"
break
fi
if [[ "$i" -eq 30 ]]; then
echo "健康检查失败:新版本未就绪,回滚并退出"
${SUDO} docker logs --tail 200 "${API_NAME}" || true
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
sleep 2
done
# 定时服务健康检查(确保 Redis/Worker/Beat 都 OK避免“接口正常但定时任务没跑”
if [[ "${SCHEDULER_HEALTHCHECK_PATH}" != /* ]]; then
SCHEDULER_HEALTHCHECK_PATH="/${SCHEDULER_HEALTHCHECK_PATH}"
fi
SCHED_URL="http://127.0.0.1:${NEW_PORT}${SCHEDULER_HEALTHCHECK_PATH}"
echo "定时服务健康检查:${SCHED_URL}"
# Beat 心跳是按分钟刷新,这里最多等 90 秒45*2s
for i in $(seq 1 45); do
RES="$(curl -fsS "${SCHED_URL}" 2>/dev/null || true)"
if [[ -n "${RES}" ]] \
&& echo "${RES}" | grep -q '"redis":{"ok":true' \
&& echo "${RES}" | grep -q '"worker":{"ok":true' \
&& echo "${RES}" | grep -q '"beat":{"ok":true' ; then
echo "定时服务健康检查通过:${RES}"
break
fi
if [[ "$i" -eq 45 ]]; then
echo "定时服务健康检查失败:${RES}"
echo "Worker/Beat 日志(各 120 行):"
${SUDO} docker logs --tail 120 "${WORKER_NAME}" || true
${SUDO} docker logs --tail 120 "${BEAT_NAME}" || true
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
sleep 2
done
# 切换 Nginx upstream在同一个 conf 文件中通过 backup 做主备切换)
if [[ ! -f "${UPSTREAM_FILE}" ]]; then
echo "未找到 Nginx upstream 配置文件:${UPSTREAM_FILE}"
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
if ! ${SUDO} grep -qE "upstream[[:space:]]+${UPSTREAM_NAME}[[:space:]]*\\{" "${UPSTREAM_FILE}"; then
echo "在 ${UPSTREAM_FILE} 中未找到 upstream${UPSTREAM_NAME}"
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
if ! ${SUDO} grep -qE "server[[:space:]]+127\\.0\\.0\\.1:${BLUE_PORT}" "${UPSTREAM_FILE}"; then
echo "在 ${UPSTREAM_FILE} 中未找到 server 127.0.0.1:${BLUE_PORT}(请先按参考配置写入 upstream"
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
if ! ${SUDO} grep -qE "server[[:space:]]+127\\.0\\.0\\.1:${GREEN_PORT}" "${UPSTREAM_FILE}"; then
echo "在 ${UPSTREAM_FILE} 中未找到 server 127.0.0.1:${GREEN_PORT}(请先按参考配置写入 upstream"
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
# 备份(回滚用)
BACKUP_FILE="${UPSTREAM_FILE}.bak.$(date +%s)"
${SUDO} cp -f "${UPSTREAM_FILE}" "${BACKUP_FILE}"
echo "切换 upstream${UPSTREAM_NAME}${OLD_PORT} -> ${NEW_PORT})通过 backup 切换"
TMP_FILE="$(mktemp)"
${SUDO} awk -v name="${UPSTREAM_NAME}" -v old="${OLD_PORT}" -v new="${NEW_PORT}" '
BEGIN { in_up = 0 }
$0 ~ ("^[[:space:]]*upstream[[:space:]]+" name "[[:space:]]*\\{[[:space:]]*$") { in_up = 1 }
in_up == 1 {
# 新端口:主(去掉 backup
if ($0 ~ ("server[[:space:]]+127\\.0\\.0\\.1:" new)) {
gsub(/[[:space:]]+backup[[:space:]]*;/, ";")
}
# 旧端口:备(确保有 backup;
if ($0 ~ ("server[[:space:]]+127\\.0\\.0\\.1:" old)) {
gsub(/[[:space:]]+backup[[:space:]]*;/, ";")
sub(/;[[:space:]]*$/, " backup;")
}
}
in_up == 1 && $0 ~ /^[[:space:]]*\}[[:space:]]*$/ { in_up = 0 }
{ print }
' "${UPSTREAM_FILE}" > "${TMP_FILE}"
${SUDO} cp -f "${TMP_FILE}" "${UPSTREAM_FILE}"
rm -f "${TMP_FILE}"
# 校验并 reload nginx失败则回滚并退出
if ${SUDO} nginx -t; then
${SUDO} nginx -s reload
else
echo "Nginx 配置校验失败,回滚 upstream 配置并退出"
${SUDO} cp -f "${BACKUP_FILE}" "${UPSTREAM_FILE}" || true
${SUDO} nginx -t && ${SUDO} nginx -s reload || true
${SUDO} docker rm -f "${API_NAME}" "${WORKER_NAME}" "${BEAT_NAME}" >/dev/null 2>&1 || true
exit 1
fi
# 记录当前在线颜色
echo "${NEW_COLOR}" | ${SUDO} tee "${ACTIVE_FILE}" >/dev/null
# 下线旧容器(切流后再停旧的)
OLD_API_NAME="mindfulness-server-api-${OLD_COLOR}"
OLD_WORKER_NAME="mindfulness-server-worker-${OLD_COLOR}"
OLD_BEAT_NAME="mindfulness-server-beat-${OLD_COLOR}"
${SUDO} docker rm -f "${OLD_API_NAME}" "${OLD_WORKER_NAME}" "${OLD_BEAT_NAME}" >/dev/null 2>&1 || true
# 兼容旧命名
${SUDO} docker rm -f "mindfulness-server-${OLD_COLOR}" >/dev/null 2>&1 || true
echo "部署完成:${NEW_COLOR} 已上线"
REMOTE

4
.gitignore vendored
View File

@@ -4,6 +4,10 @@
.DS_Store
*.pem
# Python运行产物
__pycache__/
*.py[cod]
# Node / JS
node_modules/
npm-debug.*

View File

@@ -3,6 +3,19 @@
本项目面向宝妈群体,提供情绪价值与正念练习支持。
客户端采用 React Native + Expo后端采用 Python + FastAPI数据库 MySQL任务调度 Celery推送/定时等功能完整支持。
# git提交
git add .
git commit -m '备注'
git push origin
获取最新分支
git pull
# 创建自己的分支
git checkout -b 姓名拼写
# 生产密钥
ssh-keygen -t rsa -b 4096 -m PEM -N '' -f deploy_key_rsa
# 目录结构
/mindfulness

View File

@@ -1,3 +1,7 @@
EXPO_PUBLIC_API_BASE_URL=http://localhost:8000
EXPO_PUBLIC_ENV=dev
EXPO_PUBLIC_DEFAULT_LANGUAGE=auto
#
# Expo/EAS 项目 IDUUID。用于真机获取 Expo Push Tokenexpo-notifications
# 获取方式:在 client 目录执行 `eas project:init` 或 `eas project:info` 查看。
EXPO_PUBLIC_EAS_PROJECT_ID=c519f016-e5c8-426c-868f-5545dce8beef

1
client/.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmmirror.com

33
client/app.config.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { ConfigContext, ExpoConfig } from 'expo/config';
/**
* 运行时获取 Push Tokenexpo-notifications在真机/Dev Client 场景下通常需要 projectId。
*
* 这里把 projectId 注入到 `extra.eas.projectId`
* - 开发/本地:从 `.env.local`EXPO_PUBLIC_EAS_PROJECT_ID读取并写入配置
* - CI/EAS也可通过环境变量注入EXPO_PUBLIC_EAS_PROJECT_ID 或 EAS_PROJECT_ID
*/
export default ({ config }: ConfigContext): ExpoConfig => {
const projectId =
process.env.EXPO_PUBLIC_EAS_PROJECT_ID ||
// 兼容部分 CI/EAS 注入的变量名
process.env.EAS_PROJECT_ID;
return {
...config,
// ExpoConfig 的类型要求 name 必填,避免 `...config` 的可选类型导致 tsc 报错
name: config.name ?? 'client',
// slug 在绝大多数场景也建议固定为非空字符串(保持与 app.json 一致)
slug: config.slug ?? 'client',
extra: {
...(config.extra ?? {}),
eas: {
// 保留已有配置,再覆盖 projectId
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(((config.extra as any) ?? {}).eas ?? {}),
projectId: projectId ?? (config.extra as any)?.eas?.projectId,
},
},
};
};

View File

@@ -1,6 +1,6 @@
{
"expo": {
"name": "client",
"name": "Dear Mama",
"slug": "client",
"version": "1.0.0",
"orientation": "portrait",
@@ -9,13 +9,13 @@
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash-icon.png",
"image": "./assets/images/Screen_page.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
"backgroundColor": "#EAD2BA"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.anonymous.client"
"bundleIdentifier": "com.damer.mindfulness"
},
"android": {
"adaptiveIcon": {
@@ -23,7 +23,8 @@
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
"predictiveBackGestureEnabled": false,
"package": "com.damer.mindfulness"
},
"web": {
"bundler": "metro",
@@ -35,6 +36,13 @@
],
"experiments": {
"typedRoutes": true
}
},
"extra": {
"eas": {
"projectId": "c519f016-e5c8-426c-868f-5545dce8beef"
},
"router": {}
},
"owner": "damersu"
}
}

View File

@@ -1,9 +1,22 @@
import { useEffect } from 'react';
import { Stack } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromStorage } from '@/src/modules/dailyWidgetReco';
export default function AppLayout() {
const { t } = useTranslation();
useEffect(() => {
// 小组件数据同步(仅 iOS 生效;内部会判断原生模块是否可用)
// 目的:避免“仅 Onboarding 写入一次”导致老用户小组件一直显示兜底文案
void (async () => {
await syncWidgetConfig();
await syncWidgetUserProfileFromStorage();
await ensureDailyWidgetRecoUpToDate({ reason: 'app_start' });
})();
}, []);
return (
<Stack
screenOptions={{
@@ -13,6 +26,8 @@ export default function AppLayout() {
<Stack.Screen
name="home"
options={{
// Home 页不使用系统 Header避免 iOS 原生导航栏自带的“毛玻璃/液玻璃”材质
headerShown: false,
// 卡片页标题按设计留空(右上角为 icon 按钮)
title: '',
headerShadowVisible: false,

View File

@@ -1,8 +1,18 @@
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Pressable } from 'react-native';
import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react';
import {
StyleSheet,
View,
Dimensions,
Text,
Pressable,
PanResponder,
Animated as RNAnimated,
ImageBackground,
Platform,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation } from 'expo-router';
import { useFocusEffect } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
Easing,
runOnJS,
@@ -19,165 +29,692 @@ import {
getUserProfile,
setReaction,
setThemeMode,
getRecoFeedCache,
setRecoFeedCache,
getUserProfileScoring,
getRecoFeedHistory,
recordRecoFeedServed,
type ThemeMode,
getSuixinThemeState,
setSuixinThemeState,
type SuixinThemeStateV1,
} from '@/src/storage/appStorage';
import { fetchRecoFeed } from '@/src/services/recoApi';
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
import ProfileModal from '@/components/home/ProfileModal';
import ThemeModal from '@/components/home/ThemeModal';
import ThemeIcon from '@/assets/images/home/theme.svg';
import MyIcon from '@/assets/images/home/my.svg';
import LikeOutlineIcon from '@/assets/images/home/like.svg';
import LikeFilledIcon from '@/assets/images/home/like_filled.svg';
import HateIcon from '@/assets/images/home/hate.svg';
import LikeIcon from '@/assets/images/icon/like_icon.svg';
import { getBootId } from '@/src/utils/bootSession';
import { advanceSuixinState, buildInitialSuixinState, NEUTRAL_THEME_COLORS } from '@/src/features/suixinTheme';
import { wrapText } from '@/src/features/textWrap';
import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure';
import { ensureDailyWidgetRecoUpToDate } from '@/src/modules/dailyWidgetReco';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
// 预定义风景图列表
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',
];
type FeedItem = { content_id: string; text: string };
export default function HomeScreen() {
const { t } = useTranslation();
const navigation = useNavigation();
const { t, i18n } = useTranslation();
const isEnglish = i18n.language?.startsWith('en');
const recoLang: 'en' | 'tc' = toBackendLocaleFromLanguageTag(i18n.language);
const insets = useSafeAreaInsets();
const [index, setIndex] = useState(0);
const [themeMode, setThemeModeState] = useState<ThemeMode>('scenery');
const [suixinBgColor, setSuixinBgColor] = useState<string>(NEUTRAL_THEME_COLORS[1]);
const [themeOpen, setThemeOpen] = useState(false);
const [profileOpen, setProfileOpen] = useState(false);
const [profileName, setProfileName] = useState<string | undefined>(undefined);
const [busy, setBusy] = useState(false);
const [likeFilled, setLikeFilled] = useState(false);
const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]);
const [feedItems, setFeedItems] = useState<FeedItem[]>([]);
const [isFetching, setIsFetching] = useState(false);
const [cardWidth, setCardWidth] = useState<number | null>(null);
const [wrappedText, setWrappedText] = useState<string>('');
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 重复执行
const feedItemsRef = useRef<FeedItem[]>([]);
const isFetchingRef = useRef(false);
const themeModeRef = useRef<ThemeMode>('scenery');
const suixinStateRef = useRef<SuixinThemeStateV1 | null>(null);
useEffect(() => {
feedItemsRef.current = feedItems;
}, [feedItems]);
useEffect(() => {
isFetchingRef.current = isFetching;
}, [isFetching]);
useEffect(() => {
themeModeRef.current = themeMode;
}, [themeMode]);
const ensureSuixinReady = useCallback(async () => {
const bootId = getBootId();
const stored = await getSuixinThemeState();
if (stored && stored.boot_id === bootId) {
suixinStateRef.current = stored;
setSuixinBgColor(stored.last_color || NEUTRAL_THEME_COLORS[1]);
return stored;
}
const profile = await getUserProfileScoring();
const next = buildInitialSuixinState({ bootId, profile });
suixinStateRef.current = next;
setSuixinBgColor(next.last_color || NEUTRAL_THEME_COLORS[1]);
await setSuixinThemeState(next);
return next;
}, []);
const advanceSuixinOnNextContent = useCallback(async () => {
if (themeModeRef.current !== 'suixin') return;
const bootId = getBootId();
let current = suixinStateRef.current;
if (!current) {
current = await getSuixinThemeState();
}
// 冷启动后首次触发/或状态丢失:先初始化
if (!current || current.boot_id !== bootId) {
await ensureSuixinReady();
return;
}
const next = advanceSuixinState(current);
suixinStateRef.current = next;
setSuixinBgColor(next.last_color || NEUTRAL_THEME_COLORS[1]);
await setSuixinThemeState(next);
}, [ensureSuixinReady]);
// 动画相关 Shared Values
const translateY = useSharedValue(0);
const opacity = useSharedValue(1);
const likeScale = useSharedValue(1);
// 统一文案对象结构
const currentFeed = useMemo(() => {
if (feedItems.length > 0) {
return feedItems;
}
return MOCK_CONTENT.map(item => ({
content_id: item.id,
text: t(item.textKey)
}));
}, [feedItems, t]);
useEffect(() => {
currentFeedRef.current = currentFeed;
}, [currentFeed]);
const item = useMemo(() => {
const data = currentFeed[index % currentFeed.length];
return {
id: String(data.content_id),
text: data.text
};
}, [currentFeed, index]);
// Home 文案使用自主换行算法Text Wrap 模块)
// - 通过 onLayout 获取容器宽度
// - 注入真实测量实现,确保“宽度派”评分与实际渲染一致
useEffect(() => {
let cancelled = false;
// 未拿到宽度前先用原文(避免闪烁)
if (!cardWidth || cardWidth <= 0) {
if (__DEV__) {
const key = `noWidth|${item.id}|${String(cardWidth)}`;
if (wrapLogRef.current?.key !== key) {
wrapLogRef.current = { key };
console.log('[TextWrap][Home] cardWidth 未就绪,先回退原文', {
itemId: item.id,
lang: recoLang,
cardWidth,
themeMode,
});
}
}
setWrappedText(item.text);
return () => {
cancelled = true;
};
}
const paddingHorizontal = themeMode === 'scenery' ? 50 : 30;
const availableWidth = Math.max(0, Math.floor(cardWidth - paddingHorizontal * 2));
const lang = recoLang === 'en' ? 'EN' : 'TC';
const fontFamily =
lang === 'EN'
? 'STIXTwoText'
: Platform.select({
ios: 'System',
android: 'sans-serif',
default: 'System',
});
const fontSpec = {
fontSize: 22,
fontWeight: lang === 'EN' ? '600' : '700',
fontFamily: String(fontFamily ?? 'System'),
};
(async () => {
try {
if (__DEV__) {
const key = `start|${item.id}|${lang}|${availableWidth}|${themeMode}`;
if (wrapLogRef.current?.key !== key) {
wrapLogRef.current = { key };
console.log('[TextWrap][Home] wrapText 开始', {
itemId: item.id,
lang,
themeMode,
cardWidth,
paddingHorizontal,
availableWidth,
fontSpec,
textPreview: String(item.text ?? '').slice(0, 80),
textLength: String(item.text ?? '').length,
});
}
}
const res = await wrapText({
text: item.text,
lang,
context: 'APP',
availableWidth,
maxLines: 3,
overflowMode: 'CLIP',
lineMode: 'AUTO',
// Home采用“更偏好语气停顿/更好看”的排版风格微调(不影响算法默认 v1
configVersion: 'v1-home',
debug: __DEV__,
fontSpec,
contextProfile: `APP|${Platform.OS}|home|${lang}`,
measureWidthImpl: defaultMeasureWidthImpl,
scoringOverrides:
lang === 'TC'
? {
// 更偏好在逗号/句号等处断行(即便宽度允许也不一定要塞满)
weights: { R_PUNCT_BREAK: 180 },
// 让“理想行宽”更短,避免宽屏下过度延后断行
idealWidthRatio: { APP: 0.82 },
// 更宽容短行(尤其是第一行在标点处停顿)
minPreferredRatio: 0.45,
shortLastLineRatio: 0.45,
}
: undefined,
});
if (cancelled) return;
if (__DEV__) {
console.log('[TextWrap][Home] wrapText 成功', {
itemId: item.id,
wrappedText: res.wrappedText,
linesCount: res.lines.length,
meta: res.meta,
});
}
setWrappedText(res.wrappedText);
} catch (error) {
// 任何异常都回退到原文,避免影响 Home 主流程
if (__DEV__) {
console.log('[TextWrap][Home] wrapText 异常,回退原文', {
itemId: item.id,
lang,
availableWidth,
fontSpec,
errorName: (error as any)?.name,
errorMessage: String((error as any)?.message ?? error),
errorStack: (error as any)?.stack,
});
}
if (cancelled) return;
setWrappedText(item.text);
}
})();
return () => {
cancelled = true;
};
}, [item.text, cardWidth, recoLang, themeMode]);
// 异步拉取新文案
const fetchNewFeed = useCallback(async () => {
if (isFetchingRef.current) return;
isFetchingRef.current = true;
setIsFetching(true);
try {
const scoringProfile = await getUserProfileScoring();
if (!scoringProfile) return;
const history = await getRecoFeedHistory();
const { items, meta } = await fetchRecoFeed({
k: 30,
user_profile: scoringProfile,
already_recommended_ids: history.already_recommended_ids,
touched_or_viewed_ids: history.touched_or_viewed_ids,
});
if (items.length > 0) {
const wasEmpty = feedItemsRef.current.length === 0;
const newCache = {
saved_at: new Date().toISOString(),
lang: recoLang,
items: items.map((x) => ({ content_id: x.content_id, text: x.text })),
meta: meta as Record<string, unknown>,
};
await setRecoFeedCache(newCache);
await recordRecoFeedServed(items.map((x) => x.content_id));
setFeedItems(newCache.items.map((x) => ({ content_id: String(x.content_id), text: x.text })));
// 如果当前是 mock 数据,切换到新数据的第一条
if (wasEmpty) {
setIndex(0);
}
}
} catch (error) {
console.error('Failed to fetch new feed:', error);
} finally {
isFetchingRef.current = false;
setIsFetching(false);
}
}, [recoLang]);
// 每次进入页面或页面获得焦点时刷新个人信息和缓存文案
useFocusEffect(
useCallback(() => {
let cancelled = false;
(async () => {
const mode = await getThemeMode();
const profile = await getUserProfile();
const cache = await getRecoFeedCache();
if (cancelled) return;
setThemeModeState(mode);
setProfileName(profile.name);
// 随心:若当前主题为随心,进入 Home 时确保状态就绪(仅冷启动会话重算)
if (mode === 'suixin') {
ensureSuixinReady().catch(() => {
// ignore失败时回退默认中性底色
setSuixinBgColor(NEUTRAL_THEME_COLORS[1]);
});
}
// 语言切换时:旧语言缓存不复用,触发重新拉取
if (cache && cache.items.length > 0 && (cache.lang ?? 'en') === recoLang) {
setFeedItems(cache.items.map((x) => ({ content_id: String(x.content_id), text: x.text })));
} else {
// 语言不匹配或没有缓存:先清空回落到本地 mock会立即随语言切换再拉取对应语言的推荐文案
setFeedItems([]);
setIndex(0);
fetchNewFeed();
}
// Widget前台辅助刷新尽力而为
// - 写入 App Group 的 dailyReco 缓存
// - 生成 wrapped_text_by_family供 Widget 直接渲染
ensureDailyWidgetRecoUpToDate({ reason: 'home_focus' }).catch(() => {});
})();
return () => {
cancelled = true;
};
}, []);
}, [fetchNewFeed, recoLang, ensureSuixinReady])
);
const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2';
const backgroundColor = useMemo(() => {
if (themeMode === 'suixin') {
return suixinBgColor;
}
if (themeMode === 'color') {
const colorIndex = Math.floor(index / 10) % THEME_COLORS.length;
return THEME_COLORS[colorIndex];
}
return '#F4D6C2'; // 风景模式下的默认底色(图片加载前显示)
}, [themeMode, suixinBgColor, index]);
useLayoutEffect(() => {
navigation.setOptions({
headerShadowVisible: false,
headerStyle: { backgroundColor },
headerRight: () => (
<View style={styles.headerRight}>
<CircleIconButton
onPress={() => setThemeOpen(true)}
accessibilityLabel={t('home.theme')}
>
<ThemeIcon width={18} height={18} />
</CircleIconButton>
<CircleIconButton
onPress={() => setProfileOpen(true)}
accessibilityLabel={t('home.profile')}
>
<MyIcon width={18} height={18} />
</CircleIconButton>
</View>
),
});
}, [backgroundColor, navigation, t]);
// 计算当前应该显示的风景图索引(滑动 10 次切换一张)
const natureImageIndex = useMemo(() => {
return Math.floor(index / 10) % NATURE_IMAGES.length;
}, [index]);
const likeScale = useSharedValue(1);
const hateScale = useSharedValue(1);
const currentNatureImage = NATURE_IMAGES[natureImageIndex];
const textAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
opacity: opacity.value,
}));
const likeAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: likeScale.value }],
}));
const hateAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: hateScale.value }],
}));
const setBusySafe = useCallback((next: boolean) => {
busyRef.current = next;
setBusy(next);
}, []);
function playLikeAnimationAndThen(next: () => void) {
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(() => {
if (busyRef.current) return;
setBusySafe(true);
// 注意:不要在 Reanimated worklet 回调里读取 React ref例如 indexRef/currentFeedRef会导致值不更新或异常
const nextIndex = indexRef.current + 1;
// 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);
runOnJS(advanceSuixinOnNextContent)();
// 检查是否需要拉取新文案(注意:不要把匿名函数塞进 runOnJS可能导致原生崩溃
runOnJS(maybeFetchNewFeedIfNeeded)(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, 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);
// 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function
const handlersRef = useRef({ onPressLike, triggerNextContent, triggerPrevContent });
useEffect(() => {
handlersRef.current = { onPressLike, triggerNextContent, triggerPrevContent };
}, [onPressLike, triggerNextContent, triggerPrevContent]);
// 使用系统自带的 PanResponder 代替第三方手势库
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true, // 允许开始捕获
onMoveShouldSetPanResponder: (_, gestureState) => {
// 只有当垂直滑动距离大于 20 时才接管位移手势
return Math.abs(gestureState.dy) > 20;
},
onPanResponderRelease: (_, gestureState) => {
const now = Date.now();
const DOUBLE_TAP_DELAY = 300;
// 1. 双击逻辑判定
if (now - lastTapRef.current < DOUBLE_TAP_DELAY) {
// 判定为双击
if (Math.abs(gestureState.dx) < 10 && Math.abs(gestureState.dy) < 10) {
runOnJS(handlersRef.current.onPressLike)();
lastTapRef.current = 0; // 重置
return;
}
}
lastTapRef.current = now;
// 2. 上滑/下滑逻辑判定
if (gestureState.dy < -50) { // 上滑超过 50pt
runOnJS(handlersRef.current.triggerNextContent)();
} else if (gestureState.dy > 50) { // 下滑超过 50pt
runOnJS(handlersRef.current.triggerPrevContent)();
}
},
})
).current;
async function onPressLike() {
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);
likeScale.value = withSequence(
withTiming(0.92, { duration: 90, easing: Easing.out(Easing.cubic) }),
withTiming(1.14, { duration: 120, easing: Easing.out(Easing.cubic) }),
withTiming(1, { duration: 140, easing: Easing.out(Easing.cubic) }, (finished) => {
if (finished) runOnJS(next)();
})
);
// 1. 获取当前日期
const now = new Date();
const dateStr = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`;
// 2. 保存到收藏夹,包含当前背景信息
const favItem = {
favId: String(Date.now()), // 生成唯一 ID
id: likedItemId,
text: likedItemText,
date: dateStr,
themeMode: themeMode,
background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
};
console.log('Home: Triggering addFavorite', JSON.stringify(favItem));
try {
await addFavorite(favItem);
} catch (error) {
console.error('Home: addFavorite 失败', error);
}
function playHateAnimationAndThen(next: () => void) {
hateScale.value = withSequence(
withTiming(0.92, { duration: 90, easing: Easing.out(Easing.cubic) }),
withTiming(1.06, { duration: 110, easing: Easing.out(Easing.cubic) }),
withTiming(1, { duration: 120, easing: Easing.out(Easing.cubic) }, (finished) => {
if (finished) runOnJS(next)();
})
);
}
function onPressLike() {
if (busy) return;
setBusy(true);
playLikeAnimationAndThen(async () => {
// 3. 记录到后端 Reaction喜欢
console.log('Home: Triggering setReaction', item.id);
try {
await setReaction(item.id, 'like');
await addFavorite(item.id);
setIndex((i) => i + 1);
setTimeout(() => setLikeFilled(false), 220);
setBusy(false);
});
} catch (error) {
console.error('Home: setReaction 失败', error);
}
function onPressHate() {
if (busy) return;
setBusy(true);
playHateAnimationAndThen(async () => {
await setReaction(item.id, 'dislike');
setIndex((i) => i + 1);
setBusy(false);
});
// 4. 爱心缩放动画
likeScale.value = withSequence(
withTiming(0.8, { duration: 100 }),
withTiming(1.2, { duration: 150 }),
withTiming(1, { duration: 100 }, (finished) => {
runOnJS(setLikeInFlight)(false);
if (finished) {
console.log('Home: Like animation finished, triggering next content');
runOnJS(triggerNextContent)();
}
})
);
}
async function onSelectTheme(next: ThemeMode) {
setThemeModeState(next);
await setThemeMode(next);
setThemeOpen(false);
// 切换到随心:不主动重算(除非冷启动会话变化/状态不存在),仅确保可用
if (next === 'suixin') {
await ensureSuixinReady().catch(() => {
setSuixinBgColor(NEUTRAL_THEME_COLORS[1]);
});
}
}
return (
<View style={[styles.container, { backgroundColor }]}>
<View style={styles.card}>
<Animated.Text style={styles.text}>{item.text}</Animated.Text>
<View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}>
{themeMode === 'scenery' && (
<ImageBackground
source={currentNatureImage}
style={StyleSheet.absoluteFill}
resizeMode="cover"
/>
)}
{/* 自绘顶部按钮:不使用系统 Header彻底避免 iOS 导航栏的毛玻璃/液玻璃材质 */}
<View style={[styles.topRight, { top: insets.top + 8 }]}>
<CircleIconButton
onPress={() => setThemeOpen(true)}
accessibilityLabel={t('home.theme')}
>
<ThemeIcon width={20} height={20} />
</CircleIconButton>
<CircleIconButton
onPress={() => setProfileOpen(true)}
accessibilityLabel={t('home.profile')}
>
<MyIcon width={20} height={20} />
</CircleIconButton>
</View>
<View style={styles.actions}>
<Animated.View style={[styles.reactionButton, hateAnimatedStyle]}>
<Pressable
onPress={onPressHate}
onPressIn={() => (hateScale.value = withTiming(0.92, { duration: 80 }))}
onPressOut={() => (hateScale.value = withTiming(1, { duration: 120 }))}
accessibilityRole="button"
accessibilityLabel={t('home.dislike')}
hitSlop={10}
style={styles.reactionInner}
<Animated.View
style={[styles.card, textAnimatedStyle, themeMode === 'scenery' && styles.sceneryCard]}
onLayout={(e) => {
const w = e.nativeEvent.layout.width;
if (Number.isFinite(w) && w > 0) setCardWidth(w);
}}
>
<HateIcon width={30} height={30} />
</Pressable>
<Text style={[styles.text, isEnglish && styles.textEnglish, themeMode === 'scenery' && styles.sceneryText]}>
{wrappedText || item.text}
</Text>
</Animated.View>
<View style={styles.actions}>
<Animated.View style={[styles.reactionButton, likeAnimatedStyle]}>
<Pressable
onPress={onPressLike}
onPressIn={() => (likeScale.value = withTiming(0.92, { duration: 80 }))}
onPressOut={() => (likeScale.value = withTiming(1, { duration: 120 }))}
accessibilityRole="button"
accessibilityLabel={t('home.like')}
hitSlop={10}
// 稍微增大可点击区域,提升单手操作成功率
hitSlop={24}
style={styles.reactionInner}
>
{likeFilled ? (
<LikeFilledIcon width={30} height={30} />
<LikeFilledIcon width={40} height={41} color="#EA6969" />
) : (
<LikeOutlineIcon width={30} height={30} />
<LikeIcon
width={40}
height={41}
color={themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28'}
/>
)}
</Pressable>
</Animated.View>
@@ -205,7 +742,8 @@ function CircleIconButton({
return (
<Pressable
onPress={onPress}
hitSlop={10}
// 稍微增大可点击区域,提升易用性
hitSlop={14}
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
style={styles.circleBtn}
@@ -220,53 +758,71 @@ const styles = StyleSheet.create({
flex: 1,
padding: 20,
justifyContent: 'center',
gap: 16,
alignItems: 'center',
},
headerRight: {
topRight: {
position: 'absolute',
right: 20,
flexDirection: 'row',
gap: 10,
paddingRight: 10,
zIndex: 30,
},
circleBtn: {
width: 34,
height: 34,
borderRadius: 17,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.75)',
alignItems: 'center',
justifyContent: 'center',
},
card: {
borderRadius: 16,
padding: 20,
backgroundColor: 'rgba(255,255,255,0.65)',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#E5E7EB',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 30,
zIndex: 5, // 降低层级,防止遮挡底部按钮
},
text: {
fontSize: 20,
lineHeight: 28,
fontSize: 22,
lineHeight: 32,
color: '#5E2A28',
fontWeight: '700',
textAlign: 'center',
},
textEnglish: {
fontFamily: 'STIXTwoText',
// 英文字体观感更细一点,避免过粗
fontWeight: '600',
},
sceneryCard: {
// 风景模式下稍微收窄文案宽度,增加呼吸感
paddingHorizontal: 50,
},
sceneryText: {
color: '#FFFFFF',
textShadowColor: 'rgba(0, 0, 0, 0.5)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 4,
},
actions: {
position: 'absolute',
bottom: SCREEN_HEIGHT * 0.16,
left: 0,
right: 0,
flexDirection: 'row',
gap: 12,
justifyContent: 'center',
zIndex: 20, // 提升层级,确保在最顶层可点击
},
reactionButton: {
width: 58,
height: 58,
borderRadius: 29,
backgroundColor: 'rgba(255,255,255,0.75)',
alignItems: 'center',
justifyContent: 'center',
},
reactionInner: {
width: 58,
height: 58,
borderRadius: 29,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -12,7 +12,6 @@ export default function OnboardingLayout() {
}}
>
<Stack.Screen name="onboarding" options={{ title: t('onboarding.title') }} />
<Stack.Screen name="push-prompt" options={{ title: t('push.title') }} />
</Stack>
);
}

View File

@@ -1,65 +1,260 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useRouter } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { Alert } from 'react-native';
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout';
import { NameInputStep } from '@/components/onboarding/NameInputStep';
import { IntentSelectionStep } from '@/components/onboarding/IntentSelectionStep';
import { setOnboardingCompleted, setUserProfile } from '@/src/storage/appStorage';
import { SelectionStep } from '@/components/onboarding/SelectionStep';
import { ReminderStep } from '@/components/onboarding/ReminderStep';
import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnaireAnswers } from '@/src/features/userProfileScoring';
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
import { fetchRecoFeed } from '@/src/services/recoApi';
import { ensurePushTokenRegisteredIfPermitted, setPushPreferences } from '@/src/services/pushApi';
import {
recordRecoFeedServed,
setOnboardingCompleted,
setUserProfile,
setDailyReminderSettings,
setUserProfileScoring,
setRecoFeedCache,
setPushPromptState,
} from '@/src/storage/appStorage';
type Step =
| { id: 'name'; type: 'name' }
| { id: 'status' | 'emotion' | 'influence' | 'support'; type: 'selection'; optionIds: string[] }
| { id: 'reminder'; type: 'reminder' };
const STEPS: Step[] = [
{ id: 'name', type: 'name' },
{ id: 'status', type: 'selection', optionIds: ['pregnant', 'has_kids', 'no_fill'] },
{ id: 'emotion', type: 'selection', optionIds: ['happy', 'calm', 'okay', 'tired', 'stressed', 'low'] },
{ id: 'influence', type: 'selection', optionIds: ['family', 'work', 'relationship', 'friends', 'health'] },
{ id: 'support', type: 'selection', optionIds: ['emotional', 'parenting', 'self_worth', 'anxiety', 'balance'] },
{ id: 'reminder', type: 'reminder' },
];
export default function OnboardingScreen() {
const router = useRouter();
const [step, setStep] = useState(0);
const { t, i18n } = useTranslation();
const [stepIndex, setStepIndex] = useState(0);
const [name, setName] = useState('');
const [intents, setIntents] = useState<string[]>([]);
const [selections, setSelections] = useState<Record<string, string[]>>({});
const [reminderTimes, setReminderTimes] = useState(3);
const [finishing, setFinishing] = useState(false);
const currentStep = STEPS[stepIndex];
const currentTitle = useMemo(() => t(`onboardingSurvey.steps.${currentStep.id}.title`), [t, currentStep.id]);
const currentOptions = useMemo(() => {
if (currentStep.type !== 'selection') return [];
return currentStep.optionIds.map((optId) => ({
id: optId,
label: t(`onboardingSurvey.steps.${currentStep.id}.options.${optId}`),
}));
}, [t, currentStep]);
async function onFinish() {
// Save whatever input we have
await setUserProfile({ name, intents });
if (finishing) return;
setFinishing(true);
try {
// 用户选择每日次数 > 0在此页直接触发系统通知权限已移除单独的 push 引导页)。
const wantsPush = reminderTimes > 0;
// 将 Onboarding 选择映射为标准问卷枚举(允许跳过)
const answers = mapOnboardingSelectionsToQuestionnaireAnswers(selections);
// 生成用户画像(供推荐/Push/Widget 复用)
const scoringProfile = buildUserProfileFromQuestionnaire(answers);
await setUserProfileScoring(scoringProfile);
// 同步到 App Group供 iOS Widget 拉取与展示
await syncWidgetConfig();
await syncWidgetUserProfileFromScoring(scoringProfile);
// 可选:前台辅助拉取一次“每日推荐”,提升小组件首次展示的成功率与一致性(失败不阻塞)
await ensureDailyWidgetRecoUpToDate({ reason: 'onboarding_finish', scoringProfile });
// Onboarding 结束后预拉取一次 Feed 文案(失败不阻塞进入首页)
try {
const lang = toBackendLocaleFromLanguageTag(i18n.language);
const { items, meta } = await fetchRecoFeed({
k: 30,
user_profile: {
profile_version: scoringProfile.profile_version,
profile_source: scoringProfile.profile_source,
profile_generated_at: scoringProfile.profile_generated_at,
profile_confidence: scoringProfile.profile_confidence,
profile_answered: scoringProfile.profile_answered,
stage: scoringProfile.stage,
emotion_score: scoringProfile.emotion_score,
context: scoringProfile.context,
need: scoringProfile.need,
},
});
await setRecoFeedCache({
saved_at: new Date().toISOString(),
lang,
items: items.map((x) => ({ content_id: x.content_id, text: x.text })),
meta: meta as Record<string, unknown>,
});
await recordRecoFeedServed(items.map((x) => x.content_id));
} catch {
// 网络失败时使用首页本地 mock 兜底
}
await setUserProfile({
name,
intents: Object.values(selections).flat()
});
await setDailyReminderSettings({
timesPerDay: reminderTimes,
// 这里表示“用户意愿”,不代表系统权限一定已 granted
pushEnabled: wantsPush,
});
await setOnboardingCompleted(true);
router.replace('/(onboarding)/push-prompt');
// 用户选择 0 次(关闭)或跳过:直接进入首页
if (!wantsPush) {
await setPushPromptState('skipped');
router.replace('/(app)/home');
return;
}
async function onNext() {
if (step === 0) {
setStep(1);
// 用户想要 Push请求系统权限并尽量完成 token/偏好上报(失败不阻塞进入首页)
await setPushPromptState('unknown');
try {
const { status } = await Notifications.requestPermissionsAsync();
// iOS 可能出现 provisional临时授权也应视为“已授权”
if (status !== 'granted' && status !== ('provisional' as any)) {
await setPushPromptState('skipped');
return;
}
// iOS 模拟器通常无法获取 Expo Push Token系统限制此时不要提示“失败”而是明确告知需要真机测试。
if (Device.osName === 'iOS' && !Device.isDevice) {
Alert.alert('提示', '当前为 iOS 模拟器,无法获取推送 Token。请使用真机测试推送功能。');
await setPushPromptState('unknown');
return;
}
// 1) 上报 token 到后端(幂等;失败才认为“推送开启失败”)
await ensurePushTokenRegisteredIfPermitted();
// 3) 上报推送偏好(幂等)
// 注意:这一步失败时,后端仍可能已成功接收 token。
// 为避免出现“后端已接收 token 但前端弹窗提示失败”的错觉,这里改为:偏好同步失败不弹“开启失败”,仅记录并继续。
try {
await setPushPreferences({ enabled: wantsPush, timesPerDay: reminderTimes });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.warn('[PushPreferences] 同步失败Onboarding不阻塞', msg);
}
await setPushPromptState('enabled');
} catch (e) {
// 失败不阻塞进入首页;但这里给出更明确的文案(常见原因:模拟器/网络/后端异常)
Alert.alert(t('push.errorTitle'), t('push.errorDesc'));
await setPushPromptState('unknown');
} finally {
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 = () => {
if (finishing) return;
if (stepIndex < STEPS.length - 1) {
setStepIndex(stepIndex + 1);
} else {
await onFinish();
}
onFinish();
}
};
async function onSkip() {
// Skip logic: move to next step regardless of input
if (step === 0) {
setStep(1);
} else {
await onFinish();
}
const onBack = () => {
if (finishing) return;
if (stepIndex > 0) {
setStepIndex(stepIndex - 1);
}
};
// Step 0: Next button requires input
// Step 1: Next button always enabled (can proceed with empty selection)
const nextEnabled = step === 0 ? name.trim().length > 0 : true;
/** 只跳過當前這一步(不填/不選當前題,進入下一步) */
const handleSkipCurrentStep = () => {
if (finishing) return;
if (currentStep.type === 'name') {
onNext();
} else if (currentStep.type === 'selection') {
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
onNext();
} else if (currentStep.type === 'reminder') {
setReminderTimes(0);
onFinish();
}
};
// 题目为多选:点击切换选中状态
const handleToggleSelection = (id: string) => {
setSelections(prev => {
const currentIds = prev[currentStep.id] || [];
const nextIds = currentIds.includes(id)
? currentIds.filter(i => i !== id)
: [...currentIds, id];
return { ...prev, [currentStep.id]: nextIds };
});
};
const handleSkipStep = () => {
if (finishing) return;
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
onNext();
};
return (
<OnboardingLayout
onSkip={onSkip}
onNext={onNext}
nextEnabled={nextEnabled}
title={currentTitle}
currentStep={stepIndex}
totalSteps={STEPS.length - 1}
onSkip={handleSkipCurrentStep}
onBack={onBack}
showBackButton={stepIndex > 0}
userName={name}
>
{step === 0 ? (
{currentStep.type === 'name' && (
<NameInputStep
value={name}
onChangeText={setName}
onSubmitEditing={name.trim().length > 0 ? onNext : undefined}
onNext={onNext}
/>
) : (
<IntentSelectionStep
selectedIds={intents}
onToggle={(id) => {
setIntents(prev =>
prev.includes(id)
? prev.filter(i => i !== id)
: [...prev, id]
);
)}
{currentStep.type === 'selection' && (
<SelectionStep
options={currentOptions}
selectedIds={selections[currentStep.id] || []}
onToggle={handleToggleSelection}
onNext={onNext}
/>
)}
{currentStep.type === 'reminder' && (
<ReminderStep
value={Math.max(1, reminderTimes)}
onChange={setReminderTimes}
onFinish={onFinish}
loading={finishing}
onSkip={() => {
// 跳过每日提醒:视为 0 次(关闭)
setReminderTimes(0);
onFinish();
}}
/>
)}

View File

@@ -1,74 +0,0 @@
import { useState } from 'react';
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
import { useRouter } from 'expo-router';
import { useTranslation } from 'react-i18next';
import * as Notifications from 'expo-notifications';
import { setPushPromptState } from '@/src/storage/appStorage';
export default function PushPromptScreen() {
const router = useRouter();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
async function goHome() {
router.replace('/(app)/home');
}
async function onLater() {
await setPushPromptState('skipped');
await goHome();
}
async function onEnableNow() {
// 触发系统权限申请(可失败,但不阻塞进入主功能)
setLoading(true);
try {
await Notifications.requestPermissionsAsync();
await setPushPromptState('enabled');
await goHome();
} catch (e) {
Alert.alert(t('push.errorTitle'), t('push.errorDesc'));
await goHome();
} finally {
setLoading(false);
}
}
return (
<View style={styles.container}>
<View style={styles.card}>
<Text style={styles.title}>{t('push.cardTitle')}</Text>
<Text style={styles.desc}>{t('push.cardDesc')}</Text>
</View>
<View style={styles.actions}>
<Pressable style={[styles.btn, styles.secondary]} onPress={onLater} disabled={loading}>
<Text style={[styles.btnText, styles.secondaryText]}>{t('push.later')}</Text>
</Pressable>
<Pressable style={[styles.btn, styles.primary]} onPress={onEnableNow} disabled={loading}>
<Text style={styles.btnText}>{loading ? t('push.loading') : t('push.enable')}</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, justifyContent: 'center', gap: 16 },
card: {
borderRadius: 18,
padding: 20,
backgroundColor: '#111827',
gap: 10
},
title: { color: 'white', fontSize: 20, fontWeight: '700' },
desc: { color: '#E5E7EB', fontSize: 15, lineHeight: 21 },
actions: { flexDirection: 'row', gap: 12 },
btn: { flex: 1, paddingVertical: 14, borderRadius: 14, alignItems: 'center' },
primary: { backgroundColor: '#16A34A' },
secondary: { backgroundColor: '#F3F4F6' },
btnText: { fontSize: 16, fontWeight: '600', color: '#FFFFFF' },
secondaryText: { color: '#111827' }
});

View File

@@ -1,39 +1,78 @@
import React, { useEffect, useState } from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity, Dimensions, Platform, Alert } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useRef, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, Platform, Alert, Image } from 'react-native';
import { useRouter } from 'expo-router';
import * as WebBrowser from 'expo-web-browser';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { SafeAreaView } from 'react-native-safe-area-context';
import { setConsentAccepted, getConsentAccepted } from '../../src/storage/appStorage';
import { fetchLegalLinks } from '@/src/services/legalApi';
import { getOnboardingCompleted } from '@/src/storage/appStorage';
import { API_BASE_URL } from '@/src/constants/env';
import { isTraditionalChineseLocaleTag } from '@/src/i18n/locale';
// 导入 SVG 组件
import FlowersBg from '../../assets/images/index/flowers_endbg.svg';
import WelcomeBtn from '../../assets/images/index/welcome_btn.svg';
const { width, height } = Dimensions.get('window');
// 繁中開屏 consent 文案:寫死在元件內,避免 Metro/iOS bundle 快取導致永遠顯示舊文案。
// 若需修改,請改這裡並同步 client/src/i18n/locales/zh-TW.json 的 consent 區塊。
const ZH_TW_CONSENT = {
title: '我們知道,',
subtitle: '當媽媽很不容易。',
subtitleSecondary: '這裡給你一些溫柔的肯定與提醒',
};
export default function SplashScreen() {
const router = useRouter();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [showConsent, setShowConsent] = useState(false);
// 繁中時強制使用上方常數(含 zh-TW / zh-Hant / zh-Hant-TW其餘用 i18n
const isZhTW = isTraditionalChineseLocaleTag(i18n.language || '');
const title = isZhTW ? ZH_TW_CONSENT.title : t('consent.title');
const subtitle = isZhTW ? ZH_TW_CONSENT.subtitle : t('consent.subtitle');
const subtitleSecondary = isZhTW ? ZH_TW_CONSENT.subtitleSecondary : t('consent.subtitleSecondary');
useEffect(() => {
if (typeof __DEV__ !== 'undefined' && __DEV__ && showConsent) {
console.log('[i18n consent] language=', i18n.language, 'title=', title, 'subtitle=', subtitle);
}
}, [showConsent, i18n.language, title, subtitle]);
const [links, setLinks] = useState<{ privacy?: string; terms?: string }>({});
const [linksLoading, setLinksLoading] = useState(false);
const mountedRef = useRef(true);
useEffect(() => {
checkConsent();
}, []);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
const checkConsent = async () => {
const accepted = await getConsentAccepted();
setShowConsent(!accepted);
if (accepted) {
// 如果已经同意过,直接跳转到首页分发
// 注意:这里需要与 app/index.tsx 配合,如果 app/index.tsx 已经判断了 consent=true 不会跳过来,
// 那么这里其实是防守。
// 但如果用户是通过 deep link 或其他方式强制进入 splash这个逻辑会把他们送走。
router.replace('/');
// 已同意协议则直接分发到目标页,避免先回到 /index再二次跳转导致“闪一下”
const completed = await getOnboardingCompleted();
if (completed) {
router.replace('/(app)/home');
} else {
router.replace('/(onboarding)/onboarding');
}
}
};
const handleAgree = async () => {
await setConsentAccepted(true);
setShowConsent(false);
router.replace('/');
// 跳转到 onboarding 流程
router.replace('/(onboarding)/onboarding');
};
const openLink = async (url: string) => {
@@ -44,42 +83,122 @@ export default function SplashScreen() {
}
};
async function refreshLegalLinks(): Promise<{ privacy?: string; terms?: string }> {
if (mountedRef.current) setLinksLoading(true);
try {
const res = await fetchLegalLinks();
const next = { privacy: res.privacyPolicyUrl, terms: res.termsOfUseUrl };
if (mountedRef.current) setLinks(next);
return next;
} catch (e) {
// 不阻塞主流程:失败时不崩溃,链接入口仍可点(会提示)
if (__DEV__) console.log('[LegalLinks] 拉取失败splash:', e);
if (mountedRef.current) setLinks({});
return {};
} finally {
if (mountedRef.current) setLinksLoading(false);
}
}
async function handleOpenLegal(type: 'privacy' | 'terms') {
const currentUrl = type === 'privacy' ? links.privacy : links.terms;
if (currentUrl) {
await openLink(currentUrl);
return;
}
// 链接还没拿到/拉取失败:点击时主动再拉一次,避免“点了没反应”
const next = await refreshLegalLinks();
const nextUrl = type === 'privacy' ? next.privacy : next.terms;
if (nextUrl) {
await openLink(nextUrl);
return;
}
const msg =
typeof __DEV__ !== 'undefined' && __DEV__
? t('consent.linkUnavailableDev', { baseUrl: API_BASE_URL })
: t('consent.linkUnavailable');
Alert.alert(t('common.notice'), msg);
}
// 拉取协议链接(由后端按语言下发;默认 EN
useEffect(() => {
void refreshLegalLinks();
}, []);
const bgDecorationTop = 363;
const bgDecorationHeight = height * 0.6;
const contentTop = bgDecorationTop + (bgDecorationHeight * 0.25);
return (
<View style={styles.container}>
{/* 中间的背景装饰 SVG (现在放在上面,作为上层) */}
<View style={[styles.bgDecorationContainer, { top: bgDecorationTop }]}>
<FlowersBg width={width + 10} height={bgDecorationHeight} />
</View>
{/* 顶部的花图片 (现在放在下面,作为下层) */}
<View style={styles.topImageContainer}>
<Image
source={require('../../assets/images/index/index_flowers.png')}
style={styles.image}
style={styles.topImage}
resizeMode="contain"
/>
</View>
<View style={styles.contentContainer}>
<Text style={styles.title}>{t('consent.title')}</Text>
<Text style={styles.subtitle}>{t('consent.subtitle')}</Text>
{/* 文案内容:主標題兩行 + 可選二級標題(字號更小、顏色更淺);繁中為元件內常數,其餘用 i18n */}
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
<Text style={styles.titleText}>
{title}
{'\n'}
{subtitle}
</Text>
{subtitleSecondary ? (
<Text style={styles.consentSubtitleSecondary}>{subtitleSecondary}</Text>
) : null}
</View>
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
{showConsent && (
<>
<TouchableOpacity onPress={handleAgree} activeOpacity={0.8}>
<LinearGradient
colors={['#F69F7B', '#F99CC0']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.button}
<TouchableOpacity
onPress={handleAgree}
activeOpacity={0.8}
style={styles.buttonWrapper}
accessibilityRole="button"
accessibilityLabel={t('consent.agree')}
>
<Text style={styles.buttonText}>{t('consent.agree')}</Text>
</LinearGradient>
<WelcomeBtn width={87} height={57} />
</TouchableOpacity>
<View style={styles.linksContainer}>
<TouchableOpacity onPress={() => openLink('https://example.com/privacy')}>
<Text style={styles.linkText}>{t('consent.privacy')}</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity onPress={() => openLink('https://example.com/terms')}>
<Text style={styles.linkText}>{t('consent.terms')}</Text>
</TouchableOpacity>
</View>
<Text style={styles.noticeText}>
<Trans
i18nKey="consent.noticeRich"
values={{
privacyLabel: t('consent.privacy'),
termsLabel: t('consent.terms'),
privacySuffix: !links.privacy && linksLoading ? t('consent.linkLoadingSuffix') : '',
termsSuffix: !links.terms && linksLoading ? t('consent.linkLoadingSuffix') : '',
}}
components={{
privacy: (
<Text
style={[styles.noticeLinkText, !links.privacy && styles.noticeLinkTextDisabled]}
onPress={() => void handleOpenLegal('privacy')}
suppressHighlighting
/>
),
terms: (
<Text
style={[styles.noticeLinkText, !links.terms && styles.noticeLinkTextDisabled]}
onPress={() => void handleOpenLegal('terms')}
suppressHighlighting
/>
),
}}
/>
</Text>
</>
)}
</SafeAreaView>
@@ -90,74 +209,68 @@ export default function SplashScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFF9F0', // 假设的背景色,根据图片调整
backgroundColor: '#F5D3B5', // 匹配 Figma 背景色
alignItems: 'center',
},
image: {
width: width * 0.8,
height: height * 0.5,
marginTop: height * 0.1,
topImageContainer: {
marginTop: 60,
zIndex: 1, // 降低层级
},
topImage: {
width: 308,
height: 354,
},
bgDecorationContainer: {
position: 'absolute',
left: -3,
zIndex: 2, // 提高层级,使其覆盖在图片之上
},
contentContainer: {
alignItems: 'center',
marginTop: 20,
paddingHorizontal: 30,
zIndex: 3,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#4A3427', // 深褐
marginBottom: 10,
titleText: {
fontSize: 30,
lineHeight: 42,
color: '#772F00', // 匹配 Figma 文字颜
textAlign: 'center',
fontFamily: Platform.OS === 'ios' ? 'Georgia' : 'serif', // 尝试匹配衬线体
fontWeight: '600',
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
},
subtitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#4A3427', // 深褐色
consentSubtitleSecondary: {
marginTop: 12,
fontSize: 16,
lineHeight: 22,
color: 'rgba(119, 47, 0, 0.6)',
textAlign: 'center',
lineHeight: 24,
fontFamily: Platform.OS === 'ios' ? 'Georgia' : 'serif',
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
},
bottomContainer: {
position: 'absolute',
bottom: 0,
bottom: 60,
width: '100%',
alignItems: 'center',
paddingBottom: 20,
zIndex: 4,
},
button: {
width: width * 0.8,
paddingVertical: 16,
borderRadius: 30,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
shadowColor: '#F69F7B',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 5,
buttonWrapper: {
marginBottom: 40,
},
buttonText: {
color: '#FFF',
fontSize: 18,
noticeText: {
marginTop: 10,
paddingHorizontal: 28,
fontSize: 12,
lineHeight: 16,
textAlign: 'center',
color: 'rgba(119, 47, 0, 0.45)',
},
noticeLinkText: {
fontSize: 12,
// 颜色区分:协议链接更醒目
color: 'rgba(119, 47, 0, 0.75)',
textDecorationLine: 'underline',
fontWeight: '600',
},
linksContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
linkText: {
fontSize: 12,
color: '#999',
textDecorationLine: 'underline',
},
divider: {
width: 1,
height: 12,
backgroundColor: '#CCC',
marginHorizontal: 15,
noticeLinkTextDisabled: {
opacity: 0.55,
},
});

View File

@@ -3,11 +3,28 @@ import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useState } from 'react';
import * as Notifications from 'expo-notifications';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import 'react-native-reanimated';
import { Animated, AppState, Image, StyleSheet, View } from 'react-native';
import { useColorScheme } from '@/components/useColorScheme';
import { initI18n } from '@/src/i18n';
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromStorage } from '@/src/modules/dailyWidgetReco';
import { getOrCreateClientUserId } from '@/src/storage/appStorage';
import { ensurePushTokenRegisteredIfPermitted } from '@/src/services/pushApi';
// 配置通知处理方式(即使不发送也建议配置,以确保权限接口正常)
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
// 新版 expo-notifications 类型要求显式返回 banner/list 行为
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
export {
// Catch any errors thrown by the Layout component.
@@ -16,7 +33,8 @@ export {
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: 'index',
// 让首次启动(未同意协议)直接进入协议页,避免先渲染 index 再跳转导致“闪一下”
initialRouteName: '(splash)/splash',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
@@ -24,10 +42,14 @@ SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
STIXTwoText: require('../assets/fonts/STIXTwoText-VariableFont_wght.ttf'),
...FontAwesome.font,
});
const [i18nReady, setI18nReady] = useState(false);
const [appReady, setAppReady] = useState(false);
const [splashOverlayVisible, setSplashOverlayVisible] = useState(true);
const splashOpacity = useRef(new Animated.Value(1)).current;
const hasHiddenNativeSplashRef = useRef(false);
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
@@ -38,31 +60,117 @@ export default function RootLayout() {
initI18n()
.catch((e) => {
// i18n 初始化失败不应阻塞 App 启动,先打印错误再继续
console.error('i18n 初始化失败', e);
console.error('i18n init failed', e);
})
.finally(() => setI18nReady(true));
}, []);
useEffect(() => {
// 等字体与 i18n 都准备好后再隐藏启动页,避免文案闪烁
if (loaded && i18nReady) {
SplashScreen.hideAsync();
}
// 尽早生成 client_user_id便于后续任意时刻与后端建立关联Push Token/偏好等)
getOrCreateClientUserId()
.then((id) => {
if (__DEV__) console.log('[client_user_id]', id);
})
.catch((e) => {
console.warn('[client_user_id] 生成失败(不阻塞启动)', e);
});
}, []);
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(() => {
// 字体与 i18n 都准备好后,允许渲染 App原生 splash 的隐藏交给 onLayout避免“硬切/闪白”)
if (loaded && i18nReady) setAppReady(true);
}, [loaded, i18nReady]);
const onLayoutRootView = useCallback(() => {
if (!appReady) return;
if (hasHiddenNativeSplashRef.current) return;
hasHiddenNativeSplashRef.current = true;
// 先隐藏原生 splash再把同款覆盖层淡出视觉上实现平滑过渡
void SplashScreen.hideAsync().finally(() => {
Animated.timing(splashOpacity, {
toValue: 0,
duration: 380,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) setSplashOverlayVisible(false);
});
});
}, [appReady, splashOpacity]);
const content = useMemo(() => {
if (!appReady) return null;
return <RootLayoutNav />;
}, [appReady]);
if (!loaded || !i18nReady) {
return null;
}
return <RootLayoutNav />;
return (
<View style={styles.root} onLayout={onLayoutRootView}>
{content}
{splashOverlayVisible && (
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { opacity: splashOpacity }]}>
<View style={styles.splashOverlay}>
<Image
source={require('../assets/images/Screen_page.png')}
style={styles.splashImage}
resizeMode="contain"
/>
</View>
</Animated.View>
)}
</View>
);
}
function RootLayoutNav() {
const colorScheme = useColorScheme();
useEffect(() => {
// iOS 小组件:启动时把必要信息写入共享区,并尽力刷新一次“每日推荐”
syncWidgetConfig().catch(() => {});
syncWidgetUserProfileFromStorage().catch(() => {});
ensureDailyWidgetRecoUpToDate({ reason: 'app_start' }).catch(() => {});
const sub = AppState.addEventListener('change', (state) => {
if (state === 'active') {
// App 回到前台时尝试刷新(失败不阻塞)
ensureDailyWidgetRecoUpToDate({ reason: 'app_active' }).catch(() => {});
}
});
return () => sub.remove();
}, []);
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack screenOptions={{ headerShown: false }}>
{/* 协议页分组(首次启动优先进入) */}
<Stack.Screen name="(splash)" />
{/* 启动分发页:根据 onboarding 状态跳转 */}
<Stack.Screen name="index" />
@@ -79,3 +187,21 @@ function RootLayoutNav() {
</ThemeProvider>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
},
splashOverlay: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
// 与 app.json 的 expo.splash.backgroundColor 保持一致
backgroundColor: '#EAD2BA',
},
splashImage: {
// 覆盖层图片尺寸需与系统原生 Splash 的视觉一致,避免出现“缩小一下”的错觉
width: '100%',
height: '100%',
},
});

View File

@@ -22,9 +22,17 @@ export default function Index() {
return;
}
// 2. 检查 Onboarding
// 2. 检查 Onboarding 是否已完成
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 () => {
cancelled = true;
@@ -41,4 +49,3 @@ export default function Index() {
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,3 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.52529 12.1958C8.05105 12.1958 7.62296 11.8613 7.62737 11.3871C7.63044 11.0573 7.68303 10.7472 7.78512 10.4569C7.98347 9.93995 8.27273 9.47781 8.65289 9.0705C9.04959 8.64752 9.47934 8.26371 9.94215 7.91906C10.4215 7.55875 10.876 7.20627 11.3058 6.86162C11.7355 6.51697 12.0909 6.16449 12.3719 5.80418C12.6529 5.4282 12.7934 5.02089 12.7934 4.58225C12.7934 4.03394 12.6529 3.5483 12.3719 3.12533C12.0909 2.70235 11.6777 2.37337 11.1322 2.13838C10.5868 1.88773 9.8843 1.7624 9.02479 1.7624C8.18182 1.7624 7.36364 1.94256 6.57025 2.30287C6.01404 2.54963 5.47478 2.89275 4.95246 3.33224C4.59297 3.63473 4.06302 3.65873 3.7099 3.34884C3.32525 3.01128 3.30306 2.41578 3.69475 2.08642C4.33235 1.55029 5.01812 1.10547 5.75207 0.751958C6.76033 0.250653 7.93388 0 9.27273 0C10.4463 0 11.4545 0.180157 12.2975 0.54047C13.157 0.900783 13.8182 1.40992 14.281 2.06789C14.7603 2.71018 15 3.48564 15 4.39426C15 4.98956 14.8595 5.51436 14.5785 5.96867C14.2975 6.42298 13.9339 6.83812 13.4876 7.2141C13.0579 7.59008 12.6033 7.95822 12.124 8.31854C11.6446 8.66318 11.1983 9.0235 10.7851 9.39948C10.3884 9.77546 10.0826 10.1906 9.86777 10.6449C9.81955 10.7554 9.78061 10.8695 9.75095 10.9874C9.59709 11.5988 9.15576 12.1958 8.52529 12.1958ZM8.77686 18C8.33058 18 7.95041 17.859 7.63636 17.577C7.32231 17.295 7.16529 16.9191 7.16529 16.4491C7.16529 15.9791 7.32231 15.6031 7.63636 15.3211C7.95041 15.0235 8.33058 14.8747 8.77686 14.8747C9.20661 14.8747 9.57851 15.0235 9.89256 15.3211C10.2066 15.6031 10.3636 15.9791 10.3636 16.4491C10.3636 16.9191 10.2066 17.295 9.89256 17.577C9.57851 17.859 9.20661 18 8.77686 18Z" fill="#404040"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +1,3 @@
<svg width="32" height="27" viewBox="0 0 32 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" stroke="#5E2A28" stroke-width="2"/>
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" stroke="currentColor" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +1,4 @@
<svg width="32" height="27" viewBox="0 0 32 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" fill="#5E2A28" stroke="#5E2A28" stroke-width="2"/>
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" fill="currentColor" stroke="currentColor" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22.7325C12.5691 22.7303 13.1259 22.5663 13.6053 22.2595C14.0847 21.9527 14.467 21.5159 14.7075 21H9.29246C9.53293 21.5159 9.91518 21.9527 10.3946 22.2595C10.874 22.5663 11.4308 22.7303 12 22.7325ZM21.75 16.7325C20.1075 14.835 19.8 12.9825 19.8 10.7325C19.7892 8.98974 19.1911 7.30149 18.1024 5.94058C17.0137 4.57967 15.4979 3.62559 13.8 3.23252C13.8039 3.16258 13.8039 3.09247 13.8 3.02252C13.8 2.52524 13.6024 2.04833 13.2508 1.6967C12.8992 1.34507 12.4222 1.14752 11.925 1.14752C11.4277 1.14752 10.9508 1.34507 10.5991 1.6967C10.2475 2.04833 10.05 2.52524 10.05 3.02252C10.046 3.09247 10.046 3.16258 10.05 3.23252C8.35205 3.62559 6.83626 4.57967 5.74753 5.94058C4.6588 7.30149 4.06073 8.98974 4.04996 10.7325C4.2115 12.9108 3.51125 15.0654 2.09996 16.7325C1.41746 17.6625 1.83746 19.5 3.31496 19.5H20.685C22.1625 19.5 22.59 17.6625 21.75 16.725V16.7325ZM15.75 13.5H13.5V15.75C13.5 16.1478 13.3419 16.5294 13.0606 16.8107C12.7793 17.092 12.3978 17.25 12 17.25C11.6021 17.25 11.2206 17.092 10.9393 16.8107C10.658 16.5294 10.5 16.1478 10.5 15.75V13.5H8.24996C7.85214 13.5 7.4706 13.342 7.1893 13.0607C6.908 12.7794 6.74996 12.3978 6.74996 12C6.74996 11.6022 6.908 11.2207 7.1893 10.9394C7.4706 10.6581 7.85214 10.5 8.24996 10.5H10.5V8.25002C10.5 7.8522 10.658 7.47067 10.9393 7.18936C11.2206 6.90806 11.6021 6.75002 12 6.75002C12.3978 6.75002 12.7793 6.90806 13.0606 7.18936C13.3419 7.47067 13.5 7.8522 13.5 8.25002V10.5H15.75C16.1478 10.5 16.5293 10.6581 16.8106 10.9394C17.0919 11.2207 17.25 11.6022 17.25 12C17.25 12.3978 17.0919 12.7794 16.8106 13.0607C16.5293 13.342 16.1478 13.5 15.75 13.5Z" fill="#F59250"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.535 7.5H15.4575C15.681 7.50404 15.8971 7.4196 16.0586 7.26508C16.2201 7.11056 16.3141 6.89847 16.32 6.675V4.9125C16.3102 4.35536 16.1907 3.80561 15.9682 3.29472C15.7457 2.78384 15.4246 2.32185 15.0234 1.93521C14.6221 1.54857 14.1485 1.24487 13.6297 1.0415C13.1109 0.838125 12.5571 0.73907 12 0.750004C10.8773 0.725863 9.79101 1.14831 8.9796 1.92456C8.1682 2.70081 7.69808 3.76738 7.6725 4.89V6.675C7.6784 6.89847 7.77235 7.11056 7.93389 7.26508C8.09544 7.4196 8.31149 7.50404 8.535 7.5ZM12 3C12.2967 3 12.5867 3.08798 12.8334 3.2528C13.08 3.41762 13.2723 3.65189 13.3858 3.92598C13.4994 4.20007 13.5291 4.50167 13.4712 4.79264C13.4133 5.08361 13.2704 5.35089 13.0607 5.56066C12.8509 5.77044 12.5836 5.9133 12.2926 5.97118C12.0017 6.02906 11.7001 5.99935 11.426 5.88582C11.1519 5.77229 10.9176 5.58003 10.7528 5.33336C10.588 5.08669 10.5 4.79668 10.5 4.5C10.5 4.10218 10.658 3.72065 10.9393 3.43934C11.2206 3.15804 11.6022 3 12 3ZM18.96 3.75H17.73C17.7929 4.08894 17.8255 4.43279 17.8275 4.7775V6.615C17.8295 6.92687 17.7699 7.23605 17.6521 7.52484C17.5344 7.81362 17.3608 8.07633 17.1413 8.2979C16.9218 8.51946 16.6608 8.69554 16.3731 8.81602C16.0855 8.9365 15.7769 8.99902 15.465 9H8.535C8.22313 8.99902 7.91453 8.9365 7.62687 8.81602C7.33921 8.69554 7.07816 8.51946 6.85869 8.2979C6.63921 8.07633 6.46562 7.81362 6.34787 7.52484C6.23012 7.23605 6.17052 6.92687 6.1725 6.615V4.7775C6.17451 4.43279 6.20713 4.08894 6.27 3.75H5.04C4.30065 3.75198 3.59216 4.04656 3.06936 4.56936C2.54656 5.09216 2.25198 5.80066 2.25 6.54V20.46C2.25198 21.1993 2.54656 21.9078 3.06936 22.4306C3.59216 22.9534 4.30065 23.248 5.04 23.25H18.96C19.6993 23.248 20.4078 22.9534 20.9306 22.4306C21.4534 21.9078 21.748 21.1993 21.75 20.46V6.54C21.748 5.80066 21.4534 5.09216 20.9306 4.56936C20.4078 4.04656 19.6993 3.75198 18.96 3.75ZM18.345 15.5775L16.095 19.3275C15.995 19.4947 15.8532 19.6331 15.6836 19.7291C15.514 19.8251 15.3224 19.8754 15.1275 19.875H14.9625C14.743 19.8416 14.5383 19.7441 14.3741 19.5946C14.21 19.4451 14.0937 19.2504 14.04 19.035L13.29 15.9825L11.6475 19.26C11.5538 19.4462 11.4102 19.6028 11.2328 19.7121C11.0553 19.8215 10.851 19.8794 10.6425 19.8794C10.434 19.8794 10.2297 19.8215 10.0522 19.7121C9.87476 19.6028 9.73118 19.4462 9.6375 19.26L8.25 16.5L7.8225 17.1375C7.6574 17.3861 7.4003 17.559 7.10774 17.6181C6.81518 17.6772 6.51114 17.6176 6.2625 17.4525C6.01386 17.2874 5.84099 17.0303 5.78191 16.7377C5.72284 16.4452 5.7824 16.1411 5.9475 15.8925L7.4475 13.6425C7.55672 13.4785 7.70712 13.346 7.88364 13.2583C8.06016 13.1707 8.2566 13.131 8.45331 13.1431C8.65001 13.1553 8.84008 13.2189 9.00446 13.3276C9.16885 13.4363 9.3018 13.5863 9.39 13.7625L10.635 16.2525L12.63 12.2625C12.7326 12.0561 12.8961 11.8862 13.0984 11.7757C13.3008 11.6653 13.5321 11.6197 13.7612 11.645C13.9903 11.6704 14.2061 11.7655 14.3794 11.9175C14.5527 12.0696 14.675 12.2711 14.73 12.495L15.57 15.8625L16.425 14.4375C16.5857 14.198 16.8322 14.0294 17.1137 13.9664C17.3952 13.9035 17.6901 13.9511 17.9375 14.0993C18.1849 14.2476 18.3659 14.4852 18.4431 14.763C18.5203 15.0409 18.4879 15.3379 18.3525 15.5925L18.345 15.5775Z" fill="#88D081"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,4 @@
<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="23.5" cy="23.5" r="23.5" fill="#F5D3B5"/>
<path d="M23.5 14.1562C23.9799 14.1563 24.3689 14.5455 24.3691 15.0254V22.6309H31.9746C32.4544 22.6311 32.8437 23.0202 32.8438 23.5C32.8438 23.9799 32.4545 24.3689 31.9746 24.3691H24.3691V31.9746C24.3689 32.4545 23.9799 32.8438 23.5 32.8438C23.0201 32.8437 22.6311 32.4544 22.6309 31.9746V24.3691H15.0254C14.5455 24.3689 14.1562 23.9799 14.1562 23.5C14.1563 23.0202 14.5456 22.6311 15.0254 22.6309H22.6309V15.0254C22.6311 14.5456 23.0201 14.1563 23.5 14.1562Z" fill="#8F521B"/>
</svg>

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

View File

@@ -0,0 +1,5 @@
<svg width="87" height="57" viewBox="0 0 87 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 28.5C0 12.7599 12.7599 0 28.5 0H58.5C74.2401 0 87 12.7599 87 28.5C87 44.2401 74.2401 57 58.5 57H28.5C12.7599 57 0 44.2401 0 28.5Z" fill="#F2DDCA"/>
<path d="M32 29L54 29" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M49 22L55.2929 28.2929C55.6834 28.6834 55.6834 29.3166 55.2929 29.7071L49 36" stroke="white" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,11 @@
<svg width="87" height="57" viewBox="0 0 87 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 28.5C0 12.7599 12.7599 0 28.5 0H58.5C74.2401 0 87 12.7599 87 28.5C87 44.2401 74.2401 57 58.5 57H28.5C12.7599 57 0 44.2401 0 28.5Z" fill="url(#paint0_linear_32_2902)"/>
<path d="M32 29L54 29" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M49 22L55.2929 28.2929C55.6834 28.6834 55.6834 29.3166 55.2929 29.7071L49 36" stroke="white" stroke-width="3" stroke-linecap="round"/>
<defs>
<linearGradient id="paint0_linear_32_2902" x1="0" y1="28.5" x2="87" y2="28.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F69F7B"/>
<stop offset="1" stop-color="#F99CC0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 719 B

View File

@@ -0,0 +1,3 @@
<svg width="3" height="27" viewBox="0 0 3 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 1.5L1.5 25.5" stroke="#F89DB4" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.49467 13.5033C8.78067 16.7229 9.77067 19.726 11.428 21.4441L11.9707 22L12.5133 21.4441C14.2073 19.7044 15.19 16.7013 15.4467 13.5033C14.3027 13.5827 13.122 13.626 11.9413 13.626C10.7607 13.626 9.63867 13.5899 8.49467 13.5033ZM15.5933 10.9694C15.5053 7.22276 14.486 3.5483 12.5427 1.58474L12 1L11.4573 1.55586C9.514 3.55552 8.524 7.22276 8.40667 10.9405C10.7955 11.1313 13.1957 11.141 15.586 10.9694H15.5933ZM22.4427 9.52561L22.7873 9.35957C22.4041 7.50584 21.5342 5.78328 20.2643 4.36346C18.9944 2.94363 17.3687 1.87607 15.5493 1.2671C17.1553 3.72155 18.0133 7.16501 18.1307 10.6518C19.614 10.4828 21.0657 10.1086 22.4427 9.54005V9.52561ZM18.0867 13.2362C17.9566 16.2416 17.0777 19.1695 15.5273 21.7618C17.6163 21.0689 19.4457 19.7741 20.7781 18.0454C22.1106 16.3167 22.8847 14.2337 23 12.0667C21.415 12.6465 19.7657 13.0391 18.0867 13.2362ZM1 12.0667C1.11534 14.2337 1.88942 16.3167 3.22187 18.0454C4.55432 19.7741 6.38366 21.0689 8.47267 21.7618C6.92232 19.1695 6.0434 16.2416 5.91333 13.2362C4.23432 13.0391 2.58501 12.6465 1 12.0667ZM5.84 10.6879C5.97933 7.17944 6.86667 3.72877 8.45067 1.2671C6.6331 1.87484 5.00872 2.94021 3.73896 4.35734C2.46919 5.77447 1.59825 7.49403 1.21267 9.34513L1.55733 9.51117C2.93096 10.102 4.38274 10.4982 5.86933 10.6879H5.84Z" fill="#EEB054"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="35" height="36" viewBox="0 0 35 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.0879 6C26.9887 6 28.7592 6.93575 30.0762 8.42188C31.3956 9.91097 32.2001 11.89 32.2002 13.8408C32.2002 18.2337 29.1054 22.3696 25.5225 25.499C23.7556 27.0422 21.9254 28.2909 20.4199 29.1494C19.6667 29.5789 19.0062 29.904 18.4863 30.1182C18.2262 30.2253 18.0121 30.3001 17.8467 30.3467C17.6728 30.3956 17.5993 30.4002 17.5996 30.4004C17.5928 30.3997 17.5178 30.3932 17.3525 30.3467C17.1871 30.3001 16.973 30.2253 16.7129 30.1182C16.193 29.904 15.5333 29.5788 14.7803 29.1494C13.2748 28.2909 11.4447 27.0423 9.67773 25.499C6.0947 22.3696 3 18.2338 3 13.8408C3.00008 11.8901 3.80381 9.91108 5.12207 8.42188C6.43787 6.93557 8.20587 6.00033 10.1006 6C11.5129 6.00117 12.8927 6.41742 14.0635 7.19434C14.7589 7.65582 15.3618 8.2322 15.8486 8.89355C16.2951 9.50017 17.0032 9.73135 17.5996 9.73145C18.1961 9.73144 18.905 9.5003 19.3516 8.89355C19.8376 8.23336 20.4389 7.65737 21.1328 7.19629C22.3012 6.4199 23.6779 6.00339 25.0879 6Z" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="37" height="33" viewBox="0 0 37 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.38 0C25.4752 0.00420958 23.6144 0.597538 22.0335 1.70479C21.0003 2.42849 20.117 3.35044 19.4272 4.41503C19.0322 5.02468 17.9678 5.02469 17.5728 4.41504C16.8819 3.34867 15.9968 2.42544 14.9614 1.70122C13.3781 0.593805 11.5146 0.00166536 9.60767 0C4.30433 0 0 5.51285 0 11.0514C0 23.1565 16.0333 33 18.5 33C20.9667 33 37 23.1565 37 11.0514C37 5.51285 32.6957 0 27.38 0Z" fill="#FE3C3C" fill-opacity="0.76"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@@ -0,0 +1,3 @@
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L4.29289 4.29289C4.68342 4.68342 4.68342 5.31658 4.29289 5.70711L1 9" stroke="#C7B4A1" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.48 5.6925L21.27 4.035L12 0.75L2.73003 4.035L2.52003 5.685C2.44503 6.24 0.915026 19.2525 11.0775 22.935L12 23.25L12.915 22.92C23.0475 19.275 21.555 6.2475 21.48 5.6925ZM17.85 9.1425L12.8925 17.0925L12.825 17.1975C12.5406 17.6509 12.0883 17.9733 11.5669 18.0941C11.0455 18.215 10.4974 18.1245 10.0425 17.8425C9.83101 17.7097 9.64547 17.5394 9.49503 17.34L6.29253 13.0725C6.01107 12.791 5.85294 12.4093 5.85294 12.0112C5.85294 11.6132 6.01107 11.2315 6.29253 10.95C6.57399 10.6685 6.95573 10.5104 7.35378 10.5104C7.75182 10.5104 8.13357 10.6685 8.41503 10.95L10.7625 12.75L15.45 7.3875C15.6936 7.0877 16.0435 6.89345 16.4268 6.84521C16.8101 6.79696 17.1972 6.89845 17.5075 7.12853C17.8178 7.3586 18.0274 7.69953 18.0926 8.08029C18.1578 8.46105 18.0736 8.85228 17.8575 9.1725L17.85 9.1425Z" fill="#879CE7"/>
</svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@@ -0,0 +1,4 @@
<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="23.5" cy="23.5" r="23.5" fill="#F5D3B5"/>
<path d="M31.9746 22.6309C32.4544 22.6311 32.8437 23.0202 32.8438 23.5C32.8438 23.9799 32.4545 24.3689 31.9746 24.3691H15.0254C14.5455 24.3689 14.1562 23.9799 14.1562 23.5C14.1563 23.0202 14.5456 22.6311 15.0254 22.6309H31.9746Z" fill="#8F521B"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.34273 11.3513C8.03868 11.81 7.74043 12.2721 7.4301 12.7472C7.33503 12.6924 7.25878 12.6515 7.1859 12.6053C6.46053 12.148 5.7395 11.684 5.00979 11.2335C4.19176 10.7281 3.36408 10.2386 2.42298 9.9833C1.52725 9.74047 0.938938 9.90444 0.473698 10.5824C0.296096 10.8411 0.157103 11.1426 0.0721628 11.4446C-0.0759998 11.9701 -0.00843386 12.4745 0.391171 12.8943C2.3197 14.9191 4.2656 16.9271 6.37607 18.7687L6.41256 18.8006C6.69623 19.0482 6.98003 19.296 7.2776 19.526C7.88183 19.9939 8.57921 20.0718 9.30747 19.9468C9.84414 19.8545 10.3417 19.6592 10.7481 19.2799C11.2181 18.8408 11.5632 18.323 11.7915 17.7186C12.1829 16.6809 12.5477 15.6298 13.0072 14.622C14.4999 11.3489 16.2624 8.22294 18.2995 5.25424C21.1869 1.88662 20.2691 -2.14591 16.5163 1.34116C13.3774 4.3416 10.7346 7.74499 8.34273 11.3513Z" fill="#20BD40"/>
</svg>

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

View File

@@ -0,0 +1,6 @@
<svg width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.28564 2.592C3.28564 1.16048 4.44612 0 5.87764 0H16.5581C17.9896 0 19.1501 1.16048 19.1501 2.592V13.2725C19.1501 14.704 17.9896 15.8645 16.5581 15.8645H5.87764C4.44612 15.8645 3.28564 14.704 3.28564 13.2725V2.592Z" fill="#A360F5"/>
<path d="M22.811 2.592C22.811 1.16048 23.9715 0 25.403 0H36.0835C37.515 0 38.6755 1.16048 38.6755 2.592V13.2725C38.6755 14.704 37.515 15.8645 36.0835 15.8645H25.403C23.9715 15.8645 22.811 14.704 22.811 13.2725V2.592Z" fill="#FB84D3"/>
<path d="M1.83282 29.2908C0.820581 28.2786 0.820582 26.6374 1.83282 25.6252L9.38505 18.073C10.3973 17.0607 12.0385 17.0607 13.0507 18.073L20.6029 25.6252C21.6152 26.6374 21.6152 28.2786 20.6029 29.2908L13.0507 36.8431C12.0384 37.8553 10.3973 37.8553 9.38505 36.8431L1.83282 29.2908Z" fill="#FB84D3"/>
<path d="M22.811 22.1182C22.811 20.6867 23.9715 19.5262 25.403 19.5262H36.0835C37.515 19.5262 38.6755 20.6867 38.6755 22.1182V32.7986C38.6755 34.2302 37.515 35.3906 36.0835 35.3906H25.403C23.9715 35.3906 22.811 34.2302 22.811 32.7986V22.1182Z" fill="#FCB473"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="375" height="449" viewBox="0 0 375 449" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M187.5 64.2824C257.79 -1.71779 343.473 -4.39648 378 2.69061V456.404H-3V2.69061C31.5272 -4.39653 117.21 -1.71782 187.5 64.2824Z" fill="#FFF4EA"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1,11 @@
<svg width="87" height="57" viewBox="0 0 87 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 28.5C0 12.7599 12.7599 0 28.5 0H58.5C74.2401 0 87 12.7599 87 28.5C87 44.2401 74.2401 57 58.5 57H28.5C12.7599 57 0 44.2401 0 28.5Z" fill="url(#paint0_linear_32_2902)"/>
<path d="M32 29L54 29" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M49 22L55.2929 28.2929C55.6834 28.6834 55.6834 29.3166 55.2929 29.7071L49 36" stroke="white" stroke-width="3" stroke-linecap="round"/>
<defs>
<linearGradient id="paint0_linear_32_2902" x1="0" y1="28.5" x2="87" y2="28.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F69F7B"/>
<stop offset="1" stop-color="#F99CC0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -39,13 +39,17 @@ export default function DailyReminderModal({ visible, onClose }: Props) {
}, [visible]);
function clamp(next: number) {
return Math.min(10, Math.max(1, next));
// 需求050 表示关闭)
return Math.min(5, Math.max(0, next));
}
async function onOk() {
if (loading) return;
setLoading(true);
const next: DailyReminderSettings = { timesPerDay, pushEnabled };
const next: DailyReminderSettings = {
timesPerDay: Math.min(5, Math.max(0, Math.round(timesPerDay))),
pushEnabled: Boolean(pushEnabled) && timesPerDay > 0,
};
await setDailyReminderSettings(next);
setLoading(false);
onClose();

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import SheetModal from '@/components/ui/SheetModal';
import { MOCK_CONTENT } from '@/src/constants/mockContent';
import { getFavorites } from '@/src/storage/appStorage';
import { getFavorites, getRecoFeedCache, type FavoriteItem } from '@/src/storage/appStorage';
type Props = {
visible: boolean;
@@ -13,25 +13,50 @@ type Props = {
export default function FavoritesModal({ visible, onClose }: Props) {
const { t } = useTranslation();
const [ids, setIds] = useState<string[]>([]);
const [items, setItems] = useState<(FavoriteItem & { text: string })[]>([]);
// 每次打开时刷新一次,确保展示最新“喜欢”
useEffect(() => {
if (!visible) return;
let cancelled = false;
(async () => {
const list = await getFavorites();
if (!cancelled) setIds(list);
const [favList, cache] = await Promise.all([
getFavorites(),
getRecoFeedCache()
]);
console.log('FavoritesModal: Loaded favList', favList.length, 'items');
console.log('FavoritesModal: Loaded cache items', cache?.items?.length || 0);
if (cancelled) return;
// 建立文案查找表
const textMap = new Map<string, string>();
// 1. 放入 Mock 数据
MOCK_CONTENT.forEach(c => textMap.set(String(c.id), t(c.textKey)));
// 2. 放入缓存数据
if (cache?.items) {
cache.items.forEach(c => textMap.set(String(c.content_id), c.text));
}
// 3. 组装最终展示列表
const enriched = favList.map(fav => {
const favIdStr = String(fav.id);
const text = fav.text || textMap.get(favIdStr);
console.log(`FavoritesModal: Matching fav.id=${favIdStr}, found text=${!!text}, textValue=${text?.substring(0, 10)}...`);
return {
...fav,
text: text || t('favorites.unknownText')
};
});
setItems(enriched);
})();
return () => {
cancelled = true;
};
}, [visible]);
const items = useMemo(() => {
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c]));
return ids.map((id) => map.get(id)).filter(Boolean) as { id: string; text: string }[];
}, [ids]);
}, [visible, t]);
return (
<SheetModal visible={visible} title={t('profile.favorites')} onClose={onClose}>
@@ -41,7 +66,7 @@ export default function FavoritesModal({ visible, onClose }: Props) {
) : (
<FlatList
data={items}
keyExtractor={(it) => it.id}
keyExtractor={(it) => it.favId}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.row}>

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import SheetModal from '@/components/ui/SheetModal';
export type ThemeMode = 'scenery' | 'color';
import type { ThemeMode } from '@/src/storage/appStorage';
type Props = {
visible: boolean;
@@ -16,7 +16,7 @@ type Props = {
export default function ThemeModal({ visible, mode, onSelect, onClose }: Props) {
const { t } = useTranslation();
return (
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose}>
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose} height={360}>
<View style={styles.row}>
<ThemeCard
title={t('theme.scenery')}
@@ -24,7 +24,7 @@ export default function ThemeModal({ visible, mode, onSelect, onClose }: Props)
onPress={() => onSelect('scenery')}
>
<Image
source={require('../../assets/images/index/index_flowers.png')}
source={require('../../assets/images/theme/theme_landscape.png')}
resizeMode="cover"
style={styles.previewImage}
/>
@@ -35,7 +35,24 @@ export default function ThemeModal({ visible, mode, onSelect, onClose }: Props)
selected={mode === 'color'}
onPress={() => onSelect('color')}
>
<View style={styles.colorPreview} />
<Image
source={require('../../assets/images/theme/theme_color.png')}
resizeMode="cover"
style={styles.previewImage}
/>
</ThemeCard>
<ThemeCard
title={t('theme.suixin')}
selected={mode === 'suixin'}
onPress={() => onSelect('suixin')}
>
<Image
// 占位:一期复用纯色预览图,后续可替换为专用资源
source={require('../../assets/images/theme/theme_color.png')}
resizeMode="cover"
style={styles.previewImage}
/>
</ThemeCard>
</View>
</SheetModal>
@@ -56,11 +73,25 @@ function ThemeCard({
return (
<Pressable
onPress={onPress}
style={[styles.card, selected ? styles.cardSelected : styles.cardUnselected]}
style={styles.cardContainer}
hitSlop={6}
>
<View style={styles.preview}>{children}</View>
<Text style={styles.cardTitle}>{title}</Text>
<View style={[styles.previewWrapper, selected && styles.selectedWrapper]}>
<View style={styles.previewInner}>
{children}
{/* 文案展示在图片中心 */}
<View style={styles.textOverlay}>
<Text
style={[styles.overlayTitle, selected && styles.selectedOverlayTitle]}
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.85}
>
{title}
</Text>
</View>
</View>
</View>
</Pressable>
);
}
@@ -68,43 +99,59 @@ function ThemeCard({
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
gap: 14,
paddingBottom: 18,
flexWrap: 'nowrap',
gap: 12,
paddingHorizontal: 4,
paddingBottom: 50,
paddingTop: 20,
justifyContent: 'space-between',
},
card: {
cardContainer: {
flex: 1,
borderRadius: 18,
padding: 10,
backgroundColor: 'rgba(255,255,255,0.55)',
minWidth: 0,
alignItems: 'stretch',
},
cardSelected: {
borderWidth: 2,
borderColor: '#F99CC0',
previewWrapper: {
width: '100%',
aspectRatio: 110 / 178,
borderRadius: 26,
padding: 6.5,
justifyContent: 'center',
alignItems: 'center',
},
cardUnselected: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(94,42,40,0.18)',
selectedWrapper: {
borderWidth: 4,
borderColor: '#E7837A',
borderRadius: 26,
},
preview: {
height: 96,
borderRadius: 14,
previewInner: {
width: '100%',
height: '100%',
borderRadius: 21,
overflow: 'hidden',
backgroundColor: '#fff',
marginBottom: 10,
backgroundColor: '#F5F5F5',
position: 'relative',
},
previewImage: {
width: '100%',
height: '100%',
},
colorPreview: {
flex: 1,
backgroundColor: '#F3D0E1',
textOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.05)', // 轻微遮罩增加文字可读性
},
cardTitle: {
color: '#5E2A28',
fontSize: 14,
overlayTitle: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '700',
textAlign: 'center',
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
},
selectedOverlayTitle: {
color: '#FFFFFF',
},
});

View File

@@ -13,7 +13,7 @@ export default function WidgetModal({ visible, onClose }: Props) {
const { t } = useTranslation();
return (
<SheetModal visible={visible} title={t('profile.widget')} onClose={onClose}>
<SheetModal visible={visible} title={t('widget.howToTitle')} onClose={onClose}>
<View style={styles.content}>
<View style={styles.row}>
<PreviewCard label={t('widget.lockScreen')}>

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { SerifText } from './SerifText';
import { OnboardingColors } from '@/constants/OnboardingTheme';
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
import { useTranslation } from 'react-i18next';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
export const INTENTS = [
{ id: 'love', label: '爱情', icon: '❤️' },
{ id: 'life', label: '生活', icon: '⛅' },
{ id: 'travel', label: '旅游', icon: '🌴' },
{ id: 'work', label: '职场', icon: '💼' },
{ id: 'love', labelKey: 'intent.love', icon: '❤️' },
{ id: 'life', labelKey: 'intent.life', icon: '⛅' },
{ id: 'travel', labelKey: 'intent.travel', icon: '🌴' },
{ id: 'work', labelKey: 'intent.work', icon: '💼' },
];
interface IntentSelectionStepProps {
@@ -16,9 +16,10 @@ interface IntentSelectionStepProps {
}
export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionStepProps) {
const { t } = useTranslation();
return (
<View style={styles.container}>
<SerifText style={styles.title}></SerifText>
<Text style={styles.title}>{t('intent.title')}</Text>
<View style={styles.grid}>
{INTENTS.map((intent) => {
@@ -33,10 +34,10 @@ export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionSt
onPress={() => onToggle(intent.id)}
activeOpacity={0.7}
>
<SerifText style={styles.icon}>{intent.icon}</SerifText>
<SerifText style={[styles.label, isSelected && styles.labelSelected]}>
{intent.label}
</SerifText>
<Text style={styles.icon}>{intent.icon}</Text>
<Text style={[styles.label, isSelected && styles.labelSelected]}>
{t(intent.labelKey)}
</Text>
</TouchableOpacity>
);
})}
@@ -54,6 +55,8 @@ const styles = StyleSheet.create({
fontSize: 24,
marginBottom: 40,
textAlign: 'center',
fontFamily: OnboardingFont.question,
color: OnboardingColors.textPrimary,
},
grid: {
flexDirection: 'row',
@@ -84,10 +87,12 @@ const styles = StyleSheet.create({
},
icon: {
fontSize: 32,
fontFamily: OnboardingFont.question,
},
label: {
fontSize: 18,
color: OnboardingColors.textPrimary,
fontFamily: OnboardingFont.question,
},
labelSelected: {
fontWeight: 'bold',

View File

@@ -1,64 +1,170 @@
import React from 'react';
import { View, StyleSheet, TextInput, Platform } from 'react-native';
import { SerifText } from './SerifText';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View, StyleSheet, TextInput, Platform, Animated, TouchableOpacity, Text, Keyboard, Pressable } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { OnboardingColors } from '@/constants/OnboardingTheme';
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
import EnterLightIcon from '@/assets/images/icon/enter_Light_icon.svg';
interface NameInputStepProps {
value: string;
onChangeText: (text: string) => void;
onSubmitEditing?: () => void;
onNext: () => void;
}
export function NameInputStep({ value, onChangeText, onSubmitEditing }: NameInputStepProps) {
return (
<View style={styles.container}>
<SerifText style={styles.title}></SerifText>
export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [isFocused, setIsFocused] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
const blinkAnim = useRef(new Animated.Value(1)).current;
const hasInput = value.trim().length > 0;
<View style={styles.inputContainer}>
useEffect(() => {
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const subShow = Keyboard.addListener(showEvent, (e) => {
setKeyboardHeight(e.endCoordinates?.height ?? 0);
});
const subHide = Keyboard.addListener(hideEvent, () => {
setKeyboardHeight(0);
});
return () => {
subShow.remove();
subHide.remove();
};
}, []);
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(blinkAnim, { toValue: 0, duration: 500, useNativeDriver: true }),
Animated.timing(blinkAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
])
);
if (isFocused) {
animation.start();
} else {
animation.stop();
blinkAnim.setValue(0);
}
return () => animation.stop();
}, [blinkAnim, isFocused]);
const footerBottom = useMemo(() => {
// iOS 的 keyboard height 通常已包含底部安全区,避免重复叠加
const keyboardOffset = Math.max(0, keyboardHeight - insets.bottom);
return 16 + insets.bottom + keyboardOffset;
}, [insets.bottom, keyboardHeight]);
return (
<Pressable style={styles.container} onPress={Keyboard.dismiss} accessible={false}>
<View style={styles.inputCard}>
<View style={styles.inputWrapper}>
{/* 显示层:文案 + 跟随的光标 */}
<View style={styles.displayLayer}>
<Text
style={[
styles.displayText,
(!isFocused && !hasInput) && { color: OnboardingColors.textSecondary }
]}
>
{hasInput ? value : isFocused ? '' : t('onboardingSurvey.steps.name.placeholder')}
</Text>
{isFocused && (
<Animated.View style={[styles.cursorWrapper, { opacity: blinkAnim, marginLeft: 2 }]}>
<EnterLightIcon width={3} height={27} />
</Animated.View>
)}
</View>
{/* 交互层:隐藏的输入框 */}
<TextInput
style={styles.input}
style={styles.hiddenInput}
value={value}
onChangeText={onChangeText}
placeholder="Hao"
placeholderTextColor={OnboardingColors.textSecondary}
selectionColor={OnboardingColors.cursor}
autoFocus
onSubmitEditing={onSubmitEditing}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
caretHidden={true}
autoCorrect={false}
spellCheck={false}
returnKeyType="done"
textAlign="center"
blurOnSubmit={true}
onSubmitEditing={() => {
Keyboard.dismiss();
// 不再自動跳頁,僅收起鍵盤;前進需點擊底部 ➡️
}}
/>
</View>
</View>
<View style={[styles.footer, { bottom: footerBottom }]}>
<TouchableOpacity
onPress={() => {
Keyboard.dismiss();
onNext();
}}
disabled={!hasInput}
activeOpacity={0.8}
>
{hasInput ? <BtnClicked width={87} height={57} /> : <BtnNotClicked width={87} height={57} />}
</TouchableOpacity>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
width: '100%',
paddingTop: 20,
},
title: {
fontSize: 24,
marginBottom: 40,
textAlign: 'center',
},
inputContainer: {
width: '100%',
inputCard: {
width: 335,
height: 75,
backgroundColor: OnboardingColors.cardBackground,
borderRadius: 20,
paddingVertical: 20,
paddingHorizontal: 24,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 10,
elevation: 2,
},
input: {
fontSize: 24,
fontFamily: Platform.select({ ios: 'Georgia', android: 'serif', default: 'serif' }),
inputWrapper: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
displayLayer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
displayText: {
fontSize: 22,
fontFamily: Platform.select({ ios: 'STIX Two Text', android: 'serif', default: 'serif' }),
fontWeight: '600',
color: OnboardingColors.textPrimary,
textAlign: 'center',
padding: 0, // remove default padding
},
hiddenInput: {
...StyleSheet.absoluteFillObject,
color: 'transparent', // 文字透明,只负责输入逻辑
fontSize: 22,
textAlign: 'center',
},
cursorWrapper: {
// 默认居中显示时,光标在左侧
},
footer: {
position: 'absolute',
alignItems: 'center',
}
});

View File

@@ -1,46 +1,121 @@
import React from 'react';
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar } from 'react-native';
import { OnboardingColors } from '@/constants/OnboardingTheme';
import { SerifText } from './SerifText';
import { NextButton } from './NextButton';
import React, { useRef, useEffect } from 'react';
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native';
import { useTranslation } from 'react-i18next';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
const TRANSITION_OFFSET = 24;
const TRANSITION_DURATION = 280;
interface OnboardingLayoutProps {
children: React.ReactNode;
onSkip?: () => void;
onNext?: () => void;
nextEnabled?: boolean;
showNextButton?: boolean;
title?: string;
currentStep: number;
totalSteps: number;
onSkip: () => void;
onBack?: () => void;
showBackButton?: boolean;
/** 用户名字仅在名字步骤之后的第一个问题currentStep === 1且非空时显示招呼语 */
userName?: string;
}
export function OnboardingLayout({
children,
title,
currentStep,
totalSteps,
onSkip,
onNext,
nextEnabled = true,
showNextButton = true
onBack,
showBackButton = false,
userName = '',
}: OnboardingLayoutProps) {
const { t } = useTranslation();
const showGreeting = currentStep === 1 && userName.trim().length > 0;
const displayName = userName.trim();
const prevStepRef = useRef(currentStep);
const isFirstRenderRef = useRef(true);
const translateX = useRef(new Animated.Value(0)).current;
const opacity = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
prevStepRef.current = currentStep;
return;
}
if (prevStepRef.current === currentStep) return;
const direction = currentStep > prevStepRef.current ? 'forward' : 'back';
prevStepRef.current = currentStep;
const startX = direction === 'forward' ? TRANSITION_OFFSET : -TRANSITION_OFFSET;
translateX.setValue(startX);
opacity.setValue(0.72);
Animated.parallel([
Animated.timing(translateX, {
toValue: 0,
duration: TRANSITION_DURATION,
useNativeDriver: true,
easing: Easing.out(Easing.cubic),
}),
Animated.timing(opacity, {
toValue: 1,
duration: TRANSITION_DURATION,
useNativeDriver: true,
easing: Easing.out(Easing.cubic),
}),
]).start();
}, [currentStep, translateX, opacity]);
return (
<View style={styles.container}>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.safeArea}>
{/* Header: Back & Skip */}
<View style={styles.header}>
<View style={styles.spacer} />
{onSkip && (
<TouchableOpacity onPress={onSkip} style={styles.skipButton}>
<SerifText style={styles.skipText}>Skip {'->'}</SerifText>
<View style={styles.headerLeft}>
{showBackButton && onBack && (
<TouchableOpacity onPress={onBack} style={styles.iconButton}>
<Image
source={require('@/assets/images/icon/back_icon.png')}
style={styles.backIcon}
/>
</TouchableOpacity>
)}
</View>
<View style={styles.content}>
{children}
<TouchableOpacity onPress={onSkip} style={styles.skipButton}>
<Text style={styles.skipText}>{t('onboarding.skipAll')}</Text>
<Image
source={require('@/assets/images/icon/skip_icon.png')}
style={styles.skipIcon}
/>
</TouchableOpacity>
</View>
<View style={styles.footer}>
{showNextButton && onNext && (
<NextButton onPress={onNext} disabled={!nextEnabled} />
{/* Title & Progress Row名字步骤后第一步且名字非空时显示招呼语 + 问题) */}
<View style={styles.titleRow}>
<View style={styles.titleBlock}>
{showGreeting && (
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
)}
<Text style={styles.questionTitle}>{title}</Text>
</View>
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
</View>
{/* Contentstep 切换时滑动 + 淡入 */}
<Animated.View
style={[
styles.content,
{
opacity,
transform: [{ translateX }],
},
]}
>
{children}
</Animated.View>
</SafeAreaView>
</View>
);
@@ -58,27 +133,69 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 24,
paddingVertical: 12,
paddingHorizontal: 20,
height: 44,
},
spacer: {
width: 60, // Balance the skip button width approximately
headerLeft: {
width: 44,
height: 44,
justifyContent: 'center',
},
iconButton: {
padding: 8,
},
backIcon: {
width: 19,
height: 19,
resizeMode: 'contain',
},
skipButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
skipText: {
fontSize: 16,
color: OnboardingColors.textPrimary,
fontSize: 13,
color: OnboardingColors.textMuted,
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
marginRight: 4,
},
skipIcon: {
width: 10,
height: 4,
resizeMode: 'contain',
},
titleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
paddingHorizontal: 20,
marginTop: 20,
marginBottom: 8,
},
titleBlock: {
flex: 1,
justifyContent: 'flex-end',
},
greetingText: {
fontSize: 22,
color: OnboardingColors.questionTitle,
fontFamily: OnboardingFont.question,
marginBottom: 4,
},
questionTitle: {
fontSize: 22,
color: OnboardingColors.questionTitle,
fontFamily: OnboardingFont.question,
},
progressText: {
fontSize: 18,
color: OnboardingColors.textProgress,
fontFamily: OnboardingFont.question,
marginLeft: 10,
},
content: {
flex: 1,
justifyContent: 'center',
paddingHorizontal: 24,
},
footer: {
alignItems: 'center',
paddingBottom: 40,
minHeight: 100, // Reserve space for button
paddingHorizontal: 20,
},
});

View File

@@ -0,0 +1,127 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Text, Platform, ActivityIndicator } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { LinearGradient } from 'expo-linear-gradient';
import { OnboardingColors } from '@/constants/OnboardingTheme';
import AddIcon from '@/assets/images/icon/add_icon.svg';
import ReduceIcon from '@/assets/images/icon/reduce_icon.svg';
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
interface ReminderStepProps {
value: number;
onChange: (value: number) => void;
onFinish: () => void;
onSkip?: () => void;
/** 完成后请求通知权限时的加载态 */
loading?: boolean;
}
export function ReminderStep({ value, onChange, onFinish, loading = false }: ReminderStepProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const handleReduce = () => {
// 本页最小为 1不接收提醒请使用右上角 Skip
if (value > 1) onChange(value - 1);
};
const handleAdd = () => {
if (value < 5) onChange(value + 1);
};
return (
<View style={styles.container}>
<View style={styles.counterContainer}>
<TouchableOpacity onPress={handleReduce} disabled={loading} activeOpacity={0.7}>
<ReduceIcon width={47} height={47} />
</TouchableOpacity>
<View style={styles.numberWrapper}>
<Text style={styles.numberText}>{value}</Text>
<Text style={styles.unitText}>
{value === 1 ? t('dailyReminder.timesUnitSingular') : t('dailyReminder.timesUnit')}
</Text>
</View>
<TouchableOpacity onPress={handleAdd} disabled={loading} activeOpacity={0.7}>
<AddIcon width={47} height={47} />
</TouchableOpacity>
</View>
<View style={[styles.footer, { bottom: insets.bottom + 16 }]}>
<TouchableOpacity onPress={onFinish} disabled={loading} activeOpacity={0.8}>
<View style={styles.finishWrap}>
{loading ? (
<LinearGradient
colors={['#F69F7B', '#F99CC0']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.loadingPill, styles.finishDisabled]}
>
<ActivityIndicator size="small" color="#FFFFFF" />
</LinearGradient>
) : (
<BtnClicked width={87} height={57} />
)}
</View>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
paddingTop: 20,
},
counterContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
marginTop: 40,
},
numberWrapper: {
flexDirection: 'row',
alignItems: 'flex-end',
marginHorizontal: 40,
},
numberText: {
fontSize: 107,
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
fontWeight: '600',
color: OnboardingColors.textPrimary,
lineHeight: 120,
},
unitText: {
fontSize: 17,
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
fontWeight: '600',
color: OnboardingColors.textPrimary,
marginBottom: 20,
marginLeft: 4,
},
footer: {
position: 'absolute',
alignItems: 'center',
},
finishWrap: {
width: 87,
height: 57,
alignItems: 'center',
justifyContent: 'center',
},
finishDisabled: {
opacity: 0.7,
},
loadingPill: {
width: 87,
height: 57,
borderRadius: 28.5,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, ScrollView, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
interface Option {
id: string;
label: string;
}
interface SelectionStepProps {
options: Option[];
selectedIds: string[];
onToggle: (id: string) => void;
onNext: () => void;
onSkip?: () => void;
}
export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }: SelectionStepProps) {
const hasSelection = selectedIds.length > 0;
const insets = useSafeAreaInsets();
const footerBottom = insets.bottom + 16;
const footerButtonHeight = 57;
// 底部留白加大,避免最后一项与按钮边框视觉重叠
const footerPaddingBottom = footerBottom + footerButtonHeight + 40;
return (
<View style={styles.container}>
<ScrollView
style={styles.scroll}
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.optionsList, { paddingBottom: footerPaddingBottom }]}
>
{options.map((option) => {
const isSelected = selectedIds.includes(option.id);
return (
<TouchableOpacity
key={option.id}
style={[styles.optionCard, isSelected && styles.optionCardSelected]}
onPress={() => onToggle(option.id)}
activeOpacity={0.7}
>
<Text style={styles.optionText}>{option.label}</Text>
</TouchableOpacity>
);
})}
</ScrollView>
{/* 底部按钮:距离底部 12% 高度 */}
<View style={[styles.footer, { bottom: footerBottom }]}>
<TouchableOpacity onPress={onNext} disabled={!hasSelection} activeOpacity={0.8}>
{hasSelection ? <BtnClicked width={87} height={57} /> : <BtnNotClicked width={87} height={57} />}
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 8,
},
scroll: {
flex: 1,
},
optionsList: {
// paddingBottom 由安全区 + 按钮高度动态计算,避免选项被遮住
},
optionCard: {
width: '100%',
height: 75,
backgroundColor: OnboardingColors.cardBackground,
borderRadius: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 10,
elevation: 2,
},
optionCardSelected: {
backgroundColor: OnboardingColors.cardSelected,
},
optionText: {
fontSize: 18,
color: OnboardingColors.textPrimary,
fontWeight: '500',
fontFamily: OnboardingFont.question,
},
footer: {
position: 'absolute',
left: 0,
right: 0,
alignItems: 'center',
},
footerRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
},
skipBtn: {
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 12,
backgroundColor: 'rgba(0,0,0,0.04)',
},
skipText: {
fontSize: 16,
color: OnboardingColors.textMuted,
},
});

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { Modal, Pressable, StyleSheet, Text, View, PanResponder, Animated as RNAnimated, Dimensions, Image, ImageSourcePropType } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
Easing,
@@ -9,26 +10,36 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
const FIXED_TOP_GAP = 100; // 统一距离顶部的高度
type Props = {
visible: boolean;
title?: string;
onClose: () => void;
children: React.ReactNode;
leftIcon?: ImageSourcePropType; // 新增:支持自定义左侧图标
height?: number; // 新增:支持自定义高度
};
/**
* 通用底部上拉弹窗Sheet
* - 内容区背景色固定:#FAF3EC
* - 关闭方式:点 X本期不要求点遮罩关闭
* - 高度固定:默认距离顶部固定间距,也支持传入指定高度
*/
export default function SheetModal({ visible, title, onClose, children }: Props) {
export default function SheetModal({ visible, title, onClose, children, leftIcon, height: customHeight }: Props) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [mounted, setMounted] = useState(false);
const progress = useSharedValue(0); // 0: 关闭, 1: 打开
const dragY = useSharedValue(0); // 拖拽位移
const sheetHeight = customHeight || (SCREEN_HEIGHT - FIXED_TOP_GAP);
useEffect(() => {
if (visible) {
setMounted(true);
dragY.value = 0;
progress.value = withTiming(1, { duration: 260, easing: Easing.out(Easing.cubic) });
return;
}
@@ -40,46 +51,88 @@ export default function SheetModal({ visible, title, onClose, children }: Props)
if (finished) runOnJS(setMounted)(false);
}
);
}, [visible, mounted, progress]);
}, [visible, mounted, progress, dragY]);
// 使用 PanResponder 处理下滑手势
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder: () => false, // 暂时屏蔽下拉关闭手势,解决滑动冲突
onPanResponderMove: (_, gestureState) => {
if (gestureState.dy > 0) {
dragY.value = gestureState.dy;
}
},
onPanResponderRelease: (_, gestureState) => {
if (gestureState.dy > 80 || gestureState.vy > 0.5) {
runOnJS(onClose)();
} else {
dragY.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.back(1))
});
}
},
onPanResponderTerminate: () => {
dragY.value = withTiming(0, { duration: 200 });
},
})
).current;
const overlayStyle = useAnimatedStyle(() => {
return { opacity: 0.4 * progress.value };
});
const sheetStyle = useAnimatedStyle(() => {
const translateY = (1 - progress.value) * 380;
return { transform: [{ translateY }] };
const baseTranslateY = (1 - progress.value) * sheetHeight; // 基础位移
return {
transform: [{ translateY: baseTranslateY + dragY.value }]
};
});
const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 12), [insets.bottom]);
const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 80), [insets.bottom]); // 增加底部间距至 80约占 350 高度的 22%,确保内容不被截断并留出足够呼吸感
// 注意Modal 的 visible 必须为 true 才会渲染,因此用 mounted 保持退场动画
return (
<Modal transparent visible={mounted} animationType="none" onRequestClose={onClose}>
<View style={styles.root}>
<Pressable
style={StyleSheet.absoluteFill}
onPress={onClose}
>
<Animated.View style={[styles.overlay, overlayStyle]} />
</Pressable>
<Animated.View
{...panResponder.panHandlers}
style={[
styles.sheet,
sheetStyle,
{
height: sheetHeight,
paddingBottom: containerPaddingBottom,
},
]}
>
<View style={styles.handleContainer}>
<View style={styles.handle} />
</View>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={1}>
{title ?? ''}
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="关闭"
accessibilityLabel={leftIcon ? t('common.back') : t('common.close')}
onPress={onClose}
hitSlop={10}
style={styles.close}
>
{leftIcon ? (
<Image source={leftIcon} style={styles.backIcon} />
) : (
<Text style={styles.closeText}>×</Text>
)}
</Pressable>
</View>
@@ -103,9 +156,19 @@ const styles = StyleSheet.create({
backgroundColor: '#FAF3EC',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingTop: 14,
paddingTop: 8,
paddingHorizontal: 16,
},
handleContainer: {
alignItems: 'center',
paddingVertical: 8,
},
handle: {
width: 40,
height: 5,
borderRadius: 2.5,
backgroundColor: 'rgba(94,42,40,0.15)',
},
header: {
height: 44,
justifyContent: 'center',
@@ -131,8 +194,13 @@ const styles = StyleSheet.create({
lineHeight: 28,
fontWeight: '400',
},
backIcon: {
width: 20,
height: 20,
resizeMode: 'contain',
},
body: {
paddingTop: 10,
flex: 1,
},
});

View File

@@ -1,9 +1,21 @@
import { Platform } from 'react-native';
/** 与 onboarding 问题标题一致的字体PingFang TC / sans-serif */
export const OnboardingFont = {
question: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif',
};
export const OnboardingColors = {
background: '#FFF4EA',
textPrimary: '#4A3B32',
textSecondary: '#8C8C8C',
textPrimary: '#772F00',
textSecondary: '#DED2CA', // 默认 Mama 字体颜色
textMuted: '#A27854', // Skip 按钮颜色
textProgress: '#D4B08E', // (0/5) 颜色
questionTitle: '#B8504D', // 问题标题颜色
buttonNotClicked: '#F2DDCA',
buttonStart: '#F69F7B',
buttonEnd: '#F99CC0',
cardBackground: '#FFFFFF',
cursor: '#F99CC0',
cardSelected: '#FFD8E2', // 选中后的背景色
cursor: '#F89DB4',
};

21
client/eas.json Normal file
View File

@@ -0,0 +1,21 @@
{
"cli": {
"version": ">= 16.32.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

8
client/global.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/**
* React Native 全局常量声明。
*
* 说明:`__DEV__` 在运行时由 RN 注入,用于区分开发/生产环境。
* 这里补充 TypeScript 声明,避免在代码里使用时出现类型报错。
*/
declare const __DEV__: boolean;

View File

@@ -1,128 +0,0 @@
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
completion(SimpleEntry(date: Date()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
// V1
let entry = SimpleEntry(date: Date())
let nextUpdate = Calendar.current.date(byAdding: .day, value: 7, to: Date()) ?? Date().addingTimeInterval(60 * 60 * 24 * 7)
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct MindfulnessWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
private let title = "正念"
private let text = "你已经很努力了,今天也值得被温柔对待。"
private let deepLink = URL(string: "client:///(app)/home")
var body: some View {
switch family {
case .systemSmall:
smallView()
case .systemMedium:
mediumView()
case .systemLarge:
largeView()
default:
smallView()
}
}
private func smallView() -> some View {
ZStack {
LinearGradient(
colors: [Color(red: 0.07, green: 0.09, blue: 0.13), Color(red: 0.15, green: 0.18, blue: 0.26)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline).foregroundStyle(.white)
Text(text)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Color.white.opacity(0.92))
.lineLimit(4)
Spacer(minLength: 0)
}
.padding(14)
}
.widgetURL(deepLink)
}
private func mediumView() -> some View {
ZStack {
LinearGradient(
colors: [Color(red: 0.07, green: 0.09, blue: 0.13), Color(red: 0.10, green: 0.12, blue: 0.18)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
HStack(spacing: 14) {
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline).foregroundStyle(.white)
Text(text)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Color.white.opacity(0.92))
.lineLimit(5)
Spacer(minLength: 0)
}
Spacer(minLength: 0)
}
.padding(16)
}
.widgetURL(deepLink)
}
private func largeView() -> some View {
ZStack {
LinearGradient(
colors: [Color(red: 0.07, green: 0.09, blue: 0.13), Color(red: 0.17, green: 0.22, blue: 0.32)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.title3)
.foregroundStyle(.white)
.bold()
Text(text)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(Color.white.opacity(0.92))
.lineLimit(7)
Spacer(minLength: 0)
Text("轻轻呼吸,回到当下")
.font(.footnote)
.foregroundStyle(Color.white.opacity(0.7))
}
.padding(18)
}
.widgetURL(deepLink)
}
}
struct MindfulnessWidget: Widget {
let kind: String = "MindfulnessWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MindfulnessWidgetEntryView(entry: entry)
}
.configurationDisplayName("正念")
.description("一段温柔提醒,陪你回到当下。")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}

View File

@@ -1,12 +0,0 @@
import WidgetKit
import SwiftUI
// Widget Extension @main
@main
struct MindfulnessWidgetBundle: WidgetBundle {
var body: some Widget {
MindfulnessWidget()
EmotionWidget()
}
}

View File

@@ -1,12 +0,0 @@
# MindfulnessWidgetWidgetKit 扩展骨架)
本目录提供 iOS WidgetV1 写死文案)的 SwiftUI 代码骨架。
注意:**仅把文件放进仓库还不够**,你还需要在 Xcode 中创建 Widget Extension target并把这些文件加入 target。
## 目标
- 支持 Small/Medium/Large 三种尺寸
- 展示写死文案
- 点击小组件跳转到 App 的 Home`client:///(app)/home`

View File

@@ -59,5 +59,92 @@ target 'client' do
:mac_catalyst_enabled => false,
: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
# 修复Xcode 编译阶段找不到 Expo 相关 modulemap
# 现象PrecompileSwiftBridgingHeader 报错 module map file '.../Build/Products/.../Expo/Expo.modulemap' not found
# 原因Pods-client 的 xcconfig 把 -fmodule-map-file 指向了 ${PODS_CONFIGURATION_BUILD_DIR},但该文件在构建早期并不存在。
# 方案:将这些 modulemap 路径改为 Pods 内稳定存在的 Target Support Files 路径。
def patch_pods_client_xcconfig!(path)
return unless File.exist?(path)
s = File.read(path)
# expo-dev-* 的 modulemap 文件名与 module 名不同,需要单独映射
s = s.gsub('${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.modulemap',
'${PODS_ROOT}/Target Support Files/expo-dev-launcher/expo-dev-launcher.modulemap')
s = s.gsub('${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.modulemap',
'${PODS_ROOT}/Target Support Files/expo-dev-menu/expo-dev-menu.modulemap')
s = s.gsub('${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu-interface/EXDevMenuInterface.modulemap',
'${PODS_ROOT}/Target Support Files/expo-dev-menu-interface/expo-dev-menu-interface.modulemap')
# 通用映射:${PODS_CONFIGURATION_BUILD_DIR}/<Pod>/<Pod>.modulemap -> ${PODS_ROOT}/Target Support Files/<Pod>/<Pod>.modulemap
s = s.gsub(/\$\{PODS_CONFIGURATION_BUILD_DIR\}\/([^\/]+)\/\1\.modulemap/,
'${PODS_ROOT}/Target Support Files/\1/\1.modulemap')
File.write(path, s)
end
support_dir = File.join(__dir__, 'Pods', 'Target Support Files', 'Pods-client')
patch_pods_client_xcconfig!(File.join(support_dir, 'Pods-client.debug.xcconfig'))
patch_pods_client_xcconfig!(File.join(support_dir, 'Pods-client.release.xcconfig'))
# 修复:缺失 [CP] Copy XCFrameworks 阶段时React/Expo 的 XCFramework 中间产物不会生成,
# 导致 Swift 报 no such module 'React' 等。
# 方案:在 [CP] Embed Pods Frameworks 脚本中,先执行各个 *-xcframeworks.sh 生成中间产物。
def patch_pods_client_frameworks_sh!(path)
return unless File.exist?(path)
s = File.read(path)
marker = "# [Mindfulness Fix] Prepare XCFramework intermediates\n"
return if s.include?(marker)
insert = marker +
"if [ -r \"${PODS_ROOT}/Target Support Files/React-Core-prebuilt/React-Core-prebuilt-xcframeworks.sh\" ]; then\n" \
" /bin/sh \"${PODS_ROOT}/Target Support Files/React-Core-prebuilt/React-Core-prebuilt-xcframeworks.sh\"\n" \
"fi\n" \
"if [ -r \"${PODS_ROOT}/Target Support Files/ReactNativeDependencies/ReactNativeDependencies-xcframeworks.sh\" ]; then\n" \
" /bin/sh \"${PODS_ROOT}/Target Support Files/ReactNativeDependencies/ReactNativeDependencies-xcframeworks.sh\"\n" \
"fi\n" \
"if [ -r \"${PODS_ROOT}/Target Support Files/hermes-engine/hermes-engine-xcframeworks.sh\" ]; then\n" \
" /bin/sh \"${PODS_ROOT}/Target Support Files/hermes-engine/hermes-engine-xcframeworks.sh\"\n" \
"fi\n\n"
s = s.sub(/^if \[\[ \"\$CONFIGURATION\" == \"Debug\" \]\]; then\n/, insert + "if [[ \"$CONFIGURATION\" == \"Debug\" ]]; then\n")
File.write(path, s)
end
patch_pods_client_frameworks_sh!(File.join(support_dir, 'Pods-client-frameworks.sh'))
# 让 React/ReactNativeDependencies/hermes 的 XCFramework 切片在编译 Swift 之前就准备好,
# 否则会在 AppDelegate.swift 的 `import React` 阶段报 no such module。
def patch_expo_configure_project_sh!(path)
return unless File.exist?(path)
s = File.read(path)
marker = "# [Mindfulness Fix] Prepare XCFramework intermediates (before Swift compile)\n"
return if s.include?(marker)
insert = marker +
"if [ -r \"${PODS_ROOT}/Target Support Files/React-Core-prebuilt/React-Core-prebuilt-xcframeworks.sh\" ]; then\n" \
" /bin/sh \"${PODS_ROOT}/Target Support Files/React-Core-prebuilt/React-Core-prebuilt-xcframeworks.sh\"\n" \
"fi\n" \
"if [ -r \"${PODS_ROOT}/Target Support Files/ReactNativeDependencies/ReactNativeDependencies-xcframeworks.sh\" ]; then\n" \
" /bin/sh \"${PODS_ROOT}/Target Support Files/ReactNativeDependencies/ReactNativeDependencies-xcframeworks.sh\"\n" \
"fi\n" \
"if [ -r \"${PODS_ROOT}/Target Support Files/hermes-engine/hermes-engine-xcframeworks.sh\" ]; then\n" \
" /bin/sh \"${PODS_ROOT}/Target Support Files/hermes-engine/hermes-engine-xcframeworks.sh\"\n" \
"fi\n\n"
# 插在首次调用 with_node 之前即可(不能用 ^,因为 with_node 不在文件开头)
s = s.sub("with_node \\\n", insert + "with_node \\\n")
File.write(path, s)
end
patch_expo_configure_project_sh!(File.join(support_dir, 'expo-configure-project.sh'))
end
end

View File

@@ -3,6 +3,9 @@ PODS:
- ExpoModulesCore
- EXConstants (18.0.13):
- ExpoModulesCore
- EXJSONUtils (0.15.0)
- EXManifests (1.0.10):
- ExpoModulesCore
- EXNotifications (0.32.16):
- ExpoModulesCore
- Expo (54.0.32):
@@ -30,8 +33,183 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- expo-dev-client (6.0.20):
- EXManifests
- expo-dev-launcher
- expo-dev-menu
- expo-dev-menu-interface
- EXUpdatesInterface
- expo-dev-launcher (6.0.20):
- EXManifests
- expo-dev-launcher/Main (= 6.0.20)
- expo-dev-menu
- expo-dev-menu-interface
- ExpoModulesCore
- EXUpdatesInterface
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-jsinspector
- React-NativeModulesApple
- React-RCTAppDelegate
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactAppDependencyProvider
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- expo-dev-launcher/Main (6.0.20):
- EXManifests
- expo-dev-launcher/Unsafe
- expo-dev-menu
- expo-dev-menu-interface
- ExpoModulesCore
- EXUpdatesInterface
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-jsinspector
- React-NativeModulesApple
- React-RCTAppDelegate
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactAppDependencyProvider
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- expo-dev-launcher/Unsafe (6.0.20):
- EXManifests
- expo-dev-menu
- expo-dev-menu-interface
- ExpoModulesCore
- EXUpdatesInterface
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-jsinspector
- React-NativeModulesApple
- React-RCTAppDelegate
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactAppDependencyProvider
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- expo-dev-menu (7.0.18):
- expo-dev-menu/Main (= 7.0.18)
- expo-dev-menu/ReactNativeCompatibles (= 7.0.18)
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- expo-dev-menu-interface (2.0.0)
- expo-dev-menu/Main (7.0.18):
- EXManifests
- expo-dev-menu-interface
- ExpoModulesCore
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-jsinspector
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- expo-dev-menu/ReactNativeCompatibles (7.0.18):
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- ExpoAsset (12.0.12):
- ExpoModulesCore
- ExpoCrypto (15.0.8):
- ExpoModulesCore
- ExpoDevice (8.0.10):
- ExpoModulesCore
- ExpoFileSystem (19.0.21):
- ExpoModulesCore
- ExpoFont (14.0.11):
@@ -74,6 +252,8 @@ PODS:
- ExpoModulesCore
- ExpoWebBrowser (15.0.10):
- ExpoModulesCore
- EXUpdatesInterface (2.0.0):
- ExpoModulesCore
- FBLazyVector (0.81.5)
- hermes-engine (0.81.5):
- hermes-engine/Pre-built (= 0.81.5)
@@ -1798,6 +1978,28 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RNGestureHandler (2.30.0):
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RNReanimated (4.1.6):
- hermes-engine
- RCTRequired
@@ -2038,281 +2240,319 @@ PODS:
- Yoga (0.0.0)
DEPENDENCIES:
- "EXApplication (from `../node_modules/.pnpm/expo-application@7.0.8_expo@54.0.32/node_modules/expo-application/ios`)"
- "EXConstants (from `../node_modules/.pnpm/expo-constants@18.0.13_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-constants/ios`)"
- "EXNotifications (from `../node_modules/.pnpm/expo-notifications@0.32.16_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@1_nvlvke5tn7wk5pigfsu7j4ieeq/node_modules/expo-notifications/ios`)"
- "Expo (from `../node_modules/.pnpm/expo@54.0.32_@babel+core@7.28.6_@expo+metro-runtime@6.1.2_expo-router@6.0.22_react-native@0.8_7rhpxisdkrzvrgzbu7ct455kta/node_modules/expo`)"
- "ExpoAsset (from `../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios`)"
- "ExpoFileSystem (from `../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios`)"
- "ExpoFont (from `../node_modules/.pnpm/expo-font@14.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-font/ios`)"
- "ExpoHead (from `../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.13_expo_rjurfbyy5kjn57nkkfxix5iqea/node_modules/expo-router/ios`)"
- "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@15.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-keep-awake/ios`)"
- "ExpoLinearGradient (from `../node_modules/.pnpm/expo-linear-gradient@15.0.8_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@_e6k2hjkd5k4lph2ersbp3gfshy/node_modules/expo-linear-gradient/ios`)"
- "ExpoLinking (from `../node_modules/.pnpm/expo-linking@8.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-linking/ios`)"
- "ExpoLocalization (from `../node_modules/.pnpm/expo-localization@17.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-localization/ios`)"
- "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@3.0.29_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-modules-core`)"
- "ExpoSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@31.0.13_expo@54.0.32/node_modules/expo-splash-screen/ios`)"
- "ExpoWebBrowser (from `../node_modules/.pnpm/expo-web-browser@15.0.10_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-web-browser/ios`)"
- "FBLazyVector (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/FBLazyVector`)"
- "hermes-engine (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)"
- "RCTDeprecation (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)"
- "RCTRequired (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Required`)"
- "RCTTypeSafety (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/TypeSafety`)"
- "React (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/`)"
- "React-callinvoker (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/callinvoker`)"
- "React-Core (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/`)"
- "React-Core-prebuilt (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React-Core-prebuilt.podspec`)"
- "React-Core/RCTWebSocket (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/`)"
- "React-CoreModules (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React/CoreModules`)"
- "React-cxxreact (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/cxxreact`)"
- "React-debug (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/debug`)"
- "React-defaultsnativemodule (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/defaults`)"
- "React-domnativemodule (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/dom`)"
- "React-Fabric (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon`)"
- "React-FabricComponents (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon`)"
- "React-FabricImage (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon`)"
- "React-featureflags (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/featureflags`)"
- "React-featureflagsnativemodule (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)"
- "React-graphics (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/graphics`)"
- "React-hermes (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/hermes`)"
- "React-idlecallbacksnativemodule (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)"
- "React-ImageManager (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)"
- "React-jserrorhandler (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jserrorhandler`)"
- "React-jsi (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsi`)"
- "React-jsiexecutor (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsiexecutor`)"
- "React-jsinspector (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern`)"
- "React-jsinspectorcdp (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)"
- "React-jsinspectornetwork (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern/network`)"
- "React-jsinspectortracing (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)"
- "React-jsitooling (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsitooling`)"
- "React-jsitracing (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/hermes/executor/`)"
- "React-logger (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/logger`)"
- "React-Mapbuffer (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon`)"
- "React-microtasksnativemodule (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)"
- "react-native-safe-area-context (from `../node_modules/.pnpm/react-native-safe-area-context@5.6.2_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1_azuxgonsvxb2yngtegtuvyxcpi/node_modules/react-native-safe-area-context`)"
- "React-NativeModulesApple (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)"
- "React-oscompat (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/oscompat`)"
- "React-perflogger (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/reactperflogger`)"
- "React-performancetimeline (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/performance/timeline`)"
- "React-RCTActionSheet (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/ActionSheetIOS`)"
- "React-RCTAnimation (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/NativeAnimation`)"
- "React-RCTAppDelegate (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/AppDelegate`)"
- "React-RCTBlob (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Blob`)"
- "React-RCTFabric (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React`)"
- "React-RCTFBReactNativeSpec (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React`)"
- "React-RCTImage (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Image`)"
- "React-RCTLinking (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/LinkingIOS`)"
- "React-RCTNetwork (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Network`)"
- "React-RCTRuntime (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React/Runtime`)"
- "React-RCTSettings (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Settings`)"
- "React-RCTText (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Text`)"
- "React-RCTVibration (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Vibration`)"
- "React-rendererconsistency (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/consistency`)"
- "React-renderercss (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/css`)"
- "React-rendererdebug (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/debug`)"
- "React-RuntimeApple (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/runtime/platform/ios`)"
- "React-RuntimeCore (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/runtime`)"
- "React-runtimeexecutor (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/runtimeexecutor`)"
- "React-RuntimeHermes (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/runtime`)"
- "React-runtimescheduler (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)"
- "React-timing (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/timing`)"
- "React-utils (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/utils`)"
- EXApplication (from `../node_modules/expo-application/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`)
- EXJSONUtils (from `../node_modules/expo-json-utils/ios`)
- EXManifests (from `../node_modules/expo-manifests/ios`)
- EXNotifications (from `../node_modules/expo-notifications/ios`)
- Expo (from `../node_modules/expo`)
- expo-dev-client (from `../node_modules/expo-dev-client/ios`)
- expo-dev-launcher (from `../node_modules/expo-dev-launcher`)
- expo-dev-menu (from `../node_modules/expo-dev-menu`)
- expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`)
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
- ExpoDevice (from `../node_modules/expo-device/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
- ExpoHead (from `../node_modules/expo-router/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoLinking (from `../node_modules/expo-linking/ios`)
- ExpoLocalization (from `../node_modules/expo-localization/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`)
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
- EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
- React-Core (from `../node_modules/react-native/`)
- React-Core-prebuilt (from `../node_modules/react-native/React-Core-prebuilt.podspec`)
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
- React-debug (from `../node_modules/react-native/ReactCommon/react/debug`)
- React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
- React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
- React-Fabric (from `../node_modules/react-native/ReactCommon`)
- React-FabricComponents (from `../node_modules/react-native/ReactCommon`)
- React-FabricImage (from `../node_modules/react-native/ReactCommon`)
- React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`)
- React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
- React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`)
- React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
- React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
- React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
- React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`)
- React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
- React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
- React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
- React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`)
- React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
- React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`)
- React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
- React-RCTFabric (from `../node_modules/react-native/React`)
- React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`)
- React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
- React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
- React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`)
- React-RCTRuntime (from `../node_modules/react-native/React/Runtime`)
- React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
- React-RCTText (from `../node_modules/react-native/Libraries/Text`)
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`)
- React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`)
- React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`)
- React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
- React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`)
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
- React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`)
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-timing (from `../node_modules/react-native/ReactCommon/react/timing`)
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactAppDependencyProvider (from `build/generated/ios`)
- ReactCodegen (from `build/generated/ios`)
- "ReactCommon/turbomodule/core (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon`)"
- "ReactNativeDependencies (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)"
- "RNCAsyncStorage (from `../node_modules/.pnpm/@react-native-async-storage+async-storage@2.2.0_react-native@0.81.5_@babel+core@7.28.6_@types_fp4qq3a7mejmut52v6jrlvxlzi/node_modules/@react-native-async-storage/async-storage`)"
- "RNReanimated (from `../node_modules/.pnpm/react-native-reanimated@4.1.6_@babel+core@7.28.6_react-native-worklets@0.5.1_@babel+core@7.28_ky3sbxf6i7nkyacc2hzg3xcz4q/node_modules/react-native-reanimated`)"
- "RNScreens (from `../node_modules/.pnpm/react-native-screens@4.16.0_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-screens`)"
- "RNSVG (from `../node_modules/.pnpm/react-native-svg@15.12.1_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-svg`)"
- "RNWorklets (from `../node_modules/.pnpm/react-native-worklets@0.5.1_@babel+core@7.28.6_react-native@0.81.5_@babel+core@7.28.6_@types+_5atwepuw3zy3crkgvetf35tkve/node_modules/react-native-worklets`)"
- "Yoga (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/yoga`)"
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNWorklets (from `../node_modules/react-native-worklets`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
EXTERNAL SOURCES:
EXApplication:
:path: "../node_modules/.pnpm/expo-application@7.0.8_expo@54.0.32/node_modules/expo-application/ios"
:path: "../node_modules/expo-application/ios"
EXConstants:
:path: "../node_modules/.pnpm/expo-constants@18.0.13_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-constants/ios"
:path: "../node_modules/expo-constants/ios"
EXJSONUtils:
:path: "../node_modules/expo-json-utils/ios"
EXManifests:
:path: "../node_modules/expo-manifests/ios"
EXNotifications:
:path: "../node_modules/.pnpm/expo-notifications@0.32.16_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@1_nvlvke5tn7wk5pigfsu7j4ieeq/node_modules/expo-notifications/ios"
:path: "../node_modules/expo-notifications/ios"
Expo:
:path: "../node_modules/.pnpm/expo@54.0.32_@babel+core@7.28.6_@expo+metro-runtime@6.1.2_expo-router@6.0.22_react-native@0.8_7rhpxisdkrzvrgzbu7ct455kta/node_modules/expo"
:path: "../node_modules/expo"
expo-dev-client:
:path: "../node_modules/expo-dev-client/ios"
expo-dev-launcher:
:path: "../node_modules/expo-dev-launcher"
expo-dev-menu:
:path: "../node_modules/expo-dev-menu"
expo-dev-menu-interface:
:path: "../node_modules/expo-dev-menu-interface/ios"
ExpoAsset:
:path: "../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios"
:path: "../node_modules/expo-asset/ios"
ExpoCrypto:
:path: "../node_modules/expo-crypto/ios"
ExpoDevice:
:path: "../node_modules/expo-device/ios"
ExpoFileSystem:
:path: "../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios"
:path: "../node_modules/expo-file-system/ios"
ExpoFont:
:path: "../node_modules/.pnpm/expo-font@14.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-font/ios"
:path: "../node_modules/expo-font/ios"
ExpoHead:
:path: "../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.13_expo_rjurfbyy5kjn57nkkfxix5iqea/node_modules/expo-router/ios"
:path: "../node_modules/expo-router/ios"
ExpoKeepAwake:
:path: "../node_modules/.pnpm/expo-keep-awake@15.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-keep-awake/ios"
:path: "../node_modules/expo-keep-awake/ios"
ExpoLinearGradient:
:path: "../node_modules/.pnpm/expo-linear-gradient@15.0.8_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@_e6k2hjkd5k4lph2ersbp3gfshy/node_modules/expo-linear-gradient/ios"
:path: "../node_modules/expo-linear-gradient/ios"
ExpoLinking:
:path: "../node_modules/.pnpm/expo-linking@8.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-linking/ios"
:path: "../node_modules/expo-linking/ios"
ExpoLocalization:
:path: "../node_modules/.pnpm/expo-localization@17.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-localization/ios"
:path: "../node_modules/expo-localization/ios"
ExpoModulesCore:
:path: "../node_modules/.pnpm/expo-modules-core@3.0.29_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-modules-core"
:path: "../node_modules/expo-modules-core"
ExpoSplashScreen:
:path: "../node_modules/.pnpm/expo-splash-screen@31.0.13_expo@54.0.32/node_modules/expo-splash-screen/ios"
:path: "../node_modules/expo-splash-screen/ios"
ExpoWebBrowser:
:path: "../node_modules/.pnpm/expo-web-browser@15.0.10_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-web-browser/ios"
:path: "../node_modules/expo-web-browser/ios"
EXUpdatesInterface:
:path: "../node_modules/expo-updates-interface/ios"
FBLazyVector:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/FBLazyVector"
:path: "../node_modules/react-native/Libraries/FBLazyVector"
hermes-engine:
:podspec: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:tag: hermes-2025-07-07-RNv0.81.0-e0fc67142ec0763c6b6153ca2bf96df815539782
RCTDeprecation:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
:path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
RCTRequired:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Required"
:path: "../node_modules/react-native/Libraries/Required"
RCTTypeSafety:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/TypeSafety"
:path: "../node_modules/react-native/Libraries/TypeSafety"
React:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/"
:path: "../node_modules/react-native/"
React-callinvoker:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/callinvoker"
:path: "../node_modules/react-native/ReactCommon/callinvoker"
React-Core:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/"
:path: "../node_modules/react-native/"
React-Core-prebuilt:
:podspec: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React-Core-prebuilt.podspec"
:podspec: "../node_modules/react-native/React-Core-prebuilt.podspec"
React-CoreModules:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React/CoreModules"
:path: "../node_modules/react-native/React/CoreModules"
React-cxxreact:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/cxxreact"
:path: "../node_modules/react-native/ReactCommon/cxxreact"
React-debug:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/debug"
:path: "../node_modules/react-native/ReactCommon/react/debug"
React-defaultsnativemodule:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/defaults"
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
React-domnativemodule:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/dom"
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom"
React-Fabric:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon"
:path: "../node_modules/react-native/ReactCommon"
React-FabricComponents:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon"
:path: "../node_modules/react-native/ReactCommon"
React-FabricImage:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon"
:path: "../node_modules/react-native/ReactCommon"
React-featureflags:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/featureflags"
:path: "../node_modules/react-native/ReactCommon/react/featureflags"
React-featureflagsnativemodule:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
React-graphics:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/graphics"
:path: "../node_modules/react-native/ReactCommon/react/renderer/graphics"
React-hermes:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/hermes"
:path: "../node_modules/react-native/ReactCommon/hermes"
React-idlecallbacksnativemodule:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
React-ImageManager:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
:path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
React-jserrorhandler:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jserrorhandler"
:path: "../node_modules/react-native/ReactCommon/jserrorhandler"
React-jsi:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsi"
:path: "../node_modules/react-native/ReactCommon/jsi"
React-jsiexecutor:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsiexecutor"
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern"
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern"
React-jsinspectorcdp:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
React-jsinspectornetwork:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern/network"
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network"
React-jsinspectortracing:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
React-jsitooling:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/jsitooling"
:path: "../node_modules/react-native/ReactCommon/jsitooling"
React-jsitracing:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/hermes/executor/"
:path: "../node_modules/react-native/ReactCommon/hermes/executor/"
React-logger:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/logger"
:path: "../node_modules/react-native/ReactCommon/logger"
React-Mapbuffer:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon"
:path: "../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-safe-area-context:
:path: "../node_modules/.pnpm/react-native-safe-area-context@5.6.2_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1_azuxgonsvxb2yngtegtuvyxcpi/node_modules/react-native-safe-area-context"
:path: "../node_modules/react-native-safe-area-context"
React-NativeModulesApple:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-oscompat:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/oscompat"
:path: "../node_modules/react-native/ReactCommon/oscompat"
React-perflogger:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/reactperflogger"
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
React-performancetimeline:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/performance/timeline"
:path: "../node_modules/react-native/ReactCommon/react/performance/timeline"
React-RCTActionSheet:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/ActionSheetIOS"
:path: "../node_modules/react-native/Libraries/ActionSheetIOS"
React-RCTAnimation:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/NativeAnimation"
:path: "../node_modules/react-native/Libraries/NativeAnimation"
React-RCTAppDelegate:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/AppDelegate"
:path: "../node_modules/react-native/Libraries/AppDelegate"
React-RCTBlob:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Blob"
:path: "../node_modules/react-native/Libraries/Blob"
React-RCTFabric:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React"
:path: "../node_modules/react-native/React"
React-RCTFBReactNativeSpec:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React"
:path: "../node_modules/react-native/React"
React-RCTImage:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Image"
:path: "../node_modules/react-native/Libraries/Image"
React-RCTLinking:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/LinkingIOS"
:path: "../node_modules/react-native/Libraries/LinkingIOS"
React-RCTNetwork:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Network"
:path: "../node_modules/react-native/Libraries/Network"
React-RCTRuntime:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/React/Runtime"
:path: "../node_modules/react-native/React/Runtime"
React-RCTSettings:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Settings"
:path: "../node_modules/react-native/Libraries/Settings"
React-RCTText:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Text"
:path: "../node_modules/react-native/Libraries/Text"
React-RCTVibration:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/Vibration"
:path: "../node_modules/react-native/Libraries/Vibration"
React-rendererconsistency:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/consistency"
:path: "../node_modules/react-native/ReactCommon/react/renderer/consistency"
React-renderercss:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/css"
:path: "../node_modules/react-native/ReactCommon/react/renderer/css"
React-rendererdebug:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/debug"
:path: "../node_modules/react-native/ReactCommon/react/renderer/debug"
React-RuntimeApple:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/runtime/platform/ios"
:path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
React-RuntimeCore:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/runtime"
:path: "../node_modules/react-native/ReactCommon/react/runtime"
React-runtimeexecutor:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/runtimeexecutor"
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
React-RuntimeHermes:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/runtime"
:path: "../node_modules/react-native/ReactCommon/react/runtime"
React-runtimescheduler:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
:path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
React-timing:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/timing"
:path: "../node_modules/react-native/ReactCommon/react/timing"
React-utils:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/react/utils"
:path: "../node_modules/react-native/ReactCommon/react/utils"
ReactAppDependencyProvider:
:path: build/generated/ios
ReactCodegen:
:path: build/generated/ios
ReactCommon:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon"
:path: "../node_modules/react-native/ReactCommon"
ReactNativeDependencies:
:podspec: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
:podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
RNCAsyncStorage:
:path: "../node_modules/.pnpm/@react-native-async-storage+async-storage@2.2.0_react-native@0.81.5_@babel+core@7.28.6_@types_fp4qq3a7mejmut52v6jrlvxlzi/node_modules/@react-native-async-storage/async-storage"
:path: "../node_modules/@react-native-async-storage/async-storage"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNReanimated:
:path: "../node_modules/.pnpm/react-native-reanimated@4.1.6_@babel+core@7.28.6_react-native-worklets@0.5.1_@babel+core@7.28_ky3sbxf6i7nkyacc2hzg3xcz4q/node_modules/react-native-reanimated"
:path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/.pnpm/react-native-screens@4.16.0_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-screens"
:path: "../node_modules/react-native-screens"
RNSVG:
:path: "../node_modules/.pnpm/react-native-svg@15.12.1_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-svg"
:path: "../node_modules/react-native-svg"
RNWorklets:
:path: "../node_modules/.pnpm/react-native-worklets@0.5.1_@babel+core@7.28.6_react-native@0.81.5_@babel+core@7.28.6_@types+_5atwepuw3zy3crkgvetf35tkve/node_modules/react-native-worklets"
:path: "../node_modules/react-native-worklets"
Yoga:
:path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon/yoga"
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
EXApplication: 13420f8139864183f8a04fd6099077bdf8cfb186
EXConstants: 3feb66fd1d94202fc1f0946d74e029d8b224b60e
EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd
EXManifests: 83ef0844fcf06d6099b12a7bdbd7d36fc0e1dd16
EXNotifications: 2a3feb7af6194828d9aafda72f63a9a03866230a
Expo: b8d64eb9a496ebe8c71e3dae7eeb7f394b146b80
expo-dev-client: 12ef7d5b14d93e309922acea78dcd851db583a87
expo-dev-launcher: 47994056008ffdc30a6a5e328a375b3e30a8db05
expo-dev-menu: ea4fb803ace52e60d7cd8060c7cd379612a140b2
expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533
ExpoAsset: d999f3bbd998a750f3b74cb913229848901b926b
ExpoCrypto: 4d23a9ff67c25e2ed23ca792d81e58817a7ea1b9
ExpoDevice: 0773c782b055558ca9b40b74aa4a8133a66cd0d2
ExpoFileSystem: aefcd337b94b874f88752ebefc52813b84992fad
ExpoFont: c625dbd97ed57e9089b172b2a7bb99003d074664
ExpoHead: b691a2ed7ab02ed820b6c6468941832d34969c29
@@ -2323,6 +2563,7 @@ SPEC CHECKSUMS:
ExpoModulesCore: 77496909fd3c800f97f7f2007dd26aeac4bb3798
ExpoSplashScreen: 72fbc6dd9d6404dd9d0725a56c9ac1383bc0b14f
ExpoWebBrowser: 88b116cd378d9609c776c0903fe4070fca461588
EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990
@@ -2387,16 +2628,17 @@ SPEC CHECKSUMS:
React-timing: 03c7217455d2bff459b27a3811be25796b600f47
React-utils: 6d46795ae0444ec8a5d9a5f201157b286bf5250a
ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee
ReactCodegen: 88a1f4643f15841573f833b895bfa2a0c6cb4e7f
ReactCodegen: 4c44b74b77fc41ae25b9e2c7e9bd6e2bc772c23f
ReactCommon: e6e232202a447d353e5531f2be82f50f47cbaa9a
ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a
RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4
RNReanimated: 10415bc8396eaeac0d7b2c9a1538eae7e607ec9c
RNGestureHandler: 40c2d1c168e54715fe52e0fb16cb38c54611e4f3
RNReanimated: 43f611f1c85c90e0273df7399bf1536f8e2bd125
RNScreens: dd61bc3a3e6f6901ad833efa411917d44827cf51
RNSVG: 2825ee146e0f6a16221e852299943e4cceef4528
RNWorklets: 9ccdc8112b17af6eee2c85a233891cb80db150ad
RNWorklets: 28ee7370ca8da356fcc914e3e68b97e9752196d2
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
PODFILE CHECKSUM: dfe3cc75dee014a0abd367bc9e1bdbab0ba64ee3
PODFILE CHECKSUM: c2c3838f0b2a579fef2350bff2ecaa005e27145d
COCOAPODS: 1.16.2

View File

@@ -12,8 +12,12 @@
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */; };
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
C0A1B2C3D4E5F60718293A4E /* Screen_page.png in Resources */ = {isa = PBXBuildFile; fileRef = C0A1B2C3D4E5F60718293A4D /* Screen_page.png */; };
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
EB3DAF832F2A4B8E00450593 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF822F2A4B8E00450593 /* SwiftUI.framework */; };
EB3DAF942F2A4B8F00450593 /* 情绪小组件Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -45,20 +49,24 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = client.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>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
C0A1B2C3D4E5F60718293A4D /* Screen_page.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Screen_page.png; path = ../assets/images/Screen_page.png; sourceTree = "<group>"; };
C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-client/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
E3328F0E595C1F4A244DF238 /* libPods-client.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-client.a"; sourceTree = BUILT_PRODUCTS_DIR; };
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "情绪小组件Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
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; };
EB3DAF9A2F2A4D0900450593 /* MindfulnessWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindfulnessWidget.swift; sourceTree = "<group>"; };
EBEEC7562F31D82700C68C1A /* clientRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = clientRelease.entitlements; path = client/clientRelease.entitlements; sourceTree = "<group>"; };
EBEEC7572F31D84B00C68C1A /* 情绪小组件ExtensionRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "情绪小组件ExtensionRelease.entitlements"; sourceTree = "<group>"; };
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>"; };
F11748442D0722820044C1D9 /* client-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "client-Bridging-Header.h"; path = "client/client-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -97,6 +105,7 @@
buildActionMask = 2147483647;
files = (
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */,
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -115,13 +124,16 @@
13B07FAE1A68108700A75B9A /* client */ = {
isa = PBXGroup;
children = (
EB3DAF9A2F2A4D0900450593 /* MindfulnessWidget.swift */,
EBEEC7562F31D82700C68C1A /* clientRelease.entitlements */,
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */,
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */,
F11748442D0722820044C1D9 /* client-Bridging-Header.h */,
BB2F792B24A3F905000567C9 /* Supporting */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
C0A1B2C3D4E5F60718293A4D /* Screen_page.png */,
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */,
);
name = client;
@@ -156,6 +168,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
EBEEC7572F31D84B00C68C1A /* 情绪小组件ExtensionRelease.entitlements */,
13B07FAE1A68108700A75B9A /* client */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */,
@@ -173,7 +186,7 @@
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* client.app */,
13B07F961A680F5B00A75B9A /* DearMama.app */,
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */,
);
name = Products;
@@ -237,7 +250,7 @@
);
name = client;
productName = client;
productReference = 13B07F961A680F5B00A75B9A /* client.app */;
productReference = 13B07F961A680F5B00A75B9A /* DearMama.app */;
productType = "com.apple.product-type.application";
};
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */ = {
@@ -266,8 +279,12 @@
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
KnownAssetTags = (
New,
);
LastSwiftUpdateCheck = 2620;
LastUpgradeCheck = 1130;
LastUpgradeCheck = 2620;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
@@ -284,6 +301,7 @@
knownRegions = (
en,
Base,
"zh-Hant",
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
@@ -304,6 +322,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
C0A1B2C3D4E5F60718293A4E /* Screen_page.png in Resources */,
0BE245B56A79D95AB0A7B4BA /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -368,12 +387,15 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -381,12 +403,15 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -448,6 +473,8 @@
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */,
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */,
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -475,10 +502,13 @@
baseConfigurationReference = FFF632A94C7A551AAA096858 /* Pods-client.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = WS92GPX9H2;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
@@ -489,15 +519,16 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client;
PRODUCT_NAME = client;
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
PRODUCT_NAME = DearMama;
SKIP_INSTALL = NO;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@@ -505,7 +536,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -515,31 +546,37 @@
baseConfigurationReference = 3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_ENTITLEMENTS = client/clientRelease.entitlements;
CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = WS92GPX9H2;
DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
INFOPLIST_FILE = client/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client;
PRODUCT_NAME = client;
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
PRODUCT_NAME = DearMama;
SKIP_INSTALL = NO;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -567,6 +604,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -598,9 +636,11 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native";
ONLY_ACTIVE_ARCH = NO;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SKIP_INSTALL = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
@@ -630,6 +670,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -653,9 +694,13 @@
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SKIP_INSTALL = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
@@ -675,9 +720,11 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "情绪小组件ExtensionRelease.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = WS92GPX9H2;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -691,11 +738,11 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
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)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -709,7 +756,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -726,10 +773,12 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "情绪小组件ExtensionRelease.entitlements";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = WS92GPX9H2;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -743,10 +792,10 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.1;
MTL_FAST_MATH = YES;
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)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -759,7 +808,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "client.app"
BuildableName = "DearMama.app"
BlueprintName = "client"
ReferencedContainer = "container:client.xcodeproj">
</BuildableReference>
@@ -26,19 +26,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "clientTests.xctest"
BlueprintName = "clientTests"
ReferencedContainer = "container:client.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -55,7 +44,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "client.app"
BuildableName = "DearMama.app"
BlueprintName = "client"
ReferencedContainer = "container:client.xcodeproj">
</BuildableReference>
@@ -72,7 +61,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "client.app"
BuildableName = "DearMama.app"
BlueprintName = "client"
ReferencedContainer = "container:client.xcodeproj">
</BuildableReference>
@@ -83,6 +72,26 @@
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
customArchiveName = "Dear Mama"
revealArchiveInOrganizer = "YES">
<PostActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "&#x4fee;&#x590d;&#x5f52;&#x6863;&#x5934;&#x4fe1;&#x606f;&#xff08;&#x907f;&#x514d; Generic Xcode Archive&#xff09;"
scriptText = "bash &quot;${SRCROOT}/scripts/fix-xcarchive-header.sh&quot; &quot;${ARCHIVE_PATH}&quot;&#10;"
shellToInvoke = "/bin/sh">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "DearMama.app"
BlueprintName = "client"
ReferencedContainer = "container:client.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PostActions>
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,53 @@
import Foundation
import React
import WidgetKit
/**
* App Group RN Bridge
*
*
* - suiteNamegroup.com.damer.mindfulness App Widget Extension entitlements
* - 使 JSON JS
*/
@objc(AppGroupStorage)
final class AppGroupStorage: NSObject {
// AppGroupStorageBridge.m RCT_EXTERN_MODULE RN
// / RCTBridgeModule Archive
@objc static func requiresMainQueueSetup() -> Bool { false }
private let suiteName = "group.com.damer.mindfulness"
private func defaults() -> UserDefaults? {
UserDefaults(suiteName: suiteName)
}
@objc(setString:value:resolver:rejecter:)
func setString(_ key: String, value: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
guard let d = defaults() else {
reject("E_APP_GROUP", "无法初始化 App Group UserDefaultssuiteName=\(suiteName)", nil)
return
}
d.set(value, forKey: key)
resolve(nil)
}
@objc(getString:resolver:rejecter:)
func getString(_ key: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
guard let d = defaults() else {
reject("E_APP_GROUP", "无法初始化 App Group UserDefaultssuiteName=\(suiteName)", nil)
return
}
let v = d.string(forKey: key)
resolve(v)
}
/**
* Widget
*/
@objc(reloadAllTimelines:rejecter:)
func reloadAllTimelines(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
WidgetCenter.shared.reloadAllTimelines()
resolve(nil)
}
}

View File

@@ -0,0 +1,26 @@
#import <React/RCTBridgeModule.h>
/**
* Swift
*
*
* - React Native Swift RCT_EXTERN_MODULE / RCT_EXTERN_METHOD
* - JS NativeModules.AppGroupStorage undefined
*/
@interface RCT_EXTERN_MODULE(AppGroupStorage, NSObject)
RCT_EXTERN_METHOD(setString:(NSString *)key
value:(NSString *)value
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getString:(NSString *)key
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(reloadAllTimelines:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@end

Some files were not shown because too many files have changed in this diff Show More