fix: auto repair bugs #80
This commit is contained in:
parent
518c6a26e4
commit
ce249058f2
294
airhub_app/CLAUDE.md
Normal file
294
airhub_app/CLAUDE.md
Normal file
@ -0,0 +1,294 @@
|
||||
# Airhub App 开发规范
|
||||
|
||||
> 所有新功能开发、代码修改、Bug 修复均需遵循本规范
|
||||
|
||||
---
|
||||
|
||||
## 一、项目架构
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # 基础设施层(全局共享)
|
||||
│ ├── errors/ # 异常类 & Failure 类
|
||||
│ ├── network/ # ApiClient, TokenManager, ApiConfig
|
||||
│ ├── router/ # go_router 路由配置
|
||||
│ └── services/ # 全局服务(LogCenter 等)
|
||||
├── features/ # 功能模块(按业务领域划分)
|
||||
│ ├── auth/ # 认证
|
||||
│ ├── badge/ # 电子吧唧
|
||||
│ ├── device/ # 设备管理
|
||||
│ ├── notification/ # 通知
|
||||
│ ├── spirit/ # 精灵/角色
|
||||
│ ├── user/ # 用户
|
||||
│ └── system/ # 系统(版本检查等)
|
||||
├── pages/ # 独立页面(未归入 feature 的页面)
|
||||
├── widgets/ # 全局可复用 Widget
|
||||
├── theme/ # 主题、颜色、Design Tokens
|
||||
├── services/ # 应用级服务(TTS、音乐生成等)
|
||||
└── main.dart # 入口
|
||||
```
|
||||
|
||||
### Feature 模块内部结构
|
||||
|
||||
```
|
||||
features/xxx/
|
||||
├── data/
|
||||
│ ├── datasources/ # 远程/本地数据源
|
||||
│ ├── models/ # 数据模型(Freezed)
|
||||
│ └── repositories/ # Repository 实现
|
||||
├── domain/
|
||||
│ └── repositories/ # Repository 接口
|
||||
└── presentation/
|
||||
├── controllers/ # Riverpod 控制器
|
||||
├── pages/ # 页面
|
||||
└── widgets/ # 功能专属 Widget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、状态管理(Riverpod)
|
||||
|
||||
### 规则
|
||||
|
||||
1. **使用 `@riverpod` 注解**声明 Provider,不手写 `StateNotifierProvider` 等
|
||||
2. Controller 使用 `AsyncValue<T>` 管理异步状态
|
||||
3. Repository 返回 `Either<Failure, T>`(fpdart),Controller 内 `fold` 处理
|
||||
4. **页面中优先使用 `ConsumerWidget`**(无状态);需要动画/本地状态时用 `ConsumerStatefulWidget`
|
||||
5. **`ref.watch()` 用于 UI 绑定**(触发重建),**`ref.read()` 用于事件处理**(不触发重建)
|
||||
6. **`ref.listen()` 用于副作用**(弹窗、导航、Toast),不触发 Widget 重建
|
||||
|
||||
### 示例
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class MyController extends _$MyController {
|
||||
@override
|
||||
FutureOr<MyState> build() async {
|
||||
final repo = ref.watch(myRepositoryProvider);
|
||||
final result = await repo.fetchData();
|
||||
return result.fold(
|
||||
(failure) => throw failure,
|
||||
(data) => MyState(data: data),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、网络层
|
||||
|
||||
### 规则
|
||||
|
||||
1. 所有 API 调用通过 `ApiClient`(Dio 封装),不直接使用 `http` 包或 `Dio()`
|
||||
2. `ApiClient` 自动处理:Token 附加、401 自动刷新、错误上报
|
||||
3. 后端统一返回 `{code, message, data}`,`ApiClient._request()` 自动解析
|
||||
4. 异常类型:`ServerException`(code != 0)、`NetworkException`(连接失败)
|
||||
5. **不在 Widget/Controller 中直接调用 ApiClient**,应通过 Repository 层
|
||||
|
||||
### 错误处理链
|
||||
|
||||
```
|
||||
DataSource (throw Exception)
|
||||
→ Repository (catch → Either<Failure, T>)
|
||||
→ Controller (fold → AsyncValue)
|
||||
→ UI (AsyncValue.when: loading/data/error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、路由(go_router)
|
||||
|
||||
### 规则
|
||||
|
||||
1. 所有路由定义在 `core/router/app_router.dart`
|
||||
2. 使用 `context.go()` 进行声明式导航,避免 `Navigator.push()`(页面内二级跳转除外)
|
||||
3. 登录态检查在 `redirect` 中统一处理
|
||||
4. 产品类型 → 路由映射在 `_productRoutes` Map 中维护
|
||||
|
||||
---
|
||||
|
||||
## 五、性能规范
|
||||
|
||||
### 内存管理
|
||||
|
||||
1. **所有 Stream 订阅必须存入变量,dispose 时取消**
|
||||
```dart
|
||||
// ✅ 正确
|
||||
StreamSubscription<Duration>? _positionSub;
|
||||
_positionSub = stream.listen((data) { ... });
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_positionSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ❌ 错误 — 无法取消,造成内存泄漏
|
||||
stream.listen((data) { ... });
|
||||
```
|
||||
|
||||
2. **AnimationController、VideoPlayerController、AudioPlayer 等必须在 dispose 中释放**
|
||||
|
||||
3. **页面离开时停止 BLE 扫描**
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
FlutterBluePlus.stopScan();
|
||||
_scanSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### Widget 重建优化
|
||||
|
||||
4. **能用 `const` 的地方必须加 `const`**
|
||||
```dart
|
||||
// ✅
|
||||
const SizedBox(height: 16)
|
||||
const EdgeInsets.all(16)
|
||||
const Text('Hello')
|
||||
|
||||
// ❌
|
||||
SizedBox(height: 16)
|
||||
EdgeInsets.all(16)
|
||||
```
|
||||
|
||||
5. **动画区域用 `RepaintBoundary` 包裹**,防止全页面重绘
|
||||
```dart
|
||||
RepaintBoundary(
|
||||
child: AnimatedGradientBackground(),
|
||||
)
|
||||
```
|
||||
|
||||
6. **`AnimatedBuilder` 必须使用 `child` 参数**,将不变的子树传入 child,不要在 builder 中重建
|
||||
```dart
|
||||
// ✅
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
child: const HeavyWidget(), // 只构建一次
|
||||
builder: (context, child) => Transform.rotate(
|
||||
angle: _controller.value,
|
||||
child: child, // 复用
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### API 调用
|
||||
|
||||
7. **多个独立 API 调用使用 `Future.wait` 并行**,不要顺序 await
|
||||
```dart
|
||||
// ✅ 并行
|
||||
final results = await Future.wait([
|
||||
api.get('/shelves/'),
|
||||
api.get('/stories/'),
|
||||
]);
|
||||
|
||||
// ❌ 顺序(慢 N 倍)
|
||||
final shelves = await api.get('/shelves/');
|
||||
final stories = await api.get('/stories/');
|
||||
```
|
||||
|
||||
8. **耗时计算(JSON 解析、图片处理)放到 Isolate**
|
||||
```dart
|
||||
final result = await Isolate.run(() => jsonDecode(bigJson));
|
||||
```
|
||||
|
||||
### 图片
|
||||
|
||||
9. **网络图片使用 `Image.network` + `fit: BoxFit.cover`**,不要手动指定 `cacheWidth/cacheHeight`(会导致显示异常)
|
||||
10. **大量图片列表使用 `ListView.builder`**,不要 `ListView(children: [...])`
|
||||
|
||||
---
|
||||
|
||||
## 六、构建配置
|
||||
|
||||
### Android Release
|
||||
|
||||
- `build.gradle.kts` 中 release 配置:
|
||||
- `isMinifyEnabled = true` — R8 代码压缩
|
||||
- `isShrinkResources = true` — 移除未使用资源
|
||||
- ProGuard 规则在 `proguard-rules.pro` 维护
|
||||
- 打包命令:`flutter build apk --release --split-per-abi --obfuscate --split-debug-info=./debug-info`
|
||||
|
||||
### iOS Release
|
||||
|
||||
- 构建命令:`flutter build ipa --release --obfuscate --split-debug-info=./debug-info`
|
||||
|
||||
---
|
||||
|
||||
## 七、命名规范
|
||||
|
||||
### 文件命名
|
||||
|
||||
| 类型 | 格式 | 示例 |
|
||||
|------|------|------|
|
||||
| 页面 | `xxx_page.dart` | `login_page.dart` |
|
||||
| 控制器 | `xxx_controller.dart` | `auth_controller.dart` |
|
||||
| 数据源 | `xxx_remote_data_source.dart` | `spirit_remote_data_source.dart` |
|
||||
| 仓库实现 | `xxx_repository_impl.dart` | `auth_repository_impl.dart` |
|
||||
| 仓库接口 | `xxx_repository.dart` | `auth_repository.dart` |
|
||||
| 模型 | `xxx_model.dart` | `user_model.dart` |
|
||||
| Widget | `xxx_widget.dart` 或直接描述 | `gradient_button.dart` |
|
||||
| 生成文件 | `*.g.dart` / `*.freezed.dart` | 自动生成,不手动编辑 |
|
||||
|
||||
### 类命名
|
||||
|
||||
- PascalCase:`AuthController`、`DeviceControlPage`、`ServerException`
|
||||
- 私有前缀:`_buildHeader()`、`_positionSub`
|
||||
|
||||
### 变量命名
|
||||
|
||||
- camelCase:`currentUser`、`isPlaying`、`_audioPlayer`
|
||||
- Provider:`xxxProvider`(由 `@riverpod` 自动生成)
|
||||
|
||||
---
|
||||
|
||||
## 八、主题系统
|
||||
|
||||
### 颜色
|
||||
|
||||
- 全局颜色定义在 `theme/app_colors.dart`
|
||||
- 产品主题颜色在 `theme/product_theme.dart`(4 种产品类型)
|
||||
- **不要在 Widget 中硬编码颜色值**,优先使用 `AppColors.xxx` 或 `Theme.of(context)`
|
||||
|
||||
### 字体
|
||||
|
||||
- 主字体:DM Sans(正文)、Outfit(标题)、Press Start 2P(品牌像素风)
|
||||
- 通过 `google_fonts` 包引用,**运行时自动下载并缓存**
|
||||
- 本地字体:Inter(在 `pubspec.yaml` 中声明)
|
||||
|
||||
### 间距
|
||||
|
||||
- 使用 `AppSpacing.xs/sm/md/lg/xl` 常量
|
||||
- 不要在 Widget 中硬编码间距数值
|
||||
|
||||
---
|
||||
|
||||
## 九、Git 规范
|
||||
|
||||
### 分支命名
|
||||
|
||||
- 功能分支:`fix/auto-YYYYMMDD-HHMMSS`
|
||||
- 合并目标:`main`
|
||||
|
||||
### Commit 规范
|
||||
|
||||
- `fix:` — Bug 修复
|
||||
- `feat:` — 新功能
|
||||
- `refactor:` — 重构(不改变行为)
|
||||
- `chore:` — 构建配置、依赖更新等
|
||||
|
||||
---
|
||||
|
||||
## 十、检查清单(PR 提交前)
|
||||
|
||||
- [ ] `dart analyze` 无 Error(Warning/Info 可接受)
|
||||
- [ ] 所有 Stream 订阅已在 dispose 中取消
|
||||
- [ ] 所有 Controller/Player 已在 dispose 中释放
|
||||
- [ ] 多个独立 API 调用已用 `Future.wait` 并行化
|
||||
- [ ] 动画 Widget 使用了 `RepaintBoundary` 或 `AnimatedBuilder(child:)`
|
||||
- [ ] 新增的 Widget 构造函数尽可能标记为 `const`
|
||||
- [ ] 列表使用 `ListView.builder`,而非 `ListView(children:)`
|
||||
- [ ] 未引入新的硬编码颜色值(使用 AppColors 或 theme)
|
||||
- [ ] Android release 构建正常(minify + shrinkResources 不会崩溃)
|
||||
BIN
airhub_app/assets/fonts/DMSans-Italic-Variable.ttf
Normal file
BIN
airhub_app/assets/fonts/DMSans-Italic-Variable.ttf
Normal file
Binary file not shown.
BIN
airhub_app/assets/fonts/DMSans-Variable.ttf
Normal file
BIN
airhub_app/assets/fonts/DMSans-Variable.ttf
Normal file
Binary file not shown.
BIN
airhub_app/assets/fonts/Outfit-Variable.ttf
Normal file
BIN
airhub_app/assets/fonts/Outfit-Variable.ttf
Normal file
Binary file not shown.
BIN
airhub_app/assets/fonts/PressStart2P-Regular.ttf
Normal file
BIN
airhub_app/assets/fonts/PressStart2P-Regular.ttf
Normal file
Binary file not shown.
@ -1,7 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../features/auth/presentation/pages/login_page.dart';
|
||||
import '../../pages/bluetooth_page.dart';
|
||||
@ -21,6 +23,10 @@ import '../network/token_manager.dart';
|
||||
|
||||
part 'app_router.g.dart';
|
||||
|
||||
const _lastRouteKey = 'last_business_route';
|
||||
const _lastProductTypeKey = 'last_product_type';
|
||||
const _validBusinessRoutes = {'/device-control', '/badge-control', '/badge-basic-control'};
|
||||
|
||||
/// 产品代码 → 路由 + ProductType 的映射
|
||||
const _productCodeRoutes = {
|
||||
'KPBL-ON': (route: '/device-control', type: ProductType.capybara),
|
||||
@ -43,29 +49,52 @@ GoRouter goRouter(Ref ref) {
|
||||
return '/login';
|
||||
}
|
||||
if (hasToken && isLoginRoute) {
|
||||
// 登录成功 → 跳到最近使用的设备业务页
|
||||
// 登录成功 → 获取已绑定设备列表
|
||||
try {
|
||||
final dataSource = ref.read(deviceRemoteDataSourceProvider);
|
||||
final devices = await dataSource.getMyDevices();
|
||||
debugPrint('[Router] 已绑定设备数: ${devices.length}');
|
||||
|
||||
if (devices.isNotEmpty) {
|
||||
// 按 last_online_at 降序,取最近使用的设备
|
||||
// 收集用户当前绑定的所有业务路由
|
||||
final boundRoutes = <String>{};
|
||||
for (final d in devices) {
|
||||
final dt = d.device.deviceType ?? d.device.deviceTypeInfo;
|
||||
final code = dt?.productCode ?? '';
|
||||
final m = _productCodeRoutes[code];
|
||||
if (m != null) boundRoutes.add(m.route);
|
||||
}
|
||||
|
||||
// 优先恢复上次退出时的业务页面(需确认该设备仍在绑定中)
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedRoute = prefs.getString(_lastRouteKey);
|
||||
final savedProductType = prefs.getString(_lastProductTypeKey);
|
||||
if (savedRoute != null && boundRoutes.contains(savedRoute)) {
|
||||
debugPrint('[Router] 恢复上次页面: $savedRoute');
|
||||
if (savedProductType != null) {
|
||||
final pt = ProductType.values.where((e) => e.name == savedProductType).firstOrNull;
|
||||
if (pt != null) {
|
||||
ref.read(currentProductTypeProvider.notifier).set(pt);
|
||||
}
|
||||
}
|
||||
return savedRoute;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[Router] 读取上次页面失败: $e');
|
||||
}
|
||||
|
||||
// Fallback:按设备最近使用时间跳转
|
||||
devices.sort((a, b) {
|
||||
final ta = a.device.lastOnlineAt ?? '';
|
||||
final tb = b.device.lastOnlineAt ?? '';
|
||||
return tb.compareTo(ta);
|
||||
});
|
||||
final recent = devices.first;
|
||||
final dt = recent.device.deviceType;
|
||||
final dti = recent.device.deviceTypeInfo;
|
||||
debugPrint('[Router] 最近设备 sn=${recent.device.sn}');
|
||||
debugPrint('[Router] deviceType=$dt');
|
||||
debugPrint('[Router] deviceTypeInfo=$dti');
|
||||
final resolvedDt = dt ?? dti;
|
||||
final resolvedDt = recent.device.deviceType ?? recent.device.deviceTypeInfo;
|
||||
final code = resolvedDt?.productCode ?? '';
|
||||
debugPrint('[Router] productCode=$code');
|
||||
debugPrint('[Router] 最近设备 sn=${recent.device.sn}, productCode=$code');
|
||||
final mapping = _productCodeRoutes[code];
|
||||
debugPrint('[Router] mapping=$mapping → route=${mapping?.route}');
|
||||
if (mapping != null) {
|
||||
ref.read(currentProductTypeProvider.notifier).set(mapping.type);
|
||||
return mapping.route;
|
||||
|
||||
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../../../core/services/phone_auth_service.dart';
|
||||
import '../../../../theme/app_colors.dart';
|
||||
@ -95,7 +94,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Title
|
||||
Text(
|
||||
'服务协议',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF374151),
|
||||
@ -307,7 +306,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Logo — 匹配 HTML .login-logo
|
||||
Text(
|
||||
'Airhub',
|
||||
style: GoogleFonts.pressStart2p(
|
||||
style: TextStyle(fontFamily: 'Press Start 2P',
|
||||
fontSize: 28,
|
||||
color: const Color(0xFF6366F1), // 靛蓝
|
||||
letterSpacing: 2,
|
||||
@ -463,7 +462,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
children: [
|
||||
Text(
|
||||
'欢迎使用 Airhub',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF6B5B95),
|
||||
@ -728,7 +727,7 @@ class _AgreementContentPage extends StatelessWidget {
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF374151),
|
||||
|
||||
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../core/services/ble_provisioning_service.dart';
|
||||
@ -586,7 +585,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
|
||||
child: Text(
|
||||
'搜索设备',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -658,7 +657,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
|
||||
height: 120,
|
||||
placeholderBuilder: (_) => Text(
|
||||
'?',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFFF59E0B), // Amber color per HTML
|
||||
@ -827,7 +826,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
|
||||
// 设备名称
|
||||
Text(
|
||||
device.name,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: device.isBoundByOther
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:math';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../core/network/api_client.dart';
|
||||
import 'story_detail_page.dart';
|
||||
@ -303,7 +302,7 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
statusText,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF4B5563),
|
||||
@ -327,7 +326,7 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
batteryText,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF4B5563),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../theme/app_colors.dart';
|
||||
import '../widgets/animated_gradient_background.dart';
|
||||
@ -88,7 +87,7 @@ class _HomePageState extends State<HomePage>
|
||||
child: Text(
|
||||
'Airhub',
|
||||
// Use Press Start 2P pixel font per HTML CSS
|
||||
style: GoogleFonts.pressStart2p(
|
||||
style: TextStyle(fontFamily: 'Press Start 2P',
|
||||
fontSize: 28,
|
||||
color: const Color(0xFF6366F1), // 靛蓝
|
||||
letterSpacing: 2,
|
||||
@ -239,7 +238,7 @@ class _HomePageState extends State<HomePage>
|
||||
child: Center(
|
||||
child: Text(
|
||||
'立即连接',
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../theme/app_colors.dart';
|
||||
import '../widgets/gradient_button.dart';
|
||||
@ -58,7 +57,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||
// Title
|
||||
Text(
|
||||
'服务协议',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -326,7 +325,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Text(
|
||||
'Airhub',
|
||||
style: GoogleFonts.pressStart2p(
|
||||
style: TextStyle(fontFamily: 'Press Start 2P',
|
||||
fontSize: 28,
|
||||
color: const Color(0xFF6366F1),
|
||||
letterSpacing: 2,
|
||||
@ -502,7 +501,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||
// Heading - font-size: 32px, font-weight: 700
|
||||
Text(
|
||||
'欢迎使用 Airhub',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF6B5B95),
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:math';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show PlatformException;
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import '../services/music_generation_service.dart';
|
||||
import '../widgets/animated_gradient_background.dart';
|
||||
@ -801,7 +800,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
child: Text(
|
||||
'灵感电台',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -1108,7 +1107,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
track.lyrics.isNotEmpty
|
||||
? _cleanLyrics(track.lyrics)
|
||||
: '生成音乐后\n点我看歌词',
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 12,
|
||||
height: 1.6,
|
||||
color: track.lyrics.isNotEmpty
|
||||
@ -1170,7 +1169,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
),
|
||||
child: Text(
|
||||
bubbleText,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF6B4423),
|
||||
@ -1227,7 +1226,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
child: Text(
|
||||
_currentTime,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF6B7280),
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@ -1277,7 +1276,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
child: Text(
|
||||
_totalTime,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF6B7280),
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@ -1434,7 +1433,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
children: [
|
||||
Text(
|
||||
mood['title'] as String,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 14,
|
||||
fontWeight: isActive ? FontWeight.w700 : FontWeight.w600,
|
||||
color: isActive
|
||||
@ -1447,7 +1446,7 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
mood['desc'] as String,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 11,
|
||||
color: isActive
|
||||
? const Color(0xFF6B7280)
|
||||
@ -1908,7 +1907,7 @@ class _InputModalContent extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'自由创作',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF374151),
|
||||
@ -1935,7 +1934,7 @@ class _InputModalContent extends StatelessWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'描述你想要的音乐氛围、场景或情绪',
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF9CA3AF),
|
||||
),
|
||||
@ -1950,11 +1949,11 @@ class _InputModalContent extends StatelessWidget {
|
||||
controller: controller,
|
||||
minLines: 4,
|
||||
maxLines: 6,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 14, color: const Color(0xFF374151)),
|
||||
decoration: InputDecoration(
|
||||
hintText: '例如:水豚在雨中等公交,心情却很平静...',
|
||||
hintStyle: GoogleFonts.dmSans(
|
||||
hintStyle: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 14, color: const Color(0xFF9CA3AF)),
|
||||
filled: true,
|
||||
fillColor: Colors.black.withOpacity(0.03),
|
||||
@ -2099,7 +2098,7 @@ class _PlaylistModalContentState extends State<_PlaylistModalContent>
|
||||
children: [
|
||||
Text(
|
||||
'我的唱片架',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF374151),
|
||||
@ -2266,7 +2265,7 @@ class _PlaylistModalContentState extends State<_PlaylistModalContent>
|
||||
Flexible(
|
||||
child: Text(
|
||||
track.title,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 12,
|
||||
fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isCurrent
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/animated_gradient_background.dart';
|
||||
import '../features/device/presentation/controllers/device_controller.dart';
|
||||
@ -233,7 +232,7 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
children: [
|
||||
Text(
|
||||
'选择产品',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -365,7 +364,7 @@ class _ProductCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
product['name'],
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../widgets/glass_dialog.dart';
|
||||
import '../widgets/animated_gradient_background.dart';
|
||||
import '../widgets/ios_toast.dart';
|
||||
@ -100,7 +99,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
),
|
||||
Text(
|
||||
'设置',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../core/services/ble_provisioning_service.dart';
|
||||
import '../features/device/presentation/controllers/device_controller.dart';
|
||||
@ -322,7 +321,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
child: Text(
|
||||
'WiFi配网',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -386,7 +385,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
),
|
||||
Text(
|
||||
'选择WiFi网络',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -528,7 +527,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
),
|
||||
Text(
|
||||
_selectedWifiSsid,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -596,7 +595,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
),
|
||||
Text(
|
||||
'正在配网...',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF1F2937),
|
||||
@ -712,7 +711,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'配网成功!',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF1F2937),
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get lightTheme {
|
||||
// Base text theme with DM Sans (PRD: 正文/UI 字体)
|
||||
final baseTextTheme = GoogleFonts.dmSansTextTheme(const TextTheme(
|
||||
final baseTextTheme = const TextTheme(
|
||||
// h1 / Large Headings
|
||||
displayLarge: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
@ -40,19 +39,19 @@ class AppTheme {
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
));
|
||||
).apply(fontFamily: 'DM Sans');
|
||||
|
||||
// Apply Outfit to heading styles (PRD: 标题/Display 字体)
|
||||
final textTheme = baseTextTheme.copyWith(
|
||||
displayLarge: GoogleFonts.outfit(textStyle: baseTextTheme.displayLarge),
|
||||
displayMedium: GoogleFonts.outfit(textStyle: baseTextTheme.displayMedium),
|
||||
displaySmall: GoogleFonts.outfit(textStyle: baseTextTheme.displaySmall),
|
||||
headlineLarge: GoogleFonts.outfit(textStyle: baseTextTheme.headlineLarge),
|
||||
headlineMedium: GoogleFonts.outfit(textStyle: baseTextTheme.headlineMedium),
|
||||
headlineSmall: GoogleFonts.outfit(textStyle: baseTextTheme.headlineSmall),
|
||||
titleLarge: GoogleFonts.outfit(textStyle: baseTextTheme.titleLarge),
|
||||
titleMedium: GoogleFonts.outfit(textStyle: baseTextTheme.titleMedium),
|
||||
titleSmall: GoogleFonts.outfit(textStyle: baseTextTheme.titleSmall),
|
||||
displayLarge: baseTextTheme.displayLarge?.copyWith(fontFamily: 'Outfit'),
|
||||
displayMedium: baseTextTheme.displayMedium?.copyWith(fontFamily: 'Outfit'),
|
||||
displaySmall: baseTextTheme.displaySmall?.copyWith(fontFamily: 'Outfit'),
|
||||
headlineLarge: baseTextTheme.headlineLarge?.copyWith(fontFamily: 'Outfit'),
|
||||
headlineMedium: baseTextTheme.headlineMedium?.copyWith(fontFamily: 'Outfit'),
|
||||
headlineSmall: baseTextTheme.headlineSmall?.copyWith(fontFamily: 'Outfit'),
|
||||
titleLarge: baseTextTheme.titleLarge?.copyWith(fontFamily: 'Outfit'),
|
||||
titleMedium: baseTextTheme.titleMedium?.copyWith(fontFamily: 'Outfit'),
|
||||
titleSmall: baseTextTheme.titleSmall?.copyWith(fontFamily: 'Outfit'),
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
@ -67,7 +66,7 @@ class AppTheme {
|
||||
background: AppColors.bgBase,
|
||||
),
|
||||
// PRD: DM Sans 为默认正文字体,回退到系统字体
|
||||
fontFamily: GoogleFonts.dmSans().fontFamily,
|
||||
fontFamily: 'DM Sans',
|
||||
fontFamilyFallback: const [
|
||||
'Roboto',
|
||||
'PingFang SC',
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// 颜色定义 - 精确还原 Profile PRD
|
||||
class AppColors {
|
||||
@ -111,77 +110,77 @@ class AppColors {
|
||||
/// 字体样式 - PRD规范: Outfit(标题) + DM Sans(正文) + Press Start 2P(Logo)
|
||||
class AppTextStyles {
|
||||
// 页面标题 — 统一规范: 17px w600 #1F2937
|
||||
static final TextStyle title = GoogleFonts.outfit(
|
||||
static final TextStyle title = TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1F2937),
|
||||
);
|
||||
|
||||
// User Name → Outfit (heading/display)
|
||||
static final TextStyle userName = GoogleFonts.outfit(
|
||||
static final TextStyle userName = TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
// User ID → DM Sans (body)
|
||||
static final TextStyle userId = GoogleFonts.dmSans(
|
||||
static final TextStyle userId = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.textSecondary,
|
||||
);
|
||||
|
||||
// Menu Text → DM Sans (body/UI)
|
||||
static final TextStyle menuText = GoogleFonts.dmSans(
|
||||
static final TextStyle menuText = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
// Badge Text → DM Sans (small UI)
|
||||
static final TextStyle badge = GoogleFonts.dmSans(
|
||||
static final TextStyle badge = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white,
|
||||
);
|
||||
|
||||
// Modal Title → Outfit (heading)
|
||||
static final TextStyle modalTitle = GoogleFonts.outfit(
|
||||
static final TextStyle modalTitle = TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
// Book specific styles → Outfit (heading)
|
||||
static final TextStyle bookTitle = GoogleFonts.outfit(
|
||||
static final TextStyle bookTitle = TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
// Book count → DM Sans (body)
|
||||
static final TextStyle bookCount = GoogleFonts.dmSans(
|
||||
static final TextStyle bookCount = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
);
|
||||
|
||||
// Slot title → DM Sans (small UI)
|
||||
static final TextStyle slotTitle = GoogleFonts.dmSans(
|
||||
static final TextStyle slotTitle = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
|
||||
// PRD: font-size: 24px, color: #9CA3AF, font-weight: 300, opacity: 0.7
|
||||
static final TextStyle emptyPlus = GoogleFonts.dmSans(
|
||||
static final TextStyle emptyPlus = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: const Color(0xB39CA3AF), // #9CA3AF with 0.7 opacity
|
||||
);
|
||||
|
||||
// Button text → DM Sans (UI)
|
||||
static final TextStyle createStoryBtn = GoogleFonts.dmSans(
|
||||
static final TextStyle createStoryBtn = TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'gradient_button.dart';
|
||||
import '../theme/app_colors.dart' as appclr;
|
||||
|
||||
@ -55,7 +54,7 @@ class GlassDialog extends StatelessWidget {
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(fontFamily: 'Outfit',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF4B2404),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/product_theme.dart';
|
||||
|
||||
@ -125,7 +124,7 @@ class GradientButton extends StatelessWidget {
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
style: GoogleFonts.dmSans(
|
||||
style: TextStyle(fontFamily: 'DM Sans',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
|
||||
@ -524,14 +524,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.8.1"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -61,7 +61,7 @@ dependencies:
|
||||
# Existing dependencies
|
||||
webview_flutter: ^4.4.2
|
||||
permission_handler: ^11.0.0
|
||||
google_fonts: ^6.1.0
|
||||
# google_fonts removed — local fonts used instead
|
||||
flutter_blue_plus: ^1.31.0
|
||||
flutter_svg: ^2.0.9
|
||||
image_picker: ^1.2.1
|
||||
@ -90,6 +90,17 @@ flutter:
|
||||
weight: 600
|
||||
- asset: assets/fonts/Inter-Bold.ttf
|
||||
weight: 700
|
||||
- family: DM Sans
|
||||
fonts:
|
||||
- asset: assets/fonts/DMSans-Variable.ttf
|
||||
- asset: assets/fonts/DMSans-Italic-Variable.ttf
|
||||
style: italic
|
||||
- family: Outfit
|
||||
fonts:
|
||||
- asset: assets/fonts/Outfit-Variable.ttf
|
||||
- family: Press Start 2P
|
||||
fonts:
|
||||
- asset: assets/fonts/PressStart2P-Regular.ttf
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user