Skip to content

Commit 190f4bb

Browse files
authored
Merge pull request #9 from Tonnodoubt/mobile-app-v1
收口移动端 v1
2 parents 90a33f6 + d981d90 commit 190f4bb

113 files changed

Lines changed: 13004 additions & 6720 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
name: "source-command-build-debug-apk"
3+
description: "编译 Android debug APK,使用 Gradle 本地构建,不启动模拟器"
4+
---
5+
6+
# source-command-build-debug-apk
7+
8+
Use this skill when the user asks to run the migrated source command `build-debug-apk`.
9+
10+
## Command Template
11+
12+
# /build-debug-apk — 编译 Debug APK
13+
14+
使用 Gradle 本地编译 Android debug APK。
15+
16+
## 执行流程
17+
18+
### 1. 检查项目配置
19+
20+
确认以下文件存在:
21+
- `android/gradlew` — Gradle wrapper
22+
- `android/app/build.gradle` — 应用构建配置
23+
- `eas.json` — EAS 配置(可选)
24+
25+
### 2. 执行编译
26+
27+
```bash
28+
cd android && ./gradlew assembleDebug --no-daemon
29+
```
30+
31+
### 3. 验证输出
32+
33+
检查 APK 文件是否生成:
34+
- 标准路径:`android/app/build/outputs/apk/debug/app-debug.apk`
35+
36+
### 4. 输出结果
37+
38+
显示:
39+
- APK 文件路径
40+
- 文件大小
41+
- 安装命令示例
42+
43+
## 输出格式
44+
45+
```
46+
✅ Debug APK 编译成功!
47+
48+
路径:android/app/build/outputs/apk/debug/app-debug.apk
49+
大小:XXX MB
50+
51+
安装到设备:
52+
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
53+
```
54+
55+
## 注意事项
56+
57+
- 首次编译可能需要下载依赖,耗时较长
58+
- 确保已安装 Android SDK 和 Gradle
59+
- 如需清理后重新编译,使用 `./gradlew clean` 后再 assembleDebug
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
name: "source-command-review"
3+
description: "提交前代码审查。检查 staged 变更的质量、架构规范、RN 最佳实践,输出问题列表。在 /commit 前运行以保证代码质量。"
4+
---
5+
6+
# source-command-review
7+
8+
Use this skill when the user asks to run the migrated source command `review`.
9+
10+
## Command Template
11+
12+
# /review — 提交前代码审查
13+
14+
对即将提交的变更做系统性检查,避免把问题带入 commit 历史。
15+
16+
## 执行步骤
17+
18+
### 1. 收集变更上下文
19+
20+
```bash
21+
git status # 查看 staged / unstaged 文件列表
22+
git diff --staged # 分析 staged 变更内容
23+
git diff # 确认是否有未暂存的相关变更
24+
```
25+
26+
### 2. 运行 Lint
27+
28+
```bash
29+
npx expo lint
30+
```
31+
32+
如有错误,先列出并说明修复方向,再继续后续检查。
33+
34+
### 3. 逐项检查
35+
36+
对每个 staged 文件按下列维度审查,只报告**实际存在的问题**,没有问题的维度跳过。
37+
38+
#### A. TypeScript 类型安全
39+
- 有无 `any` 类型滥用(尤其是 API 响应、事件回调)
40+
- 有无缺失 `undefined` / `null` 处理
41+
- 泛型使用是否合理
42+
43+
#### B. React Native 最佳实践
44+
- StyleSheet 是否在组件外定义(避免每次渲染重建对象)
45+
- 平台差异处理:`Platform.OS` 分支、`.android.tsx` / `.ios.tsx` 文件分离
46+
- `FlatList` 是否提供 `keyExtractor`;长列表是否启用 `getItemLayout`
47+
- 异步操作是否在组件卸载后有 cleanup(防止 `setState` on unmounted)
48+
- 图片资源是否使用 `require()` 而非网络 URI 直接展示静态资产
49+
50+
#### C. 项目架构规范(`docs/arch/design.md`
51+
- **Service 层**`services/`):不得直接依赖 React 组件生命周期;纯 TS Class,无 hooks 调用
52+
- **Hook 桥接层**`hooks/`):不得包含业务逻辑,只做事件→状态映射
53+
- **Manager 单例**(MainManager):跨 Service 协调逻辑统一放在 Manager,不散落到 UI 层
54+
- `components/` 不应直接 import Service 实例,应通过 Hook 消费
55+
56+
#### D. 包依赖边界
57+
- `@project_neko/*` 内部包的修改是否需要同步运行 `npm run sync:neko-packages`
58+
- 有无引入新的外部依赖但未更新 `package.json`
59+
60+
#### E. 遗漏项
61+
- 修改了功能但没有更新对应的 `docs/` 文档
62+
- 有无调试用的 `console.log` / `TODO` 未清理
63+
- 新增文件是否加入了正确的 TypeScript path alias
64+
65+
### 4. 输出格式
66+
67+
```
68+
## 审查结果
69+
70+
### Lint
71+
✅ 无错误 / ❌ N 个错误(列出文件和行号)
72+
73+
### 问题列表
74+
75+
🔴 [严重] services/AudioService.ts:42
76+
直接在 Service 内调用了 useState,违反 Service 层规范
77+
建议:将状态提升到 useAudio hook
78+
79+
🟡 [警告] components/VoiceButton.tsx:18
80+
StyleSheet 定义在组件函数内部,每次渲染会重建
81+
建议:移到组件外顶层定义
82+
83+
🔵 [建议] hooks/useLive2D.ts:95
84+
缺少卸载时的 cleanup,可能导致内存泄漏
85+
86+
### 结论
87+
88+
⛔ 建议修复严重问题后再提交 / ✅ 可以提交(附注意事项)
89+
```
90+
91+
严重性说明:
92+
- 🔴 严重:架构违规、潜在 crash、类型不安全
93+
- 🟡 警告:性能问题、代码质量问题
94+
- 🔵 建议:可选优化,不阻断提交

.gitmodules

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[submodule "packages/react-native-pcm-stream"]
22
path = packages/react-native-pcm-stream
3-
url = https://github.com/noahzaozao/react-native-pcm-stream.git
3+
url = https://github.com/Tonnodoubt/react-native-pcm-stream.git
44
[submodule "packages/react-native-live2d"]
55
path = packages/react-native-live2d
6-
url = https://github.com/noahzaozao/react-native-live2d.git
6+
url = https://github.com/Tonnodoubt/react-native-live2d.git

AGENTS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# N.E.K.O.-RN
2+
3+
## Build
4+
5+
```bash
6+
# 编译 debug APK(唯一方式,不要用 expo run:android)
7+
cd android && ./gradlew assembleDebug
8+
```
9+
10+
APK 输出:`android/app/build/outputs/apk/debug/app-debug.apk`
11+
12+
## 项目结构
13+
14+
- `app/` — Expo Router 页面
15+
- `components/` — 通用 UI 组件
16+
- `services/` — 业务服务层
17+
- `hooks/` — React hooks
18+
- `packages/` — 原生模块(react-native-pcm-stream, react-native-live2d 等)
19+
- `android/` — Android 原生工程
20+
- `i18n/` — 国际化
21+
22+
## 关键约定
23+
24+
- 嘴形同步:JS 层做平滑(LipSyncService),Kotlin 层做时间对齐(PCMStreamPlayer 按 playbackHeadPosition 取振幅)
25+
- 修改 Kotlin 原生代码后需要 `./gradlew assembleDebug` 重新编译,Metro 热更新不生效

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# N.E.K.O.-RN
2+
3+
## Build
4+
5+
```bash
6+
# 编译 debug APK(唯一方式,不要用 expo run:android)
7+
cd android && ./gradlew assembleDebug
8+
```
9+
10+
APK 输出:`android/app/build/outputs/apk/debug/app-debug.apk`
11+
12+
## 项目结构
13+
14+
- `app/` — Expo Router 页面
15+
- `components/` — 通用 UI 组件
16+
- `services/` — 业务服务层
17+
- `hooks/` — React hooks
18+
- `packages/` — 原生模块(react-native-pcm-stream, react-native-live2d 等)
19+
- `android/` — Android 原生工程
20+
- `i18n/` — 国际化
21+
22+
## 关键约定
23+
24+
- 嘴形同步:JS 层做平滑(LipSyncService),Kotlin 层做时间对齐(PCMStreamPlayer 按 playbackHeadPosition 取振幅)
25+
- 修改 Kotlin 原生代码后需要 `./gradlew assembleDebug` 重新编译,Metro 热更新不生效

app/(tabs)/_layout.tsx

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,102 @@
11
import { Tabs } from 'expo-router';
22
import React from 'react';
33
import { Ionicons } from '@expo/vector-icons';
4+
import { Platform, StyleSheet, TouchableOpacity, View, Text } from 'react-native';
5+
import { BlurView } from 'expo-blur';
6+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
7+
import * as Haptics from 'expo-haptics';
48

5-
import { HapticTab } from '@/components/haptic-tab';
6-
import { Colors } from '@/constants/theme';
7-
import { useColorScheme } from '@/hooks/use-color-scheme';
9+
import { useTheme } from '@/constants/ThemeContext';
10+
11+
function GlassTabBar({ state, descriptors, navigation }: any) {
12+
const theme = useTheme();
13+
const cc = theme.colors;
14+
const insets = useSafeAreaInsets();
15+
16+
return (
17+
<BlurView
18+
intensity={theme.isDark ? 40 : 60}
19+
tint={theme.isDark ? 'dark' : 'light'}
20+
style={[
21+
styles.tabBar,
22+
{
23+
paddingBottom: Math.max(insets.bottom, 8),
24+
borderColor: cc.border,
25+
},
26+
]}
27+
>
28+
{state.routes.map((route: any, index: number) => {
29+
const { options } = descriptors[route.key];
30+
const isFocused = state.index === index;
31+
const label = options.title ?? route.name;
32+
const Icon = options.tabBarIcon;
33+
34+
const onPress = () => {
35+
if (Platform.OS === 'ios') {
36+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
37+
}
38+
const event = navigation.emit({ type: 'tabPress', target: route.key, canPreventDefault: true });
39+
if (!isFocused && !event.defaultPrevented) {
40+
navigation.navigate(route.name);
41+
}
42+
};
43+
44+
return (
45+
<TouchableOpacity
46+
key={route.key}
47+
onPress={onPress}
48+
style={[
49+
styles.tabItem,
50+
isFocused && { backgroundColor: cc.accentSoft },
51+
]}
52+
activeOpacity={0.7}
53+
>
54+
{Icon && <Icon size={22} color={isFocused ? cc.accent : cc.textMuted} />}
55+
<Text
56+
style={[
57+
styles.tabLabel,
58+
{ color: isFocused ? cc.accent : cc.textMuted },
59+
]}
60+
>
61+
{label}
62+
</Text>
63+
</TouchableOpacity>
64+
);
65+
})}
66+
</BlurView>
67+
);
68+
}
69+
70+
const styles = StyleSheet.create({
71+
tabBar: {
72+
flexDirection: 'row',
73+
borderTopWidth: 1,
74+
paddingTop: 6,
75+
},
76+
tabItem: {
77+
flex: 1,
78+
alignItems: 'center',
79+
justifyContent: 'center',
80+
gap: 3,
81+
paddingVertical: 6,
82+
borderRadius: 12,
83+
},
84+
tabLabel: {
85+
fontSize: 11,
86+
fontWeight: '500',
87+
},
88+
});
889

990
export default function TabLayout() {
10-
const colorScheme = useColorScheme();
91+
const theme = useTheme();
1192

1293
return (
1394
<Tabs
95+
tabBar={(props) => <GlassTabBar {...props} />}
1496
screenOptions={{
15-
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
1697
headerShown: false,
17-
tabBarButton: HapticTab,
18-
}}>
98+
}}
99+
>
19100
<Tabs.Screen
20101
name="index"
21102
options={{

0 commit comments

Comments
 (0)