diff --git a/assets/images/default_avatar.png b/assets/images/avatar/default.png
similarity index 100%
rename from assets/images/default_avatar.png
rename to assets/images/avatar/default.png
diff --git a/assets/images/bk.jpg b/assets/images/bk.jpg
new file mode 100644
index 0000000..eb27d19
Binary files /dev/null and b/assets/images/bk.jpg differ
diff --git a/assets/images/notify/dd.png b/assets/images/notify/dd.png
new file mode 100644
index 0000000..79d4bed
Binary files /dev/null and b/assets/images/notify/dd.png differ
diff --git a/assets/images/notify/guanzhu.png b/assets/images/notify/guanzhu.png
new file mode 100644
index 0000000..a5c88f1
Binary files /dev/null and b/assets/images/notify/guanzhu.png differ
diff --git a/assets/images/notify/hudong.png b/assets/images/notify/hudong.png
new file mode 100644
index 0000000..d9be6f1
Binary files /dev/null and b/assets/images/notify/hudong.png differ
diff --git a/assets/images/notify/msr.png b/assets/images/notify/msr.png
new file mode 100644
index 0000000..ae4145c
Binary files /dev/null and b/assets/images/notify/msr.png differ
diff --git a/assets/images/notify/qun.png b/assets/images/notify/qun.png
new file mode 100644
index 0000000..2c5100a
Binary files /dev/null and b/assets/images/notify/qun.png differ
diff --git a/assets/images/notify/xitong.png b/assets/images/notify/xitong.png
new file mode 100644
index 0000000..53c761c
Binary files /dev/null and b/assets/images/notify/xitong.png differ
diff --git a/assets/images/svg/report.svg b/assets/images/svg/report.svg
new file mode 100644
index 0000000..949b3f5
--- /dev/null
+++ b/assets/images/svg/report.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ios/Podfile b/ios/Podfile
index e549ee2..2c53880 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -1,9 +1,11 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '12.0'
-
+platform :ios, '12.0'
+# 允许拉取http资源
+# ENV['COCOAPODS_ALLOW_INSECURE_SOURCES'] = 'true'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 9d1f391..9f0155d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,11 +1,25 @@
PODS:
+ - audioplayers_darwin (0.0.1):
+ - Flutter
+ - FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
+ - flutter_image_compress_common (1.0.0):
+ - Flutter
+ - Mantle
+ - SDWebImage
+ - SDWebImageWebPCoder
- flutter_native_splash (2.4.3):
- Flutter
- flutter_upgrader (1.0.7):
- Flutter
+ - fluwx (0.0.1):
+ - Flutter
+ - fluwx/pay (= 0.0.1)
+ - fluwx/pay (0.0.1):
+ - Flutter
+ - WechatOpenSDK-XCFramework (~> 2.0.4)
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
@@ -14,6 +28,21 @@ PODS:
- Flutter
- install_plugin (2.0.0):
- Flutter
+ - libwebp (1.3.2):
+ - libwebp/demux (= 1.3.2)
+ - libwebp/mux (= 1.3.2)
+ - libwebp/sharpyuv (= 1.3.2)
+ - libwebp/webp (= 1.3.2)
+ - libwebp/demux (1.3.2):
+ - libwebp/webp
+ - libwebp/mux (1.3.2):
+ - libwebp/demux
+ - libwebp/sharpyuv (1.3.2)
+ - libwebp/webp (1.3.2):
+ - libwebp/sharpyuv
+ - Mantle (2.2.0):
+ - Mantle/extobjc (= 2.2.0)
+ - Mantle/extobjc (2.2.0)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_video (0.0.1):
@@ -31,26 +60,47 @@ PODS:
- photo_manager (3.7.1):
- Flutter
- FlutterMacOS
+ - record_ios (1.0.0):
+ - Flutter
+ - SDWebImage (5.20.0):
+ - SDWebImage/Core (= 5.20.0)
+ - SDWebImage/Core (5.20.0)
+ - SDWebImageWebPCoder (0.14.6):
+ - libwebp (~> 1.0)
+ - SDWebImage/Core (~> 5.17)
+ - tencent_cloud_chat_push (8.6.7019):
+ - Flutter
+ - TIMPush (= 8.6.7019)
+ - TXIMSDK_Plus_iOS_XCFramework
- tencent_cloud_chat_sdk (8.0.0):
- Flutter
- HydraAsync
- TXIMSDK_Plus_iOS_XCFramework (~> 8.6.7019)
+ - TIMPush (8.6.7019):
+ - TXIMSDK_Plus_iOS_XCFramework (>= 8.6.7019)
- TXIMSDK_Plus_iOS_XCFramework (8.6.7019)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
+ - video_thumbnail (0.0.1):
+ - Flutter
+ - libwebp
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
+ - WechatOpenSDK-XCFramework (2.0.4)
DEPENDENCIES:
+ - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
+ - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_upgrader (from `.symlinks/plugins/flutter_upgrader/ios`)
+ - fluwx (from `.symlinks/plugins/fluwx/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- install_plugin (from `.symlinks/plugins/install_plugin/ios`)
@@ -61,26 +111,41 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
+ - record_ios (from `.symlinks/plugins/record_ios/ios`)
+ - tencent_cloud_chat_push (from `.symlinks/plugins/tencent_cloud_chat_push/ios`)
- tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
+ - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- HydraAsync
+ - libwebp
+ - Mantle
+ - SDWebImage
+ - SDWebImageWebPCoder
+ - TIMPush
- TXIMSDK_Plus_iOS_XCFramework
+ - WechatOpenSDK-XCFramework
EXTERNAL SOURCES:
+ audioplayers_darwin:
+ :path: ".symlinks/plugins/audioplayers_darwin/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
+ flutter_image_compress_common:
+ :path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_upgrader:
:path: ".symlinks/plugins/flutter_upgrader/ios"
+ fluwx:
+ :path: ".symlinks/plugins/fluwx/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
@@ -101,26 +166,37 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
+ record_ios:
+ :path: ".symlinks/plugins/record_ios/ios"
+ tencent_cloud_chat_push:
+ :path: ".symlinks/plugins/tencent_cloud_chat_push/ios"
tencent_cloud_chat_sdk:
:path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
+ video_thumbnail:
+ :path: ".symlinks/plugins/video_thumbnail/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
+ audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_upgrader: 16a975eb987fc210cdf6bebffe0069a480f80523
+ fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
install_plugin: e17e38d6f504857748a3ec1299d8a2bbeeeea854
+ libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
+ Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
@@ -128,13 +204,20 @@ SPEC CHECKSUMS:
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
+ record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
+ SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
+ SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
+ tencent_cloud_chat_push: f87ae58098c2062b06e81f39fc53afc528395916
tencent_cloud_chat_sdk: 0a406f1854a65aad2f853494c02a2e084a027ab2
+ TIMPush: d0dfe96355ee413a7cacb2576f8aaa66f6073ab2
TXIMSDK_Plus_iOS_XCFramework: cb54f7de6e30e1368c6831c6eff31c25393bbb98
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
+ video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
+ WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
-PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
+PODFILE CHECKSUM: 866435f3a12ad92d8fb66fa46b52776da7e16ce5
COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index fa53fd0..c95476e 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -15,6 +15,8 @@
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 */; };
+ C8092B212E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */; };
+ C8092B222E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
ECDFBB33253E89949730F7D8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 898AE91CA73F2F6E910D884D /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
@@ -35,6 +37,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
+ C8092B222E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -63,6 +66,8 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
A9774DDA95C7FD895F6925A4 /* 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 = ""; };
BB84C2FA9C50ACAF0C376254 /* 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 = ""; };
+ C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "WechatOpenSDK-XCFramework.xcframework"; path = "Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework"; sourceTree = ""; };
+ C891EF1D2E43F9730021EB39 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
DCA23AF172275D04ECB63EFB /* 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 = ""; };
E3EC116A6CCDD06C6D4615E2 /* 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 = ""; };
/* End PBXFileReference section */
@@ -72,6 +77,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ C8092B212E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Frameworks */,
ECDFBB33253E89949730F7D8 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -143,6 +149,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ C891EF1D2E43F9730021EB39 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -158,6 +165,7 @@
9CE4C2341F34F9A85A1D3EED /* Frameworks */ = {
isa = PBXGroup;
children = (
+ C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */,
898AE91CA73F2F6E910D884D /* Pods_Runner.framework */,
1AE799326ED7557212A901E0 /* Pods_RunnerTests.framework */,
);
@@ -487,8 +495,11 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = VZ6V44Q3T4;
+ DEVELOPMENT_TEAM = 9C9VWBX77X;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -496,8 +507,9 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.wzj41.test1;
+ PRODUCT_BUNDLE_IDENTIFIER = cn.net.wzj.mall;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -671,8 +683,11 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = VZ6V44Q3T4;
+ DEVELOPMENT_TEAM = 9C9VWBX77X;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -680,8 +695,9 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.wzj41.test1;
+ PRODUCT_BUNDLE_IDENTIFIER = cn.net.wzj.mall;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -695,8 +711,11 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = VZ6V44Q3T4;
+ DEVELOPMENT_TEAM = 9C9VWBX77X;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -704,8 +723,9 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.wzj41.test1;
+ PRODUCT_BUNDLE_IDENTIFIER = cn.net.wzj.mall;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 8be1cec..5cc1db3 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,13 +1,39 @@
-import Flutter
import UIKit
+import Flutter
-@main
-@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-}
+// Add these two import lines
+import TIMPush
+import tencent_cloud_chat_push
+
+// Add `, TIMPushDelegate` to the following line
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate, TIMPushDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ // To be deprecated,please use the new field businessID below.
+ @objc func offlinePushCertificateID() -> Int32 {
+ return TencentCloudChatPushFlutterModal.shared.offlinePushCertificateID();
+ }
+
+ // Add this function
+ @objc func businessID() -> Int32 {
+ return TencentCloudChatPushFlutterModal.shared.businessID();
+ }
+
+ // Add this function
+ @objc func applicationGroupID() -> String {
+ return TencentCloudChatPushFlutterModal.shared.applicationGroupID()
+ }
+
+ // Add this function
+ @objc func onRemoteNotificationReceived(_ notice: String?) -> Bool {
+ TencentCloudChatPushPlugin.shared.tryNotifyDartOnNotificationClickEvent(notice)
+ return true
+ }
+}
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
index f3c2851..0fb20ec 100644
--- a/ios/Runner/Base.lproj/Main.storyboard
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -1,8 +1,10 @@
-
-
+
+
+
-
+
+
@@ -14,13 +16,14 @@
-
+
-
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 36250c5..2b101d6 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -7,7 +7,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- 无终见41
+ 无终街
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -22,20 +22,44 @@
$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLName
+ weixin
+ CFBundleURLSchemes
+
+ wxebcdaea31881caab
+
+
+
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
+ LSApplicationQueriesSchemes
+
+ weixin
+ wechat
+ weixinULAPI
+
LSRequiresIPhoneOS
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
NSCameraUsageDescription
App需要使用您的相机进行拍摄
NSMicrophoneUsageDescription
- App需要访问麦克风用于视频录制
+ App需要访问麦克风用于发送语音消息
NSPhotoLibraryAddUsageDescription
- App需要权限以保存视频到您的相册
+ App需要权限以保存图片或视频到您的相册
NSPhotoLibraryLimitedUsageDescription
- App需要访问部分照片用于选择视频
+ App需要访问部分照片用于选择图片或视频
NSPhotoLibraryUsageDescription
- App需要访问您的相册用于选择视频
+ App需要访问您的相册用于选择图片或视频
UIApplicationSupportsIndirectInputEvents
UILaunchStoryboardName
@@ -59,5 +83,15 @@
UIViewControllerBasedStatusBarAppearance
+
+ UIBackgroundModes
+
+ remote-notification
+
+
+ com.apple.developer.associated-domains
+
+ applinks:wuzhongjie.com.cn
+
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
new file mode 100644
index 0000000..cf469c6
--- /dev/null
+++ b/ios/Runner/Runner.entitlements
@@ -0,0 +1,9 @@
+
+
+
+
+ aps-environment
+ development
+
+
+
diff --git a/lib/IM/controller/chat_controller.dart b/lib/IM/controller/chat_controller.dart
index 665fb5c..cda9069 100644
--- a/lib/IM/controller/chat_controller.dart
+++ b/lib/IM/controller/chat_controller.dart
@@ -1,24 +1,118 @@
import 'package:get/get.dart';
import 'package:loopin/IM/im_service.dart';
+import 'package:loopin/models/conversation_type.dart' as myConversationType;
import 'package:loopin/models/conversation_view_model.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart';
class ChatController extends GetxController {
- RxInt count = 100.obs; // 每页条数
+ RxInt count = 20.obs; // 每页条数
RxString nextSeq = '0'.obs; // 页码
+ RxBool isFinished = false.obs; // 是否拉取完?默认未拉取完
final chatList = [].obs;
- // 获取会话列表
+ void initChatData() {
+ chatList.value = [];
+ nextSeq.value = '0';
+ isFinished.value = false;
+ }
+
+ // 获取所有会话列表
void getConversationList() async {
+ if (isFinished.value) {
+ // 拉取完数据了,直接结束
+ return;
+ }
final res = await ImService.instance.getConversationList(nextSeq.value, count.value);
if (!res.success || res.data == null) return;
final List convList = res.data;
- // for (var conv in convList) {
- // logger.i('基本会话: ${conv.conversation.toLogString()}, 头像: ${conv.faceUrl}');
- // }
+ for (var conv in convList) {
+ logger.i('基本会话: ${conv.conversation.toJson()}, 会话ID: ${conv.conversation.conversationID}');
+ }
- chatList.value = convList;
+ chatList.addAll(convList);
+ // 不包含noFriend才执行加载数据逻辑,分页加载时候过滤
+ final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
+ if (!hasNoFriend) {
+ getNoFriendData();
+ }
}
+
+ ///构建陌生人消息菜单入口
+ void getNoFriendData({V2TimConversation? csion}) async {
+ // 检测会话列表是否已有陌生人消息菜单
+ final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
+ if (hasNoFriend) {
+ // 已经有了入口
+ final ConversationViewModel matchItem = chatList.firstWhere(
+ (item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false,
+ );
+ // 获取陌生人未读总数
+ final unreadTotal = await ImService.instance.getUnreadMessageCountByFilter(
+ filter: V2TimConversationFilter(
+ conversationGroup: myConversationType.ConversationType.noFriend.name,
+ hasUnreadCount: true,
+ ),
+ );
+ matchItem.conversation.lastMessage = csion!.lastMessage;
+ matchItem.conversation.unreadCount = unreadTotal.data;
+ chatList.refresh();
+ return;
+ }
+ // 没有则执行创建逻辑
+ final res = await ImService.instance.getConversationListByFilter(
+ filter: V2TimConversationFilter(conversationGroup: myConversationType.ConversationType.noFriend.name),
+ nextSeq: 0,
+ count: 1,
+ );
+ if (res.success && res.data != null) {
+ final convList = res.data!.conversationList ?? [];
+ if (convList.isNotEmpty) {
+ // logger.i(res.data!.toJson());
+ // 有陌生人消息,1.获取未读数,2.组装converstaionviewmodel
+ final unread = await ImService.instance.getUnreadMessageCountByFilter(
+ filter: V2TimConversationFilter(
+ conversationGroup: myConversationType.ConversationType.noFriend.name,
+ hasUnreadCount: true,
+ ),
+ );
+ if (unread.success) {
+ final conv = convList.first;
+ final faceUrl = 'assets/images/notify/msr.png';
+ conv.showName = '陌生人消息';
+ conv.unreadCount = unread.data;
+ final createItem = ConversationViewModel(
+ conversation: conv,
+ faceUrl: faceUrl,
+ );
+ final newList = List.from(chatList);
+ newList.add(createItem);
+ newList.sort((a, b) {
+ final atime = a.conversation.lastMessage?.timestamp ?? 0;
+ final btime = b.conversation.lastMessage?.timestamp ?? 0;
+ return btime.compareTo(atime); // 降序
+ });
+ chatList.value = newList;
+ }
+ }
+ }
+ }
+
+ /// 按会话分组查询 getConversationListByFilter
+ // void getConversationList() async {
+ // final res = await ImService.instance.getConversationListByFilter(
+ // filter: V2TimConversationFilter(conversationGroup: null),
+ // nextSeq: nextSeq.value,
+ // );
+ // final convList = res.data!.conversationList;
+ // logger.i(res.data!.toJson());
+ // chatList.value = convList;
+ // // for (var element in convList ?? []) {
+ // // logger.i(element.toJson());
+ // // // 你可以在这里继续处理 element
+ // // }
+ // }
}
diff --git a/lib/IM/controller/chat_detail_controller.dart b/lib/IM/controller/chat_detail_controller.dart
index 1a29b49..566fc13 100644
--- a/lib/IM/controller/chat_detail_controller.dart
+++ b/lib/IM/controller/chat_detail_controller.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_message.dart';
import 'package:loopin/IM/im_service.dart';
@@ -8,8 +9,10 @@ class ChatDetailController extends GetxController {
final String userID;
ChatDetailController({required this.userID});
+ final ScrollController chatController = ScrollController();
final RxList chatList = [].obs;
+ final RxBool isFriend = true.obs;
void updateChatListWithTimeLabels(List originMessages) async {
final idRes = await ImService.instance.selfUserId();
@@ -37,17 +40,54 @@ class ChatDetailController extends GetxController {
}
}
- // 把当前消息先插入后插入标签
- displayMessages.add(current);
+ // if (i == 0) {
+ // // 第一条一定插时间
+ // needInsertLabel = true;
+ // } else {
+ // final prev = originMessages[i - 1];
+ // final prevTimestamp = prev.timestamp ?? 0;
+ // final diff = currentTimestamp - prevTimestamp;
+ // if (diff > 180) {
+ // needInsertLabel = true;
+ // }
+ // }
+ // 把当前消息先插入,label后插入
+ displayMessages.add(current);
if (needInsertLabel) {
final labelTime = Utils().formatChatTime(currentTimestamp);
final timeLabel = await IMMessage().insertTimeLabel(labelTime, selfUserId);
displayMessages.add(timeLabel.data);
}
}
-
+ // 新加载的记录放在最上面
chatList.addAll(displayMessages);
}
}
+
+ ///滚动
+ void scrollToBottom() {
+ if (chatController.hasClients) {
+ chatController.animateTo(
+ 0,
+ duration: Duration(milliseconds: 200),
+ curve: Curves.easeOut,
+ );
+ }
+ // Future.delayed(Duration(milliseconds: 300), () {
+ // if (chatController.hasClients) {
+ // chatController.animateTo(
+ // chatController.position.maxScrollExtent,
+ // duration: Duration(milliseconds: 200),
+ // curve: Curves.easeOut,
+ // );
+ // }
+ // });
+ }
+
+ @override
+ void onClose() {
+ chatController.dispose();
+ super.onClose();
+ }
}
diff --git a/lib/IM/controller/im_user_info_controller.dart b/lib/IM/controller/im_user_info_controller.dart
new file mode 100644
index 0000000..5485fb3
--- /dev/null
+++ b/lib/IM/controller/im_user_info_controller.dart
@@ -0,0 +1,137 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:loopin/IM/im_service.dart';
+import 'package:shirne_dialog/shirne_dialog.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
+
+class ImUserInfoController extends GetxController {
+ @override
+ void onInit() {
+ super.onInit();
+ refreshUserInfo();
+ logger.i('IM用户信息初始化');
+ }
+
+ V2TimUserFullInfo? rawUserInfo;
+
+ final userID = ''.obs;
+ final nickname = ''.obs;
+ final faceUrl = ''.obs;
+ final signature = ''.obs;
+ final gender = 0.obs;
+ final allowType = 0.obs;
+ final customInfo = {
+ "coverBg": "",
+ "area": "",
+ "areaCode": "",
+ "openId": "",
+ }.obs;
+ final role = 0.obs;
+ final level = 0.obs;
+ final birthday = 0.obs;
+
+ void init(V2TimUserFullInfo userInfo) {
+ logger.i(userInfo.toJson());
+ rawUserInfo = userInfo;
+ userID.value = userInfo.userID ?? '';
+ nickname.value = userInfo.nickName ?? '';
+ faceUrl.value = userInfo.faceUrl ?? '';
+ signature.value = userInfo.selfSignature ?? '';
+ gender.value = userInfo.gender ?? 0;
+ allowType.value = userInfo.allowType ?? 0;
+ customInfo.assignAll(userInfo.customInfo ??
+ {
+ "coverBg": "",
+ "area": "",
+ "areaCode": "",
+ "openId": "",
+ });
+
+ role.value = userInfo.role ?? 0;
+ level.value = userInfo.level ?? 0;
+ birthday.value = userInfo.birthday ?? 0;
+ }
+
+ void refreshUserInfo() async {
+ try {
+ final updatedUserInfo = await ImService.instance.selfInfo();
+ if (updatedUserInfo.success) {
+ init(updatedUserInfo.data);
+ }
+ } catch (e) {
+ print('刷新用户信息失败: $e');
+ }
+ }
+
+ /// 更新昵称
+ Future updateNickname(newnickname) async {
+ final res = await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(nickName: newnickname));
+ if (res.success) {
+ nickname.value = newnickname;
+ } else {
+ logger.i(res.desc);
+ if (res.code == 80001) {
+ MyDialog.toast('昵称违规', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ }
+ }
+ return res.success;
+ }
+
+ /// 更新简介
+ Future updateSignature(newsignature) async {
+ final res = await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(selfSignature: newsignature));
+ if (res.success) {
+ signature.value = newsignature;
+ } else {
+ logger.i(res.desc);
+ if (res.code == 80001) {
+ MyDialog.toast('简介内容违规', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ }
+ }
+ return res.success;
+ }
+
+ /// 更新头像
+ Future updateFaceUrl() async {
+ if (faceUrl.value.trim().isEmpty) return;
+ await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(faceUrl: faceUrl.value));
+ }
+
+ /// 更新背景图
+ Future updateCover() async {
+ final coverBg = customInfo['coverBg'];
+ if (coverBg == null || coverBg.trim().isEmpty) return;
+ await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(customInfo: customInfo));
+ }
+
+ /// 更新openId
+ Future updateOpenId() async {
+ final openId = customInfo['openId'];
+ if (openId == null || openId.trim().isEmpty) return;
+ await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(customInfo: customInfo));
+ }
+ // customInfo.update("coverBg", (value) => coverBgUrl);
+
+ /// 更新所在地
+ Future updateArea() async {
+ final area = customInfo['area'];
+ if (area == null || area.trim().isEmpty) return;
+ final areaCode = customInfo['areaCode'];
+ if (areaCode == null || areaCode.trim().isEmpty) return;
+ await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(customInfo: customInfo));
+ }
+
+ ///更新生日
+ Future updateBirthday() async {
+ if (birthday.value < 0) return;
+ await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(birthday: birthday.value));
+ }
+
+ ///更新性别
+ Future updateGender() async {
+ if (gender.value < 0) return;
+ await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(gender: gender.value));
+ }
+
+ /// updateAvatar、updateSignature 等方法
+}
diff --git a/lib/IM/global_badge.dart b/lib/IM/global_badge.dart
index e677432..aef3fe0 100644
--- a/lib/IM/global_badge.dart
+++ b/lib/IM/global_badge.dart
@@ -1,9 +1,12 @@
import 'package:get/get.dart';
import 'package:loopin/IM/controller/chat_controller.dart';
import 'package:loopin/IM/controller/tab_bar_controller.dart';
-import 'package:loopin/IM/im_core.dart';
+import 'package:loopin/IM/im_service.dart';
+import 'package:loopin/models/conversation_type.dart';
import 'package:loopin/models/tab_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
class GlobalBadge extends GetxController {
@@ -21,14 +24,103 @@ class GlobalBadge extends GetxController {
logger.i('未读数发生变化$count');
totalUnread.value = count;
Get.find().setBadge(TabType.chat, totalUnread.value);
- // 更新会话列表
- Get.find().getConversationList();
+ },
+ onNewConversation: (List conversationList) {
+ for (var conv in conversationList) {
+ logger.i("新会话创建:${conv.conversationGroupList}");
+ handleCoverstion(conv);
+ }
+ },
+ onConversationChanged: (List conversationList) async {
+ logger.w('会话变更:会话分组:${conversationList.first.conversationGroupList},会话内容${conversationList.first.toLogString()}');
+ final ctl = Get.find();
+ final updatedIds = conversationList.map((e) => e.conversationID).toSet();
+ logger.w('要变更的会话id:$updatedIds');
+ for (int i = 0; i < ctl.chatList.length; i++) {
+ final chatItem = ctl.chatList[i];
+ logger.w('需要更新的ID:${chatItem.conversation.conversationID}');
+ if (updatedIds.contains(chatItem.conversation.conversationID)) {
+ final updatedConv = conversationList.firstWhere(
+ (c) => c.conversationID == chatItem.conversation.conversationID,
+ orElse: () => V2TimConversation(conversationID: ''),
+ );
+
+ if (updatedConv.conversationID != '' && (updatedConv.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false)) {
+ // 单独处理陌生人会话
+ final unread = await ImService.instance.getUnreadMessageCountByFilter(
+ filter: V2TimConversationFilter(
+ conversationGroup: ConversationType.noFriend.name,
+ hasUnreadCount: true,
+ ),
+ );
+ chatItem.conversation.lastMessage = updatedConv.lastMessage;
+ chatItem.conversation.unreadCount = unread.data; // 获取陌生人未读总数
+ } else {
+ // 其他类型统一更新处理
+ chatItem.conversation = updatedConv;
+ }
+ }
+ }
+ //重新排序
+ ctl.chatList.sort((a, b) {
+ final atime = a.conversation.lastMessage?.timestamp ?? 0;
+ final btime = b.conversation.lastMessage?.timestamp ?? 0;
+ return btime.compareTo(atime); // 降序
+ });
+ ctl.chatList.refresh();
},
);
+ final ctl = Get.find();
+ ctl.getConversationList();
_initUnreadCount();
_addListener();
}
+ // final rr = await ImService.instance.deleteConversationsFromGroup(
+ // conversationIDList: [cov.conversationID],
+ // groupName: 'noFriend',
+ // );
+ // logger.w(rr.desc);
+
+ /// 新建会话时候,根据消息的自定义属性给会话分组
+ void handleCoverstion(V2TimConversation cov) async {
+ final message = cov.lastMessage;
+ final isSelfSend = message!.isSelf; // 是否本人发送的消息
+ final typeEnum = conversationTypeFromString(message.cloudCustomData); // 会话类型
+ final needAdd = cov.conversationGroupList!.isEmpty == true; // 当前会话是否已加入了分组中
+ if (typeEnum != null && needAdd && isSelfSend == false) {
+ logger.i('当前会话的类型要加入的组是:$typeEnum');
+ // 当前会话需要进行分组,检测 组 是否存在
+ final hasGroupRes = await ImService.instance.getConversationGroupList();
+ if (hasGroupRes.success) {
+ final exists = hasGroupRes.data?.any((item) => item == typeEnum) ?? false;
+ if (!exists) {
+ // 组不存在,创建组并把会话加入group中
+ await ImService.instance.createConversationGroup(
+ groupName: typeEnum,
+ conversationIDList: ['c2c_${message.sender}'],
+ );
+ logger.i('首次创建会话分组$typeEnum');
+ } else {
+ // 分组存在直接添加
+ await ImService.instance.addConversationsToGroup(
+ groupName: typeEnum,
+ conversationIDList: ['c2c_${message.sender}'],
+ );
+ logger.i('添加会话分组$typeEnum成功');
+ }
+ if (typeEnum == ConversationType.noFriend.name) {
+ //陌生人分组特殊处理 满足分组条件且已经有分组,
+ final ctl = Get.find();
+ // 这个方法执行的逻辑:已有则刷新菜单入口数据,没有则创建菜单入口
+ ctl.getNoFriendData(csion: cov);
+ }
+ }
+ } else {
+ logger.w('不需要进行分组');
+ }
+ }
+
/// 初始化时获取一次未读总数
void _initUnreadCount() async {
final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getTotalUnreadMessageCount();
diff --git a/lib/IM/im_core.dart b/lib/IM/im_core.dart
index b0d121d..3f7ba92 100644
--- a/lib/IM/im_core.dart
+++ b/lib/IM/im_core.dart
@@ -14,14 +14,16 @@ class ImCore {
final res = await TencentImSDKPlugin.v2TIMManager.initSDK(
sdkAppID: sdkAppId,
- loglevel: LogLevelEnum.V2TIM_LOG_ALL,
+ loglevel: LogLevelEnum.V2TIM_LOG_ERROR,
listener: V2TimSDKListener(
- onConnectSuccess: () => logger.i("IM连接成功"),
+ onConnectSuccess: () {
+ logger.i("IM连接成功");
+ },
onConnectFailed: (code, error) => logger.e("IM连接失败: $code $error"),
onKickedOffline: () => logger.w("IM被踢下线"),
onUserSigExpired: () => logger.w("UserSig 过期"),
onSelfInfoUpdated: (V2TimUserFullInfo info) {
- logger.i("用户信息更新: ${info.nickName}");
+ logger.i("用户信息更新: ${info.toJson()}");
},
),
);
@@ -29,6 +31,7 @@ class ImCore {
if (res.code == 0) {
_isInitialized = true;
logger.i("IM SDK 初始化成功");
+
return true;
} else {
logger.e("IM SDK 初始化失败: ${res.code} - ${res.desc}");
diff --git a/lib/IM/im_friend_listeners.dart b/lib/IM/im_friend_listeners.dart
index 16ac3dd..58234f3 100644
--- a/lib/IM/im_friend_listeners.dart
+++ b/lib/IM/im_friend_listeners.dart
@@ -1,4 +1,5 @@
import 'package:logger/logger.dart';
+import 'package:loopin/utils/notification_banner.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimFriendshipListener.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_application.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart';
@@ -16,7 +17,6 @@ class ImFriendListeners {
_listener = V2TimFriendshipListener(onFriendApplicationListAdded: (List list) async {
//好友请求数量增加的回调
//applicationList 新增的好友请求信息列表
- logger.i('收到好友申请: ${list.map((e) => e.userID).join(",")}');
}, onFriendApplicationListRead: () async {
//好友请求已读的回调
}, onFriendApplicationListDeleted: (List userIDList) async {
@@ -25,11 +25,15 @@ class ImFriendListeners {
}, onFriendListAdded: (List users) async {
//好友列表增加人员的回调
//users 新增的好友信息列表
- logger.i('新增好友: ${users.map((u) => u.userID).join(",")}');
+ for (var item in users) {
+ logger.i('新增好友:${item.toLogString()}');
+ }
}, onFriendListDeleted: (List userList) async {
//好友列表减少人员的回调
//userList 减少的好友id列表
- logger.i('删除好友: ${userList.join(",")}');
+ for (var item in userList) {
+ logger.i('新增好友:$item');
+ }
}, onFriendInfoChanged: (List list) async {
//好友信息改变的回调
//infoList 好友信息改变的好友列表
@@ -43,14 +47,27 @@ class ImFriendListeners {
}, onMyFollowingListChanged: (List userInfoList, bool isAdd) async {
if (isAdd) {
// 关注列表新增用户的通知
+ for (var item in userInfoList) {
+ logger.i('我新关注的人:${item.toJson()}');
+ }
} else {
// 关注列表删除用户的通知
+ for (var item in userInfoList) {
+ logger.i('我取消关注了:${item.toJson()}');
+ }
}
}, onMyFollowersListChanged: (List userInfoList, bool isAdd) async {
if (isAdd) {
// 粉丝列表新增用户的通知
+ for (var item in userInfoList) {
+ logger.i('新增粉丝:${item.toJson()}');
+ }
+ NotificationBanner.foucs(userInfoList.last);
} else {
// 粉丝列表删除用户的通知
+ for (var item in userInfoList) {
+ logger.i('掉粉:${item.toJson()}');
+ }
}
}, onMutualFollowersListChanged: (List userInfoList, bool isAdd) async {
if (isAdd) {
diff --git a/lib/IM/im_message.dart b/lib/IM/im_message.dart
index 96a1337..45c8073 100644
--- a/lib/IM/im_message.dart
+++ b/lib/IM/im_message.dart
@@ -1,20 +1,27 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:get/get.dart';
import 'package:logger/logger.dart';
+import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/im_result.dart';
+import 'package:loopin/utils/parse_message_summary.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_priority_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/offlinePushInfo.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
-final logger = Logger();
-
class IMMessage {
- /// 发送文本消息
- Future sendText({
- required String text,
+ final logger = Logger();
+
+ /// 1.发送消息
+ Future sendMessage({
+ required V2TimMessage msg,
String? toUserID,
String? groupID,
- String? data,
+ String? cloudCustomData,
}) async {
// 必须且只能设置一个:toUserID(单聊)或 groupID(群聊)
if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
@@ -24,40 +31,44 @@ class IMMessage {
desc: "只能指定一个 receiver(toUserID)或 groupID",
);
}
-
- // 创建消息
- final createRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createTextMessage(text: text);
-
- if (createRes.code != 0 || createRes.data == null) {
- return ImResult(
- success: false,
- code: createRes.code,
- desc: "创建消息失败",
- );
+ if (cloudCustomData != null) {
+ msg.cloudCustomData = cloudCustomData;
}
- final V2TimMessage? messageInfo = createRes.data?.messageInfo;
-
+ // 解析消息类型
V2TimValueCallback sendRes;
-
+ // final controller = Get.find();
+ final myInfo = Get.find();
+ logger.w('启用默认title:${myInfo.nickname.value}');
// 单聊
if (toUserID != null) {
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
- message: messageInfo,
+ message: msg,
receiver: toUserID,
+ // onSyncMsgID: (msgID) async {
+ // 这里立刻拿到消息ID,可以提前把这条消息展示到列表中(发送中状态)有时间再改吧
+ // 根据类型,创建对应的elem;
+ // logger.w(msg.imageElem!.toLogString());
+ // controller.chatList.add(msg.imageElem);
+ // controller.scrollToBottom();
+ // },
groupID: "",
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
onlineUserOnly: false,
isExcludedFromUnreadCount: false,
isExcludedFromLastMessage: false,
needReadReceipt: false,
- offlinePushInfo: OfflinePushInfo(title: "新消息", desc: text),
- cloudCustomData: "",
+ offlinePushInfo: OfflinePushInfo(
+ title: myInfo.nickname.value,
+ desc: parseMessageSummary(msg),
+ ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}),
+ ),
+ cloudCustomData: cloudCustomData,
localCustomData: "",
);
} else {
// 群聊
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
- message: messageInfo,
+ message: msg,
receiver: "",
groupID: groupID!,
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
@@ -65,7 +76,7 @@ class IMMessage {
isExcludedFromUnreadCount: false,
isExcludedFromLastMessage: false,
needReadReceipt: false,
- offlinePushInfo: OfflinePushInfo(title: "新群聊消息", desc: text),
+ offlinePushInfo: OfflinePushInfo(title: '群聊消息', desc: parseMessageSummary(msg)),
cloudCustomData: "",
localCustomData: "",
);
@@ -79,62 +90,18 @@ class IMMessage {
);
}
- /// 发送自定义消息
- Future sendCustomMessage({
+ /// 2=创建自定义消息
+ Future> createCustomMessage({
required String data,
- String? toUserID,
- String? groupID,
- String? description,
- String? extension,
+ String desc = "",
+ String extension = "",
}) async {
- // 校验逻辑:单聊或群聊,二选一
- if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
- return ImResult(
- success: false,
- code: -1,
- desc: "只能指定一个 receiver(toUserID)或 groupID",
- );
- }
-
- // 1. 创建自定义消息
- final createRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createCustomMessage(
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createCustomMessage(
data: data,
- desc: description ?? '',
- extension: extension ?? '',
+ desc: desc,
+ extension: extension,
);
-
- if (createRes.code != 0 || createRes.data?.id == null) {
- return ImResult(
- success: false,
- code: createRes.code,
- desc: "创建自定义消息失败",
- );
- }
- final V2TimMessage? messageInfo = createRes.data?.messageInfo;
-
- // 2. 发送消息
- final sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
- message: messageInfo,
- receiver: toUserID ?? '',
- groupID: groupID ?? '',
- priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
- onlineUserOnly: false,
- isExcludedFromUnreadCount: false,
- isExcludedFromLastMessage: false,
- needReadReceipt: false,
- offlinePushInfo: OfflinePushInfo(
- title: "自定义消息",
- desc: description ?? '您收到一条自定义消息',
- ),
- cloudCustomData: "",
- localCustomData: "",
- );
-
- return ImResult(
- success: sendRes.code == 0,
- code: sendRes.code,
- desc: sendRes.desc,
- );
+ return ImResult.wrap(res);
}
/// 构造单聊伪消息
@@ -162,24 +129,124 @@ class IMMessage {
desc: "success",
data: timeMsg,
);
+ }
- // final sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
- // message: timeMsg,
- // receiver: userId,
- // groupID: "",
- // onlineUserOnly: false,
- // isExcludedFromUnreadCount: true,
- // isExcludedFromLastMessage: true,
- // needReadReceipt: false,
- // cloudCustomData: "",
- // localCustomData: "time_label",
- // );
+ /// 创建文本消息==1
+ Future> createTextMessage({
+ required String text,
+ }) async {
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createTextMessage(text: text);
+ return ImResult.wrap(res);
+ }
- // return ImResult(
- // success: sendRes.code == 0,
- // code: sendRes.code,
- // desc: sendRes.code == 0 ? "时间标签发送成功" : sendRes.desc,
- // data: timeMsg,
- // );
+ /// 创建图片消息==3
+ Future> createImageMessage({
+ required String imagePath,
+ String? imageName,
+ }) async {
+ final fileExists = await File(imagePath).exists();
+ if (fileExists) {
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createImageMessage(
+ imagePath: imagePath,
+ imageName: imageName,
+ );
+ return ImResult.wrap(res);
+ } else {
+ // 构造失败的回调
+ final failed = V2TimValueCallback.fromJson({
+ "code": -5,
+ "desc": "imagePath is not found",
+ "data": V2TimMsgCreateInfoResult.fromJson({}),
+ });
+
+ return ImResult.wrap(failed);
+ }
+ }
+
+ ///创建视频消息==5
+ Future> createVideoMessage({
+ //最大100MB
+ required String videoFilePath, //视频地址
+ required String type, // 类型mp4/avi==
+ required int duration, // 时长
+ required String snapshotPath, // 封面图
+ }) async {
+ final videoExists = await File(videoFilePath).exists();
+ final snapshotExists = await File(snapshotPath).exists();
+
+ if (videoExists && snapshotExists) {
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createVideoMessage(
+ videoFilePath: videoFilePath,
+ type: type,
+ duration: duration,
+ snapshotPath: snapshotPath,
+ );
+
+ return ImResult.wrap(res);
+ }
+
+ // 构造失败回调
+ final failed = V2TimValueCallback.fromJson({
+ "code": -5,
+ "desc": "视频或首帧图缺失",
+ "data": V2TimMsgCreateInfoResult.fromJson({}),
+ });
+
+ return ImResult.wrap(failed);
+ }
+
+ /// 语音消息==4
+ Future> createSoundMessage({
+ required String soundPath,
+ required int duration,
+ String? path,
+ }) async {
+ final soundExists = await File(soundPath).exists();
+
+ if (soundExists) {
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createSoundMessage(
+ soundPath: soundPath,
+ duration: duration,
+ );
+
+ return ImResult.wrap(res);
+ }
+
+ final failed = V2TimValueCallback.fromJson({
+ "code": -5,
+ "desc": "音频文件缺失",
+ "data": V2TimMsgCreateInfoResult.fromJson({}),
+ });
+
+ return ImResult.wrap(failed);
+ }
+
+ /// 表情 == 8
+ Future> createFaceMessage({
+ required int index,
+ required String data,
+ }) async {
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createFaceMessage(
+ index: index,
+ data: data,
+ );
+
+ return ImResult.wrap(res);
+ }
+
+ ///相当于一个被禁用了网络发送能力的 sendMessage() 接口
+ Future> insertC2CMessageToLocalStorageV2({
+ required String userID,
+ required String senderID,
+ V2TimMessage? message,
+ String? createdMsgID,
+ }) async {
+ final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().insertC2CMessageToLocalStorageV2(
+ userID: userID,
+ senderID: senderID,
+ message: message,
+ createdMsgID: createdMsgID,
+ );
+ return ImResult.wrap(res);
}
}
diff --git a/lib/IM/im_message_listeners.dart b/lib/IM/im_message_listeners.dart
index a3d4241..2d85f31 100644
--- a/lib/IM/im_message_listeners.dart
+++ b/lib/IM/im_message_listeners.dart
@@ -8,6 +8,7 @@ import 'package:loopin/IM/im_service.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/lifecycle_handler.dart';
import 'package:loopin/utils/notification_banner.dart';
+import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
@@ -15,14 +16,18 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
class ImMessageListenerService extends GetxService {
+ ProgressController? _sendProgressController; // 消息发送提示
+
final logger = Logger();
V2TimAdvancedMsgListener? _listener;
Timer? _debounceTimer;
+ /// 插入标签时间间隔
bool needInsertTimeLabel(int lastTimestamp, int newTimestamp, {int interval = 3 * 60}) {
return (newTimestamp - lastTimestamp) > interval * 1000;
}
+ ///插入标签
void insertTimeLabel(message) async {
// 待插入的消息
List messagesToInsert = [];
@@ -54,21 +59,26 @@ class ImMessageListenerService extends GetxService {
messagesToInsert.add(resMsg.data);
}
messagesToInsert.insert(0, message);
+ // messagesToInsert.add(message);
+ // 新进入的消息插入列表
chatDetailController.chatList.insertAll(0, messagesToInsert);
+ // chatDetailController.chatList.addAll(messagesToInsert);
+ // 滚动
+ chatDetailController.scrollToBottom();
}
+ /// 处理消息
void _handleNewMessage(V2TimMessage message) async {
final userID = message.sender ?? '';
if (userID.isEmpty) return;
- // 是否正在聊天 优先处理
- if (Get.currentRoute == '/chat' && Get.isRegistered()) {
+ /// 是否正在聊天 优先处理
+ if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') && Get.isRegistered()) {
final chatDetailController = Get.find();
// 单聊的处理
if (chatDetailController.userID == userID) {
- // 确认正在聊天
- // 插入消息前检测是否需要打时间标签
+ // 确认正在聊天,插入消息前检测是否需要打时间标签
insertTimeLabel(message);
// 标注为已读
await ImService.instance.clearConversationUnreadCount(conversationID: 'c2c_$userID');
@@ -142,9 +152,29 @@ class ImMessageListenerService extends GetxService {
},
onRecvMessageModified: (V2TimMessage message) {
logger.i("消息被修改: ${message.msgID}");
+ // 目前就红包领取状态的变更
+ if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') &&
+ Get.isRegistered()) {
+ final controller = Get.find();
+ final index = controller.chatList.indexWhere((m) => m.msgID == message.msgID);
+ if (index != -1) {
+ final newJson = message.customElem!.data!;
+ controller.chatList[index].customElem!.data = newJson;
+ controller.chatList.refresh();
+ }
+ }
},
onSendMessageProgress: (V2TimMessage message, int progress) {
logger.i("发送中: ${message.msgID} -> $progress%");
+ if (progress < 100) {
+ _sendProgressController ??= MyDialog.loading(
+ "发送中...",
+ duration: null, // 不自动关闭
+ );
+ } else {
+ _sendProgressController?.close();
+ _sendProgressController = null;
+ }
},
onRecvC2CReadReceipt: (List receiptList) {
for (var receipt in receiptList) {
@@ -159,7 +189,6 @@ class ImMessageListenerService extends GetxService {
);
TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener(listener: _listener!);
- logger.i("$_listener");
logger.i("高级消息监听器已注册");
return this;
diff --git a/lib/IM/im_result.dart b/lib/IM/im_result.dart
index 73623a3..f4121d5 100644
--- a/lib/IM/im_result.dart
+++ b/lib/IM/im_result.dart
@@ -1,3 +1,6 @@
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
+
class ImResult {
final bool success;
final int code;
@@ -10,4 +13,21 @@ class ImResult {
required this.desc,
this.data,
});
+ static ImResult wrap(V2TimValueCallback res) {
+ return ImResult(
+ success: res.code == 0,
+ code: res.code,
+ desc: res.desc,
+ data: res.data,
+ );
+ }
+
+ static ImResult wrapNoData(V2TimCallback res) {
+ return ImResult(
+ success: res.code == 0,
+ code: res.code,
+ desc: res.desc,
+ data: null,
+ );
+ }
}
diff --git a/lib/IM/im_service.dart b/lib/IM/im_service.dart
index 317831d..b401f5d 100644
--- a/lib/IM/im_service.dart
+++ b/lib/IM/im_service.dart
@@ -1,19 +1,40 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
+import 'package:loopin/IM/controller/chat_controller.dart';
+import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/controller/tab_bar_controller.dart';
import 'package:loopin/IM/global_badge.dart';
import 'package:loopin/IM/im_core.dart';
import 'package:loopin/IM/im_friend_listeners.dart';
import 'package:loopin/IM/im_message_listeners.dart';
import 'package:loopin/IM/im_result.dart';
+import 'package:loopin/IM/push_service.dart';
import 'package:loopin/models/conversation_view_model.dart';
+import 'package:loopin/utils/wxsdk.dart';
+import 'package:tencent_cloud_chat_sdk/enum/friend_application_type_enum.dart';
+import 'package:tencent_cloud_chat_sdk/enum/friend_response_type_enum.dart';
+import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_operation_result.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_info.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_operation_result.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info_result.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_operation_result.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_info_result.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_conversation_manager.dart';
+import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_friendship_manager.dart';
+import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_message_manager.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
final logger = Logger();
@@ -40,6 +61,16 @@ class ImService {
if (result.success) {
logger.i("IM 登录成功:$userID");
+ // 初始化push服务
+ PushService().initPush(
+ sdkAppId: 1600080789,
+ appKey: 'vkFpe55aYqfV7Sk5uGaoxhEstJ3tcI9dquk7JwG1GloDSLD2HeMWeQweWWXgNlhC',
+ );
+ // 初始化微信 SDK
+ await Wxsdk.init();
+
+ // 注册用户信息(基本信息+自定义信息)
+ Get.put(ImUserInfoController(), permanent: true);
// 登录成功后注册高级消息监听器
final messageService = ImMessageListenerService();
Get.put(messageService, permanent: true);
@@ -48,13 +79,11 @@ class ImService {
// 注册关系链监听器
final friendListener = ImFriendListeners();
- logger.i(friendListener);
Get.put(friendListener, permanent: true);
friendListener.register();
/// 注册消息未读数监听器
Get.put(GlobalBadge(), permanent: true);
- // Get.lazyPut(() => GlobalBadge());
} else {
logger.i("IM 登录失败:${result.code} - ${result.desc}");
Get.snackbar(
@@ -72,6 +101,9 @@ class ImService {
Future logout() async {
final res = await TencentImSDKPlugin.v2TIMManager.logout();
if (res.code == 0) {
+ /// 清理用户信息
+ Get.delete(force: true);
+
/// 移出消息监听器
Get.find().onClose();
Get.delete(force: true);
@@ -83,25 +115,114 @@ class ImService {
/// 清理tabbar
Get.find().badgeMap.clear();
+ /// 清理会话列表数据
+ Get.find().initChatData();
+
/// 移出未读消息监听器
Get.find().onClose();
Get.delete(force: true);
+ /// 移除推送服务
+ PushService.unInitPush();
+
/// 反初始化
ImCore.unInit();
}
- return ImResult(
- success: res.code == 0,
- code: res.code,
- desc: res.desc,
+ return ImResult.wrapNoData(res);
+ }
+
+ /// 设置会话自定义属性
+ Future>> setConversationCustomData({
+ required String customData,
+ required List conversationIDList,
+ }) async {
+ final res = await TIMConversationManager.instance.setConversationCustomData(customData: customData, conversationIDList: conversationIDList);
+
+ return ImResult.wrap(res);
+ }
+
+ /// 获取符合过滤条件的未读消息总数
+ Future> getUnreadMessageCountByFilter({
+ required V2TimConversationFilter filter,
+ }) async {
+ final res = await TIMConversationManager.instance.getUnreadMessageCountByFilter(
+ filter: filter,
);
+ return ImResult.wrap(res);
+ }
+
+ /// 删除会话
+ Future> deleteConversation({
+ required String conversationID,
+ }) async {
+ final res = await TIMConversationManager.instance.deleteConversation(conversationID: conversationID);
+
+ return ImResult.wrapNoData(res);
+ }
+
+ /// 创建会话分组
+ Future>> createConversationGroup({
+ required String groupName,
+ required List conversationIDList,
+ }) async {
+ final res = await TIMConversationManager.instance.createConversationGroup(
+ groupName: groupName,
+ conversationIDList: conversationIDList,
+ );
+
+ return ImResult.wrap(res);
+ }
+
+ /// 获取会话分组列表
+ Future>> getConversationGroupList() async {
+ final res = await TIMConversationManager.instance.getConversationGroupList();
+ return ImResult.wrap(res);
+ }
+
+ /// 将会话添加到分组
+ Future>> addConversationsToGroup({
+ required String groupName,
+ required List conversationIDList,
+ }) async {
+ final res = await TIMConversationManager.instance.addConversationsToGroup(
+ groupName: groupName,
+ conversationIDList: conversationIDList,
+ );
+ return ImResult.wrap(res);
+ }
+
+ ///将会话移除分组
+ Future>> deleteConversationsFromGroup({
+ required String groupName,
+ required List conversationIDList,
+ }) async {
+ final res = await TIMConversationManager.instance.deleteConversationsFromGroup(
+ groupName: groupName,
+ conversationIDList: conversationIDList,
+ );
+ return ImResult.wrap(res);
+ }
+
+ /// 高级查询会话列表
+ Future> getConversationListByFilter({
+ required V2TimConversationFilter filter,
+ required int nextSeq,
+ int count = 20,
+ }) async {
+ final res = await TIMConversationManager.instance.getConversationListByFilter(
+ filter: filter,
+ nextSeq: nextSeq,
+ count: count,
+ );
+
+ return ImResult.wrap(res);
}
/// 查询会话记录
Future getConversationList(String nextSeq, int count) async {
- final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count);
-
- if (res.code != 0) {
+ // final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count);
+ final res = await getConvData(nextSeq, count);
+ if (res.success == false) {
return ImResult(
success: false,
code: res.code,
@@ -138,7 +259,7 @@ class ImService {
// 读取管理员标识
final customInfo = user.customInfo;
if (customInfo != null) {
- isCustomAdmin = customInfo['Tag_Profile_Custom_admin'] ?? '0';
+ isCustomAdmin = customInfo['admin'] ?? '0';
}
}
}
@@ -171,6 +292,40 @@ class ImService {
return ConversationViewModel(conversation: conv, faceUrl: faceUrl, isCustomAdmin: isCustomAdmin);
}).toList();
+ // 筛选数据,过滤掉陌生人消息
+ viewList.removeWhere((conv) {
+ final special = conv.conversation.conversationGroupList ?? [];
+ return special.contains('noFriend');
+ });
+
+ ChatController chatcontroller = Get.find();
+ logger.e('新的分页内容:${res.data!.toLogString()},控制器中的:${chatcontroller.nextSeq.value}');
+ String newNextSeq = res.data?.nextSeq ?? '0';
+ bool isEnd = res.data?.isFinished ?? true;
+
+ if (isEnd) {
+ //没数据了,关闭拉取;
+ chatcontroller.isFinished.value = isEnd;
+ } else {
+ // 没拉完,记录游标
+ chatcontroller.nextSeq.value = newNextSeq;
+ }
+
+ // 更新分页
+ chatcontroller.nextSeq.value = res.data!.nextSeq!;
+ if (res.data!.isFinished == false) {
+ if (viewList.length < 20) {
+ // 递归补偿拉取
+ final nextRes = await getConversationList(
+ res.data!.nextSeq!,
+ count,
+ );
+ if (nextRes.success && nextRes.data != null) {
+ viewList.addAll(nextRes.data as List);
+ }
+ }
+ }
+
return ImResult(
success: res.code == 0,
code: res.code,
@@ -179,30 +334,13 @@ class ImService {
);
}
- /// 获取自己的userId
- Future selfUserId() async {
- V2TimValueCallback self = await TencentImSDKPlugin.v2TIMManager.getLoginUser();
- String? userId = self.data;
- return ImResult(
- success: self.code == 0,
- code: self.code,
- desc: self.desc,
- data: userId,
- );
- }
-
- /// 查询当前登录用户的个人信息
- Future selfInfo() async {
- // 获取当前登录的用户 ID
- final idRes = await selfUserId();
- // 获取用户信息
- V2TimValueCallback> res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [idRes.data]);
- return ImResult(
- success: res.code == 0,
- code: res.code,
- desc: res.desc,
- data: res.data?.isNotEmpty == true ? res.data![0] : null,
- );
+ ///获取所有会话数据
+ Future> getConvData(String nextSeq, int count) async {
+ final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count);
+ // for (var element in res.data!.conversationList) {
+ // logger.e('所有的会话数据:${element.toJson()}');
+ // }
+ return ImResult.wrap(res);
}
///获取指定会话
@@ -219,13 +357,29 @@ class ImService {
);
}
+ /// 获取消息
+ Future>> findMessages({
+ required List messageIDList,
+ }) async {
+ final res = await TIMMessageManager.instance.findMessages(messageIDList: messageIDList);
+ return ImResult.wrap(res);
+ }
+
+ /// 修改消息
+ Future> modifyMessage({
+ required V2TimMessage message,
+ }) async {
+ final res = await TIMMessageManager.instance.modifyMessage(message: message);
+ return ImResult.wrap(res);
+ }
+
/// 获取聊天记录 如果是群聊传 groupID,单聊传 userID,二选一
Future>> getHistoryMessageList({
HistoryMsgGetTypeEnum getType = HistoryMsgGetTypeEnum.V2TIM_GET_LOCAL_OLDER_MSG,
String? userID,
String? groupID,
int? lastMsgSeq,
- int count = 20,
+ int count = 10,
V2TimMessage? lastMsg,
List? messageTypeList,
List? messageSeqList,
@@ -298,10 +452,223 @@ class ImService {
cleanSequence: cleanSequence, // 群聊生效
);
+ return ImResult.wrapNoData(res);
+ }
+
+ /// 获取自己的userId
+ Future selfUserId() async {
+ V2TimValueCallback self = await TencentImSDKPlugin.v2TIMManager.getLoginUser();
+ String? userId = self.data;
+ return ImResult(
+ success: self.code == 0,
+ code: self.code,
+ desc: self.desc,
+ data: userId,
+ );
+ }
+
+ /// 查询当前登录用户的个人信息
+ Future selfInfo() async {
+ // 获取当前登录的用户 ID
+ final idRes = await selfUserId();
+ // 获取用户信息
+ V2TimValueCallback> res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [idRes.data]);
return ImResult(
success: res.code == 0,
code: res.code,
desc: res.desc,
+ data: res.data?.isNotEmpty == true ? res.data!.first : null,
);
}
+
+ /// 查询其他人的信息
+ Future otherInfo(id) async {
+ // 获取用户信息
+ V2TimValueCallback> res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [id]);
+ return ImResult(
+ success: res.code == 0,
+ code: res.code,
+ desc: res.desc,
+ data: res.data?.isNotEmpty == true ? res.data!.first : null,
+ );
+ }
+
+ /// 设置个人资料
+ Future setSelfInfo({
+ required V2TimUserFullInfo userFullInfo,
+ }) async {
+ final res = await TencentImSDKPlugin.v2TIMManager.setSelfInfo(
+ userFullInfo: userFullInfo,
+ );
+ return ImResult.wrapNoData(res);
+ }
+
+ /// 检查是否是好友(双向或单向)
+ Future isMyFriend(String userID, FriendTypeEnum checkType) async {
+ final res = await TIMFriendshipManager.instance.checkFriend(
+ userIDList: [userID],
+ checkType: checkType, //V2TIM_FRIEND_TYPE_BOTH V2TIM_FRIEND_TYPE_SINGLE
+ );
+
+ if (res.code == 0 && res.data != null && res.data!.isNotEmpty) {
+ final resultType = res.data!.first.resultType;
+ final isFriend = resultType == 3; //0=无, 1=单向, 2=我在对方列表,3=双向
+ return ImResult(
+ success: true,
+ desc: res.desc,
+ code: res.code,
+ data: isFriend,
+ );
+ } else {
+ return ImResult(
+ success: false,
+ code: res.code,
+ desc: res.desc,
+ data: false,
+ );
+ }
+ }
+
+ /// 添加好友
+ Future> addFriend({
+ required String userID,
+ String? remark,
+ String? friendGroup,
+ String? addWording,
+ String? addSource,
+ required FriendTypeEnum addType,
+ }) async {
+ final res = await TIMFriendshipManager.instance.addFriend(
+ userID: userID,
+ remark: remark,
+ friendGroup: friendGroup,
+ addWording: addWording,
+ addSource: addSource,
+ addType: addType,
+ );
+ return ImResult.wrap(res);
+ }
+
+ ///接受好友申请
+ Future> acceptFriendApplication({
+ required FriendResponseTypeEnum responseType,
+ required FriendApplicationTypeEnum type, // V2TIM_FRIEND_ACCEPT_AGREE,同意添加单向好友;V2TIM_FRIEND_ACCEPT_AGREE_AND_ADD,同意并添加为双向好友
+ required String userID,
+ }) async {
+ final res = await TIMFriendshipManager.instance.acceptFriendApplication(
+ responseType: responseType,
+ type: type,
+ userID: userID,
+ );
+ return ImResult.wrap(res);
+ }
+
+ /// 拉黑
+ Future>> addToBlackList({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.addToBlackList(userIDList: userIDList);
+ return ImResult.wrap(res);
+ }
+
+ /// 取消拉黑
+ Future>> deleteFromBlackList({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.deleteFromBlackList(userIDList: userIDList);
+ return ImResult.wrap(res);
+ }
+
+ ///获取好友列表
+ Future>> getFriendList() async {
+ final res = await TIMFriendshipManager.instance.getFriendList();
+ return ImResult.wrap(res);
+ }
+
+ /// set好友备注
+ Future setFriendInfo({
+ required String userID,
+ String? friendRemark,
+ Map? friendCustomInfo,
+ }) async {
+ late V2TimCallback res;
+ res = await TIMFriendshipManager.instance.setFriendInfo(
+ userID: userID,
+ friendRemark: friendRemark,
+ friendCustomInfo: friendCustomInfo,
+ );
+ return ImResult.wrapNoData(res);
+ }
+
+ /// 获取好友信息
+ Future>> getFriendInfo({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.getFriendsInfo(userIDList: userIDList);
+ return ImResult.wrap(res);
+ }
+
+ ///关注
+ Future>> followUser({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.followUser(
+ userIDList: userIDList,
+ );
+ return ImResult.wrap(res);
+ }
+
+ ///取关
+ Future>> unfollowUser({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.unfollowUser(
+ userIDList: userIDList,
+ );
+ return ImResult.wrap(res);
+ }
+
+ /// check关注的类型
+ /// 0:不是好友也没有关注
+ /// 1:你关注了对方(单向)
+ /// 2:对方关注了你(单向)
+ /// 3:互相关注(双向好友)
+ Future>> checkFollowType({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.checkFollowType(
+ userIDList: userIDList,
+ );
+ return ImResult.wrap(res);
+ }
+
+ ///获取指定用户的 关注/粉丝/互关 数量信息
+ Future>> getUserFollowInfo({
+ required List userIDList,
+ }) async {
+ final res = await TIMFriendshipManager.instance.getUserFollowInfo(userIDList: userIDList);
+ return ImResult.wrap(res);
+ }
+
+ /// 获取双向关注列表(互关好友)
+ /// [nextCursor] 分页游标,首次传空字符串
+ Future> getMutualFollowersList({
+ required String nextCursor,
+ }) async {
+ final res = await TIMFriendshipManager.instance.getMutualFollowersList(
+ nextCursor: nextCursor,
+ );
+ return ImResult.wrap(res);
+ }
+
+ /// 获取我的粉丝列表
+ /// [nextCursor] 分页游标,首次传空字符串
+ Future> getMyFollowersList({
+ required String nextCursor,
+ }) async {
+ final res = await TIMFriendshipManager.instance.getMyFollowersList(
+ nextCursor: nextCursor,
+ );
+ return ImResult.wrap(res);
+ }
}
diff --git a/lib/IM/push_service.dart b/lib/IM/push_service.dart
new file mode 100644
index 0000000..02b839a
--- /dev/null
+++ b/lib/IM/push_service.dart
@@ -0,0 +1,208 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:get/get.dart';
+import 'package:logger/logger.dart';
+import 'package:loopin/IM/im_service.dart';
+import 'package:loopin/models/conversation_type.dart';
+import 'package:loopin/utils/storage.dart';
+import 'package:tencent_cloud_chat_push/common/tim_push_listener.dart';
+import 'package:tencent_cloud_chat_push/common/tim_push_message.dart';
+import 'package:tencent_cloud_chat_push/tencent_cloud_chat_push.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
+import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
+
+final logger = Logger();
+
+class PushService {
+ static late TIMPushListener _timPushListener;
+
+ Future _registerPushInIsolate(Map args) async {
+ final sdkAppId = args['sdkAppId'] as int;
+ final appKey = args['appKey'] as String;
+ final apnsCertificateID = args['apnsCertificateID'] as int;
+
+ try {
+ await TencentCloudChatPush().registerPush(
+ sdkAppId: sdkAppId,
+ appKey: appKey,
+ apnsCertificateID: apnsCertificateID,
+ onNotificationClicked: _onNotificationClicked,
+ );
+ } catch (e) {
+ logger.e('注册推送失败: $e');
+ }
+ }
+
+ /// 初始化推送服务
+ Future initPush({
+ required int sdkAppId,
+ required String appKey, // 客户端密钥
+ }) async {
+ int apnsCertificateID;
+ final devices = await _getDeviceBrand();
+ apnsCertificateID = _getApnsCertificateIDForBrand(devices);
+ if (apnsCertificateID == 0) {
+ logger.w('手机厂商:$devices, 未配置证书');
+ }
+
+ // 注册推送(初始化)
+ // if (Platform.isAndroid) {
+ // await compute(_registerPushInIsolate, {
+ // 'sdkAppId': sdkAppId,
+ // 'appKey': appKey,
+ // 'apnsCertificateID': apnsCertificateID,
+ // });
+ // } else {
+ await TencentCloudChatPush().registerPush(
+ onNotificationClicked: _onNotificationClicked,
+ sdkAppId: sdkAppId,
+ appKey: appKey,
+ apnsCertificateID: apnsCertificateID,
+ );
+ // }
+
+ // 关闭 App 在前台时弹出通知栏
+ await TencentCloudChatPush().disablePostNotificationInForeground(disable: true);
+
+ ///处理安卓端异常问题;
+ if (Platform.isAndroid) {
+ await TencentImSDKPlugin.v2TIMManager.login(userID: Storage.read('userId'), userSig: Storage.read('userSig'));
+ }
+
+ logger.i('推送服务已注册,手机:$devices,证书ID:$apnsCertificateID');
+
+ // 添加在线时监听器
+ _addPushListener();
+ }
+
+ /// 注销推送(退出登录时调用)
+ static Future unInitPush() async {
+ try {
+ await TencentCloudChatPush().unRegisterPush();
+ _removePushListener();
+ } catch (e) {
+ logger.i("注销推送失败: $e");
+ }
+ }
+
+ /// 添加监听器
+ static void _addPushListener() {
+ _timPushListener = TIMPushListener(
+ onRecvPushMessage: (TimPushMessage message) {
+ logger.i("[推送] 收到消息: ${message.toLogString()}");
+ },
+ onRevokePushMessage: (String messageId) {
+ logger.i("[推送] 消息被撤回: $messageId");
+ },
+ onNotificationClicked: (String ext) {
+ logger.i("[推送] 点击横幅 ext: $ext");
+ _handleNotificationClick(ext);
+ },
+ );
+ TencentCloudChatPush().addPushListener(listener: _timPushListener);
+ logger.i('推送服务在线监听器已注册');
+ }
+
+ /// 移除监听器
+ static void _removePushListener() {
+ TencentCloudChatPush().removePushListener(listener: _timPushListener);
+ }
+
+ /// 横幅点击事件处理
+ static void _onNotificationClicked({
+ required String ext,
+ String? userID,
+ String? groupID,
+ }) {
+ logger.i("[点击通知回调] ext: $ext, userID: $userID, groupID: $groupID");
+ _handleNotificationClick(ext, userID: userID, groupID: groupID);
+ }
+
+ /// 统一处理跳转逻辑
+ static void _handleNotificationClick(String ext, {String? userID, String? groupID}) async {
+ try {
+ // ext={id:对应业务ID,type:'newFoucs',userID:发送人的id,groupID:群ID}
+ // final ext = jsonEncode({
+ // "userID": "123456",
+ // "groupID": "654321",
+ // });
+ final data = jsonDecode(ext);
+ logger.i(data);
+ final type = data['type'];
+ final router = conversationTypeFromString(type);
+ logger.w(router);
+ if (router == null || router != '') {
+ // 聊天
+ if (data['userID'] != null) {
+ logger.w('有userID');
+ // 单聊,获取会话
+ final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}');
+ final V2TimConversation conversation = covRes.data;
+ logger.w(conversation.toJson());
+ if (conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false) {
+ // nofriend会话,是否第一次聊天
+ conversation.showName = conversation.showName ?? data['title'];
+ Get.toNamed('/chatNoFriend', arguments: conversation);
+ } else {
+ // 去正常的会话
+ Get.toNamed('/chat', arguments: conversation);
+ }
+ } else {
+ logger.w('没有userID');
+
+ // 群聊消息
+ final groupRes = await ImService.instance.getConversation(conversationID: 'group_${data['groupID']}');
+ Get.toNamed('/chatGroup', arguments: groupRes.data);
+ }
+ } else {
+ // 通知类相关
+ Get.toNamed('/$router', arguments: data['id'] ?? '');
+ }
+ } catch (e) {
+ logger.i("[推送点击] ext 解析失败: $e");
+ }
+ }
+
+ /// 获取手机品牌
+ static Future _getDeviceBrand() async {
+ final deviceInfo = DeviceInfoPlugin();
+ try {
+ if (Platform.isAndroid) {
+ final androidInfo = await deviceInfo.androidInfo;
+ return androidInfo.brand.toLowerCase();
+ } else if (Platform.isIOS) {
+ return 'apple';
+ } else {
+ return 'unknown';
+ }
+ } catch (e, stack) {
+ logger.w("获取设备品牌失败: $e\n$stack");
+ return 'unknown';
+ }
+ }
+
+ /// 获取对应厂商的证书ID
+ static int _getApnsCertificateIDForBrand(String brand) {
+ switch (brand) {
+ case 'xiaomi':
+ case 'redmi':
+ return 41169;
+ case 'oppo':
+ return 41170;
+ case 'vivo':
+ return 41177;
+ case 'meizu':
+ return 41176;
+ case 'apple':
+ return 45356;
+ case 'huawei':
+ return 41171;
+ case 'honor':
+ return 41178;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/lib/api/common_api.dart b/lib/api/common_api.dart
index 7fdd2b8..37dce77 100644
--- a/lib/api/common_api.dart
+++ b/lib/api/common_api.dart
@@ -1,7 +1,15 @@
class CommonApi {
- static const String checkVersion = '/check/version'; // 查询版本
- static const String getCode = '/resource/sms/code'; // 发送短信验证码
- static const String login = '/auth/login'; // 登录
- static const String uploadFile = '/upload'; // 上传文件
- static const String accountInfo = '/ums/member/account/'; // 账户信息
+ ///----------get
+ static const String getCode = '/resource/sms/code'; // 发送短信验证码 {'phonenumber'}
+ static const String accountInfo = '/app/member/info'; // 账户信息
+
+ ///---------post
+ static const String login = '/auth/login'; // 登录 {'phonenumber': '', 'smsCode': '', 'clientId': '428a8310cd442757ae699df5d894f051', 'grantType': 'sms'};
+ static const String checkVersion = '/system/version/list'; // 查询app版本 {'platformType': Platform.isAndroid ? 'android' : 'ios','status': 1}
+ static const String uploadFile = '/resource/oss/upload';
+
+ ///[source]=wechat_open [clientId]=428a8310cd442757ae699df5d894f051 [grantType]=social [socialState]=1
+ static const String wxLogin = '/app/member/bind/wechat';
+
+ ///resource/oss/upload
}
diff --git a/lib/api/shop_api.dart b/lib/api/shop_api.dart
new file mode 100644
index 0000000..f984fd5
--- /dev/null
+++ b/lib/api/shop_api.dart
@@ -0,0 +1,17 @@
+class ShopApi {
+ ///---------------------post
+ /// [size]分页数量
+ /// [current] 第几页
+ /// [categoryId] 分类id
+ /// [nameLike] 商品名称
+ static const String shopList = '/app/product/page'; // 商品列表
+
+ /// [showStatus]1=显示 [nameLike]分类名称
+ static const String shopCategory = '/app/productCategory/page'; // 商品分类
+ /// []
+ static const String shopSwiperList = '/app/article/carousel'; // 商品首页轮播图
+
+ ///---------------------get
+ /// [url参数/id]
+ static const String shopDetail = '/app/product'; // 商品详情
+}
diff --git a/lib/api/video_api.dart b/lib/api/video_api.dart
index b86ce65..7cf0f30 100644
--- a/lib/api/video_api.dart
+++ b/lib/api/video_api.dart
@@ -1,18 +1,19 @@
class VideoApi {
// get
- static const String vlogList = '/vlog/indexList'; // 推荐视频列表数据
- static const String myPublicList = '/vlog/myPublicList'; // 我发布的视频
- static const String myPrivateList = '/vlog/myPrivateList'; // 我的私密视频
- static const String myLikedList = '/vlog/myLikedList'; // 我点赞的视频
- static const String friendList = '/vlog/friendList'; //互关好友的视频
- static const String followList = '/vlog/followList'; // 我关注的博主视频
- static const String detail = '/vlog/detail'; // 视频详情
+ static const String vlogList = '/app/vlog/indexList'; // 推荐视频列表数据
+ static const String myPrivateList = '/app/vlog/myPrivateList'; // 我的私密视频
+ static const String friendList = '/app/vlog/friendList'; //互关好友的视频
+ static const String followList = '/app/vlog/followList'; // 我关注的博主视频
+ static const String detail = '/app/vlog/detail'; // 视频详情
// post
- static const String unlike = '/vlog/unlike'; //取消点赞
- static const String totalLikedCounts = '/vlog/totalLikedCounts'; //收到点赞总数
- static const String publish = '/vlog/publish'; //发布视频
- static const String like = '/vlog/like'; //点赞
- static const String changeVlogStatus = '/vlog/changeVlogStatus'; //修改我的视频状态(删除视频)
- static const String changeToPublic = '/vlog/changeToPublic'; //将视频改为公开状态
- static const String changeToPrivate = '/vlog/changeToPrivate'; //将视频改为私密状态
+ static const String myPublicList = '/app/vlog/myPublicList'; // 我发布的视频
+ static const String myLikedList = '/app/vlog/myLikedList'; // 我点赞的视频
+
+ static const String unlike = '/app/vlog/unlike'; //取消点赞
+ static const String totalLikedCounts = '/app/vlog/totalLikedCounts'; //收到点赞总数
+ static const String publish = '/app/vlog/publish'; //发布视频
+ static const String like = '/app/vlog/like'; //点赞
+ static const String changeVlogStatus = '/app/vlog/changeVlogStatus'; //修改我的视频状态(删除视频)
+ static const String changeToPublic = '/app/vlog/changeToPublic'; //将视频改为公开状态
+ static const String changeToPrivate = '/app/vlog/changeToPrivate'; //将视频改为私密状态
}
diff --git a/lib/bings/chat_binding.dart b/lib/bings/chat_binding.dart
new file mode 100644
index 0000000..dbf8ac0
--- /dev/null
+++ b/lib/bings/chat_binding.dart
@@ -0,0 +1,11 @@
+import 'package:get/get.dart';
+import 'package:loopin/IM/controller/chat_detail_controller.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
+
+class ChatBinding extends Bindings {
+ @override
+ void dependencies() {
+ V2TimConversation conversation = Get.arguments;
+ Get.put(ChatDetailController(userID: conversation.userID!));
+ }
+}
diff --git a/lib/components/custom_sticky_header.dart b/lib/components/custom_sticky_header.dart
index 4d65ae6..8017bea 100644
--- a/lib/components/custom_sticky_header.dart
+++ b/lib/components/custom_sticky_header.dart
@@ -2,11 +2,20 @@
library;
import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+typedef OnPinnedChanged = void Function(bool pinned);
class CustomStickyHeader extends SliverPersistentHeaderDelegate {
final PreferredSize child;
+ RxBool? isPinned;
+ RxDouble? positions;
- CustomStickyHeader({required this.child});
+ CustomStickyHeader({
+ required this.child,
+ this.isPinned,
+ this.positions,
+ });
@override
double get minExtent => child.preferredSize.height;
@@ -21,6 +30,21 @@ class CustomStickyHeader extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+ // overlapsContent 或 shrinkOffset >= maxExtent - minExtent 都可以判断是否吸顶
+ bool pinned = overlapsContent; // true 表示已经吸顶
+ if (isPinned != null && isPinned!.value != pinned) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ isPinned!.value = pinned;
+ });
+ }
+ if (positions != null) {
+ if ((maxExtent - minExtent) >= shrinkOffset) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ positions!.value = shrinkOffset;
+ });
+ }
+ }
+
return child;
}
}
diff --git a/lib/components/my_toast.dart b/lib/components/my_toast.dart
new file mode 100644
index 0000000..806f604
--- /dev/null
+++ b/lib/components/my_toast.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:shirne_dialog/shirne_dialog.dart';
+
+class MyToast {
+ ///
+ void tip({
+ required String title,
+ String? type, // 默认失败
+ String? position, // 默认底部显示
+ }) {
+ final baseStyle = position == 'top'
+ ? MyDialog.theme.toastStyle?.top()
+ : position == 'center'
+ ? MyDialog.theme.toastStyle?.center()
+ : MyDialog.theme.toastStyle?.bottom();
+ MyDialog.toast(
+ title,
+ icon: type == 'success' ? const Icon(Icons.check_circle) : Icon(Icons.warning),
+ duration: Duration(milliseconds: 5000),
+ style: baseStyle?.copyWith(
+ backgroundColor: type == 'success' ? Colors.green.withAlpha(200) : Colors.red.withAlpha(200),
+ ),
+ );
+ }
+}
diff --git a/lib/components/network_or_asset_image.dart b/lib/components/network_or_asset_image.dart
new file mode 100644
index 0000000..5d3d3b9
--- /dev/null
+++ b/lib/components/network_or_asset_image.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+
+class NetworkOrAssetImage extends StatelessWidget {
+ final String? imageUrl;
+ final double width;
+ final double? height;
+ final BoxFit fit;
+ final String placeholderAsset;
+
+ const NetworkOrAssetImage({
+ super.key,
+ required this.imageUrl,
+ this.width = 60.0,
+ this.height,
+ this.fit = BoxFit.cover,
+ this.placeholderAsset = 'assets/images/avatar/default.png',
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final isNetwork = imageUrl != null && imageUrl!.isNotEmpty && (imageUrl!.startsWith('http://') || imageUrl!.startsWith('https://'));
+
+ if (isNetwork) {
+ return Image.network(
+ imageUrl!,
+ width: width,
+ height: height,
+ fit: fit,
+ errorBuilder: (context, error, stackTrace) {
+ return Image.asset(
+ placeholderAsset,
+ width: width,
+ height: height,
+ fit: fit,
+ );
+ },
+ );
+ } else {
+ return Image.asset(
+ (imageUrl != null && imageUrl!.isNotEmpty) ? imageUrl! : placeholderAsset,
+ width: width,
+ height: height,
+ fit: fit,
+ );
+ }
+ }
+}
diff --git a/lib/components/preview_video.dart b/lib/components/preview_video.dart
new file mode 100644
index 0000000..5a4cfec
--- /dev/null
+++ b/lib/components/preview_video.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+import 'package:loopin/components/shark_video.dart';
+import 'package:media_kit/media_kit.dart';
+import 'package:media_kit_video/media_kit_video.dart';
+
+class PreviewVideo extends StatefulWidget {
+ final String videoUrl;
+ final double? width;
+ final double? height;
+
+ const PreviewVideo({
+ super.key,
+ required this.videoUrl,
+ this.width,
+ this.height,
+ });
+
+ @override
+ State createState() => _PreviewVideoPageState();
+}
+
+class _PreviewVideoPageState extends State {
+ late final Player _player = Player();
+ late VideoController videoController = VideoController(_player);
+
+ @override
+ void initState() {
+ super.initState();
+ _player.open(Media(widget.videoUrl));
+ }
+
+ @override
+ void dispose() {
+ _player.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final videoWidth = widget.width ?? 1.0;
+ final videoHeight = widget.height ?? 1.0;
+ final isHorizontal = videoWidth > videoHeight;
+
+ return SafeArea(
+ child: Stack(
+ children: [
+ Scaffold(
+ backgroundColor: Colors.black,
+ body: Center(
+ child: Video(
+ controller: videoController,
+ fit: isHorizontal ? BoxFit.contain : BoxFit.cover,
+ controls: (state) => MyMaterialVideoControls(state),
+ ),
+ ),
+ ),
+ // 关闭按钮
+ Positioned(
+ top: 20,
+ left: 20,
+ child: GestureDetector(
+ onTap: () {
+ Navigator.pop(context);
+ },
+ child: const Icon(
+ Icons.close,
+ color: Colors.white,
+ size: 28,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/scan_util.dart b/lib/components/scan_util.dart
index a74ee88..5de2e22 100644
--- a/lib/components/scan_util.dart
+++ b/lib/components/scan_util.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:ai_barcode_scanner/ai_barcode_scanner.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
+import 'package:permission_handler/permission_handler.dart';
class ScanUtil {
static Future openScanner({required void Function(String) onResult}) async {
@@ -42,6 +43,29 @@ class ScanUtil {
width: MediaQuery.of(Get.context!).size.width * 0.8,
height: MediaQuery.of(Get.context!).size.height * 0.5,
),
+ // 异常处理
+ errorBuilder: (context, error) {
+ String message = "无法启动摄像头,请检查权限";
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.error, color: Colors.red, size: 60),
+ const SizedBox(height: 16),
+ Text(
+ message,
+ style: const TextStyle(fontSize: 18, color: Colors.red),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ ElevatedButton(
+ onPressed: () async => await openAppSettings(),
+ child: const Text('去开启权限'),
+ ),
+ ],
+ ),
+ );
+ },
),
);
diff --git a/lib/components/shark_video.dart b/lib/components/shark_video.dart
new file mode 100644
index 0000000..943b54f
--- /dev/null
+++ b/lib/components/shark_video.dart
@@ -0,0 +1,2062 @@
+/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
+///
+/// Copyright © 2021 & onwards, Hitesh Kumar Saini .
+/// All rights reserved.
+/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
+library;
+
+// ignore_for_file: non_constant_identifier_names
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:media_kit_video/media_kit_video.dart';
+import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
+import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart';
+import 'package:media_kit_video/media_kit_video_controls/src/controls/widgets/video_controls_theme_data_injector.dart';
+import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart';
+import 'package:volume_controller/volume_controller.dart';
+
+/// {@template material_video_controls}
+///
+/// [Video] controls which use Material design.
+///
+/// {@endtemplate}
+Widget MyMaterialVideoControls(VideoState state) {
+ return const VideoControlsThemeDataInjector(
+ child: _MaterialVideoControls(),
+ );
+}
+
+/// [MaterialVideoControlsThemeData] available in this [context].
+MaterialVideoControlsThemeData _theme(BuildContext context) => FullscreenInheritedWidget.maybeOf(context) == null
+ ? MaterialVideoControlsTheme.maybeOf(context)?.normal ?? kDefaultMaterialVideoControlsThemeData
+ : MaterialVideoControlsTheme.maybeOf(context)?.fullscreen ?? kDefaultMaterialVideoControlsThemeDataFullscreen;
+
+/// Default [MaterialVideoControlsThemeData].
+const kDefaultMaterialVideoControlsThemeData = MaterialVideoControlsThemeData();
+
+/// Default [MaterialVideoControlsThemeData] for fullscreen.
+const kDefaultMaterialVideoControlsThemeDataFullscreen = MaterialVideoControlsThemeData(
+ displaySeekBar: true,
+ automaticallyImplySkipNextButton: true,
+ automaticallyImplySkipPreviousButton: true,
+ volumeGesture: true,
+ brightnessGesture: true,
+ seekGesture: true,
+ gesturesEnabledWhileControlsVisible: true,
+ seekOnDoubleTap: true,
+ seekOnDoubleTapEnabledWhileControlsVisible: true,
+ visibleOnMount: false,
+ speedUpOnLongPress: false,
+ speedUpFactor: 2.0,
+ verticalGestureSensitivity: 100,
+ horizontalGestureSensitivity: 1000,
+ backdropColor: Color(0x66000000),
+ padding: null,
+ controlsHoverDuration: Duration(seconds: 3),
+ controlsTransitionDuration: Duration(milliseconds: 300),
+ bufferingIndicatorBuilder: null,
+ volumeIndicatorBuilder: null,
+ brightnessIndicatorBuilder: null,
+ seekIndicatorBuilder: null,
+ speedUpIndicatorBuilder: null,
+ primaryButtonBar: [
+ Spacer(flex: 2),
+ MaterialSkipPreviousButton(),
+ Spacer(),
+ MaterialPlayOrPauseButton(iconSize: 56.0),
+ Spacer(),
+ MaterialSkipNextButton(),
+ Spacer(flex: 2),
+ ],
+ topButtonBar: [],
+ topButtonBarMargin: EdgeInsets.symmetric(
+ horizontal: 16.0,
+ ),
+ bottomButtonBar: [
+ MaterialPositionIndicator(),
+ Spacer(),
+ // 全屏后的
+ MaterialFullscreenButton(),
+ ],
+ bottomButtonBarMargin: EdgeInsets.only(
+ left: 16.0,
+ right: 8.0,
+ bottom: 42.0,
+ ),
+ buttonBarHeight: 56.0,
+ buttonBarButtonSize: 24.0,
+ buttonBarButtonColor: Color(0xFFFFFFFF),
+ seekBarMargin: EdgeInsets.only(
+ left: 16.0,
+ right: 16.0,
+ bottom: 42.0,
+ ),
+ seekBarHeight: 2.4,
+ seekBarContainerHeight: 36.0,
+ seekBarColor: Color(0x3DFFFFFF),
+ seekBarPositionColor: Color(0xFFFF0000),
+ seekBarBufferColor: Color(0x3DFFFFFF),
+ seekBarThumbSize: 12.8,
+ seekBarThumbColor: Color(0xFFFF0000),
+ seekBarAlignment: Alignment.bottomCenter,
+ shiftSubtitlesOnControlsVisibilityChange: false,
+);
+
+/// {@template material_video_controls_theme_data}
+///
+/// Theming related data for [MaterialVideoControls]. These values are used to theme the descendant [MaterialVideoControls].
+///
+/// {@endtemplate}
+class MaterialVideoControlsThemeData {
+ // BEHAVIOR
+
+ /// Whether to display seek bar.
+ final bool displaySeekBar;
+
+ /// Whether a skip next button should be displayed if there are more than one videos in the playlist.
+ final bool automaticallyImplySkipNextButton;
+
+ /// Whether a skip previous button should be displayed if there are more than one videos in the playlist.
+ final bool automaticallyImplySkipPreviousButton;
+
+ /// Whether to modify volume on vertical drag gesture on the right side of the screen.
+ final bool volumeGesture;
+
+ /// Whether to modify screen brightness on vertical drag gesture on the left side of the screen.
+ final bool brightnessGesture;
+
+ /// Whether to seek on horizontal drag gesture.
+ final bool seekGesture;
+
+ /// Whether to allow gesture controls to work while controls are visible.
+ /// NOTE: This option is ignored when gestures are false.
+ final bool gesturesEnabledWhileControlsVisible;
+
+ /// Whether to enable double tap to seek on left or right side of the screen.
+ final bool seekOnDoubleTap;
+
+ /// Whether to allow double tap to seek on left or right side of the screen to work while controls are visible.
+ /// NOTE: This option is ignored when [seekOnDoubleTap] is false.
+ final bool seekOnDoubleTapEnabledWhileControlsVisible;
+
+ /// `seekOnDoubleTapLayoutTapsRatios` defines the width proportions for the interactive areas
+ /// responsible for seek actions (backward seek, instant tap, forward seek) when a double tap
+ /// occurs on the video widget. This property divides the video widget into three segments
+ /// horizontally. Each integer in the list represents the relative width of each segment.
+ /// By default, the value `[1, 1, 1]` means that the video widget is equally divided into three
+ /// segments: the left segment for backward seek, the middle segment for instant tap (usually show and hide controls),
+ /// and the right segment for forward seek. Adjusting these values changes the width of the interactive areas
+ /// for each double tap action.
+ final List seekOnDoubleTapLayoutTapsRatios;
+
+ /// `seekOnDoubleTapLayoutWidgetRatios` defines the width proportions for the visual indicators or
+ /// widgets that appear during the double tap actions (backward seek, instant tap, forward seek).
+ /// Similar to `seekOnDoubleTapLayoutTapsRatios`, it divides the area where these indicators are
+ /// displayed into three segments. Each integer in the list represents the relative width of each
+ /// segment where the corresponding indicators will be shown. The default `[1, 1, 1]` equally divides
+ /// the space for each indicator. Modifying these values can change the layout of the seek indicators,
+ /// giving more or less space to each one based on the specified ratios.
+ final List seekOnDoubleTapLayoutWidgetRatios;
+
+ /// Duration of seek on double tap backward.
+ final Duration seekOnDoubleTapBackwardDuration;
+
+ /// Duration of seek on double tap forward.
+ final Duration seekOnDoubleTapForwardDuration;
+
+ /// Whether the controls are initially visible.
+ final bool visibleOnMount;
+
+ /// Whether to speed up on long press.
+ final bool speedUpOnLongPress;
+
+ /// Factor to speed up on long press.
+ final double speedUpFactor;
+
+ /// Gesture sensitivity on vertical drag gestures, the higher the value is the less sensitive the gesture.
+ final double verticalGestureSensitivity;
+
+ /// Gesture sensitivity on horizontal drag gestures, the higher the value is the less sensitive the gesture.
+ final double horizontalGestureSensitivity;
+
+ /// Color of backdrop that comes up when controls are visible.
+ final Color? backdropColor;
+
+ // GENERIC
+
+ /// Padding around the controls.
+ ///
+ /// * Default: `EdgeInsets.zero`
+ /// * FullScreen: `MediaQuery.of(context).padding`
+ ///
+ /// NOTE: In fullscreen, this will be safe area (set [padding] to [EdgeInsets.zero] to disable safe area)
+ final EdgeInsets? padding;
+
+ /// [Duration] after which the controls will be hidden when there is no mouse movement.
+ final Duration controlsHoverDuration;
+
+ /// [Duration] for which the controls will be animated when shown or hidden.
+ final Duration controlsTransitionDuration;
+
+ /// Builder for the buffering indicator.
+ final Widget Function(BuildContext)? bufferingIndicatorBuilder;
+
+ /// Custom builder for volume indicator.
+ final Widget Function(BuildContext, double)? volumeIndicatorBuilder;
+
+ /// Custom builder for brightness indicator.
+ final Widget Function(BuildContext, double)? brightnessIndicatorBuilder;
+
+ /// Custom builder for seek indicator.
+ final Widget Function(BuildContext, Duration)? seekIndicatorBuilder;
+
+ /// Custom builder for seek indicator.
+ final Widget Function(BuildContext, double)? speedUpIndicatorBuilder;
+
+ // BUTTON BAR
+
+ /// Buttons to be displayed in the primary button bar.
+ final List primaryButtonBar;
+
+ /// Buttons to be displayed in the top button bar.
+ final List topButtonBar;
+
+ /// Margin around the top button bar.
+ final EdgeInsets topButtonBarMargin;
+
+ /// Buttons to be displayed in the bottom button bar.
+ final List bottomButtonBar;
+
+ /// Margin around the button bar.
+ final EdgeInsets bottomButtonBarMargin;
+
+ /// Height of the button bar.
+ final double buttonBarHeight;
+
+ /// Size of the button bar buttons.
+ final double buttonBarButtonSize;
+
+ /// Color of the button bar buttons.
+ final Color buttonBarButtonColor;
+
+ // SEEK BAR
+
+ /// Margin around the seek bar.
+ final EdgeInsets seekBarMargin;
+
+ /// Height of the seek bar.
+ final double seekBarHeight;
+
+ /// Height of the seek bar [Container].
+ final double seekBarContainerHeight;
+
+ /// [Color] of the seek bar.
+ final Color seekBarColor;
+
+ /// [Color] of the playback position section in the seek bar.
+ final Color seekBarPositionColor;
+
+ /// [Color] of the playback buffer section in the seek bar.
+ final Color seekBarBufferColor;
+
+ /// Size of the seek bar thumb.
+ final double seekBarThumbSize;
+
+ /// [Color] of the seek bar thumb.
+ final Color seekBarThumbColor;
+
+ /// [Alignment] of seek bar inside the seek bar container.
+ final Alignment seekBarAlignment;
+
+ // SUBTITLE
+
+ /// Whether to shift the subtitles upwards when the controls are visible.
+ final bool shiftSubtitlesOnControlsVisibilityChange;
+
+ /// {@macro material_video_controls_theme_data}
+ const MaterialVideoControlsThemeData({
+ this.displaySeekBar = true,
+ this.automaticallyImplySkipNextButton = true,
+ this.automaticallyImplySkipPreviousButton = true,
+ this.volumeGesture = false,
+ this.brightnessGesture = false,
+ this.seekGesture = false,
+ this.gesturesEnabledWhileControlsVisible = true,
+ this.seekOnDoubleTap = false,
+ this.seekOnDoubleTapEnabledWhileControlsVisible = true,
+ this.seekOnDoubleTapLayoutTapsRatios = const [1, 1, 1],
+ this.seekOnDoubleTapLayoutWidgetRatios = const [1, 1, 1],
+ this.seekOnDoubleTapBackwardDuration = const Duration(seconds: 10),
+ this.seekOnDoubleTapForwardDuration = const Duration(seconds: 10),
+ this.visibleOnMount = false,
+ this.speedUpOnLongPress = false,
+ this.speedUpFactor = 2.0,
+ this.verticalGestureSensitivity = 100,
+ this.horizontalGestureSensitivity = 1000,
+ this.backdropColor = const Color(0x66000000),
+ this.padding,
+ this.controlsHoverDuration = const Duration(seconds: 3),
+ this.controlsTransitionDuration = const Duration(milliseconds: 300),
+ this.bufferingIndicatorBuilder,
+ this.volumeIndicatorBuilder,
+ this.brightnessIndicatorBuilder,
+ this.seekIndicatorBuilder,
+ this.speedUpIndicatorBuilder,
+ this.primaryButtonBar = const [
+ Spacer(flex: 2),
+ MaterialSkipPreviousButton(),
+ Spacer(),
+ MaterialPlayOrPauseButton(iconSize: 48.0),
+ Spacer(),
+ MaterialSkipNextButton(),
+ Spacer(flex: 2),
+ ],
+ this.topButtonBar = const [],
+ this.topButtonBarMargin = const EdgeInsets.symmetric(horizontal: 16.0),
+ this.bottomButtonBar = const [
+ MaterialPositionIndicator(),
+ Spacer(),
+ // 未全屏的
+ // MaterialFullscreenButton(),
+ ],
+ this.bottomButtonBarMargin = const EdgeInsets.only(left: 16.0, right: 8.0),
+ this.buttonBarHeight = 56.0,
+ this.buttonBarButtonSize = 24.0,
+ this.buttonBarButtonColor = const Color(0xFFFFFFFF),
+ this.seekBarMargin = EdgeInsets.zero,
+ this.seekBarHeight = 2.4,
+ this.seekBarContainerHeight = 36.0,
+ this.seekBarColor = const Color(0x3DFFFFFF),
+ this.seekBarPositionColor = const Color(0xFFFF0000),
+ this.seekBarBufferColor = const Color(0x3DFFFFFF),
+ this.seekBarThumbSize = 12.8,
+ this.seekBarThumbColor = const Color(0xFFFF0000),
+ this.seekBarAlignment = Alignment.bottomCenter,
+ this.shiftSubtitlesOnControlsVisibilityChange = false,
+ });
+
+ /// Creates a copy of this [MaterialVideoControlsThemeData] with the given fields replaced by the non-null parameter values.
+ MaterialVideoControlsThemeData copyWith({
+ bool? displaySeekBar,
+ bool? automaticallyImplySkipNextButton,
+ bool? automaticallyImplySkipPreviousButton,
+ bool? volumeGesture,
+ bool? brightnessGesture,
+ bool? seekGesture,
+ bool? gesturesEnabledWhileControlsVisible,
+ bool? seekOnDoubleTap,
+ bool? seekOnDoubleTapEnabledWhileControlsVisible,
+ List? seekOnDoubleTapLayoutTapsRatios,
+ List? seekOnDoubleTapLayoutWidgetRatios,
+ Duration? seekOnDoubleTapBackwardDuration,
+ Duration? seekOnDoubleTapForwardDuration,
+ bool? visibleOnMount,
+ bool? speedUpOnLongPress,
+ double? speedUpFactor,
+ double? verticalGestureSensitivity,
+ double? horizontalGestureSensitivity,
+ Color? backdropColor,
+ Duration? controlsHoverDuration,
+ Duration? controlsTransitionDuration,
+ Widget Function(BuildContext)? bufferingIndicatorBuilder,
+ Widget Function(BuildContext, double)? volumeIndicatorBuilder,
+ Widget Function(BuildContext, double)? brightnessIndicatorBuilder,
+ Widget Function(BuildContext, Duration)? seekIndicatorBuilder,
+ Widget Function(BuildContext, double)? speedUpIndicatorBuilder,
+ List? primaryButtonBar,
+ List? topButtonBar,
+ EdgeInsets? topButtonBarMargin,
+ List? bottomButtonBar,
+ EdgeInsets? bottomButtonBarMargin,
+ double? buttonBarHeight,
+ double? buttonBarButtonSize,
+ Color? buttonBarButtonColor,
+ EdgeInsets? seekBarMargin,
+ double? seekBarHeight,
+ double? seekBarContainerHeight,
+ Color? seekBarColor,
+ Color? seekBarPositionColor,
+ Color? seekBarBufferColor,
+ double? seekBarThumbSize,
+ Color? seekBarThumbColor,
+ Alignment? seekBarAlignment,
+ bool? shiftSubtitlesOnControlsVisibilityChange,
+ }) {
+ return MaterialVideoControlsThemeData(
+ displaySeekBar: displaySeekBar ?? this.displaySeekBar,
+ automaticallyImplySkipNextButton: automaticallyImplySkipNextButton ?? this.automaticallyImplySkipNextButton,
+ automaticallyImplySkipPreviousButton: automaticallyImplySkipPreviousButton ?? this.automaticallyImplySkipPreviousButton,
+ volumeGesture: volumeGesture ?? this.volumeGesture,
+ brightnessGesture: brightnessGesture ?? this.brightnessGesture,
+ seekGesture: seekGesture ?? this.seekGesture,
+ gesturesEnabledWhileControlsVisible: gesturesEnabledWhileControlsVisible ?? this.gesturesEnabledWhileControlsVisible,
+ seekOnDoubleTap: seekOnDoubleTap ?? this.seekOnDoubleTap,
+ seekOnDoubleTapEnabledWhileControlsVisible: seekOnDoubleTapEnabledWhileControlsVisible ?? this.seekOnDoubleTapEnabledWhileControlsVisible,
+ seekOnDoubleTapLayoutTapsRatios: seekOnDoubleTapLayoutTapsRatios ?? this.seekOnDoubleTapLayoutTapsRatios,
+ seekOnDoubleTapLayoutWidgetRatios: seekOnDoubleTapLayoutWidgetRatios ?? this.seekOnDoubleTapLayoutWidgetRatios,
+ seekOnDoubleTapBackwardDuration: seekOnDoubleTapBackwardDuration ?? this.seekOnDoubleTapBackwardDuration,
+ seekOnDoubleTapForwardDuration: seekOnDoubleTapForwardDuration ?? this.seekOnDoubleTapForwardDuration,
+ visibleOnMount: visibleOnMount ?? this.visibleOnMount,
+ speedUpOnLongPress: speedUpOnLongPress ?? this.speedUpOnLongPress,
+ speedUpFactor: speedUpFactor ?? this.speedUpFactor,
+ verticalGestureSensitivity: verticalGestureSensitivity ?? this.verticalGestureSensitivity,
+ horizontalGestureSensitivity: horizontalGestureSensitivity ?? this.horizontalGestureSensitivity,
+ backdropColor: backdropColor ?? this.backdropColor,
+ controlsHoverDuration: controlsHoverDuration ?? this.controlsHoverDuration,
+ controlsTransitionDuration: controlsTransitionDuration ?? this.controlsTransitionDuration,
+ bufferingIndicatorBuilder: bufferingIndicatorBuilder ?? this.bufferingIndicatorBuilder,
+ volumeIndicatorBuilder: volumeIndicatorBuilder ?? this.volumeIndicatorBuilder,
+ brightnessIndicatorBuilder: brightnessIndicatorBuilder ?? this.brightnessIndicatorBuilder,
+ seekIndicatorBuilder: seekIndicatorBuilder ?? this.seekIndicatorBuilder,
+ speedUpIndicatorBuilder: speedUpIndicatorBuilder ?? this.speedUpIndicatorBuilder,
+ primaryButtonBar: primaryButtonBar ?? this.primaryButtonBar,
+ topButtonBar: topButtonBar ?? this.topButtonBar,
+ topButtonBarMargin: topButtonBarMargin ?? this.topButtonBarMargin,
+ bottomButtonBar: bottomButtonBar ?? this.bottomButtonBar,
+ bottomButtonBarMargin: bottomButtonBarMargin ?? this.bottomButtonBarMargin,
+ buttonBarHeight: buttonBarHeight ?? this.buttonBarHeight,
+ buttonBarButtonSize: buttonBarButtonSize ?? this.buttonBarButtonSize,
+ buttonBarButtonColor: buttonBarButtonColor ?? this.buttonBarButtonColor,
+ seekBarMargin: seekBarMargin ?? this.seekBarMargin,
+ seekBarHeight: seekBarHeight ?? this.seekBarHeight,
+ seekBarContainerHeight: seekBarContainerHeight ?? this.seekBarContainerHeight,
+ seekBarColor: seekBarColor ?? this.seekBarColor,
+ seekBarPositionColor: seekBarPositionColor ?? this.seekBarPositionColor,
+ seekBarBufferColor: seekBarBufferColor ?? this.seekBarBufferColor,
+ seekBarThumbSize: seekBarThumbSize ?? this.seekBarThumbSize,
+ seekBarThumbColor: seekBarThumbColor ?? this.seekBarThumbColor,
+ seekBarAlignment: seekBarAlignment ?? this.seekBarAlignment,
+ shiftSubtitlesOnControlsVisibilityChange: shiftSubtitlesOnControlsVisibilityChange ?? this.shiftSubtitlesOnControlsVisibilityChange,
+ );
+ }
+}
+
+/// {@template material_video_controls_theme}
+///
+/// Inherited widget which provides [MaterialVideoControlsThemeData] to descendant widgets.
+///
+/// {@endtemplate}
+class MaterialVideoControlsTheme extends InheritedWidget {
+ final MaterialVideoControlsThemeData normal;
+ final MaterialVideoControlsThemeData fullscreen;
+ const MaterialVideoControlsTheme({
+ super.key,
+ required this.normal,
+ required this.fullscreen,
+ required super.child,
+ });
+
+ static MaterialVideoControlsTheme? maybeOf(BuildContext context) {
+ return context.dependOnInheritedWidgetOfExactType();
+ }
+
+ static MaterialVideoControlsTheme of(BuildContext context) {
+ final MaterialVideoControlsTheme? result = maybeOf(context);
+ assert(
+ result != null,
+ 'No [MaterialVideoControlsTheme] found in [context]',
+ );
+ return result!;
+ }
+
+ @override
+ bool updateShouldNotify(MaterialVideoControlsTheme oldWidget) => identical(normal, oldWidget.normal) && identical(fullscreen, oldWidget.fullscreen);
+}
+
+/// {@macro material_video_controls}
+class _MaterialVideoControls extends StatefulWidget {
+ const _MaterialVideoControls();
+
+ @override
+ State<_MaterialVideoControls> createState() => _MaterialVideoControlsState();
+}
+
+/// {@macro material_video_controls}
+class _MaterialVideoControlsState extends State<_MaterialVideoControls> {
+ late bool mount = _theme(context).visibleOnMount;
+ late bool visible = _theme(context).visibleOnMount;
+ Timer? _timer;
+
+ double _brightnessValue = 0.0;
+ bool _brightnessIndicator = false;
+ Timer? _brightnessTimer;
+ double _currentRate = 1.0;
+ double _volumeValue = 0.0;
+ bool _volumeIndicator = false;
+ Timer? _volumeTimer;
+ // The default event stream in package:volume_controller is buggy.
+ bool _volumeInterceptEventStream = false;
+
+ Offset _dragInitialDelta = Offset.zero; // Initial position for horizontal drag
+ int swipeDuration = 0; // Duration to seek in video
+ bool showSwipeDuration = false; // Whether to show the seek duration overlay
+
+ bool _speedUpIndicator = false;
+ late /* private */ var playlist = controller(context).player.state.playlist;
+ late bool buffering = controller(context).player.state.buffering;
+ final VolumeController _volumeController = VolumeController.instance;
+
+ bool _mountSeekBackwardButton = false;
+ bool _mountSeekForwardButton = false;
+ bool _hideSeekBackwardButton = false;
+ bool _hideSeekForwardButton = false;
+ Timer? _timerSeekBackwardButton;
+ Timer? _timerSeekForwardButton;
+
+ final ValueNotifier _seekBarDeltaValueNotifier = ValueNotifier(Duration.zero);
+
+ final List subscriptions = [];
+
+ double get subtitleVerticalShiftOffset =>
+ (_theme(context).padding?.bottom ?? 0.0) +
+ (_theme(context).bottomButtonBarMargin.vertical) +
+ (_theme(context).bottomButtonBar.isNotEmpty ? _theme(context).buttonBarHeight : 0.0);
+ Offset? _tapPosition;
+
+ void _handleDoubleTapDown(TapDownDetails details) {
+ setState(() {
+ _tapPosition = details.localPosition;
+ });
+ }
+
+ void _handleLongPress() {
+ setState(() {
+ _speedUpIndicator = true;
+ });
+ _currentRate = controller(context).player.state.rate;
+ controller(context).player.setRate(_theme(context).speedUpFactor);
+ }
+
+ void _handleLongPressEnd(LongPressEndDetails details) {
+ setState(() {
+ _speedUpIndicator = false;
+ });
+ controller(context).player.setRate(_currentRate);
+ }
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (subscriptions.isEmpty) {
+ subscriptions.addAll(
+ [
+ controller(context).player.stream.playlist.listen(
+ (event) {
+ setState(() {
+ playlist = event;
+ });
+ },
+ ),
+ controller(context).player.stream.buffering.listen(
+ (event) {
+ setState(() {
+ buffering = event;
+ });
+ },
+ ),
+ ],
+ );
+
+ if (_theme(context).visibleOnMount) {
+ _timer = Timer(
+ _theme(context).controlsHoverDuration,
+ () {
+ if (mounted) {
+ setState(() {
+ visible = false;
+ });
+ unshiftSubtitle();
+ }
+ },
+ );
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ for (final subscription in subscriptions) {
+ subscription.cancel();
+ }
+ // --------------------------------------------------
+ // package:screen_brightness
+ Future.microtask(() async {
+ try {
+ await ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness();
+ } catch (_) {}
+ });
+ // --------------------------------------------------
+ _timerSeekBackwardButton?.cancel();
+ _timerSeekForwardButton?.cancel();
+ super.dispose();
+ }
+
+ void shiftSubtitle() {
+ if (_theme(context).shiftSubtitlesOnControlsVisibilityChange) {
+ state(context).setSubtitleViewPadding(
+ state(context).widget.subtitleViewConfiguration.padding +
+ EdgeInsets.fromLTRB(
+ 0.0,
+ 0.0,
+ 0.0,
+ subtitleVerticalShiftOffset,
+ ),
+ );
+ }
+ }
+
+ void unshiftSubtitle() {
+ if (_theme(context).shiftSubtitlesOnControlsVisibilityChange) {
+ state(context).setSubtitleViewPadding(
+ state(context).widget.subtitleViewConfiguration.padding,
+ );
+ }
+ }
+
+ void onTap() {
+ if (!visible) {
+ setState(() {
+ mount = true;
+ visible = true;
+ });
+ shiftSubtitle();
+ _timer?.cancel();
+ _timer = Timer(_theme(context).controlsHoverDuration, () {
+ if (mounted) {
+ setState(() {
+ visible = false;
+ });
+ unshiftSubtitle();
+ }
+ });
+ } else {
+ setState(() {
+ visible = false;
+ });
+ unshiftSubtitle();
+ _timer?.cancel();
+ }
+ }
+
+ void onDoubleTapSeekBackward() {
+ setState(() {
+ _mountSeekBackwardButton = true;
+ });
+ }
+
+ void onDoubleTapSeekForward() {
+ setState(() {
+ _mountSeekForwardButton = true;
+ });
+ }
+
+ void onHorizontalDragUpdate(DragUpdateDetails details) {
+ if (_dragInitialDelta == Offset.zero) {
+ _dragInitialDelta = details.localPosition;
+ return;
+ }
+
+ final diff = _dragInitialDelta.dx - details.localPosition.dx;
+ final duration = controller(context).player.state.duration.inSeconds;
+ final position = controller(context).player.state.position.inSeconds;
+
+ final seconds = -(diff * duration / _theme(context).horizontalGestureSensitivity).round();
+ final relativePosition = position + seconds;
+
+ if (relativePosition <= duration && relativePosition >= 0) {
+ setState(() {
+ swipeDuration = seconds;
+ showSwipeDuration = true;
+ _seekBarDeltaValueNotifier.value = Duration(seconds: seconds);
+ });
+ }
+ }
+
+ void onHorizontalDragEnd() {
+ if (swipeDuration != 0) {
+ Duration newPosition = controller(context).player.state.position + Duration(seconds: swipeDuration);
+ newPosition = newPosition.clamp(
+ Duration.zero,
+ controller(context).player.state.duration,
+ );
+ controller(context).player.seek(newPosition);
+ }
+
+ setState(() {
+ _dragInitialDelta = Offset.zero;
+ showSwipeDuration = false;
+ });
+ }
+
+ bool _isInSegment(double localX, int segmentIndex) {
+ // Local variable with the list of ratios
+ List segmentRatios = _theme(context).seekOnDoubleTapLayoutTapsRatios;
+
+ int totalRatios = segmentRatios.reduce((a, b) => a + b);
+
+ double segmentWidthMultiplier = widgetWidth(context) / totalRatios;
+ double start = 0;
+ double end;
+
+ for (int i = 0; i < segmentRatios.length; i++) {
+ end = start + (segmentWidthMultiplier * segmentRatios[i]);
+
+ // Check if the current index matches the segmentIndex and if localX falls within it
+ if (i == segmentIndex && localX >= start && localX <= end) {
+ return true;
+ }
+
+ // Set the start of the next segment
+ start = end;
+ }
+
+ // If localX does not fall within the specified segment
+ return false;
+ }
+
+ bool _isInRightSegment(double localX) {
+ return _isInSegment(localX, 2);
+ }
+
+ bool _isInLeftSegment(double localX) {
+ return _isInSegment(localX, 0);
+ }
+
+ void _handlePointerDown(PointerDownEvent event) {
+ onTap();
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ // --------------------------------------------------
+ // package:volume_controller
+ Future.microtask(() async {
+ try {
+ _volumeController.showSystemUI = false;
+ _volumeValue = await _volumeController.getVolume();
+ _volumeController.addListener((value) {
+ if (mounted && !_volumeInterceptEventStream) {
+ setState(() {
+ _volumeValue = value;
+ });
+ }
+ });
+ } catch (_) {}
+ });
+ // --------------------------------------------------
+ // --------------------------------------------------
+ // package:screen_brightness
+ Future.microtask(() async {
+ try {
+ _brightnessValue = await ScreenBrightnessPlatform.instance.application;
+ ScreenBrightnessPlatform.instance.onApplicationScreenBrightnessChanged.listen((value) {
+ if (mounted) {
+ setState(() {
+ _brightnessValue = value;
+ });
+ }
+ });
+ } catch (_) {}
+ });
+ // --------------------------------------------------
+ }
+
+ Future setVolume(double value) async {
+ // --------------------------------------------------
+ // package:volume_controller
+ try {
+ _volumeController.setVolume(value);
+ } catch (_) {}
+ setState(() {
+ _volumeValue = value;
+ _volumeIndicator = true;
+ _volumeInterceptEventStream = true;
+ });
+ _volumeTimer?.cancel();
+ _volumeTimer = Timer(const Duration(milliseconds: 200), () {
+ if (mounted) {
+ setState(() {
+ _volumeIndicator = false;
+ _volumeInterceptEventStream = false;
+ });
+ }
+ });
+ // --------------------------------------------------
+ }
+
+ Future setBrightness(double value) async {
+ // --------------------------------------------------
+ // package:screen_brightness
+ try {
+ await ScreenBrightnessPlatform.instance.setApplicationScreenBrightness(value);
+ } catch (_) {}
+ setState(() {
+ _brightnessIndicator = true;
+ });
+ _brightnessTimer?.cancel();
+ _brightnessTimer = Timer(const Duration(milliseconds: 200), () {
+ if (mounted) {
+ setState(() {
+ _brightnessIndicator = false;
+ });
+ }
+ });
+ // --------------------------------------------------
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var seekOnDoubleTapEnabledWhileControlsAreVisible = (_theme(context).seekOnDoubleTap && _theme(context).seekOnDoubleTapEnabledWhileControlsVisible);
+ assert(_theme(context).seekOnDoubleTapLayoutTapsRatios.length == 3, "The number of seekOnDoubleTapLayoutTapsRatios must be 3, i.e. [1, 1, 1]");
+ assert(_theme(context).seekOnDoubleTapLayoutWidgetRatios.length == 3, "The number of seekOnDoubleTapLayoutWidgetRatios must be 3, i.e. [1, 1, 1]");
+ return Theme(
+ data: Theme.of(context).copyWith(
+ focusColor: const Color(0x00000000),
+ hoverColor: const Color(0x00000000),
+ splashColor: const Color(0x00000000),
+ highlightColor: const Color(0x00000000),
+ ),
+ child: Focus(
+ autofocus: true,
+ child: Material(
+ elevation: 0.0,
+ borderOnForeground: false,
+ animationDuration: Duration.zero,
+ color: const Color(0x00000000),
+ shadowColor: const Color(0x00000000),
+ surfaceTintColor: const Color(0x00000000),
+ child: Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.center,
+ children: [
+ // Controls:
+ AnimatedOpacity(
+ curve: Curves.easeInOut,
+ opacity: visible ? 1.0 : 0.0,
+ duration: _theme(context).controlsTransitionDuration,
+ onEnd: () {
+ setState(() {
+ if (!visible) {
+ mount = false;
+ }
+ });
+ },
+ child: Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.center,
+ children: [
+ Positioned.fill(
+ child: Container(
+ color: _theme(context).backdropColor,
+ ),
+ ),
+ // We are adding 16.0 boundary around the actual controls (which contain the vertical drag gesture detectors).
+ // This will make the hit-test on edges (e.g. swiping to: show status-bar, show navigation-bar, go back in navigation) not activate the swipe gesture annoyingly.
+ Positioned.fill(
+ left: 16.0,
+ top: 16.0,
+ right: 16.0,
+ bottom: 16.0 + subtitleVerticalShiftOffset,
+ child: Listener(
+ onPointerDown: (event) => _handlePointerDown(event),
+ child: GestureDetector(
+ onDoubleTapDown: _handleDoubleTapDown,
+ onLongPress: _theme(context).speedUpOnLongPress ? _handleLongPress : null,
+ onLongPressEnd: _theme(context).speedUpOnLongPress ? _handleLongPressEnd : null,
+ onDoubleTap: () {
+ if (_tapPosition == null) {
+ return;
+ }
+ if (_isInRightSegment(_tapPosition!.dx)) {
+ if ((!mount && _theme(context).seekOnDoubleTap) || seekOnDoubleTapEnabledWhileControlsAreVisible) {
+ onDoubleTapSeekForward();
+ }
+ } else {
+ if (_isInLeftSegment(_tapPosition!.dx)) {
+ if ((!mount && _theme(context).seekOnDoubleTap) || seekOnDoubleTapEnabledWhileControlsAreVisible) {
+ onDoubleTapSeekBackward();
+ }
+ }
+ }
+ },
+ onHorizontalDragUpdate: (details) {
+ if ((!mount && _theme(context).seekGesture) ||
+ (_theme(context).seekGesture && _theme(context).gesturesEnabledWhileControlsVisible)) {
+ onHorizontalDragUpdate(details);
+ }
+ },
+ onHorizontalDragEnd: (details) {
+ onHorizontalDragEnd();
+ },
+ onVerticalDragUpdate: (e) async {
+ final delta = e.delta.dy;
+ final Offset position = e.localPosition;
+
+ if (position.dx <= widgetWidth(context) / 2) {
+ // Left side of screen swiped
+ if ((!mount && _theme(context).brightnessGesture) ||
+ (_theme(context).brightnessGesture && _theme(context).gesturesEnabledWhileControlsVisible)) {
+ final brightness = _brightnessValue - delta / _theme(context).verticalGestureSensitivity;
+ final result = brightness.clamp(0.0, 1.0);
+ setBrightness(result);
+ }
+ } else {
+ // Right side of screen swiped
+
+ if ((!mount && _theme(context).volumeGesture) ||
+ (_theme(context).volumeGesture && _theme(context).gesturesEnabledWhileControlsVisible)) {
+ final volume = _volumeValue - delta / _theme(context).verticalGestureSensitivity;
+ final result = volume.clamp(0.0, 1.0);
+ setVolume(result);
+ }
+ }
+ },
+ child: Container(
+ color: const Color(0x00000000),
+ ),
+ ),
+ ),
+ ),
+ if (mount)
+ Padding(
+ padding: _theme(context).padding ??
+ (
+ // Add padding in fullscreen!
+ isFullscreen(context) ? MediaQuery.of(context).padding : EdgeInsets.zero),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).topButtonBarMargin,
+ child: Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: _theme(context).topButtonBar,
+ ),
+ ),
+ // Only display [primaryButtonBar] if [buffering] is false.
+ Expanded(
+ child: AnimatedOpacity(
+ curve: Curves.easeInOut,
+ opacity: buffering ? 0.0 : 1.0,
+ duration: _theme(context).controlsTransitionDuration,
+ child: Center(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: _theme(context).primaryButtonBar,
+ ),
+ ),
+ ),
+ ),
+ Stack(
+ alignment: Alignment.bottomCenter,
+ children: [
+ if (_theme(context).displaySeekBar)
+ MaterialSeekBar(
+ onSeekStart: () {
+ _timer?.cancel();
+ },
+ onSeekEnd: () {
+ _timer = Timer(
+ _theme(context).controlsHoverDuration,
+ () {
+ if (mounted) {
+ setState(() {
+ visible = false;
+ });
+ unshiftSubtitle();
+ }
+ },
+ );
+ },
+ ),
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).bottomButtonBarMargin,
+ child: Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: _theme(context).bottomButtonBar,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ // Double-Tap Seek Seek-Bar:
+ if (!mount)
+ if (_mountSeekBackwardButton || _mountSeekForwardButton || showSwipeDuration)
+ Column(
+ children: [
+ const Spacer(),
+ Stack(
+ alignment: Alignment.bottomCenter,
+ children: [
+ if (_theme(context).displaySeekBar)
+ MaterialSeekBar(
+ delta: _seekBarDeltaValueNotifier,
+ ),
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).bottomButtonBarMargin,
+ ),
+ ],
+ ),
+ ],
+ ),
+ // Buffering Indicator.
+ IgnorePointer(
+ child: Padding(
+ padding: _theme(context).padding ??
+ (
+ // Add padding in fullscreen!
+ isFullscreen(context) ? MediaQuery.of(context).padding : EdgeInsets.zero),
+ child: Column(
+ children: [
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).topButtonBarMargin,
+ ),
+ Expanded(
+ child: Center(
+ child: TweenAnimationBuilder(
+ tween: Tween(
+ begin: 0.0,
+ end: buffering ? 1.0 : 0.0,
+ ),
+ duration: _theme(context).controlsTransitionDuration,
+ builder: (context, value, child) {
+ // Only mount the buffering indicator if the opacity is greater than 0.0.
+ // This has been done to prevent redundant resource usage in [CircularProgressIndicator].
+ if (value > 0.0) {
+ return Opacity(
+ opacity: value,
+ child: _theme(context).bufferingIndicatorBuilder?.call(context) ?? child!,
+ );
+ }
+ return const SizedBox.shrink();
+ },
+ child: const CircularProgressIndicator(
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ),
+ ),
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).bottomButtonBarMargin,
+ ),
+ ],
+ ),
+ ),
+ ),
+ // Volume Indicator.
+ IgnorePointer(
+ child: AnimatedOpacity(
+ curve: Curves.easeInOut,
+ opacity: (!mount || _theme(context).gesturesEnabledWhileControlsVisible) && _volumeIndicator ? 1.0 : 0.0,
+ duration: _theme(context).controlsTransitionDuration,
+ child: _theme(context).volumeIndicatorBuilder?.call(context, _volumeValue) ??
+ Container(
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: const Color(0x88000000),
+ borderRadius: BorderRadius.circular(64.0),
+ ),
+ height: 52.0,
+ width: 108.0,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Container(
+ height: 52.0,
+ width: 42.0,
+ alignment: Alignment.centerRight,
+ child: Icon(
+ _volumeValue == 0.0
+ ? Icons.volume_off
+ : _volumeValue < 0.5
+ ? Icons.volume_down
+ : Icons.volume_up,
+ color: const Color(0xFFFFFFFF),
+ size: 24.0,
+ ),
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: Text(
+ '${(_volumeValue * 100.0).round()}%',
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 14.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ ],
+ ),
+ ),
+ ),
+ ),
+ // Brightness Indicator.
+ IgnorePointer(
+ child: AnimatedOpacity(
+ curve: Curves.easeInOut,
+ opacity: (!mount || _theme(context).gesturesEnabledWhileControlsVisible) && _brightnessIndicator ? 1.0 : 0.0,
+ duration: _theme(context).controlsTransitionDuration,
+ child: _theme(context).brightnessIndicatorBuilder?.call(context, _brightnessValue) ??
+ Container(
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: const Color(0x88000000),
+ borderRadius: BorderRadius.circular(64.0),
+ ),
+ height: 52.0,
+ width: 108.0,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Container(
+ height: 52.0,
+ width: 42.0,
+ alignment: Alignment.centerRight,
+ child: Icon(
+ _brightnessValue < 1.0 / 3.0
+ ? Icons.brightness_low
+ : _brightnessValue < 2.0 / 3.0
+ ? Icons.brightness_medium
+ : Icons.brightness_high,
+ color: const Color(0xFFFFFFFF),
+ size: 24.0,
+ ),
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: Text(
+ '${(_brightnessValue * 100.0).round()}%',
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 14.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ ],
+ ),
+ ),
+ ),
+ ),
+ // Speedup Indicator.
+ IgnorePointer(
+ child: Padding(
+ padding: _theme(context).padding ??
+ (
+ // Add padding in fullscreen!
+ isFullscreen(context) ? MediaQuery.of(context).padding : EdgeInsets.zero),
+ child: Column(
+ children: [
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).topButtonBarMargin,
+ ),
+ Expanded(
+ child: AnimatedOpacity(
+ duration: _theme(context).controlsTransitionDuration,
+ opacity: _speedUpIndicator ? 1 : 0,
+ child: _theme(context).speedUpIndicatorBuilder?.call(context, _theme(context).speedUpFactor) ??
+ Container(
+ alignment: Alignment.topCenter,
+ child: Container(
+ margin: const EdgeInsets.all(16.0),
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: const Color(0x88000000),
+ borderRadius: BorderRadius.circular(64.0),
+ ),
+ height: 48.0,
+ width: 108.0,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(width: 16.0),
+ Expanded(
+ child: Text(
+ '${_theme(context).speedUpFactor.toStringAsFixed(1)}x',
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 14.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ),
+ Container(
+ height: 48.0,
+ width: 48.0 - 16.0,
+ alignment: Alignment.centerRight,
+ child: const Icon(
+ Icons.fast_forward,
+ color: Color(0xFFFFFFFF),
+ size: 24.0,
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ Container(
+ height: _theme(context).buttonBarHeight,
+ margin: _theme(context).bottomButtonBarMargin,
+ ),
+ ],
+ ),
+ ),
+ ),
+ // Seek Indicator.
+ IgnorePointer(
+ child: AnimatedOpacity(
+ duration: _theme(context).controlsTransitionDuration,
+ opacity: showSwipeDuration ? 1 : 0,
+ child: _theme(context).seekIndicatorBuilder?.call(context, Duration(seconds: swipeDuration)) ??
+ Container(
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: const Color(0x88000000),
+ borderRadius: BorderRadius.circular(64.0),
+ ),
+ height: 52.0,
+ width: 108.0,
+ child: Text(
+ swipeDuration > 0 ? "+ ${Duration(seconds: swipeDuration).label()}" : "- ${Duration(seconds: swipeDuration).label()}",
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 14.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ // Double-Tap Seek Button(s):
+ if (!mount || seekOnDoubleTapEnabledWhileControlsAreVisible)
+ if (_mountSeekBackwardButton || _mountSeekForwardButton)
+ Positioned.fill(
+ child: Row(
+ children: [
+ Expanded(
+ flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[0],
+ child: _mountSeekBackwardButton
+ ? AnimatedOpacity(
+ opacity: _hideSeekBackwardButton ? 0 : 1.0,
+ duration: const Duration(milliseconds: 200),
+ child: _BackwardSeekIndicator(
+ duration: _theme(context).seekOnDoubleTapBackwardDuration,
+ onChanged: (value) {
+ _seekBarDeltaValueNotifier.value = -value;
+ },
+ onSubmitted: (value) {
+ _timerSeekBackwardButton?.cancel();
+ _timerSeekBackwardButton = Timer(
+ const Duration(milliseconds: 200),
+ () {
+ setState(() {
+ _hideSeekBackwardButton = false;
+ _mountSeekBackwardButton = false;
+ });
+ },
+ );
+
+ setState(() {
+ _hideSeekBackwardButton = true;
+ });
+ var result = controller(context).player.state.position - value;
+ result = result.clamp(
+ Duration.zero,
+ controller(context).player.state.duration,
+ );
+ controller(context).player.seek(result);
+ },
+ ),
+ )
+ : const SizedBox(),
+ ),
+ //Area in the middle where the double-tap seek buttons are ignored in
+ if (_theme(context).seekOnDoubleTapLayoutWidgetRatios[1] > 0)
+ Spacer(
+ flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[1],
+ ),
+ Expanded(
+ flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[2],
+ child: _mountSeekForwardButton
+ ? AnimatedOpacity(
+ opacity: _hideSeekForwardButton ? 0 : 1.0,
+ duration: const Duration(milliseconds: 200),
+ child: _ForwardSeekIndicator(
+ duration: _theme(context).seekOnDoubleTapForwardDuration,
+ onChanged: (value) {
+ _seekBarDeltaValueNotifier.value = value;
+ },
+ onSubmitted: (value) {
+ _timerSeekForwardButton?.cancel();
+ _timerSeekForwardButton = Timer(const Duration(milliseconds: 200), () {
+ if (_hideSeekForwardButton) {
+ setState(() {
+ _hideSeekForwardButton = false;
+ _mountSeekForwardButton = false;
+ });
+ }
+ });
+ setState(() {
+ _hideSeekForwardButton = true;
+ });
+
+ var result = controller(context).player.state.position + value;
+ result = result.clamp(
+ Duration.zero,
+ controller(context).player.state.duration,
+ );
+ controller(context).player.seek(result);
+ },
+ ),
+ )
+ : const SizedBox(),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ double widgetWidth(BuildContext context) => (context.findRenderObject() as RenderBox).paintBounds.width;
+}
+
+// SEEK BAR
+
+/// Material design seek bar.
+class MaterialSeekBar extends StatefulWidget {
+ final ValueNotifier? delta;
+ final VoidCallback? onSeekStart;
+ final VoidCallback? onSeekEnd;
+
+ const MaterialSeekBar({
+ super.key,
+ this.delta,
+ this.onSeekStart,
+ this.onSeekEnd,
+ });
+
+ @override
+ MaterialSeekBarState createState() => MaterialSeekBarState();
+}
+
+class MaterialSeekBarState extends State {
+ bool tapped = false;
+ double slider = 0.0;
+
+ late bool playing = controller(context).player.state.playing;
+ late Duration position = controller(context).player.state.position;
+ late Duration duration = controller(context).player.state.duration;
+ late Duration buffer = controller(context).player.state.buffer;
+
+ final List subscriptions = [];
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ void listener() {
+ setState(() {
+ final delta = widget.delta?.value ?? Duration.zero;
+ position = controller(context).player.state.position + delta;
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ widget.delta?.addListener(listener);
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (subscriptions.isEmpty && widget.delta == null) {
+ subscriptions.addAll(
+ [
+ controller(context).player.stream.playing.listen((event) {
+ setState(() {
+ playing = event;
+ });
+ }),
+ controller(context).player.stream.completed.listen((event) {
+ setState(() {
+ position = Duration.zero;
+ });
+ }),
+ controller(context).player.stream.position.listen((event) {
+ setState(() {
+ if (!tapped) {
+ position = event;
+ }
+ });
+ }),
+ controller(context).player.stream.duration.listen((event) {
+ setState(() {
+ duration = event;
+ });
+ }),
+ controller(context).player.stream.buffer.listen((event) {
+ setState(() {
+ buffer = event;
+ });
+ }),
+ ],
+ );
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.delta?.removeListener(listener);
+ for (final subscription in subscriptions) {
+ subscription.cancel();
+ }
+ super.dispose();
+ }
+
+ void onPointerMove(PointerMoveEvent e, BoxConstraints constraints) {
+ final percent = e.localPosition.dx / constraints.maxWidth;
+ setState(() {
+ tapped = true;
+ slider = percent.clamp(0.0, 1.0);
+ });
+ controller(context).player.seek(duration * slider);
+ }
+
+ void onPointerDown() {
+ widget.onSeekStart?.call();
+ setState(() {
+ tapped = true;
+ });
+ }
+
+ void onPointerUp() {
+ widget.onSeekEnd?.call();
+ setState(() {
+ // Explicitly set the position to prevent the slider from jumping.
+ tapped = false;
+ position = duration * slider;
+ });
+ controller(context).player.seek(duration * slider);
+ }
+
+ void onPanStart(DragStartDetails e, BoxConstraints constraints) {
+ final percent = e.localPosition.dx / constraints.maxWidth;
+ setState(() {
+ tapped = true;
+ slider = percent.clamp(0.0, 1.0);
+ });
+ }
+
+ void onPanDown(DragDownDetails e, BoxConstraints constraints) {
+ final percent = e.localPosition.dx / constraints.maxWidth;
+ setState(() {
+ tapped = true;
+ slider = percent.clamp(0.0, 1.0);
+ });
+ }
+
+ void onPanUpdate(DragUpdateDetails e, BoxConstraints constraints) {
+ final percent = e.localPosition.dx / constraints.maxWidth;
+ setState(() {
+ tapped = true;
+ slider = percent.clamp(0.0, 1.0);
+ });
+ }
+
+ /// Returns the current playback position in percentage.
+ double get positionPercent {
+ if (position == Duration.zero || duration == Duration.zero) {
+ return 0.0;
+ } else {
+ final value = position.inMilliseconds / duration.inMilliseconds;
+ return value.clamp(0.0, 1.0);
+ }
+ }
+
+ /// Returns the current playback buffer position in percentage.
+ double get bufferPercent {
+ if (buffer == Duration.zero || duration == Duration.zero) {
+ return 0.0;
+ } else {
+ final value = buffer.inMilliseconds / duration.inMilliseconds;
+ return value.clamp(0.0, 1.0);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ clipBehavior: Clip.none,
+ margin: _theme(context).seekBarMargin,
+ child: LayoutBuilder(
+ builder: (context, constraints) => MouseRegion(
+ cursor: SystemMouseCursors.click,
+ child: GestureDetector(
+ onHorizontalDragUpdate: (_) {},
+ onPanStart: (e) => onPanStart(e, constraints),
+ onPanDown: (e) => onPanDown(e, constraints),
+ onPanUpdate: (e) => onPanUpdate(e, constraints),
+ child: Listener(
+ onPointerMove: (e) => onPointerMove(e, constraints),
+ onPointerDown: (e) => onPointerDown(),
+ onPointerUp: (e) => onPointerUp(),
+ child: Container(
+ color: Colors.transparent,
+ width: constraints.maxWidth,
+ alignment: _theme(context).seekBarAlignment,
+ height: _theme(context).seekBarContainerHeight,
+ child: Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.bottomCenter,
+ children: [
+ Container(
+ width: constraints.maxWidth,
+ height: _theme(context).seekBarHeight,
+ alignment: Alignment.bottomLeft,
+ color: _theme(context).seekBarColor,
+ child: Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.bottomLeft,
+ children: [
+ Container(
+ width: constraints.maxWidth * bufferPercent,
+ color: _theme(context).seekBarBufferColor,
+ ),
+ Container(
+ width: tapped ? constraints.maxWidth * slider : constraints.maxWidth * positionPercent,
+ color: _theme(context).seekBarPositionColor,
+ ),
+ ],
+ ),
+ ),
+ Positioned(
+ left: tapped
+ ? (constraints.maxWidth - _theme(context).seekBarThumbSize / 2) * slider
+ : (constraints.maxWidth - _theme(context).seekBarThumbSize / 2) * positionPercent,
+ bottom: -1.0 * _theme(context).seekBarThumbSize / 2 + _theme(context).seekBarHeight / 2,
+ child: Container(
+ width: _theme(context).seekBarThumbSize,
+ height: _theme(context).seekBarThumbSize,
+ decoration: BoxDecoration(
+ color: _theme(context).seekBarThumbColor,
+ borderRadius: BorderRadius.circular(
+ _theme(context).seekBarThumbSize / 2,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// BUTTON: PLAY/PAUSE
+
+/// A material design play/pause button.
+class MaterialPlayOrPauseButton extends StatefulWidget {
+ /// Overriden icon size for [MaterialSkipPreviousButton].
+ final double? iconSize;
+
+ /// Overriden icon color for [MaterialSkipPreviousButton].
+ final Color? iconColor;
+
+ const MaterialPlayOrPauseButton({
+ super.key,
+ this.iconSize,
+ this.iconColor,
+ });
+
+ @override
+ MaterialPlayOrPauseButtonState createState() => MaterialPlayOrPauseButtonState();
+}
+
+class MaterialPlayOrPauseButtonState extends State with SingleTickerProviderStateMixin {
+ late final animation = AnimationController(
+ vsync: this,
+ value: controller(context).player.state.playing ? 1 : 0,
+ duration: const Duration(milliseconds: 200),
+ );
+
+ StreamSubscription? subscription;
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ subscription ??= controller(context).player.stream.playing.listen((event) {
+ if (event) {
+ animation.forward();
+ } else {
+ animation.reverse();
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ animation.dispose();
+ subscription?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return IconButton(
+ onPressed: controller(context).player.playOrPause,
+ iconSize: widget.iconSize ?? _theme(context).buttonBarButtonSize,
+ color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
+ icon: IgnorePointer(
+ child: AnimatedIcon(
+ progress: animation,
+ icon: AnimatedIcons.play_pause,
+ size: widget.iconSize ?? _theme(context).buttonBarButtonSize,
+ color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
+ ),
+ ),
+ );
+ }
+}
+
+// BUTTON: SKIP NEXT
+
+/// Material design skip next button.
+class MaterialSkipNextButton extends StatelessWidget {
+ /// Icon for [MaterialSkipNextButton].
+ final Widget? icon;
+
+ /// Overriden icon size for [MaterialSkipNextButton].
+ final double? iconSize;
+
+ /// Overriden icon color for [MaterialSkipNextButton].
+ final Color? iconColor;
+
+ const MaterialSkipNextButton({
+ super.key,
+ this.icon,
+ this.iconSize,
+ this.iconColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (!_theme(context).automaticallyImplySkipNextButton ||
+ (controller(context).player.state.playlist.medias.length > 1 && _theme(context).automaticallyImplySkipNextButton)) {
+ return IconButton(
+ onPressed: controller(context).player.next,
+ icon: icon ?? const Icon(Icons.skip_next),
+ iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
+ color: iconColor ?? _theme(context).buttonBarButtonColor,
+ );
+ }
+ return const SizedBox.shrink();
+ }
+}
+
+// BUTTON: SKIP PREVIOUS
+
+/// Material design skip previous button.
+class MaterialSkipPreviousButton extends StatelessWidget {
+ /// Icon for [MaterialSkipPreviousButton].
+ final Widget? icon;
+
+ /// Overriden icon size for [MaterialSkipPreviousButton].
+ final double? iconSize;
+
+ /// Overriden icon color for [MaterialSkipPreviousButton].
+ final Color? iconColor;
+
+ const MaterialSkipPreviousButton({
+ super.key,
+ this.icon,
+ this.iconSize,
+ this.iconColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (!_theme(context).automaticallyImplySkipPreviousButton ||
+ (controller(context).player.state.playlist.medias.length > 1 && _theme(context).automaticallyImplySkipPreviousButton)) {
+ return IconButton(
+ onPressed: controller(context).player.previous,
+ icon: icon ?? const Icon(Icons.skip_previous),
+ iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
+ color: iconColor ?? _theme(context).buttonBarButtonColor,
+ );
+ }
+ return const SizedBox.shrink();
+ }
+}
+
+// BUTTON: FULL SCREEN
+
+/// Material design fullscreen button.
+class MaterialFullscreenButton extends StatelessWidget {
+ /// Icon for [MaterialFullscreenButton].
+ final Widget? icon;
+
+ /// Overriden icon size for [MaterialFullscreenButton].
+ final double? iconSize;
+
+ /// Overriden icon color for [MaterialFullscreenButton].
+ final Color? iconColor;
+
+ const MaterialFullscreenButton({
+ super.key,
+ this.icon,
+ this.iconSize,
+ this.iconColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return IconButton(
+ onPressed: () => toggleFullscreen(context),
+ icon: icon ?? (isFullscreen(context) ? const Icon(Icons.fullscreen_exit) : const Icon(Icons.fullscreen)),
+ iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
+ color: iconColor ?? _theme(context).buttonBarButtonColor,
+ );
+ }
+}
+
+// BUTTON: CUSTOM
+
+/// Material design custom button.
+class MaterialCustomButton extends StatelessWidget {
+ /// Icon for [MaterialCustomButton].
+ final Widget? icon;
+
+ /// Icon size for [MaterialCustomButton].
+ final double? iconSize;
+
+ /// Icon color for [MaterialCustomButton].
+ final Color? iconColor;
+
+ /// The callback that is called when the button is tapped or otherwise activated.
+ final VoidCallback onPressed;
+
+ const MaterialCustomButton({
+ super.key,
+ this.icon,
+ this.iconSize,
+ this.iconColor,
+ required this.onPressed,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return IconButton(
+ onPressed: onPressed,
+ icon: icon ?? const Icon(Icons.settings),
+ padding: EdgeInsets.zero,
+ iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
+ color: iconColor ?? _theme(context).buttonBarButtonColor,
+ );
+ }
+}
+
+// POSITION INDICATOR
+
+/// Material design position indicator.
+class MaterialPositionIndicator extends StatefulWidget {
+ /// Overriden [TextStyle] for the [MaterialPositionIndicator].
+ final TextStyle? style;
+ const MaterialPositionIndicator({super.key, this.style});
+
+ @override
+ MaterialPositionIndicatorState createState() => MaterialPositionIndicatorState();
+}
+
+class MaterialPositionIndicatorState extends State {
+ late Duration position = controller(context).player.state.position;
+ late Duration duration = controller(context).player.state.duration;
+
+ final List subscriptions = [];
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (subscriptions.isEmpty) {
+ subscriptions.addAll(
+ [
+ controller(context).player.stream.position.listen((event) {
+ setState(() {
+ position = event;
+ });
+ }),
+ controller(context).player.stream.duration.listen((event) {
+ setState(() {
+ duration = event;
+ });
+ }),
+ ],
+ );
+ }
+ }
+
+ @override
+ void dispose() {
+ for (final subscription in subscriptions) {
+ subscription.cancel();
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Text(
+ '${position.label(reference: duration)} / ${duration.label(reference: duration)}',
+ style: widget.style ??
+ TextStyle(
+ height: 1.0,
+ fontSize: 12.0,
+ color: _theme(context).buttonBarButtonColor,
+ ),
+ );
+ }
+}
+
+class _BackwardSeekIndicator extends StatefulWidget {
+ final Duration duration;
+ final void Function(Duration) onChanged;
+ final void Function(Duration) onSubmitted;
+ const _BackwardSeekIndicator({
+ super.key,
+ required this.duration,
+ required this.onChanged,
+ required this.onSubmitted,
+ });
+
+ @override
+ State<_BackwardSeekIndicator> createState() => _BackwardSeekIndicatorState();
+}
+
+class _BackwardSeekIndicatorState extends State<_BackwardSeekIndicator> {
+ late Duration value = widget.duration;
+
+ Timer? timer;
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ timer = Timer(const Duration(milliseconds: 400), () {
+ widget.onSubmitted.call(value);
+ });
+ }
+
+ void increment() {
+ timer?.cancel();
+ timer = Timer(const Duration(milliseconds: 400), () {
+ widget.onSubmitted.call(value);
+ });
+ widget.onChanged.call(value);
+ setState(() {
+ value += const Duration(seconds: 10);
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ decoration: const BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Color(0x88767676),
+ Color(0x00767676),
+ ],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ child: InkWell(
+ splashColor: const Color(0x44767676),
+ onTap: increment,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.fast_rewind,
+ size: 24.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ const SizedBox(height: 8.0),
+ Text(
+ '${value.inSeconds} seconds',
+ style: const TextStyle(
+ fontSize: 12.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _ForwardSeekIndicator extends StatefulWidget {
+ final Duration duration;
+ final void Function(Duration) onChanged;
+ final void Function(Duration) onSubmitted;
+ const _ForwardSeekIndicator({
+ super.key,
+ required this.duration,
+ required this.onChanged,
+ required this.onSubmitted,
+ });
+
+ @override
+ State<_ForwardSeekIndicator> createState() => _ForwardSeekIndicatorState();
+}
+
+class _ForwardSeekIndicatorState extends State<_ForwardSeekIndicator> {
+ late Duration value = widget.duration;
+
+ Timer? timer;
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ timer = Timer(const Duration(milliseconds: 400), () {
+ widget.onSubmitted.call(value);
+ });
+ }
+
+ void increment() {
+ timer?.cancel();
+ timer = Timer(const Duration(milliseconds: 400), () {
+ widget.onSubmitted.call(value);
+ });
+ widget.onChanged.call(value);
+ setState(() {
+ value += const Duration(seconds: 10);
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ decoration: const BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Color(0x00767676),
+ Color(0x88767676),
+ ],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ child: InkWell(
+ splashColor: const Color(0x44767676),
+ onTap: increment,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.fast_forward,
+ size: 24.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ const SizedBox(height: 8.0),
+ Text(
+ '${value.inSeconds} seconds',
+ style: const TextStyle(
+ fontSize: 12.0,
+ color: Color(0xFFFFFFFF),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/controller/shop_index_controller.dart b/lib/controller/shop_index_controller.dart
new file mode 100644
index 0000000..17667ce
--- /dev/null
+++ b/lib/controller/shop_index_controller.dart
@@ -0,0 +1,145 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:loopin/IM/im_service.dart';
+import 'package:loopin/api/shop_api.dart';
+import 'package:loopin/service/http.dart';
+
+/// 单个 Tab 的状态
+class TabState {
+ final ScrollController scrollController;
+ final RxInt currentPage;
+ final RxDouble scrollOffset;
+ final RxList dataList;
+ final RxBool isLoading;
+ final RxBool hasLoaded;
+
+ TabState({
+ required this.scrollController,
+ required this.currentPage,
+ required this.scrollOffset,
+ required this.dataList,
+ required this.isLoading,
+ required this.hasLoaded,
+ });
+}
+
+class ShopIndexController extends GetxController with GetSingleTickerProviderStateMixin {
+ TabController? tabController;
+
+ ///轮播图数据
+ RxList swiperData = [].obs;
+
+ /// tab 分类列表
+ RxList tabList = [].obs;
+
+ /// 每个 tab 对应的状态
+ final Map tabs = {};
+
+ /// 当前 tab index
+ RxInt currentTabIndex = 0.obs;
+
+ /// 初始化 Tab 分类
+ void initTabs() async {
+ // 释放旧的 ScrollController
+ tabs.forEach((_, state) => state.scrollController.dispose());
+ tabs.clear();
+ tabList.clear();
+ // 先释放旧 TabController(并移除监听)
+ tabController?.removeListener(_tabListener);
+ tabController?.dispose();
+
+ // 赋值 tab 数据
+ final res = await Http.post(ShopApi.shopCategory, data: {
+ 'showStatus': 1,
+ });
+ final data = res['data']['records'] as List;
+ logger.w(data);
+ tabList.addAll(data);
+
+ // 初始化每个 tab 的状态
+ for (int i = 0; i < tabList.length; i++) {
+ final controller = ScrollController();
+ tabs[i] = TabState(
+ scrollController: controller,
+ currentPage: 1.obs,
+ scrollOffset: 0.0.obs,
+ dataList: [].obs,
+ isLoading: false.obs,
+ hasLoaded: false.obs,
+ );
+ }
+
+ // 创建新的 TabController
+ tabController = TabController(length: tabList.length, vsync: this);
+ tabController!.addListener(_tabListener);
+ // 初始化第一个 tab 的数据
+ if (tabList.isNotEmpty) {
+ loadSwiperData();
+ loadData(0);
+ }
+ }
+
+ /// Tab 切换监听
+ void _tabListener() {
+ if (!tabController!.indexIsChanging) {
+ currentTabIndex.value = tabController!.index;
+
+ final tab = tabs[currentTabIndex.value];
+ if (tab != null && !tab.hasLoaded.value) {
+ loadData(currentTabIndex.value);
+ }
+ }
+ }
+
+ Future refreshData(int index) async {
+ await loadSwiperData();
+ final tab = tabs[index];
+ if (tab == null) return;
+
+ tab.currentPage.value = 1;
+ tab.dataList.clear();
+ tab.isLoading.value = false;
+ tab.hasLoaded.value = false;
+
+ await loadData(index);
+ }
+
+ /// 加载pageview数据
+ Future loadData(int index) async {
+ final tab = tabs[index];
+ if (tab == null || tab.isLoading.value) return;
+
+ tab.isLoading.value = true;
+ final res = await Http.post(ShopApi.shopList, data: {
+ 'size': 10,
+ 'current': tab.currentPage.value,
+ 'categoryId': tabList[index]['id'],
+ });
+
+ final data = res['data']['records'];
+ tab.dataList.addAll(data);
+ logger.w(res);
+
+ tab.currentPage.value += 1;
+ tab.isLoading.value = false;
+ tab.hasLoaded.value = true;
+ }
+
+ /// 加载swiper数据
+ Future loadSwiperData() async {
+ final res = await Http.post(ShopApi.shopSwiperList, data: {
+ 'type': 1,
+ });
+ final data = res['data'];
+ logger.w(res);
+ swiperData.assignAll(data);
+ }
+
+ @override
+ void onClose() {
+ tabController?.removeListener(_tabListener);
+ tabController?.dispose();
+ tabs.forEach((_, state) => state.scrollController.dispose());
+ super.onClose();
+ }
+}
diff --git a/lib/layouts/index.dart b/lib/layouts/index.dart
index 0d00390..17cff8a 100644
--- a/lib/layouts/index.dart
+++ b/lib/layouts/index.dart
@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/chat_controller.dart';
import 'package:loopin/IM/controller/tab_bar_controller.dart';
-import 'package:loopin/IM/im_service.dart';
import 'package:loopin/models/tab_type.dart';
import 'package:loopin/pages/video/module/recommend.dart';
import 'package:loopin/update/upgrade_service.dart';
@@ -38,7 +37,7 @@ class _LayoutState extends State {
// tabs选项
List navItems = [
BottomNavigationBarItem(icon: Icon(Icons.play_circle_outline), label: '视频'),
- BottomNavigationBarItem(icon: Icon(Icons.local_mall), label: '团购'),
+ BottomNavigationBarItem(icon: Icon(Icons.local_mall), label: '易选'),
BottomNavigationBarItem(
icon: Icon(
Icons.camera_alt_rounded,
@@ -79,7 +78,7 @@ class _LayoutState extends State {
super.initState();
// 页面初始化后检查版本更新
WidgetsBinding.instance.addPostFrameCallback((_) {
- UpgradeService.checkUpgrade(context);
+ UpgradeService.checkUpgrade(this);
});
}
@@ -209,7 +208,7 @@ class _LayoutState extends State {
// 点击底部导航
void onTabTap(int index) {
- logger.i(index);
+ // logger.i(index);
if (index == 0) {
if (videoModuleController.videoTabIndex.value == 2) {
RecommendModule.playVideo();
@@ -223,7 +222,10 @@ class _LayoutState extends State {
}
if (index == 3) {
// 更新会话列表
- Get.find().getConversationList();
+ final ctl = Get.find();
+ if (ctl.chatList.isEmpty) {
+ Get.find().getConversationList();
+ }
}
if (index == 4) {
myPageKey.currentState?.refreshData();
diff --git a/lib/main.dart b/lib/main.dart
index 15593e0..4d8de78 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -28,6 +28,7 @@ void main() async {
Get.put(TabBarController());
// 注入会话列表
Get.put(ChatController());
+
// 监听app前后台状态
WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding.instance.addObserver(LifecycleHandler());
@@ -52,13 +53,23 @@ void main() async {
// 检测登录状态
if (Common.isLogin()) {
// 初始化Im,并进行Im登录
- await im_core.ImCore.init(sdkAppId: 1600080789);
- // 缺少userSig先用固定值ios用1587,安卓用188
- await ImService.instance.login(userID: Storage.read('userId'), userSig: Storage.read('userSig'));
- // String userId = '1909990634551795712'; //15877777777
- // String userId = '18832510385';
+ final res = await im_core.ImCore.init(sdkAppId: 1600080789);
+
+ // 缺少userSig先用固定值ios用9365,安卓用0385
+ try {
+ if (res) {
+ await ImService.instance.login(userID: Storage.read('userId'), userSig: Storage.read('userSig'));
+ } else {
+ logger.w('初始化未完成');
+ }
+ } catch (e) {
+ logger.w(e.toString());
+ Common.logout();
+ }
+ // String userId = '1940667704585248769'; //13212279365
+ // String userId = '1943510443312078850'; //18832510385
// String userSig =
- // 'eJwtzcsOgjAQBdB-6dqQKThth8QdG*JrIRHjTqGYiagNDzUx-rsVWN5zJ3M-IlvtgqdtRCzCAMRsyFzae8cVDywJiAhUNEeUmlDLcDpry*vJOS5FLBUAGNCGxsa*HTfWOyKGvhq149vftH8DhHLSli9*Jdrk2hqTVgdK2dKxXSfKbYuXpnPSu1zl*0fd18skg2Ihvj*7ADL4';
+ // 'eJwtjcEKgkAURf9l1iFPm*e8EdoYYUWFURAtg5nk5VRiEln0703q8p57Ofcj9qtd8LS1SEQUgBh1mY29NXzmDodaQhwrBRIJI0kq1sPsYcpTVbERSRgDAIEi3Tf2VXFtPUfEyFc9bfj6ZwrH4J1Ig4UL-6LX0ihyS7U5bi-Wzd8LzrK8TFs6TJ1sZwWGxlGas71PxPcHwH4y9Q__';
// 'eJwtzLEKwjAUheF3ySwlNzXNbcHFxSIOaqTWUUgsF1FDG2tEfHdj2-F8P5wPO2x00tuWFUwknM2GTcbePV1oYEBMhQSeopxyZ65n58iwAjLOOXKF*VhscNTa6FJKEdOonm5-UxJQpZhN2lET3599Xllbv9ZBH2uHuDfvst5tG6FX0EFYVhpOpZ973z8W7PsDmYwyIw__';
// await ImService.instance.login(userID: userId, userSig: userSig);
}
diff --git a/lib/models/conversation_type.dart b/lib/models/conversation_type.dart
new file mode 100644
index 0000000..e8e286a
--- /dev/null
+++ b/lib/models/conversation_type.dart
@@ -0,0 +1,48 @@
+/// 枚举定义:所有的会话类型分组,用于一级消息分类
+enum ConversationType {
+ noFriend, // 陌生人消息
+ system, //系统消息
+ newFoucs, //新的关注
+ interaction, //互动
+ order, //订单类通知消息
+ groupNotify, //群通知
+}
+
+extension ConversationTypeExtension on ConversationType {
+ String get name {
+ switch (this) {
+ case ConversationType.noFriend:
+ return 'noFriend';
+ case ConversationType.system:
+ return 'system';
+ case ConversationType.newFoucs:
+ return 'newFoucs';
+ case ConversationType.interaction:
+ return 'interaction';
+ case ConversationType.order:
+ return 'order';
+ case ConversationType.groupNotify:
+ return 'groupNotify';
+ }
+ }
+}
+
+conversationTypeFromString(String? type) {
+ if (type == null) return null;
+
+ if (type.contains('noFriend')) {
+ return ConversationType.noFriend.name;
+ } else if (type.contains('system')) {
+ return ConversationType.system.name;
+ } else if (type.contains('newFoucs')) {
+ return ConversationType.newFoucs.name;
+ } else if (type.contains('interaction')) {
+ return ConversationType.interaction.name;
+ } else if (type.contains('order')) {
+ return ConversationType.order.name;
+ } else if (type.contains('groupNotify')) {
+ return ConversationType.groupNotify.name;
+ }
+
+ return null;
+}
diff --git a/lib/models/conversation_view_model.dart b/lib/models/conversation_view_model.dart
index e420584..70f26c6 100644
--- a/lib/models/conversation_view_model.dart
+++ b/lib/models/conversation_view_model.dart
@@ -1,9 +1,9 @@
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
class ConversationViewModel {
- final V2TimConversation conversation;
- final String? faceUrl;
- final String isCustomAdmin;
+ late V2TimConversation conversation;
+ String? faceUrl;
+ String? isCustomAdmin;
ConversationViewModel({
required this.conversation,
diff --git a/lib/models/notify_message.type.dart b/lib/models/notify_message.type.dart
new file mode 100644
index 0000000..97ebfde
--- /dev/null
+++ b/lib/models/notify_message.type.dart
@@ -0,0 +1,97 @@
+/// 枚举定义:所有通知的二级类型
+enum NotifyMessageType {
+ newFoucs, //新的关注
+ systemNotify, // 系统->通知
+ systemReport, // 系统->举报下架(视频,视频评论)
+ systemCheck, // 系统->审核结果(复审,驳回 ,通过)
+ systemPush, //系统->推广
+ interactionComment, //互动->评论
+ interactionAt, //互动->视频评论中的@
+ interactionLike, //互动->点赞
+ interactionReply, //互动->评论回复
+ orderRecharge, //订单->充值 online
+ orderPay, //订单->订单交易成功通知 online
+ orderRefund, //订单->退款结果通知
+ groupNotifyCheck, //群通知->进群申请 online
+ groupNotifyAccpet, // 群通知->进群审核审核通过 online
+ groupNotifyFail, // 群通知->进群审核审核拒绝 online
+ groupNotifyLeaveUp, // 群通知->群升级为达人群通知
+}
+
+extension NotifyMessageTypeExtension on NotifyMessageType {
+ String get name {
+ switch (this) {
+ case NotifyMessageType.newFoucs:
+ return 'newFoucs';
+ case NotifyMessageType.systemNotify:
+ return 'systemNotify';
+ case NotifyMessageType.systemReport:
+ return 'systemReport';
+ case NotifyMessageType.systemCheck:
+ return 'systemCheck';
+ case NotifyMessageType.systemPush:
+ return 'systemPush';
+ case NotifyMessageType.interactionComment:
+ return 'interactionComment';
+ case NotifyMessageType.interactionAt:
+ return 'interactionAt';
+ case NotifyMessageType.interactionLike:
+ return 'interactionLike';
+ case NotifyMessageType.interactionReply:
+ return 'interactionReply';
+ case NotifyMessageType.orderRecharge:
+ return 'orderRecharge';
+ case NotifyMessageType.orderPay:
+ return 'orderPay';
+ case NotifyMessageType.orderRefund:
+ return 'orderRefund';
+ case NotifyMessageType.groupNotifyCheck:
+ return 'groupNotifyCheck';
+ case NotifyMessageType.groupNotifyAccpet:
+ return 'groupNotifyAccpet';
+ case NotifyMessageType.groupNotifyFail:
+ return 'groupNotifyFail';
+ case NotifyMessageType.groupNotifyLeaveUp:
+ return 'groupNotifyLeaveUp';
+ }
+ }
+}
+
+notifyMessageTypeFromString(String? type) {
+ switch (type) {
+ case 'newFoucs':
+ return NotifyMessageType.newFoucs.name;
+ case 'systemNotify':
+ return NotifyMessageType.systemNotify.name;
+ case 'systemReport':
+ return NotifyMessageType.systemReport.name;
+ case 'systemCheck':
+ return NotifyMessageType.systemCheck.name;
+ case 'systemPush':
+ return NotifyMessageType.systemPush.name;
+ case 'interactionComment':
+ return NotifyMessageType.interactionComment.name;
+ case 'interactionAt':
+ return NotifyMessageType.interactionAt.name;
+ case 'interactionLike':
+ return NotifyMessageType.interactionLike.name;
+ case 'interactionReply':
+ return NotifyMessageType.interactionReply.name;
+ case 'orderRecharge':
+ return NotifyMessageType.orderRecharge.name;
+ case 'orderPay':
+ return NotifyMessageType.orderPay.name;
+ case 'orderRefund':
+ return NotifyMessageType.orderRefund.name;
+ case 'groupNotifyCheck':
+ return NotifyMessageType.groupNotifyCheck.name;
+ case 'groupNotifyAccpet':
+ return NotifyMessageType.groupNotifyAccpet.name;
+ case 'groupNotifyFail':
+ return NotifyMessageType.groupNotifyFail.name;
+ case 'groupNotifyLeaveUp':
+ return NotifyMessageType.groupNotifyLeaveUp.name;
+ default:
+ return null;
+ }
+}
diff --git a/lib/models/summary_type.dart b/lib/models/summary_type.dart
new file mode 100644
index 0000000..15db9dc
--- /dev/null
+++ b/lib/models/summary_type.dart
@@ -0,0 +1,6 @@
+/// 枚举定义:自定义消息标签类型
+class SummaryType {
+ static const hongbao = 'hongbao';
+ static const shareVideo = 'shareVideo';
+ static const shareTuangou = 'shareTuangou';
+}
diff --git a/lib/pages/auth/login.dart b/lib/pages/auth/login.dart
index 5fa1c91..19dafc6 100644
--- a/lib/pages/auth/login.dart
+++ b/lib/pages/auth/login.dart
@@ -74,10 +74,10 @@ class _LoginState extends State {
// 初始化im_sdk
await im_core.ImCore.init(sdkAppId: 1600080789);
- // String userId = '1909990634551795712'; //15877777777
- // String userId = '18832510385';
+ // String userId = '1940667704585248769'; //13212279365
+ // String userId = '1943510443312078850'; //18832510385
// String userSig =
- // 'eJwtzcsOgjAQBdB-6dqQKThth8QdG*JrIRHjTqGYiagNDzUx-rsVWN5zJ3M-IlvtgqdtRCzCAMRsyFzae8cVDywJiAhUNEeUmlDLcDpry*vJOS5FLBUAGNCGxsa*HTfWOyKGvhq149vftH8DhHLSli9*Jdrk2hqTVgdK2dKxXSfKbYuXpnPSu1zl*0fd18skg2Ihvj*7ADL4';
+ // 'eJwtjcEKgkAURf9l1iFPm*e8EdoYYUWFURAtg5nk5VRiEln0703q8p57Ofcj9qtd8LS1SEQUgBh1mY29NXzmDodaQhwrBRIJI0kq1sPsYcpTVbERSRgDAIEi3Tf2VXFtPUfEyFc9bfj6ZwrH4J1Ig4UL-6LX0ihyS7U5bi-Wzd8LzrK8TFs6TJ1sZwWGxlGas71PxPcHwH4y9Q__';
// 'eJwtzLEKwjAUheF3ySwlNzXNbcHFxSIOaqTWUUgsF1FDG2tEfHdj2-F8P5wPO2x00tuWFUwknM2GTcbePV1oYEBMhQSeopxyZ65n58iwAjLOOXKF*VhscNTa6FJKEdOonm5-UxJQpZhN2lET3599Xllbv9ZBH2uHuDfvst5tG6FX0EFYVhpOpZ973z8W7PsDmYwyIw__';
try {
@@ -87,13 +87,17 @@ class _LoginState extends State {
if (loginRes.success) {
// 存储登录信息
Storage.write('hasLogged', true);
- // Storage.write('userSig', userSig);
+ Storage.write('userSig', userSig);
Storage.write('userId', userId);
- // Storage.write('token', obj['access_token']);
+ Storage.write('token', obj['access_token']);
+ // 获取用户账户信息
+ final accountRes = await Http.get('${CommonApi.accountInfo}/$userId');
+ logger.i(accountRes);
// 刷新短视频列表
final videoController = Get.find();
videoController.markNeedRefresh();
dialogController.close();
+
Get.back();
}
} catch (e) {
@@ -126,7 +130,7 @@ class _LoginState extends State {
vcodeText = '获取验证码(${time--})';
} else {
vcodeText = '获取验证码';
- time = 6;
+ time = 60;
disabled = false;
timer.cancel();
}
diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart
index 7a9658d..3f3839a 100644
--- a/lib/pages/chat/chat.dart
+++ b/lib/pages/chat/chat.dart
@@ -1,18 +1,26 @@
/// 聊天模板
library;
+import 'dart:convert';
+
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:get/get.dart';
-import 'package:loopin/IM/controller/chat_controller.dart';
import 'package:loopin/IM/controller/chat_detail_controller.dart';
import 'package:loopin/IM/im_message.dart';
+import 'package:loopin/IM/im_result.dart';
import 'package:loopin/IM/im_service.dart';
+import 'package:loopin/components/network_or_asset_image.dart';
+import 'package:loopin/components/preview_video.dart';
+import 'package:loopin/models/summary_type.dart';
+import 'package:loopin/utils/audio_player_service.dart';
+import 'package:loopin/utils/snapshot.dart';
+import 'package:loopin/utils/voice_service.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
-import '../../behavior/custom_scroll_behavior.dart';
-import '../../components/image_group.dart';
import '../../styles/index.dart';
import '../../utils/index.dart';
import './components/redpacket.dart';
@@ -30,18 +38,16 @@ class Chat extends StatefulWidget {
class _ChatState extends State with SingleTickerProviderStateMixin {
late final ChatDetailController controller;
// 接收参数
- V2TimConversation arguments = Get.arguments;
+ late final Rx arguments;
late String selfUserId;
// 聊天消息模块
final bool isNeedScrollBottom = true;
- // final RxList chatList = [].obs;
-
bool isLoading = false; // 是否在加载中
bool hasMore = true; // 是否还有更多数据
- bool _throttleFlag = false; // 滚动节流锁
+ final RxBool _throttleFlag = false.obs; // 滚动节流锁
// 表情json
List emoJson = emotionData;
@@ -70,7 +76,9 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
];
// controller监听
- ScrollController chatController = ScrollController();
+ // ScrollController chatController = ScrollController();
+ late ScrollController chatController;
+
ScrollController emojController = ScrollController();
// 模拟开红包按钮动画
@@ -83,7 +91,10 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
- controller = Get.put(ChatDetailController(userID: arguments.userID ?? ''));
+ final arg = Get.arguments as V2TimConversation;
+ arguments = arg.obs;
+ controller = Get.find();
+ chatController = controller.chatController;
animController = AnimationController(
vsync: this,
@@ -103,18 +114,22 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
setState(() {
toolbarEnable = false;
});
- scrollToBottom();
+ controller.scrollToBottom();
}
});
// 滚动监听
+ // Future.delayed(Duration(milliseconds: 1000), () {
+
+ // });
chatController.addListener(() {
- if (_throttleFlag) return;
+ if (_throttleFlag.value) return;
if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) {
- _throttleFlag = true;
+ // if (chatController.position.pixels <= 50) {
+ _throttleFlag.value = true;
getMsgData().then((_) {
// 解锁
- Future.delayed(Duration(milliseconds: 300), () {
- _throttleFlag = false;
+ Future.delayed(Duration(milliseconds: 1000), () {
+ _throttleFlag.value = false;
});
});
}
@@ -126,16 +141,67 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
if (Get.isRegistered()) {
Get.delete();
}
- chatController.dispose();
emojController.dispose();
editorFocusNode.dispose();
animController.dispose();
super.dispose();
}
+ // 设置好友备注
+ void setRemark() async {
+ String remark = '';
+ await MyDialog.confirm(
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 8),
+ TextField(
+ onChanged: (value) => remark = value,
+ maxLength: 16,
+ maxLengthEnforcement: MaxLengthEnforcement.enforced, // 强制不能输入超过
+ decoration: InputDecoration(
+ hintText: '请输入备注',
+ filled: true,
+ fillColor: const Color(0xFFF5F5F5),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide(color: Color(0xFFBE4EFF), width: 1),
+ ),
+ ),
+ )
+ ],
+ ),
+ title: '设置备注',
+ buttonText: '确认',
+ cancelText: '取消',
+ onConfirm: () async {
+ // print('备注为:$remark');
+ final res = await ImService.instance.setFriendInfo(userID: arguments.value.userID!, friendRemark: remark);
+ if (res.success) {
+ // 刷新会话列表数据
+ // Get.find().getConversationList();
+ arguments.update((val) {
+ val?.showName = remark;
+ });
+ } else {
+ print(res.desc);
+ print(arguments.value.userID);
+ MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ }
+ return true;
+ },
+ );
+ }
+
void cleanUnRead() async {
- if ((arguments.unreadCount ?? 0) > 0) {
- final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.conversationID);
+ if ((arguments.value.unreadCount ?? 0) > 0) {
+ final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.value.conversationID);
if (!res.success) {
MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
@@ -156,18 +222,20 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
// 获取最旧一条消息作为游标
V2TimMessage? lastRealMsg;
+ // for (var msg in controller.chatList.reversed) {
for (var msg in controller.chatList.reversed) {
if (msg.localCustomData != 'time_label') {
lastRealMsg = msg;
break;
}
}
- final lastMsg = lastRealMsg ?? arguments.lastMessage; // 如果找不到,就用传入的参数
+ final lastMsg = lastRealMsg ?? arguments.value.lastMessage; // 如果找不到,就用传入的参数
+ print(lastMsg?.toLogString());
// final lastMsg = controller.chatList.isNotEmpty ? controller.chatList.last : arguments.lastMessage;
final res = await ImService.instance.getHistoryMessageList(
- userID: arguments.userID,
+ userID: arguments.value.userID,
lastMsg: lastMsg,
);
@@ -176,14 +244,26 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
if (newMessages.isEmpty) {
hasMore = false;
- MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
- } else {
- if (initFlag) {
- newMessages.insert(0, lastMsg!);
- }
- controller.updateChatListWithTimeLabels(newMessages);
- print('聊天数据加载成功');
+ // MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
+ if (initFlag && lastMsg != null) {
+ newMessages.insert(0, lastMsg);
+ // controller.scrollToBottom();
+ }
+ controller.updateChatListWithTimeLabels(newMessages);
+ if (initFlag) {
+ // 初始化时滚到最底部
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (chatController.hasClients) {
+ // controller.scrollToBottom();
+ // final bottomPadding = MediaQuery.of(context).padding.bottom; // 底部安全区域高度
+ // chatController.jumpTo(chatController.position.maxScrollExtent); // 60为底部操作栏高度
+ chatController.jumpTo(0);
+ }
+ });
+ }
+
+ print('聊天数据加载成功');
} else {
MyDialog.toast("获取聊天记录失败:${res.desc}", icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
@@ -210,31 +290,33 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
));
}
- // 文本消息模板
+ // 文本消息模板=1
else if (item.elemType == 1) {
- msgtpl.add(RenderChatItem(
- data: item,
- child: Ink(
- decoration: BoxDecoration(
- color: !(item.isSelf ?? false) ? Color(0xFFFFFFFF) : Color(0xFF89E45B),
- borderRadius: BorderRadius.circular(10.0),
- ),
- child: InkWell(
- overlayColor: WidgetStateProperty.all(Colors.transparent),
- borderRadius: BorderRadius.circular(10.0),
- child: Container(
- padding: const EdgeInsets.all(10.0),
- child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话
+ msgtpl.add(
+ RenderChatItem(
+ data: item,
+ child: Ink(
+ decoration: BoxDecoration(
+ color: !(item.isSelf ?? false) ? Color(0xFFFFFFFF) : Color(0xFF89E45B),
+ borderRadius: BorderRadius.circular(10.0),
+ ),
+ child: InkWell(
+ overlayColor: WidgetStateProperty.all(Colors.transparent),
+ borderRadius: BorderRadius.circular(10.0),
+ child: Container(
+ padding: const EdgeInsets.all(10.0),
+ child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话
+ ),
+ onLongPress: () {
+ contextMenuDialog();
+ },
),
- onLongPress: () {
- contextMenuDialog();
- },
),
),
- ));
+ );
}
- // gif表情模板
- else if (item.elemType == 4) {
+ // gif表情模板=8
+ else if (item.elemType == 8) {
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
@@ -245,7 +327,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
maxHeight: 100.0,
maxWidth: 100.0,
),
- child: Image.asset('assets/images/emotion/${item.customElem?.data}'),
+ // child: Image.asset('assets/images/emotion/${item.faceElem?.data}'),
+ child: Image.asset('${item.faceElem?.data}'),
),
onLongPress: () {
contextMenuDialog();
@@ -254,9 +337,11 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
));
}
- // 图片模板
- else if (item.elemType == 5) {
- List imagePaths = item.imageElem?.imageList?.where((e) => e != null && e.url != null).map((e) => e!.url!).toList() ?? [];
+ // 图片模板=3
+ else if (item.elemType == 3) {
+ // List imagePaths = item.imageElem?.imageList?.where((e) => e != null && e.url != null).map((e) => e!.url!).toList() ?? [];
+ final originImage = item.imageElem?.imageList?.firstWhere((e) => e?.type == 0 && e?.url != null, orElse: () => null);
+ List imagePaths = originImage != null ? [originImage.url!] : [];
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
@@ -264,9 +349,36 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
- child: ImageGroup(
- images: imagePaths,
+ // child: ImageGroup(
+ // images: imagePaths,
+ // width: 120,
+ // ),
+ child: Image.network(
+ imagePaths.first,
width: 120,
+ fit: BoxFit.cover,
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) {
+ // controller.scrollToBottom();
+ return child; // 加载完成,显示图片
+ }
+ return Container(
+ width: 120,
+ height: 240,
+ color: Colors.grey[300],
+ alignment: Alignment.center,
+ child: CircularProgressIndicator(
+ value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
+ ),
+ );
+ },
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ color: Colors.grey[300],
+ alignment: Alignment.center,
+ child: Icon(Icons.broken_image, color: Colors.grey, size: 40),
+ );
+ },
),
),
onLongPress: () {
@@ -276,22 +388,24 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
));
}
- // 视频模板
- else if (item.elemType == 6) {
+ // 视频模板=5
+ else if (item.elemType == 5) {
+ // print(item.videoElem!.toLogString());
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: SizedBox(
- width: 90.0,
+ width: 120.0,
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
- child: Image.network(
- item.videoElem?.videoUrl ?? '',
+ child: NetworkOrAssetImage(
+ imageUrl: item.videoElem?.snapshotUrl ?? '',
+ width: 120,
),
),
const Align(
@@ -306,7 +420,24 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
),
onTap: () {
- MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ showGeneralDialog(
+ context: context,
+ // barrierDismissible: true,
+ barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
+ pageBuilder: (_, __, ___) {
+ return SafeArea(
+ child: PreviewVideo(
+ videoUrl: item.videoElem?.videoUrl ?? '',
+ width: item.videoElem?.snapshotWidth?.toDouble(),
+ height: item.videoElem?.snapshotHeight?.toDouble(),
+ ),
+ );
+ },
+ transitionBuilder: (_, anim, __, child) {
+ return FadeTransition(opacity: anim, child: child);
+ },
+ transitionDuration: const Duration(milliseconds: 200),
+ );
},
onLongPress: () {
contextMenuDialog();
@@ -315,8 +446,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
));
}
- // 语音模板
- else if (item.elemType == 7) {
+ // 语音模板=4
+ else if (item.elemType == 4) {
+ final durationMs = item.soundElem?.duration ?? 0;
+ final durationSeconds = (durationMs / 1000).round();
+ final maxWidth = (durationSeconds / 60 * 230).clamp(80.0, 230.0);
+
List audiobody = [
Ink(
decoration: BoxDecoration(
@@ -329,8 +464,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
child: Container(
padding: const EdgeInsets.all(10.0),
constraints: BoxConstraints(
- // maxWidth: 120.0,
- maxWidth: (item.soundElem?.duration)! / 60 * 230,
+ maxWidth: maxWidth,
+ // maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230,
),
child: Row(
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
@@ -340,10 +475,10 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
const SizedBox(
width: 5.0,
),
- Text('${item.soundElem?.duration}'),
+ Text('$durationSeconds"'),
]
: [
- Text('${item.soundElem?.duration}'),
+ Text('$durationSeconds"'),
const SizedBox(
width: 5.0,
),
@@ -352,7 +487,15 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
),
onTap: () {
- MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ final locUrl = item.soundElem?.path ?? '';
+ final netUrl = item.soundElem?.url ?? '';
+ if (locUrl.isNotEmpty) {
+ AudioPlayerService().playNetwork(locUrl);
+ } else if (netUrl.isNotEmpty) {
+ AudioPlayerService().playLocal(netUrl);
+ } else {
+ MyDialog.toast('音频文件已过期', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ }
},
onLongPress: () {
contextMenuDialog();
@@ -362,7 +505,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
const SizedBox(
width: 5.0,
),
- FStyle.badge(0, isdot: true),
+
+ // FStyle.badge(0, isdot: true),
];
if (item.isSelf ?? false) {
@@ -379,8 +523,157 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
children: audiobody,
)));
}
- // 红包模板
- else if (item.elemType == 0 && item.customElem?.desc == 'hongbao') {
+ // 分享团购商品
+ else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) {
+ //price,title,url,sell
+ final obj = jsonDecode(item.customElem!.data!);
+ final url = obj['url'];
+ final title = obj['title'];
+ final price = obj['price'];
+ final sell = Utils().graceNumber(int.tryParse(obj['sell'])!);
+ msgtpl.add(RenderChatItem(
+ data: item,
+ child: GestureDetector(
+ child: Container(
+ width: 160,
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [
+ BoxShadow(
+ color: Colors.black.withAlpha(5),
+ offset: Offset(0.0, 1.0),
+ blurRadius: 1.0,
+ spreadRadius: 0.0,
+ ),
+ ]),
+ child: Column(
+ children: [
+ NetworkOrAssetImage(
+ imageUrl: url,
+ width: 160.0,
+ ),
+ Container(
+ padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 5.0,
+ children: [
+ Text(
+ '$title',
+ style: TextStyle(fontSize: 14.0, height: 1.2),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Flexible(
+ child: Text.rich(
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [
+ TextSpan(text: '¥'),
+ TextSpan(
+ text: '$price',
+ style: TextStyle(
+ fontSize: 14.0,
+ )),
+ ]),
+ ),
+ ),
+ SizedBox(
+ width: 5,
+ ),
+ Text(
+ '已售$sell件',
+ style: TextStyle(color: Colors.grey, fontSize: 10.0),
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+ ],
+ ),
+ ),
+ onTap: () {
+ // 这里带上分享人的ID
+ Get.toNamed('/goods');
+ },
+ ),
+ ));
+ }
+ // 分享短视频
+ else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareVideo) {
+ /// {imgUrl,videoUrl,width,height}
+ final obj = jsonDecode(item.customElem!.data!);
+ final videoUrl = obj['videoUrl'];
+ final imgUrl = obj['imgUrl'];
+ final width = obj['width'] as num;
+ final height = obj['height'] as num;
+ msgtpl.add(RenderChatItem(
+ data: item,
+ child: Ink(
+ child: InkWell(
+ overlayColor: WidgetStateProperty.all(Colors.transparent),
+ child: SizedBox(
+ width: 120.0,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(10.0),
+ child: NetworkOrAssetImage(
+ imageUrl: imgUrl,
+ width: 120,
+ ),
+ ),
+ const Align(
+ alignment: Alignment.center,
+ child: Icon(
+ Icons.play_circle,
+ color: Colors.white,
+ size: 30.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ onTap: () {
+ showGeneralDialog(
+ context: context,
+ barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
+ pageBuilder: (_, __, ___) {
+ return SafeArea(
+ bottom: true,
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: PreviewVideo(
+ videoUrl: videoUrl,
+ width: width.toDouble(),
+ height: height.toDouble(),
+ ),
+ ),
+ );
+ },
+ transitionBuilder: (_, anim, __, child) {
+ return FadeTransition(opacity: anim, child: child);
+ },
+ transitionDuration: const Duration(milliseconds: 200),
+ );
+ },
+ onLongPress: () {
+ contextMenuDialog();
+ },
+ ),
+ ),
+ ));
+ }
+ // 红包模板=自定义=2;
+ else if (item.elemType == 2 && item.cloudCustomData == SummaryType.hongbao) {
+ final obj = jsonDecode(item.customElem!.data!);
+ final open = obj['open'] ?? false;
+ final remark = obj['remark'];
+ // final maxNum = obj['maxNum'];
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
@@ -399,16 +692,26 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
+ width: 210.0,
padding: const EdgeInsets.all(10.0),
child: Row(
- spacing: 10.0,
children: [
- Image.asset(
- 'assets/images/hbico.png',
- width: 32.0,
- fit: BoxFit.contain,
+ open
+ ? Icon(Icons.check_circle, size: 32.0, color: Colors.white70)
+ : Image.asset(
+ 'assets/images/hbico.png',
+ width: 32.0,
+ fit: BoxFit.contain,
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ '$remark',
+ style: const TextStyle(color: Colors.white, fontSize: 14.0),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
),
- Text(item.customElem?.data ?? '', style: const TextStyle(color: Colors.white, fontSize: 14.0)),
],
),
),
@@ -418,7 +721,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
width: double.infinity,
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))),
child: const Text(
- '拼手气红包',
+ '红包',
style: TextStyle(color: Colors.white70, fontSize: 11.0),
),
),
@@ -435,8 +738,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
));
}
- // 位置模板
- else if (item.elemType == 9) {
+ // 位置模板=7
+ else if (item.elemType == 7) {
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
@@ -583,7 +886,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
child: Image.asset(emoj),
),
onTap: () {
- handleGIFClick(emoj);
+ handleGIFClick(emoj, item['index']);
},
),
);
@@ -663,17 +966,19 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
// chatController.animateTo(isNeedScrollBottom ? 0 : chatController.position.maxScrollExtent,
// duration: const Duration(milliseconds: 200), curve: Curves.easeIn);
// }
- void scrollToBottom() {
- Future.delayed(Duration(milliseconds: 100), () {
- if (chatController.hasClients) {
- chatController.animateTo(
- 0, // reverse: true 时滚动到底部是 offset: 0
- duration: Duration(milliseconds: 300),
- curve: Curves.easeOut,
- );
- }
- });
- }
+
+ // void scrollToBottom() {
+ // Future.delayed(Duration(milliseconds: 300), () {
+ // if (chatController.hasClients) {
+ // chatController.animateTo(
+ // // 0, // reverse: true 时滚动到底部是 offset: 0
+ // chatController.position.maxScrollExtent,
+ // duration: Duration(milliseconds: 300),
+ // curve: Curves.easeOut,
+ // );
+ // }
+ // });
+ // }
// 点击消息区域
void handleClickChatArea() {
@@ -745,20 +1050,22 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
}
// 不需要时间标签
- final res = await IMMessage().sendText(
- text: message['content'],
- toUserID: arguments.userID,
+ // 消息类型
+ late final ImResult res;
+ res = await IMMessage().sendMessage(
+ msg: message,
+ toUserID: arguments.value.userID,
);
if (res.success && res.data != null) {
messagesToInsert.insert(0, res.data); // 加入消息本体
+ // messagesToInsert.add(res.data); // 加入消息本体
controller.chatList.insertAll(0, messagesToInsert);
+ // controller.chatList.addAll(messagesToInsert);
- scrollToBottom();
+ controller.scrollToBottom();
print('发送成功');
- // 更新会话列表数据
- Get.find().getConversationList();
} else {
print('消息发送失败: ${res.code} - ${res.desc}');
}
@@ -783,7 +1090,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
toolbarIndex = index;
voiceBtnEnable = false;
});
- scrollToBottom();
+ controller.scrollToBottom();
}
// 表情Tab切换
@@ -799,51 +1106,107 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
emojController.jumpTo(0);
}
- // 点击表情
+ // 点击表情插入到输入框
void handleEmojClick(emoj) {
insertTextAtCursor(emoj);
}
- // 点击Gif大图
- void handleGIFClick(gifpath) {
+ // 点击Gif大图发送=8
+ void handleGIFClick(gifpath, index) async {
// 消息队列
- Map message = {
- 'id': Utils.uuid(),
- 'contentType': 4,
- 'isme': true,
- 'avatar': 'assets/images/avatar/img11.jpg',
- 'author': 'Andy',
- 'content': '',
- 'image': gifpath,
- 'video': '',
- };
- sendMessage(message);
+ // Map message = {
+ // 'contentType': 8,
+ // 'content': gifpath,
+ // };
+ final res = await IMMessage().createFaceMessage(data: gifpath, index: index);
+ if (res.success) {
+ sendMessage(res.data?.messageInfo);
+ }
}
- // 提交消息
- void handleSubmit() {
+ // 发送文本消息=1
+ void handleSubmit() async {
if (editorController.text.isEmpty) return;
// 消息队列
- Map message = {
- 'id': Utils.uuid(),
- 'contentType': 3,
- 'isme': true,
- 'avatar': 'assets/images/avatar/img11.jpg',
- 'author': 'Andy',
- 'content': editorController.text,
- 'image': '',
- 'video': '',
- };
- sendMessage(message);
- editorController.clear();
+ // Map message = {
+ // 'contentType': 1,
+ // 'content': editorController.text,
+ // };
+ final res = await IMMessage().createTextMessage(text: editorController.text);
+ if (res.success) {
+ sendMessage(res.data?.messageInfo);
+ editorController.clear();
+ }
}
- // 选择区操作
+ // 发红包消息
+ void sendHongbao(date) async {
+ final amount = date['amount']; //用户输入的金额
+ final remark = date['remark']; //用户输入的留言
+ final maxNum = date['maxNum']; //红包数量
+
+ // 先检测可用余额
+ final makeJson = jsonEncode({
+ "amount": amount,
+ "remark": remark,
+ "maxNum": maxNum,
+ "open": false,
+ });
+ final res = await IMMessage().createCustomMessage(data: makeJson);
+ if (res.success && (res.data != null)) {
+ final custMsg = res.data!.messageInfo;
+ custMsg!.cloudCustomData = SummaryType.hongbao;
+ sendMessage(res.data!.messageInfo);
+ Get.back();
+ }
+ }
+
+ // 发送图片消息=3
+ void sendImage(imgPath) async {
+ final resImg = await IMMessage().createImageMessage(imagePath: imgPath);
+ if (resImg.success) {
+ sendMessage(resImg.data?.messageInfo);
+ }
+ }
+
+ // 发送语音消息=4
+ void sendVoiceMsg() async {
+ final fileMap = await VoiceService().stopRecording();
+ if (fileMap != null) {
+ final res = await IMMessage().createSoundMessage(
+ soundPath: fileMap['path'],
+ duration: fileMap['duration'],
+ );
+ if (res.success && res.data != null) {
+ sendMessage(res.data!.messageInfo);
+ } else {
+ MyDialog.toast('创建语音消息失败');
+ }
+ } else {
+ MyDialog.toast('语音限制1-60秒');
+ }
+ }
+
+ // 发送视频消息=5
+ void sendVideo(videoFilePath, type, duration, snapshotPath) async {
+ final resImg = await IMMessage().createVideoMessage(
+ videoFilePath: videoFilePath,
+ type: type,
+ duration: duration,
+ snapshotPath: snapshotPath,
+ );
+ if (resImg.success) {
+ sendMessage(resImg.data?.messageInfo);
+ }
+ }
+
+ // 底部操作蓝选择区操作
void handleChooseAction(key) {
MyDialog.toast('$key');
switch (key) {
case 'photo':
// ....
+ pickFile(context);
break;
case 'camera':
// ....
@@ -854,12 +1217,97 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
}
}
+ ///从相册选取图片/视频
+ void pickFile(BuildContext context) async {
+ final pickedAssets = await AssetPicker.pickAssets(
+ context,
+ pickerConfig: AssetPickerConfig(
+ textDelegate: const AssetPickerTextDelegate(),
+ pathNameBuilder: (AssetPathEntity album) {
+ return Utils.translateAlbumName(album);
+ },
+ maxAssets: 5,
+ requestType: RequestType.common,
+ filterOptions: FilterOptionGroup(
+ imageOption: const FilterOption(),
+ videoOption: const FilterOption(
+ durationConstraint: DurationConstraint(
+ max: Duration(seconds: 120),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ if (pickedAssets != null && pickedAssets.isNotEmpty) {
+ for (final asset in pickedAssets) {
+ switch (asset.type) {
+ case AssetType.image:
+ print("选中了图片:${asset.title}");
+ var file = await asset.file;
+ if (file != null) {
+ var fileSizeInBytes = await file.length();
+ var sizeInMB = fileSizeInBytes / (1024 * 1024);
+ if (sizeInMB > 28) {
+ MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ } else {
+ print("图片合法,大小:$sizeInMB MB");
+ // 执行发送逻辑
+ sendImage(file.path);
+ }
+ }
+
+ break;
+ case AssetType.video:
+ print("选中了视频:${asset.title}");
+ var file = await asset.file;
+ if (file != null) {
+ var fileSizeInBytes = await file.length();
+ var sizeInMB = fileSizeInBytes / (1024 * 1024);
+ if (sizeInMB > 28) {
+ MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ } else {
+ print("图片合法,大小:$sizeInMB MB");
+ // 执行发送逻辑
+ var snapshot = await generateVideoThumbnail(file.path);
+ String? mimeType = await asset.mimeTypeAsync;
+ String vdType = mimeType?.split('/').last ?? 'mp4';
+ print(vdType);
+ sendVideo(file.path, vdType, asset.duration, snapshot);
+ }
+ }
+ break;
+ default:
+ print("不支持的类型:${asset.type}");
+ }
+ }
+ // final asset = pickedAssets.first;
+ // final file = await asset.file; // 获取实际文件
+ // if (file != null) {
+ // final fileSizeInBytes = await file.length();
+ // final sizeInMB = fileSizeInBytes / (1024 * 1024);
+ // if (sizeInMB > 100) {
+ // MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
+ // } else {
+ // print("图片合法,大小:$sizeInMB MB");
+ // //走upload(file)上传图片拿到url地址
+ // // file;
+ // }
+ // }
+ }
+ }
+
/* ---------- { 弹窗功能模块 } ---------- */
// 红包弹窗
- void receiveRedPacketDialog(data) {
+ void receiveRedPacketDialog(V2TimMessage data) {
showDialog(
context: context,
builder: (context) {
+ final obj = jsonDecode(data.customElem!.data!);
+ final amount = obj['amount'];
+ final remark = obj['remark'];
+ final open = obj['open'] ?? false;
+
return Material(
type: MaterialType.transparency,
child: Column(
@@ -868,7 +1316,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 50.0),
- padding: const EdgeInsets.symmetric(vertical: 50.0),
+ padding: const EdgeInsets.symmetric(vertical: 50.0, horizontal: 20.0),
decoration: const BoxDecoration(
color: Color(0xFFFF7F43),
borderRadius: BorderRadius.all(Radius.circular(12.0)),
@@ -877,54 +1325,70 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5.0),
- child: Image.asset(data['avatar'], height: 40.0, width: 40.0, fit: BoxFit.cover),
+ child: NetworkOrAssetImage(
+ imageUrl: data.senderProfile?.faceUrl,
+ ),
),
const SizedBox(
height: 5.0,
),
Text(
- data['author'],
+ '${data.senderProfile?.nickName}的红包',
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w600),
),
+ SizedBox(height: 10),
Text(
- data['content'],
+ amount,
+ style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0),
+ ),
+ SizedBox(height: 20.0),
+ Text(
+ remark,
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0),
),
SizedBox(
height: 100.0,
),
- AnimatedBuilder(
- animation: animTurns,
- builder: (context, child) {
- return Transform(
- transform: Matrix4.rotationY(animTurns.value),
- alignment: Alignment.center,
- child: FilledButton(
- style: ButtonStyle(
- backgroundColor: WidgetStateProperty.all(const Color(0xFFFFF9C7)),
- padding: WidgetStateProperty.all(EdgeInsets.zero),
- minimumSize: WidgetStateProperty.all(const Size(80.0, 80.0)),
- shape: WidgetStateProperty.all(const CircleBorder()),
- elevation: WidgetStateProperty.all(3.0),
+ if (open == false && data.isSelf == false)
+ AnimatedBuilder(
+ animation: animTurns,
+ builder: (context, child) {
+ return Transform(
+ transform: Matrix4.rotationY(animTurns.value),
+ alignment: Alignment.center,
+ child: FilledButton(
+ style: ButtonStyle(
+ backgroundColor: WidgetStateProperty.all(const Color(0xFFFFF9C7)),
+ padding: WidgetStateProperty.all(EdgeInsets.zero),
+ minimumSize: WidgetStateProperty.all(const Size(80.0, 80.0)),
+ shape: WidgetStateProperty.all(const CircleBorder()),
+ elevation: WidgetStateProperty.all(3.0),
+ ),
+ child: Text(
+ '开',
+ style: TextStyle(color: Color(0xFF3B3B3B), fontSize: 28.0),
+ ),
+ onPressed: () async {
+ // 点击开红包,开始动画
+ animController.repeat();
+ // 执行抢红包结果查询,(群)展示抢红包人员信息,单不用管
+ // 执行消费红包动作
+ //--------
+ // 成功后修改消息体
+ obj['open'] = true; //成功标记为true
+ data.customElem!.data = jsonEncode(obj);
+ ImService.instance.modifyMessage(message: data);
+ // 模拟开红包逻辑,1 秒后停止动画
+ Future.delayed(Duration(seconds: 1), () {
+ animController.stop();
+ animController.reset();
+ Get.back();
+ });
+ },
),
- child: Text(
- '開',
- style: TextStyle(color: Color(0xFF3B3B3B), fontSize: 28.0),
- ),
- onPressed: () {
- // 开始动画
- animController.repeat();
- // 模拟开红包逻辑,1 秒后停止动画
- Future.delayed(Duration(seconds: 1), () {
- animController.stop();
- animController.reset();
- Get.back();
- });
- },
- ),
- );
- },
- ),
+ );
+ },
+ ),
],
),
),
@@ -998,7 +1462,11 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
context: context,
builder: (context) {
- return const RedPacket();
+ return RedPacket(
+ flag: false,
+ onSend: (date) {
+ sendHongbao(date);
+ });
},
);
}
@@ -1013,6 +1481,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
// 页面主体(聊天消息区/底部操作区)
Scaffold(
backgroundColor: Colors.grey[200],
+ resizeToAvoidBottomInset: true, // 启用键盘自动避让
appBar: AppBar(
centerTitle: true,
backgroundColor: Colors.transparent,
@@ -1027,11 +1496,13 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
},
),
titleSpacing: 1.0,
- title: Text(
- // '${arguments['title']}',
- '${arguments.showName}',
- style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'),
- ),
+ title: Obx(() {
+ return Text(
+ // '${arguments['title']}',
+ '${arguments.value.showName}',
+ style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'),
+ );
+ }),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
@@ -1046,7 +1517,109 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
Icons.more_horiz,
color: Colors.white,
),
- onPressed: () {},
+ onPressed: () async {
+ final paddingTop = MediaQuery.of(Get.context!).padding.top;
+
+ final selected = await showMenu(
+ context: Get.context!,
+ position: RelativeRect.fromLTRB(
+ double.infinity,
+ kToolbarHeight + paddingTop - 12,
+ 8,
+ double.infinity,
+ ),
+ color: FStyle.primaryColor,
+ elevation: 8,
+ items: [
+ PopupMenuItem(
+ value: 'remark',
+ child: Row(
+ children: [
+ Icon(Icons.edit, color: Colors.white, size: 18),
+ SizedBox(width: 8),
+ Text(
+ '设置备注',
+ style: TextStyle(color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'not',
+ child: Row(
+ children: [
+ Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18),
+ SizedBox(width: 8),
+ Text(
+ '设为免打扰',
+ style: TextStyle(color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'report',
+ child: Row(
+ children: [
+ Icon(Icons.report, color: Colors.white, size: 18),
+ SizedBox(width: 8),
+ Text(
+ '举报',
+ style: TextStyle(color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'block',
+ child: Row(
+ children: [
+ Icon(Icons.block, color: Colors.white, size: 18),
+ SizedBox(width: 8),
+ Text(
+ '拉黑',
+ style: TextStyle(color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'foucs',
+ child: Row(
+ children: [
+ Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18),
+ SizedBox(width: 8),
+ Text(
+ '取消关注',
+ style: TextStyle(color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+
+ if (selected != null) {
+ switch (selected) {
+ case 'remark':
+ print('点击了备注');
+ setRemark();
+ break;
+ case 'not':
+ print('点击了免打扰');
+ break;
+ case 'report':
+ print('点击了举报');
+ break;
+ case 'block':
+ print('点击了拉黑');
+ break;
+ case 'foucs':
+ print('点击了取关');
+ break;
+ }
+ }
+ },
),
],
),
@@ -1056,19 +1629,31 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
children: [
// 渲染聊天消息
Expanded(
- child: ScrollConfiguration(
- behavior: CustomScrollBehavior(),
- child: GestureDetector(
- child: Obx(() {
- return ListView(
- controller: chatController,
- reverse: true,
- padding: const EdgeInsets.all(10.0),
- children: renderChatList(),
- );
- }),
- onTap: () {
- handleClickChatArea();
+ child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: handleClickChatArea,
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Obx(() {
+ final msgWidgets = renderChatList().reversed.toList();
+
+ return ListView(
+ controller: chatController,
+ reverse: true,
+ padding: const EdgeInsets.all(10.0),
+ children: [
+ ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 40,
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: msgWidgets,
+ ),
+ ),
+ ],
+ );
+ });
},
),
),
@@ -1076,201 +1661,210 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
// 底部操作栏
Container(
- color: Colors.grey[100],
- child: SafeArea(
- bottom: true,
- child: Container(
- decoration: BoxDecoration(
- color: Colors.grey[100],
- border: const Border(top: BorderSide(color: Colors.black38, width: .1)),
- ),
- child: Column(
- children: [
- // 输入框编辑器模块
- Container(
- padding: const EdgeInsets.all(10.0),
- child: Row(
- children: [
- InkWell(
- child: Icon(
- voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
- color: const Color(0xFF3B3B3B),
- size: 30.0,
+ color: Colors.grey[100],
+ child: SafeArea(
+ bottom: true,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.grey[100],
+ border: const Border(top: BorderSide(color: Colors.black38, width: .1)),
+ ),
+ child: Column(
+ children: [
+ // 输入框编辑器模块
+ Container(
+ padding: const EdgeInsets.all(10.0),
+ child: Row(
+ children: [
+ InkWell(
+ child: Icon(
+ voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
+ color: const Color(0xFF3B3B3B),
+ size: 30.0,
+ ),
+ onTap: () {
+ setState(() {
+ toolbarEnable = false;
+ if (voiceBtnEnable) {
+ voiceBtnEnable = false;
+ editorFocusNode.requestFocus();
+ } else {
+ voiceBtnEnable = true;
+ editorFocusNode.unfocus();
+ }
+ });
+ },
+ ),
+ const SizedBox(
+ width: 10.0,
+ ),
+ Expanded(
+ child: Container(
+ constraints: const BoxConstraints(minHeight: 40.0),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(5),
),
- onTap: () {
- setState(() {
- toolbarEnable = false;
- if (voiceBtnEnable) {
- voiceBtnEnable = false;
- editorFocusNode.requestFocus();
- } else {
- voiceBtnEnable = true;
- editorFocusNode.unfocus();
- }
- });
- },
- ),
- const SizedBox(
- width: 10.0,
- ),
- Expanded(
- child: Container(
- constraints: const BoxConstraints(minHeight: 40.0),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(5),
- ),
- child: Stack(
- children: [
- // 输入框
- Offstage(
- offstage: voiceBtnEnable,
- child: TextField(
- decoration: const InputDecoration(
- isDense: true,
- hoverColor: Colors.transparent,
- contentPadding: EdgeInsets.all(8.0),
- border: OutlineInputBorder(borderSide: BorderSide.none),
- ),
- style: const TextStyle(
- fontSize: 16.0,
- ),
- maxLines: null,
- controller: editorController,
- focusNode: editorFocusNode,
- cursorColor: const Color(0xFF07C160),
- onChanged: (value) {},
+ child: Stack(
+ children: [
+ // 输入框
+ Offstage(
+ offstage: voiceBtnEnable,
+ child: TextField(
+ decoration: const InputDecoration(
+ isDense: true,
+ hoverColor: Colors.transparent,
+ contentPadding: EdgeInsets.all(8.0),
+ border: OutlineInputBorder(borderSide: BorderSide.none),
),
+ style: const TextStyle(
+ fontSize: 16.0,
+ ),
+ maxLines: null,
+ controller: editorController,
+ focusNode: editorFocusNode,
+ cursorColor: const Color(0xFF07C160),
+ onChanged: (value) {},
),
- // 语音
- Offstage(
- offstage: !voiceBtnEnable,
- child: GestureDetector(
- child: Container(
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(5),
- ),
- alignment: Alignment.center,
- height: 40.0,
- width: double.infinity,
- child: Text(
- voiceTypeMap[voiceType],
- style: const TextStyle(fontSize: 15.0),
- ),
+ ),
+ // 语音
+ Offstage(
+ offstage: !voiceBtnEnable,
+ child: GestureDetector(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(5),
),
- onPanStart: (details) {
+ alignment: Alignment.center,
+ height: 40.0,
+ width: double.infinity,
+ child: Text(
+ voiceTypeMap[voiceType],
+ style: const TextStyle(fontSize: 15.0),
+ ),
+ ),
+ onPanStart: (details) async {
+ // 开始录音
+ final res = await VoiceService().startRecording();
+ if (res) {
setState(() {
voiceType = 1;
voicePanelEnable = true;
});
- },
- onPanUpdate: (details) {
- Offset pos = details.globalPosition;
- double swipeY = MediaQuery.of(context).size.height - 120;
- double swipeX = MediaQuery.of(context).size.width / 2 + 50;
- setState(() {
- if (pos.dy >= swipeY) {
- voiceType = 1; // 松开发送
- } else if (pos.dy < swipeY && pos.dx < swipeX) {
- voiceType = 2; // 左滑松开取消
- } else if (pos.dy < swipeY && pos.dx >= swipeX) {
- voiceType = 3; // 右滑语音转文字
- }
- });
- },
- onPanEnd: (details) {
- // print('停止录音');
- setState(() {
- switch (voiceType) {
- case 1:
- MyDialog.toast('发送录音文件');
- voicePanelEnable = false;
- break;
- case 2:
- MyDialog.toast('取消发送');
- voicePanelEnable = false;
- break;
- case 3:
- MyDialog.toast('语音转文字');
- voicePanelEnable = true;
- voiceToTransfer = true;
- break;
- }
- voiceType = 0;
- });
- },
- ),
+ } else {
+ MyDialog.toast('未获得麦克风权限');
+ }
+ },
+ onPanUpdate: (details) {
+ Offset pos = details.globalPosition;
+ double swipeY = MediaQuery.of(context).size.height - 120;
+ double swipeX = MediaQuery.of(context).size.width / 2 + 50;
+ setState(() {
+ if (pos.dy >= swipeY) {
+ voiceType = 1; // 松开发送
+ } else if (pos.dy < swipeY && pos.dx < swipeX) {
+ voiceType = 2; // 左滑松开取消
+ } else if (pos.dy < swipeY && pos.dx >= swipeX) {
+ voiceType = 3; // 右滑语音转文字
+ }
+ });
+ },
+ onPanEnd: (details) {
+ // print('停止录音');
+ setState(() {
+ switch (voiceType) {
+ case 1:
+ // MyDialog.toast('发送录音文件');
+ sendVoiceMsg();
+ voicePanelEnable = false;
+ break;
+ case 2:
+ // MyDialog.toast('取消发送');
+ VoiceService().cancelRecording;
+ voicePanelEnable = false;
+ break;
+ case 3:
+ MyDialog.toast('语音转文字');
+ voicePanelEnable = true;
+ voiceToTransfer = true;
+ break;
+ }
+ voiceType = 0;
+ });
+ },
),
- ],
- ),
+ ),
+ ],
),
),
- const SizedBox(
- width: 10.0,
+ ),
+ const SizedBox(
+ width: 10.0,
+ ),
+ InkWell(
+ child: const Icon(
+ Icons.add_reaction_rounded,
+ color: Color(0xFF3B3B3B),
+ size: 30.0,
),
- InkWell(
+ onTap: () {
+ handleEmojChooseState(0);
+ },
+ ),
+ const SizedBox(
+ width: 8.0,
+ ),
+ InkWell(
+ child: const Icon(
+ Icons.add,
+ color: Color(0xFF3B3B3B),
+ size: 30.0,
+ ),
+ onTap: () {
+ handleEmojChooseState(1);
+ },
+ ),
+ const SizedBox(
+ width: 8.0,
+ ),
+ InkWell(
+ child: Container(
+ height: 25.0,
+ width: 25.0,
+ decoration: BoxDecoration(
+ color: const Color(0xFF07C160),
+ borderRadius: BorderRadius.circular(20.0),
+ ),
child: const Icon(
- Icons.add_reaction_rounded,
- color: Color(0xFF3B3B3B),
- size: 30.0,
+ Icons.arrow_upward,
+ color: Colors.white,
+ size: 20.0,
),
- onTap: () {
- handleEmojChooseState(0);
- },
),
- const SizedBox(
- width: 8.0,
- ),
- InkWell(
- child: const Icon(
- Icons.add,
- color: Color(0xFF3B3B3B),
- size: 30.0,
- ),
- onTap: () {
- handleEmojChooseState(1);
- },
- ),
- const SizedBox(
- width: 8.0,
- ),
- InkWell(
- child: Container(
- height: 25.0,
- width: 25.0,
- decoration: BoxDecoration(
- color: const Color(0xFF07C160),
- borderRadius: BorderRadius.circular(20.0),
- ),
- child: const Icon(
- Icons.arrow_upward,
- color: Colors.white,
- size: 20.0,
- ),
- ),
- onTap: () {
- handleSubmit();
- },
- ),
- ],
+ onTap: () {
+ handleSubmit();
+ },
+ ),
+ ],
+ ),
+ ),
+
+ // 表情+选择模块
+ Visibility(
+ visible: toolbarEnable,
+ child: SizedBox(
+ height: keyboardHeight,
+ child: Column(
+ children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(),
),
),
-
- // 表情+选择模块
- Visibility(
- visible: toolbarEnable,
- child: SizedBox(
- height: keyboardHeight,
- child: Column(
- children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(),
- ),
- ),
- )
- ],
- ),
+ )
+ ],
),
- ))
+ ),
+ ),
+ )
],
),
),
@@ -1285,13 +1879,14 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
children: [
// 取消发送+语音转文字
Positioned(
- bottom: 120,
+ bottom: 160,
left: 30,
right: 30,
child: Visibility(
visible: !voiceToTransfer,
child: Column(
- crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center,
+ // crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 语音动画层
Stack(
@@ -1300,7 +1895,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
AnimatedContainer(
duration: Duration(milliseconds: 200),
height: 70.0,
- width: voiceType == 2 ? 70.0 : 200.0,
+ // width: voiceType == 2 ? 70.0 : 200.0,
+ width: 200.0,
decoration: BoxDecoration(
color: voiceType == 2 ? Colors.red : Color(0xFF89E45B),
borderRadius: BorderRadius.circular(15.0),
@@ -1324,7 +1920,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
// 操作项
Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
// 取消发送
Container(
@@ -1340,18 +1936,18 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
),
// 语音转文字
- Container(
- height: 60.0,
- width: 60.0,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(50.0),
- color: voiceType == 3 ? Color(0xFF89E45B) : Colors.black38,
- ),
- child: Icon(
- Icons.translate,
- color: Colors.white54,
- ),
- ),
+ // Container(
+ // height: 60.0,
+ // width: 60.0,
+ // decoration: BoxDecoration(
+ // borderRadius: BorderRadius.circular(50.0),
+ // color: voiceType == 3 ? Color(0xFF89E45B) : Colors.black38,
+ // ),
+ // child: Icon(
+ // Icons.translate,
+ // color: Colors.white54,
+ // ),
+ // ),
],
),
],
@@ -1404,7 +2000,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
),
// 操作项
Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
child: Container(
@@ -1498,7 +2094,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin {
alignment: Alignment.bottomCenter,
child: Visibility(
visible: !voiceToTransfer,
- child: Image.asset('assets/images/voice_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill),
+ child: Image.asset(
+ 'assets/images/voice_bg.webp',
+ width: double.infinity,
+ height: 100.0,
+ fit: BoxFit.fill,
+ ),
),
),
// 背景图标
@@ -1564,7 +2165,7 @@ class RenderChatItem extends StatelessWidget {
width: 35.0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
- child: Image.network(data.faceUrl ?? 'https://wuzhongjie.com.cn/download/logo.png'),
+ child: NetworkOrAssetImage(imageUrl: data.faceUrl),
),
)
: const SizedBox.shrink(),
@@ -1612,7 +2213,7 @@ class RenderChatItem extends StatelessWidget {
width: 35.0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
- child: Image.network(data.faceUrl ?? 'https://wuzhongjie.com.cn/download/logo.png'),
+ child: NetworkOrAssetImage(imageUrl: data.faceUrl),
),
)
: const SizedBox.shrink(),
diff --git a/lib/pages/chat/chat_group.dart b/lib/pages/chat/chat_group.dart
new file mode 100644
index 0000000..988dac8
--- /dev/null
+++ b/lib/pages/chat/chat_group.dart
@@ -0,0 +1,1813 @@
+/// 聊天模板
+library;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:get/get.dart';
+import 'package:loopin/IM/controller/chat_controller.dart';
+import 'package:loopin/IM/controller/chat_detail_controller.dart';
+import 'package:loopin/IM/im_message.dart';
+import 'package:loopin/IM/im_service.dart';
+import 'package:shirne_dialog/shirne_dialog.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
+import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
+
+import '../../behavior/custom_scroll_behavior.dart';
+import '../../components/image_group.dart';
+import '../../styles/index.dart';
+import '../../utils/index.dart';
+import './components/redpacket.dart';
+import './components/richtext.dart';
+// import 'mock/chat_json.dart';
+import 'mock/emoj_json.dart';
+
+class ChatGroup extends StatefulWidget {
+ const ChatGroup({super.key});
+
+ @override
+ State createState() => _ChatState();
+}
+
+class _ChatState extends State with SingleTickerProviderStateMixin {
+ late final ChatDetailController controller;
+ // 接收参数
+ // Rx arguments = Get.arguments.obs;
+ late final Rx