Browse Source

no message

ios
Jolie 4 months ago
parent
commit
ae734dd371
9 changed files with 359 additions and 104 deletions
  1. 67
      ios/Podfile.lock
  2. 8
      ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  3. 8
      ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  4. 96
      lib/controller/message/voice_player_manager.dart
  5. 104
      lib/pages/message/chat_page.dart
  6. 1
      lib/widget/message/message_item.dart
  7. 122
      lib/widget/message/voice_item.dart
  8. 56
      pubspec.lock
  9. 1
      pubspec.yaml

67
ios/Podfile.lock

@ -1,32 +1,59 @@
PODS:
- AgoraInfra_iOS (1.2.13.1)
- AgoraInfra_iOS (1.2.13)
- camera_avfoundation (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_native_splash (2.4.3):
- Flutter
- HyphenateChat (4.15.0):
- AgoraInfra_iOS (~> 1.2.13)
- im_flutter_sdk_ios (4.14.0):
- HyphenateChat (4.15.1):
- AgoraInfra_iOS (= 1.2.13)
- im_flutter_sdk_ios (4.15.2):
- Flutter
- HyphenateChat (= 4.15.0)
- HyphenateChat (= 4.15.1)
- image_picker_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.7.1):
- Flutter
- FlutterMacOS
- record_ios (1.1.0):
- Flutter
- sensors_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- im_flutter_sdk_ios (from `.symlinks/plugins/im_flutter_sdk_ios/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- 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`)
- sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
@ -34,6 +61,8 @@ SPEC REPOS:
- HyphenateChat
EXTERNAL SOURCES:
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
Flutter:
:path: Flutter
flutter_native_splash:
@ -42,23 +71,45 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/im_flutter_sdk_ios/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
sensors_plus:
:path: ".symlinks/plugins/sensors_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
AgoraInfra_iOS: 3691b2b277a1712a35ae96de25af319de0d73d08
AgoraInfra_iOS: 65e11a2183ab7836258768868d06058c22701b13
camera_avfoundation: 281867ff09f1da66f031a184ecfbc6f2e625c9f5
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
HyphenateChat: 4523c7fb2075771c49a2c492b31544d6cc82ff50
im_flutter_sdk_ios: 2348d34baa17e98d8c490d92023410956c8afee1
HyphenateChat: ec813941100d602d24e06b04b867474d634cb39d
im_flutter_sdk_ios: de87814fcf3a3cb585a78b55fba5f9fec989096b
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: 81954a1bf804b6e882d0453b3b6bc7fad7b47d3d
record_ios: 840d21cce013c5a3b2168b74a54ebdb4136359e2
sensors_plus: 7229095999f30740798f0eeef5cd120357a8f4f2
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

8
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

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

8
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

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

96
lib/controller/message/voice_player_manager.dart

@ -0,0 +1,96 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
///
class VoicePlayerManager extends GetxController {
static VoicePlayerManager? _instance;
static VoicePlayerManager get instance {
_instance ??= Get.put(VoicePlayerManager());
return _instance!;
}
final AudioPlayer _audioPlayer = AudioPlayer();
String? _currentPlayingId;
final Rx<String?> currentPlayingId = Rx<String?>(null);
final Rx<PlayerState> playerState = Rx<PlayerState>(PlayerState.stopped);
VoicePlayerManager() {
//
_audioPlayer.onPlayerStateChanged.listen((state) {
playerState.value = state;
if (state == PlayerState.completed) {
//
_currentPlayingId = null;
currentPlayingId.value = null;
}
});
}
///
/// [audioId] ID
/// [filePath]
Future<void> play(String audioId, String filePath) async {
try {
//
if (_currentPlayingId != null && _currentPlayingId != audioId) {
await stop();
}
// /
if (_currentPlayingId == audioId) {
if (playerState.value == PlayerState.playing) {
await _audioPlayer.pause();
} else {
await _audioPlayer.resume();
}
return;
}
//
_currentPlayingId = audioId;
currentPlayingId.value = audioId;
await _audioPlayer.play(DeviceFileSource(filePath));
} catch (e) {
print('播放音频失败: $e');
_currentPlayingId = null;
currentPlayingId.value = null;
}
}
///
Future<void> stop() async {
try {
await _audioPlayer.stop();
_currentPlayingId = null;
currentPlayingId.value = null;
} catch (e) {
print('停止播放失败: $e');
}
}
///
Future<void> pause() async {
try {
await _audioPlayer.pause();
} catch (e) {
print('暂停播放失败: $e');
}
}
///
bool isPlaying(String audioId) {
return _currentPlayingId == audioId &&
playerState.value == PlayerState.playing;
}
///
bool isLoaded(String audioId) {
return _currentPlayingId == audioId;
}
@override
void onClose() {
_audioPlayer.dispose();
super.onClose();
}
}

104
lib/pages/message/chat_page.dart

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../controller/message/chat_controller.dart';
import '../../controller/message/voice_player_manager.dart';
import '../../generated/assets.dart';
import '../../../widget/message/chat_input_bar.dart';
import '../../../widget/message/message_item.dart';
@ -19,59 +20,65 @@ class ChatPage extends StatelessWidget {
return GetBuilder<ChatController>(
init: ChatController(userId: userId),
builder: (controller) {
return Scaffold(
backgroundColor: Color(0xffF5F5F5),
appBar: AppBar(
title: Text(controller.userInfo.value?.nickName ?? ''),
centerTitle: true,
actions: [
Container(
padding: EdgeInsets.only(right: 16.w),
child: Image.asset(Assets.imagesMore, width: 16.w),
).onTap(() {}),
],
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
Get.back();
},
return WillPopScope(
onWillPop: () async {
// 退
await VoicePlayerManager.instance.stop();
return true;
},
child: Scaffold(
backgroundColor: Color(0xffF5F5F5),
appBar: AppBar(
title: Text(controller.userInfo.value?.nickName ?? ''),
centerTitle: true,
actions: [
Container(
padding: EdgeInsets.only(right: 16.w),
child: Image.asset(Assets.imagesMore, width: 16.w),
).onTap(() {}),
],
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
Get.back();
},
),
),
),
body: Column(
children: [
//
Expanded(
child: Container(
color: Color(0xffF5F5F5),
child: GestureDetector(
onTap: () {
//
FocusManager.instance.primaryFocus?.unfocus();
},
behavior: HitTestBehavior.opaque,
child: ListView.builder(
reverse: true,
padding: EdgeInsets.all(16.w),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller.messages[index];
final isSentByMe =
message.direction == MessageDirection.SEND;
body: Column(
children: [
//
Expanded(
child: Container(
color: Color(0xffF5F5F5),
child: GestureDetector(
onTap: () {
//
FocusManager.instance.primaryFocus?.unfocus();
},
behavior: HitTestBehavior.opaque,
child: ListView.builder(
reverse: true,
padding: EdgeInsets.all(16.w),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller.messages[index];
final isSentByMe =
message.direction == MessageDirection.SEND;
final previousMessage = index > 0
? controller.messages[index - 1]
: null;
final previousMessage = index > 0
? controller.messages[index - 1]
: null;
return MessageItem(
message: message,
isSentByMe: isSentByMe,
previousMessage: previousMessage,
);
},
return MessageItem(
message: message,
isSentByMe: isSentByMe,
previousMessage: previousMessage,
);
},
),
),
),
),
),
// 使
ChatInputBar(
onSendMessage: (message) async {
@ -97,7 +104,8 @@ class ChatPage extends StatelessWidget {
),
],
),
);
),
);
},
);
}

1
lib/widget/message/message_item.dart

@ -48,6 +48,7 @@ class MessageItem extends StatelessWidget {
final voiceBody = message.body as EMVoiceMessageBody;
return VoiceItem(
voiceBody: voiceBody,
messageId: message.msgId, // ID作为音频唯一标识
isSentByMe: isSentByMe,
showTime: shouldShowTime(),
formattedTime: formatMessageTime(message.serverTime),

122
lib/widget/message/voice_item.dart

@ -1,53 +1,110 @@
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../../generated/assets.dart';
import '../../../controller/message/voice_player_manager.dart';
class VoiceItem extends StatelessWidget {
class VoiceItem extends StatefulWidget {
final EMVoiceMessageBody voiceBody;
final String messageId; // ID
final bool isSentByMe;
final bool showTime;
final String formattedTime;
const VoiceItem({
required this.voiceBody,
required this.messageId,
required this.isSentByMe,
required this.showTime,
required this.formattedTime,
super.key,
});
@override
State<VoiceItem> createState() => _VoiceItemState();
}
class _VoiceItemState extends State<VoiceItem> {
final VoicePlayerManager _playerManager = VoicePlayerManager.instance;
@override
void initState() {
super.initState();
//
ever(_playerManager.currentPlayingId, (audioId) {
if (mounted) {
setState(() {});
}
});
ever(_playerManager.playerState, (state) {
if (mounted) {
setState(() {});
}
});
}
// /
Future<void> _handlePlayPause() async {
try {
//
String? filePath;
final localPath = widget.voiceBody.localPath;
final remotePath = widget.voiceBody.remotePath;
if (localPath.isNotEmpty) {
filePath = localPath;
} else if (remotePath != null && remotePath.isNotEmpty) {
//
filePath = remotePath;
}
SmartDialog.showToast('来了$remotePath');
if (filePath != null && filePath.isNotEmpty) {
await _playerManager.play(widget.messageId, filePath);
} else {
print('音频文件路径为空');
}
} catch (e) {
print('播放音频失败: $e');
}
}
@override
Widget build(BuildContext context) {
//
final duration = voiceBody.duration;
final duration = widget.voiceBody.duration;
final durationText = '${duration}s';
//
final isPlaying = _playerManager.isPlaying(widget.messageId);
return Column(
children: [
//
if (showTime) _buildTimeLabel(),
if (widget.showTime) _buildTimeLabel(),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row(
mainAxisAlignment: isSentByMe
mainAxisAlignment: widget.isSentByMe
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isSentByMe) _buildAvatar(),
if (!isSentByMe) SizedBox(width: 8.w),
if (!widget.isSentByMe) _buildAvatar(),
if (!widget.isSentByMe) SizedBox(width: 8.w),
Container(
margin: EdgeInsets.only(top: 10.h),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
decoration: BoxDecoration(
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white,
color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white,
borderRadius: BorderRadius.only(
topLeft: isSentByMe
topLeft: widget.isSentByMe
? Radius.circular(12.w)
: Radius.circular(0),
topRight: isSentByMe
topRight: widget.isSentByMe
? Radius.circular(0)
: Radius.circular(12.w),
bottomLeft: Radius.circular(12.w),
@ -58,22 +115,21 @@ class VoiceItem extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
//
GestureDetector(
onTap: () {
// TODO: /
},
child: Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSentByMe ? Colors.white : Colors.black,
),
child: Icon(
Icons.play_arrow,
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white,
size: 16.w,
),
Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.isSentByMe
? Colors.white
: Colors.black,
),
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: widget.isSentByMe
? Color(0xff8E7BF6)
: Colors.white,
size: 16.w,
),
),
SizedBox(width: 8.w),
@ -82,7 +138,7 @@ class VoiceItem extends StatelessWidget {
durationText,
style: TextStyle(
fontSize: 14.sp,
color: isSentByMe ? Colors.white : Colors.black,
color: widget.isSentByMe ? Colors.white : Colors.black,
fontWeight: FontWeight.w500,
),
),
@ -91,9 +147,11 @@ class VoiceItem extends StatelessWidget {
_buildWaveform(),
],
),
),
if (isSentByMe) SizedBox(width: 8.w),
if (isSentByMe) _buildAvatar(),
).onTap(() {
_handlePlayPause();
}),
if (widget.isSentByMe) SizedBox(width: 8.w),
if (widget.isSentByMe) _buildAvatar(),
],
),
),
@ -109,7 +167,7 @@ class VoiceItem extends StatelessWidget {
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
formattedTime,
widget.formattedTime,
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
),
@ -134,7 +192,7 @@ class VoiceItem extends StatelessWidget {
//
Widget _buildWaveform() {
// 20
final barCount = (voiceBody.duration / 2).ceil().clamp(5, 20);
final barCount = (widget.voiceBody.duration / 2).ceil().clamp(5, 20);
return SizedBox(
height: 16.h,
@ -152,7 +210,7 @@ class VoiceItem extends StatelessWidget {
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: isSentByMe
color: widget.isSentByMe
? Colors.white.withOpacity(0.8)
: Colors.grey.withOpacity(0.6),
borderRadius: BorderRadius.circular(1.w),

56
pubspec.lock

@ -49,6 +49,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.2.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.1"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.1.1"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.1"
boolean_selector:
dependency: transitive
description:

1
pubspec.yaml

@ -61,6 +61,7 @@ dependencies:
record: ^6.1.2
video_player: ^2.9.2
chewie: ^1.8.5 # 视频播放器UI
audioplayers: ^6.5.1
dev_dependencies:
flutter_test:

Loading…
Cancel
Save