chore: vendor client into main repo
1
client
3
client/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
EXPO_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
EXPO_PUBLIC_ENV=dev
|
||||
EXPO_PUBLIC_DEFAULT_LANGUAGE=auto
|
||||
1
client/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "recommendations": ["expo.vscode-expo-tools"] }
|
||||
7
client/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "explicit"
|
||||
}
|
||||
}
|
||||
343
client/README.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 正念 APP(客户端)
|
||||
|
||||
本目录用于存放「正念 APP」客户端工程,定位是面向宝妈人群的情绪价值与正念练习支持应用。
|
||||
|
||||
## 功能概览(一期)
|
||||
|
||||
- **APP-PUSH 推送**:定时推送情绪文字(固定时间 / 用户自定义时间),推送内容可由后端配置更新
|
||||
- **iOS 小组件(Widget)**:展示当前情绪文字与背景图/渐变色,支持多尺寸与可配置刷新频率
|
||||
- **情绪卡片滑动**:卡片列表左右滑动切换(类似 Tinder),收藏/分享可作为后续迭代
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **React Native + Expo**
|
||||
- **TypeScript**
|
||||
- **React Navigation**:页面路由
|
||||
- **状态管理**:Zustand(或 Redux Toolkit)
|
||||
- **网络请求**:Axios(或 Fetch)
|
||||
- **推送**:Expo Notifications
|
||||
- **环境区分**:Expo App Config + `.env.dev` / `.env.prod`
|
||||
- **iOS 打包**:EAS Build
|
||||
|
||||
## 开发环境要求
|
||||
|
||||
- **Node.js**:建议使用 **20+(LTS)**
|
||||
- **包管理器**:pnpm
|
||||
- **Expo CLI**:推荐通过 `npx expo` 使用(避免全局版本漂移)
|
||||
- **iOS 真机调试**:macOS + Xcode(如需)
|
||||
|
||||
|
||||
> 本项目已内置 `postinstall` 补丁脚本用于兼容(安装依赖后会自动修复),但仍建议升级到 Node 20+。
|
||||
|
||||
## 快速开始
|
||||
|
||||
> 说明:本仓库已包含 Expo 工程文件;如果你只是想把客户端跑起来,从「安装依赖」开始即可。
|
||||
|
||||
### 1)初始化工程(若尚未创建)
|
||||
|
||||
在 `client/` 目录下创建 Expo 工程(示例):
|
||||
|
||||
```bash
|
||||
cd client
|
||||
npx create-expo-app@latest .
|
||||
```
|
||||
|
||||
执行后按提示选择模板即可;如果你希望固定使用 TypeScript 模板,也可以在创建时选择对应模板(以实际 CLI 提示为准)。
|
||||
|
||||
### 2)安装依赖
|
||||
|
||||
```bash
|
||||
cd client
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3)配置环境变量(dev / prod)
|
||||
|
||||
按根目录约定,客户端使用 `.env.dev` 与 `.env.prod` 区分环境;`.env` 文件不提交到 Git,仓库内应提供 `.env.example` 作为模板。
|
||||
|
||||
#### 本地开发如何注入环境变量(推荐)
|
||||
|
||||
Expo 在本地开发时会自动读取当前目录下的 `.env` / `.env.local`(仅以 `EXPO_PUBLIC_` 前缀变量注入到客户端运行时)。推荐做法:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
cp .env.example .env.local
|
||||
# 然后按需修改 .env.local
|
||||
pnpm start
|
||||
```
|
||||
|
||||
> 修改 `.env*` 后需要重启 `pnpm start` 才会生效。
|
||||
|
||||
#### 本地开发如何区分 dev / prod(可选)
|
||||
|
||||
如果你希望坚持使用 `.env.dev` / `.env.prod` 文件名,可以在启动前导出环境变量(zsh 示例):
|
||||
|
||||
```bash
|
||||
cd client
|
||||
set -a
|
||||
source .env.dev
|
||||
set +a
|
||||
pnpm start
|
||||
```
|
||||
|
||||
> 注意:`source` 方式要求 `.env.dev` 内容是合法的 shell 格式(例如 `KEY=value`),不要带 `export` 以外的复杂语法;值包含空格时需要加引号。
|
||||
|
||||
#### 必填环境变量
|
||||
|
||||
- **`EXPO_PUBLIC_API_BASE_URL`**:后端 API 基地址
|
||||
- 本地:`http://localhost:8000`
|
||||
- 真机联调:改为电脑局域网 IP(例如 `http://192.168.1.10:8000`)
|
||||
- **`EXPO_PUBLIC_ENV`**:环境标识(建议 `dev` / `prod`)
|
||||
|
||||
#### 可选环境变量
|
||||
|
||||
- **`EXPO_PUBLIC_DEFAULT_LANGUAGE`**:默认语言策略
|
||||
- `auto`:优先设备语言(默认)
|
||||
- `zh-CN/en/es/pt/zh-TW`:固定默认语言(仍允许用户在设置中切换并持久化)
|
||||
|
||||
建议字段(示例):
|
||||
|
||||
```env
|
||||
EXPO_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
EXPO_PUBLIC_ENV=dev
|
||||
EXPO_PUBLIC_DEFAULT_LANGUAGE=auto
|
||||
```
|
||||
|
||||
> 提示:在 Expo 中建议使用 `EXPO_PUBLIC_` 前缀,方便在运行时读取并支持 EAS 构建注入。
|
||||
|
||||
### 4)启动开发服务器
|
||||
|
||||
```bash
|
||||
cd client
|
||||
pnpm start
|
||||
```
|
||||
|
||||
启动后你会看到终端输出的二维码(QR Code)与可用命令提示。
|
||||
|
||||
### 5)运行到 iOS / Android / 真机 / Web
|
||||
|
||||
- **iOS 模拟器(macOS + Xcode)**:
|
||||
- 方式一:在 `pnpm start` 的终端里按 `i`
|
||||
- 方式二:直接运行:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
pnpm ios
|
||||
```
|
||||
|
||||
- **Android 模拟器(Android Studio)**:
|
||||
- 方式一:在 `pnpm start` 的终端里按 `a`
|
||||
- 方式二:直接运行:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
pnpm android
|
||||
```
|
||||
|
||||
- **真机(推荐使用 Expo Go)**:
|
||||
- 手机安装 **Expo Go**
|
||||
- 确保手机与电脑在**同一局域网**
|
||||
- 运行 `pnpm start` 后,用 Expo Go 扫描终端二维码即可打开 App
|
||||
|
||||
- **Web(可选)**:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
pnpm web
|
||||
```
|
||||
|
||||
常用命令(脚本):
|
||||
|
||||
```bash
|
||||
pnpm ios
|
||||
pnpm android
|
||||
pnpm web
|
||||
```
|
||||
|
||||
### 6)常见问题
|
||||
|
||||
- **真机打不开 / 扫码后卡住**:
|
||||
- 检查手机与电脑是否同一 Wi-Fi
|
||||
- 尝试重启 `pnpm start -- --clear` 清缓存
|
||||
- **启动时提示 `Networking has been disabled` / `Network connection is unreliable`**:
|
||||
- 这是 Expo CLI 的网络探测/请求失败导致的,按下面方式跳过网络请求即可:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
EXPO_OFFLINE=1 pnpm start
|
||||
```
|
||||
|
||||
- 如果你本机设置了代理(例如 `HTTP_PROXY/ALL_PROXY`),也可以临时关闭代理后再启动:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY
|
||||
pnpm start
|
||||
```
|
||||
|
||||
- **Watchman 提示 Recrawl(不影响运行,但建议处理)**:
|
||||
- 按提示执行一次即可清除警告:
|
||||
|
||||
```bash
|
||||
watchman watch-del '/Users/jojo/Desktop/lxy/gitea/mindfulness/client' ; watchman watch-project '/Users/jojo/Desktop/lxy/gitea/mindfulness/client'
|
||||
```
|
||||
- **联调时请求打到 localhost**:
|
||||
- 真机上 `localhost` 指向手机自身,请把 `EXPO_PUBLIC_API_BASE_URL` 改成电脑的局域网 IP(例如 `http://192.168.1.10:8000`),或使用内网穿透方案
|
||||
|
||||
## 与后端联调
|
||||
|
||||
- **后端基座**:FastAPI(详见 `server/README.md`)
|
||||
- **API Base URL**:通过 `EXPO_PUBLIC_API_BASE_URL` 配置
|
||||
- **本地真机注意**:如果在手机上调试,请将 `localhost` 替换为电脑局域网 IP,或使用内网穿透方案
|
||||
|
||||
## 多语言(i18n):CN / EN / ES / PT / TC
|
||||
|
||||
### 语言码约定
|
||||
|
||||
- **CN(简体中文)**:`zh-CN`
|
||||
- **EN(英语)**:`en`
|
||||
- **ES(西班牙语)**:`es`
|
||||
- **PT(葡萄牙语)**:`pt`
|
||||
- **TC(繁体中文)**:`zh-TW`
|
||||
|
||||
### 推荐选型(Expo/RN 常用组合)
|
||||
|
||||
- **文案管理**:`i18next` + `react-i18next`
|
||||
- **系统语言读取**:`expo-localization`
|
||||
|
||||
### 建议目录结构
|
||||
|
||||
```text
|
||||
src/
|
||||
└── i18n/
|
||||
├── index.ts # i18n 初始化(默认语言、回退语言、资源注册)
|
||||
├── locales/
|
||||
│ ├── zh-CN.json
|
||||
│ ├── en.json
|
||||
│ ├── es.json
|
||||
│ ├── pt.json
|
||||
│ └── zh-TW.json
|
||||
└── types.ts #(可选)语言码与 key 的类型定义
|
||||
```
|
||||
|
||||
### 约定与最佳实践
|
||||
|
||||
- **key 规则**:使用稳定的点分层 key,例如 `common.ok`、`push.permissionTitle`、`cards.swipeHint`
|
||||
- **默认与回退**:默认优先跟随用户当前设备语言(在支持列表内时生效);设备语言不支持时回退到 `zh-CN`(或团队指定默认)
|
||||
- **插值与复数**:优先使用 i18next 的插值(例如 `{{name}}`),避免在代码里拼字符串
|
||||
- **动态切换**:提供“语言设置”入口允许手动切换;切换后持久化(例如 AsyncStorage),后续启动优先生效,并立即刷新文案
|
||||
- **语言优先级**:用户设置(若存在)> 设备语言(在支持列表内)> 默认回退(`zh-CN` 或团队指定默认)
|
||||
- **设置入口建议**:设置页 -> 语言(或 设置页 -> 通用 -> 语言)
|
||||
- **与服务端一致性**:若后端也需要多语言(推送文案等),建议统一语言码(`zh-CN/en/es/pt/zh-TW`)并在接口里明确 `lang`
|
||||
|
||||
## 推送(Expo Notifications)
|
||||
|
||||
客户端侧通常需要:
|
||||
|
||||
- **申请通知权限**
|
||||
- **获取 Expo Push Token**
|
||||
- **将 Token 上报后端**(用于定时推送任务)
|
||||
|
||||
后端侧通常需要:
|
||||
|
||||
- **保存用户 Push Token**
|
||||
- **按策略触发推送**(固定时间 / 用户自定义时间)
|
||||
- **支持后台配置推送文案并更新**
|
||||
|
||||
> 具体实现以客户端工程代码为准;当代码接入后,建议在 README 补充「Token 上报接口」与「字段定义」。
|
||||
|
||||
## iOS 小组件(Widget)
|
||||
|
||||
小组件通常需要:
|
||||
|
||||
- **数据源**:来自本地缓存或后端拉取(需要设计刷新策略与缓存)
|
||||
- **展示内容**:当前情绪文字 + 背景图/渐变
|
||||
- **尺寸**:小 / 中 / 大
|
||||
- **刷新频率**:如每小时/每天(iOS 对频率有系统限制,以实际效果为准)
|
||||
|
||||
> 若你计划使用 Expo 的相关能力,请在工程中明确选型与实现路径,并补充到本 README。
|
||||
|
||||
## EAS Build(iOS 打包)
|
||||
|
||||
建议按根目录约定区分 dev/prod:
|
||||
|
||||
- **dev**:`com.damer.mindfulness.dev`
|
||||
- **prod**:`com.damer.mindfulness`
|
||||
|
||||
常见流程(示例):
|
||||
|
||||
```bash
|
||||
cd client
|
||||
eas login
|
||||
eas build --platform ios --profile development
|
||||
```
|
||||
|
||||
> 实际 profile、证书、bundle id、环境变量注入方式以工程内 `eas.json` / app config 为准。
|
||||
|
||||
## 目录结构(建议)
|
||||
|
||||
当客户端工程落地后,建议使用更标准、可扩展的目录结构(Expo + TypeScript 常见组织方式):
|
||||
|
||||
```text
|
||||
client/
|
||||
├── app/ #(推荐)expo-router 路由目录(若使用 expo-router)
|
||||
│ ├── (tabs)/ # Tabs 分组(可选)
|
||||
│ ├── _layout.tsx # 根布局
|
||||
│ └── index.tsx # 首页
|
||||
├── src/ # 业务源码(与路由/平台代码解耦)
|
||||
│ ├── components/ # 通用 UI 组件
|
||||
│ ├── features/ # 按功能域拆分(推荐)
|
||||
│ │ ├── push/ # 推送相关(权限、token、上报等)
|
||||
│ │ ├── widget/ # 小组件数据与样式相关
|
||||
│ │ └── cards/ # 情绪卡片滑动相关
|
||||
│ ├── hooks/ # 自定义 hooks
|
||||
│ ├── navigation/ #(若不用 expo-router)React Navigation 配置
|
||||
│ ├── screens/ #(若不用 expo-router)页面
|
||||
│ ├── services/ # API 封装(request、接口定义)
|
||||
│ ├── store/ # 状态管理(Zustand/RTK)
|
||||
│ ├── utils/ # 工具函数(时间、格式化、校验等)
|
||||
│ ├── constants/ # 常量与配置(主题、枚举等)
|
||||
│ └── types/ # 全局类型声明
|
||||
├── assets/ # 静态资源(图片/字体/音频等)
|
||||
├── app.json / app.config.ts # Expo 配置(环境区分可在此处理)
|
||||
├── eas.json # EAS Build 配置(如使用)
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
> 说明:如果你不使用 `expo-router`,可以删除 `app/`,并以 `src/navigation/` + `src/screens/` 作为主路由结构;其余目录保持不变即可。
|
||||
|
||||
## iOS 小组件开发(V1:写死文案)
|
||||
|
||||
> 重要:**Expo Go 不支持 WidgetKit**。要做真正的小组件,必须预构建 iOS 工程并用 Xcode 跑。
|
||||
|
||||
### 1)生成 iOS 工程
|
||||
|
||||
```bash
|
||||
cd client
|
||||
npx expo prebuild -p ios --no-install
|
||||
```
|
||||
|
||||
生成后会得到 `client/ios/`(Xcode 工程文件都在里面)。
|
||||
|
||||
### 2)在 Xcode 创建 Widget Extension
|
||||
|
||||
- 用 Xcode 打开:`client/ios/client.xcworkspace`
|
||||
- 菜单:`File -> New -> Target... -> Widget Extension`
|
||||
- Target 名称示例:`MindfulnessWidget`
|
||||
|
||||
### 3)写死文案与三尺寸布局
|
||||
|
||||
仓库里已提供 SwiftUI 代码骨架(写死文案 + Small/Medium/Large + 点击跳转):
|
||||
|
||||
- `client/ios/情绪小组件/EmotionWidget.swift`
|
||||
|
||||
创建 Widget target 后,把该文件加入到 Widget target 中即可(Target Membership 勾选:`情绪小组件`)。
|
||||
|
||||
### 4)点击跳转到 Home(Deep Link)
|
||||
|
||||
Widget 点击跳转使用:
|
||||
|
||||
- `client:///(app)/home`
|
||||
|
||||
代码里已通过 `widgetURL` 设置(见 `MindfulnessWidget.swift`)。
|
||||
40
client/app.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "client",
|
||||
"slug": "client",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "client",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.anonymous.client"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
50
client/app/(app)/_layout.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Pressable, Text } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
export default function AppLayout() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerTitleAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: t('home.title'),
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPress={() => router.push('/(app)/settings')}
|
||||
hitSlop={10}
|
||||
>
|
||||
<Text>{t('home.settings')}</Text>
|
||||
</Pressable>
|
||||
),
|
||||
headerLeft: () => (
|
||||
<Pressable onPress={() => router.push('/(app)/favorites')} hitSlop={10}>
|
||||
<Text>{t('home.favorites')}</Text>
|
||||
</Pressable>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="favorites"
|
||||
options={{
|
||||
title: t('favorites.title'),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: t('settings.title'),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
61
client/app/(app)/favorites.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { getFavorites } from '@/src/storage/appStorage';
|
||||
|
||||
export default function FavoritesScreen() {
|
||||
const { t } = useTranslation();
|
||||
const [ids, setIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const list = await getFavorites();
|
||||
if (!cancelled) setIds(list);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{items.length === 0 ? (
|
||||
<Text style={styles.empty}>{t('favorites.empty')}</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={(it) => it.id}
|
||||
contentContainerStyle={styles.list}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16 },
|
||||
empty: { color: '#6B7280', fontSize: 16, textAlign: 'center', marginTop: 40 },
|
||||
list: { gap: 12, paddingBottom: 24 },
|
||||
row: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
backgroundColor: '#F9FAFB',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
text: { color: '#111827', fontSize: 16, lineHeight: 22 },
|
||||
});
|
||||
|
||||
76
client/app/(app)/home.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { addFavorite, setReaction } from '@/src/storage/appStorage';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]);
|
||||
|
||||
async function onLike() {
|
||||
await setReaction(item.id, 'like');
|
||||
await addFavorite(item.id);
|
||||
setIndex((i) => i + 1);
|
||||
}
|
||||
|
||||
async function onDislike() {
|
||||
await setReaction(item.id, 'dislike');
|
||||
setIndex((i) => i + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.button, styles.dislike]} onPress={onDislike}>
|
||||
<Text style={styles.buttonText}>{t('home.dislike')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.button, styles.like]} onPress={onLike}>
|
||||
<Text style={styles.buttonText}>{t('home.like')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
backgroundColor: '#F6F7FB',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
lineHeight: 28,
|
||||
color: '#111827',
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
like: { backgroundColor: '#16A34A' },
|
||||
dislike: { backgroundColor: '#EF4444' },
|
||||
buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
|
||||
});
|
||||
|
||||
50
client/app/(app)/settings.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Constants from 'expo-constants';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const version =
|
||||
Constants.expoConfig?.version ??
|
||||
// 兜底:部分环境下 expoConfig 可能为空
|
||||
'unknown';
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>{t('settings.version')}</Text>
|
||||
<Text style={styles.value}>{version}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>{t('settings.widgetTitle')}</Text>
|
||||
<Text style={styles.cardText}>{t('settings.widgetDesc')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16, gap: 16 },
|
||||
section: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
label: { color: '#374151', fontSize: 16 },
|
||||
value: { color: '#111827', fontSize: 16, fontWeight: '600' },
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
backgroundColor: '#111827',
|
||||
},
|
||||
cardTitle: { color: 'white', fontSize: 16, fontWeight: '700', marginBottom: 8 },
|
||||
cardText: { color: '#E5E7EB', fontSize: 14, lineHeight: 20 },
|
||||
});
|
||||
|
||||
18
client/app/(onboarding)/_layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function OnboardingLayout() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerTitleAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="onboarding" options={{ title: t('onboarding.title') }} />
|
||||
<Stack.Screen name="push-prompt" options={{ title: t('push.title') }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
104
client/app/(onboarding)/onboarding.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { setOnboardingCompleted } from '@/src/storage/appStorage';
|
||||
|
||||
type OnboardingPage = {
|
||||
title: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 3–5 页:这里默认 4 页,后续可按产品调整为 3 或 5
|
||||
const pages = useMemo<OnboardingPage[]>(
|
||||
() => [
|
||||
{ title: t('onboarding.q1Title'), desc: t('onboarding.q1Desc') },
|
||||
{ title: t('onboarding.q2Title'), desc: t('onboarding.q2Desc') },
|
||||
{ title: t('onboarding.q3Title'), desc: t('onboarding.q3Desc') },
|
||||
{ title: t('onboarding.q4Title'), desc: t('onboarding.q4Desc') }
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const total = pages.length;
|
||||
const [step, setStep] = useState(0);
|
||||
const page = pages[Math.min(step, total - 1)];
|
||||
|
||||
async function finishAndNext() {
|
||||
await setOnboardingCompleted(true);
|
||||
router.replace('/(onboarding)/push-prompt');
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (step >= total - 1) {
|
||||
await finishAndNext();
|
||||
return;
|
||||
}
|
||||
setStep((s) => s + 1);
|
||||
}
|
||||
|
||||
async function onSkipPage() {
|
||||
// 每页可跳过:直接进入下一页(最后一页则结束)
|
||||
await onNext();
|
||||
}
|
||||
|
||||
async function onSkipAll() {
|
||||
// 一键跳过整个 Onboarding
|
||||
await finishAndNext();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.progress}>
|
||||
{t('onboarding.progress', { current: step + 1, total })}
|
||||
</Text>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.title}>{page.title}</Text>
|
||||
<Text style={styles.desc}>{page.desc}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.secondary]} onPress={onSkipPage}>
|
||||
<Text style={[styles.btnText, styles.secondaryText]}>{t('onboarding.skip')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.btn, styles.primary]} onPress={onNext}>
|
||||
<Text style={styles.btnText}>{t('onboarding.next')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Pressable style={styles.skipAll} onPress={onSkipAll}>
|
||||
<Text style={styles.skipAllText}>{t('onboarding.skipAll')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 20, justifyContent: 'center', gap: 16 },
|
||||
progress: { textAlign: 'center', color: '#6B7280' },
|
||||
card: {
|
||||
borderRadius: 18,
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
gap: 10
|
||||
},
|
||||
title: { fontSize: 22, fontWeight: '700', color: '#111827' },
|
||||
desc: { fontSize: 16, lineHeight: 22, color: '#374151' },
|
||||
actions: { flexDirection: 'row', gap: 12 },
|
||||
btn: { flex: 1, paddingVertical: 14, borderRadius: 14, alignItems: 'center' },
|
||||
primary: { backgroundColor: '#111827' },
|
||||
secondary: { backgroundColor: '#F3F4F6' },
|
||||
btnText: { fontSize: 16, fontWeight: '600', color: '#FFFFFF' },
|
||||
secondaryText: { color: '#111827' },
|
||||
skipAll: { alignItems: 'center', paddingTop: 10 },
|
||||
skipAllText: { color: '#6B7280', textDecorationLine: 'underline' }
|
||||
});
|
||||
|
||||
74
client/app/(onboarding)/push-prompt.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
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' }
|
||||
});
|
||||
|
||||
38
client/app/+html.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ScrollViewStyleReset } from 'expo-router/html';
|
||||
|
||||
// This file is web-only and used to configure the root HTML for every
|
||||
// web page during static rendering.
|
||||
// The contents of this function only run in Node.js environments and
|
||||
// do not have access to the DOM or browser APIs.
|
||||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
|
||||
{/*
|
||||
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
|
||||
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
|
||||
*/}
|
||||
<ScrollViewStyleReset />
|
||||
|
||||
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
|
||||
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
|
||||
{/* Add any additional <head> elements that you want globally available on web... */}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const responsiveBackground = `
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
}`;
|
||||
40
client/app/+not-found.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { Text, View } from '@/components/Themed';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>This screen doesn't exist.</Text>
|
||||
|
||||
<Link href="/" style={styles.link}>
|
||||
<Text style={styles.linkText}>Go to home screen!</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
color: '#2e78b7',
|
||||
},
|
||||
});
|
||||
81
client/app/_layout.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||
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 'react-native-reanimated';
|
||||
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
import { initI18n } from '@/src/i18n';
|
||||
|
||||
export {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
ErrorBoundary,
|
||||
} from 'expo-router';
|
||||
|
||||
export const unstable_settings = {
|
||||
// Ensure that reloading on `/modal` keeps a back button present.
|
||||
initialRouteName: 'index',
|
||||
};
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function RootLayout() {
|
||||
const [loaded, error] = useFonts({
|
||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||
...FontAwesome.font,
|
||||
});
|
||||
const [i18nReady, setI18nReady] = useState(false);
|
||||
|
||||
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
||||
useEffect(() => {
|
||||
if (error) throw error;
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
initI18n()
|
||||
.catch((e) => {
|
||||
// i18n 初始化失败不应阻塞 App 启动,先打印错误再继续
|
||||
console.error('i18n 初始化失败', e);
|
||||
})
|
||||
.finally(() => setI18nReady(true));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 等字体与 i18n 都准备好后再隐藏启动页,避免文案闪烁
|
||||
if (loaded && i18nReady) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded, i18nReady]);
|
||||
|
||||
if (!loaded || !i18nReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RootLayoutNav />;
|
||||
}
|
||||
|
||||
function RootLayoutNav() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
{/* 启动分发页:根据 onboarding 状态跳转 */}
|
||||
<Stack.Screen name="index" />
|
||||
|
||||
{/* Onboarding 分组 */}
|
||||
<Stack.Screen name="(onboarding)" />
|
||||
|
||||
{/* 主应用分组(不使用 Tabs) */}
|
||||
<Stack.Screen name="(app)" />
|
||||
|
||||
{/* 其他 */}
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
35
client/app/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
import { getOnboardingCompleted } from '@/src/storage/appStorage';
|
||||
|
||||
/**
|
||||
* 启动分发:根据 onboarding 状态跳转
|
||||
*/
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const completed = await getOnboardingCompleted();
|
||||
if (cancelled) return;
|
||||
router.replace(completed ? '/(app)/home' : '/(onboarding)/onboarding');
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||
});
|
||||
|
||||
35
client/app/modal.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Modal</Text>
|
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
||||
<EditScreenInfo path="app/modal.tsx" />
|
||||
|
||||
{/* Use a light status bar on iOS to account for the black space above the modal */}
|
||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 30,
|
||||
height: 1,
|
||||
width: '80%',
|
||||
},
|
||||
});
|
||||
BIN
client/assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
client/assets/images/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
client/assets/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
client/assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
client/assets/images/splash-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
77
client/components/EditScreenInfo.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ExternalLink } from './ExternalLink';
|
||||
import { MonoText } from './StyledText';
|
||||
import { Text, View } from './Themed';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
export default function EditScreenInfo({ path }: { path: string }) {
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.getStartedContainer}>
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)">
|
||||
Open up the code for this screen:
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
|
||||
darkColor="rgba(255,255,255,0.05)"
|
||||
lightColor="rgba(0,0,0,0.05)">
|
||||
<MonoText>{path}</MonoText>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)">
|
||||
Change any of the text, save the file, and your app will automatically update.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpContainer}>
|
||||
<ExternalLink
|
||||
style={styles.helpLink}
|
||||
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet">
|
||||
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
|
||||
Tap here if your app doesn't automatically update after making changes
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
getStartedContainer: {
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 50,
|
||||
},
|
||||
homeScreenFilename: {
|
||||
marginVertical: 7,
|
||||
},
|
||||
codeHighlightContainer: {
|
||||
borderRadius: 3,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
getStartedText: {
|
||||
fontSize: 17,
|
||||
lineHeight: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
helpContainer: {
|
||||
marginTop: 15,
|
||||
marginHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
helpLink: {
|
||||
paddingVertical: 15,
|
||||
},
|
||||
helpLinkText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
26
client/components/ExternalLink.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Link } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export function ExternalLink(
|
||||
props: Omit<React.ComponentProps<typeof Link>, 'href'> & { href: string }
|
||||
) {
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
{...props}
|
||||
// expo-router 的 Link 类型会限制 href(主要用于站内路由)
|
||||
// 这里明确把 href 视为外部链接(字符串),并由 onPress 接管打开逻辑
|
||||
href={props.href as any}
|
||||
onPress={(e) => {
|
||||
if (Platform.OS !== 'web') {
|
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
e.preventDefault();
|
||||
// Open the link in an in-app browser.
|
||||
WebBrowser.openBrowserAsync(props.href);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
client/components/StyledText.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Text, TextProps } from './Themed';
|
||||
|
||||
export function MonoText(props: TextProps) {
|
||||
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />;
|
||||
}
|
||||
45
client/components/Themed.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Learn more about Light and Dark modes:
|
||||
* https://docs.expo.io/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Text as DefaultText, View as DefaultView } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from './useColorScheme';
|
||||
|
||||
type ThemeProps = {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
};
|
||||
|
||||
export type TextProps = ThemeProps & DefaultText['props'];
|
||||
export type ViewProps = ThemeProps & DefaultView['props'];
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
||||
|
||||
export function Text(props: TextProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
return <DefaultText style={[{ color }, style]} {...otherProps} />;
|
||||
}
|
||||
|
||||
export function View(props: ViewProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
|
||||
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
|
||||
}
|
||||
10
client/components/__tests__/StyledText-test.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { MonoText } from '../StyledText';
|
||||
|
||||
it(`renders correctly`, () => {
|
||||
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
4
client/components/useClientOnlyValue.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
||||
return client;
|
||||
}
|
||||
12
client/components/useClientOnlyValue.web.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
// `useEffect` is not invoked during server rendering, meaning
|
||||
// we can use this to determine if we're on the server or not.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
||||
const [value, setValue] = React.useState<S | C>(server);
|
||||
React.useEffect(() => {
|
||||
setValue(client);
|
||||
}, [client]);
|
||||
|
||||
return value;
|
||||
}
|
||||
1
client/components/useColorScheme.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useColorScheme } from 'react-native';
|
||||
8
client/components/useColorScheme.web.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// NOTE: The default React Native styling doesn't support server rendering.
|
||||
// Server rendered styles should not change between the first render of the HTML
|
||||
// and the first render on the client. Typically, web developers will use CSS media queries
|
||||
// to render different styles on the client and server, these aren't directly supported in React Native
|
||||
// but can be achieved using a styling library like Nativewind.
|
||||
export function useColorScheme() {
|
||||
return 'light';
|
||||
}
|
||||
19
client/constants/Colors.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
const tintColorLight = '#2f95dc';
|
||||
const tintColorDark = '#fff';
|
||||
|
||||
export default {
|
||||
light: {
|
||||
text: '#000',
|
||||
background: '#fff',
|
||||
tint: tintColorLight,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#fff',
|
||||
background: '#000',
|
||||
tint: tintColorDark,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
11
client/ios/.xcode.env
Normal file
@@ -0,0 +1,11 @@
|
||||
# This `.xcode.env` file is versioned and is used to source the environment
|
||||
# used when running script phases inside Xcode.
|
||||
# To customize your local environment, you can create an `.xcode.env.local`
|
||||
# file that is not versioned.
|
||||
|
||||
# NODE_BINARY variable contains the PATH to the node executable.
|
||||
#
|
||||
# Customize the NODE_BINARY variable here.
|
||||
# For example, to use nvm with brew, add the following line
|
||||
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||
export NODE_BINARY=$(command -v node)
|
||||
128
client/ios/MindfulnessWidget/MindfulnessWidget.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
12
client/ios/MindfulnessWidget/MindfulnessWidgetBundle.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// 统一的 Widget Extension 入口(模块内只能有一个 @main)
|
||||
@main
|
||||
struct MindfulnessWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
MindfulnessWidget()
|
||||
EmotionWidget()
|
||||
}
|
||||
}
|
||||
|
||||
12
client/ios/MindfulnessWidget/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# MindfulnessWidget(WidgetKit 扩展骨架)
|
||||
|
||||
本目录提供 iOS Widget(V1 写死文案)的 SwiftUI 代码骨架。
|
||||
|
||||
注意:**仅把文件放进仓库还不够**,你还需要在 Xcode 中创建 Widget Extension target,并把这些文件加入 target。
|
||||
|
||||
## 目标
|
||||
|
||||
- 支持 Small/Medium/Large 三种尺寸
|
||||
- 展示写死文案
|
||||
- 点击小组件跳转到 App 的 Home:`client:///(app)/home`
|
||||
|
||||
63
client/ios/Podfile
Normal file
@@ -0,0 +1,63 @@
|
||||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||
|
||||
require 'json'
|
||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||
|
||||
def ccache_enabled?(podfile_properties)
|
||||
# Environment variable takes precedence
|
||||
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
|
||||
|
||||
# Fall back to Podfile properties
|
||||
podfile_properties['apple.ccacheEnabled'] == 'true'
|
||||
end
|
||||
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||
|
||||
prepare_react_native_project!
|
||||
|
||||
target 'client' do
|
||||
use_expo_modules!
|
||||
|
||||
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||
else
|
||||
config_command = [
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(\'expo/bin/autolinking\')',
|
||||
'expo-modules-autolinking',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'ios'
|
||||
]
|
||||
end
|
||||
|
||||
config = use_native_modules!(config_command)
|
||||
|
||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
:ccache_enabled => ccache_enabled?(podfile_properties),
|
||||
)
|
||||
end
|
||||
end
|
||||
2347
client/ios/Podfile.lock
Normal file
5
client/ios/Podfile.properties.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"expo.jsEngine": "hermes",
|
||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||
"newArchEnabled": "true"
|
||||
}
|
||||
778
client/ios/client.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,778 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0BE245B56A79D95AB0A7B4BA /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
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, ); }; };
|
||||
EB3DAF9B2F2A4D0A00450593 /* MindfulnessWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3DAF9A2F2A4D0900450593 /* MindfulnessWidget.swift */; };
|
||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */; };
|
||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
EB3DAF922F2A4B8F00450593 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = EB3DAF7E2F2A4B8D00450593;
|
||||
remoteInfo = "情绪小组件Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
EB3DAF992F2A4B8F00450593 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
EB3DAF942F2A4B8F00450593 /* 情绪小组件Extension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = client.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; 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>"; };
|
||||
FFF632A94C7A551AAA096858 /* Pods-client.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.debug.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
EmotionWidget.swift,
|
||||
Info.plist,
|
||||
);
|
||||
target = EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
EB3DAF7C2F2A4B8D00450593 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EB3DAF832F2A4B8E00450593 /* SwiftUI.framework in Frameworks */,
|
||||
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* client */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB3DAF9A2F2A4D0900450593 /* MindfulnessWidget.swift */,
|
||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
||||
F11748442D0722820044C1D9 /* client-Bridging-Header.h */,
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = client;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
E3328F0E595C1F4A244DF238 /* libPods-client.a */,
|
||||
EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */,
|
||||
EB3DAF822F2A4B8E00450593 /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D6DC77C8D61E21DC5AFD4A8 /* client */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = client;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* client */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
9BE464729A978E7F7FF8BB61 /* Pods */,
|
||||
F7A1EA0ECA728F3AC6EE9C33 /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* client.app */,
|
||||
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9BE464729A978E7F7FF8BB61 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FFF632A94C7A551AAA096858 /* Pods-client.debug.xcconfig */,
|
||||
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */,
|
||||
);
|
||||
name = Supporting;
|
||||
path = client/Supporting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F7A1EA0ECA728F3AC6EE9C33 /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D6DC77C8D61E21DC5AFD4A8 /* client */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* client */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "client" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
A3E56FD9DDA5EAC6D80AF012 /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||
BCFE46B970C8F37BCB841413 /* [CP] Embed Pods Frameworks */,
|
||||
EB3DAF992F2A4B8F00450593 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
EB3DAF932F2A4B8F00450593 /* PBXTargetDependency */,
|
||||
);
|
||||
name = client;
|
||||
productName = client;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* client.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = EB3DAF962F2A4B8F00450593 /* Build configuration list for PBXNativeTarget "情绪小组件Extension" */;
|
||||
buildPhases = (
|
||||
EB3DAF7B2F2A4B8D00450593 /* Sources */,
|
||||
EB3DAF7C2F2A4B8D00450593 /* Frameworks */,
|
||||
EB3DAF7D2F2A4B8D00450593 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */,
|
||||
);
|
||||
name = "情绪小组件Extension";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "情绪小组件Extension";
|
||||
productReference = EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 2620;
|
||||
LastUpgradeCheck = 1130;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1250;
|
||||
};
|
||||
EB3DAF7E2F2A4B8D00450593 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "client" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* client */,
|
||||
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
0BE245B56A79D95AB0A7B4BA /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
EB3DAF7D2F2A4B8D00450593 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
||||
};
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-client-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-client/Pods-client-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||
"${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}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||
"${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}/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}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-client/Pods-client-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A3E56FD9DDA5EAC6D80AF012 /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/client/client.entitlements",
|
||||
"$(SRCROOT)/Pods/Target Support Files/Pods-client/expo-configure-project.sh",
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(SRCROOT)/Pods/Target Support Files/Pods-client/ExpoModulesProvider.swift",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-client/expo-configure-project.sh\"\n";
|
||||
};
|
||||
BCFE46B970C8F37BCB841413 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-client/Pods-client-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-client/Pods-client-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
||||
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
EB3DAF7B2F2A4B8D00450593 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
EB3DAF932F2A4B8F00450593 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */;
|
||||
targetProxy = EB3DAF922F2A4B8F00450593 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FFF632A94C7A551AAA096858 /* Pods-client.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"FB_SONARKIT_ENABLED=1",
|
||||
);
|
||||
INFOPLIST_FILE = client/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client;
|
||||
PRODUCT_NAME = client;
|
||||
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_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = client/client.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = client/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client;
|
||||
PRODUCT_NAME = client;
|
||||
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";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
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";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(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";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
EB3DAF972F2A4B8F00450593 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "情绪小组件/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "情绪小组件";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.client.-----";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
EB3DAF982F2A4B8F00450593 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "情绪小组件/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "情绪小组件";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.client.-----";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "client" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "client" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
EB3DAF962F2A4B8F00450593 /* Build configuration list for PBXNativeTarget "情绪小组件Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
EB3DAF972F2A4B8F00450593 /* Debug */,
|
||||
EB3DAF982F2A4B8F00450593 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
7
client/ios/client.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "client.app"
|
||||
BlueprintName = "client"
|
||||
ReferencedContainer = "container:client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
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>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "client.app"
|
||||
BlueprintName = "client"
|
||||
ReferencedContainer = "container:client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "client.app"
|
||||
BlueprintName = "client"
|
||||
ReferencedContainer = "container:client.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
client/ios/client.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:client.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
70
client/ios/client/AppDelegate.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Expo
|
||||
import React
|
||||
import ReactAppDependencyProvider
|
||||
|
||||
@UIApplicationMain
|
||||
public class AppDelegate: ExpoAppDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
||||
var reactNativeFactory: RCTReactNativeFactory?
|
||||
|
||||
public override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = ExpoReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
|
||||
reactNativeDelegate = delegate
|
||||
reactNativeFactory = factory
|
||||
bindReactNativeFactory(factory)
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
factory.startReactNative(
|
||||
withModuleName: "main",
|
||||
in: window,
|
||||
launchOptions: launchOptions)
|
||||
#endif
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
// Linking API
|
||||
public override func application(
|
||||
_ app: UIApplication,
|
||||
open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
// Universal Links
|
||||
public override func application(
|
||||
_ application: UIApplication,
|
||||
continue userActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||||
) -> Bool {
|
||||
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
|
||||
}
|
||||
}
|
||||
|
||||
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
|
||||
// Extension point for config-plugins
|
||||
|
||||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||
// needed to return the correct URL for expo-dev-client.
|
||||
bridge.bundleURL ?? bundleURL()
|
||||
}
|
||||
|
||||
override func bundleURL() -> URL? {
|
||||
#if DEBUG
|
||||
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
||||
#else
|
||||
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "App-Icon-1024x1024@1x.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
6
client/ios/client/Images.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "expo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"components": {
|
||||
"alpha": "1.000",
|
||||
"blue": "1.00000000000000",
|
||||
"green": "1.00000000000000",
|
||||
"red": "1.00000000000000"
|
||||
},
|
||||
"color-space": "srgb"
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
23
client/ios/client/Images.xcassets/SplashScreenLegacy.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "image.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "image@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "image@3x.png",
|
||||
"scale": "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
BIN
client/ios/client/Images.xcassets/SplashScreenLegacy.imageset/image.png
vendored
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
client/ios/client/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
client/ios/client/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 59 KiB |
81
client/ios/client/Info.plist
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>client</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>client</string>
|
||||
<string>com.anonymous.client</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
48
client/ios/client/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
<string>0A2A.1</string>
|
||||
<string>3B52.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
<string>85F4.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
48
client/ios/client/SplashScreen.storyboard
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24053.1"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<scene sceneID="EXPO-SCENE-1">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
|
||||
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLegacy" image="SplashScreenLegacy" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
|
||||
<rect key="frame" x="0" y="0" width="414" height="736"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
||||
<constraints>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
|
||||
</constraints>
|
||||
<color key="backgroundColor" name="SplashScreenBackground"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="SplashScreenLegacy" width="414" height="736"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<namedColor name="SplashScreenBackground">
|
||||
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
12
client/ios/client/Supporting/Expo.plist
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<false/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
3
client/ios/client/client-Bridging-Header.h
Normal file
@@ -0,0 +1,3 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
8
client/ios/client/client.entitlements
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
18
client/ios/情绪小组件/AppIntent.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// AppIntent.swift
|
||||
// 情绪小组件
|
||||
//
|
||||
// Created by jojo on 2026/1/28.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
|
||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Configuration" }
|
||||
static var description: IntentDescription { "This is an example widget." }
|
||||
|
||||
// An example configurable parameter.
|
||||
@Parameter(title: "Favorite Emoji", default: "😃")
|
||||
var favoriteEmoji: String
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
client/ios/情绪小组件/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
131
client/ios/情绪小组件/EmotionWidget.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// V1:写死文案的小组件(Small/Medium/Large + 点击跳转 Home)
|
||||
|
||||
struct EmotionProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> EmotionEntry {
|
||||
EmotionEntry(date: Date())
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) {
|
||||
completion(EmotionEntry(date: Date()))
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<EmotionEntry>) -> ()) {
|
||||
// V1:内容写死,不做数据更新;给一个较长的刷新间隔(系统仍可能自行调度)
|
||||
let entry = EmotionEntry(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 EmotionEntry: TimelineEntry {
|
||||
let date: Date
|
||||
}
|
||||
|
||||
struct EmotionWidgetView: View {
|
||||
var entry: EmotionProvider.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 EmotionWidget: Widget {
|
||||
let kind: String = "EmotionWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
|
||||
EmotionWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("情绪小组件")
|
||||
.description("一段温柔提醒,陪你回到当下。")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||||
}
|
||||
}
|
||||
|
||||
11
client/ios/情绪小组件/Info.plist
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
43
client/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "client",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"postinstall": "node ./scripts/postinstall-fix-metro.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"expo": "~54.0.32",
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-font": "~14.0.11",
|
||||
"expo-linking": "~8.0.11",
|
||||
"expo-localization": "^17.0.8",
|
||||
"expo-notifications": "^0.32.16",
|
||||
"expo-router": "~6.0.22",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"i18next": "^25.8.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
7115
client/pnpm-lock.yaml
generated
Normal file
33
client/src/constants/env.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 客户端环境变量统一入口(Expo 推荐使用 EXPO_PUBLIC_ 前缀)
|
||||
*
|
||||
* 注意:
|
||||
* - 这里读取的是构建时/运行时注入的环境变量(EXPO_PUBLIC_*)
|
||||
* - 真机联调时不要用 localhost,改为电脑局域网 IP,例如:http://192.168.1.10:8000
|
||||
*/
|
||||
|
||||
export type AppEnv = 'dev' | 'prod';
|
||||
|
||||
function getRequiredEnv(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`缺少环境变量:${name}(请检查 .env 配置)`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getOptionalEnv(name: string, fallback: string): string {
|
||||
return process.env[name] ?? fallback;
|
||||
}
|
||||
|
||||
export const APP_ENV = (getOptionalEnv('EXPO_PUBLIC_ENV', 'dev') as AppEnv) ?? 'dev';
|
||||
|
||||
export const API_BASE_URL = getRequiredEnv('EXPO_PUBLIC_API_BASE_URL');
|
||||
|
||||
/**
|
||||
* 默认语言策略:
|
||||
* - auto:优先设备语言(支持列表内时),否则回退 zh-CN
|
||||
* - zh-CN/en/es/pt/zh-TW:固定默认语言(仍允许用户在设置中手动切换并持久化)
|
||||
*/
|
||||
export const DEFAULT_LANGUAGE = getOptionalEnv('EXPO_PUBLIC_DEFAULT_LANGUAGE', 'auto');
|
||||
|
||||
16
client/src/constants/mockContent.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type MockContentItem = {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地 mock 内容(后续接后端时可替换)
|
||||
*/
|
||||
export const MOCK_CONTENT: MockContentItem[] = [
|
||||
{ id: 'c1', text: '你已经很努力了,今天也值得被温柔对待。' },
|
||||
{ id: 'c2', text: '深呼吸三次,把注意力带回当下。' },
|
||||
{ id: 'c3', text: '允许自己慢一点,情绪会像云一样飘过。' },
|
||||
{ id: 'c4', text: '你不需要完美,你已经足够好。' },
|
||||
{ id: 'c5', text: '把手放在心口,对自己说一句:辛苦了。' }
|
||||
];
|
||||
|
||||
123
client/src/i18n/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as Localization from 'expo-localization';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import en from './locales/en.json';
|
||||
import es from './locales/es.json';
|
||||
import pt from './locales/pt.json';
|
||||
import zhCN from './locales/zh-CN.json';
|
||||
import zhTW from './locales/zh-TW.json';
|
||||
|
||||
/**
|
||||
* 语言码约定:
|
||||
* - 简体中文:zh-CN
|
||||
* - 繁体中文:zh-TW
|
||||
* - 英语:en
|
||||
* - 西班牙语:es
|
||||
* - 葡萄牙语:pt
|
||||
*/
|
||||
export type AppLanguage = 'zh-CN' | 'zh-TW' | 'en' | 'es' | 'pt';
|
||||
|
||||
export const SUPPORTED_LANGUAGES: readonly AppLanguage[] = [
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'en',
|
||||
'es',
|
||||
'pt',
|
||||
] as const;
|
||||
|
||||
const DEFAULT_FALLBACK_LANGUAGE: AppLanguage = 'zh-CN';
|
||||
const STORAGE_KEY_LANGUAGE = 'settings.language';
|
||||
|
||||
function isSupportedLanguage(lang: string): lang is AppLanguage {
|
||||
return (SUPPORTED_LANGUAGES as readonly string[]).includes(lang);
|
||||
}
|
||||
|
||||
function normalizeDeviceLanguageTagToAppLanguage(languageTag: string): AppLanguage {
|
||||
const tag = languageTag.toLowerCase();
|
||||
|
||||
// 中文:优先区分繁简
|
||||
if (tag.startsWith('zh')) {
|
||||
// 常见繁体标记:zh-TW / zh-HK / zh-Hant
|
||||
if (tag.includes('tw') || tag.includes('hk') || tag.includes('hant')) {
|
||||
return 'zh-TW';
|
||||
}
|
||||
return 'zh-CN';
|
||||
}
|
||||
|
||||
// 其他语言:按前缀匹配
|
||||
if (tag.startsWith('en')) return 'en';
|
||||
if (tag.startsWith('es')) return 'es';
|
||||
if (tag.startsWith('pt')) return 'pt';
|
||||
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
function getDeviceLanguage(): AppLanguage {
|
||||
// expo-localization 返回系统 locale 列表,取第一个作为当前设备偏好
|
||||
const locales = Localization.getLocales();
|
||||
const first = locales?.[0]?.languageTag;
|
||||
|
||||
if (first) {
|
||||
return normalizeDeviceLanguageTagToAppLanguage(first);
|
||||
}
|
||||
|
||||
// 兜底:少数情况下 locales 为空
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
export async function getLanguagePreference(): Promise<AppLanguage | null> {
|
||||
const value = await AsyncStorage.getItem(STORAGE_KEY_LANGUAGE);
|
||||
if (!value) return null;
|
||||
return isSupportedLanguage(value) ? value : null;
|
||||
}
|
||||
|
||||
export async function setLanguagePreference(lang: AppLanguage): Promise<void> {
|
||||
await AsyncStorage.setItem(STORAGE_KEY_LANGUAGE, lang);
|
||||
}
|
||||
|
||||
export async function clearLanguagePreference(): Promise<void> {
|
||||
await AsyncStorage.removeItem(STORAGE_KEY_LANGUAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 i18n(只会初始化一次)
|
||||
*
|
||||
* 语言选择优先级:
|
||||
* 1) 用户设置(若存在)
|
||||
* 2) 设备语言(在支持列表内时生效;否则会被 normalize 到默认回退)
|
||||
* 3) 默认回退(zh-CN)
|
||||
*/
|
||||
export async function initI18n(): Promise<void> {
|
||||
if (i18n.isInitialized) return;
|
||||
|
||||
const userLang = await getLanguagePreference();
|
||||
const deviceLang = getDeviceLanguage();
|
||||
const initialLang = userLang ?? deviceLang;
|
||||
|
||||
await i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
'zh-CN': { translation: zhCN },
|
||||
'zh-TW': { translation: zhTW },
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
pt: { translation: pt },
|
||||
},
|
||||
lng: initialLang,
|
||||
fallbackLng: DEFAULT_FALLBACK_LANGUAGE,
|
||||
interpolation: {
|
||||
// React Native 不需要对内容做 HTML 转义
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户手动切换语言:立即生效 + 持久化
|
||||
*/
|
||||
export async function changeLanguage(lang: AppLanguage): Promise<void> {
|
||||
await setLanguagePreference(lang);
|
||||
await i18n.changeLanguage(lang);
|
||||
}
|
||||
|
||||
50
client/src/i18n/locales/en.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Welcome",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Next",
|
||||
"skip": "Skip",
|
||||
"skipAll": "Skip onboarding",
|
||||
"q1Title": "How are you feeling lately?",
|
||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||
"q2Title": "What kind of support do you want?",
|
||||
"q2Desc": "For example: gentle reminders, mindfulness, emotional support.",
|
||||
"q3Title": "When do you need comfort the most?",
|
||||
"q3Desc": "Morning, afternoon, late night, or specific moments.",
|
||||
"q4Title": "A gentle sentence for yourself",
|
||||
"q4Desc": "You can skip. We’ll stay with you along the way."
|
||||
},
|
||||
"push": {
|
||||
"title": "Notifications",
|
||||
"cardTitle": "Turn on gentle reminders",
|
||||
"cardDesc": "We’ll send a short mindful phrase when you may need it. You can change this anytime in Settings.",
|
||||
"enable": "Enable",
|
||||
"later": "Later",
|
||||
"loading": "Working…",
|
||||
"errorTitle": "Notice",
|
||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"like": "Like",
|
||||
"dislike": "Dislike",
|
||||
"favorites": "Favorites",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites",
|
||||
"empty": "No favorites yet."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"widgetTitle": "iOS Widget",
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Mindfulness” → add a size you like."
|
||||
}
|
||||
}
|
||||
|
||||
50
client/src/i18n/locales/es.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "Aceptar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Bienvenida",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Siguiente",
|
||||
"skip": "Saltar",
|
||||
"skipAll": "Saltar introducción",
|
||||
"q1Title": "¿Cómo te sientes últimamente?",
|
||||
"q1Desc": "No hay respuestas correctas. Puedes saltar y ajustar después.",
|
||||
"q2Title": "¿Qué tipo de apoyo quieres?",
|
||||
"q2Desc": "Por ejemplo: recordatorios suaves, mindfulness, apoyo emocional.",
|
||||
"q3Title": "¿Cuándo necesitas más consuelo?",
|
||||
"q3Desc": "Mañana, tarde, noche o momentos específicos.",
|
||||
"q4Title": "Una frase amable para ti",
|
||||
"q4Desc": "Puedes saltar. Te acompañaremos en el camino."
|
||||
},
|
||||
"push": {
|
||||
"title": "Notificaciones",
|
||||
"cardTitle": "Activar recordatorios suaves",
|
||||
"cardDesc": "Te enviaremos una frase breve cuando lo necesites. Puedes cambiarlo en Ajustes.",
|
||||
"enable": "Activar",
|
||||
"later": "Más tarde",
|
||||
"loading": "Procesando…",
|
||||
"errorTitle": "Aviso",
|
||||
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"like": "Me gusta",
|
||||
"dislike": "No me gusta",
|
||||
"favorites": "Favoritos",
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favoritos",
|
||||
"empty": "Aún no hay favoritos."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ajustes",
|
||||
"language": "Idioma",
|
||||
"version": "Versión",
|
||||
"widgetTitle": "Widget de iOS",
|
||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Mindfulness” → añade el tamaño."
|
||||
}
|
||||
}
|
||||
|
||||
50
client/src/i18n/locales/pt.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "OK",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Bem-vinda",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Próximo",
|
||||
"skip": "Pular",
|
||||
"skipAll": "Pular introdução",
|
||||
"q1Title": "Como você tem se sentido ultimamente?",
|
||||
"q1Desc": "Não há certo ou errado. Você pode pular e ajustar depois.",
|
||||
"q2Title": "Que tipo de apoio você quer?",
|
||||
"q2Desc": "Por exemplo: lembretes gentis, mindfulness, apoio emocional.",
|
||||
"q3Title": "Quando você mais precisa de conforto?",
|
||||
"q3Desc": "Manhã, tarde, noite ou momentos específicos.",
|
||||
"q4Title": "Uma frase gentil para você",
|
||||
"q4Desc": "Você pode pular. Vamos seguir com você no caminho."
|
||||
},
|
||||
"push": {
|
||||
"title": "Notificações",
|
||||
"cardTitle": "Ativar lembretes gentis",
|
||||
"cardDesc": "Enviaremos uma frase curta quando você precisar. Você pode mudar isso em Configurações.",
|
||||
"enable": "Ativar",
|
||||
"later": "Depois",
|
||||
"loading": "Processando…",
|
||||
"errorTitle": "Aviso",
|
||||
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"like": "Curtir",
|
||||
"dislike": "Não curtir",
|
||||
"favorites": "Favoritos",
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favoritos",
|
||||
"empty": "Ainda não há favoritos."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"language": "Idioma",
|
||||
"version": "Versão",
|
||||
"widgetTitle": "Widget do iOS",
|
||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Mindfulness” → adicione o tamanho."
|
||||
}
|
||||
}
|
||||
|
||||
50
client/src/i18n/locales/zh-CN.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "确定",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "欢迎",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳过",
|
||||
"skipAll": "跳过整个引导",
|
||||
"q1Title": "你最近的感受更接近哪一种?",
|
||||
"q1Desc": "没有对错,你可以跳过,之后也可以慢慢调整。",
|
||||
"q2Title": "你更希望获得哪种支持?",
|
||||
"q2Desc": "例如:温柔提醒、正念练习、情绪陪伴。",
|
||||
"q3Title": "你通常在什么时候最需要被安慰?",
|
||||
"q3Desc": "比如:清晨、午后、深夜,或某些特定时刻。",
|
||||
"q4Title": "给自己一句温柔的话",
|
||||
"q4Desc": "你可以直接跳过,我们会在之后继续陪你。"
|
||||
},
|
||||
"push": {
|
||||
"title": "通知",
|
||||
"cardTitle": "开启温柔提醒",
|
||||
"cardDesc": "我们会在你需要的时候,送上一句正念短句或温柔提醒(可随时在设置中调整)。",
|
||||
"enable": "立即开启",
|
||||
"later": "稍后",
|
||||
"loading": "处理中…",
|
||||
"errorTitle": "提示",
|
||||
"errorDesc": "开启失败也没关系,你仍然可以继续使用应用。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"like": "点赞",
|
||||
"dislike": "讨厌",
|
||||
"favorites": "收藏",
|
||||
"settings": "设置"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "收藏夹",
|
||||
"empty": "这里还没有收藏内容。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"language": "语言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小组件",
|
||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“正念” → 添加你喜欢的尺寸。"
|
||||
}
|
||||
}
|
||||
|
||||
50
client/src/i18n/locales/zh-TW.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "確定",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "歡迎",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳過",
|
||||
"skipAll": "跳過整個引導",
|
||||
"q1Title": "你最近的感受更接近哪一種?",
|
||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||
"q2Title": "你更希望獲得哪種支持?",
|
||||
"q2Desc": "例如:溫柔提醒、正念練習、情緒陪伴。",
|
||||
"q3Title": "你通常在什麼時候最需要被安慰?",
|
||||
"q3Desc": "例如:清晨、午后、深夜,或某些特定時刻。",
|
||||
"q4Title": "給自己一句溫柔的話",
|
||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
||||
},
|
||||
"push": {
|
||||
"title": "通知",
|
||||
"cardTitle": "開啟溫柔提醒",
|
||||
"cardDesc": "我們會在你需要的時候,送上一句正念短句或溫柔提醒(可隨時在設定中調整)。",
|
||||
"enable": "立即開啟",
|
||||
"later": "稍後",
|
||||
"loading": "處理中…",
|
||||
"errorTitle": "提示",
|
||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"like": "喜歡",
|
||||
"dislike": "不喜歡",
|
||||
"favorites": "收藏",
|
||||
"settings": "設定"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "收藏夾",
|
||||
"empty": "這裡還沒有收藏內容。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"language": "語言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小工具",
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
}
|
||||
}
|
||||
|
||||
67
client/src/storage/appStorage.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
/**
|
||||
* 本地存储 key 统一管理,避免 UI 里散落硬编码
|
||||
*/
|
||||
const KEY_ONBOARDING_COMPLETED = 'onboarding.completed';
|
||||
const KEY_PUSH_PROMPT_STATE = 'push.promptState';
|
||||
const KEY_CONTENT_REACTIONS = 'content.reactions';
|
||||
const KEY_FAVORITES_ITEMS = 'favorites.items';
|
||||
|
||||
export type PushPromptState = 'enabled' | 'skipped' | 'unknown';
|
||||
export type Reaction = 'like' | 'dislike';
|
||||
export type ReactionsMap = Record<string, Reaction>;
|
||||
|
||||
async function getJson<T>(key: string, fallback: T): Promise<T> {
|
||||
const raw = await AsyncStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function setJson<T>(key: string, value: T): Promise<void> {
|
||||
await AsyncStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
export async function getOnboardingCompleted(): Promise<boolean> {
|
||||
const raw = await AsyncStorage.getItem(KEY_ONBOARDING_COMPLETED);
|
||||
return raw === 'true';
|
||||
}
|
||||
|
||||
export async function setOnboardingCompleted(completed: boolean): Promise<void> {
|
||||
await AsyncStorage.setItem(KEY_ONBOARDING_COMPLETED, completed ? 'true' : 'false');
|
||||
}
|
||||
|
||||
export async function getPushPromptState(): Promise<PushPromptState> {
|
||||
const raw = await AsyncStorage.getItem(KEY_PUSH_PROMPT_STATE);
|
||||
if (raw === 'enabled' || raw === 'skipped' || raw === 'unknown') return raw;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export async function setPushPromptState(state: PushPromptState): Promise<void> {
|
||||
await AsyncStorage.setItem(KEY_PUSH_PROMPT_STATE, state);
|
||||
}
|
||||
|
||||
export async function getReactions(): Promise<ReactionsMap> {
|
||||
return await getJson<ReactionsMap>(KEY_CONTENT_REACTIONS, {});
|
||||
}
|
||||
|
||||
export async function setReaction(contentId: string, reaction: Reaction): Promise<void> {
|
||||
const next = await getReactions();
|
||||
next[contentId] = reaction;
|
||||
await setJson(KEY_CONTENT_REACTIONS, next);
|
||||
}
|
||||
|
||||
export async function getFavorites(): Promise<string[]> {
|
||||
return await getJson<string[]>(KEY_FAVORITES_ITEMS, []);
|
||||
}
|
||||
|
||||
export async function addFavorite(contentId: string): Promise<void> {
|
||||
const list = await getFavorites();
|
||||
if (list.includes(contentId)) return;
|
||||
await setJson(KEY_FAVORITES_ITEMS, [...list, contentId]);
|
||||
}
|
||||
|
||||
17
client/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
}
|
||||