Initial project

This commit is contained in:
Cody
2026-03-06 14:56:17 +08:00
parent 977b627b15
commit bf9e099747
1180 changed files with 50973 additions and 0 deletions

45
apps/im_app/.gitignore vendored Normal file
View File

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

30
apps/im_app/.metadata Normal file
View 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
View File

@@ -0,0 +1,3 @@
# im_app
A new Flutter project.

View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

14
apps/im_app/android/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,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")
}

View 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在此追加

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

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

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.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
View File

@@ -0,0 +1,6 @@
targets:
$default:
builders:
drift_dev|preparing_builder:
generate_for:
- lib/**

View 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
View File

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

View File

@@ -0,0 +1,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>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View 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
View 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

View 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 */;
}

View File

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

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,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>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,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)
}
}

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Icon-App-1024x1024@1x.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

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

View 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)
}
}

View File

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

View File

@@ -0,0 +1,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),
);
}
}

View 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(),
),
);
}

View 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(),
// ),
],
);
});

View 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 负责 schemaAppDatabase + 各业务表)。
///
/// 用法:
/// ```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),
);
});

View 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.onStatusChangedtrue / false
// → SocketManager.handleNetworkStatusChanged()
// → 断网: disconnect()
// → 恢复: connect(token: lastToken)
//
// 前后台事件驱动链路:
//
// WidgetsBindingObserverApp 层 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: 定义 Requestdata/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: Repositorydata/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));
// });
//
// // --- ViewModelfeatures/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)
// → 返回 LoginDataDTO
// → _onTokenUpdate(token) 回调写入 TokenProvider 层组合:内存 + 持久化)
// → LoginData.toEntity() → UserDomain 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 不走标准信封
//

View 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';
}

View 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 ChatDetailPageextra 传参)
/// /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 NavigatorShell 不会被盖住TabBar 仍然可见。
///
/// 设置 `parentNavigatorKey: _rootKey` 后,路由强制放到 Root Navigator
/// 盖住整个 ShellTabBar 消失,表现为真正的全屏页面。
///
/// ## 登录守卫
///
/// [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 确保路由覆盖 ShellTabBar 消失
//
// 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(),
),
],
);
});

View 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;
}
}

View 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';
}

View 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;
}

View 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);
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/widgets.dart';
/// App 启动初始化器
///
/// 两阶段串行队列,确保启动流畅、资源不竞争。
///
/// ## 为什么不能在 initState 里一股脑 await
///
/// 初始化任务并发执行会导致:
/// - 多个 await 阻塞首帧渲染 → 白屏时间长
/// - 任务间资源竞争网络、IO、CPU→ 互相拖慢
/// - 一个任务失败可能阻塞后续所有任务
///
/// ## 解决方案:两阶段 + 串行队列
///
/// ```
/// App 启动
/// │
/// ├── Phase 1: CriticalinitState 中同步触发)
/// │ 串行执行,必须在用户交互前完成。
/// │ 只放真正阻塞用户操作的任务(尽量少)。
/// │ 例:网络监听(后续所有网络操作依赖它)
/// │
/// ├── 首帧渲染(用户看到 UI
/// │
/// └── Phase 2: DeferredaddPostFrameCallback 触发)
/// 串行执行,首帧渲染后逐个跑。
/// 不争抢资源,不影响 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,
});
}

View 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();
}
}

View 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');
}
}

View 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 连接管理
///
/// 在 SocketClientSDK 底层能力)之上封装:
/// - 连接/断连生命周期(登录连接、登出断连)
/// - 前后台生命周期(后台断连省电、前台自动重连)
/// - 网络状态响应(断网断连、恢复网络立即重连)
/// - 操作前置检查(网络可用性 + 后台状态)
/// - 消息预处理管道(通过 [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 保持一致:
// 不仅 connectedconnecting / 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');
}
}

View 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),
),
),
);
}
}

View 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);
}

View 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);
}

View File

@@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
/// 字体体系 -- 与 Figma 设计稿对应
///
/// L1 基础常量 — 不含颜色,只定义字号/字重/行高/字距。
/// View 层通过 [AppStyles]`context.styles`)消费,颜色由主题决定。
/// 特殊场景(固定样式、不跟主题)可直接引用 AppFont。
///
/// ## 数据流位置
///
/// ```
/// AppFont字体常量← 你在这里
/// → AppTheme组装为 TextTheme → ThemeData
/// → MaterialApp注入
/// → context.stylesView 层消费)
/// ```
///
/// ## 使用
///
/// ```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,
);
}
}

View 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,
);
}
}

View 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 widgetAlertDialog 布局)
/// → 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
View File

View 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);
}
}
}
},
);
}
}

View 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';
}

View File

@@ -0,0 +1,68 @@
import 'package:json_annotation/json_annotation.dart';
import '../../domain/entities/user.dart';
part 'user_dto.g.dart';
/// 用户 DTOData 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,
);
}
}

View 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 请求无 bodytoJson() 返回空 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);
}

View 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);
}

View 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() => {};
}

View 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 为完整 URLoverride 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 URLSDK 检测到 http 开头不拼 baseURL
/// - uploadData 为 Uint8List 二进制数据
/// - 自定义 headersContent-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);
}
}

View 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
/// ← LoginDataResponse DTO
/// → onTokenUpdate(token) ← 回调写入 Token
/// ← LoginData.toEntity() → User ← DTO → Entity 转换在这里
/// ← UserDomain 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 层组合)
}
}

View File

@@ -0,0 +1,28 @@
/// 用户 Domain 实体
///
/// 全局共享实体,被 auth / chat / contact 等多个 Feature 共用。
/// 纯 Dart 类,零 Flutter / 零网络 / 零 DB 依赖。
///
/// ## 数据流位置
///
/// ```
/// 服务端 JSON
/// → LoginDataResponse DTOdata/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,
});
}

View File

@@ -0,0 +1,26 @@
import '../entities/user.dart';
/// 认证 Repository 接口(依赖倒置)
///
/// Domain 层定义 WhatData 层实现 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();
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// 主 Tab 容器Shell 层)
///
/// 由 [StatefulShellRoute.indexedStack] 驱动,不持有任何状态。
/// Tab 切换通过 [navigationShell.goBranch] 完成go_router 负责保持各 Tab 的导航栈。
///
/// Tabsindex 顺序):
/// - 0聊天
/// - 1联系人
/// - 2设置
class AppTab extends StatelessWidget {
const AppTab({
super.key,
required this.navigationShell,
});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (index) => navigationShell.goBranch(
index,
// 再次点击已激活的 Tab 时回到该 Tab 的初始路由
initialLocation: index == navigationShell.currentIndex,
),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.chat_bubble_outline),
activeIcon: Icon(Icons.chat_bubble),
label: '聊天',
),
BottomNavigationBarItem(
icon: Icon(Icons.people_outline),
activeIcon: Icon(Icons.people),
label: '联系人',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: '设置',
),
],
),
);
}
}

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