Initial project
40
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
# 合并 PR 后触发(branch protection 保证只有 merge 能到达这里)
|
||||||
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
|
# PR 提交/更新时触发,main 和 dev 都接受 PR
|
||||||
|
pull_request:
|
||||||
|
branches: [main, dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Flutter (stable)
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install Melos
|
||||||
|
run: dart pub global activate melos
|
||||||
|
|
||||||
|
- name: Deep clean
|
||||||
|
run: melos run clean:deep
|
||||||
|
|
||||||
|
- name: Bootstrap
|
||||||
|
run: melos bootstrap
|
||||||
|
|
||||||
|
- name: Generate code
|
||||||
|
run: melos run gen
|
||||||
|
|
||||||
|
- name: Analyze
|
||||||
|
run: melos run analyze
|
||||||
|
|
||||||
72
.gitignore
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Dart/Flutter
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
||||||
|
*.g.dart
|
||||||
|
*.freezed.dart
|
||||||
|
*.mocks.dart
|
||||||
|
# SDK packages do not commit lock files; root workspace lock is tracked
|
||||||
|
packages/*/pubspec.lock
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
**/ios/Pods/
|
||||||
|
**/ios/.symlinks/
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/ephemeral/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/DerivedData/
|
||||||
|
**/Podfile.lock
|
||||||
|
|
||||||
|
# Android
|
||||||
|
**/android/.gradle/
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
**/android/**/GeneratedPluginRegistrant.kt
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
**/macos/Flutter/ephemeral/
|
||||||
|
**/macos/Pods/
|
||||||
|
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
**/windows/flutter/ephemeral/
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Melos
|
||||||
|
.melos/
|
||||||
|
|
||||||
|
# Mason
|
||||||
|
.mason/
|
||||||
|
mason-lock.json
|
||||||
|
|
||||||
|
# System
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Claude Code (local instructions, not shared)
|
||||||
|
CLAUDE.local.md
|
||||||
|
|
||||||
|
# 架构文档备份(本地临时文件,不入库)
|
||||||
|
Doc/*.bak.html
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
9865
Doc/IM_App_架构设计.html
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# customer-im-client
|
||||||
|
|
||||||
|
架构文档在 `Doc/IM_App_架构设计.html`,clone 到本地后用浏览器直接打开。
|
||||||
49
analysis_options.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- "**/*.g.dart"
|
||||||
|
- "**/*.freezed.dart"
|
||||||
|
- "mason/**"
|
||||||
|
language:
|
||||||
|
strict-casts: true
|
||||||
|
strict-inference: true
|
||||||
|
strict-raw-types: true
|
||||||
|
errors:
|
||||||
|
missing_required_param: error
|
||||||
|
missing_return: error
|
||||||
|
todo: ignore
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
# 架构规则
|
||||||
|
avoid_classes_with_only_static_members: true
|
||||||
|
prefer_final_fields: true
|
||||||
|
|
||||||
|
# 代码风格
|
||||||
|
prefer_single_quotes: true
|
||||||
|
require_trailing_commas: true
|
||||||
|
sort_child_properties_last: true
|
||||||
|
prefer_const_constructors: true
|
||||||
|
prefer_const_declarations: true
|
||||||
|
prefer_const_literals_to_create_immutables: true
|
||||||
|
prefer_final_locals: true
|
||||||
|
|
||||||
|
# 命名规则
|
||||||
|
camel_case_types: true
|
||||||
|
non_constant_identifier_names: true
|
||||||
|
constant_identifier_names: true
|
||||||
|
|
||||||
|
# 代码质量
|
||||||
|
avoid_print: true
|
||||||
|
avoid_empty_else: true
|
||||||
|
no_duplicate_case_values: true
|
||||||
|
unawaited_futures: true
|
||||||
|
avoid_unnecessary_containers: true
|
||||||
|
|
||||||
|
# 性能
|
||||||
|
avoid_function_literals_in_foreach_calls: true
|
||||||
|
prefer_collection_literals: true
|
||||||
|
|
||||||
|
# 安全性
|
||||||
|
avoid_web_libraries_in_flutter: true
|
||||||
45
apps/im_app/.gitignore
vendored
Normal 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
|
||||||
30
apps/im_app/.metadata
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 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: "ff37bef603469fb030f2b72995ab929ccfc227f0"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||||
|
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||||
|
- platform: ios
|
||||||
|
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||||
|
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||||
|
|
||||||
|
# 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'
|
||||||
3
apps/im_app/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# im_app
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
1
apps/im_app/analysis_options.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
14
apps/im_app/android/.gitignore
vendored
Normal 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
|
||||||
76
apps/im_app/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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.cusotmer.im.im_app"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.cusotmer.im.im_app"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
// 只支持 arm64-v8a,减少包体积(现代 Android 设备均为 64-bit)
|
||||||
|
abiFilters += "arm64-v8a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
// 排除其他架构的 .so,防止第三方库把 armeabi-v7a / x86 带进包
|
||||||
|
excludes += setOf(
|
||||||
|
"lib/armeabi-v7a/**",
|
||||||
|
"lib/x86/**",
|
||||||
|
"lib/x86_64/**",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// R8 代码压缩:移除未使用的 Java/Kotlin 代码(来自依赖库)
|
||||||
|
isMinifyEnabled = true
|
||||||
|
// 资源压缩:移除未引用的 drawable / layout / string
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
|
}
|
||||||
9
apps/im_app/android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Flutter
|
||||||
|
-keep class io.flutter.** { *; }
|
||||||
|
-keep class io.flutter.plugins.** { *; }
|
||||||
|
-dontwarn io.flutter.embedding.**
|
||||||
|
|
||||||
|
# Dart / Flutter engine(避免混淆后反射找不到类)
|
||||||
|
-keep class com.cusotmer.im.** { *; }
|
||||||
|
|
||||||
|
# 如有第三方 SDK 需要 keep,在此追加
|
||||||
7
apps/im_app/android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||||
45
apps/im_app/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="im_app"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.cusotmer.im.im_app
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
BIN
apps/im_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
apps/im_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
18
apps/im_app/android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||||
18
apps/im_app/android/app/src/main/res/values/styles.xml
Normal 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>
|
||||||
7
apps/im_app/android/app/src/profile/AndroidManifest.xml
Normal 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>
|
||||||
31
apps/im_app/android/build.gradle.kts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress obsolete -source/-target 8 warnings from third-party packages
|
||||||
|
subprojects {
|
||||||
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
|
options.compilerArgs.add("-Xlint:-options")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
2
apps/im_app/android/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
5
apps/im_app/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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.14-all.zip
|
||||||
26
apps/im_app/android/settings.gradle.kts
Normal 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.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
6
apps/im_app/build.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
targets:
|
||||||
|
$default:
|
||||||
|
builders:
|
||||||
|
drift_dev|preparing_builder:
|
||||||
|
generate_for:
|
||||||
|
- lib/**
|
||||||
4
apps/im_app/config/config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"IS_DEV": true,
|
||||||
|
"API_BASE_URL": "https://dev-api.example.com"
|
||||||
|
}
|
||||||
34
apps/im_app/ios/.gitignore
vendored
Normal 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
|
||||||
24
apps/im_app/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
2
apps/im_app/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
2
apps/im_app/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
42
apps/im_app/ios/Podfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
platform :ios, '17.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
750
apps/im_app/ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
// !$*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 */; };
|
||||||
|
4AF8AC8CCD305DA5219C08B6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D1569589D1A8DD0B778AFE6 /* Pods_Runner.framework */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
|
80500FBD20ABA2A35C57BC6B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C85B542015499C5482C6CB8 /* Pods_RunnerTests.framework */; };
|
||||||
|
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>"; };
|
||||||
|
1C416905D0EA345032C4E612 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
1D1569589D1A8DD0B778AFE6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
2742F60AD86140513F6B3160 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; 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>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
8C85B542015499C5482C6CB8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
90ECAABE4B55AAF434605D4C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
95060CD06CB7511C6AB7758C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
9538107A41BCB5B5D84FBAF3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.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>"; };
|
||||||
|
F1DAD66556D3A4B4CEF9C370 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
4AF8AC8CCD305DA5219C08B6 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
9BC6F15E3BE2CA9CB90D9F51 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
80500FBD20ABA2A35C57BC6B /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
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 */,
|
||||||
|
E9F81EBB1CF574A7BCCFB55C /* Pods */,
|
||||||
|
99A2F5FCB14B04FC61850769 /* Frameworks */,
|
||||||
|
);
|
||||||
|
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 */,
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
99A2F5FCB14B04FC61850769 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1D1569589D1A8DD0B778AFE6 /* Pods_Runner.framework */,
|
||||||
|
8C85B542015499C5482C6CB8 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E9F81EBB1CF574A7BCCFB55C /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
95060CD06CB7511C6AB7758C /* Pods-Runner.debug.xcconfig */,
|
||||||
|
F1DAD66556D3A4B4CEF9C370 /* Pods-Runner.release.xcconfig */,
|
||||||
|
90ECAABE4B55AAF434605D4C /* Pods-Runner.profile.xcconfig */,
|
||||||
|
2742F60AD86140513F6B3160 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
1C416905D0EA345032C4E612 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
9538107A41BCB5B5D84FBAF3 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
48B3DF5D42F51925F4815036 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
9BC6F15E3BE2CA9CB90D9F51 /* Frameworks */,
|
||||||
|
);
|
||||||
|
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 = (
|
||||||
|
E32D12D1F8E7FA54A9FB59C5 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
307BDBCBDA303E47FEA59E4A /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
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 */
|
||||||
|
307BDBCBDA303E47FEA59E4A /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
48B3DF5D42F51925F4815036 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
E32D12D1F8E7FA54A9FB59C5 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* 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 */,
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 2742F60AD86140513F6B3160 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
|
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.cusotmer.im.imApp.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;
|
||||||
|
baseConfigurationReference = 1C416905D0EA345032C4E612 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
|
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.cusotmer.im.imApp.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;
|
||||||
|
baseConfigurationReference = 9538107A41BCB5B5D84FBAF3 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
|
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.cusotmer.im.imApp.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 = 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;
|
||||||
|
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 = 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;
|
||||||
|
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;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
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;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
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 */;
|
||||||
|
}
|
||||||
7
apps/im_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,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>
|
||||||
@@ -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>
|
||||||
10
apps/im_app/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,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>
|
||||||
25
apps/im_app/ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
|
||||||
|
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
let sceneConfig = UISceneConfiguration(
|
||||||
|
name: "Default Configuration",
|
||||||
|
sessionRole: connectingSceneSession.role
|
||||||
|
)
|
||||||
|
sceneConfig.delegateClass = SceneDelegate.self
|
||||||
|
return sceneConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FlutterImplicitEngineDelegate
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
23
apps/im_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/im_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
apps/im_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
apps/im_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
apps/im_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal 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.
|
||||||
37
apps/im_app/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal 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>
|
||||||
26
apps/im_app/ios/Runner/Base.lproj/Main.storyboard
Normal 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>
|
||||||
70
apps/im_app/ios/Runner/Info.plist
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Im App</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>im_app</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>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</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>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
apps/im_app/ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
29
apps/im_app/ios/Runner/SceneDelegate.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import UIKit
|
||||||
|
import Flutter
|
||||||
|
|
||||||
|
@objc class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
var flutterViewController: FlutterViewController?
|
||||||
|
|
||||||
|
override func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
|
guard let windowScene = scene as? UIWindowScene else { return }
|
||||||
|
|
||||||
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
|
let flutterVC = FlutterViewController()
|
||||||
|
flutterViewController = flutterVC
|
||||||
|
|
||||||
|
window?.rootViewController = flutterVC
|
||||||
|
window?.makeKeyAndVisible()
|
||||||
|
|
||||||
|
registerSceneLifeCycle(with: flutterVC.engine)
|
||||||
|
|
||||||
|
super.scene(scene, willConnectTo: session, options: connectionOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
|
if let flutterVC = flutterViewController {
|
||||||
|
self.unregisterSceneLifeCycle(with: flutterVC.engine)
|
||||||
|
}
|
||||||
|
super.sceneDidDisconnect(scene)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/im_app/ios/RunnerTests/RunnerTests.swift
Normal 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
71
apps/im_app/lib/app/app.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../core/ui/base/app_theme.dart';
|
||||||
|
import 'di/app_providers.dart';
|
||||||
|
import 'di/network_provider.dart';
|
||||||
|
import 'router/app_router.dart';
|
||||||
|
|
||||||
|
/// 应用根组件
|
||||||
|
///
|
||||||
|
/// 职责:
|
||||||
|
/// - 路由配置(go_router,含登录守卫)
|
||||||
|
/// - 主题配置(亮色 / 暗色 / 跟随系统)
|
||||||
|
/// - App 生命周期监听(前后台切换 → WebSocket 断连/重连)
|
||||||
|
/// - 启动初始化([AppInitializer] 两阶段串行队列)
|
||||||
|
///
|
||||||
|
/// ## 启动初始化
|
||||||
|
///
|
||||||
|
/// 通过 [appInitializerProvider] 声明任务,两阶段串行执行:
|
||||||
|
/// - Critical:首帧前,只放必须阻塞的任务
|
||||||
|
/// - Deferred:首帧后,不争抢资源、不卡 UI
|
||||||
|
///
|
||||||
|
/// 详见 [AppInitializer] 文档。
|
||||||
|
class IMApp extends ConsumerStatefulWidget {
|
||||||
|
const IMApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<IMApp> createState() => _IMAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IMAppState extends ConsumerState<IMApp> with WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
ref.read(appInitializerProvider).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
final socketManager = ref.read(socketManagerProvider);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
socketManager.onEnterForeground();
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
socketManager.onEnterBackground();
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
case AppLifecycleState.hidden:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'IM App', // TODO: 接入国际化
|
||||||
|
theme: AppTheme.theme,
|
||||||
|
darkTheme: AppTheme.darkTheme,
|
||||||
|
themeMode: ref.watch(themeModeProvider),
|
||||||
|
routerConfig: ref.watch(routerProvider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/im_app/lib/app/bootstrap.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'app.dart';
|
||||||
|
|
||||||
|
void bootstrap() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// ProviderScope 包裹全局,network_provider 等 Provider 懒加载单例
|
||||||
|
runApp(
|
||||||
|
const ProviderScope(
|
||||||
|
child: IMApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
141
apps/im_app/lib/app/di/app_providers.dart
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../core/services/app_initializer.dart';
|
||||||
|
import 'network_provider.dart';
|
||||||
|
|
||||||
|
// ── 认证 ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 登录状态管理
|
||||||
|
///
|
||||||
|
/// 同时继承 [ChangeNotifier],作为 go_router [GoRouter.refreshListenable] 使用,
|
||||||
|
/// 登录 / 退出时 go_router 自动重新执行 redirect,无需手动触发。
|
||||||
|
///
|
||||||
|
/// ## 当前状态
|
||||||
|
///
|
||||||
|
/// Demo 实现,无持久化。storage_sdk 就绪后替换为:
|
||||||
|
/// - `build`:从安全存储读取 token,有则视为已登录
|
||||||
|
/// - `login` / `logout`:同步更新安全存储
|
||||||
|
class AuthNotifier extends ChangeNotifier {
|
||||||
|
bool _isLoggedIn = false;
|
||||||
|
|
||||||
|
bool get isLoggedIn => _isLoggedIn;
|
||||||
|
|
||||||
|
void login() {
|
||||||
|
_isLoggedIn = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void logout() {
|
||||||
|
_isLoggedIn = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录状态 Provider
|
||||||
|
///
|
||||||
|
/// 使用 [Provider] 持有 [AuthNotifier] 单例。
|
||||||
|
/// go_router 通过 [GoRouter.refreshListenable] 直接监听 [AuthNotifier](ChangeNotifier),
|
||||||
|
/// Riverpod 侧不需要响应式更新(导航由 go_router 接管)。
|
||||||
|
final authNotifierProvider = Provider<AuthNotifier>(
|
||||||
|
(ref) => AuthNotifier(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 主题 ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 主题模式 Notifier — 控制应用全局亮 / 暗主题
|
||||||
|
///
|
||||||
|
/// 启动时从持久化存储读取上次保存的主题模式,无则默认跟随系统。
|
||||||
|
/// 切换时先更新内存状态,再写入持久化存储。
|
||||||
|
///
|
||||||
|
/// ## storage_sdk 接入步骤
|
||||||
|
///
|
||||||
|
/// 1. 在 `build()` 解开 TODO:读取存储值作为初始模式
|
||||||
|
/// 2. 在 `setMode()` 解开 TODO:每次切换后写入存储
|
||||||
|
/// 3. 若存储接口是异步的,将 `Notifier<ThemeMode>` 改为
|
||||||
|
/// `AsyncNotifier<ThemeMode>`,`build()` 改为 `Future<ThemeMode>`
|
||||||
|
class ThemeModeNotifier extends Notifier<ThemeMode> {
|
||||||
|
@override
|
||||||
|
ThemeMode build() {
|
||||||
|
// TODO: storage_sdk 就绪后从持久化读取初始值:
|
||||||
|
// final saved = ref.read(themeStorageProvider).readThemeMode();
|
||||||
|
// return saved ?? ThemeMode.system;
|
||||||
|
return ThemeMode.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMode(ThemeMode mode) {
|
||||||
|
state = mode;
|
||||||
|
// TODO: storage_sdk 就绪后写入持久化:
|
||||||
|
// ref.read(themeStorageProvider).saveThemeMode(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 主题模式 Provider
|
||||||
|
///
|
||||||
|
/// ## Setting 页切换(只需一行)
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// ref.read(themeModeProvider.notifier).setMode(ThemeMode.system);
|
||||||
|
/// ref.read(themeModeProvider.notifier).setMode(ThemeMode.light);
|
||||||
|
/// ref.read(themeModeProvider.notifier).setMode(ThemeMode.dark);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 持久化(storage_sdk TODO)
|
||||||
|
///
|
||||||
|
/// 读取和写入的 TODO 均在 [ThemeModeNotifier] 内,接入 storage_sdk 后解开即可。
|
||||||
|
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||||
|
ThemeModeNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 启动初始化 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// AppInitializer Provider
|
||||||
|
///
|
||||||
|
/// 集中声明所有启动初始化任务,app.dart 只需一行 `.run()`。
|
||||||
|
///
|
||||||
|
/// ## 任务分类规则
|
||||||
|
///
|
||||||
|
/// 问自己:「这个任务不完成,用户能正常看到首页吗?」
|
||||||
|
/// - **不能** → 放 critical(谨慎,每多一个都拖慢启动)
|
||||||
|
/// - **能** → 放 deferred(绝大多数情况)
|
||||||
|
///
|
||||||
|
/// ## 当前任务清单
|
||||||
|
///
|
||||||
|
/// | 阶段 | 任务 | 说明 |
|
||||||
|
/// |---|---|---|
|
||||||
|
/// | Critical | NetworkMonitor | 后续 HTTP、WebSocket 都依赖网络状态 |
|
||||||
|
/// | Deferred | (待扩展) | 推送注册、登录态恢复、缓存预热等 |
|
||||||
|
final appInitializerProvider = Provider<AppInitializer>((ref) {
|
||||||
|
return AppInitializer(
|
||||||
|
onLog: (message, {tag}) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[${tag ?? 'AppInit'}] $message');
|
||||||
|
},
|
||||||
|
critical: [
|
||||||
|
// 网络监听必须最先就绪(后续 HTTP、WebSocket 都依赖它)
|
||||||
|
InitTask(
|
||||||
|
name: 'NetworkMonitor',
|
||||||
|
task: () => ref.read(networkMonitorProvider).initialize(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
deferred: [
|
||||||
|
// TODO: 推送注册
|
||||||
|
// InitTask(
|
||||||
|
// name: 'PushNotification',
|
||||||
|
// task: () => ref.read(pushServiceProvider).register(),
|
||||||
|
// ),
|
||||||
|
//
|
||||||
|
// TODO: 登录态恢复(从安全存储读取 token → 自动登录)
|
||||||
|
// InitTask(
|
||||||
|
// name: 'AuthRestore',
|
||||||
|
// task: () => ref.read(authRestoreUseCaseProvider).execute(),
|
||||||
|
// ),
|
||||||
|
//
|
||||||
|
// TODO: 缓存预热
|
||||||
|
// InitTask(
|
||||||
|
// name: 'CacheWarmup',
|
||||||
|
// task: () => ref.read(cacheServiceProvider).warmup(),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
24
apps/im_app/lib/app/di/db_provider.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:storage_sdk/storage_sdk.dart';
|
||||||
|
|
||||||
|
import '../../data/local/drift/app_database.dart';
|
||||||
|
|
||||||
|
/// 全局单例 StorageSdkApi,整个 App 生命周期内唯一实例。
|
||||||
|
///
|
||||||
|
/// storage_sdk 负责数据库连接生命周期和 CRUD 机制;
|
||||||
|
/// im_app 负责 schema(AppDatabase + 各业务表)。
|
||||||
|
///
|
||||||
|
/// 用法:
|
||||||
|
/// ```dart
|
||||||
|
/// // 登录后开库
|
||||||
|
/// await ref.read(storageSdkProvider).openDatabase(user.id);
|
||||||
|
///
|
||||||
|
/// // CRUD 示例
|
||||||
|
/// final db = ref.read(storageSdkProvider);
|
||||||
|
/// await db.insertOrReplace(appDb.users, companion);
|
||||||
|
/// ```
|
||||||
|
final storageSdkProvider = Provider<StorageSdkApi>((ref) {
|
||||||
|
return StorageSdkApi(
|
||||||
|
databaseFactory: (executor) => AppDatabase(executor),
|
||||||
|
);
|
||||||
|
});
|
||||||
404
apps/im_app/lib/app/di/network_provider.dart
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import '../../core/foundation/api_paths.dart';
|
||||||
|
import '../../core/foundation/config.dart';
|
||||||
|
import '../../core/foundation/constants.dart';
|
||||||
|
import '../../core/services/network_monitor.dart';
|
||||||
|
import '../../core/services/socket_manager.dart';
|
||||||
|
|
||||||
|
// ── 网络状态监听 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 网络状态监听 Provider(全局单例)
|
||||||
|
///
|
||||||
|
/// 基于 connectivity_plus 监听平台网络变化,
|
||||||
|
/// 作为公共服务供多个模块使用:
|
||||||
|
/// - SocketManager:网络变化时自动断连/重连 WebSocket
|
||||||
|
/// - HTTP 层:请求前检查网络可用性
|
||||||
|
/// - UI 层:显示网络状态提示
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // 查询当前状态
|
||||||
|
/// final isConnected = ref.read(networkMonitorProvider).isConnected;
|
||||||
|
///
|
||||||
|
/// // 监听状态变化
|
||||||
|
/// ref.listen(networkMonitorProvider, (prev, monitor) {
|
||||||
|
/// monitor.onStatusChanged.listen((isAvailable) {
|
||||||
|
/// // 处理网络变化
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||||
|
final monitor = NetworkMonitor(
|
||||||
|
onLog: (message, {tag}) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[${tag ?? 'Network'}] $message');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
monitor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return monitor;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── HTTP 基础设施 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// API 配置 Provider(全局单例)
|
||||||
|
///
|
||||||
|
/// 从 [AppConfig.apiBaseUrl](config.json → --dart-define-from-file)读取 baseURL,
|
||||||
|
/// 注入到 Network SDK 作为所有 HTTP 请求的基础 URL。
|
||||||
|
///
|
||||||
|
/// [onCheckNetworkAvailable] 由 [networkMonitorProvider](公共服务)注入,
|
||||||
|
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
|
||||||
|
final apiConfigProvider = Provider<ApiConfig>((ref) {
|
||||||
|
final networkMonitor = ref.read(networkMonitorProvider);
|
||||||
|
|
||||||
|
return ApiConfig(
|
||||||
|
baseURL: AppConfig.apiBaseUrl,
|
||||||
|
platformHeaders: {
|
||||||
|
'Platform': 'Android', // TODO: 运行时从平台 API 获取
|
||||||
|
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
|
||||||
|
},
|
||||||
|
tokenExpiredCodes: {30002, 30003, 30124},
|
||||||
|
forceLogoutCodes: {30125},
|
||||||
|
onForceLogout: () {
|
||||||
|
// TODO: 清除登录态,跳转登录页
|
||||||
|
},
|
||||||
|
onTokenRefresh: () async {
|
||||||
|
// TODO: App 层刷新 token 逻辑
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||||||
|
maxRetries: AppConstants.maxRetries,
|
||||||
|
retryBaseDelay: AppConstants.retryBaseDelay,
|
||||||
|
onLog: (message, {tag}) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[${tag ?? 'Network'}] $message');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// API 客户端 Provider(全局单例)
|
||||||
|
///
|
||||||
|
/// 含拦截器(Auth / Retry / Logging)、超时配置。
|
||||||
|
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
||||||
|
final config = ref.read(apiConfigProvider);
|
||||||
|
return NetworksSdkApi()..initialize(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// SocketConfig Provider(全局单例)
|
||||||
|
///
|
||||||
|
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
|
||||||
|
/// SDK 内部不调用其他 SDK。
|
||||||
|
final socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||||
|
final networkMonitor = ref.read(networkMonitorProvider);
|
||||||
|
|
||||||
|
return SocketConfig(
|
||||||
|
maxReconnectAttempts: AppConstants.maxRetries,
|
||||||
|
maxReconnectDelay: AppConstants.maxReconnectDelay,
|
||||||
|
onLog: (message, {tag}) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[${tag ?? 'Socket'}] $message');
|
||||||
|
},
|
||||||
|
onCheckNetworkAvailable: () async {
|
||||||
|
return networkMonitor.isConnected;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// SocketClient Provider(全局单例)
|
||||||
|
///
|
||||||
|
/// 与 apiClientProvider 对称。
|
||||||
|
final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
||||||
|
{
|
||||||
|
final config = ref.read(socketConfigProvider);
|
||||||
|
return NetworksMessagingApi()..initialize(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// SocketManager Provider
|
||||||
|
///
|
||||||
|
/// 封装连接生命周期、网络/前后台事件响应、操作前置检查、消息预处理。
|
||||||
|
/// 业务模块通过此 Provider 访问 WebSocket 能力。
|
||||||
|
///
|
||||||
|
/// ## 前置检查
|
||||||
|
///
|
||||||
|
/// connect / send 前先检查网络可用性 + 后台状态,
|
||||||
|
/// 无效操作直接跳过,避免无意义的网络请求。
|
||||||
|
/// 与 HTTP 层 [ApiClient.executeRequest] 的网络前置检查对称。
|
||||||
|
///
|
||||||
|
/// ## 事件驱动
|
||||||
|
///
|
||||||
|
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
|
||||||
|
/// 自动触发断连/重连。
|
||||||
|
///
|
||||||
|
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
|
||||||
|
/// 后续接入加解密 SDK 时,在此注入解密回调,
|
||||||
|
/// SDK 内部不调用其他 SDK。
|
||||||
|
final socketManagerProvider = Provider<SocketManager>((ref) {
|
||||||
|
final client = ref.read(socketClientProvider);
|
||||||
|
final networkMonitor = ref.read(networkMonitorProvider);
|
||||||
|
|
||||||
|
final manager = SocketManager(
|
||||||
|
client: client,
|
||||||
|
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
|
||||||
|
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
|
||||||
|
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||||||
|
onLog: (message, {tag}) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[${tag ?? 'SocketManager'}] $message');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
|
||||||
|
final subscription = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||||||
|
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
subscription.cancel();
|
||||||
|
unawaited(manager.dispose());
|
||||||
|
});
|
||||||
|
|
||||||
|
return manager;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 辅助 ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// HTTP baseURL → WebSocket URL 转换
|
||||||
|
///
|
||||||
|
/// https://api.example.com → wss://api.example.com/ws
|
||||||
|
/// http://api.example.com → ws://api.example.com/ws
|
||||||
|
String _buildWsUrl(String httpBaseUrl) {
|
||||||
|
String base = httpBaseUrl;
|
||||||
|
if (base.startsWith('https://')) {
|
||||||
|
base = base.replaceFirst('https://', 'wss://');
|
||||||
|
} else if (base.startsWith('http://')) {
|
||||||
|
base = base.replaceFirst('http://', 'ws://');
|
||||||
|
}
|
||||||
|
return '$base${ApiPaths.wsConnect}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 本文件的职责
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// 提供所有网络基础设施 Provider:网络监听 + HTTP + WebSocket。
|
||||||
|
// 业务模块的 DI 链路(Repository → UseCase 按需)
|
||||||
|
// 内聚在 features/{模块}/di/{模块}_providers.dart 中。
|
||||||
|
//
|
||||||
|
// di/ 目录只放「需要手动装配的 Provider」(构造注入、回调组合等)。
|
||||||
|
// ViewModel Provider 由 @riverpod 注解自动生成,不在 di/ 中。
|
||||||
|
//
|
||||||
|
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||||||
|
// │ DI 架构 │
|
||||||
|
// ├──────────────────────────────────────────────────────────────────────────┤
|
||||||
|
// │ app/di/ ← 手动装配:SDK 基础设施 │
|
||||||
|
// │ ├── app_providers.dart → 主题 + 启动初始化 │
|
||||||
|
// │ └── network_provider.dart → 网络监听 + HTTP + WebSocket │
|
||||||
|
// │ │
|
||||||
|
// │ features/{模块}/di/ ← 手动装配:业务模块 DI 链路 │
|
||||||
|
// │ └── auth_providers.dart → Repository → UseCase(按需) │
|
||||||
|
// │ chat_providers.dart 每个模块 DI 链路内聚在一个文件 │
|
||||||
|
// │ │
|
||||||
|
// │ features/{模块}/presentation/ ← @riverpod 自动生成:ViewModel │
|
||||||
|
// │ └── login_view_model.dart → loginViewModelProvider(代码生成) │
|
||||||
|
// └──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
//
|
||||||
|
// Provider 链路:
|
||||||
|
//
|
||||||
|
// networkMonitorProvider(公共服务,HTTP + WS 共用)
|
||||||
|
// ├── apiConfigProvider → apiClientProvider ← HTTP 层
|
||||||
|
// └── socketConfigProvider → socketClientProvider ← WS 层
|
||||||
|
// → socketManagerProvider
|
||||||
|
//
|
||||||
|
// 网络事件驱动链路:
|
||||||
|
//
|
||||||
|
// connectivity_plus(平台网络事件)
|
||||||
|
// → NetworkMonitor.onStatusChanged(true / false)
|
||||||
|
// → SocketManager.handleNetworkStatusChanged()
|
||||||
|
// → 断网: disconnect()
|
||||||
|
// → 恢复: connect(token: lastToken)
|
||||||
|
//
|
||||||
|
// 前后台事件驱动链路:
|
||||||
|
//
|
||||||
|
// WidgetsBindingObserver(App 层 app.dart)
|
||||||
|
// → SocketManager.onEnterBackground() → disconnect
|
||||||
|
// → SocketManager.onEnterForeground() → reconnect
|
||||||
|
//
|
||||||
|
// Repository 直接注入 ApiClient,通过回调注入其他 SDK 能力:
|
||||||
|
//
|
||||||
|
// onTokenUpdate: (token) {
|
||||||
|
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||||||
|
// secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 这样 network_sdk 和 crypto_sdk 互不依赖,App 层是唯一知道两者的地方。
|
||||||
|
//
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 新增接口的完整流程(以登录为例)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// 「一个接口 = 一个 Request 文件」,严格按层调用,禁止跳层。
|
||||||
|
//
|
||||||
|
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||||||
|
// │ 文件 & 职责总览 │
|
||||||
|
// ├──────────────────────────────────────────────────────────────────────────┤
|
||||||
|
// │ login_request.dart Request + Response DTO(一个端点一个文件) │
|
||||||
|
// │ auth_repository_impl.dart executeRequest → DTO → Entity + 回调写 Token│
|
||||||
|
// │ login_usecase.dart 格式校验 → 调 Repository(按需,非必须) │
|
||||||
|
// │ auth_providers.dart DI 装配(Repository → UseCase 按需) │
|
||||||
|
// │ login_view_model.dart ref.read(authRepositoryProvider).login() │
|
||||||
|
// │ 或 ref.read(loginUseCaseProvider).execute() │
|
||||||
|
// │ login_page.dart ref.watch(loginViewModelProvider) │
|
||||||
|
// └──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
//
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Step 1: 定义 Request(data/remote/login_request.dart)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// 一个文件包含两部分:Response DTO + Request 类。
|
||||||
|
//
|
||||||
|
// @JsonSerializable()
|
||||||
|
// class LoginData { // Response DTO
|
||||||
|
// final String token;
|
||||||
|
// final String userId;
|
||||||
|
// factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
|
||||||
|
// User toEntity() => User(id: userId, ...); // DTO → Domain Entity
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @ApiRequest(path: ApiPaths.authLogin, method: HttpMethod.post,
|
||||||
|
// responseType: LoginData, requestType: ApiRequestType.login)
|
||||||
|
// @JsonSerializable()
|
||||||
|
// class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||||||
|
// final String email;
|
||||||
|
// final String password;
|
||||||
|
// LoginRequest({required this.email, required this.password});
|
||||||
|
// @override Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// build_runner 生成 _$LoginRequestApi mixin → 自动提供 path / method / fromJson 注册。
|
||||||
|
//
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Step 2: Repository(data/repositories/auth_repository_impl.dart)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// class AuthRepositoryImpl implements AuthRepository {
|
||||||
|
// final ApiClient _client; // ← 直接注入 ApiClient
|
||||||
|
// final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
|
||||||
|
//
|
||||||
|
// Future<User> login({required String email, required String password}) async {
|
||||||
|
// final dto = await _client.executeRequest(
|
||||||
|
// LoginRequest(email: email, password: password),
|
||||||
|
// );
|
||||||
|
// _onTokenUpdate(dto!.token); // 回调写入 Token
|
||||||
|
// return dto.toEntity(); // DTO → Domain Entity
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Step 3: Provider 装配 + ViewModel
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// // --- Provider 装配(features/auth/di/auth_providers.dart) ---
|
||||||
|
//
|
||||||
|
// // Repository(直接注入 ApiClient + 回调组合多个 SDK 能力)
|
||||||
|
// final authRepositoryProvider = Provider((ref) {
|
||||||
|
// final apiConfig = ref.read(apiConfigProvider);
|
||||||
|
// return AuthRepositoryImpl(
|
||||||
|
// client: ref.read(apiClientProvider), // 直接注入
|
||||||
|
// onTokenUpdate: (token) {
|
||||||
|
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||||||
|
// // secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // UseCase(按需 — 登录有多步编排,所以需要)
|
||||||
|
// final loginUseCaseProvider = Provider((ref) {
|
||||||
|
// return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // --- ViewModel(features/auth/presentation/login_view_model.dart) ---
|
||||||
|
//
|
||||||
|
// // 常规写法:ViewModel 直接调 Repository
|
||||||
|
// @riverpod
|
||||||
|
// class LoginViewModel extends _$LoginViewModel {
|
||||||
|
// Future<void> login(String email, String password) async {
|
||||||
|
// state = state.copyWith(isLoading: true);
|
||||||
|
// try {
|
||||||
|
// final user = await ref.read(authRepositoryProvider).login(
|
||||||
|
// email: email, password: password,
|
||||||
|
// );
|
||||||
|
// state = state.copyWith(user: user, isLoading: false);
|
||||||
|
// } on ApiError catch (e) {
|
||||||
|
// state = state.copyWith(error: e.displayMessage, isLoading: false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 进阶写法:有 UseCase 时(格式校验 + 多步编排)
|
||||||
|
// // final user = await ref.read(loginUseCaseProvider).execute(
|
||||||
|
// // email: email, password: password,
|
||||||
|
// // );
|
||||||
|
//
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
// 内部执行链路(点击登录按钮后发生了什么)
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// View: vm.login(email, password)
|
||||||
|
// → ViewModel: ref.read(authRepositoryProvider).login(...) ← 常规路径
|
||||||
|
// → ViewModel: ref.read(loginUseCaseProvider).execute(...) ← 进阶路径(有 UseCase 时)
|
||||||
|
// → UseCase: 格式校验(邮箱 + 密码)
|
||||||
|
// → UseCase/ViewModel: authRepository.login(...)
|
||||||
|
// → Repository: _client.executeRequest(LoginRequest(...))
|
||||||
|
// → ApiClient.executeRequest(request)
|
||||||
|
// 1. 拼 URL: baseURL + "/auth/login"
|
||||||
|
// 2. request.parameters 触发 fromJson 自动注册
|
||||||
|
// 3. AuthInterceptor: 注入 token + platform headers
|
||||||
|
// 4. Dio.request(url, data: {email, password})
|
||||||
|
// 5. RetryInterceptor: token 过期 → 刷新 → 自动重试
|
||||||
|
// 6. LoggingInterceptor: 打印请求/响应日志
|
||||||
|
// → request.decodeResponse(response)
|
||||||
|
// 1. ApiResponseWrapper.fromJson: 拆 { code, message, data }
|
||||||
|
// 2. 检查 code != 0 → 抛 ApiError
|
||||||
|
// 3. fromJsonRegistry[LoginData] → LoginData.fromJson(data)
|
||||||
|
// → 返回 LoginData(DTO)
|
||||||
|
// → _onTokenUpdate(token) 回调写入 Token(Provider 层组合:内存 + 持久化)
|
||||||
|
// → LoginData.toEntity() → User(Domain Entity)
|
||||||
|
// → state.copyWith(user: user) 更新状态
|
||||||
|
// View: ref.watch → 自动 rebuild UI
|
||||||
|
//
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
// 各 HTTP 方法速查 — 新增接口时参照
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// GET(参数走 URL query string)
|
||||||
|
// @ApiRequest(path: ..., method: HttpMethod.get, responseType: ProfileData)
|
||||||
|
// class GetProfileRequest extends ApiRequestable<ProfileData> with _$... { }
|
||||||
|
//
|
||||||
|
// POST(参数走 JSON body)
|
||||||
|
// 见上方 LoginRequest 示例。
|
||||||
|
//
|
||||||
|
// DELETE / PUT / PATCH(与 POST 相同,只改 method)
|
||||||
|
// @ApiRequest(path: ..., method: HttpMethod.delete, responseType: ...)
|
||||||
|
//
|
||||||
|
// POST 无响应数据(如 logout)
|
||||||
|
// class LogoutRequest extends ApiRequestable<void> { ... }
|
||||||
|
// // → 返回 null
|
||||||
|
//
|
||||||
|
// Upload A: FormData 上传到自有后端
|
||||||
|
// @override Object? get uploadData => FormData.fromMap({ 'file': ... });
|
||||||
|
//
|
||||||
|
// Upload B: 二进制上传到 S3 presigned URL
|
||||||
|
// @override String get path => presignedURL; // 完整 URL,不拼 baseURL
|
||||||
|
// @override Object? get uploadData => bytes; // Uint8List
|
||||||
|
// @override decodeResponse(response) { ... } // S3 不走标准信封
|
||||||
|
//
|
||||||
97
apps/im_app/lib/app/router/app_route_name.dart
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/// 应用路由枚举
|
||||||
|
///
|
||||||
|
/// 每个枚举值对应一个注册路由及其绝对路径。
|
||||||
|
///
|
||||||
|
/// ## 为什么用枚举而不是常量类
|
||||||
|
///
|
||||||
|
/// `auth_guard.dart` 对路由做 switch 分析,Dart 的枚举 switch 是穷举的:
|
||||||
|
/// 新增路由时若没在 switch 里补 case,编译器直接报错,而不是等运行时漏掉。
|
||||||
|
///
|
||||||
|
/// ## 使用方式
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // 无参数导航
|
||||||
|
/// context.push(AppRouteName.settingsTheme.path);
|
||||||
|
/// context.go(AppRouteName.chat.path);
|
||||||
|
///
|
||||||
|
/// // 带参数导航(extra 传对象,适合列表点入详情等已有数据的场景)
|
||||||
|
/// context.push(
|
||||||
|
/// AppRouteName.chatDetail.path,
|
||||||
|
/// extra: (conversationId: '42', title: '技术支持'),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// // 带路径参数导航(路径中内嵌 id,适合需要直接链接或分享的场景)
|
||||||
|
/// context.push(AppRouteName.chatDetailByIdPath('99'));
|
||||||
|
///
|
||||||
|
/// // 路由表定义
|
||||||
|
/// GoRoute(path: AppRouteName.chat.path, ...)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 注意:子路由 path 是相对路径片段
|
||||||
|
///
|
||||||
|
/// go_router 在子路由中使用相对路径片段(不含父路径前缀),
|
||||||
|
/// 这是框架规定,不是硬编码字符串:
|
||||||
|
/// ```dart
|
||||||
|
/// GoRoute(
|
||||||
|
/// path: AppRouteName.settings.path, // '/settings'
|
||||||
|
/// routes: [
|
||||||
|
/// GoRoute(path: AppRouteName.settingsTheme.segment, ...), // 'theme'
|
||||||
|
/// ],
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// 导航时仍用 `AppRouteName.settingsTheme.path`,与枚举保持一致。
|
||||||
|
///
|
||||||
|
/// 子路由声明使用 [segment](相对路径片段),避免在路由表中硬编码字符串:
|
||||||
|
/// ```dart
|
||||||
|
/// GoRoute(path: AppRouteName.settingsTheme.segment, ...) // 'theme'
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 注意:含路径参数的路由
|
||||||
|
///
|
||||||
|
/// [chatDetailById] 的 [path] 包含占位符 `:id`,不能直接用于导航。
|
||||||
|
/// 导航时使用 [chatDetailByIdPath] 传入实际 id:
|
||||||
|
/// ```dart
|
||||||
|
/// context.push(AppRouteName.chatDetailByIdPath('99'));
|
||||||
|
/// ```
|
||||||
|
enum AppRouteName {
|
||||||
|
// ── Tab 根路由 ────────────────────────────────────────────────────────────
|
||||||
|
chat('/chat'),
|
||||||
|
contact('/contact'),
|
||||||
|
settings('/settings'),
|
||||||
|
|
||||||
|
// ── Chat 子路由 ──────────────────────────────────────────────────────────
|
||||||
|
// extra: ({String conversationId, String title})
|
||||||
|
chatDetail('/chat/detail'),
|
||||||
|
// 路径参数形式:导航用 AppRouteName.chatDetailByIdPath(id),不直接用 .path
|
||||||
|
chatDetailById('/chat/:id'),
|
||||||
|
|
||||||
|
// ── Settings 子路由 ───────────────────────────────────────────────────────
|
||||||
|
settingsTheme('/settings/theme'),
|
||||||
|
|
||||||
|
// ── 全屏页面(无底部导航栏)──────────────────────────────────────────────
|
||||||
|
login('/login');
|
||||||
|
|
||||||
|
const AppRouteName(this.path);
|
||||||
|
|
||||||
|
/// 绝对路径,用于 [context.push] / [context.go] 导航及顶层路由表声明
|
||||||
|
///
|
||||||
|
/// 注意:[chatDetailById] 的 path 含占位符 `:id`,导航时用 [chatDetailByIdPath]。
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
/// 相对路径片段(path 的最后一段),用于 go_router 子路由的 [GoRoute.path] 声明
|
||||||
|
///
|
||||||
|
/// 例:`AppRouteName.settingsTheme.segment` → `'theme'`
|
||||||
|
String get segment => path.split('/').last;
|
||||||
|
|
||||||
|
/// 从绝对路径查找枚举值,路径未注册时返回 null
|
||||||
|
///
|
||||||
|
/// 注意:含路径参数的路由(如 `/chat/99`)无法匹配,返回 null,
|
||||||
|
/// auth_guard 会按受保护路由处理(未登录重定向到登录页)。
|
||||||
|
static AppRouteName? fromPath(String path) =>
|
||||||
|
AppRouteName.values.where((r) => r.path == path).firstOrNull;
|
||||||
|
|
||||||
|
/// 生成 [chatDetailById] 的实际导航路径,将 `:id` 替换为真实 id
|
||||||
|
///
|
||||||
|
/// 例:`AppRouteName.chatDetailByIdPath('99')` → `'/chat/99'`
|
||||||
|
static String chatDetailByIdPath(String id) => '/chat/$id';
|
||||||
|
}
|
||||||
154
apps/im_app/lib/app/router/app_router.dart
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../features/app_tab/view/app_tab.dart';
|
||||||
|
import '../../features/chat/view/chat_detail_page.dart';
|
||||||
|
import '../../features/chat/view/chat_page.dart';
|
||||||
|
import '../../features/contact/view/contact_page.dart';
|
||||||
|
import '../../features/login/view/login_page.dart';
|
||||||
|
import '../../features/settings/view/settings_page.dart';
|
||||||
|
import '../../features/settings/view/theme_view.dart';
|
||||||
|
import '../di/app_providers.dart';
|
||||||
|
import 'app_route_name.dart';
|
||||||
|
import 'guards/auth_guard.dart';
|
||||||
|
|
||||||
|
/// 应用路由 Provider
|
||||||
|
///
|
||||||
|
/// 路由结构:
|
||||||
|
/// ```
|
||||||
|
/// StatefulShellRoute(底部导航栏持久容器)
|
||||||
|
/// ├── /chat ChatPage
|
||||||
|
/// ├── /contact ContactPage
|
||||||
|
/// └── /settings SettingsPage
|
||||||
|
///
|
||||||
|
/// ── 全屏页面(无底部导航栏,parentNavigatorKey = _rootKey)──
|
||||||
|
/// /chat/detail ChatDetailPage(extra 传参)
|
||||||
|
/// /chat/:id ChatDetailPage(路径参数)
|
||||||
|
/// /settings/theme ThemeView
|
||||||
|
/// /login LoginPage
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Shell 内 vs Shell 外
|
||||||
|
///
|
||||||
|
/// Shell 内路由(Tab 根路由)始终显示底部导航栏。
|
||||||
|
/// Shell 外路由(详情页 / 子功能页)无底部导航栏,push 进入后有返回按钮。
|
||||||
|
/// 这与 iOS / Android 主流 IM App 的交互一致(会话详情、设置子页均全屏)。
|
||||||
|
///
|
||||||
|
/// ## parentNavigatorKey 的作用
|
||||||
|
///
|
||||||
|
/// go_router push 时,路由默认放到"最近的 Navigator 祖先"上。
|
||||||
|
/// 在 StatefulShellBranch 内 push,最近的 Navigator 是 Branch Navigator,
|
||||||
|
/// 而不是 Root Navigator,Shell 不会被盖住,TabBar 仍然可见。
|
||||||
|
///
|
||||||
|
/// 设置 `parentNavigatorKey: _rootKey` 后,路由强制放到 Root Navigator,
|
||||||
|
/// 盖住整个 Shell,TabBar 消失,表现为真正的全屏页面。
|
||||||
|
///
|
||||||
|
/// ## 登录守卫
|
||||||
|
///
|
||||||
|
/// [authGuard] 检查 [AuthNotifier.isLoggedIn],未登录时重定向到 /login。
|
||||||
|
/// 登录 / 退出后 [AuthNotifier.notifyListeners] 触发 [refreshListenable],
|
||||||
|
/// go_router 自动重新执行 redirect,无需手动跳转。
|
||||||
|
///
|
||||||
|
/// ## Tab 状态保持
|
||||||
|
///
|
||||||
|
/// [StatefulShellRoute.indexedStack] 为每个 Tab 维护独立的 Navigator 栈,
|
||||||
|
/// 切换 Tab 时页面状态(滚动位置、输入内容等)不丢失。
|
||||||
|
|
||||||
|
// Root Navigator Key:供全屏路由声明 parentNavigatorKey,确保覆盖整个 Shell
|
||||||
|
final _rootKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
final routerProvider = Provider<GoRouter>((ref) {
|
||||||
|
final authNotifier = ref.read(authNotifierProvider);
|
||||||
|
|
||||||
|
return GoRouter(
|
||||||
|
// Root Navigator 的 Key,供全屏路由声明 parentNavigatorKey 使用,
|
||||||
|
// 确保 push 时覆盖整个 Shell(包括 TabBar)
|
||||||
|
navigatorKey: _rootKey,
|
||||||
|
|
||||||
|
// 冷启动默认落地页;authGuard 会在进入前检查登录状态并按需重定向
|
||||||
|
initialLocation: AppRouteName.chat.path,
|
||||||
|
|
||||||
|
// 在控制台打印每次路由变化,方便开发期间调试;上线前设为 false
|
||||||
|
debugLogDiagnostics: true,
|
||||||
|
|
||||||
|
// 监听 authNotifier 变化:登录 / 退出时自动触发 redirect 重新执行,
|
||||||
|
// 无需在业务代码里手动 context.go,守卫统一负责跳转
|
||||||
|
refreshListenable: authNotifier,
|
||||||
|
|
||||||
|
redirect: (context, state) => authGuard(authNotifier, state),
|
||||||
|
routes: [
|
||||||
|
// ── Shell 内:底部导航栏始终可见 ─────────────────────────────────────
|
||||||
|
StatefulShellRoute.indexedStack(
|
||||||
|
builder: (context, state, navigationShell) {
|
||||||
|
return AppTab(navigationShell: navigationShell);
|
||||||
|
},
|
||||||
|
branches: [
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: AppRouteName.chat.path,
|
||||||
|
builder: (context, state) => const ChatPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: AppRouteName.contact.path,
|
||||||
|
builder: (context, state) => const ContactPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: AppRouteName.settings.path,
|
||||||
|
builder: (context, state) => const SettingsPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Shell 外:全屏页面,无底部导航栏 ─────────────────────────────────
|
||||||
|
// parentNavigatorKey: _rootKey 确保路由覆盖 Shell,TabBar 消失
|
||||||
|
//
|
||||||
|
// extra 传参:接收 ({String conversationId, String title})
|
||||||
|
GoRoute(
|
||||||
|
parentNavigatorKey: _rootKey,
|
||||||
|
path: AppRouteName.chatDetail.path,
|
||||||
|
builder: (context, state) {
|
||||||
|
final extra =
|
||||||
|
state.extra as ({String conversationId, String title});
|
||||||
|
return ChatDetailPage(
|
||||||
|
conversationId: extra.conversationId,
|
||||||
|
title: extra.title,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// 路径参数:id 内嵌在 URL 中(/chat/:id)
|
||||||
|
GoRoute(
|
||||||
|
parentNavigatorKey: _rootKey,
|
||||||
|
path: AppRouteName.chatDetailById.path,
|
||||||
|
builder: (context, state) {
|
||||||
|
final id = state.pathParameters['id']!;
|
||||||
|
return ChatDetailPage(
|
||||||
|
conversationId: id,
|
||||||
|
title: '路径参数详情',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
parentNavigatorKey: _rootKey,
|
||||||
|
path: AppRouteName.settingsTheme.path,
|
||||||
|
builder: (context, state) => const ThemeView(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
parentNavigatorKey: _rootKey,
|
||||||
|
path: AppRouteName.login.path,
|
||||||
|
builder: (context, state) => const LoginPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
0
apps/im_app/lib/app/router/guards/.gitkeep
Normal file
47
apps/im_app/lib/app/router/guards/auth_guard.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../di/app_providers.dart';
|
||||||
|
import '../app_route_name.dart';
|
||||||
|
|
||||||
|
/// 登录守卫
|
||||||
|
///
|
||||||
|
/// 在 [GoRouter.redirect] 中调用,返回 null 表示放行,返回路径表示重定向目标。
|
||||||
|
/// 接收 [AuthNotifier] 而非 [Ref],避免守卫内部依赖 Riverpod,便于单测。
|
||||||
|
///
|
||||||
|
/// ## 穷举保护
|
||||||
|
///
|
||||||
|
/// 使用 [AppRouteName] 枚举 + switch 分析路由权限,Dart 编译器保证穷举:
|
||||||
|
/// 在 [AppRouteName] 新增枚举值后,此处 switch 未补 case 则编译报错。
|
||||||
|
///
|
||||||
|
/// ## 路由权限规则
|
||||||
|
///
|
||||||
|
/// | 路由 | 未登录 | 已登录 |
|
||||||
|
/// |------|--------|--------|
|
||||||
|
/// | login | 放行 | 重定向 → chat |
|
||||||
|
/// | 其余 | 重定向 → login | 放行 |
|
||||||
|
///
|
||||||
|
/// ## storage_sdk 接入后
|
||||||
|
///
|
||||||
|
/// 将 [AuthNotifier] 内的 Demo 状态替换为持久化 token,守卫本身无需改动。
|
||||||
|
String? authGuard(AuthNotifier authNotifier, GoRouterState state) {
|
||||||
|
final isLoggedIn = authNotifier.isLoggedIn;
|
||||||
|
final route = AppRouteName.fromPath(state.matchedLocation);
|
||||||
|
|
||||||
|
// 路径不在枚举中(理论上不应出现)→ 按受保护处理
|
||||||
|
if (route == null) return isLoggedIn ? null : AppRouteName.login.path;
|
||||||
|
|
||||||
|
switch (route) {
|
||||||
|
case AppRouteName.login:
|
||||||
|
// 已登录还在登录页 → 跳聊天页
|
||||||
|
return isLoggedIn ? AppRouteName.chat.path : null;
|
||||||
|
|
||||||
|
case AppRouteName.chat:
|
||||||
|
case AppRouteName.chatDetail:
|
||||||
|
case AppRouteName.chatDetailById:
|
||||||
|
case AppRouteName.contact:
|
||||||
|
case AppRouteName.settings:
|
||||||
|
case AppRouteName.settingsTheme:
|
||||||
|
// 受保护路由 → 未登录跳登录页
|
||||||
|
return isLoggedIn ? null : AppRouteName.login.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/im_app/lib/core/foundation/api_paths.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/// API 路径常量 — 全局统一管理
|
||||||
|
///
|
||||||
|
/// 所有 HTTP 接口路径在此定义,`@ApiRequest(path: ApiPaths.xxx)` 引用。
|
||||||
|
/// 集中管理便于:搜索、重命名、接口迁移、后端对齐。
|
||||||
|
///
|
||||||
|
/// 命名规则:`模块_操作`,如 `authLogin`、`chatSendMessage`。
|
||||||
|
///
|
||||||
|
/// 新增路径时,先 Cmd+F 搜索路径值,确认不重复后再添加。
|
||||||
|
// ignore: avoid_classes_with_only_static_members
|
||||||
|
class ApiPaths {
|
||||||
|
ApiPaths._();
|
||||||
|
|
||||||
|
// ── Auth ──
|
||||||
|
static const authLogin = '/auth/login';
|
||||||
|
static const authRefreshToken = '/auth/refresh-token';
|
||||||
|
static const authLogout = '/auth/logout';
|
||||||
|
|
||||||
|
// ── User ──
|
||||||
|
static const userProfile = '/user/profile';
|
||||||
|
static const userUpdateProfile = '/user/update-profile';
|
||||||
|
|
||||||
|
// ── Chat ──
|
||||||
|
static const chatSendMessage = '/chat/send-message';
|
||||||
|
static const chatHistory = '/chat/history';
|
||||||
|
|
||||||
|
// ── Upload ──
|
||||||
|
static const uploadFile = '/upload/file';
|
||||||
|
|
||||||
|
// ── WebSocket ──
|
||||||
|
static const wsConnect = '/ws';
|
||||||
|
}
|
||||||
14
apps/im_app/lib/core/foundation/config.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 编译期从 --dart-define-from-file=config/config.json 注入
|
||||||
|
// CI 打包时脚本修改 config.json 写入线上值,本地开发保持默认(IS_DEV=true)
|
||||||
|
// ignore: avoid_classes_with_only_static_members
|
||||||
|
class AppConfig {
|
||||||
|
AppConfig._();
|
||||||
|
|
||||||
|
static const isDev = bool.fromEnvironment('IS_DEV', defaultValue: true);
|
||||||
|
static const apiBaseUrl = String.fromEnvironment(
|
||||||
|
'API_BASE_URL',
|
||||||
|
defaultValue: 'https://dev-api.example.com',
|
||||||
|
);
|
||||||
|
|
||||||
|
static bool get isProd => !isDev;
|
||||||
|
}
|
||||||
17
apps/im_app/lib/core/foundation/constants.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/// 全局常量
|
||||||
|
///
|
||||||
|
/// 跨模块共用的配置值集中管理,避免散落在各处导致不一致。
|
||||||
|
class AppConstants {
|
||||||
|
AppConstants._();
|
||||||
|
|
||||||
|
// ── 网络重试 ──
|
||||||
|
|
||||||
|
/// 最大重试次数(HTTP 瞬态错误 + WebSocket 重连 统一)
|
||||||
|
static const maxRetries = 3;
|
||||||
|
|
||||||
|
/// 重试基础延迟(指数退避起点)
|
||||||
|
static const retryBaseDelay = Duration(seconds: 1);
|
||||||
|
|
||||||
|
/// 重连最大延迟(指数退避上限)
|
||||||
|
static const maxReconnectDelay = Duration(seconds: 30);
|
||||||
|
}
|
||||||
0
apps/im_app/lib/core/foundation/errors.dart
Normal file
0
apps/im_app/lib/core/foundation/extensions.dart
Normal file
0
apps/im_app/lib/core/foundation/logger.dart
Normal file
0
apps/im_app/lib/core/foundation/types.dart
Normal file
0
apps/im_app/lib/core/foundation/utils.dart
Normal file
147
apps/im_app/lib/core/services/app_initializer.dart
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// App 启动初始化器
|
||||||
|
///
|
||||||
|
/// 两阶段串行队列,确保启动流畅、资源不竞争。
|
||||||
|
///
|
||||||
|
/// ## 为什么不能在 initState 里一股脑 await?
|
||||||
|
///
|
||||||
|
/// 初始化任务并发执行会导致:
|
||||||
|
/// - 多个 await 阻塞首帧渲染 → 白屏时间长
|
||||||
|
/// - 任务间资源竞争(网络、IO、CPU)→ 互相拖慢
|
||||||
|
/// - 一个任务失败可能阻塞后续所有任务
|
||||||
|
///
|
||||||
|
/// ## 解决方案:两阶段 + 串行队列
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// App 启动
|
||||||
|
/// │
|
||||||
|
/// ├── Phase 1: Critical(initState 中同步触发)
|
||||||
|
/// │ 串行执行,必须在用户交互前完成。
|
||||||
|
/// │ 只放真正阻塞用户操作的任务(尽量少)。
|
||||||
|
/// │ 例:网络监听(后续所有网络操作依赖它)
|
||||||
|
/// │
|
||||||
|
/// ├── 首帧渲染(用户看到 UI)
|
||||||
|
/// │
|
||||||
|
/// └── Phase 2: Deferred(addPostFrameCallback 触发)
|
||||||
|
/// 串行执行,首帧渲染后逐个跑。
|
||||||
|
/// 不争抢资源,不影响 UI 流畅度。
|
||||||
|
/// 例:推送注册、缓存预热、埋点 SDK 初始化
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 添加新任务的规则
|
||||||
|
///
|
||||||
|
/// 问自己:「这个任务不完成,用户能正常看到首页吗?」
|
||||||
|
/// - **能** → 放 deferred(绝大多数情况)
|
||||||
|
/// - **不能** → 放 critical(谨慎添加,每多一个都会拖慢启动)
|
||||||
|
///
|
||||||
|
/// ## 设计原则
|
||||||
|
///
|
||||||
|
/// - **串行不并发**:同阶段内任务按顺序逐个执行,避免资源竞争
|
||||||
|
/// - **隔离不传染**:每个任务独立 try-catch,一个失败不阻塞后续
|
||||||
|
/// - **可观测**:每个任务计时 + 日志,方便排查启动瓶颈
|
||||||
|
/// - **首帧优先**:deferred 阶段等首帧渲染完再开始
|
||||||
|
class AppInitializer {
|
||||||
|
/// 关键任务(首帧前串行执行)
|
||||||
|
final List<InitTask> critical;
|
||||||
|
|
||||||
|
/// 延迟任务(首帧后串行执行)
|
||||||
|
final List<InitTask> deferred;
|
||||||
|
|
||||||
|
/// 日志回调
|
||||||
|
final void Function(String message, {String? tag})? onLog;
|
||||||
|
|
||||||
|
const AppInitializer({
|
||||||
|
this.critical = const [],
|
||||||
|
this.deferred = const [],
|
||||||
|
this.onLog,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 启动初始化
|
||||||
|
///
|
||||||
|
/// 1. 立即串行执行 critical 任务
|
||||||
|
/// 2. 注册 addPostFrameCallback,首帧后串行执行 deferred 任务
|
||||||
|
///
|
||||||
|
/// 在 initState 中调用(fire-and-forget,不 await)。
|
||||||
|
void run() {
|
||||||
|
_runCritical();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行关键阶段
|
||||||
|
Future<void> _runCritical() async {
|
||||||
|
if (critical.isEmpty) {
|
||||||
|
_log('No critical tasks');
|
||||||
|
} else {
|
||||||
|
_log('── Critical phase: ${critical.length} task(s) ──');
|
||||||
|
final totalSw = Stopwatch()..start();
|
||||||
|
|
||||||
|
await _runTasksSequentially(critical);
|
||||||
|
|
||||||
|
totalSw.stop();
|
||||||
|
_log('── Critical phase done in ${totalSw.elapsedMilliseconds}ms ──');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键阶段完成后,注册首帧回调执行延迟阶段
|
||||||
|
_scheduleDeferredPhase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 注册 addPostFrameCallback
|
||||||
|
///
|
||||||
|
/// 需要在 critical 完成后再注册,确保顺序:
|
||||||
|
/// critical 完成 → 首帧渲染 → deferred 开始
|
||||||
|
void _scheduleDeferredPhase() {
|
||||||
|
if (deferred.isEmpty) return;
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _runDeferred());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行延迟阶段
|
||||||
|
Future<void> _runDeferred() async {
|
||||||
|
_log('── Deferred phase: ${deferred.length} task(s) ──');
|
||||||
|
final totalSw = Stopwatch()..start();
|
||||||
|
|
||||||
|
await _runTasksSequentially(deferred);
|
||||||
|
|
||||||
|
totalSw.stop();
|
||||||
|
_log('── Deferred phase done in ${totalSw.elapsedMilliseconds}ms ──');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 串行执行任务队列
|
||||||
|
///
|
||||||
|
/// 逐个 await,不并发。每个任务独立 try-catch + 计时。
|
||||||
|
Future<void> _runTasksSequentially(List<InitTask> tasks) async {
|
||||||
|
for (var i = 0; i < tasks.length; i++) {
|
||||||
|
final task = tasks[i];
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_log('[${i + 1}/${tasks.length}] ${task.name} ...');
|
||||||
|
await task.task();
|
||||||
|
sw.stop();
|
||||||
|
_log('[${i + 1}/${tasks.length}] ${task.name} done (${sw.elapsedMilliseconds}ms)');
|
||||||
|
} catch (e) {
|
||||||
|
sw.stop();
|
||||||
|
_log('[${i + 1}/${tasks.length}] ${task.name} FAILED (${sw.elapsedMilliseconds}ms): $e');
|
||||||
|
// 不 rethrow — 隔离失败,继续执行后续任务
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log(String message) {
|
||||||
|
onLog?.call(message, tag: 'AppInit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化任务
|
||||||
|
class InitTask {
|
||||||
|
/// 任务名称(用于日志)
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// 任务执行体
|
||||||
|
final Future<void> Function() task;
|
||||||
|
|
||||||
|
const InitTask({
|
||||||
|
required this.name,
|
||||||
|
required this.task,
|
||||||
|
});
|
||||||
|
}
|
||||||
105
apps/im_app/lib/core/services/network_backoff_debouncer.dart
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
/// 网络恢复退避防抖器
|
||||||
|
///
|
||||||
|
/// 网络状态频繁切换(WiFi 不稳定、隧道信号间断等)时,
|
||||||
|
/// 避免每次恢复都立即重连,用指数退避控制重连频率。
|
||||||
|
///
|
||||||
|
/// 增加 jitter 防止多设备同时重连的群体效应(thundering herd)。
|
||||||
|
///
|
||||||
|
/// ## 退避策略
|
||||||
|
///
|
||||||
|
/// - 首次触发 → 等 baseDelay 后执行
|
||||||
|
/// - 短时间内再次触发 → 延迟翻倍(指数退避)
|
||||||
|
/// - 长时间静默后触发 → 重置为 baseDelay(网络已稳定)
|
||||||
|
///
|
||||||
|
/// ## 退避进程(默认参数)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// 触发 1 → 4s 后执行
|
||||||
|
/// 触发 2 → 8s 后执行
|
||||||
|
/// 触发 3 → 16s 后执行
|
||||||
|
/// 触发 4 → 32s 后执行
|
||||||
|
/// 触发 5 → 60s(封顶)
|
||||||
|
/// ...静默超过 2 分钟...
|
||||||
|
/// 触发 N → 4s(重置)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final debouncer = NetworkBackoffDebouncer();
|
||||||
|
///
|
||||||
|
/// // 网络恢复事件
|
||||||
|
/// debouncer.call(() {
|
||||||
|
/// socketManager.reconnect();
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
class NetworkBackoffDebouncer {
|
||||||
|
/// 初始延迟
|
||||||
|
final Duration baseDelay;
|
||||||
|
|
||||||
|
/// 退避上限
|
||||||
|
final Duration maxDelay;
|
||||||
|
|
||||||
|
/// 静默多久后重置为初始延迟(网络已稳定,不再退避)
|
||||||
|
final Duration resetThreshold;
|
||||||
|
|
||||||
|
/// 退避倍数
|
||||||
|
final double factor;
|
||||||
|
|
||||||
|
Duration _currentDelay;
|
||||||
|
DateTime? _lastTriggerTime;
|
||||||
|
Timer? _timer;
|
||||||
|
final _random = Random();
|
||||||
|
|
||||||
|
NetworkBackoffDebouncer({
|
||||||
|
this.baseDelay = const Duration(seconds: 4),
|
||||||
|
this.maxDelay = const Duration(seconds: 60),
|
||||||
|
this.resetThreshold = const Duration(minutes: 2),
|
||||||
|
this.factor = 2.0,
|
||||||
|
}) : _currentDelay = baseDelay;
|
||||||
|
|
||||||
|
/// 触发退避执行
|
||||||
|
///
|
||||||
|
/// 取消上一个待执行的 action,按当前退避延迟重新计时。
|
||||||
|
/// 短时间内多次触发只执行最后一次,延迟逐步递增。
|
||||||
|
void call(void Function() action) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
if (_lastTriggerTime == null ||
|
||||||
|
now.difference(_lastTriggerTime!) > resetThreshold) {
|
||||||
|
// 首次触发 or 长时间静默 → 重置
|
||||||
|
_currentDelay = baseDelay;
|
||||||
|
} else {
|
||||||
|
// 短时间内再次触发 → 退避
|
||||||
|
final nextMs = (_currentDelay.inMilliseconds * factor).toInt();
|
||||||
|
_currentDelay = Duration(
|
||||||
|
milliseconds: nextMs < maxDelay.inMilliseconds
|
||||||
|
? nextMs
|
||||||
|
: maxDelay.inMilliseconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastTriggerTime = now;
|
||||||
|
|
||||||
|
// 加 jitter(+0~25%),防止多设备同时重连
|
||||||
|
final jitterMs = _random.nextInt((_currentDelay.inMilliseconds * 0.25).toInt().clamp(1, 15000));
|
||||||
|
final delayWithJitter = _currentDelay + Duration(milliseconds: jitterMs);
|
||||||
|
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(delayWithJitter, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取消待执行的 action
|
||||||
|
void cancel() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源
|
||||||
|
void dispose() {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
110
apps/im_app/lib/core/services/network_monitor.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
|
||||||
|
/// 网络状态监听
|
||||||
|
///
|
||||||
|
/// 基于 connectivity_plus 监听平台网络变化,
|
||||||
|
/// 提供 [isConnected] 查询和 [onStatusChanged] 事件流。
|
||||||
|
///
|
||||||
|
/// 非单例,由 Riverpod Provider 构造注入。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// connectivity_plus(平台网络事件)
|
||||||
|
/// → ★ NetworkMonitor ★ ← 你在这里
|
||||||
|
/// → SocketManager.handleNetworkStatusChanged()
|
||||||
|
/// → SocketClient.connect / disconnect
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final monitor = ref.read(networkMonitorProvider);
|
||||||
|
/// monitor.onStatusChanged.listen((isAvailable) {
|
||||||
|
/// // 网络状态变化
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
class NetworkMonitor {
|
||||||
|
final Connectivity _connectivity = Connectivity();
|
||||||
|
StreamSubscription<List<ConnectivityResult>>? _subscription;
|
||||||
|
|
||||||
|
List<ConnectivityResult> _results = [];
|
||||||
|
|
||||||
|
final _statusController = StreamController<bool>.broadcast();
|
||||||
|
|
||||||
|
/// 日志输出回调
|
||||||
|
final void Function(String message, {String? tag})? onLog;
|
||||||
|
|
||||||
|
NetworkMonitor({this.onLog});
|
||||||
|
|
||||||
|
/// 当前是否有网络连接
|
||||||
|
///
|
||||||
|
/// 排除无连接和仅蓝牙连接的情况。
|
||||||
|
bool get isConnected {
|
||||||
|
if (_results.isEmpty) return false;
|
||||||
|
return !_results.contains(ConnectivityResult.none) &&
|
||||||
|
!_results.every((r) => r == ConnectivityResult.bluetooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网络状态变化流
|
||||||
|
///
|
||||||
|
/// 只在连接状态真正改变时发送事件(connected ↔ disconnected),
|
||||||
|
/// 同类型切换(WiFi → 4G)不会触发。
|
||||||
|
Stream<bool> get onStatusChanged => _statusController.stream;
|
||||||
|
|
||||||
|
/// 初始化监听
|
||||||
|
///
|
||||||
|
/// App 启动时调用一次。获取当前状态并开始监听变化。
|
||||||
|
Future<void> initialize() async {
|
||||||
|
try {
|
||||||
|
_results = await _connectivity.checkConnectivity();
|
||||||
|
_log('Network status: $_connectionDescription');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Failed to check connectivity: $e');
|
||||||
|
_results = [ConnectivityResult.none];
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscription = _connectivity.onConnectivityChanged.listen(
|
||||||
|
_onChanged,
|
||||||
|
onError: (Object error) {
|
||||||
|
_log('Connectivity listener error: $error');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_statusController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void _onChanged(List<ConnectivityResult> results) {
|
||||||
|
final wasConnected = isConnected;
|
||||||
|
_results = results;
|
||||||
|
final nowConnected = isConnected;
|
||||||
|
|
||||||
|
_log('Network changed: $_connectionDescription');
|
||||||
|
|
||||||
|
// 只在真正切换时通知
|
||||||
|
if (wasConnected != nowConnected) {
|
||||||
|
_statusController.add(nowConnected);
|
||||||
|
_log(nowConnected ? 'Network restored' : 'Network lost');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _connectionDescription {
|
||||||
|
if (_results.contains(ConnectivityResult.wifi)) return 'WiFi';
|
||||||
|
if (_results.contains(ConnectivityResult.mobile)) return 'Mobile';
|
||||||
|
if (_results.contains(ConnectivityResult.ethernet)) return 'Ethernet';
|
||||||
|
if (_results.contains(ConnectivityResult.none)) return 'None';
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log(String message) {
|
||||||
|
onLog?.call(message, tag: 'Network');
|
||||||
|
}
|
||||||
|
}
|
||||||
376
apps/im_app/lib/core/services/socket_manager.dart
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
|
||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import 'network_backoff_debouncer.dart';
|
||||||
|
|
||||||
|
/// 消息预处理回调
|
||||||
|
///
|
||||||
|
/// 参考 HTTP 层 onTokenRefresh 的回调注入模式。
|
||||||
|
/// App 层在 Provider 装配时注入解密/解析逻辑,
|
||||||
|
/// 不在 SDK 内部调用加解密 SDK。
|
||||||
|
typedef MessageTransformer = Map<String, dynamic> Function(
|
||||||
|
Map<String, dynamic> raw,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// WebSocket 连接管理
|
||||||
|
///
|
||||||
|
/// 在 SocketClient(SDK 底层能力)之上封装:
|
||||||
|
/// - 连接/断连生命周期(登录连接、登出断连)
|
||||||
|
/// - 前后台生命周期(后台断连省电、前台自动重连)
|
||||||
|
/// - 网络状态响应(断网断连、恢复网络立即重连)
|
||||||
|
/// - 操作前置检查(网络可用性 + 后台状态)
|
||||||
|
/// - 消息预处理管道(通过 [onMessageTransform] 回调注入解密等)
|
||||||
|
/// - 发送 API 透传
|
||||||
|
///
|
||||||
|
/// 不使用单例,通过 Riverpod Provider 注入。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// SocketClient.messageStream(原始消息)
|
||||||
|
/// → onMessageTransform?(解密回调,App 层注入)
|
||||||
|
/// → ★ SocketManager.messageStream ★ ← 你在这里
|
||||||
|
/// → 业务模块消费
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 生命周期流程
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// 登录成功 → connect(token) → 前置检查 → 建立连接
|
||||||
|
/// App 进后台 → onEnterBackground() → 断开连接(省电)
|
||||||
|
/// App 回前台 → onEnterForeground() → 检查网络 → 自动重连
|
||||||
|
/// 网络丢失 → handleNetworkLost() → 断开连接
|
||||||
|
/// 网络恢复 → handleNetworkRestored() → 退避重连(防抖动)
|
||||||
|
/// 登出 → disconnect() → 断开连接,清除 token
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 前置检查策略
|
||||||
|
///
|
||||||
|
/// 所有会发起网络操作的方法都先检查前置条件:
|
||||||
|
/// - connect → 检查网络可用性 + 是否在后台
|
||||||
|
/// - send / sendString → 检查连接状态 + 是否在后台
|
||||||
|
/// - onEnterForeground 重连 → 检查网络可用性
|
||||||
|
class SocketManager {
|
||||||
|
final NetworksMessagingApi _client;
|
||||||
|
final String _wsUrl;
|
||||||
|
|
||||||
|
/// 消息预处理回调
|
||||||
|
///
|
||||||
|
/// 登录后由 Provider 层注入,用于消息解密等。
|
||||||
|
/// 不注入时直接透传原始消息。
|
||||||
|
///
|
||||||
|
/// TODO: 接入加解密 SDK 后实现
|
||||||
|
final MessageTransformer? onMessageTransform;
|
||||||
|
|
||||||
|
/// 网络可用性查询(App 层注入)
|
||||||
|
///
|
||||||
|
/// 与 HTTP 层 [ApiConfig.onCheckNetworkAvailable] 对称。
|
||||||
|
/// 连接和重连前调用,无网络时跳过操作并标记恢复时重试。
|
||||||
|
final Future<bool> Function()? onCheckNetworkAvailable;
|
||||||
|
|
||||||
|
/// 日志回调
|
||||||
|
final void Function(String message, {String? tag})? onLog;
|
||||||
|
|
||||||
|
// ── 内部状态 ──
|
||||||
|
|
||||||
|
/// 上次连接使用的 token,用于前台/网络恢复时自动重连
|
||||||
|
String? _lastToken;
|
||||||
|
|
||||||
|
/// 后台断连标记:前台恢复时需要重连
|
||||||
|
bool _reconnectOnForeground = false;
|
||||||
|
|
||||||
|
/// 断网标记:网络恢复时需要重连
|
||||||
|
bool _reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
|
/// 当前是否在后台
|
||||||
|
bool _isInBackground = false;
|
||||||
|
|
||||||
|
/// 网络恢复退避防抖器
|
||||||
|
///
|
||||||
|
/// 网络抖动(快速 offline → online 切换)时,
|
||||||
|
/// 用指数退避避免反复锤服务器。
|
||||||
|
/// 通过 [NetworkBackoffDebouncer] 实现指数退避。
|
||||||
|
final NetworkBackoffDebouncer _networkDebouncer = NetworkBackoffDebouncer();
|
||||||
|
|
||||||
|
/// 前台恢复延迟重连定时器
|
||||||
|
///
|
||||||
|
/// 回前台后延迟 500ms 等待网络稳定再重连。
|
||||||
|
/// 期间如果再次进后台 / 主动断连 / dispose,及时取消。
|
||||||
|
Timer? _foregroundReconnectTimer;
|
||||||
|
|
||||||
|
SocketManager({
|
||||||
|
required NetworksMessagingApi client,
|
||||||
|
required String wsUrl,
|
||||||
|
this.onMessageTransform,
|
||||||
|
this.onCheckNetworkAvailable,
|
||||||
|
this.onLog,
|
||||||
|
}) : _client = client,
|
||||||
|
_wsUrl = wsUrl;
|
||||||
|
|
||||||
|
// ── 连接 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 连接 WebSocket
|
||||||
|
///
|
||||||
|
/// 登录成功后调用,token 从登录响应获取。
|
||||||
|
/// URL 由 Provider 层从 AppConfig 构建后注入,此处不关心来源。
|
||||||
|
///
|
||||||
|
/// 前置检查:
|
||||||
|
/// - 在后台 → 跳过,标记前台恢复时重连
|
||||||
|
/// - 无网络 → 跳过,标记网络恢复时重连
|
||||||
|
Future<bool> connect({required String token}) async {
|
||||||
|
_lastToken = token;
|
||||||
|
_reconnectOnForeground = false;
|
||||||
|
_reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
|
// 前置检查:在后台不连接(省电)
|
||||||
|
if (_isInBackground) {
|
||||||
|
_reconnectOnForeground = true;
|
||||||
|
_log('In background, defer connect to foreground');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前置检查:无网络不连接
|
||||||
|
if (!await _isNetworkAvailable()) {
|
||||||
|
_reconnectOnNetworkRestore = true;
|
||||||
|
_log('No network, defer connect to network restore');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Connecting...');
|
||||||
|
return _client.connect(_wsUrl, token: token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 断开连接(主动断连)
|
||||||
|
///
|
||||||
|
/// 登出时调用。清除 token,取消所有待执行的重连,不再自动重连。
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
_lastToken = null;
|
||||||
|
_reconnectOnForeground = false;
|
||||||
|
_reconnectOnNetworkRestore = false;
|
||||||
|
_foregroundReconnectTimer?.cancel();
|
||||||
|
_foregroundReconnectTimer = null;
|
||||||
|
_networkDebouncer.cancel();
|
||||||
|
_log('Disconnecting (manual)');
|
||||||
|
await _client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当前是否已连接
|
||||||
|
bool get isConnected => _client.isConnected;
|
||||||
|
|
||||||
|
/// 当前连接状态
|
||||||
|
SocketConnectionState get connectionState => _client.connectionState;
|
||||||
|
|
||||||
|
/// 当前是否在后台
|
||||||
|
bool get isInBackground => _isInBackground;
|
||||||
|
|
||||||
|
// ── 前后台生命周期 ────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// 后台 → 断连(省电省流量)
|
||||||
|
// 前台 → 自动重连(如果之前有连接)
|
||||||
|
|
||||||
|
/// App 进后台 → 断开连接,标记前台恢复时重连
|
||||||
|
///
|
||||||
|
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
|
||||||
|
/// 后台保持连接会消耗电量和流量,断开后由 push 通知兜底。
|
||||||
|
void onEnterBackground() {
|
||||||
|
_isInBackground = true;
|
||||||
|
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
|
||||||
|
_foregroundReconnectTimer?.cancel();
|
||||||
|
_foregroundReconnectTimer = null;
|
||||||
|
// 同步 SocketClient 内部状态(与 onEnterForeground 对称)
|
||||||
|
_client.onEnterBackground();
|
||||||
|
|
||||||
|
if (_lastToken == null) return; // 未登录,无需处理
|
||||||
|
|
||||||
|
// 与 _handleNetworkLost 保持一致:
|
||||||
|
// 不仅 connected,connecting / reconnecting 也要断开,
|
||||||
|
// 防止 SocketClient 在后台继续尝试连接浪费电量和流量。
|
||||||
|
if (_client.isConnected ||
|
||||||
|
_client.connectionState == SocketConnectionState.connecting ||
|
||||||
|
_client.connectionState == SocketConnectionState.reconnecting) {
|
||||||
|
_reconnectOnForeground = true;
|
||||||
|
_log('Entering background, disconnecting to save battery');
|
||||||
|
_client.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App 回前台 → 自动重连(如果之前后台断连)
|
||||||
|
///
|
||||||
|
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.resumed] 时调用。
|
||||||
|
/// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
|
||||||
|
void onEnterForeground() {
|
||||||
|
_isInBackground = false;
|
||||||
|
_client.onEnterForeground();
|
||||||
|
|
||||||
|
if (_reconnectOnForeground && _lastToken != null) {
|
||||||
|
_reconnectOnForeground = false;
|
||||||
|
_log('Returning to foreground, reconnecting...');
|
||||||
|
// 延迟 500ms 等待网络稳定,通过 Timer 跟踪以便进后台时取消
|
||||||
|
_foregroundReconnectTimer?.cancel();
|
||||||
|
_foregroundReconnectTimer = Timer(
|
||||||
|
const Duration(milliseconds: 500),
|
||||||
|
() async {
|
||||||
|
_foregroundReconnectTimer = null;
|
||||||
|
// 双重保险:回调执行时再次检查后台状态
|
||||||
|
if (_isInBackground) {
|
||||||
|
_reconnectOnForeground = true;
|
||||||
|
_log('Went back to background during delay, skip reconnect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_client.isConnected && _lastToken != null) {
|
||||||
|
// 前置检查:网络可用性
|
||||||
|
if (!await _isNetworkAvailable()) {
|
||||||
|
_reconnectOnNetworkRestore = true;
|
||||||
|
_log('Network unavailable, defer reconnect to network restore');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_client.connect(_wsUrl, token: _lastToken!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 网络状态变化 ──────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// 网络丢失 → 断连(避免无效重试消耗资源)
|
||||||
|
// 网络恢复 → 退避重连(防网络抖动)
|
||||||
|
|
||||||
|
/// 网络状态变化处理
|
||||||
|
///
|
||||||
|
/// 由 App 层 NetworkMonitor.onStatusChanged 事件驱动。
|
||||||
|
void handleNetworkStatusChanged({required bool isAvailable}) {
|
||||||
|
if (isAvailable) {
|
||||||
|
_handleNetworkRestored();
|
||||||
|
} else {
|
||||||
|
_handleNetworkLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网络丢失 → 断开连接,标记网络恢复时重连
|
||||||
|
///
|
||||||
|
/// 断网后继续重试没有意义,主动断连避免无效重连消耗资源。
|
||||||
|
void _handleNetworkLost() {
|
||||||
|
if (_lastToken == null) return; // 未登录,无需处理
|
||||||
|
|
||||||
|
if (_client.isConnected ||
|
||||||
|
_client.connectionState == SocketConnectionState.connecting ||
|
||||||
|
_client.connectionState == SocketConnectionState.reconnecting) {
|
||||||
|
_reconnectOnNetworkRestore = true;
|
||||||
|
_log('Network lost, disconnecting');
|
||||||
|
_client.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网络恢复 → 退避重连
|
||||||
|
///
|
||||||
|
/// 通过 [NetworkBackoffDebouncer] 控制重连频率,
|
||||||
|
/// 网络抖动(快速 offline/online 切换)时不会反复锤服务器。
|
||||||
|
///
|
||||||
|
/// 退避进程:4s → 8s → 16s → 32s → 60s(封顶),
|
||||||
|
/// 网络稳定超过 2 分钟后重置。
|
||||||
|
void _handleNetworkRestored() {
|
||||||
|
if (_reconnectOnNetworkRestore && _lastToken != null) {
|
||||||
|
_reconnectOnNetworkRestore = false;
|
||||||
|
|
||||||
|
// 在后台不重连,等前台恢复时再连
|
||||||
|
if (_isInBackground) {
|
||||||
|
_reconnectOnForeground = true;
|
||||||
|
_log('Network restored but in background, defer to foreground');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Network restored, scheduling reconnect with backoff');
|
||||||
|
_networkDebouncer.call(() {
|
||||||
|
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
|
||||||
|
_log('Backoff timer fired, reconnecting');
|
||||||
|
_client.connect(_wsUrl, token: _lastToken!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 消息流 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 处理后的 JSON 消息流
|
||||||
|
///
|
||||||
|
/// 经过 [onMessageTransform] 预处理(解密等)后的消息。
|
||||||
|
/// 业务模块应监听此流,不直接监听 SocketClient.messageStream。
|
||||||
|
Stream<Map<String, dynamic>> get messageStream {
|
||||||
|
if (onMessageTransform != null) {
|
||||||
|
return _client.messageStream.map(onMessageTransform!);
|
||||||
|
}
|
||||||
|
return _client.messageStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原始消息流(不经预处理,调试用)
|
||||||
|
Stream<String> get rawMessageStream => _client.rawMessageStream;
|
||||||
|
|
||||||
|
/// 连接状态变化流
|
||||||
|
Stream<SocketConnectionState> get connectionStateStream =>
|
||||||
|
_client.connectionStateStream;
|
||||||
|
|
||||||
|
/// 错误流
|
||||||
|
Stream<SocketError> get errorStream => _client.errorStream;
|
||||||
|
|
||||||
|
// ── 发送 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 发送 JSON 消息
|
||||||
|
///
|
||||||
|
/// 前置检查:未连接或在后台时不发送。
|
||||||
|
Future<bool> send(Map<String, dynamic> message) {
|
||||||
|
if (!_canSend()) return Future.value(false);
|
||||||
|
return _client.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送原始字符串
|
||||||
|
///
|
||||||
|
/// 前置检查:未连接或在后台时不发送。
|
||||||
|
Future<bool> sendString(String message) {
|
||||||
|
if (!_canSend()) return Future.value(false);
|
||||||
|
return _client.sendString(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 释放 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 释放所有资源
|
||||||
|
Future<void> dispose() {
|
||||||
|
_foregroundReconnectTimer?.cancel();
|
||||||
|
_foregroundReconnectTimer = null;
|
||||||
|
_networkDebouncer.dispose();
|
||||||
|
return _client.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 发送前置检查
|
||||||
|
///
|
||||||
|
/// 两重保险:连接状态 + 后台状态。
|
||||||
|
/// 后台已断连所以 isConnected 通常就能拦住,
|
||||||
|
/// 但显式检查 _isInBackground 防止边界情况遗漏。
|
||||||
|
bool _canSend() {
|
||||||
|
if (!_client.isConnected) {
|
||||||
|
_log('Not connected, cannot send');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_isInBackground) {
|
||||||
|
_log('In background, skip send');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询网络可用性
|
||||||
|
///
|
||||||
|
/// 未注入回调时默认网络可用(不阻塞操作)。
|
||||||
|
Future<bool> _isNetworkAvailable() async {
|
||||||
|
if (onCheckNetworkAvailable == null) return true;
|
||||||
|
return onCheckNetworkAvailable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log(String message) {
|
||||||
|
onLog?.call(message, tag: 'SocketManager');
|
||||||
|
}
|
||||||
|
}
|
||||||
91
apps/im_app/lib/core/ui/base/app_theme.dart
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'colors.dart';
|
||||||
|
import 'font.dart';
|
||||||
|
|
||||||
|
/// 主题组装 -- 将 AppColors / AppFont 组装为 ThemeData
|
||||||
|
///
|
||||||
|
/// 同时提供 Light / Dark 双主题,按钮形状/颜色/字体统一在此定义,
|
||||||
|
/// AppButton 只负责变体切换和 loading 逻辑,不硬编码颜色和字体。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// AppColors + AppFont (L1 常量)
|
||||||
|
/// → ★ AppTheme ★ (L1 组装) ← 你在这里
|
||||||
|
/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme)
|
||||||
|
/// → Theme.of(context) → 所有 Widget 自动响应主题变化
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // app/app.dart
|
||||||
|
/// MaterialApp(
|
||||||
|
/// theme: AppTheme.theme, // getter 名与 MaterialApp 参数名一一对应
|
||||||
|
/// darkTheme: AppTheme.darkTheme,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
class AppTheme {
|
||||||
|
AppTheme._();
|
||||||
|
|
||||||
|
/// 亮色主题 — 对应 MaterialApp `theme:` 参数
|
||||||
|
static ThemeData get theme => _build(Brightness.light);
|
||||||
|
|
||||||
|
/// 暗色主题 — 对应 MaterialApp `darkTheme:` 参数
|
||||||
|
static ThemeData get darkTheme => _build(Brightness.dark);
|
||||||
|
|
||||||
|
static ThemeData _build(Brightness brightness) {
|
||||||
|
final isDark = brightness == Brightness.dark;
|
||||||
|
final primary = isDark ? AppColors.primaryLight : AppColors.primary;
|
||||||
|
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: brightness,
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
brightness: brightness,
|
||||||
|
primary: primary,
|
||||||
|
onPrimary: AppColors.white,
|
||||||
|
secondary: primary,
|
||||||
|
onSecondary: AppColors.white,
|
||||||
|
error: AppColors.error,
|
||||||
|
onError: AppColors.white,
|
||||||
|
surface: isDark ? AppColors.gray800 : AppColors.white,
|
||||||
|
onSurface: isDark ? AppColors.white : AppColors.gray900,
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: isDark ? AppColors.gray900 : AppColors.gray50,
|
||||||
|
|
||||||
|
// 字体
|
||||||
|
textTheme: AppFont.textTheme(brightness),
|
||||||
|
|
||||||
|
// ElevatedButton → AppButton.primary
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
foregroundColor: AppColors.white,
|
||||||
|
disabledBackgroundColor: AppColors.gray400,
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// OutlinedButton → AppButton.secondary
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: primary,
|
||||||
|
side: BorderSide(color: primary),
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// TextButton → AppButton.text
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: primary,
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
apps/im_app/lib/core/ui/base/colors.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 颜色体系 — 与 Figma 设计稿对应
|
||||||
|
///
|
||||||
|
/// L1 基础常量 -- 不含任何 Widget,只输出颜色常量。
|
||||||
|
/// View 层不直接引用 AppColors,通过 Theme.of(context) 访问语义色;
|
||||||
|
/// 有特殊硬编码需求(插图、固定品牌色)时可直接引用。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// AppColors(颜色常量)← 你在这里
|
||||||
|
/// → AppTheme(组装为 ThemeData)
|
||||||
|
/// → MaterialApp(注入)
|
||||||
|
/// → Theme.of(context)(View 层消费)
|
||||||
|
/// ```
|
||||||
|
class AppColors {
|
||||||
|
AppColors._();
|
||||||
|
|
||||||
|
// ── Brand Primary ──────────────────────────────────────────────────────────
|
||||||
|
static const primary = Color(0xFF2F80ED);
|
||||||
|
static const primaryDark = Color(0xFF1A6BD4);
|
||||||
|
static const primaryLight = Color(0xFF5BA3F5);
|
||||||
|
|
||||||
|
// ── Semantic ───────────────────────────────────────────────────────────────
|
||||||
|
static const success = Color(0xFF27AE60);
|
||||||
|
static const warning = Color(0xFFF2C94C);
|
||||||
|
static const error = Color(0xFFEB5757);
|
||||||
|
|
||||||
|
// ── Neutral Gray Scale ─────────────────────────────────────────────────────
|
||||||
|
static const white = Color(0xFFFFFFFF);
|
||||||
|
static const gray50 = Color(0xFFF8F9FA);
|
||||||
|
static const gray100 = Color(0xFFF1F3F4);
|
||||||
|
static const gray200 = Color(0xFFE8EAED);
|
||||||
|
static const gray400 = Color(0xFFBDC1C6);
|
||||||
|
static const gray600 = Color(0xFF80868B);
|
||||||
|
static const gray800 = Color(0xFF3C4043);
|
||||||
|
static const gray900 = Color(0xFF202124);
|
||||||
|
static const black = Color(0xFF000000);
|
||||||
|
}
|
||||||
89
apps/im_app/lib/core/ui/base/context_theme_ext.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'font.dart';
|
||||||
|
|
||||||
|
/// 主题样式快捷封装
|
||||||
|
///
|
||||||
|
/// `context.styles` 返回此对象,build 方法里一行获取所有样式,
|
||||||
|
/// 之后直接用 `s.bodySmall`、`s.primary`,不再写 Theme.of(context)。
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final s = context.styles;
|
||||||
|
///
|
||||||
|
/// Text('标题', style: s.titleMedium)
|
||||||
|
/// Text('描述', style: s.bodySmall)
|
||||||
|
/// Icon(Icons.home, color: s.primary)
|
||||||
|
/// Text('改色', style: s.bodySmall?.copyWith(color: s.primary))
|
||||||
|
/// ```
|
||||||
|
class AppStyles {
|
||||||
|
AppStyles(BuildContext context)
|
||||||
|
: _t = Theme.of(context).textTheme,
|
||||||
|
_c = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
final TextTheme _t;
|
||||||
|
final ColorScheme _c;
|
||||||
|
|
||||||
|
// ── 字体 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TextStyle? get displayLarge => _t.displayLarge;
|
||||||
|
TextStyle? get displayMedium => _t.displayMedium;
|
||||||
|
TextStyle? get displaySmall => _t.displaySmall;
|
||||||
|
|
||||||
|
TextStyle? get headlineLarge => _t.headlineLarge;
|
||||||
|
TextStyle? get headlineMedium => _t.headlineMedium;
|
||||||
|
TextStyle? get headlineSmall => _t.headlineSmall;
|
||||||
|
|
||||||
|
TextStyle? get titleLarge => _t.titleLarge;
|
||||||
|
TextStyle? get titleMedium => _t.titleMedium;
|
||||||
|
TextStyle? get titleSmall => _t.titleSmall;
|
||||||
|
|
||||||
|
TextStyle? get bodyLarge => _t.bodyLarge;
|
||||||
|
TextStyle? get bodyMedium => _t.bodyMedium;
|
||||||
|
TextStyle? get bodySmall => _t.bodySmall;
|
||||||
|
|
||||||
|
TextStyle? get labelLarge => _t.labelLarge;
|
||||||
|
TextStyle? get labelMedium => _t.labelMedium;
|
||||||
|
TextStyle? get labelSmall => _t.labelSmall;
|
||||||
|
|
||||||
|
// ── 颜色 + 亮暗 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Brightness get brightness => _c.brightness;
|
||||||
|
bool get isDark => _c.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
Color get primary => _c.primary;
|
||||||
|
Color get onPrimary => _c.onPrimary;
|
||||||
|
Color get secondary => _c.secondary;
|
||||||
|
Color get onSecondary => _c.onSecondary;
|
||||||
|
Color get error => _c.error;
|
||||||
|
Color get onError => _c.onError;
|
||||||
|
Color get surface => _c.surface;
|
||||||
|
Color get onSurface => _c.onSurface;
|
||||||
|
Color get outline => _c.outline;
|
||||||
|
Color get outlineVariant => _c.outlineVariant;
|
||||||
|
|
||||||
|
// ── 预组合样式(字体 + 颜色,开箱即用)──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// 与 AppButton 变体理念一致:按语义选用,无需手动拼 TextStyle 或 copyWith。
|
||||||
|
// 新增场景时在此扩展,保持全局一致。
|
||||||
|
|
||||||
|
/// 分组标题 — 列表 Section、设置分组等(sectionLabel 字体 + primary 色)
|
||||||
|
TextStyle get sectionLabel => AppFont.sectionLabel.copyWith(color: primary);
|
||||||
|
|
||||||
|
/// 辅助文字 — 元数据、次要信息、时间戳等(labelMedium + outline 色)
|
||||||
|
TextStyle? get labelMuted => labelMedium?.copyWith(color: outline);
|
||||||
|
|
||||||
|
/// 正文次要 — 描述、提示等(bodySmall + outline 色)
|
||||||
|
TextStyle? get bodyMuted => bodySmall?.copyWith(color: outline);
|
||||||
|
|
||||||
|
/// 错误提示 — 表单错误、警告等(bodySmall + error 色)
|
||||||
|
TextStyle? get bodyError => bodySmall?.copyWith(color: error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BuildContext 主题入口
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final s = context.styles;
|
||||||
|
/// ```
|
||||||
|
extension AppThemeX on BuildContext {
|
||||||
|
AppStyles get styles => AppStyles(this);
|
||||||
|
}
|
||||||
215
apps/im_app/lib/core/ui/base/font.dart
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 字体体系 -- 与 Figma 设计稿对应
|
||||||
|
///
|
||||||
|
/// L1 基础常量 — 不含颜色,只定义字号/字重/行高/字距。
|
||||||
|
/// View 层通过 [AppStyles](`context.styles`)消费,颜色由主题决定。
|
||||||
|
/// 特殊场景(固定样式、不跟主题)可直接引用 AppFont。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// AppFont(字体常量)← 你在这里
|
||||||
|
/// → AppTheme(组装为 TextTheme → ThemeData)
|
||||||
|
/// → MaterialApp(注入)
|
||||||
|
/// → context.styles(View 层消费)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // 推荐:通过 context.styles 消费(自动响应亮暗主题)
|
||||||
|
/// final s = context.styles;
|
||||||
|
/// Text('标题', style: s.headlineMedium);
|
||||||
|
/// Text('分组', style: s.sectionLabel); // 预组合:字体 + 主题色
|
||||||
|
///
|
||||||
|
/// // 特殊场景:固定样式,不跟主题切换
|
||||||
|
/// Text('固定', style: AppFont.bodyMedium);
|
||||||
|
/// ```
|
||||||
|
class AppFont {
|
||||||
|
AppFont._();
|
||||||
|
|
||||||
|
// ── 字体族 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 默认字体族(系统字体)
|
||||||
|
///
|
||||||
|
/// 接入自定义字体时只需修改此常量 + pubspec.yaml fonts 配置。
|
||||||
|
static const String? _fontFamily = null; // null = 系统默认字体
|
||||||
|
|
||||||
|
// ── Display -- 超大展示(启动页、空状态大标题)──────────────────────────
|
||||||
|
|
||||||
|
static const displayLarge = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 57,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
letterSpacing: -0.25,
|
||||||
|
height: 64 / 57,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const displayMedium = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 45,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 52 / 45,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const displaySmall = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 44 / 36,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Headline -- 页面标题(导航栏、Section 标题)────────────────────────
|
||||||
|
|
||||||
|
static const headlineLarge = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 40 / 32,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const headlineMedium = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 36 / 28,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const headlineSmall = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 32 / 24,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Title -- 卡片 / 列表标题(聊天列表名称、设置项标题)──────────────
|
||||||
|
|
||||||
|
static const titleLarge = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 28 / 22,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const titleMedium = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.15,
|
||||||
|
height: 24 / 16,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const titleSmall = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
height: 20 / 14,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Body -- 正文内容(聊天气泡、表单输入、描述文字)──────────────────
|
||||||
|
|
||||||
|
static const bodyLarge = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
height: 24 / 16,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const bodyMedium = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
letterSpacing: 0.25,
|
||||||
|
height: 20 / 14,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const bodySmall = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
height: 16 / 12,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Label -- 按钮 / 标签 / 辅助文字(按钮文字、Tab、Badge)──────────
|
||||||
|
|
||||||
|
static const labelLarge = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
height: 20 / 14,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const labelMedium = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
height: 16 / 12,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const labelSmall = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
height: 16 / 11,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 语义字体(超出 M3 标准级别的产品专属规格)────────────────────────────
|
||||||
|
//
|
||||||
|
// 这里只定义字号/字重/字距,不含颜色。
|
||||||
|
// 颜色由 AppStyles 的预组合样式注入(如 AppStyles.sectionLabel)。
|
||||||
|
|
||||||
|
/// 分组标题:列表 Section、设置分组等(13 / w600 / 0.5 字距)
|
||||||
|
static const sectionLabel = TextStyle(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 组装 TextTheme(供 AppTheme 调用)──────────────────────────────────
|
||||||
|
|
||||||
|
/// 根据亮暗模式组装 TextTheme
|
||||||
|
///
|
||||||
|
/// 默认亮暗共用同一套字体规格。需要按模式区分时,
|
||||||
|
/// 用 copyWith 覆盖个别样式即可,不影响其他级别。
|
||||||
|
///
|
||||||
|
/// 示例 -- 暗色模式下 labelLarge 改为 regular:
|
||||||
|
/// ```dart
|
||||||
|
/// labelLarge: isDark
|
||||||
|
/// ? labelLarge.copyWith(fontWeight: FontWeight.w400)
|
||||||
|
/// : labelLarge,
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// AppTheme._build() 中调用:
|
||||||
|
/// ```dart
|
||||||
|
/// textTheme: AppFont.textTheme(brightness),
|
||||||
|
/// ```
|
||||||
|
static TextTheme textTheme(Brightness brightness) {
|
||||||
|
// final isDark = brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return TextTheme(
|
||||||
|
displayLarge: displayLarge,
|
||||||
|
displayMedium: displayMedium,
|
||||||
|
displaySmall: displaySmall,
|
||||||
|
headlineLarge: headlineLarge,
|
||||||
|
headlineMedium: headlineMedium,
|
||||||
|
headlineSmall: headlineSmall,
|
||||||
|
titleLarge: titleLarge,
|
||||||
|
titleMedium: titleMedium,
|
||||||
|
titleSmall: titleSmall,
|
||||||
|
bodyLarge: bodyLarge,
|
||||||
|
bodyMedium: bodyMedium,
|
||||||
|
bodySmall: bodySmall,
|
||||||
|
labelLarge: labelLarge,
|
||||||
|
labelMedium: labelMedium,
|
||||||
|
labelSmall: labelSmall,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
apps/im_app/lib/core/ui/components/app_button.dart
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../base/context_theme_ext.dart';
|
||||||
|
|
||||||
|
/// # AppButton — 按钮原子组件(L2 Component)
|
||||||
|
///
|
||||||
|
/// 四种命名构造器对应四种变体,loading 状态自动禁用点击。
|
||||||
|
/// 颜色和形状由 AppTheme 定义,AppButton 只做变体切换和 loading 逻辑。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// View 层 Widget 树
|
||||||
|
/// → ★ AppButton.primary / .secondary / .text / .inverse ★ ← 你在这里
|
||||||
|
/// → ElevatedButton / OutlinedButton / TextButton / FilledButton
|
||||||
|
/// → AppTheme(颜色 / 形状已在 ThemeData 中定义)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // 主按钮(全宽,填充色)
|
||||||
|
/// AppButton.primary(label: '登录', onPressed: () => vm.login()),
|
||||||
|
///
|
||||||
|
/// // 加载状态(禁用点击,显示进度圈)
|
||||||
|
/// AppButton.primary(label: '登录', onPressed: null, isLoading: true),
|
||||||
|
///
|
||||||
|
/// // 副按钮(描边)
|
||||||
|
/// AppButton.secondary(label: '注册', onPressed: () {}),
|
||||||
|
///
|
||||||
|
/// // 文字按钮(非全宽)
|
||||||
|
/// AppButton.text(label: '忘记密码?', onPressed: () {}),
|
||||||
|
///
|
||||||
|
/// // 反色按钮:亮色模式黑底白字,暗色模式白底黑字
|
||||||
|
/// AppButton.inverse(
|
||||||
|
/// label: '切换 Tab',
|
||||||
|
/// icon: const Icon(Icons.swap_horiz),
|
||||||
|
/// onPressed: () {},
|
||||||
|
/// ),
|
||||||
|
/// ```
|
||||||
|
enum _ButtonVariant { primary, secondary, text, inverse }
|
||||||
|
|
||||||
|
class AppButton extends StatelessWidget {
|
||||||
|
const AppButton.primary({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
this.onPressed,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.fullWidth = true,
|
||||||
|
}) : _variant = _ButtonVariant.primary,
|
||||||
|
icon = null;
|
||||||
|
|
||||||
|
const AppButton.secondary({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
this.onPressed,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.fullWidth = true,
|
||||||
|
}) : _variant = _ButtonVariant.secondary,
|
||||||
|
icon = null;
|
||||||
|
|
||||||
|
const AppButton.text({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
this.onPressed,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.fullWidth = false,
|
||||||
|
}) : _variant = _ButtonVariant.text,
|
||||||
|
icon = null;
|
||||||
|
|
||||||
|
/// 反色按钮:颜色随明暗主题取反。
|
||||||
|
///
|
||||||
|
/// 亮色模式:黑色背景 + 白色文字。
|
||||||
|
/// 暗色模式:白色背景 + 黑色文字。
|
||||||
|
///
|
||||||
|
/// 可选传 [icon](`Icon` widget),自动切换为带图标布局。
|
||||||
|
const AppButton.inverse({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
this.onPressed,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.fullWidth = false,
|
||||||
|
this.icon,
|
||||||
|
}) : _variant = _ButtonVariant.inverse;
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool fullWidth;
|
||||||
|
|
||||||
|
/// 仅 [AppButton.inverse] 使用,其余变体固定为 null
|
||||||
|
final Widget? icon;
|
||||||
|
|
||||||
|
final _ButtonVariant _variant;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final label = isLoading
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: 20,
|
||||||
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Text(this.label);
|
||||||
|
|
||||||
|
final button = switch (_variant) {
|
||||||
|
_ButtonVariant.primary =>
|
||||||
|
ElevatedButton(onPressed: isLoading ? null : onPressed, child: label),
|
||||||
|
_ButtonVariant.secondary =>
|
||||||
|
OutlinedButton(onPressed: isLoading ? null : onPressed, child: label),
|
||||||
|
_ButtonVariant.text =>
|
||||||
|
TextButton(onPressed: isLoading ? null : onPressed, child: label),
|
||||||
|
_ButtonVariant.inverse => _buildInverse(context, label),
|
||||||
|
};
|
||||||
|
|
||||||
|
return fullWidth ? SizedBox(width: double.infinity, child: button) : button;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInverse(BuildContext context, Widget label) {
|
||||||
|
final s = context.styles;
|
||||||
|
final isDark = s.isDark;
|
||||||
|
final bg = isDark ? Colors.white : Colors.black;
|
||||||
|
final fg = isDark ? Colors.black : Colors.white;
|
||||||
|
final style = FilledButton.styleFrom(
|
||||||
|
backgroundColor: bg,
|
||||||
|
foregroundColor: fg,
|
||||||
|
);
|
||||||
|
if (icon != null) {
|
||||||
|
return FilledButton.icon(
|
||||||
|
onPressed: isLoading ? null : onPressed,
|
||||||
|
style: style,
|
||||||
|
icon: icon!,
|
||||||
|
label: label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FilledButton(
|
||||||
|
onPressed: isLoading ? null : onPressed,
|
||||||
|
style: style,
|
||||||
|
child: label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
apps/im_app/lib/core/ui/composites/app_dialog.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../components/app_button.dart';
|
||||||
|
|
||||||
|
/// # AppDialog — 业务确认弹窗(L3 Composite)
|
||||||
|
///
|
||||||
|
/// 封装 showDialog,统一弹窗交互规范(标题 + 内容 + 确认/取消)。
|
||||||
|
/// 内部使用 AppButton,展示 L3 → L2 → L1 的完整组合链路。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// View 层调用
|
||||||
|
/// → AppDialog.show() ← 你在这里(静态入口)
|
||||||
|
/// → showDialog<bool>
|
||||||
|
/// → AppDialog widget(AlertDialog 布局)
|
||||||
|
/// → AppButton.text(取消)
|
||||||
|
/// → AppButton.primary(确认)
|
||||||
|
/// ← Future<bool?> → true=确认, false=取消, null=点背景关闭
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## 使用
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // View 层
|
||||||
|
/// final confirmed = await AppDialog.show(
|
||||||
|
/// context,
|
||||||
|
/// title: '删除联系人',
|
||||||
|
/// content: '确定要删除该联系人吗?此操作不可恢复。',
|
||||||
|
/// confirmLabel: '删除',
|
||||||
|
/// );
|
||||||
|
/// if (confirmed == true) {
|
||||||
|
/// ref.read(contactViewModelProvider.notifier).deleteContact(id);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
class AppDialog extends StatelessWidget {
|
||||||
|
const AppDialog._({
|
||||||
|
required this.title,
|
||||||
|
required this.content,
|
||||||
|
required this.confirmLabel,
|
||||||
|
this.cancelLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String content;
|
||||||
|
final String confirmLabel;
|
||||||
|
final String? cancelLabel;
|
||||||
|
|
||||||
|
/// 显示确认弹窗
|
||||||
|
///
|
||||||
|
/// 返回:`true` = 确认,`false` = 取消,`null` = 点背景关闭
|
||||||
|
static Future<bool?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String content,
|
||||||
|
String confirmLabel = '确定', // TODO: 接入国际化
|
||||||
|
String? cancelLabel = '取消', // TODO: 接入国际化
|
||||||
|
bool barrierDismissible = true,
|
||||||
|
}) =>
|
||||||
|
showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: barrierDismissible,
|
||||||
|
builder: (_) => AppDialog._(
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
confirmLabel: confirmLabel,
|
||||||
|
cancelLabel: cancelLabel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(content),
|
||||||
|
actions: [
|
||||||
|
if (cancelLabel != null)
|
||||||
|
AppButton.text(
|
||||||
|
label: cancelLabel!,
|
||||||
|
fullWidth: false,
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
),
|
||||||
|
AppButton.primary(
|
||||||
|
label: confirmLabel,
|
||||||
|
fullWidth: false,
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
0
apps/im_app/lib/data/cache/.gitkeep
vendored
Normal file
40
apps/im_app/lib/data/local/drift/app_database.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:im_app/data/local/drift/tables/users.dart';
|
||||||
|
|
||||||
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(tables: [Users])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
AppDatabase(super.e);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration {
|
||||||
|
return MigrationStrategy(
|
||||||
|
onCreate: (m) async {
|
||||||
|
await m.createAll();
|
||||||
|
},
|
||||||
|
onUpgrade: (m, from, to) async {
|
||||||
|
// 自动检测并添加缺失列
|
||||||
|
for (final table in allTables) {
|
||||||
|
//取原来的字段
|
||||||
|
final existingColumns = await m.database
|
||||||
|
.customSelect('PRAGMA table_info(${table.actualTableName})')
|
||||||
|
.get();
|
||||||
|
final existingNames = existingColumns
|
||||||
|
.map((r) => r.data['name'] as String)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
for (final column in table.$columns) {
|
||||||
|
if (!existingNames.contains(column.name)) {
|
||||||
|
//字段缺失,添加。
|
||||||
|
await m.addColumn(table, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
apps/im_app/lib/data/local/drift/daos/.gitkeep
Normal file
41
apps/im_app/lib/data/local/drift/tables/users.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
@DataClassName('User')
|
||||||
|
class Users extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get uid => integer().nullable()();
|
||||||
|
TextColumn get uuid => text().nullable()();
|
||||||
|
IntColumn get lastOnline => integer().nullable()();
|
||||||
|
TextColumn get profilePic => text().nullable()();
|
||||||
|
TextColumn get profilePicGaussian => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get nickname => text().nullable()();
|
||||||
|
TextColumn get depositName => text().nullable()();
|
||||||
|
IntColumn get hasSetDepositName => integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get contact => text().nullable()();
|
||||||
|
TextColumn get countryCode => text().nullable()();
|
||||||
|
TextColumn get username => text().nullable()();
|
||||||
|
IntColumn get role => integer().nullable()();
|
||||||
|
IntColumn get relationship => integer().nullable()();
|
||||||
|
IntColumn get friendStatus => integer().nullable()();
|
||||||
|
TextColumn get bio => text().nullable()();
|
||||||
|
TextColumn get userAlias => text().nullable()();
|
||||||
|
IntColumn get requestAt => integer().nullable()();
|
||||||
|
IntColumn get deletedAt => integer().nullable()();
|
||||||
|
TextColumn get email => text().nullable()();
|
||||||
|
TextColumn get recoveryEmail => text().nullable()();
|
||||||
|
TextColumn get remark => text().nullable()();
|
||||||
|
TextColumn get source => text().nullable()();
|
||||||
|
IntColumn get addIndex => integer().nullable()();
|
||||||
|
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get notificationSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get sendMessageSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get groupNotificationSoundId => integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get groupTags => text().withDefault(const Constant('[]'))();
|
||||||
|
TextColumn get friendTags => text().withDefault(const Constant('[]'))();
|
||||||
|
TextColumn get publicKey => text().nullable()();
|
||||||
|
IntColumn get configBits => integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get hint => text().nullable()();
|
||||||
|
@override
|
||||||
|
String get tableName => 'user';
|
||||||
|
}
|
||||||
0
apps/im_app/lib/data/local/storage/.gitkeep
Normal file
68
apps/im_app/lib/data/models/user_dto.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/user.dart';
|
||||||
|
|
||||||
|
part 'user_dto.g.dart';
|
||||||
|
|
||||||
|
/// 用户 DTO(Data Transfer Object)
|
||||||
|
///
|
||||||
|
/// local / remote 共用的数据传输对象,放在 data/models/。
|
||||||
|
/// 提供与 Domain Entity [User] 之间的双向转换。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置(本地存储场景)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// 写入本地:
|
||||||
|
/// LoginData.toEntity() → User
|
||||||
|
/// → UserDto.fromEntity(user) → ★ UserDto ★ ← 你在这里
|
||||||
|
/// → toJson() → SQLite / SharedPreferences
|
||||||
|
///
|
||||||
|
/// 读取本地:
|
||||||
|
/// SQLite / SharedPreferences → JSON
|
||||||
|
/// → ★ UserDto.fromJson() ★ ← 你在这里
|
||||||
|
/// → UserDto.toEntity() → User
|
||||||
|
/// → ViewModel.state → View
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// 注意:登录接口的 Response DTO 是 [LoginData](含 token),
|
||||||
|
/// 本类用于纯用户信息的本地持久化,不含 token。
|
||||||
|
@JsonSerializable()
|
||||||
|
class UserDto {
|
||||||
|
@JsonKey(name: 'user_id')
|
||||||
|
final String userId;
|
||||||
|
final String email;
|
||||||
|
final String? nickname;
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
const UserDto({
|
||||||
|
required this.userId,
|
||||||
|
required this.email,
|
||||||
|
this.nickname,
|
||||||
|
this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UserDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UserDtoFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$UserDtoToJson(this);
|
||||||
|
|
||||||
|
/// DTO → Domain Entity
|
||||||
|
User toEntity() {
|
||||||
|
return User(
|
||||||
|
id: userId,
|
||||||
|
email: email,
|
||||||
|
nickname: nickname,
|
||||||
|
avatar: avatar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Domain Entity → DTO
|
||||||
|
factory UserDto.fromEntity(User user) {
|
||||||
|
return UserDto(
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
nickname: user.nickname,
|
||||||
|
avatar: user.avatar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
apps/im_app/lib/data/remote/get_profile_request.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import '../../../core/foundation/api_paths.dart';
|
||||||
|
import '../../../domain/entities/user.dart';
|
||||||
|
|
||||||
|
part 'get_profile_request.g.dart';
|
||||||
|
|
||||||
|
/// # /user/profile — 获取用户资料(GET 请求示例)
|
||||||
|
///
|
||||||
|
/// 演示:GET 请求 + 无 body 参数的模式。
|
||||||
|
/// GET 请求的 toJson() 结果会自动作为 URL query parameters 发送。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// UserRepositoryImpl.getProfile()
|
||||||
|
/// → _client.executeRequest( ★ GetProfileRequest ★ ) ← 你在这里
|
||||||
|
/// → 服务端 GET /user/profile
|
||||||
|
/// → 响应 JSON → ★ ProfileData ★ ← 也在这里
|
||||||
|
/// → ProfileData.toEntity() → User
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Response DTO
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 用户资料响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class ProfileData {
|
||||||
|
@JsonKey(name: 'user_id')
|
||||||
|
final String userId;
|
||||||
|
final String email;
|
||||||
|
final String? nickname;
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
const ProfileData({
|
||||||
|
required this.userId,
|
||||||
|
required this.email,
|
||||||
|
this.nickname,
|
||||||
|
this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ProfileData.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ProfileDataFromJson(json);
|
||||||
|
|
||||||
|
/// DTO → Domain Entity
|
||||||
|
User toEntity() {
|
||||||
|
return User(
|
||||||
|
id: userId,
|
||||||
|
email: email,
|
||||||
|
nickname: nickname,
|
||||||
|
avatar: avatar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Request
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 获取用户资料请求(GET,无参数)
|
||||||
|
///
|
||||||
|
/// GET 请求无 body,toJson() 返回空 map。
|
||||||
|
/// 如需 query 参数(如分页),添加字段即可,
|
||||||
|
/// toJson() 会自动将字段序列化为 URL query string。
|
||||||
|
@ApiRequest(
|
||||||
|
path: ApiPaths.userProfile,
|
||||||
|
method: HttpMethod.get,
|
||||||
|
responseType: ProfileData,
|
||||||
|
)
|
||||||
|
@JsonSerializable()
|
||||||
|
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||||||
|
with _$GetProfileRequestApi {
|
||||||
|
GetProfileRequest();
|
||||||
|
|
||||||
|
factory GetProfileRequest.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$GetProfileRequestFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
||||||
|
}
|
||||||
90
apps/im_app/lib/data/remote/login_request.dart
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import '../../../core/foundation/api_paths.dart';
|
||||||
|
import '../../../domain/entities/user.dart';
|
||||||
|
|
||||||
|
part 'login_request.g.dart';
|
||||||
|
|
||||||
|
/// # /auth/login — 登录接口
|
||||||
|
///
|
||||||
|
/// 一个端点 = 一个文件,Response DTO + Request 放在同一文件中。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// AuthRepositoryImpl.login(email, password)
|
||||||
|
/// → _client.executeRequest( ★ LoginRequest ★ ) ← 你在这里
|
||||||
|
/// → 服务端 POST /auth/login
|
||||||
|
/// → 响应 JSON → ★ LoginData ★ ← 也在这里
|
||||||
|
/// → LoginData.toEntity() → User
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Response DTO
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 登录响应 DTO
|
||||||
|
///
|
||||||
|
/// 服务端返回的登录数据,包含 token 和用户信息。
|
||||||
|
/// 通过 [toEntity] 转换为 Domain Entity [User]。
|
||||||
|
@JsonSerializable()
|
||||||
|
class LoginData {
|
||||||
|
final String token;
|
||||||
|
@JsonKey(name: 'user_id')
|
||||||
|
final String userId;
|
||||||
|
final String email;
|
||||||
|
final String? nickname;
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
const LoginData({
|
||||||
|
required this.token,
|
||||||
|
required this.userId,
|
||||||
|
required this.email,
|
||||||
|
this.nickname,
|
||||||
|
this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginData.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
||||||
|
|
||||||
|
/// DTO → Domain Entity
|
||||||
|
User toEntity() {
|
||||||
|
return User(
|
||||||
|
id: userId,
|
||||||
|
email: email,
|
||||||
|
nickname: nickname,
|
||||||
|
avatar: avatar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Request
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 登录请求
|
||||||
|
///
|
||||||
|
/// `@ApiRequest` 自动生成 `_$LoginRequestApi` mixin,
|
||||||
|
/// 提供 path / method / requestType / includeToken / fromJson 自动注册。
|
||||||
|
@ApiRequest(
|
||||||
|
path: ApiPaths.authLogin,
|
||||||
|
method: HttpMethod.post,
|
||||||
|
responseType: LoginData,
|
||||||
|
requestType: ApiRequestType.login,
|
||||||
|
)
|
||||||
|
@JsonSerializable()
|
||||||
|
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
LoginRequest({required this.email, required this.password});
|
||||||
|
|
||||||
|
factory LoginRequest.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginRequestFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||||||
|
}
|
||||||
32
apps/im_app/lib/data/remote/logout_request.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import '../../../core/foundation/api_paths.dart';
|
||||||
|
|
||||||
|
/// # /auth/logout — 登出接口(无响应数据示例)
|
||||||
|
///
|
||||||
|
/// 演示:POST 请求 + 无 Response DTO 的模式。
|
||||||
|
/// 服务端返回 `{"code": 0, "message": "ok"}` 无 data 字段,
|
||||||
|
/// `executeRequest` 返回 null,调用方直接 await 即可。
|
||||||
|
///
|
||||||
|
/// 此接口不使用 @ApiRequest 注解,直接实现 ApiRequestable,
|
||||||
|
/// 演示手动实现方式(适用于不需要代码生成器的简单接口)。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// AuthRepositoryImpl.logout()
|
||||||
|
/// → _client.executeRequest( ★ LogoutRequest ★ ) ← 你在这里
|
||||||
|
/// → 服务端 POST /auth/logout
|
||||||
|
/// → 响应 {"code": 0, "message": "ok"} → null
|
||||||
|
/// ```
|
||||||
|
class LogoutRequest extends ApiRequestable<void> {
|
||||||
|
@override
|
||||||
|
String get path => ApiPaths.authLogout;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpMethod get method => HttpMethod.post;
|
||||||
|
|
||||||
|
/// 登出不需要请求体参数
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => {};
|
||||||
|
}
|
||||||
160
apps/im_app/lib/data/remote/upload_file_request.dart
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import '../../../core/foundation/api_paths.dart';
|
||||||
|
|
||||||
|
part 'upload_file_request.g.dart';
|
||||||
|
|
||||||
|
/// # /upload/file — 文件上传(Upload 请求示例)
|
||||||
|
///
|
||||||
|
/// 演示两种上传模式:
|
||||||
|
///
|
||||||
|
/// ## 模式 A: FormData 上传到自有后端
|
||||||
|
/// 适用于后端直接接收文件的场景。
|
||||||
|
/// 使用 [UploadFileRequest] — path 为相对路径,SDK 自动拼 baseURL。
|
||||||
|
///
|
||||||
|
/// ## 模式 B: 二进制上传到 S3 presigned URL
|
||||||
|
/// 适用于先向后端获取 presigned URL,再直接上传到 S3 的场景。
|
||||||
|
/// 使用 [S3UploadRequest] — path 为完整 URL,override decodeResponse。
|
||||||
|
///
|
||||||
|
/// ## Upload 与普通请求的区别
|
||||||
|
///
|
||||||
|
/// | 普通请求 | Upload 请求 |
|
||||||
|
/// |---------|-----------|
|
||||||
|
/// | `toJson()` → JSON body | `uploadData` → FormData / Uint8List |
|
||||||
|
/// | `requestType: request` | `requestType: upload` |
|
||||||
|
/// | `parameters` 有值 | `parameters` 返回 null |
|
||||||
|
/// | 标准 `{ code, msg, data }` 响应 | 可能需要 override `decodeResponse` |
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Response DTO
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 文件上传响应 DTO(只需反序列化,禁止生成无用的 toJson)
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class UploadResult {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
@JsonKey(name: 'file_id')
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
const UploadResult({required this.url, required this.fileId});
|
||||||
|
|
||||||
|
factory UploadResult.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadResultFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════
|
||||||
|
// 模式 A: FormData 上传到自有后端
|
||||||
|
// ═════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// FormData 上传请求
|
||||||
|
///
|
||||||
|
/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 信封。
|
||||||
|
/// 无需 override `decodeResponse`。
|
||||||
|
@ApiRequest(
|
||||||
|
path: ApiPaths.uploadFile,
|
||||||
|
method: HttpMethod.post,
|
||||||
|
responseType: UploadResult,
|
||||||
|
requestType: ApiRequestType.upload,
|
||||||
|
)
|
||||||
|
class UploadFileRequest extends ApiRequestable<UploadResult>
|
||||||
|
with _$UploadFileRequestApi {
|
||||||
|
final String filePath;
|
||||||
|
final String? fileName;
|
||||||
|
|
||||||
|
UploadFileRequest({required this.filePath, this.fileName});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => {};
|
||||||
|
|
||||||
|
/// FormData — SDK 通过 uploadData 获取上传数据
|
||||||
|
@override
|
||||||
|
Object? get uploadData {
|
||||||
|
return FormData.fromMap({
|
||||||
|
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════
|
||||||
|
// 模式 B: 二进制上传到 S3 presigned URL
|
||||||
|
// ═════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// S3 presigned URL 上传响应
|
||||||
|
class S3UploadResponse {
|
||||||
|
final bool success;
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
const S3UploadResponse({this.success = true, this.message});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// S3 presigned URL 上传请求
|
||||||
|
///
|
||||||
|
/// 特点:
|
||||||
|
/// - path 为完整的 presigned URL(SDK 检测到 http 开头不拼 baseURL)
|
||||||
|
/// - uploadData 为 Uint8List 二进制数据
|
||||||
|
/// - 自定义 headers(Content-Type: application/octet-stream)
|
||||||
|
/// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准信封
|
||||||
|
class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
|
||||||
|
final Uint8List data;
|
||||||
|
final String presignedURL;
|
||||||
|
|
||||||
|
S3UploadRequest({required this.data, required this.presignedURL});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get path => presignedURL;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpMethod get method => HttpMethod.put;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ApiRequestType get requestType => ApiRequestType.upload;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String>? get customHeaders => {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => {};
|
||||||
|
|
||||||
|
/// 二进制数据
|
||||||
|
@override
|
||||||
|
Object? get uploadData => data;
|
||||||
|
|
||||||
|
/// S3 响应不走标准 { code, message, data } 信封,需要自定义解码
|
||||||
|
///
|
||||||
|
/// 可能的响应:
|
||||||
|
/// - 204 No Content(空 body)→ 成功
|
||||||
|
/// - 200 + XML body → 成功
|
||||||
|
/// - 200 + JSON body → 尝试解码
|
||||||
|
@override
|
||||||
|
S3UploadResponse? decodeResponse(Response response) {
|
||||||
|
// 空响应或 2xx 状态码 → 成功
|
||||||
|
if (response.data == null ||
|
||||||
|
(response.data is List && (response.data as List).isEmpty)) {
|
||||||
|
return const S3UploadResponse(success: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 响应 → 尝试解码
|
||||||
|
if (response.data is Map<String, dynamic>) {
|
||||||
|
final json = response.data as Map<String, dynamic>;
|
||||||
|
return S3UploadResponse(
|
||||||
|
success: true,
|
||||||
|
message: json['message'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2xx 状态码 → 成功
|
||||||
|
if (response.statusCode != null &&
|
||||||
|
response.statusCode! >= 200 &&
|
||||||
|
response.statusCode! < 300) {
|
||||||
|
return const S3UploadResponse(success: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const S3UploadResponse(success: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
apps/im_app/lib/data/repositories/auth_repository_impl.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:networks_sdk/networks_sdk.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/user.dart';
|
||||||
|
import '../../domain/repositories/auth_repository.dart';
|
||||||
|
import '../remote/login_request.dart';
|
||||||
|
import '../remote/logout_request.dart';
|
||||||
|
|
||||||
|
/// 认证 Repository 实现
|
||||||
|
///
|
||||||
|
/// implements [AuthRepository] 接口(domain/repositories/ 中定义)。
|
||||||
|
/// 直接使用 [ApiClient] 发送请求,将 DTO 转为 Domain Entity。
|
||||||
|
/// 后续可加 Local DataSource 实现离线缓存。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// LoginUseCase.execute(email, password)
|
||||||
|
/// → ★ AuthRepositoryImpl.login() ★ ← 你在这里
|
||||||
|
/// → ApiClient.executeRequest(LoginRequest)
|
||||||
|
/// → 服务端 POST /auth/login
|
||||||
|
/// ← LoginData(Response DTO)
|
||||||
|
/// → onTokenUpdate(token) ← 回调写入 Token
|
||||||
|
/// ← LoginData.toEntity() → User ← DTO → Entity 转换在这里
|
||||||
|
/// ← User(Domain Entity)
|
||||||
|
/// ```
|
||||||
|
class AuthRepositoryImpl implements AuthRepository {
|
||||||
|
final NetworksSdkApi _client;
|
||||||
|
final void Function(String?) _onTokenUpdate;
|
||||||
|
|
||||||
|
AuthRepositoryImpl({required NetworksSdkApi client, required void Function(String?) onTokenUpdate,}) : _client = client, _onTokenUpdate = onTokenUpdate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<User> login({required String email, required String password,}) async
|
||||||
|
{
|
||||||
|
final LoginData? loginData = await _client.executeRequest(LoginRequest(email: email, password: password),);
|
||||||
|
|
||||||
|
if (loginData == null) {
|
||||||
|
throw Exception('Login failed: empty response'); // TODO: 接入国际化
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回调写入 Token(内存 + 持久化由 Provider 层组合)
|
||||||
|
_onTokenUpdate(loginData.token);
|
||||||
|
|
||||||
|
return loginData.toEntity(); // DTO → Domain Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<User?> getCurrentUser() async {
|
||||||
|
// TODO: 从本地存储获取用户信息
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> logout() async {
|
||||||
|
await _client.executeRequest(LogoutRequest());
|
||||||
|
_onTokenUpdate(null); // 回调清除 Token(内存 + 持久化由 Provider 层组合)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
apps/im_app/lib/domain/entities/user.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// 用户 Domain 实体
|
||||||
|
///
|
||||||
|
/// 全局共享实体,被 auth / chat / contact 等多个 Feature 共用。
|
||||||
|
/// 纯 Dart 类,零 Flutter / 零网络 / 零 DB 依赖。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// 服务端 JSON
|
||||||
|
/// → LoginData(Response DTO,data/remote/login_request.dart)
|
||||||
|
/// → LoginData.toEntity()
|
||||||
|
/// → ★ User ★ ← 你在这里
|
||||||
|
/// → ViewModel.state
|
||||||
|
/// → View 渲染
|
||||||
|
/// ```
|
||||||
|
class User {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String? nickname;
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
this.nickname,
|
||||||
|
this.avatar,
|
||||||
|
});
|
||||||
|
}
|
||||||
26
apps/im_app/lib/domain/repositories/auth_repository.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import '../entities/user.dart';
|
||||||
|
|
||||||
|
/// 认证 Repository 接口(依赖倒置)
|
||||||
|
///
|
||||||
|
/// Domain 层定义 What,Data 层实现 How。
|
||||||
|
/// ViewModel 依赖此接口,不依赖具体实现 [AuthRepositoryImpl]。
|
||||||
|
///
|
||||||
|
/// ## 数据流位置
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// ViewModel
|
||||||
|
/// → ★ AuthRepository.login() ★ ← 你在这里(接口)
|
||||||
|
/// → AuthRepositoryImpl.login() ← data/repositories/(实现)
|
||||||
|
/// → _client.executeRequest(LoginRequest)
|
||||||
|
/// → 服务端
|
||||||
|
/// ```
|
||||||
|
abstract interface class AuthRepository {
|
||||||
|
/// 登录,返回 Domain Entity [User]
|
||||||
|
Future<User> login({required String email, required String password});
|
||||||
|
|
||||||
|
/// 获取当前登录用户信息
|
||||||
|
Future<User?> getCurrentUser();
|
||||||
|
|
||||||
|
/// 退出登录
|
||||||
|
Future<void> logout();
|
||||||
|
}
|
||||||