This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

73
client/.dockerignore Normal file
View File

@@ -0,0 +1,73 @@
# Git相关
.git
.gitignore
# Flutter构建产物
build/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
# IDE文件
.vscode/
.idea/
*.swp
*.swo
*~
# 操作系统文件
.DS_Store
Thumbs.db
# 测试文件
test/
tests/
coverage/
# 文档
*.md
README*
DOCS*
docs/
# 日志文件
*.log
logs/
# 临时文件
tmp/
temp/
*.tmp
*.temp
# 环境文件
.env
.env.local
.env.*.local
# Android相关
android/.gradle/
android/app/build/
android/build/
android/gradle/
android/gradlew
android/gradlew.bat
android/local.properties
android/key.properties
# iOS相关
ios/Pods/
ios/Runner.xcworkspace/xcuserdata/
ios/Runner.xcodeproj/xcuserdata/
ios/Flutter/flutter_export_environment.sh
# Web相关保留build/web用于生产构建
# build/web
# 其他
*.bak
*.backup
node_modules/

45
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
client/.metadata Normal file
View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "05db9689081f091050f01aed79f04dce0c750154"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: android
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: ios
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: linux
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: macos
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: web
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: windows
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

32
client/Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# 使用官方Flutter镜像作为构建环境
FROM cirrusci/flutter:stable AS build
# 设置工作目录
WORKDIR /app
# 复制pubspec文件
COPY pubspec.yaml pubspec.lock ./
# 获取依赖
RUN flutter pub get
# 复制源代码
COPY . .
# 构建Web应用
RUN flutter build web --release
# 使用nginx作为生产环境
FROM nginx:alpine
# 复制构建产物到nginx目录
COPY --from=build /app/build/web /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,186 @@
# 多环境配置说明
## 概述
前端应用现在支持多环境后端 API 配置,可以在开发、预发布和生产环境之间切换。
## 环境类型
### 1. 开发环境 (Development)
- **默认 API 地址**: `http://localhost:8080/api/v1`
- **Android 模拟器**: `http://10.0.2.2:8080/api/v1`
- **用途**: 本地开发和测试
### 2. 预发布环境 (Staging)
- **默认 API 地址**: `http://your-staging-domain.com/api/v1`
- **用途**: 上线前测试
### 3. 生产环境 (Production)
- **默认 API 地址**: `http://your-production-domain.com/api/v1`
- **用途**: 正式上线
## 使用方法
### 方法一:通过命令行参数设置
#### 开发环境
```bash
flutter run --dart-define=ENVIRONMENT=development
```
#### 预发布环境
```bash
flutter run --dart-define=ENVIRONMENT=staging
```
#### 生产环境
```bash
flutter run --dart-define=ENVIRONMENT=production
```
#### 自定义 API 地址
```bash
flutter run --dart-define=API_BASE_URL=http://192.168.1.100:8080/api/v1
```
### 方法二:通过开发者设置页面(推荐开发环境使用)
1. 在应用的设置页面找到"开发者设置"选项
2. 选择目标环境或输入自定义 API 地址
3. 保存设置并重启应用
## 构建配置
### Android 构建
#### 开发版本
```bash
flutter build apk --dart-define=ENVIRONMENT=development
```
#### 生产版本
```bash
flutter build apk --dart-define=ENVIRONMENT=production --release
```
### iOS 构建
#### 开发版本
```bash
flutter build ios --dart-define=ENVIRONMENT=development
```
#### 生产版本
```bash
flutter build ios --dart-define=ENVIRONMENT=production --release
```
### Web 构建
#### 开发版本
```bash
flutter build web --dart-define=ENVIRONMENT=development
```
#### 生产版本
```bash
flutter build web --dart-define=ENVIRONMENT=production --release
```
## 配置文件位置
环境配置文件位于:
```
lib/core/config/environment.dart
```
## 自定义环境配置
如需修改环境配置,编辑 `environment.dart` 文件:
```dart
static const Map<String, String> productionConfig = {
'baseUrl': 'http://your-production-domain.com/api/v1',
'wsUrl': 'ws://your-production-domain.com/ws',
};
```
## 常见场景
### 场景 1: 本地开发Web
- **设备**: 开发电脑浏览器
- **API 地址**: `http://localhost:8080/api/v1`
- **运行命令**: `flutter run -d chrome`
### 场景 2: Android 模拟器开发
- **设备**: Android 模拟器
- **API 地址**: `http://10.0.2.2:8080/api/v1`
- **运行命令**: `flutter run -d android`
- **说明**: 10.0.2.2 是 Android 模拟器访问宿主机 localhost 的特殊地址
### 场景 3: 真机调试
- **设备**: 手机真机
- **API 地址**: `http://你的电脑IP:8080/api/v1`
- **设置方法**:
1. 确保手机和电脑在同一局域网
2. 查看电脑 IP 地址(如 192.168.1.100
3. 在开发者设置中输入: `http://192.168.1.100:8080/api/v1`
4. 或使用命令: `flutter run --dart-define=API_BASE_URL=http://192.168.1.100:8080/api/v1`
### 场景 4: 生产环境部署
- **设备**: 正式用户设备
- **API 地址**: 生产服务器地址
- **构建命令**: `flutter build apk --dart-define=ENVIRONMENT=production --release`
## 环境检测
在代码中可以使用以下方法检测当前环境:
```dart
import 'package:your_app/core/config/environment.dart';
// 检查是否为开发环境
if (EnvironmentConfig.isDevelopment) {
print('当前是开发环境');
}
// 检查是否为生产环境
if (EnvironmentConfig.isProduction) {
print('当前是生产环境');
}
// 获取当前 API 地址
String apiUrl = EnvironmentConfig.baseUrl;
print('API 地址: $apiUrl');
```
## 注意事项
1. **重启应用**: 修改环境配置后必须重启应用才能生效
2. **生产环境**: 生产环境配置应该在构建时通过命令行参数指定,不要在代码中硬编码
3. **安全性**: 不要在代码中提交敏感信息,如生产环境的真实 API 地址
4. **测试**: 切换环境后应该进行充分测试,确保 API 连接正常
5. **网络权限**: Android 需要在 `AndroidManifest.xml` 中添加网络权限
6. **HTTPS**: 生产环境建议使用 HTTPS 协议
## 故障排查
### 问题 1: Android 模拟器无法连接 localhost
**解决方案**: 使用 `10.0.2.2` 代替 `localhost`
### 问题 2: 真机无法连接开发服务器
**解决方案**:
- 确保手机和电脑在同一网络
- 检查防火墙设置
- 使用电脑的局域网 IP 地址
### 问题 3: 环境切换后仍然连接旧地址
**解决方案**: 完全关闭并重启应用
### 问题 4: iOS 模拟器无法连接
**解决方案**: iOS 模拟器可以直接使用 `localhost`,无需特殊配置
## 扩展阅读
- [Flutter 环境变量配置](https://flutter.dev/docs/development/tools/sdk/overview#environment-variables)
- [Dart 编译时常量](https://dart.dev/guides/language/language-tour#const-keyword)

20
client/README.md Normal file
View File

@@ -0,0 +1,20 @@
# client
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
flutter build apk --dart-define=ENVIRONMENT=production --release
flutter run -d chrome --web-port=3003

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
client/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,43 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.aienglish.learning"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// AI英语学习应用的唯一 Application ID
applicationId = "com.aienglish.learning"
// 支持 Android 5.0 (Lollipop) 及以上版本
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,64 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 音频录制权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Android 13+ 媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 唤醒锁权限(用于音频播放时保持屏幕唤醒) -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:label="WOW Talk"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.aienglish.learning
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,15 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
android.useAndroidX=true
android.enableJetifier=true
# Network and TLS configuration
systemProp.https.protocols=TLSv1.2,TLSv1.3
systemProp.jdk.tls.client.protocols=TLSv1.2,TLSv1.3
systemProp.javax.net.ssl.trustStore=
systemProp.javax.net.ssl.trustStorePassword=
# Gradle daemon configuration
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

85
client/build_prod.bat Normal file
View File

@@ -0,0 +1,85 @@
@echo off
REM 生产环境构建脚本 (Windows)
echo ========================================
echo Building AI English Learning App for Production
echo ========================================
echo.
:menu
echo Please select build target:
echo 1. Android APK
echo 2. Android App Bundle (AAB)
echo 3. iOS
echo 4. Web
echo 5. Windows
echo 6. Exit
echo.
set /p choice=Enter your choice (1-6):
if "%choice%"=="1" goto android_apk
if "%choice%"=="2" goto android_aab
if "%choice%"=="3" goto ios
if "%choice%"=="4" goto web
if "%choice%"=="5" goto windows
if "%choice%"=="6" goto end
echo Invalid choice. Please try again.
echo.
goto menu
:android_apk
echo.
echo Building Android APK...
flutter build apk --dart-define=ENVIRONMENT=production --release
echo.
echo Build completed! APK location:
echo build\app\outputs\flutter-apk\app-release.apk
echo.
pause
goto end
:android_aab
echo.
echo Building Android App Bundle...
flutter build appbundle --dart-define=ENVIRONMENT=production --release
echo.
echo Build completed! AAB location:
echo build\app\outputs\bundle\release\app-release.aab
echo.
pause
goto end
:ios
echo.
echo Building iOS...
flutter build ios --dart-define=ENVIRONMENT=production --release
echo.
echo Build completed! Please open Xcode to archive and distribute.
echo.
pause
goto end
:web
echo.
echo Building Web...
flutter build web --dart-define=ENVIRONMENT=production --release
echo.
echo Build completed! Web files location:
echo build\web
echo.
pause
goto end
:windows
echo.
echo Building Windows...
flutter build windows --dart-define=ENVIRONMENT=production --release
echo.
echo Build completed! Windows executable location:
echo build\windows\runner\Release
echo.
pause
goto end
:end
exit

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

34
client/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
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;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
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 = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.client;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.client.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.client.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.client.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
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;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
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_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 = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
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;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
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 = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.client;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.client;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View 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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View 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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View 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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View 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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,49 @@
<?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>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>client</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,133 @@
/// 环境配置
enum Environment {
development,
staging,
production,
}
/// 环境配置管理
class EnvironmentConfig {
static Environment _currentEnvironment = Environment.development;
/// 获取当前环境
static Environment get current => _currentEnvironment;
/// 设置当前环境
static void setEnvironment(Environment env) {
_currentEnvironment = env;
}
/// 从字符串设置环境
static void setEnvironmentFromString(String? envString) {
switch (envString?.toLowerCase()) {
case 'production':
case 'prod':
_currentEnvironment = Environment.production;
break;
case 'staging':
case 'stage':
_currentEnvironment = Environment.staging;
break;
case 'development':
case 'dev':
default:
_currentEnvironment = Environment.development;
break;
}
}
/// 获取当前环境的API基础URL
static String get baseUrl {
switch (_currentEnvironment) {
case Environment.production:
return 'https://loukao.cn/api/v1';
case Environment.staging:
return 'http://localhost:8080/api/v1';
case Environment.development:
default:
// 开发环境localhost 用于 Web10.0.2.2 用于 Android 模拟器
return const String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080/api/v1',
);
}
}
/// 获取环境名称
static String get environmentName {
switch (_currentEnvironment) {
case Environment.production:
return 'Production';
case Environment.staging:
return 'Staging';
case Environment.development:
return 'Development';
}
}
/// 是否为开发环境
static bool get isDevelopment => _currentEnvironment == Environment.development;
/// 是否为生产环境
static bool get isProduction => _currentEnvironment == Environment.production;
/// 是否为预发布环境
static bool get isStaging => _currentEnvironment == Environment.staging;
/// 开发环境配置
static const Map<String, String> developmentConfig = {
'baseUrl': 'http://localhost:8080/api/v1',
'baseUrlAndroid': 'http://10.0.2.2:8080/api/v1',
'wsUrl': 'ws://localhost:8080/ws',
};
/// 预发布环境配置
static const Map<String, String> stagingConfig = {
'baseUrl': 'https://loukao.cn/api/v1',
'wsUrl': 'ws://your-staging-domain.com/ws',
};
/// 生产环境配置
static const Map<String, String> productionConfig = {
'baseUrl': 'https://loukao.cn/api/v1',
'wsUrl': 'ws://your-production-domain.com/ws',
};
/// 获取当前环境配置
static Map<String, String> get config {
switch (_currentEnvironment) {
case Environment.production:
return productionConfig;
case Environment.staging:
return stagingConfig;
case Environment.development:
default:
return developmentConfig;
}
}
/// 获取WebSocket URL
static String get wsUrl {
return config['wsUrl'] ?? '';
}
/// 获取连接超时时间(毫秒)
static int get connectTimeout {
return isProduction ? 10000 : 30000;
}
/// 获取接收超时时间(毫秒)
static int get receiveTimeout {
return isProduction ? 10000 : 30000;
}
/// 是否启用日志
static bool get enableLogging {
return !isProduction;
}
/// 是否启用调试模式
static bool get debugMode {
return isDevelopment;
}
}

View File

@@ -0,0 +1,88 @@
import '../config/environment.dart';
/// 应用常量配置
class AppConstants {
// 应用信息
static const String appName = 'AI英语学习';
static const String appVersion = '1.0.0';
// API配置 - 从环境配置获取
static String get baseUrl => EnvironmentConfig.baseUrl;
static int get connectTimeout => EnvironmentConfig.connectTimeout;
static int get receiveTimeout => EnvironmentConfig.receiveTimeout;
// 存储键名
static const String accessTokenKey = 'access_token';
static const String refreshTokenKey = 'refresh_token';
static const String userInfoKey = 'user_info';
static const String settingsKey = 'app_settings';
// 分页配置
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// 学习配置
static const int dailyWordGoal = 50;
static const int maxRetryAttempts = 3;
static const Duration studySessionDuration = Duration(minutes: 25);
// 音频配置
static const double defaultPlaybackSpeed = 1.0;
static const double minPlaybackSpeed = 0.5;
static const double maxPlaybackSpeed = 2.0;
// 图片配置
static const int maxImageSize = 5 * 1024 * 1024; // 5MB
static const List<String> supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp'];
// 缓存配置
static const Duration cacheExpiration = Duration(hours: 24);
static const int maxCacheSize = 100 * 1024 * 1024; // 100MB
}
/// 路由常量
class RouteConstants {
static const String splash = '/splash';
static const String onboarding = '/onboarding';
static const String login = '/login';
static const String register = '/register';
static const String home = '/home';
static const String profile = '/profile';
static const String vocabulary = '/vocabulary';
static const String vocabularyTest = '/vocabulary/test';
static const String listening = '/listening';
static const String reading = '/reading';
static const String writing = '/writing';
static const String speaking = '/speaking';
static const String settings = '/settings';
}
/// 学习等级常量
enum LearningLevel {
beginner('beginner', '初级'),
intermediate('intermediate', '中级'),
advanced('advanced', '高级');
const LearningLevel(this.value, this.label);
final String value;
final String label;
}
/// 词库类型常量
enum VocabularyType {
elementary('elementary', '小学'),
junior('junior', '初中'),
senior('senior', '高中'),
cet4('cet4', '四级'),
cet6('cet6', '六级'),
toefl('toefl', '托福'),
ielts('ielts', '雅思'),
business('business', '商务'),
daily('daily', '日常');
const VocabularyType(this.value, this.label);
final String value;
final String label;
}

View File

@@ -0,0 +1,299 @@
/// 应用错误基类
abstract class AppError implements Exception {
final String message;
final String? code;
final dynamic originalError;
const AppError({
required this.message,
this.code,
this.originalError,
});
@override
String toString() {
return 'AppError(message: $message, code: $code)';
}
}
/// 网络错误
class NetworkError extends AppError {
const NetworkError({
required super.message,
super.code,
super.originalError,
});
factory NetworkError.connectionTimeout() {
return const NetworkError(
message: '连接超时,请检查网络连接',
code: 'CONNECTION_TIMEOUT',
);
}
factory NetworkError.noInternet() {
return const NetworkError(
message: '网络连接不可用,请检查网络设置',
code: 'NO_INTERNET',
);
}
factory NetworkError.serverError(int statusCode, [String? message]) {
return NetworkError(
message: message ?? '服务器错误 ($statusCode)',
code: 'SERVER_ERROR_$statusCode',
);
}
factory NetworkError.unknown([dynamic error]) {
return NetworkError(
message: '网络请求失败',
code: 'UNKNOWN_NETWORK_ERROR',
originalError: error,
);
}
}
/// 认证错误
class AuthError extends AppError {
const AuthError({
required super.message,
super.code,
super.originalError,
});
factory AuthError.unauthorized() {
return const AuthError(
message: '未授权访问,请重新登录',
code: 'UNAUTHORIZED',
);
}
factory AuthError.tokenExpired() {
return const AuthError(
message: '登录已过期,请重新登录',
code: 'TOKEN_EXPIRED',
);
}
factory AuthError.invalidCredentials() {
return const AuthError(
message: '用户名或密码错误',
code: 'INVALID_CREDENTIALS',
);
}
factory AuthError.accountLocked() {
return const AuthError(
message: '账户已被锁定,请联系客服',
code: 'ACCOUNT_LOCKED',
);
}
}
/// 验证错误
class ValidationError extends AppError {
final Map<String, List<String>>? fieldErrors;
const ValidationError({
required super.message,
super.code,
super.originalError,
this.fieldErrors,
});
factory ValidationError.required(String field) {
return ValidationError(
message: '$field不能为空',
code: 'FIELD_REQUIRED',
fieldErrors: {field: ['不能为空']},
);
}
factory ValidationError.invalid(String field, String reason) {
return ValidationError(
message: '$field格式不正确$reason',
code: 'FIELD_INVALID',
fieldErrors: {field: [reason]},
);
}
factory ValidationError.multiple(Map<String, List<String>> errors) {
return ValidationError(
message: '表单验证失败',
code: 'VALIDATION_FAILED',
fieldErrors: errors,
);
}
}
/// 业务逻辑错误
class BusinessError extends AppError {
const BusinessError({
required super.message,
super.code,
super.originalError,
});
factory BusinessError.notFound(String resource) {
return BusinessError(
message: '$resource不存在',
code: 'RESOURCE_NOT_FOUND',
);
}
factory BusinessError.alreadyExists(String resource) {
return BusinessError(
message: '$resource已存在',
code: 'RESOURCE_ALREADY_EXISTS',
);
}
factory BusinessError.operationNotAllowed(String operation) {
return BusinessError(
message: '不允许执行操作:$operation',
code: 'OPERATION_NOT_ALLOWED',
);
}
factory BusinessError.quotaExceeded(String resource) {
return BusinessError(
message: '$resource配额已用完',
code: 'QUOTA_EXCEEDED',
);
}
}
/// 存储错误
class StorageError extends AppError {
const StorageError({
required super.message,
super.code,
super.originalError,
});
factory StorageError.readFailed(String key) {
return StorageError(
message: '读取数据失败:$key',
code: 'STORAGE_READ_FAILED',
);
}
factory StorageError.writeFailed(String key) {
return StorageError(
message: '写入数据失败:$key',
code: 'STORAGE_WRITE_FAILED',
);
}
factory StorageError.notInitialized() {
return const StorageError(
message: '存储服务未初始化',
code: 'STORAGE_NOT_INITIALIZED',
);
}
}
/// 文件错误
class FileError extends AppError {
const FileError({
required super.message,
super.code,
super.originalError,
});
factory FileError.notFound(String path) {
return FileError(
message: '文件不存在:$path',
code: 'FILE_NOT_FOUND',
);
}
factory FileError.accessDenied(String path) {
return FileError(
message: '文件访问被拒绝:$path',
code: 'FILE_ACCESS_DENIED',
);
}
factory FileError.formatNotSupported(String format) {
return FileError(
message: '不支持的文件格式:$format',
code: 'FILE_FORMAT_NOT_SUPPORTED',
);
}
factory FileError.sizeTooLarge(int size, int maxSize) {
return FileError(
message: '文件大小超出限制:${size}B > ${maxSize}B',
code: 'FILE_SIZE_TOO_LARGE',
);
}
}
/// 音频错误
class AudioError extends AppError {
const AudioError({
required super.message,
super.code,
super.originalError,
});
factory AudioError.playbackFailed() {
return const AudioError(
message: '音频播放失败',
code: 'AUDIO_PLAYBACK_FAILED',
);
}
factory AudioError.recordingFailed() {
return const AudioError(
message: '音频录制失败',
code: 'AUDIO_RECORDING_FAILED',
);
}
factory AudioError.permissionDenied() {
return const AudioError(
message: '音频权限被拒绝',
code: 'AUDIO_PERMISSION_DENIED',
);
}
}
/// 学习相关错误
class LearningError extends AppError {
const LearningError({
required super.message,
super.code,
super.originalError,
});
factory LearningError.progressNotFound() {
return const LearningError(
message: '学习进度不存在',
code: 'LEARNING_PROGRESS_NOT_FOUND',
);
}
factory LearningError.vocabularyNotFound() {
return const LearningError(
message: '词汇不存在',
code: 'VOCABULARY_NOT_FOUND',
);
}
factory LearningError.testNotCompleted() {
return const LearningError(
message: '测试未完成',
code: 'TEST_NOT_COMPLETED',
);
}
factory LearningError.levelNotUnlocked() {
return const LearningError(
message: '等级未解锁',
code: 'LEVEL_NOT_UNLOCKED',
);
}
}

View File

@@ -0,0 +1,62 @@
/// 应用异常基类
class AppException implements Exception {
final String message;
final String? code;
final dynamic details;
const AppException(
this.message, {
this.code,
this.details,
});
@override
String toString() {
return 'AppException: $message';
}
}
/// 网络异常
class NetworkException extends AppException {
const NetworkException(
super.message, {
super.code,
super.details,
});
}
/// 认证异常
class AuthException extends AppException {
const AuthException(
super.message, {
super.code,
super.details,
});
}
/// 服务器异常
class ServerException extends AppException {
const ServerException(
super.message, {
super.code,
super.details,
});
}
/// 缓存异常
class CacheException extends AppException {
const CacheException(
super.message, {
super.code,
super.details,
});
}
/// 验证异常
class ValidationException extends AppException {
const ValidationException(
super.message, {
super.code,
super.details,
});
}

View File

@@ -0,0 +1,121 @@
/// API响应基础模型
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final int? code;
final Map<String, dynamic>? errors;
const ApiResponse({
required this.success,
required this.message,
this.data,
this.code,
this.errors,
});
factory ApiResponse.success({
required String message,
T? data,
int? code,
}) {
return ApiResponse<T>(
success: true,
message: message,
data: data,
code: code ?? 200,
);
}
factory ApiResponse.error({
required String message,
int? code,
Map<String, dynamic>? errors,
}) {
return ApiResponse<T>(
success: false,
message: message,
code: code ?? 400,
errors: errors,
);
}
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromJsonT,
) {
return ApiResponse<T>(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null && fromJsonT != null
? fromJsonT(json['data'])
: json['data'],
code: json['code'],
errors: json['errors'],
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data,
'code': code,
'errors': errors,
};
}
@override
String toString() {
return 'ApiResponse{success: $success, message: $message, data: $data, code: $code}';
}
}
/// 分页响应模型
class PaginatedResponse<T> {
final List<T> data;
final int total;
final int page;
final int pageSize;
final int totalPages;
final bool hasNext;
final bool hasPrevious;
const PaginatedResponse({
required this.data,
required this.total,
required this.page,
required this.pageSize,
required this.totalPages,
required this.hasNext,
required this.hasPrevious,
});
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
) {
final List<dynamic> dataList = json['data'] ?? [];
return PaginatedResponse<T>(
data: dataList.map((item) => fromJsonT(item)).toList(),
total: json['total'] ?? 0,
page: json['page'] ?? 1,
pageSize: json['page_size'] ?? 10,
totalPages: json['total_pages'] ?? 0,
hasNext: json['has_next'] ?? false,
hasPrevious: json['has_previous'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'data': data,
'total': total,
'page': page,
'page_size': pageSize,
'total_pages': totalPages,
'has_next': hasNext,
'has_previous': hasPrevious,
};
}
}

View File

@@ -0,0 +1,280 @@
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
/// 用户模型
@JsonSerializable()
class User {
final String id;
final String username;
final String email;
final String? phone;
final String? avatar;
final DateTime createdAt;
final DateTime updatedAt;
final UserProfile? profile;
final UserSettings? settings;
const User({
required this.id,
required this.username,
required this.email,
this.phone,
this.avatar,
required this.createdAt,
required this.updatedAt,
this.profile,
this.settings,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
User copyWith({
String? id,
String? username,
String? email,
String? phone,
String? avatar,
DateTime? createdAt,
DateTime? updatedAt,
UserProfile? profile,
UserSettings? settings,
}) {
return User(
id: id ?? this.id,
username: username ?? this.username,
email: email ?? this.email,
phone: phone ?? this.phone,
avatar: avatar ?? this.avatar,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
profile: profile ?? this.profile,
settings: settings ?? this.settings,
);
}
}
/// 用户资料
@JsonSerializable()
class UserProfile {
final String? firstName;
final String? lastName;
final String? phone;
final String? bio;
final String? avatar;
final String? realName;
final String? gender;
final DateTime? birthday;
final String? location;
final String? occupation;
final String? education;
final List<String>? interests;
final LearningGoal? learningGoal;
final EnglishLevel? currentLevel;
final EnglishLevel? targetLevel;
final EnglishLevel? englishLevel;
final UserSettings? settings;
const UserProfile({
this.firstName,
this.lastName,
this.phone,
this.bio,
this.avatar,
this.realName,
this.gender,
this.birthday,
this.location,
this.occupation,
this.education,
this.interests,
this.learningGoal,
this.currentLevel,
this.targetLevel,
this.englishLevel,
this.settings,
});
factory UserProfile.fromJson(Map<String, dynamic> json) => _$UserProfileFromJson(json);
Map<String, dynamic> toJson() => _$UserProfileToJson(this);
UserProfile copyWith({
String? firstName,
String? lastName,
String? phone,
String? bio,
String? avatar,
String? realName,
String? gender,
DateTime? birthday,
String? location,
String? occupation,
String? education,
List<String>? interests,
LearningGoal? learningGoal,
EnglishLevel? currentLevel,
EnglishLevel? targetLevel,
EnglishLevel? englishLevel,
UserSettings? settings,
}) {
return UserProfile(
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
phone: phone ?? this.phone,
bio: bio ?? this.bio,
avatar: avatar ?? this.avatar,
realName: realName ?? this.realName,
gender: gender ?? this.gender,
birthday: birthday ?? this.birthday,
location: location ?? this.location,
occupation: occupation ?? this.occupation,
education: education ?? this.education,
interests: interests ?? this.interests,
learningGoal: learningGoal ?? this.learningGoal,
currentLevel: currentLevel ?? this.currentLevel,
targetLevel: targetLevel ?? this.targetLevel,
englishLevel: englishLevel ?? this.englishLevel,
settings: settings ?? this.settings,
);
}
}
/// 用户设置
@JsonSerializable()
class UserSettings {
final bool notificationsEnabled;
final bool soundEnabled;
final bool vibrationEnabled;
final String language;
final String theme;
final int dailyGoal;
final int dailyWordGoal;
final int dailyStudyMinutes;
final List<String> reminderTimes;
final bool autoPlayAudio;
final double audioSpeed;
final bool showTranslation;
final bool showPronunciation;
const UserSettings({
this.notificationsEnabled = true,
this.soundEnabled = true,
this.vibrationEnabled = true,
this.language = 'zh-CN',
this.theme = 'system',
this.dailyGoal = 30,
this.dailyWordGoal = 20,
this.dailyStudyMinutes = 30,
this.reminderTimes = const ['09:00', '20:00'],
this.autoPlayAudio = true,
this.audioSpeed = 1.0,
this.showTranslation = true,
this.showPronunciation = true,
});
factory UserSettings.fromJson(Map<String, dynamic> json) => _$UserSettingsFromJson(json);
Map<String, dynamic> toJson() => _$UserSettingsToJson(this);
UserSettings copyWith({
bool? notificationsEnabled,
bool? soundEnabled,
bool? vibrationEnabled,
String? language,
String? theme,
int? dailyGoal,
int? dailyWordGoal,
int? dailyStudyMinutes,
List<String>? reminderTimes,
bool? autoPlayAudio,
double? audioSpeed,
bool? showTranslation,
bool? showPronunciation,
}) {
return UserSettings(
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
soundEnabled: soundEnabled ?? this.soundEnabled,
vibrationEnabled: vibrationEnabled ?? this.vibrationEnabled,
language: language ?? this.language,
theme: theme ?? this.theme,
dailyGoal: dailyGoal ?? this.dailyGoal,
dailyWordGoal: dailyWordGoal ?? this.dailyWordGoal,
dailyStudyMinutes: dailyStudyMinutes ?? this.dailyStudyMinutes,
reminderTimes: reminderTimes ?? this.reminderTimes,
autoPlayAudio: autoPlayAudio ?? this.autoPlayAudio,
audioSpeed: audioSpeed ?? this.audioSpeed,
showTranslation: showTranslation ?? this.showTranslation,
showPronunciation: showPronunciation ?? this.showPronunciation,
);
}
}
/// 学习目标
enum LearningGoal {
@JsonValue('daily_communication')
dailyCommunication,
@JsonValue('business_english')
businessEnglish,
@JsonValue('academic_study')
academicStudy,
@JsonValue('exam_preparation')
examPreparation,
@JsonValue('travel')
travel,
@JsonValue('hobby')
hobby,
}
/// 英语水平
enum EnglishLevel {
@JsonValue('beginner')
beginner,
@JsonValue('elementary')
elementary,
@JsonValue('intermediate')
intermediate,
@JsonValue('upper_intermediate')
upperIntermediate,
@JsonValue('advanced')
advanced,
@JsonValue('proficient')
proficient,
@JsonValue('expert')
expert,
}
/// 认证响应
@JsonSerializable()
class AuthResponse {
final User user;
final String token;
final String? refreshToken;
final DateTime expiresAt;
const AuthResponse({
required this.user,
required this.token,
this.refreshToken,
required this.expiresAt,
});
factory AuthResponse.fromJson(Map<String, dynamic> json) => _$AuthResponseFromJson(json);
Map<String, dynamic> toJson() => _$AuthResponseToJson(this);
}
/// Token刷新响应
@JsonSerializable()
class TokenRefreshResponse {
final String token;
final String? refreshToken;
final DateTime expiresAt;
const TokenRefreshResponse({
required this.token,
this.refreshToken,
required this.expiresAt,
});
factory TokenRefreshResponse.fromJson(Map<String, dynamic> json) => _$TokenRefreshResponseFromJson(json);
Map<String, dynamic> toJson() => _$TokenRefreshResponseToJson(this);
}

View File

@@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
username: json['username'] as String,
email: json['email'] as String,
phone: json['phone'] as String?,
avatar: json['avatar'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
profile: json['profile'] == null
? null
: UserProfile.fromJson(json['profile'] as Map<String, dynamic>),
settings: json['settings'] == null
? null
: UserSettings.fromJson(json['settings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'id': instance.id,
'username': instance.username,
'email': instance.email,
'phone': instance.phone,
'avatar': instance.avatar,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
'profile': instance.profile,
'settings': instance.settings,
};
UserProfile _$UserProfileFromJson(Map<String, dynamic> json) => UserProfile(
firstName: json['firstName'] as String?,
lastName: json['lastName'] as String?,
phone: json['phone'] as String?,
bio: json['bio'] as String?,
avatar: json['avatar'] as String?,
realName: json['realName'] as String?,
gender: json['gender'] as String?,
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
location: json['location'] as String?,
occupation: json['occupation'] as String?,
education: json['education'] as String?,
interests: (json['interests'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
learningGoal:
$enumDecodeNullable(_$LearningGoalEnumMap, json['learningGoal']),
currentLevel:
$enumDecodeNullable(_$EnglishLevelEnumMap, json['currentLevel']),
targetLevel:
$enumDecodeNullable(_$EnglishLevelEnumMap, json['targetLevel']),
englishLevel:
$enumDecodeNullable(_$EnglishLevelEnumMap, json['englishLevel']),
settings: json['settings'] == null
? null
: UserSettings.fromJson(json['settings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$UserProfileToJson(UserProfile instance) =>
<String, dynamic>{
'firstName': instance.firstName,
'lastName': instance.lastName,
'phone': instance.phone,
'bio': instance.bio,
'avatar': instance.avatar,
'realName': instance.realName,
'gender': instance.gender,
'birthday': instance.birthday?.toIso8601String(),
'location': instance.location,
'occupation': instance.occupation,
'education': instance.education,
'interests': instance.interests,
'learningGoal': _$LearningGoalEnumMap[instance.learningGoal],
'currentLevel': _$EnglishLevelEnumMap[instance.currentLevel],
'targetLevel': _$EnglishLevelEnumMap[instance.targetLevel],
'englishLevel': _$EnglishLevelEnumMap[instance.englishLevel],
'settings': instance.settings,
};
const _$LearningGoalEnumMap = {
LearningGoal.dailyCommunication: 'daily_communication',
LearningGoal.businessEnglish: 'business_english',
LearningGoal.academicStudy: 'academic_study',
LearningGoal.examPreparation: 'exam_preparation',
LearningGoal.travel: 'travel',
LearningGoal.hobby: 'hobby',
};
const _$EnglishLevelEnumMap = {
EnglishLevel.beginner: 'beginner',
EnglishLevel.elementary: 'elementary',
EnglishLevel.intermediate: 'intermediate',
EnglishLevel.upperIntermediate: 'upper_intermediate',
EnglishLevel.advanced: 'advanced',
EnglishLevel.proficient: 'proficient',
EnglishLevel.expert: 'expert',
};
UserSettings _$UserSettingsFromJson(Map<String, dynamic> json) => UserSettings(
notificationsEnabled: json['notificationsEnabled'] as bool? ?? true,
soundEnabled: json['soundEnabled'] as bool? ?? true,
vibrationEnabled: json['vibrationEnabled'] as bool? ?? true,
language: json['language'] as String? ?? 'zh-CN',
theme: json['theme'] as String? ?? 'system',
dailyGoal: (json['dailyGoal'] as num?)?.toInt() ?? 30,
dailyWordGoal: (json['dailyWordGoal'] as num?)?.toInt() ?? 20,
dailyStudyMinutes: (json['dailyStudyMinutes'] as num?)?.toInt() ?? 30,
reminderTimes: (json['reminderTimes'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['09:00', '20:00'],
autoPlayAudio: json['autoPlayAudio'] as bool? ?? true,
audioSpeed: (json['audioSpeed'] as num?)?.toDouble() ?? 1.0,
showTranslation: json['showTranslation'] as bool? ?? true,
showPronunciation: json['showPronunciation'] as bool? ?? true,
);
Map<String, dynamic> _$UserSettingsToJson(UserSettings instance) =>
<String, dynamic>{
'notificationsEnabled': instance.notificationsEnabled,
'soundEnabled': instance.soundEnabled,
'vibrationEnabled': instance.vibrationEnabled,
'language': instance.language,
'theme': instance.theme,
'dailyGoal': instance.dailyGoal,
'dailyWordGoal': instance.dailyWordGoal,
'dailyStudyMinutes': instance.dailyStudyMinutes,
'reminderTimes': instance.reminderTimes,
'autoPlayAudio': instance.autoPlayAudio,
'audioSpeed': instance.audioSpeed,
'showTranslation': instance.showTranslation,
'showPronunciation': instance.showPronunciation,
};
AuthResponse _$AuthResponseFromJson(Map<String, dynamic> json) => AuthResponse(
user: User.fromJson(json['user'] as Map<String, dynamic>),
token: json['token'] as String,
refreshToken: json['refreshToken'] as String?,
expiresAt: DateTime.parse(json['expiresAt'] as String),
);
Map<String, dynamic> _$AuthResponseToJson(AuthResponse instance) =>
<String, dynamic>{
'user': instance.user,
'token': instance.token,
'refreshToken': instance.refreshToken,
'expiresAt': instance.expiresAt.toIso8601String(),
};
TokenRefreshResponse _$TokenRefreshResponseFromJson(
Map<String, dynamic> json) =>
TokenRefreshResponse(
token: json['token'] as String,
refreshToken: json['refreshToken'] as String?,
expiresAt: DateTime.parse(json['expiresAt'] as String),
);
Map<String, dynamic> _$TokenRefreshResponseToJson(
TokenRefreshResponse instance) =>
<String, dynamic>{
'token': instance.token,
'refreshToken': instance.refreshToken,
'expiresAt': instance.expiresAt.toIso8601String(),
};

View File

@@ -0,0 +1,209 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../services/storage_service.dart';
import 'api_endpoints.dart';
import '../config/environment.dart';
/// AI相关API服务
class AIApiService {
static String get _baseUrl => EnvironmentConfig.baseUrl;
/// 获取认证头部
Map<String, String> _getAuthHeaders() {
final storageService = StorageService.instance;
final token = storageService.getString(StorageKeys.accessToken);
return {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
}
/// 写作批改
Future<Map<String, dynamic>> correctWriting({
required String content,
required String taskType,
}) async {
try {
final headers = _getAuthHeaders();
final response = await http.post(
Uri.parse('$_baseUrl/api/v1/ai/writing/correct'),
headers: headers,
body: json.encode({
'content': content,
'task_type': taskType,
}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to correct writing: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error correcting writing: $e');
}
}
/// 口语评估
Future<Map<String, dynamic>> evaluateSpeaking({
required String audioText,
required String prompt,
}) async {
try {
final headers = _getAuthHeaders();
final response = await http.post(
Uri.parse('$_baseUrl/api/v1/ai/speaking/evaluate'),
headers: headers,
body: json.encode({
'audio_text': audioText,
'prompt': prompt,
}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to evaluate speaking: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error evaluating speaking: $e');
}
}
/// 获取AI使用统计
Future<Map<String, dynamic>> getAIUsageStats() async {
try {
final headers = _getAuthHeaders();
final response = await http.get(
Uri.parse('$_baseUrl/api/v1/ai/stats'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to get AI stats: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting AI stats: $e');
}
}
/// 上传音频文件
Future<Map<String, dynamic>> uploadAudio(File audioFile) async {
try {
final storageService = StorageService.instance;
final token = storageService.getString(StorageKeys.accessToken);
final request = http.MultipartRequest(
'POST',
Uri.parse('$_baseUrl/api/v1/upload/audio'),
);
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.files.add(
await http.MultipartFile.fromPath('audio', audioFile.path),
);
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to upload audio: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error uploading audio: $e');
}
}
/// 上传图片文件
Future<Map<String, dynamic>> uploadImage(File imageFile) async {
try {
final storageService = StorageService.instance;
final token = storageService.getString(StorageKeys.accessToken);
final request = http.MultipartRequest(
'POST',
Uri.parse('$_baseUrl/api/v1/upload/image'),
);
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.files.add(
await http.MultipartFile.fromPath('image', imageFile.path),
);
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to upload image: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error uploading image: $e');
}
}
/// 删除文件
Future<Map<String, dynamic>> deleteFile(String fileId) async {
try {
final headers = _getAuthHeaders();
final response = await http.delete(
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to delete file: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error deleting file: $e');
}
}
/// 获取文件信息
Future<Map<String, dynamic>> getFileInfo(String fileId) async {
try {
final headers = _getAuthHeaders();
final response = await http.get(
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to get file info: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting file info: $e');
}
}
/// 获取上传统计
Future<Map<String, dynamic>> getUploadStats({int days = 30}) async {
try {
final headers = _getAuthHeaders();
final response = await http.get(
Uri.parse('$_baseUrl/api/v1/upload/stats?days=$days'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to get upload stats: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting upload stats: $e');
}
}
}

View File

@@ -0,0 +1,252 @@
import 'package:dio/dio.dart';
import '../constants/app_constants.dart';
import '../services/storage_service.dart';
import '../services/navigation_service.dart';
import '../routes/app_routes.dart';
/// API客户端配置
class ApiClient {
static ApiClient? _instance;
late Dio _dio;
late StorageService _storageService;
ApiClient._internal() {
_dio = Dio();
}
static Future<ApiClient> getInstance() async {
if (_instance == null) {
_instance = ApiClient._internal();
_instance!._storageService = await StorageService.getInstance();
await _instance!._setupInterceptors();
}
return _instance!;
}
static ApiClient get instance {
if (_instance == null) {
throw Exception('ApiClient not initialized. Call ApiClient.getInstance() first.');
}
return _instance!;
}
Dio get dio => _dio;
/// 配置拦截器
Future<void> _setupInterceptors() async {
// 基础配置
_dio.options = BaseOptions(
baseUrl: AppConstants.baseUrl,
connectTimeout: Duration(milliseconds: AppConstants.connectTimeout),
receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeout),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
// 请求拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
// 添加认证token
final token = await _storageService.getToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onResponse: (response, handler) {
handler.next(response);
},
onError: (error, handler) async {
// 处理401错误尝试刷新token
if (error.response?.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// 重新发送请求
final options = error.requestOptions;
final token = await _storageService.getToken();
options.headers['Authorization'] = 'Bearer $token';
try {
final response = await _dio.fetch(options);
handler.resolve(response);
return;
} catch (e) {
// 刷新后仍然失败清除token并跳转登录
await _clearTokensAndRedirectToLogin();
}
} else {
// 刷新失败清除token并跳转登录
await _clearTokensAndRedirectToLogin();
}
}
handler.next(error);
},
),
);
// 日志拦截器(仅在调试模式下)
if (const bool.fromEnvironment('dart.vm.product') == false) {
_dio.interceptors.add(
LogInterceptor(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
logPrint: (obj) => print(obj),
),
);
}
}
/// 刷新token
Future<bool> _refreshToken() async {
try {
final refreshToken = await _storageService.getRefreshToken();
if (refreshToken == null || refreshToken.isEmpty) {
return false;
}
final response = await _dio.post(
'/auth/refresh',
data: {'refresh_token': refreshToken},
options: Options(
headers: {'Authorization': null}, // 移除Authorization头
),
);
if (response.statusCode == 200) {
final data = response.data;
await _storageService.saveToken(data['access_token']);
if (data['refresh_token'] != null) {
await _storageService.saveRefreshToken(data['refresh_token']);
}
return true;
}
} catch (e) {
print('Token refresh failed: $e');
}
return false;
}
/// 清除token并跳转登录
Future<void> _clearTokensAndRedirectToLogin() async {
await _storageService.clearTokens();
// 跳转到登录页面并清除所有历史记录
NavigationService.instance.navigateToAndClearStack(Routes.login);
// 显示提示信息
NavigationService.instance.showErrorSnackBar('登录已过期,请重新登录');
}
/// GET请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// POST请求
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// PUT请求
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// DELETE请求
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// 上传文件
Future<Response<T>> upload<T>(
String path,
FormData formData, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
return await _dio.post<T>(
path,
data: formData,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
);
}
/// 下载文件
Future<Response> download(
String urlPath,
String savePath, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
return await _dio.download(
urlPath,
savePath,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
}

View File

@@ -0,0 +1,77 @@
import '../config/environment.dart';
/// API端点配置
class ApiEndpoints {
// 基础URL - 从环境配置获取
static String get baseUrl => EnvironmentConfig.baseUrl;
// 认证相关
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String logout = '/auth/logout';
static const String refreshToken = '/auth/refresh';
static const String forgotPassword = '/auth/forgot-password';
static const String resetPassword = '/auth/reset-password';
static const String changePassword = '/auth/change-password';
static const String socialLogin = '/auth/social-login';
static const String verifyEmail = '/auth/verify-email';
static const String resendVerificationEmail = '/auth/resend-verification';
// 用户相关
static const String userInfo = '/user/profile';
static const String updateProfile = '/user/profile';
static const String uploadAvatar = '/user/avatar';
static const String checkUsername = '/user/check-username';
static const String checkEmail = '/user/check-email';
// 学习相关
static const String learningProgress = '/learning/progress';
static const String learningStats = '/learning/stats';
static const String dailyGoal = '/learning/daily-goal';
// 词汇相关
static const String vocabulary = '/vocabulary';
static const String vocabularyTest = '/vocabulary/test';
static const String vocabularyProgress = '/vocabulary/progress';
static const String wordBooks = '/vocabulary/books';
static const String wordLists = '/vocabulary/lists';
// 听力相关
static const String listening = '/listening';
static const String listeningMaterials = '/listening/materials';
static const String listeningRecords = '/listening/records';
static const String listeningStats = '/listening/stats';
// 阅读相关
static const String reading = '/reading';
static const String readingMaterials = '/reading/materials';
static const String readingRecords = '/reading/records';
static const String readingStats = '/reading/stats';
// 写作相关
static const String writing = '/writing';
static const String writingPrompts = '/writing/prompts';
static const String writingSubmissions = '/writing/submissions';
static const String writingStats = '/writing/stats';
// 口语相关
static const String speaking = '/speaking';
static const String speakingScenarios = '/speaking/scenarios';
static const String speakingRecords = '/speaking/records';
static const String speakingStats = '/speaking/stats';
// AI相关
static const String aiChat = '/ai/chat';
static const String aiCorrection = '/ai/correction';
static const String aiSuggestion = '/ai/suggestion';
// 文件上传
static const String upload = '/upload';
static const String uploadAudio = '/upload/audio';
static const String uploadImage = '/upload/image';
// 系统相关
static const String version = '/version';
static const String config = '/system/config';
static const String feedback = '/system/feedback';
}

View File

@@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../shared/providers/auth_provider.dart';
import '../../shared/providers/vocabulary_provider.dart';
import '../../shared/services/auth_service.dart';
import '../../shared/services/vocabulary_service.dart';
import '../network/api_client.dart';
/// 全局应用状态管理
class AppStateNotifier extends StateNotifier<AppState> {
AppStateNotifier() : super(const AppState());
void updateTheme(ThemeMode themeMode) {
state = state.copyWith(themeMode: themeMode);
}
void updateLocale(String locale) {
state = state.copyWith(locale: locale);
}
void updateNetworkStatus(bool isOnline) {
state = state.copyWith(isOnline: isOnline);
}
void updateLoading(bool isLoading) {
state = state.copyWith(isGlobalLoading: isLoading);
}
}
/// 应用状态模型
class AppState {
final ThemeMode themeMode;
final String locale;
final bool isOnline;
final bool isGlobalLoading;
const AppState({
this.themeMode = ThemeMode.light,
this.locale = 'zh_CN',
this.isOnline = true,
this.isGlobalLoading = false,
});
AppState copyWith({
ThemeMode? themeMode,
String? locale,
bool? isOnline,
bool? isGlobalLoading,
}) {
return AppState(
themeMode: themeMode ?? this.themeMode,
locale: locale ?? this.locale,
isOnline: isOnline ?? this.isOnline,
isGlobalLoading: isGlobalLoading ?? this.isGlobalLoading,
);
}
}
/// 全局状态Provider
final appStateProvider = StateNotifierProvider<AppStateNotifier, AppState>(
(ref) => AppStateNotifier(),
);
/// API客户端Provider
final apiClientProvider = Provider<ApiClient>(
(ref) => ApiClient.instance,
);
/// 认证服务Provider
final authServiceProvider = Provider<AuthService>(
(ref) => AuthService(),
);
/// 词汇服务Provider
final vocabularyServiceProvider = Provider<VocabularyService>(
(ref) => VocabularyService(),
);
/// 认证Provider
final authProvider = ChangeNotifierProvider<AuthProvider>(
(ref) {
final authService = ref.read(authServiceProvider);
return AuthProvider()..initialize();
},
);
/// 词汇Provider
final vocabularyProvider = ChangeNotifierProvider<VocabularyProvider>(
(ref) {
final vocabularyService = ref.read(vocabularyServiceProvider);
return VocabularyProvider(vocabularyService);
},
);
/// 网络状态Provider
final networkStatusProvider = StreamProvider<bool>(
(ref) async* {
// 这里可以实现网络状态监听
yield true; // 默认在线状态
},
);
/// 缓存管理Provider
final cacheManagerProvider = Provider<CacheManager>(
(ref) => CacheManager(),
);
/// 缓存管理器
class CacheManager {
final Map<String, dynamic> _cache = {};
final Map<String, DateTime> _cacheTimestamps = {};
final Duration _defaultCacheDuration = const Duration(minutes: 30);
/// 设置缓存
void set(String key, dynamic value, {Duration? duration}) {
_cache[key] = value;
_cacheTimestamps[key] = DateTime.now();
}
/// 获取缓存
T? get<T>(String key, {Duration? duration}) {
final timestamp = _cacheTimestamps[key];
if (timestamp == null) return null;
final cacheDuration = duration ?? _defaultCacheDuration;
if (DateTime.now().difference(timestamp) > cacheDuration) {
remove(key);
return null;
}
return _cache[key] as T?;
}
/// 移除缓存
void remove(String key) {
_cache.remove(key);
_cacheTimestamps.remove(key);
}
/// 清空缓存
void clear() {
_cache.clear();
_cacheTimestamps.clear();
}
/// 检查缓存是否存在且有效
bool isValid(String key, {Duration? duration}) {
final timestamp = _cacheTimestamps[key];
if (timestamp == null) return false;
final cacheDuration = duration ?? _defaultCacheDuration;
return DateTime.now().difference(timestamp) <= cacheDuration;
}
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app_state_provider.dart';
import '../../shared/providers/network_provider.dart';
import '../../shared/providers/error_provider.dart';
import '../../features/auth/providers/auth_provider.dart' as auth;
import '../../features/vocabulary/providers/vocabulary_provider.dart' as vocab;
import '../../features/comprehensive_test/providers/test_riverpod_provider.dart' as test;
/// 全局Provider配置
class GlobalProviders {
static final List<Override> overrides = [
// 这里可以添加测试时的Provider覆盖
];
static final List<ProviderObserver> observers = [
ProviderLogger(),
];
/// 获取所有核心Provider
static List<ProviderBase> get coreProviders => [
// 应用状态
appStateProvider,
networkProvider,
errorProvider,
// 认证相关
auth.authProvider,
// 词汇相关
vocab.vocabularyProvider,
// 综合测试相关
test.testProvider,
];
/// 预加载Provider
static Future<void> preloadProviders(ProviderContainer container) async {
// 预加载网络状态
container.read(networkProvider.notifier).refreshNetworkStatus();
// 认证状态会在AuthNotifier构造时自动检查
// 这里只需要读取provider来触发初始化
container.read(auth.authProvider);
}
}
/// Provider状态监听器
class ProviderLogger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} updated: $newValue');
}
@override
void didAddProvider(
ProviderBase provider,
Object? value,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} added: $value');
}
@override
void didDisposeProvider(
ProviderBase provider,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} disposed');
}
@override
void providerDidFail(
ProviderBase provider,
Object error,
StackTrace stackTrace,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} failed: $error');
}
}

View File

@@ -0,0 +1,496 @@
import 'package:flutter/material.dart';
import '../../features/auth/screens/splash_screen.dart';
import '../../features/auth/screens/login_screen.dart';
import '../../features/auth/screens/register_screen.dart';
import '../../features/auth/screens/forgot_password_screen.dart';
import '../../features/main/screens/main_app_screen.dart';
import '../../features/learning/screens/learning_home_screen.dart';
import '../../features/vocabulary/screens/vocabulary_home_screen.dart';
import '../../features/vocabulary/screens/vocabulary_category_screen.dart';
import '../../features/vocabulary/screens/vocabulary_book_screen.dart';
import '../../features/vocabulary/screens/word_learning_screen.dart';
import '../../features/vocabulary/screens/smart_review_screen.dart';
import '../../features/vocabulary/screens/vocabulary_test_screen.dart';
import '../../features/vocabulary/screens/daily_words_screen.dart';
import '../../features/vocabulary/screens/ai_recommendation_screen.dart';
import '../../features/vocabulary/screens/word_book_screen.dart';
import '../../features/vocabulary/screens/study_plan_screen.dart';
import '../../features/vocabulary/models/word_model.dart';
import '../../features/vocabulary/models/vocabulary_book_model.dart';
import '../../features/vocabulary/models/review_models.dart';
import '../../features/listening/screens/listening_home_screen.dart';
import '../../features/listening/screens/listening_category_screen.dart';
import '../../features/listening/screens/listening_exercise_detail_screen.dart';
import '../../features/listening/screens/listening_difficulty_screen.dart';
import '../../features/listening/screens/listening_stats_screen.dart';
import '../../features/listening/models/listening_exercise_model.dart';
// 移除静态数据依赖
import '../../features/reading/screens/reading_home_screen.dart';
import '../../features/writing/screens/writing_home_screen.dart';
import '../../features/speaking/screens/speaking_home_screen.dart';
import '../../features/comprehensive_test/screens/comprehensive_test_screen.dart';
import '../../features/profile/screens/profile_home_screen.dart';
import '../../features/profile/screens/profile_edit_screen.dart';
import '../../features/profile/screens/settings_screen.dart';
import '../../features/profile/screens/help_feedback_screen.dart';
import '../../features/ai/pages/ai_main_page.dart';
import '../../features/ai/pages/ai_writing_page.dart';
import '../../features/ai/pages/ai_speaking_page.dart';
import '../../features/home/screens/learning_stats_detail_screen.dart';
import '../../features/notification/screens/notification_list_screen.dart';
import '../widgets/not_found_screen.dart';
// 学习模式枚举
enum LearningMode {
normal,
review,
test
}
/// 路由名称常量
class Routes {
static const String splash = '/splash';
static const String login = '/login';
static const String register = '/register';
static const String forgotPassword = '/forgot-password';
static const String home = '/home';
static const String learning = '/learning';
static const String profile = '/profile';
static const String editProfile = '/edit-profile';
static const String settings = '/settings';
static const String helpFeedback = '/help-feedback';
static const String vocabularyHome = '/vocabulary';
static const String vocabularyCategory = '/vocabulary/category';
static const String vocabularyList = '/vocabulary/list';
static const String vocabularyBook = '/vocabulary/book';
static const String wordDetail = '/vocabulary/word';
static const String vocabularyTest = '/vocabulary/test';
static const String wordLearning = '/vocabulary/learning';
static const String smartReview = '/vocabulary/smart-review';
static const String dailyWords = '/vocabulary/daily-words';
static const String aiRecommendation = '/vocabulary/ai-recommendation';
static const String wordBook = '/vocabulary/word-book';
static const String studyPlan = '/vocabulary/study-plan';
static const String listeningHome = '/listening';
static const String listeningExercise = '/listening/exercise';
static const String listeningCategory = '/listening/category';
static const String listeningExerciseDetail = '/listening/exercise-detail';
static const String listeningDifficulty = '/listening/difficulty';
static const String listeningStats = '/listening/stats';
static const String readingHome = '/reading';
static const String readingExercise = '/reading/exercise';
static const String writingHome = '/writing';
static const String writingExercise = '/writing/exercise';
static const String speakingHome = '/speaking';
static const String speakingExercise = '/speaking/exercise';
static const String comprehensiveTest = '/comprehensive-test';
static const String ai = '/ai';
static const String aiWriting = '/ai/writing';
static const String aiSpeaking = '/ai/speaking';
static const String learningStatsDetail = '/learning-stats-detail';
static const String notifications = '/notifications';
}
/// 应用路由配置
class AppRoutes {
/// 路由映射表
static final Map<String, WidgetBuilder> _routes = {
Routes.splash: (context) => const SplashScreen(),
Routes.login: (context) => const LoginScreen(),
Routes.register: (context) => const RegisterScreen(),
Routes.forgotPassword: (context) => const ForgotPasswordScreen(),
Routes.home: (context) => const MainAppScreen(),
Routes.learning: (context) => const LearningHomeScreen(),
Routes.vocabularyHome: (context) => const VocabularyHomeScreen(),
// TODO: 这些路由需要参数暂时注释掉后续通过onGenerateRoute处理
// Routes.vocabularyList: (context) => const VocabularyBookScreen(),
// Routes.wordDetail: (context) => const WordLearningScreen(),
// Routes.vocabularyTest: (context) => const VocabularyTestScreen(),
// Routes.wordLearning: (context) => const SmartReviewScreen(),
Routes.dailyWords: (context) => const DailyWordsScreen(),
Routes.aiRecommendation: (context) => const AIRecommendationScreen(),
Routes.wordBook: (context) => const WordBookScreen(),
Routes.studyPlan: (context) => const StudyPlanScreen(),
Routes.listeningHome: (context) => const ListeningHomeScreen(),
Routes.listeningDifficulty: (context) => const ListeningDifficultyScreen(),
Routes.listeningStats: (context) => const ListeningStatsScreen(),
Routes.readingHome: (context) => const ReadingHomeScreen(),
Routes.writingHome: (context) => const WritingHomeScreen(),
Routes.speakingHome: (context) => const SpeakingHomeScreen(),
Routes.comprehensiveTest: (context) => const ComprehensiveTestScreen(),
Routes.profile: (context) => const ProfileHomeScreen(),
Routes.editProfile: (context) => const ProfileEditScreen(),
Routes.settings: (context) => const SettingsScreen(),
Routes.helpFeedback: (context) => const HelpFeedbackScreen(),
Routes.ai: (context) => const AIMainPage(),
Routes.aiWriting: (context) => const AIWritingPage(),
Routes.aiSpeaking: (context) => const AISpeakingPage(),
Routes.learningStatsDetail: (context) => const LearningStatsDetailScreen(),
Routes.notifications: (context) => const NotificationListScreen(),
// TODO: 添加其他页面路由
};
/// 获取路由映射表
static Map<String, WidgetBuilder> get routes => _routes;
/// 路由生成器
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
final String routeName = settings.name ?? '';
final arguments = settings.arguments;
// 处理带参数的词汇学习路由
switch (routeName) {
case Routes.vocabularyCategory:
if (arguments is Map<String, dynamic>) {
final category = arguments['category'];
if (category != null) {
return MaterialPageRoute(
builder: (context) => VocabularyCategoryScreen(category: category),
settings: settings,
);
}
}
break;
case Routes.vocabularyList:
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
if (vocabularyBook != null) {
return MaterialPageRoute(
builder: (context) => VocabularyBookScreen(vocabularyBook: vocabularyBook),
settings: settings,
);
}
}
break;
case Routes.wordLearning:
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final specificWords = arguments['specificWords'];
final mode = arguments['mode'];
if (vocabularyBook != null) {
return MaterialPageRoute(
builder: (context) => WordLearningScreen(
vocabularyBook: vocabularyBook,
specificWords: specificWords,
mode: mode ?? LearningMode.normal,
),
settings: settings,
);
}
}
break;
case Routes.vocabularyTest:
// 词汇测试路由,支持带参数和不带参数的情况
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final testType = arguments['testType'];
final questionCount = arguments['questionCount'];
return MaterialPageRoute(
builder: (context) => VocabularyTestScreen(
vocabularyBook: vocabularyBook,
testType: testType ?? TestType.vocabularyLevel,
questionCount: questionCount ?? 20,
),
settings: settings,
);
} else {
// 没有参数时,使用默认设置
return MaterialPageRoute(
builder: (context) => const VocabularyTestScreen(
testType: TestType.vocabularyLevel,
questionCount: 20,
),
settings: settings,
);
}
break;
case Routes.wordDetail:
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final reviewMode = arguments['reviewMode'];
final dailyTarget = arguments['dailyTarget'];
return MaterialPageRoute(
builder: (context) => SmartReviewScreen(
vocabularyBook: vocabularyBook,
reviewMode: reviewMode ?? ReviewMode.adaptive,
dailyTarget: dailyTarget ?? 20,
),
settings: settings,
);
}
break;
case Routes.smartReview:
// 智能复习路由,支持带参数和不带参数的情况
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final reviewMode = arguments['reviewMode'];
final dailyTarget = arguments['dailyTarget'];
return MaterialPageRoute(
builder: (context) => SmartReviewScreen(
vocabularyBook: vocabularyBook,
reviewMode: reviewMode ?? ReviewMode.adaptive,
dailyTarget: dailyTarget ?? 20,
),
settings: settings,
);
} else {
// 没有参数时,使用默认设置
return MaterialPageRoute(
builder: (context) => const SmartReviewScreen(
reviewMode: ReviewMode.adaptive,
dailyTarget: 20,
),
settings: settings,
);
}
break;
// 听力相关路由
case Routes.listeningCategory:
if (arguments is Map<String, dynamic>) {
final type = arguments['type'] as ListeningExerciseType;
final title = arguments['title'] as String;
final category = ListeningCategory(
id: type.toString(),
name: title,
description: '${title}练习材料',
icon: Icons.headphones,
exerciseCount: 0,
type: type,
);
return MaterialPageRoute(
builder: (context) => ListeningCategoryScreen(
category: category,
),
settings: settings,
);
}
break;
case Routes.listeningExerciseDetail:
if (arguments is Map<String, dynamic>) {
final exerciseId = arguments['exerciseId'];
if (exerciseId != null) {
return MaterialPageRoute(
builder: (context) => ListeningExerciseDetailScreen(
exerciseId: exerciseId,
),
settings: settings,
);
}
}
break;
}
// 默认路由处理
final WidgetBuilder? builder = _routes[routeName];
if (builder != null) {
return MaterialPageRoute(
builder: builder,
settings: settings,
);
}
// 未找到路由时的处理
return MaterialPageRoute(
builder: (context) => const NotFoundScreen(),
settings: settings,
);
}
/// 路由守卫 - 检查是否需要认证
static bool requiresAuth(String routeName) {
const publicRoutes = [
Routes.splash,
Routes.login,
Routes.register,
Routes.forgotPassword,
];
return !publicRoutes.contains(routeName);
}
/// 获取初始路由
static String getInitialRoute(bool isLoggedIn) {
return isLoggedIn ? Routes.home : Routes.splash;
}
}
/// 启动页面
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_initializeApp();
}
Future<void> _initializeApp() async {
// 初始化应用配置
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
// 导航到登录页面,让用户进行认证
Navigator.of(context).pushReplacementNamed(Routes.login);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// TODO: 添加应用Logo
Icon(
Icons.school,
size: 100,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 24),
Text(
'AI英语学习',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(height: 16),
Text(
'智能化英语学习平台',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
const SizedBox(height: 48),
const CircularProgressIndicator(),
],
),
),
);
}
}
/// 404页面
class NotFoundScreen extends StatelessWidget {
const NotFoundScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('页面未找到'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 100,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 24),
Text(
'404',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 16),
Text(
'抱歉,您访问的页面不存在',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamedAndRemoveUntil(
Routes.home,
(route) => false,
);
},
child: const Text('返回首页'),
),
],
),
),
);
}
}
/// 路由导航辅助类
class AppNavigator {
/// 导航到指定页面
static Future<T?> push<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
}) {
return Navigator.of(context).pushNamed<T>(
routeName,
arguments: arguments,
);
}
/// 替换当前页面
static Future<T?> pushReplacement<T extends Object?, TO extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
TO? result,
}) {
return Navigator.of(context).pushReplacementNamed<T, TO>(
routeName,
arguments: arguments,
result: result,
);
}
/// 清空栈并导航到指定页面
static Future<T?> pushAndRemoveUntil<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
bool Function(Route<dynamic>)? predicate,
}) {
return Navigator.of(context).pushNamedAndRemoveUntil<T>(
routeName,
predicate ?? (route) => false,
arguments: arguments,
);
}
/// 返回上一页
static void pop<T extends Object?>(BuildContext context, [T? result]) {
Navigator.of(context).pop<T>(result);
}
/// 返回到指定页面
static void popUntil(BuildContext context, String routeName) {
Navigator.of(context).popUntil(ModalRoute.withName(routeName));
}
/// 检查是否可以返回
static bool canPop(BuildContext context) {
return Navigator.of(context).canPop();
}
}

View File

@@ -0,0 +1,284 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'storage_service.dart';
import '../config/environment.dart';
class ApiResponse {
final dynamic data;
final int statusCode;
final String? message;
ApiResponse({
required this.data,
required this.statusCode,
this.message,
});
}
class ApiService {
late final Dio _dio;
final StorageService _storageService;
ApiService({required StorageService storageService})
: _storageService = storageService {
_dio = Dio(BaseOptions(
baseUrl: EnvironmentConfig.baseUrl,
connectTimeout: Duration(milliseconds: EnvironmentConfig.connectTimeout),
receiveTimeout: Duration(milliseconds: EnvironmentConfig.receiveTimeout),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_setupInterceptors();
}
void _setupInterceptors() {
// 请求拦截器
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// 添加认证token
final token = await _storageService.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
if (kDebugMode) {
print('API Request: ${options.method} ${options.uri}');
print('Headers: ${options.headers}');
if (options.data != null) {
print('Data: ${options.data}');
}
}
handler.next(options);
},
onResponse: (response, handler) {
if (kDebugMode) {
print('API Response: ${response.statusCode} ${response.requestOptions.uri}');
}
handler.next(response);
},
onError: (error, handler) {
if (kDebugMode) {
print('API Error: ${error.message}');
print('Response: ${error.response?.data}');
}
handler.next(error);
},
));
}
// GET请求
Future<ApiResponse> get(
String path, {
Map<String, dynamic>? queryParams,
Options? options,
}) async {
try {
final response = await _dio.get(
path,
queryParameters: queryParams,
options: options,
);
return ApiResponse(
data: response.data,
statusCode: response.statusCode ?? 200,
message: response.statusMessage,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// POST请求
Future<ApiResponse> post(
String path,
dynamic data, {
Map<String, dynamic>? queryParams,
Options? options,
}) async {
try {
final response = await _dio.post(
path,
data: data,
queryParameters: queryParams,
options: options,
);
return ApiResponse(
data: response.data,
statusCode: response.statusCode ?? 200,
message: response.statusMessage,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// PUT请求
Future<ApiResponse> put(
String path,
dynamic data, {
Map<String, dynamic>? queryParams,
Options? options,
}) async {
try {
final response = await _dio.put(
path,
data: data,
queryParameters: queryParams,
options: options,
);
return ApiResponse(
data: response.data,
statusCode: response.statusCode ?? 200,
message: response.statusMessage,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// PATCH请求
Future<ApiResponse> patch(
String path,
dynamic data, {
Map<String, dynamic>? queryParams,
Options? options,
}) async {
try {
final response = await _dio.patch(
path,
data: data,
queryParameters: queryParams,
options: options,
);
return ApiResponse(
data: response.data,
statusCode: response.statusCode ?? 200,
message: response.statusMessage,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// DELETE请求
Future<ApiResponse> delete(
String path, {
Map<String, dynamic>? queryParams,
Options? options,
}) async {
try {
final response = await _dio.delete(
path,
queryParameters: queryParams,
options: options,
);
return ApiResponse(
data: response.data,
statusCode: response.statusCode ?? 200,
message: response.statusMessage,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// 上传文件
Future<ApiResponse> uploadFile(
String path,
String filePath, {
String? fileName,
Map<String, dynamic>? data,
ProgressCallback? onSendProgress,
}) async {
try {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
filePath,
filename: fileName,
),
...?data,
});
final response = await _dio.post(
path,
data: formData,
options: Options(
headers: {
'Content-Type': 'multipart/form-data',
},
),
onSendProgress: onSendProgress,
);
return ApiResponse(
data: response.data,
statusCode: response.statusCode ?? 200,
message: response.statusMessage,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// 下载文件
Future<void> downloadFile(
String url,
String savePath, {
ProgressCallback? onReceiveProgress,
CancelToken? cancelToken,
}) async {
try {
await _dio.download(
url,
savePath,
onReceiveProgress: onReceiveProgress,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// 错误处理
Exception _handleError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return Exception('网络连接超时,请检查网络设置');
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
final message = error.response?.data?['message'] ?? '请求失败';
switch (statusCode) {
case 400:
return Exception('请求参数错误: $message');
case 401:
return Exception('认证失败,请重新登录');
case 403:
return Exception('权限不足: $message');
case 404:
return Exception('请求的资源不存在');
case 500:
return Exception('服务器内部错误,请稍后重试');
default:
return Exception('请求失败($statusCode): $message');
}
case DioExceptionType.cancel:
return Exception('请求已取消');
case DioExceptionType.connectionError:
return Exception('网络连接失败,请检查网络设置');
case DioExceptionType.unknown:
default:
return Exception('未知错误: ${error.message}');
}
}
// 取消所有请求
void cancelRequests() {
_dio.close();
}
}

View File

@@ -0,0 +1,378 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
// 音频录制状态
enum RecordingState {
idle,
recording,
paused,
stopped,
}
// 音频播放状态
enum PlaybackState {
idle,
playing,
paused,
stopped,
}
class AudioService {
// 录制相关
RecordingState _recordingState = RecordingState.idle;
String? _currentRecordingPath;
DateTime? _recordingStartTime;
// 播放相关
PlaybackState _playbackState = PlaybackState.idle;
String? _currentPlayingPath;
// 回调函数
Function(RecordingState)? onRecordingStateChanged;
Function(PlaybackState)? onPlaybackStateChanged;
Function(Duration)? onRecordingProgress;
Function(Duration)? onPlaybackProgress;
Function(String)? onRecordingComplete;
Function()? onPlaybackComplete;
// Getters
RecordingState get recordingState => _recordingState;
PlaybackState get playbackState => _playbackState;
String? get currentRecordingPath => _currentRecordingPath;
String? get currentPlayingPath => _currentPlayingPath;
bool get isRecording => _recordingState == RecordingState.recording;
bool get isPlaying => _playbackState == PlaybackState.playing;
// 初始化音频服务
Future<void> initialize() async {
// 请求麦克风权限
await _requestPermissions();
}
// 请求权限
Future<bool> _requestPermissions() async {
// Web平台不支持某些权限需要特殊处理
if (kIsWeb) {
// Web平台只需要麦克风权限且通过浏览器API处理
if (kDebugMode) {
print('Web平台跳过权限请求');
}
return true;
}
try {
final microphoneStatus = await Permission.microphone.request();
if (microphoneStatus != PermissionStatus.granted) {
throw Exception('需要麦克风权限才能录音');
}
// 存储权限在某些平台可能不需要
try {
final storageStatus = await Permission.storage.request();
if (storageStatus != PermissionStatus.granted) {
if (kDebugMode) {
print('存储权限未授予,但继续执行');
}
}
} catch (e) {
// 某些平台不支持存储权限,忽略错误
if (kDebugMode) {
print('存储权限请求失败(可能不支持): $e');
}
}
return true;
} catch (e) {
if (kDebugMode) {
print('权限请求失败: $e');
}
// 在某些平台上,权限请求可能失败,但仍然可以继续
return true;
}
}
// 开始录音
Future<void> startRecording({String? fileName}) async {
try {
if (_recordingState == RecordingState.recording) {
throw Exception('已经在录音中');
}
await _requestPermissions();
// 生成录音文件路径
if (kIsWeb) {
// Web平台使用内存存储或IndexedDB
fileName ??= 'recording_${DateTime.now().millisecondsSinceEpoch}.webm';
_currentRecordingPath = '/recordings/$fileName';
} else {
final directory = await getApplicationDocumentsDirectory();
final recordingsDir = Directory('${directory.path}/recordings');
if (!await recordingsDir.exists()) {
await recordingsDir.create(recursive: true);
}
fileName ??= 'recording_${DateTime.now().millisecondsSinceEpoch}.m4a';
_currentRecordingPath = '${recordingsDir.path}/$fileName';
}
// 这里应该使用实际的录音插件,比如 record 或 flutter_sound
// 由于没有实际的录音插件,这里只是模拟
_recordingStartTime = DateTime.now();
_setRecordingState(RecordingState.recording);
if (kDebugMode) {
print('开始录音: $_currentRecordingPath');
}
// 模拟录音进度更新
_startRecordingProgressTimer();
} catch (e) {
throw Exception('开始录音失败: ${e.toString()}');
}
}
// 停止录音
Future<String?> stopRecording() async {
try {
if (_recordingState != RecordingState.recording) {
throw Exception('当前没有在录音');
}
// 这里应该调用实际录音插件的停止方法
_setRecordingState(RecordingState.stopped);
final recordingPath = _currentRecordingPath;
if (kDebugMode) {
print('录音完成: $recordingPath');
}
// 通知录音完成
if (recordingPath != null && onRecordingComplete != null) {
onRecordingComplete!(recordingPath);
}
return recordingPath;
} catch (e) {
throw Exception('停止录音失败: ${e.toString()}');
}
}
// 暂停录音
Future<void> pauseRecording() async {
try {
if (_recordingState != RecordingState.recording) {
throw Exception('当前没有在录音');
}
// 这里应该调用实际录音插件的暂停方法
_setRecordingState(RecordingState.paused);
if (kDebugMode) {
print('录音已暂停');
}
} catch (e) {
throw Exception('暂停录音失败: ${e.toString()}');
}
}
// 恢复录音
Future<void> resumeRecording() async {
try {
if (_recordingState != RecordingState.paused) {
throw Exception('录音没有暂停');
}
// 这里应该调用实际录音插件的恢复方法
_setRecordingState(RecordingState.recording);
if (kDebugMode) {
print('录音已恢复');
}
} catch (e) {
throw Exception('恢复录音失败: ${e.toString()}');
}
}
// 播放音频
Future<void> playAudio(String audioPath) async {
try {
if (_playbackState == PlaybackState.playing) {
await stopPlayback();
}
_currentPlayingPath = audioPath;
// 这里应该使用实际的音频播放插件,比如 audioplayers 或 just_audio
// 由于没有实际的播放插件,这里只是模拟
_setPlaybackState(PlaybackState.playing);
if (kDebugMode) {
print('开始播放: $audioPath');
}
// 模拟播放进度和完成
_startPlaybackProgressTimer();
} catch (e) {
throw Exception('播放音频失败: ${e.toString()}');
}
}
// 暂停播放
Future<void> pausePlayback() async {
try {
if (_playbackState != PlaybackState.playing) {
throw Exception('当前没有在播放');
}
// 这里应该调用实际播放插件的暂停方法
_setPlaybackState(PlaybackState.paused);
if (kDebugMode) {
print('播放已暂停');
}
} catch (e) {
throw Exception('暂停播放失败: ${e.toString()}');
}
}
// 恢复播放
Future<void> resumePlayback() async {
try {
if (_playbackState != PlaybackState.paused) {
throw Exception('播放没有暂停');
}
// 这里应该调用实际播放插件的恢复方法
_setPlaybackState(PlaybackState.playing);
if (kDebugMode) {
print('播放已恢复');
}
} catch (e) {
throw Exception('恢复播放失败: ${e.toString()}');
}
}
// 停止播放
Future<void> stopPlayback() async {
try {
// 这里应该调用实际播放插件的停止方法
_setPlaybackState(PlaybackState.stopped);
_currentPlayingPath = null;
if (kDebugMode) {
print('播放已停止');
}
} catch (e) {
throw Exception('停止播放失败: ${e.toString()}');
}
}
// 获取音频文件时长
Future<Duration?> getAudioDuration(String audioPath) async {
try {
// 这里应该使用实际的音频插件获取时长
// 模拟返回时长
return const Duration(seconds: 30);
} catch (e) {
if (kDebugMode) {
print('获取音频时长失败: ${e.toString()}');
}
return null;
}
}
// 删除录音文件
Future<bool> deleteRecording(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
return true;
}
return false;
} catch (e) {
if (kDebugMode) {
print('删除录音文件失败: ${e.toString()}');
}
return false;
}
}
// 获取所有录音文件
Future<List<String>> getAllRecordings() async {
try {
final directory = await getApplicationDocumentsDirectory();
final recordingsDir = Directory('${directory.path}/recordings');
if (!await recordingsDir.exists()) {
return [];
}
final files = await recordingsDir.list().toList();
return files
.where((file) => file is File && file.path.endsWith('.m4a'))
.map((file) => file.path)
.toList();
} catch (e) {
if (kDebugMode) {
print('获取录音文件列表失败: ${e.toString()}');
}
return [];
}
}
// 私有方法:设置录音状态
void _setRecordingState(RecordingState state) {
_recordingState = state;
onRecordingStateChanged?.call(state);
}
// 私有方法:设置播放状态
void _setPlaybackState(PlaybackState state) {
_playbackState = state;
onPlaybackStateChanged?.call(state);
}
// 私有方法:录音进度计时器
void _startRecordingProgressTimer() {
// 这里应该实现实际的进度更新逻辑
// 模拟进度更新
}
// 私有方法:播放进度计时器
void _startPlaybackProgressTimer() {
// 这里应该实现实际的播放进度更新逻辑
// 模拟播放完成
Future.delayed(const Duration(seconds: 3), () {
_setPlaybackState(PlaybackState.stopped);
onPlaybackComplete?.call();
});
}
// 释放资源
void dispose() {
// 停止所有操作
if (_recordingState == RecordingState.recording) {
stopRecording();
}
if (_playbackState == PlaybackState.playing) {
stopPlayback();
}
// 清理回调
onRecordingStateChanged = null;
onPlaybackStateChanged = null;
onRecordingProgress = null;
onPlaybackProgress = null;
onRecordingComplete = null;
onPlaybackComplete = null;
}
}

View File

@@ -0,0 +1,327 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import '../models/user_model.dart';
import '../network/api_client.dart';
import '../network/api_endpoints.dart';
import '../errors/app_exception.dart';
/// 认证服务
class AuthService {
final ApiClient _apiClient;
AuthService(this._apiClient);
/// 登录
Future<AuthResponse> login({
required String account, // 用户名或邮箱
required String password,
bool rememberMe = false,
}) async {
try {
final response = await _apiClient.post(
ApiEndpoints.login,
data: {
'account': account,
'password': password,
},
);
// 后端返回格式: {code, message, data: {user, access_token, refresh_token, expires_in}}
final data = response.data['data'];
final userInfo = data['user'];
return AuthResponse(
user: User(
id: userInfo['id'].toString(),
username: userInfo['username'],
email: userInfo['email'],
avatar: userInfo['avatar'],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
token: data['access_token'],
refreshToken: data['refresh_token'],
expiresAt: DateTime.now().add(Duration(seconds: data['expires_in'])),
);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('登录失败: $e');
}
}
/// 用户注册
Future<AuthResponse> register({
required String email,
required String password,
required String username,
required String nickname,
}) async {
try {
final response = await _apiClient.post(
ApiEndpoints.register,
data: {
'email': email,
'username': username,
'password': password,
'nickname': nickname,
},
);
// 后端返回的数据结构需要转换
final data = response.data['data'];
final userInfo = data['user'];
return AuthResponse(
user: User(
id: userInfo['id'].toString(),
username: userInfo['username'],
email: userInfo['email'],
avatar: userInfo['avatar'],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
token: data['access_token'],
refreshToken: data['refresh_token'],
expiresAt: DateTime.now().add(Duration(seconds: data['expires_in'])),
);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('注册失败: $e');
}
}
/// 第三方登录
Future<AuthResponse> socialLogin({
required String provider,
required String accessToken,
}) async {
try {
final response = await _apiClient.post(
ApiEndpoints.socialLogin,
data: {
'provider': provider,
'access_token': accessToken,
},
);
return AuthResponse.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('第三方登录失败: $e');
}
}
/// 忘记密码
Future<void> forgotPassword(String email) async {
try {
await _apiClient.post(
ApiEndpoints.forgotPassword,
data: {'email': email},
);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('发送重置密码邮件失败: $e');
}
}
/// 重置密码
Future<void> resetPassword({
required String token,
required String newPassword,
required String confirmPassword,
}) async {
try {
await _apiClient.post(
ApiEndpoints.resetPassword,
data: {
'token': token,
'new_password': newPassword,
'confirm_password': confirmPassword,
},
);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('重置密码失败: $e');
}
}
/// 修改密码
Future<void> changePassword({
required String currentPassword,
required String newPassword,
required String confirmPassword,
}) async {
try {
await _apiClient.put(
ApiEndpoints.changePassword,
data: {
'current_password': currentPassword,
'new_password': newPassword,
'confirm_password': confirmPassword,
},
);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('修改密码失败: $e');
}
}
/// 刷新Token
Future<TokenRefreshResponse> refreshToken(String refreshToken) async {
try {
final response = await _apiClient.post(
ApiEndpoints.refreshToken,
data: {'refresh_token': refreshToken},
);
return TokenRefreshResponse.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('刷新Token失败: $e');
}
}
/// 登出
Future<void> logout() async {
try {
await _apiClient.post(ApiEndpoints.logout);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('登出失败: $e');
}
}
/// 获取用户信息
Future<User> getUserInfo() async {
try {
final response = await _apiClient.get(ApiEndpoints.userInfo);
return User.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('获取用户信息失败: $e');
}
}
/// 获取当前用户信息getUserInfo的别名
Future<User> getCurrentUser() async {
return await getUserInfo();
}
/// 更新用户信息
Future<User> updateUserInfo(Map<String, dynamic> data) async {
try {
final response = await _apiClient.put(
ApiEndpoints.userInfo,
data: data,
);
return User.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('更新用户信息失败: $e');
}
}
/// 验证邮箱
Future<void> verifyEmail(String token) async {
try {
await _apiClient.post(
ApiEndpoints.verifyEmail,
data: {'token': token},
);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('验证邮箱失败: $e');
}
}
/// 重新发送验证邮件
Future<void> resendVerificationEmail() async {
try {
await _apiClient.post(ApiEndpoints.resendVerificationEmail);
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('发送验证邮件失败: $e');
}
}
/// 检查用户名是否可用
Future<bool> checkUsernameAvailability(String username) async {
try {
final response = await _apiClient.get(
'${ApiEndpoints.checkUsername}?username=$username',
);
return response.data['available'] ?? false;
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('检查用户名失败: $e');
}
}
/// 检查邮箱是否可用
Future<bool> checkEmailAvailability(String email) async {
try {
final response = await _apiClient.get(
'${ApiEndpoints.checkEmail}?email=$email',
);
return response.data['available'] ?? false;
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AppException('检查邮箱失败: $e');
}
}
/// 处理Dio异常
AppException _handleDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return NetworkException('连接超时');
case DioExceptionType.sendTimeout:
return NetworkException('发送超时');
case DioExceptionType.receiveTimeout:
return NetworkException('接收超时');
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final message = e.response?.data?['message'] ?? '请求失败';
switch (statusCode) {
case 400:
return ValidationException(message);
case 401:
return AuthException('认证失败');
case 403:
return AuthException('权限不足');
case 404:
return AppException('资源不存在');
case 422:
return ValidationException(message);
case 500:
return ServerException('服务器内部错误');
default:
return AppException('请求失败: $message');
}
case DioExceptionType.cancel:
return AppException('请求已取消');
case DioExceptionType.connectionError:
return NetworkException('网络连接错误');
case DioExceptionType.badCertificate:
return NetworkException('证书错误');
case DioExceptionType.unknown:
default:
return AppException('未知错误: ${e.message}');
}
}
}

View File

@@ -0,0 +1,252 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../storage/storage_service.dart';
/// API数据缓存服务
class CacheService {
static const String _cachePrefix = 'api_cache_';
static const String _timestampPrefix = 'cache_timestamp_';
static const Duration _defaultCacheDuration = Duration(minutes: 30);
/// 设置缓存
static Future<void> setCache(
String key,
dynamic data, {
Duration? duration,
}) async {
try {
final cacheKey = _cachePrefix + key;
final timestampKey = _timestampPrefix + key;
// 存储数据
final jsonData = jsonEncode(data);
await StorageService.setString(cacheKey, jsonData);
// 存储时间戳
final timestamp = DateTime.now().millisecondsSinceEpoch;
await StorageService.setInt(timestampKey, timestamp);
if (kDebugMode) {
print('Cache set for key: $key');
}
} catch (e) {
if (kDebugMode) {
print('Error setting cache for key $key: $e');
}
}
}
/// 获取缓存
static Future<T?> getCache<T>(
String key, {
Duration? duration,
T Function(Map<String, dynamic>)? fromJson,
}) async {
try {
final cacheKey = _cachePrefix + key;
final timestampKey = _timestampPrefix + key;
// 检查缓存是否存在
final cachedData = await StorageService.getString(cacheKey);
final timestamp = await StorageService.getInt(timestampKey);
if (cachedData == null || timestamp == null) {
return null;
}
// 检查缓存是否过期
final cacheDuration = duration ?? _defaultCacheDuration;
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final now = DateTime.now();
if (now.difference(cacheTime) > cacheDuration) {
// 缓存过期,删除缓存
await removeCache(key);
return null;
}
// 解析数据
final jsonData = jsonDecode(cachedData);
if (fromJson != null && jsonData is Map<String, dynamic>) {
return fromJson(jsonData);
}
return jsonData as T;
} catch (e) {
if (kDebugMode) {
print('Error getting cache for key $key: $e');
}
return null;
}
}
/// 获取列表缓存
static Future<List<T>?> getListCache<T>(
String key, {
Duration? duration,
required T Function(Map<String, dynamic>) fromJson,
}) async {
try {
final cacheKey = _cachePrefix + key;
final timestampKey = _timestampPrefix + key;
// 检查缓存是否存在
final cachedData = await StorageService.getString(cacheKey);
final timestamp = await StorageService.getInt(timestampKey);
if (cachedData == null || timestamp == null) {
return null;
}
// 检查缓存是否过期
final cacheDuration = duration ?? _defaultCacheDuration;
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final now = DateTime.now();
if (now.difference(cacheTime) > cacheDuration) {
// 缓存过期,删除缓存
await removeCache(key);
return null;
}
// 解析数据
final jsonData = jsonDecode(cachedData);
if (jsonData is List) {
return jsonData
.map((item) => fromJson(item as Map<String, dynamic>))
.toList();
}
return null;
} catch (e) {
if (kDebugMode) {
print('Error getting list cache for key $key: $e');
}
return null;
}
}
/// 检查缓存是否有效
static Future<bool> isCacheValid(
String key, {
Duration? duration,
}) async {
try {
final timestampKey = _timestampPrefix + key;
final timestamp = await StorageService.getInt(timestampKey);
if (timestamp == null) {
return false;
}
final cacheDuration = duration ?? _defaultCacheDuration;
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final now = DateTime.now();
return now.difference(cacheTime) <= cacheDuration;
} catch (e) {
return false;
}
}
/// 移除缓存
static Future<void> removeCache(String key) async {
try {
final cacheKey = _cachePrefix + key;
final timestampKey = _timestampPrefix + key;
await StorageService.remove(cacheKey);
await StorageService.remove(timestampKey);
if (kDebugMode) {
print('Cache removed for key: $key');
}
} catch (e) {
if (kDebugMode) {
print('Error removing cache for key $key: $e');
}
}
}
/// 清空所有缓存
static Future<void> clearAllCache() async {
try {
final keys = StorageService.getKeys();
final cacheKeys = keys.where((key) =>
key.startsWith(_cachePrefix) || key.startsWith(_timestampPrefix));
for (final key in cacheKeys) {
await StorageService.remove(key);
}
if (kDebugMode) {
print('All cache cleared');
}
} catch (e) {
if (kDebugMode) {
print('Error clearing all cache: $e');
}
}
}
/// 获取缓存大小信息
static Future<Map<String, int>> getCacheInfo() async {
try {
final keys = StorageService.getKeys();
final cacheKeys = keys.where((key) => key.startsWith(_cachePrefix));
final timestampKeys = keys.where((key) => key.startsWith(_timestampPrefix));
int totalSize = 0;
for (final key in cacheKeys) {
final data = await StorageService.getString(key);
if (data != null) {
totalSize += data.length;
}
}
return {
'count': cacheKeys.length,
'size': totalSize,
'timestamps': timestampKeys.length,
};
} catch (e) {
return {
'count': 0,
'size': 0,
'timestamps': 0,
};
}
}
/// 清理过期缓存
static Future<void> cleanExpiredCache() async {
try {
final keys = StorageService.getKeys();
final timestampKeys = keys.where((key) => key.startsWith(_timestampPrefix));
for (final timestampKey in timestampKeys) {
final timestamp = await StorageService.getInt(timestampKey);
if (timestamp != null) {
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final now = DateTime.now();
if (now.difference(cacheTime) > _defaultCacheDuration) {
final cacheKey = timestampKey.replaceFirst(_timestampPrefix, _cachePrefix);
await StorageService.remove(cacheKey);
await StorageService.remove(timestampKey);
}
}
}
if (kDebugMode) {
print('Expired cache cleaned');
}
} catch (e) {
if (kDebugMode) {
print('Error cleaning expired cache: $e');
}
}
}
}

View File

@@ -0,0 +1,288 @@
import 'package:flutter/foundation.dart';
import 'package:dio/dio.dart';
import '../network/api_client.dart';
import '../services/cache_service.dart';
import '../models/api_response.dart';
import '../storage/storage_service.dart';
/// 增强版API服务集成缓存功能
class EnhancedApiService {
final ApiClient _apiClient = ApiClient.instance;
/// GET请求支持缓存
Future<ApiResponse<T>> get<T>(
String endpoint, {
Map<String, dynamic>? queryParameters,
bool useCache = true,
Duration? cacheDuration,
T Function(Map<String, dynamic>)? fromJson,
}) async {
try {
// 生成缓存键
final cacheKey = _generateCacheKey('GET', endpoint, queryParameters);
// 尝试从缓存获取数据
if (useCache) {
final cachedData = await CacheService.getCache<T>(
cacheKey,
duration: cacheDuration,
fromJson: fromJson,
);
if (cachedData != null) {
if (kDebugMode) {
print('Cache hit for: $endpoint');
}
return ApiResponse.success(
data: cachedData,
message: 'Data from cache',
);
}
}
// 发起网络请求
final response = await _apiClient.get(
endpoint,
queryParameters: queryParameters,
);
if (response.statusCode == 200 && response.data != null) {
// 缓存成功响应的数据
if (useCache) {
await CacheService.setCache(
cacheKey,
response.data,
duration: cacheDuration,
);
}
return ApiResponse.success(
data: fromJson != null ? fromJson(response.data) : response.data,
message: 'Success',
);
}
return ApiResponse.error(
message: 'Request failed with status: ${response.statusCode}',
code: response.statusCode,
);
} catch (e) {
if (kDebugMode) {
print('Error in enhanced GET request: $e');
}
return ApiResponse.error(
message: e.toString(),
);
}
}
/// POST请求
Future<ApiResponse<T>> post<T>(
String endpoint, {
Map<String, dynamic>? data,
bool invalidateCache = true,
List<String>? cacheKeysToInvalidate,
T Function(Map<String, dynamic>)? fromJson,
}) async {
try {
final response = await _apiClient.post(
endpoint,
data: data,
);
if (response.statusCode == 200 || response.statusCode == 201) {
// POST请求成功后清除相关缓存
if (invalidateCache) {
await _invalidateRelatedCache(endpoint, cacheKeysToInvalidate);
}
return ApiResponse.success(
data: fromJson != null && response.data != null ? fromJson(response.data) : response.data,
message: 'Success',
);
}
return ApiResponse.error(
message: 'Request failed with status: ${response.statusCode}',
code: response.statusCode,
);
} catch (e) {
if (kDebugMode) {
print('Error in enhanced POST request: $e');
}
return ApiResponse.error(
message: e.toString(),
);
}
}
/// PUT请求
Future<ApiResponse<T>> put<T>(
String endpoint, {
Map<String, dynamic>? data,
bool invalidateCache = true,
List<String>? cacheKeysToInvalidate,
T Function(Map<String, dynamic>)? fromJson,
}) async {
try {
final response = await _apiClient.put(
endpoint,
data: data,
);
if (response.statusCode == 200) {
// PUT请求成功后清除相关缓存
if (invalidateCache) {
await _invalidateRelatedCache(endpoint, cacheKeysToInvalidate);
}
return ApiResponse.success(
data: fromJson != null && response.data != null ? fromJson(response.data) : response.data,
message: 'Success',
);
}
return ApiResponse.error(
message: 'Request failed with status: ${response.statusCode}',
code: response.statusCode,
);
} catch (e) {
if (kDebugMode) {
print('Error in enhanced PUT request: $e');
}
return ApiResponse.error(
message: e.toString(),
);
}
}
/// DELETE请求
Future<ApiResponse<T>> delete<T>(
String endpoint, {
bool invalidateCache = true,
List<String>? cacheKeysToInvalidate,
}) async {
try {
final response = await _apiClient.delete(
endpoint,
);
if (response.statusCode == 200 || response.statusCode == 204) {
// DELETE请求成功后清除相关缓存
if (invalidateCache) {
await _invalidateRelatedCache(endpoint, cacheKeysToInvalidate);
}
return ApiResponse.success(
data: null,
message: 'Success',
);
}
return ApiResponse.error(
message: 'Request failed with status: ${response.statusCode}',
code: response.statusCode,
);
} catch (e) {
if (kDebugMode) {
print('Error in enhanced DELETE request: $e');
}
return ApiResponse.error(
message: e.toString(),
);
}
}
/// 生成缓存键
String _generateCacheKey(
String method,
String endpoint,
Map<String, dynamic>? queryParameters,
) {
final buffer = StringBuffer();
buffer.write(method);
buffer.write('_');
buffer.write(endpoint.replaceAll('/', '_'));
if (queryParameters != null && queryParameters.isNotEmpty) {
final sortedKeys = queryParameters.keys.toList()..sort();
for (final key in sortedKeys) {
buffer.write('_${key}_${queryParameters[key]}');
}
}
return buffer.toString();
}
/// 清除相关缓存
Future<void> _invalidateRelatedCache(
String endpoint,
List<String>? specificKeys,
) async {
try {
// 清除指定的缓存键
if (specificKeys != null) {
for (final key in specificKeys) {
await CacheService.removeCache(key);
}
}
// 清除与当前端点相关的缓存
final endpointKey = endpoint.replaceAll('/', '_');
final keys = StorageService.getKeys();
final relatedKeys = keys.where((key) => key.contains(endpointKey));
for (final key in relatedKeys) {
final cacheKey = key.replaceFirst('api_cache_', '');
await CacheService.removeCache(cacheKey);
}
if (kDebugMode) {
print('Cache invalidated for endpoint: $endpoint');
}
} catch (e) {
if (kDebugMode) {
print('Error invalidating cache: $e');
}
}
}
/// 预加载数据到缓存
Future<void> preloadCache(
String endpoint, {
Map<String, dynamic>? queryParameters,
Duration? cacheDuration,
}) async {
try {
await get(
endpoint,
queryParameters: queryParameters,
useCache: true,
cacheDuration: cacheDuration,
);
if (kDebugMode) {
print('Cache preloaded for: $endpoint');
}
} catch (e) {
if (kDebugMode) {
print('Error preloading cache: $e');
}
}
}
/// 获取缓存信息
Future<Map<String, int>> getCacheInfo() async {
return await CacheService.getCacheInfo();
}
/// 清理过期缓存
Future<void> cleanExpiredCache() async {
await CacheService.cleanExpiredCache();
}
/// 清空所有缓存
Future<void> clearAllCache() async {
await CacheService.clearAllCache();
}
}

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