Browse Source

feat(message): 实现聊天页面图片发送功能

- 新增图片和视频读取权限声明 (Android)- 新增相册访问权限描述 (iOS)
- 添加图片消息发送方法到聊天控制器- 创建更多选项视图组件用于选择图片和拍照
- 在聊天输入栏集成图片选择和相机功能- 更新依赖项以支持图片和相机相关功能- 实现从相册选择图片并发送的功能- 实现拍照并发送图片的功能
ios
Jolie 4 months ago
parent
commit
106b64217b
8 changed files with 389 additions and 73 deletions
  1. 2
      android/app/src/main/AndroidManifest.xml
  2. 2
      ios/Runner/Info.plist
  3. 19
      lib/controller/message/chat_controller.dart
  4. 6
      lib/pages/message/chat_page.dart
  5. 102
      lib/widget/message/chat_input_bar.dart
  6. 137
      lib/widget/message/more_options_view.dart
  7. 192
      pubspec.lock
  8. 2
      pubspec.yaml

2
android/app/src/main/AndroidManifest.xml

@ -2,6 +2,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<application
android:label="动我"
android:name="${applicationName}"

2
ios/Runner/Info.plist

@ -28,6 +28,8 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Replace with your permission description.</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

19
lib/controller/message/chat_controller.dart

@ -65,6 +65,25 @@ class ChatController extends GetxController {
}
}
///
Future<bool> sendImageMessage(String imagePath) async {
try {
final message = await IMManager.instance.sendImageMessage(imagePath, userId);
if (message != null) {
//
messages.insert(0, message);
update();
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('发送图片消息失败: $e');
}
return false;
}
}
///
Future<void> fetchMessages({bool loadMore = false}) async {
try {

6
lib/pages/message/chat_page.dart

@ -80,6 +80,12 @@ class ChatPage extends StatelessWidget {
onSendMessage: (message) async {
await controller.sendMessage(message);
},
onImageSelected: (imagePaths) async {
//
for (var imagePath in imagePaths) {
await controller.sendImageMessage(imagePath);
}
},
),
],
),

102
lib/widget/message/chat_input_bar.dart

@ -4,11 +4,13 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../generated/assets.dart';
import 'more_options_view.dart';
class ChatInputBar extends StatefulWidget {
final ValueChanged<String> onSendMessage;
final ValueChanged<List<String>>? onImageSelected;
const ChatInputBar({required this.onSendMessage, super.key});
const ChatInputBar({required this.onSendMessage, this.onImageSelected, super.key});
@override
State<ChatInputBar> createState() => _ChatInputBarState();
@ -38,6 +40,26 @@ class _ChatInputBarState extends State<ChatInputBar> {
});
}
void _handleImageTap(List<String> imagePaths) {
if (Get.isLogEnable) {
Get.log("选择了图片: $imagePaths");
}
//
if (widget.onImageSelected != null) {
widget.onImageSelected!(imagePaths);
}
}
void _handleCameraTap(String imagePath) {
if (Get.isLogEnable) {
Get.log("拍摄了照片: $imagePath");
}
//
if (widget.onImageSelected != null) {
widget.onImageSelected!([imagePath]);
}
}
@override
Widget build(BuildContext context) {
return Column(
@ -110,80 +132,14 @@ class _ChatInputBarState extends State<ChatInputBar> {
),
),
//
_buildMoreOptionsView(),
MoreOptionsView(
isVisible: _isMoreOptionsVisible,
onImageSelected: _handleImageTap,
onCameraSelected: _handleCameraTap,
)
],
);
}
//
Widget _buildMoreOptionsView() {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: _isMoreOptionsVisible ? 180.h : 0,
color: Colors.white,
child: _isMoreOptionsVisible
? Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
child: Column(
children: [
SizedBox(height: 10.h),
//
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
//
Column(
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Color(0xffF0F5FF),
borderRadius: BorderRadius.circular(8.w),
),
padding: EdgeInsets.all(10.w),
child: Image.asset(Assets.imagesPhoto, width: 40.w, height: 40.w),
),
SizedBox(height: 8.h),
Text(
"图片",
style: TextStyle(
fontSize: 12.sp,
color: Colors.black,
),
),
],
),
SizedBox(width: 40.w),
//
Column(
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Color(0xffF0F5FF),
borderRadius: BorderRadius.circular(8.w),
),
padding: EdgeInsets.all(10.w),
child: Image.asset(Assets.imagesCamera, width: 40.w, height: 40.w),
),
SizedBox(height: 8.h),
Text(
"相机",
style: TextStyle(
fontSize: 12.sp,
color: Colors.black,
),
),
],
),
],
),
],
),
)
: null,
);
}
}

137
lib/widget/message/more_options_view.dart

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
import '../../generated/assets.dart';
class MoreOptionsView extends StatelessWidget {
final bool isVisible;
final ValueChanged<List<String>> onImageSelected;
final ValueChanged<String> onCameraSelected;
const MoreOptionsView({
required this.isVisible,
required this.onImageSelected,
required this.onCameraSelected,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: isVisible ? 180.h : 0,
color: Colors.white,
child: isVisible
? Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
child: Column(
children: [
SizedBox(height: 10.h),
//
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
//
GestureDetector(
onTap: () async{
try {
List<AssetEntity>? result = await AssetPicker.pickAssets(context);
if (result != null && result.isNotEmpty) {
//
List<String> imagePaths = [];
for (var asset in result) {
final file = await asset.file;
if (file != null) {
imagePaths.add(file.path);
}
}
if (imagePaths.isNotEmpty) {
onImageSelected(imagePaths);
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log("选择图片失败: $e");
}
}
},
child: Column(
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Color(0xffF0F5FF),
borderRadius: BorderRadius.circular(8.w),
),
padding: EdgeInsets.all(10.w),
child: Image.asset(Assets.imagesPhoto, width: 40.w, height: 40.w),
),
SizedBox(height: 8.h),
Text(
"图片",
style: TextStyle(
fontSize: 12.sp,
color: Colors.black,
),
),
],
),
),
SizedBox(width: 40.w),
//
GestureDetector(
onTap: () async{
try {
AssetEntity? entity = await CameraPicker.pickFromCamera(
context,
pickerConfig: const CameraPickerConfig(),
);
if (entity != null) {
//
final file = await entity.file;
if (file != null) {
onCameraSelected(file.path);
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log("拍照失败: $e");
}
}
},
child: Column(
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Color(0xffF0F5FF),
borderRadius: BorderRadius.circular(8.w),
),
padding: EdgeInsets.all(10.w),
child: Image.asset(Assets.imagesCamera, width: 40.w, height: 40.w),
),
SizedBox(height: 8.h),
Text(
"相机",
style: TextStyle(
fontSize: 12.sp,
color: Colors.black,
),
),
],
),
),
],
),
],
),
)
: null,
);
}
}

192
pubspec.lock

@ -105,6 +105,46 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.12.0"
camera:
dependency: transitive
description:
name: camera
sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.10.6"
camera_android:
dependency: transitive
description:
name: camera_android
sha256: "8397c4fcec4f4dbafdeff994adc6ed16dcaa4a25f02e70e3b8fbe59c9a88807f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.10.10+11"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: "34bcd5db30e52414f1f0783c5e3f566909fab14141a21b3b576c78bd35382bf6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.22+4"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.12.0"
camera_web:
dependency: transitive
description:
name: camera_web
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.5"
characters:
dependency: transitive
description:
@ -257,6 +297,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
extended_image:
dependency: transitive
description:
name: extended_image
sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.1"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.1"
fake_async:
dependency: transitive
description:
@ -461,6 +517,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
http_client_helper:
dependency: transitive
description:
name: http_client_helper
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
http_multi_server:
dependency: transitive
description:
@ -597,6 +661,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
json_annotation:
dependency: transitive
description:
@ -693,6 +765,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.6"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
package_config:
dependency: transitive
description:
@ -829,6 +909,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
photo_manager:
dependency: transitive
description:
name: photo_manager
sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.7.1"
photo_manager_image_provider:
dependency: transitive
description:
name: photo_manager_image_provider
sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
platform:
dependency: transitive
description:
@ -869,6 +965,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.0"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
@ -909,6 +1013,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.1.3"
sensors_plus:
dependency: transitive
description:
name: sensors_plus
sha256: "89e2bfc3d883743539ce5774a2b93df61effde40ff958ecad78cd66b1a8b8d52"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.2"
sensors_plus_platform_interface:
dependency: transitive
description:
name: sensors_plus_platform_interface
sha256: "58815d2f5e46c0c41c40fb39375d3f127306f7742efe3b891c0b1c87e2b5cd5d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
shared_preferences:
dependency: transitive
description:
@ -1098,6 +1218,54 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
video_player:
dependency: transitive
description:
name: video_player
sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.10.0"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: cf768d02924b91e333e2bc1ff928528f57d686445874f383bafab12d0bdfc340
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.8.17"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: "19ed1162a7a5520e7d7791e0b7b73ba03161b6a69428b82e4689e435b325432d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.8.5"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.6.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
visibility_detector:
dependency: transitive
description:
name: visibility_detector
sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.0+2"
vm_service:
dependency: transitive
description:
@ -1138,6 +1306,30 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.3"
wechat_assets_picker:
dependency: "direct main"
description:
name: wechat_assets_picker
sha256: c307e50394c1e6dfcd5c4701e84efb549fce71444fedcf2e671c50d809b3e2a1
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.8.0"
wechat_camera_picker:
dependency: "direct main"
description:
name: wechat_camera_picker
sha256: "776ce32feda72d84b63425533a27d3b822bfb93cc063d2aef3cc6d788769f36b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.4.0"
wechat_picker_library:
dependency: transitive
description:
name: wechat_picker_library
sha256: "5cb61b9aa935b60da5b043f8446fbb9c5077419f20ccc4856bf444aec4f44bc1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.7"
xdg_directories:
dependency: transitive
description:

2
pubspec.yaml

@ -53,6 +53,8 @@ dependencies:
permission_handler: ^12.0.1
flustars: ^2.0.1
easy_refresh: ^3.4.0
wechat_assets_picker: ^9.8.0
wechat_camera_picker: ^4.4.0
dev_dependencies:
flutter_test:

Loading…
Cancel
Save