From 1224d3da7a70771f2a229e574e105b7d2e7d0acf Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:28:41 -0300 Subject: [PATCH 1/7] add readme --- README.md | 44 +++++++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 9 ++++ examples/flyer_chat/ios/Runner/Info.plist | 4 ++ examples/flyer_chat/macos/Runner/Info.plist | 4 ++ 4 files changed, 61 insertions(+) diff --git a/README.md b/README.md index 996e8b98..6daf67b4 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,47 @@ Welcome to the next generation of Flutter Chat UI! ✨ 💡 **Looking for the stable version?** The v1 release is available on the [v1 branch](https://github.com/flyerhq/flutter_chat_ui/tree/v1). + +## Platform-Specific Configuration for Audio Recording + +To enable audio recording functionality, you need to configure permissions for both Android and iOS platforms. + +### Android + +1. **Add Permissions to `AndroidManifest.xml`:** + + Add the following permissions to your `AndroidManifest.xml` file to allow microphone access and file writing: + + ```xml + + + + ``` + +2. **Add Queries for Android 11 and above:** + + If your app targets Android 11 (API level 30) or higher, add the following queries to allow access to certain actions: + + ```xml + + + + + + + ``` + +### iOS + +1. **Add Permissions to `Info.plist`:** + + Add the following keys to your `Info.plist` file to request microphone access: + + ```xml + NSMicrophoneUsageDescription + This app requires access to the microphone to record audio. + NSPhotoLibraryAddUsageDescription + This app requires access to save audio files. + ``` + +By following these steps, you can ensure that your app has the necessary permissions to record audio on both Android and iOS platforms. diff --git a/examples/flyer_chat/android/app/src/main/AndroidManifest.xml b/examples/flyer_chat/android/app/src/main/AndroidManifest.xml index c92e2ead..ef5423fc 100644 --- a/examples/flyer_chat/android/app/src/main/AndroidManifest.xml +++ b/examples/flyer_chat/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,13 @@ + + + + + + + + + UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSMicrophoneUsageDescription + This app requires access to the microphone to record audio. + NSPhotoLibraryAddUsageDescription + This app requires access to save audio files. diff --git a/examples/flyer_chat/macos/Runner/Info.plist b/examples/flyer_chat/macos/Runner/Info.plist index 4789daa6..02562cf9 100644 --- a/examples/flyer_chat/macos/Runner/Info.plist +++ b/examples/flyer_chat/macos/Runner/Info.plist @@ -28,5 +28,9 @@ MainMenu NSPrincipalClass NSApplication + NSMicrophoneUsageDescription + This app requires access to the microphone to record audio. + NSPhotoLibraryAddUsageDescription + This app requires access to save audio files. From ffaf4e9bc29d8042c29d60f3261b3db8decb6cde Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Sun, 10 Nov 2024 04:05:59 -0300 Subject: [PATCH 2/7] add working audio record --- .../android/app/src/debug/AndroidManifest.xml | 6 + examples/flyer_chat/ios/Podfile | 2 +- examples/flyer_chat/ios/Podfile.lock | 14 +- .../ios/Runner.xcodeproj/project.pbxproj | 18 + examples/flyer_chat/ios/Runner/Info.plist | 4 +- examples/flyer_chat/lib/local.dart | 26 +- examples/flyer_chat/lib/main.dart | 373 +++++++++--------- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + examples/flyer_chat/macos/Runner/Info.plist | 6 +- .../flutter/generated_plugin_registrant.cc | 6 + .../windows/flutter/generated_plugins.cmake | 2 + packages/flutter_chat_ui/lib/src/chat.dart | 12 +- .../flutter_chat_ui/lib/src/chat_input.dart | 212 ++++++++-- .../lib/src/utils/typedefs.dart | 1 + .../lib/src/wave_animation.dart | 37 ++ packages/flutter_chat_ui/pubspec.yaml | 2 + .../lib/src/flyer_chat_audio_message.dart | 1 + .../lib/src/flyer_chat_custom_message.dart | 1 + .../lib/src/flyer_chat_file_message.dart | 1 + .../lib/src/flyer_chat_location_message.dart | 1 + .../lib/src/flyer_chat_system_message.dart | 1 + .../lib/src/flyer_chat_video_message.dart | 1 + 24 files changed, 480 insertions(+), 254 deletions(-) create mode 100644 packages/flutter_chat_ui/lib/src/wave_animation.dart diff --git a/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml b/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml index 399f6981..ffa6aff6 100644 --- a/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml +++ b/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,10 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + + + + diff --git a/examples/flyer_chat/ios/Podfile b/examples/flyer_chat/ios/Podfile index d97f17e2..164df534 100644 --- a/examples/flyer_chat/ios/Podfile +++ b/examples/flyer_chat/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/examples/flyer_chat/ios/Podfile.lock b/examples/flyer_chat/ios/Podfile.lock index be750adc..39e0eacd 100644 --- a/examples/flyer_chat/ios/Podfile.lock +++ b/examples/flyer_chat/ios/Podfile.lock @@ -7,12 +7,18 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - record_darwin (1.0.0): + - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - record_darwin (from `.symlinks/plugins/record_darwin/ios`) EXTERNAL SOURCES: Flutter: @@ -23,13 +29,19 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/isar_flutter_libs/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + record_darwin: + :path: ".symlinks/plugins/record_darwin/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + record_darwin: 3b1a8e7d5c0cbf45ad6165b4d83a6ca643d929c3 -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: 7be2f5f74864d463a8ad433546ed1de7e0f29aef COCOAPODS: 1.15.2 diff --git a/examples/flyer_chat/ios/Runner.xcodeproj/project.pbxproj b/examples/flyer_chat/ios/Runner.xcodeproj/project.pbxproj index 0aedf6ef..1cc576d4 100644 --- a/examples/flyer_chat/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flyer_chat/ios/Runner.xcodeproj/project.pbxproj @@ -199,6 +199,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 63E7C95A71D88018D5F96D49 /* [CP] Embed Pods Frameworks */, + AD038D3EB588E610E689CA64 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -340,6 +341,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + AD038D3EB588E610E689CA64 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; C60C1FADAB37336E433945E1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/examples/flyer_chat/ios/Runner/Info.plist b/examples/flyer_chat/ios/Runner/Info.plist index 2775516a..c02d5a1d 100644 --- a/examples/flyer_chat/ios/Runner/Info.plist +++ b/examples/flyer_chat/ios/Runner/Info.plist @@ -50,8 +50,6 @@ UIInterfaceOrientationLandscapeRight NSMicrophoneUsageDescription - This app requires access to the microphone to record audio. - NSPhotoLibraryAddUsageDescription - This app requires access to save audio files. + Some message to describe why you need this permission diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart index f4ea04bc..dd27129d 100644 --- a/examples/flyer_chat/lib/local.dart +++ b/examples/flyer_chat/lib/local.dart @@ -34,18 +34,18 @@ class LocalState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Chat( - builders: Builders( - textMessageBuilder: (context, message) => - FlyerChatTextMessage(message: message), - imageMessageBuilder: (context, message) => - FlyerChatImageMessage(message: message), + body: SafeArea( + child: Chat( + builders: Builders( + textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message), + imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message), + ), + chatController: _chatController, + user: widget.author, + onMessageSend: _addItem, + onMessageTap: _removeItem, + onAttachmentTap: _handleAttachmentTap, ), - chatController: _chatController, - user: widget.author, - onMessageSend: _addItem, - onMessageTap: _removeItem, - onAttachmentTap: _handleAttachmentTap, ), persistentFooterButtons: [ TextButton( @@ -71,9 +71,7 @@ class LocalState extends State { } void _addItem(String? text) async { - final randomUser = Random().nextInt(2) == 0 - ? const User(id: 'sender1') - : const User(id: 'sender2'); + final randomUser = Random().nextInt(2) == 0 ? const User(id: 'sender1') : const User(id: 'sender2'); final message = await createMessage(randomUser, widget.dio, text: text); diff --git a/examples/flyer_chat/lib/main.dart b/examples/flyer_chat/lib/main.dart index 163ac94f..3bdde593 100644 --- a/examples/flyer_chat/lib/main.dart +++ b/examples/flyer_chat/lib/main.dart @@ -59,8 +59,7 @@ class _FlyerChatHomePageState extends State { final _dio = Dio(); User _author = const User(id: 'sender1'); final _chatIdController = TextEditingController(text: defaultChatId); - final _geminiApiKeyController = - TextEditingController(text: defaultGeminiApiKey); + final _geminiApiKeyController = TextEditingController(text: defaultGeminiApiKey); @override void dispose() { @@ -71,207 +70,207 @@ class _FlyerChatHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 8), - SegmentedButton( - segments: const >[ - ButtonSegment( - value: 'sender1', - label: Text('sender1'), - ), - ButtonSegment( - value: 'sender2', - label: Text('sender2'), - ), - ], - selected: {_author.id}, - onSelectionChanged: (Set newSender) { - setState(() { - // By default there is only a single segment that can be - // selected at one time, so its value is always the first - // item in the selected set. - _author = User(id: newSender.first); - }); - }, - ), - const SizedBox(height: 8), - SizedBox( - width: 200, - child: TextField( - controller: _chatIdController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'chat id', + body: SafeArea( + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 8), + SegmentedButton( + segments: const >[ + ButtonSegment( + value: 'sender1', + label: Text('sender1'), + ), + ButtonSegment( + value: 'sender2', + label: Text('sender2'), + ), + ], + selected: {_author.id}, + onSelectionChanged: (Set newSender) { + setState(() { + // By default there is only a single segment that can be + // selected at one time, so its value is always the first + // item in the selected set. + _author = User(id: newSender.first); + }); + }, + ), + const SizedBox(height: 8), + SizedBox( + width: 200, + child: TextField( + controller: _chatIdController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'chat id', + ), ), ), - ), - const SizedBox(height: 8), - Wrap( - alignment: WrapAlignment.center, - spacing: 8, - runSpacing: 8, - children: [ - ElevatedButton( - onPressed: () { - getInitialMessages(_dio, chatId: _chatIdController.text) - .then((messages) { - if (mounted && context.mounted) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Api( - author: _author, - chatId: _chatIdController.text, - initialMessages: messages, - dio: _dio, - ), - ), - ); - } - }).catchError((error) { - if (mounted && context.mounted) { - debugPrint(error.toString()); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Error'), - content: const Text( - 'Make sure the chat ID is correct', + const SizedBox(height: 8), + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton( + onPressed: () { + getInitialMessages(_dio, chatId: _chatIdController.text).then((messages) { + if (mounted && context.mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Api( + author: _author, + chatId: _chatIdController.text, + initialMessages: messages, + dio: _dio, ), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () => - Navigator.of(context).pop(), - ), - ], - ); - }, - ); - } - }); - }, - child: const Text('api'), - ), - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Confirmation'), - content: const Text( - 'Are you sure you want to generate a new chat ID?', - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () => Navigator.of(context).pop(), ), - TextButton( - child: const Text('Yes'), - onPressed: () { - Navigator.of(context).pop(); - getChatId(_dio).then((chatId) { - if (mounted && context.mounted) { - _chatIdController.text = chatId; - } - }); - }, + ); + } + }).catchError((error) { + if (mounted && context.mounted) { + debugPrint(error.toString()); + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Error'), + content: const Text( + 'Make sure the chat ID is correct', + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ); + } + }); + }, + child: const Text('api'), + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Confirmation'), + content: const Text( + 'Are you sure you want to generate a new chat ID?', ), - ], - ); - }, - ); - }, - child: const Text('generate chat id'), - ), - ElevatedButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: _chatIdController.text), - ); - }, - child: const Text('copy chat id'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: const Text('Yes'), + onPressed: () { + Navigator.of(context).pop(); + getChatId(_dio).then((chatId) { + if (mounted && context.mounted) { + _chatIdController.text = chatId; + } + }); + }, + ), + ], + ); + }, + ); + }, + child: const Text('generate chat id'), + ), + ElevatedButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: _chatIdController.text), + ); + }, + child: const Text('copy chat id'), + ), + ], + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'In order to test the api, you need to generate a chat id. Chat will be reset after 24 hours. Use the same chat id to access chat on different devices.', + textAlign: TextAlign.center, ), - ], - ), - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'In order to test the api, you need to generate a chat id. Chat will be reset after 24 hours. Use the same chat id to access chat on different devices.', - textAlign: TextAlign.center, ), - ), - const SizedBox( - width: 200, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Divider( - color: Colors.grey, - thickness: 1, + const SizedBox( + width: 200, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider( + color: Colors.grey, + thickness: 1, + ), ), ), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Local(author: _author, dio: _dio), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Local(author: _author, dio: _dio), + ), + ); + }, + child: const Text('local'), + ), + const SizedBox( + width: 200, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider( + color: Colors.grey, + thickness: 1, ), - ); - }, - child: const Text('local'), - ), - const SizedBox( - width: 200, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Divider( - color: Colors.grey, - thickness: 1, ), ), - ), - SizedBox( - width: 200, - child: TextField( - controller: _geminiApiKeyController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'gemini api key', + SizedBox( + width: 200, + child: TextField( + controller: _geminiApiKeyController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'gemini api key', + ), ), ), - ), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Gemini( - geminiApiKey: _geminiApiKeyController.text, - database: widget.database, + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Gemini( + geminiApiKey: _geminiApiKeyController.text, + database: widget.database, + ), ), - ), - ); - }, - child: const Text('gemini'), - ), - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'In order to test the AI example, you need to provide your own Gemini API key', - textAlign: TextAlign.center, + ); + }, + child: const Text('gemini'), + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'In order to test the AI example, you need to provide your own Gemini API key', + textAlign: TextAlign.center, + ), ), - ), - const SizedBox(height: 8), - ], + const SizedBox(height: 8), + ], + ), ), ), ), diff --git a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc index 02bc14a7..b8f668e5 100644 --- a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc +++ b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = @@ -16,4 +17,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) record_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); + record_linux_plugin_register_with_registrar(record_linux_registrar); } diff --git a/examples/flyer_chat/linux/flutter/generated_plugins.cmake b/examples/flyer_chat/linux/flutter/generated_plugins.cmake index 00bef184..0eae5ce6 100644 --- a/examples/flyer_chat/linux/flutter/generated_plugins.cmake +++ b/examples/flyer_chat/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux isar_flutter_libs + record_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift index 0b202afd..65fbf823 100644 --- a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,11 @@ import Foundation import file_selector_macos import isar_flutter_libs import path_provider_foundation +import record_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) } diff --git a/examples/flyer_chat/macos/Runner/Info.plist b/examples/flyer_chat/macos/Runner/Info.plist index 02562cf9..db5da9ed 100644 --- a/examples/flyer_chat/macos/Runner/Info.plist +++ b/examples/flyer_chat/macos/Runner/Info.plist @@ -29,8 +29,8 @@ NSPrincipalClass NSApplication NSMicrophoneUsageDescription - This app requires access to the microphone to record audio. - NSPhotoLibraryAddUsageDescription - This app requires access to save audio files. + Some message to describe why you need this permission + com.apple.security.device.audio-input + diff --git a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc index df32f874..38602c46 100644 --- a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc +++ b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,16 @@ #include #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + RecordWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); } diff --git a/examples/flyer_chat/windows/flutter/generated_plugins.cmake b/examples/flyer_chat/windows/flutter/generated_plugins.cmake index 52869ab3..7140f4ff 100644 --- a/examples/flyer_chat/windows/flutter/generated_plugins.cmake +++ b/examples/flyer_chat/windows/flutter/generated_plugins.cmake @@ -5,6 +5,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows isar_flutter_libs + permission_handler_windows + record_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/flutter_chat_ui/lib/src/chat.dart b/packages/flutter_chat_ui/lib/src/chat.dart index c3291ed5..5373c89e 100644 --- a/packages/flutter_chat_ui/lib/src/chat.dart +++ b/packages/flutter_chat_ui/lib/src/chat.dart @@ -20,6 +20,7 @@ class Chat extends StatefulWidget { final ChatTheme? darkTheme; final ThemeMode themeMode; final OnMessageSendCallback? onMessageSend; + final OnAudioSendCallback? onAudioSend; final OnMessageTapCallback? onMessageTap; final OnAttachmentTapCallback? onAttachmentTap; @@ -35,6 +36,7 @@ class Chat extends StatefulWidget { this.themeMode = ThemeMode.system, this.onMessageSend, this.onMessageTap, + this.onAudioSend, this.onAttachmentTap, }); @@ -69,8 +71,7 @@ class _ChatState extends State with WidgetsBindingObserver { void didUpdateWidget(covariant Chat oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.theme != widget.theme || - oldWidget.darkTheme != widget.darkTheme) { + if (oldWidget.theme != widget.theme || oldWidget.darkTheme != widget.darkTheme) { _updateTheme(theme: _theme, darkTheme: _theme); } @@ -151,10 +152,9 @@ class _ChatState extends State with WidgetsBindingObserver { _theme = (darkTheme ?? ChatTheme.dark()).merge(widget.darkTheme); break; case ThemeMode.system: - _theme = - PlatformDispatcher.instance.platformBrightness == Brightness.dark - ? (darkTheme ?? ChatTheme.dark()).merge(widget.darkTheme) - : (theme ?? ChatTheme.light()).merge(widget.theme); + _theme = PlatformDispatcher.instance.platformBrightness == Brightness.dark + ? (darkTheme ?? ChatTheme.dark()).merge(widget.darkTheme) + : (theme ?? ChatTheme.light()).merge(widget.theme); break; } } diff --git a/packages/flutter_chat_ui/lib/src/chat_input.dart b/packages/flutter_chat_ui/lib/src/chat_input.dart index 24d33c29..9e5ce2f5 100644 --- a/packages/flutter_chat_ui/lib/src/chat_input.dart +++ b/packages/flutter_chat_ui/lib/src/chat_input.dart @@ -3,24 +3,54 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:provider/provider.dart'; +import 'package:record/record.dart'; +import './wave_animation.dart'; import 'utils/chat_input_height_notifier.dart'; import 'utils/typedefs.dart'; +/// A widget that provides a chat input interface with text, attachment, and audio recording capabilities. class ChatInput extends StatefulWidget { + /// The left position of the chat input. final double? left; + + /// The right position of the chat input. final double? right; + + /// The top position of the chat input. final double? top; + + /// The bottom position of the chat input. final double? bottom; + + /// The horizontal blur radius for the backdrop filter. final double? sigmaX; + + /// The vertical blur radius for the backdrop filter. final double? sigmaY; + + /// The padding around the chat input. final EdgeInsetsGeometry? padding; + + /// The icon for attachments. final Widget? attachmentIcon; + + /// The icon for sending messages. final Widget? sendIcon; + + /// The icon for audio recording. + final Widget? audioIcon; + + /// The gap between elements in the chat input. final double? gap; + + /// The border for the text input field. final InputBorder? inputBorder; + + /// Whether the text input field is filled. final bool? filled; + /// Creates a [ChatInput] widget. const ChatInput({ super.key, this.left = 0, @@ -32,6 +62,7 @@ class ChatInput extends StatefulWidget { this.padding = const EdgeInsets.all(8.0), this.attachmentIcon = const Icon(Icons.attachment), this.sendIcon = const Icon(Icons.send), + this.audioIcon = const Icon(Icons.mic), this.gap = 8, this.inputBorder = const OutlineInputBorder( borderSide: BorderSide.none, @@ -47,6 +78,9 @@ class ChatInput extends StatefulWidget { class _ChatInputState extends State { final GlobalKey _inputKey = GlobalKey(); final TextEditingController _textController = TextEditingController(); + final AudioRecorder _audioRecorder = AudioRecorder(); + bool _isRecording = false; + String? _recordedAudioPath; @override void initState() { @@ -57,13 +91,13 @@ class _ChatInputState extends State { @override void dispose() { _textController.dispose(); + _audioRecorder.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final backgroundColor = - context.select((ChatTheme theme) => theme.backgroundColor); + final backgroundColor = context.select((ChatTheme theme) => theme.backgroundColor); final inputTheme = context.select((ChatTheme theme) => theme.inputTheme); final onAttachmentTap = context.read(); @@ -76,7 +110,6 @@ class _ChatInputState extends State { child: ClipRect( child: BackdropFilter( filter: ImageFilter.blur( - // TODO: remove backdrop filter if both are 0 sigmaX: widget.sigmaX ?? 0, sigmaY: widget.sigmaY ?? 0, ), @@ -84,43 +117,61 @@ class _ChatInputState extends State { key: _inputKey, color: backgroundColor.withOpacity(0.8), child: Padding( - // TODO: remove padding if it's 0 padding: widget.padding ?? EdgeInsets.zero, child: Row( children: [ - widget.attachmentIcon != null - ? IconButton( - icon: widget.attachmentIcon!, - color: inputTheme.hintStyle?.color, - onPressed: onAttachmentTap, - ) - : const SizedBox.shrink(), - SizedBox(width: widget.gap), - Expanded( - child: TextField( - controller: _textController, - decoration: InputDecoration( - hintText: 'Type a message', - hintStyle: inputTheme.hintStyle, - border: widget.inputBorder, - filled: widget.filled, - fillColor: inputTheme.backgroundColor, - hoverColor: Colors.transparent, - ), - style: inputTheme.textStyle, - onSubmitted: _handleSubmitted, - textInputAction: TextInputAction.send, + if (widget.attachmentIcon != null && !_isRecording) + IconButton( + icon: widget.attachmentIcon!, + color: inputTheme.hintStyle?.color, + onPressed: onAttachmentTap, ), + if (!_isRecording) SizedBox(width: widget.gap), + Expanded( + child: _isRecording + ? _buildRecordingIndicator() + : TextField( + controller: _textController, + decoration: InputDecoration( + hintText: 'Type a message', + hintStyle: inputTheme.hintStyle, + border: widget.inputBorder, + filled: widget.filled, + fillColor: inputTheme.backgroundColor, + hoverColor: Colors.transparent, + ), + style: inputTheme.textStyle, + onSubmitted: _handleSubmitted, + textInputAction: TextInputAction.send, + ), ), SizedBox(width: widget.gap), - widget.sendIcon != null - ? IconButton( - icon: widget.sendIcon!, - color: inputTheme.hintStyle?.color, - onPressed: () => - _handleSubmitted(_textController.text), - ) - : const SizedBox.shrink(), + ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, child) { + return widget.sendIcon != null + ? Container( + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: IconButton( + icon: value.text.isNotEmpty || _isRecording ? widget.sendIcon! : widget.audioIcon!, + color: inputTheme.hintStyle?.color, + onPressed: () async { + if (_isRecording) { + _handleAudioSubmitted(); + } else if (value.text.isEmpty) { + await _handleRecordAudio(); + } else { + _handleSubmitted(value.text); + } + }, + ), + ) + : const SizedBox.shrink(); + }, + ), ], ), ), @@ -130,13 +181,60 @@ class _ChatInputState extends State { ); } + Widget _buildRecordingIndicator() { + final streamAmplitude = _audioRecorder.onAmplitudeChanged(const Duration(seconds: 1)); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.circle, color: Colors.red), + const SizedBox(width: 8), + StreamBuilder( + stream: Stream.periodic(const Duration(seconds: 1), (count) => Duration(seconds: count)), + builder: (context, snapshot) { + final duration = snapshot.data ?? Duration.zero; + final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return Row( + children: [ + Text( + '$minutes:$seconds', + style: const TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(width: 8), + StreamBuilder( + stream: streamAmplitude, + builder: (context, snapshot) { + final current = snapshot.data?.current ?? 0.0; + final max = snapshot.data?.max ?? 0.0; + final audioLevel = (current / max).clamp(0.0, 1.0); + + print("current: $current"); + print("max $max"); + print("audioLevel: ${audioLevel}"); + + return BarAnimation( + color: Colors.red, + audioLevel: audioLevel, + ); + }, + ), + ], + ); + }, + ), + ], + ); + } + void _updateInputHeight() { - final renderBox = - _inputKey.currentContext?.findRenderObject() as RenderBox?; + final renderBox = _inputKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { - context - .read() - .updateHeight(renderBox.size.height); + context.read().updateHeight(renderBox.size.height); } } @@ -146,4 +244,40 @@ class _ChatInputState extends State { _textController.clear(); } } + + void _handleAudioSubmitted() { + _audioRecorder.stop(); + setState(() { + _isRecording = false; + }); + + if (_recordedAudioPath!.isNotEmpty) { + context.read()?.call(_recordedAudioPath!); + _resetAudioState(); + } + } + + Future _handleRecordAudio() async { + try { + final hasPermission = await _audioRecorder.hasPermission(); + + if (hasPermission) { + setState(() { + _isRecording = true; + }); + + _recordedAudioPath = 'audio.m4a'; + + await _audioRecorder.start(const RecordConfig(), path: _recordedAudioPath!); + } + } catch (e) { + debugPrint('Error during audio recording: $e'); + } + } + + void _resetAudioState() { + setState(() { + _recordedAudioPath = null; + }); + } } diff --git a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart index 6978adf0..5478dc4c 100644 --- a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart +++ b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart @@ -4,4 +4,5 @@ import 'package:flutter_chat_core/flutter_chat_core.dart'; typedef OnMessageTapCallback = void Function(Message message); typedef OnMessageSendCallback = void Function(String text); +typedef OnAudioSendCallback = void Function(String filePath); typedef OnAttachmentTapCallback = VoidCallback; diff --git a/packages/flutter_chat_ui/lib/src/wave_animation.dart b/packages/flutter_chat_ui/lib/src/wave_animation.dart new file mode 100644 index 00000000..c49b0af4 --- /dev/null +++ b/packages/flutter_chat_ui/lib/src/wave_animation.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +/// A widget that displays bars based on audio input level. +/// +/// This widget is used to indicate ongoing processes like recording by +/// adjusting the height of bars according to the audio input level. +class BarAnimation extends StatelessWidget { + /// The color of the bars in the animation. + final Color color; + + /// The audio input level that affects the bar height. + final double audioLevel; + + /// Creates a [BarAnimation] widget. + const BarAnimation({super.key, required this.color, required this.audioLevel}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(32, (index) { + final barHeight = audioLevel * 16.0; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Container( + width: 4.0, + height: barHeight, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2.0), + ), + ), + ); + }), + ); + } +} diff --git a/packages/flutter_chat_ui/pubspec.yaml b/packages/flutter_chat_ui/pubspec.yaml index 0ca1541c..e64c6007 100644 --- a/packages/flutter_chat_ui/pubspec.yaml +++ b/packages/flutter_chat_ui/pubspec.yaml @@ -16,7 +16,9 @@ dependencies: flutter: sdk: flutter flutter_chat_core: ^0.0.2 + permission_handler: ^11.3.1 provider: ^6.1.2 + record: ^5.2.0 dev_dependencies: flutter_lints: ^4.0.0 diff --git a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart index e69de29b..8b137891 100644 --- a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart +++ b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart @@ -0,0 +1 @@ + diff --git a/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart b/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart index e69de29b..8b137891 100644 --- a/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart +++ b/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart @@ -0,0 +1 @@ + diff --git a/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart b/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart index e69de29b..8b137891 100644 --- a/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart +++ b/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart @@ -0,0 +1 @@ + diff --git a/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart b/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart index e69de29b..8b137891 100644 --- a/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart +++ b/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart @@ -0,0 +1 @@ + diff --git a/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart b/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart index e69de29b..8b137891 100644 --- a/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart +++ b/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart @@ -0,0 +1 @@ + diff --git a/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart b/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart index e69de29b..8b137891 100644 --- a/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart +++ b/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart @@ -0,0 +1 @@ + From dfdd038cbdb1b1a315e9785ce8de08a35241e017 Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Sun, 10 Nov 2024 04:59:40 -0300 Subject: [PATCH 3/7] added audio builders --- examples/flyer_chat/lib/local.dart | 2 + examples/flyer_chat/lib/main.dart | 9 +- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + examples/flyer_chat/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + .../lib/src/models/builders.dart | 2 + .../lib/src/models/builders.freezed.dart | 25 +- .../lib/src/models/message.dart | 11 +- .../lib/src/models/message.freezed.dart | 390 ++++++++++++++++++ .../lib/src/models/message.g.dart | 30 ++ packages/flutter_chat_ui/lib/src/chat.dart | 1 + .../flutter_chat_ui/lib/src/chat_input.dart | 36 +- .../chat_message/chat_message_internal.dart | 24 +- .../lib/src/wave_animation.dart | 3 +- .../lib/src/flyer_chat_audio_message.dart | 66 +++ .../flyer_chat_audio_message/pubspec.yaml | 2 + .../lib/src/flyer_chat_image_message.dart | 18 +- 20 files changed, 593 insertions(+), 38 deletions(-) diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart index dd27129d..81d4ed1f 100644 --- a/examples/flyer_chat/lib/local.dart +++ b/examples/flyer_chat/lib/local.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:flyer_chat_audio_message/flyer_chat_audio_message.dart'; import 'package:flyer_chat_image_message/flyer_chat_image_message.dart'; import 'package:flyer_chat_text_message/flyer_chat_text_message.dart'; import 'package:image_picker/image_picker.dart'; @@ -38,6 +39,7 @@ class LocalState extends State { child: Chat( builders: Builders( textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message), + audioMessageBuilder: (context, message) => FlyerChatAudioMessage(message: message), imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message), ), chatController: _chatController, diff --git a/examples/flyer_chat/lib/main.dart b/examples/flyer_chat/lib/main.dart index 3bdde593..3eadd3e9 100644 --- a/examples/flyer_chat/lib/main.dart +++ b/examples/flyer_chat/lib/main.dart @@ -59,7 +59,8 @@ class _FlyerChatHomePageState extends State { final _dio = Dio(); User _author = const User(id: 'sender1'); final _chatIdController = TextEditingController(text: defaultChatId); - final _geminiApiKeyController = TextEditingController(text: defaultGeminiApiKey); + final _geminiApiKeyController = + TextEditingController(text: defaultGeminiApiKey); @override void dispose() { @@ -117,7 +118,8 @@ class _FlyerChatHomePageState extends State { children: [ ElevatedButton( onPressed: () { - getInitialMessages(_dio, chatId: _chatIdController.text).then((messages) { + getInitialMessages(_dio, chatId: _chatIdController.text) + .then((messages) { if (mounted && context.mounted) { Navigator.of(context).push( MaterialPageRoute( @@ -144,7 +146,8 @@ class _FlyerChatHomePageState extends State { actions: [ TextButton( child: const Text('OK'), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => + Navigator.of(context).pop(), ), ], ); diff --git a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc index b8f668e5..b42b2618 100644 --- a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc +++ b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); + audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/examples/flyer_chat/linux/flutter/generated_plugins.cmake b/examples/flyer_chat/linux/flutter/generated_plugins.cmake index 0eae5ce6..fb848f3a 100644 --- a/examples/flyer_chat/linux/flutter/generated_plugins.cmake +++ b/examples/flyer_chat/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux file_selector_linux isar_flutter_libs record_linux diff --git a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift index 65fbf823..d9caf934 100644 --- a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,14 @@ import FlutterMacOS import Foundation +import audioplayers_darwin import file_selector_macos import isar_flutter_libs import path_provider_foundation import record_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/examples/flyer_chat/pubspec.yaml b/examples/flyer_chat/pubspec.yaml index 914f16d8..3fe82e31 100644 --- a/examples/flyer_chat/pubspec.yaml +++ b/examples/flyer_chat/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_chat_core: ^0.0.2 flutter_chat_ui: ^2.0.0-dev.1 flutter_lorem: ^2.0.0 + flyer_chat_audio_message: ^0.0.2 flyer_chat_image_message: ^0.0.2 flyer_chat_text_message: ^0.0.2 google_generative_ai: ^0.4.5 diff --git a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc index 38602c46..439d5f4b 100644 --- a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc +++ b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/examples/flyer_chat/windows/flutter/generated_plugins.cmake b/examples/flyer_chat/windows/flutter/generated_plugins.cmake index 7140f4ff..1cc4d573 100644 --- a/examples/flyer_chat/windows/flutter/generated_plugins.cmake +++ b/examples/flyer_chat/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows file_selector_windows isar_flutter_libs permission_handler_windows diff --git a/packages/flutter_chat_core/lib/src/models/builders.dart b/packages/flutter_chat_core/lib/src/models/builders.dart index 4dd041ae..d2edecfd 100644 --- a/packages/flutter_chat_core/lib/src/models/builders.dart +++ b/packages/flutter_chat_core/lib/src/models/builders.dart @@ -6,6 +6,7 @@ import 'message.dart'; part 'builders.freezed.dart'; typedef TextMessageBuilder = Widget Function(BuildContext, TextMessage); +typedef AudioMessageBuilder = Widget Function(BuildContext, AudioMessage); typedef ImageMessageBuilder = Widget Function(BuildContext, ImageMessage); typedef UnsupportedMessageBuilder = Widget Function( BuildContext, @@ -34,6 +35,7 @@ class Builders with _$Builders { const factory Builders({ TextMessageBuilder? textMessageBuilder, ImageMessageBuilder? imageMessageBuilder, + AudioMessageBuilder? audioMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, InputBuilder? inputBuilder, ChatMessageBuilder? chatMessageBuilder, diff --git a/packages/flutter_chat_core/lib/src/models/builders.freezed.dart b/packages/flutter_chat_core/lib/src/models/builders.freezed.dart index ff9a9871..67966bcb 100644 --- a/packages/flutter_chat_core/lib/src/models/builders.freezed.dart +++ b/packages/flutter_chat_core/lib/src/models/builders.freezed.dart @@ -20,6 +20,8 @@ mixin _$Builders { throw _privateConstructorUsedError; ImageMessageBuilder? get imageMessageBuilder => throw _privateConstructorUsedError; + AudioMessageBuilder? get audioMessageBuilder => + throw _privateConstructorUsedError; UnsupportedMessageBuilder? get unsupportedMessageBuilder => throw _privateConstructorUsedError; InputBuilder? get inputBuilder => throw _privateConstructorUsedError; @@ -45,6 +47,7 @@ abstract class $BuildersCopyWith<$Res> { $Res call( {TextMessageBuilder? textMessageBuilder, ImageMessageBuilder? imageMessageBuilder, + AudioMessageBuilder? audioMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, InputBuilder? inputBuilder, ChatMessageBuilder? chatMessageBuilder, @@ -69,6 +72,7 @@ class _$BuildersCopyWithImpl<$Res, $Val extends Builders> $Res call({ Object? textMessageBuilder = freezed, Object? imageMessageBuilder = freezed, + Object? audioMessageBuilder = freezed, Object? unsupportedMessageBuilder = freezed, Object? inputBuilder = freezed, Object? chatMessageBuilder = freezed, @@ -84,6 +88,10 @@ class _$BuildersCopyWithImpl<$Res, $Val extends Builders> ? _value.imageMessageBuilder : imageMessageBuilder // ignore: cast_nullable_to_non_nullable as ImageMessageBuilder?, + audioMessageBuilder: freezed == audioMessageBuilder + ? _value.audioMessageBuilder + : audioMessageBuilder // ignore: cast_nullable_to_non_nullable + as AudioMessageBuilder?, unsupportedMessageBuilder: freezed == unsupportedMessageBuilder ? _value.unsupportedMessageBuilder : unsupportedMessageBuilder // ignore: cast_nullable_to_non_nullable @@ -119,6 +127,7 @@ abstract class _$$BuildersImplCopyWith<$Res> $Res call( {TextMessageBuilder? textMessageBuilder, ImageMessageBuilder? imageMessageBuilder, + AudioMessageBuilder? audioMessageBuilder, UnsupportedMessageBuilder? unsupportedMessageBuilder, InputBuilder? inputBuilder, ChatMessageBuilder? chatMessageBuilder, @@ -141,6 +150,7 @@ class __$$BuildersImplCopyWithImpl<$Res> $Res call({ Object? textMessageBuilder = freezed, Object? imageMessageBuilder = freezed, + Object? audioMessageBuilder = freezed, Object? unsupportedMessageBuilder = freezed, Object? inputBuilder = freezed, Object? chatMessageBuilder = freezed, @@ -156,6 +166,10 @@ class __$$BuildersImplCopyWithImpl<$Res> ? _value.imageMessageBuilder : imageMessageBuilder // ignore: cast_nullable_to_non_nullable as ImageMessageBuilder?, + audioMessageBuilder: freezed == audioMessageBuilder + ? _value.audioMessageBuilder + : audioMessageBuilder // ignore: cast_nullable_to_non_nullable + as AudioMessageBuilder?, unsupportedMessageBuilder: freezed == unsupportedMessageBuilder ? _value.unsupportedMessageBuilder : unsupportedMessageBuilder // ignore: cast_nullable_to_non_nullable @@ -186,6 +200,7 @@ class _$BuildersImpl extends _Builders { const _$BuildersImpl( {this.textMessageBuilder, this.imageMessageBuilder, + this.audioMessageBuilder, this.unsupportedMessageBuilder, this.inputBuilder, this.chatMessageBuilder, @@ -198,6 +213,8 @@ class _$BuildersImpl extends _Builders { @override final ImageMessageBuilder? imageMessageBuilder; @override + final AudioMessageBuilder? audioMessageBuilder; + @override final UnsupportedMessageBuilder? unsupportedMessageBuilder; @override final InputBuilder? inputBuilder; @@ -210,7 +227,7 @@ class _$BuildersImpl extends _Builders { @override String toString() { - return 'Builders(textMessageBuilder: $textMessageBuilder, imageMessageBuilder: $imageMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, inputBuilder: $inputBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder)'; + return 'Builders(textMessageBuilder: $textMessageBuilder, imageMessageBuilder: $imageMessageBuilder, audioMessageBuilder: $audioMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, inputBuilder: $inputBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder)'; } @override @@ -222,6 +239,8 @@ class _$BuildersImpl extends _Builders { other.textMessageBuilder == textMessageBuilder) && (identical(other.imageMessageBuilder, imageMessageBuilder) || other.imageMessageBuilder == imageMessageBuilder) && + (identical(other.audioMessageBuilder, audioMessageBuilder) || + other.audioMessageBuilder == audioMessageBuilder) && (identical(other.unsupportedMessageBuilder, unsupportedMessageBuilder) || other.unsupportedMessageBuilder == unsupportedMessageBuilder) && @@ -241,6 +260,7 @@ class _$BuildersImpl extends _Builders { runtimeType, textMessageBuilder, imageMessageBuilder, + audioMessageBuilder, unsupportedMessageBuilder, inputBuilder, chatMessageBuilder, @@ -260,6 +280,7 @@ abstract class _Builders extends Builders { const factory _Builders( {final TextMessageBuilder? textMessageBuilder, final ImageMessageBuilder? imageMessageBuilder, + final AudioMessageBuilder? audioMessageBuilder, final UnsupportedMessageBuilder? unsupportedMessageBuilder, final InputBuilder? inputBuilder, final ChatMessageBuilder? chatMessageBuilder, @@ -272,6 +293,8 @@ abstract class _Builders extends Builders { @override ImageMessageBuilder? get imageMessageBuilder; @override + AudioMessageBuilder? get audioMessageBuilder; + @override UnsupportedMessageBuilder? get unsupportedMessageBuilder; @override InputBuilder? get inputBuilder; diff --git a/packages/flutter_chat_core/lib/src/models/message.dart b/packages/flutter_chat_core/lib/src/models/message.dart index 40a89650..d4bdbd0a 100644 --- a/packages/flutter_chat_core/lib/src/models/message.dart +++ b/packages/flutter_chat_core/lib/src/models/message.dart @@ -30,6 +30,14 @@ sealed class Message with _$Message { double? height, }) = ImageMessage; + const factory Message.audio({ + required String id, + required User author, + Map? metadata, + @EpochDateTimeConverter() required DateTime createdAt, + required String audioFile, + }) = AudioMessage; + const factory Message.unsupported({ required String id, required User author, @@ -39,6 +47,5 @@ sealed class Message with _$Message { const Message._(); - factory Message.fromJson(Map json) => - _$MessageFromJson(json); + factory Message.fromJson(Map json) => _$MessageFromJson(json); } diff --git a/packages/flutter_chat_core/lib/src/models/message.freezed.dart b/packages/flutter_chat_core/lib/src/models/message.freezed.dart index eacfcc68..14072447 100644 --- a/packages/flutter_chat_core/lib/src/models/message.freezed.dart +++ b/packages/flutter_chat_core/lib/src/models/message.freezed.dart @@ -20,6 +20,8 @@ Message _$MessageFromJson(Map json) { return TextMessage.fromJson(json); case 'image': return ImageMessage.fromJson(json); + case 'audio': + return AudioMessage.fromJson(json); default: return UnsupportedMessage.fromJson(json); @@ -54,6 +56,13 @@ mixin _$Message { double? width, double? height) image, + required TResult Function( + String id, + User author, + Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, + String audioFile) + audio, required TResult Function( String id, User author, @@ -83,6 +92,9 @@ mixin _$Message { double? width, double? height)? image, + TResult? Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult? Function( String id, User author, @@ -112,6 +124,9 @@ mixin _$Message { double? width, double? height)? image, + TResult Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult Function( String id, User author, @@ -125,6 +140,7 @@ mixin _$Message { TResult map({ required TResult Function(TextMessage value) text, required TResult Function(ImageMessage value) image, + required TResult Function(AudioMessage value) audio, required TResult Function(UnsupportedMessage value) unsupported, }) => throw _privateConstructorUsedError; @@ -132,6 +148,7 @@ mixin _$Message { TResult? mapOrNull({ TResult? Function(TextMessage value)? text, TResult? Function(ImageMessage value)? image, + TResult? Function(AudioMessage value)? audio, TResult? Function(UnsupportedMessage value)? unsupported, }) => throw _privateConstructorUsedError; @@ -139,6 +156,7 @@ mixin _$Message { TResult maybeMap({ TResult Function(TextMessage value)? text, TResult Function(ImageMessage value)? image, + TResult Function(AudioMessage value)? audio, TResult Function(UnsupportedMessage value)? unsupported, required TResult orElse(), }) => @@ -400,6 +418,13 @@ class _$TextMessageImpl extends TextMessage { double? width, double? height) image, + required TResult Function( + String id, + User author, + Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, + String audioFile) + audio, required TResult Function( String id, User author, @@ -432,6 +457,9 @@ class _$TextMessageImpl extends TextMessage { double? width, double? height)? image, + TResult? Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult? Function( String id, User author, @@ -464,6 +492,9 @@ class _$TextMessageImpl extends TextMessage { double? width, double? height)? image, + TResult Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult Function( String id, User author, @@ -483,6 +514,7 @@ class _$TextMessageImpl extends TextMessage { TResult map({ required TResult Function(TextMessage value) text, required TResult Function(ImageMessage value) image, + required TResult Function(AudioMessage value) audio, required TResult Function(UnsupportedMessage value) unsupported, }) { return text(this); @@ -493,6 +525,7 @@ class _$TextMessageImpl extends TextMessage { TResult? mapOrNull({ TResult? Function(TextMessage value)? text, TResult? Function(ImageMessage value)? image, + TResult? Function(AudioMessage value)? audio, TResult? Function(UnsupportedMessage value)? unsupported, }) { return text?.call(this); @@ -503,6 +536,7 @@ class _$TextMessageImpl extends TextMessage { TResult maybeMap({ TResult Function(TextMessage value)? text, TResult Function(ImageMessage value)? image, + TResult Function(AudioMessage value)? audio, TResult Function(UnsupportedMessage value)? unsupported, required TResult orElse(), }) { @@ -760,6 +794,13 @@ class _$ImageMessageImpl extends ImageMessage { double? width, double? height) image, + required TResult Function( + String id, + User author, + Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, + String audioFile) + audio, required TResult Function( String id, User author, @@ -793,6 +834,9 @@ class _$ImageMessageImpl extends ImageMessage { double? width, double? height)? image, + TResult? Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult? Function( String id, User author, @@ -826,6 +870,9 @@ class _$ImageMessageImpl extends ImageMessage { double? width, double? height)? image, + TResult Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult Function( String id, User author, @@ -846,6 +893,7 @@ class _$ImageMessageImpl extends ImageMessage { TResult map({ required TResult Function(TextMessage value) text, required TResult Function(ImageMessage value) image, + required TResult Function(AudioMessage value) audio, required TResult Function(UnsupportedMessage value) unsupported, }) { return image(this); @@ -856,6 +904,7 @@ class _$ImageMessageImpl extends ImageMessage { TResult? mapOrNull({ TResult? Function(TextMessage value)? text, TResult? Function(ImageMessage value)? image, + TResult? Function(AudioMessage value)? audio, TResult? Function(UnsupportedMessage value)? unsupported, }) { return image?.call(this); @@ -866,6 +915,7 @@ class _$ImageMessageImpl extends ImageMessage { TResult maybeMap({ TResult Function(TextMessage value)? text, TResult Function(ImageMessage value)? image, + TResult Function(AudioMessage value)? audio, TResult Function(UnsupportedMessage value)? unsupported, required TResult orElse(), }) { @@ -922,6 +972,330 @@ abstract class ImageMessage extends Message { throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$AudioMessageImplCopyWith<$Res> + implements $MessageCopyWith<$Res> { + factory _$$AudioMessageImplCopyWith( + _$AudioMessageImpl value, $Res Function(_$AudioMessageImpl) then) = + __$$AudioMessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + User author, + Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, + String audioFile}); + + @override + $UserCopyWith<$Res> get author; +} + +/// @nodoc +class __$$AudioMessageImplCopyWithImpl<$Res> + extends _$MessageCopyWithImpl<$Res, _$AudioMessageImpl> + implements _$$AudioMessageImplCopyWith<$Res> { + __$$AudioMessageImplCopyWithImpl( + _$AudioMessageImpl _value, $Res Function(_$AudioMessageImpl) _then) + : super(_value, _then); + + /// Create a copy of Message + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? author = null, + Object? metadata = freezed, + Object? createdAt = null, + Object? audioFile = null, + }) { + return _then(_$AudioMessageImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + author: null == author + ? _value.author + : author // ignore: cast_nullable_to_non_nullable + as User, + metadata: freezed == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + audioFile: null == audioFile + ? _value.audioFile + : audioFile // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AudioMessageImpl extends AudioMessage { + const _$AudioMessageImpl( + {required this.id, + required this.author, + final Map? metadata, + @EpochDateTimeConverter() required this.createdAt, + required this.audioFile, + final String? $type}) + : _metadata = metadata, + $type = $type ?? 'audio', + super._(); + + factory _$AudioMessageImpl.fromJson(Map json) => + _$$AudioMessageImplFromJson(json); + + @override + final String id; + @override + final User author; + final Map? _metadata; + @override + Map? get metadata { + final value = _metadata; + if (value == null) return null; + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + @EpochDateTimeConverter() + final DateTime createdAt; + @override + final String audioFile; + + @JsonKey(name: 'type') + final String $type; + + @override + String toString() { + return 'Message.audio(id: $id, author: $author, metadata: $metadata, createdAt: $createdAt, audioFile: $audioFile)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AudioMessageImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.author, author) || other.author == author) && + const DeepCollectionEquality().equals(other._metadata, _metadata) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.audioFile, audioFile) || + other.audioFile == audioFile)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, author, + const DeepCollectionEquality().hash(_metadata), createdAt, audioFile); + + /// Create a copy of Message + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AudioMessageImplCopyWith<_$AudioMessageImpl> get copyWith => + __$$AudioMessageImplCopyWithImpl<_$AudioMessageImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata, + String text, + LinkPreview? linkPreview) + text, + required TResult Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata, + String source, + String? thumbhash, + String? blurhash, + double? width, + double? height) + image, + required TResult Function( + String id, + User author, + Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, + String audioFile) + audio, + required TResult Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata) + unsupported, + }) { + return audio(id, author, metadata, createdAt, audioFile); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata, + String text, + LinkPreview? linkPreview)? + text, + TResult? Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata, + String source, + String? thumbhash, + String? blurhash, + double? width, + double? height)? + image, + TResult? Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, + TResult? Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata)? + unsupported, + }) { + return audio?.call(id, author, metadata, createdAt, audioFile); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata, + String text, + LinkPreview? linkPreview)? + text, + TResult Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata, + String source, + String? thumbhash, + String? blurhash, + double? width, + double? height)? + image, + TResult Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, + TResult Function( + String id, + User author, + @EpochDateTimeConverter() DateTime createdAt, + Map? metadata)? + unsupported, + required TResult orElse(), + }) { + if (audio != null) { + return audio(id, author, metadata, createdAt, audioFile); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(TextMessage value) text, + required TResult Function(ImageMessage value) image, + required TResult Function(AudioMessage value) audio, + required TResult Function(UnsupportedMessage value) unsupported, + }) { + return audio(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(TextMessage value)? text, + TResult? Function(ImageMessage value)? image, + TResult? Function(AudioMessage value)? audio, + TResult? Function(UnsupportedMessage value)? unsupported, + }) { + return audio?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(TextMessage value)? text, + TResult Function(ImageMessage value)? image, + TResult Function(AudioMessage value)? audio, + TResult Function(UnsupportedMessage value)? unsupported, + required TResult orElse(), + }) { + if (audio != null) { + return audio(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$AudioMessageImplToJson( + this, + ); + } +} + +abstract class AudioMessage extends Message { + const factory AudioMessage( + {required final String id, + required final User author, + final Map? metadata, + @EpochDateTimeConverter() required final DateTime createdAt, + required final String audioFile}) = _$AudioMessageImpl; + const AudioMessage._() : super._(); + + factory AudioMessage.fromJson(Map json) = + _$AudioMessageImpl.fromJson; + + @override + String get id; + @override + User get author; + @override + Map? get metadata; + @override + @EpochDateTimeConverter() + DateTime get createdAt; + String get audioFile; + + /// Create a copy of Message + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AudioMessageImplCopyWith<_$AudioMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc abstract class _$$UnsupportedMessageImplCopyWith<$Res> implements $MessageCopyWith<$Res> { @@ -1068,6 +1442,13 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage { double? width, double? height) image, + required TResult Function( + String id, + User author, + Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, + String audioFile) + audio, required TResult Function( String id, User author, @@ -1100,6 +1481,9 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage { double? width, double? height)? image, + TResult? Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult? Function( String id, User author, @@ -1132,6 +1516,9 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage { double? width, double? height)? image, + TResult Function(String id, User author, Map? metadata, + @EpochDateTimeConverter() DateTime createdAt, String audioFile)? + audio, TResult Function( String id, User author, @@ -1151,6 +1538,7 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage { TResult map({ required TResult Function(TextMessage value) text, required TResult Function(ImageMessage value) image, + required TResult Function(AudioMessage value) audio, required TResult Function(UnsupportedMessage value) unsupported, }) { return unsupported(this); @@ -1161,6 +1549,7 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage { TResult? mapOrNull({ TResult? Function(TextMessage value)? text, TResult? Function(ImageMessage value)? image, + TResult? Function(AudioMessage value)? audio, TResult? Function(UnsupportedMessage value)? unsupported, }) { return unsupported?.call(this); @@ -1171,6 +1560,7 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage { TResult maybeMap({ TResult Function(TextMessage value)? text, TResult Function(ImageMessage value)? image, + TResult Function(AudioMessage value)? audio, TResult Function(UnsupportedMessage value)? unsupported, required TResult orElse(), }) { diff --git a/packages/flutter_chat_core/lib/src/models/message.g.dart b/packages/flutter_chat_core/lib/src/models/message.g.dart index 359927c5..987bcfbd 100644 --- a/packages/flutter_chat_core/lib/src/models/message.g.dart +++ b/packages/flutter_chat_core/lib/src/models/message.g.dart @@ -78,6 +78,36 @@ Map _$$ImageMessageImplToJson(_$ImageMessageImpl instance) { return val; } +_$AudioMessageImpl _$$AudioMessageImplFromJson(Map json) => + _$AudioMessageImpl( + id: json['id'] as String, + author: User.fromJson(json['author'] as Map), + metadata: json['metadata'] as Map?, + createdAt: const EpochDateTimeConverter() + .fromJson((json['createdAt'] as num).toInt()), + audioFile: json['audioFile'] as String, + $type: json['type'] as String?, + ); + +Map _$$AudioMessageImplToJson(_$AudioMessageImpl instance) { + final val = { + 'id': instance.id, + 'author': instance.author.toJson(), + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('metadata', instance.metadata); + val['createdAt'] = const EpochDateTimeConverter().toJson(instance.createdAt); + val['audioFile'] = instance.audioFile; + val['type'] = instance.$type; + return val; +} + _$UnsupportedMessageImpl _$$UnsupportedMessageImplFromJson( Map json) => _$UnsupportedMessageImpl( diff --git a/packages/flutter_chat_ui/lib/src/chat.dart b/packages/flutter_chat_ui/lib/src/chat.dart index 5373c89e..42884204 100644 --- a/packages/flutter_chat_ui/lib/src/chat.dart +++ b/packages/flutter_chat_ui/lib/src/chat.dart @@ -105,6 +105,7 @@ class _ChatState extends State with WidgetsBindingObserver { Provider.value(value: _builders), Provider.value(value: _crossCache), Provider.value(value: widget.onMessageSend), + Provider.value(value: widget.onAudioSend), Provider.value(value: widget.onMessageTap), Provider.value(value: widget.onAttachmentTap), ChangeNotifierProvider(create: (_) => ChatInputHeightNotifier()), diff --git a/packages/flutter_chat_ui/lib/src/chat_input.dart b/packages/flutter_chat_ui/lib/src/chat_input.dart index 9e5ce2f5..162bd72b 100644 --- a/packages/flutter_chat_ui/lib/src/chat_input.dart +++ b/packages/flutter_chat_ui/lib/src/chat_input.dart @@ -1,3 +1,4 @@ +import 'dart:developer' as developer; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -97,7 +98,8 @@ class _ChatInputState extends State { @override Widget build(BuildContext context) { - final backgroundColor = context.select((ChatTheme theme) => theme.backgroundColor); + final backgroundColor = + context.select((ChatTheme theme) => theme.backgroundColor); final inputTheme = context.select((ChatTheme theme) => theme.inputTheme); final onAttachmentTap = context.read(); @@ -156,7 +158,9 @@ class _ChatInputState extends State { shape: BoxShape.circle, ), child: IconButton( - icon: value.text.isNotEmpty || _isRecording ? widget.sendIcon! : widget.audioIcon!, + icon: value.text.isNotEmpty || _isRecording + ? widget.sendIcon! + : widget.audioIcon!, color: inputTheme.hintStyle?.color, onPressed: () async { if (_isRecording) { @@ -182,7 +186,8 @@ class _ChatInputState extends State { } Widget _buildRecordingIndicator() { - final streamAmplitude = _audioRecorder.onAmplitudeChanged(const Duration(seconds: 1)); + final streamAmplitude = + _audioRecorder.onAmplitudeChanged(const Duration(seconds: 1)); return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -190,11 +195,14 @@ class _ChatInputState extends State { const Icon(Icons.circle, color: Colors.red), const SizedBox(width: 8), StreamBuilder( - stream: Stream.periodic(const Duration(seconds: 1), (count) => Duration(seconds: count)), + stream: Stream.periodic( + const Duration(seconds: 1), (count) => Duration(seconds: count),), builder: (context, snapshot) { final duration = snapshot.data ?? Duration.zero; - final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + final minutes = + duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = + duration.inSeconds.remainder(60).toString().padLeft(2, '0'); return Row( children: [ Text( @@ -213,9 +221,9 @@ class _ChatInputState extends State { final max = snapshot.data?.max ?? 0.0; final audioLevel = (current / max).clamp(0.0, 1.0); - print("current: $current"); - print("max $max"); - print("audioLevel: ${audioLevel}"); + developer.log('current: $current', name: 'ChatInput'); + developer.log('max: $max', name: 'ChatInput'); + developer.log('audioLevel: $audioLevel', name: 'ChatInput'); return BarAnimation( color: Colors.red, @@ -232,9 +240,12 @@ class _ChatInputState extends State { } void _updateInputHeight() { - final renderBox = _inputKey.currentContext?.findRenderObject() as RenderBox?; + final renderBox = + _inputKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { - context.read().updateHeight(renderBox.size.height); + context + .read() + .updateHeight(renderBox.size.height); } } @@ -268,7 +279,8 @@ class _ChatInputState extends State { _recordedAudioPath = 'audio.m4a'; - await _audioRecorder.start(const RecordConfig(), path: _recordedAudioPath!); + await _audioRecorder.start(const RecordConfig(), + path: _recordedAudioPath!,); } } catch (e) { debugPrint('Error during audio recording: $e'); diff --git a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart index 360b4456..073e7b4d 100644 --- a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart +++ b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart @@ -7,11 +7,18 @@ import 'package:provider/provider.dart'; import '../simple_text_message.dart'; import 'chat_message.dart'; +/// A widget that represents an internal chat message with animation and update capabilities. class ChatMessageInternal extends StatefulWidget { + /// Animation for the message. final Animation animation; + + /// The message to be displayed. final Message message; + + /// Indicates if the message is removed. final bool? isRemoved; + /// Creates a [ChatMessageInternal] widget. const ChatMessageInternal({ super.key, required this.animation, @@ -23,6 +30,7 @@ class ChatMessageInternal extends StatefulWidget { State createState() => ChatMessageInternalState(); } +/// State for [ChatMessageInternal] that handles message updates and rendering. class ChatMessageInternalState extends State { late StreamSubscription? _operationsSubscription; late Message _updatedMessage; @@ -36,8 +44,7 @@ class ChatMessageInternalState extends State { if (widget.isRemoved == true) { _operationsSubscription = null; } else { - final chatController = - Provider.of(context, listen: false); + final chatController = Provider.of(context, listen: false); _operationsSubscription = chatController.operationsStream.listen((event) { switch (event.type) { case ChatOperationType.update: @@ -63,8 +70,8 @@ class ChatMessageInternalState extends State { @override void dispose() { - super.dispose(); _operationsSubscription?.cancel(); + super.dispose(); } @override @@ -92,11 +99,9 @@ class ChatMessageInternalState extends State { ) { switch (message) { case TextMessage(): - return builders.textMessageBuilder?.call(context, message) ?? - SimpleTextMessage(message: message); + return builders.textMessageBuilder?.call(context, message) ?? SimpleTextMessage(message: message); case ImageMessage(): - final result = builders.imageMessageBuilder?.call(context, message) ?? - const SizedBox.shrink(); + final result = builders.imageMessageBuilder?.call(context, message) ?? const SizedBox.shrink(); assert( !(result is SizedBox && result.width == 0 && result.height == 0), 'You are trying to display an image message but you have not provided an imageMessageBuilder. ' @@ -104,6 +109,11 @@ class ChatMessageInternalState extends State { 'If you want to use default image message widget, install flyer_chat_image_message package and use FlyerChatImageMessage widget.', ); return result; + case AudioMessage(): + return builders.audioMessageBuilder?.call(context, message) ?? + const Text( + 'Audio message received.', + ); case UnsupportedMessage(): return builders.unsupportedMessageBuilder?.call(context, message) ?? const Text( diff --git a/packages/flutter_chat_ui/lib/src/wave_animation.dart b/packages/flutter_chat_ui/lib/src/wave_animation.dart index c49b0af4..4d9d1a8b 100644 --- a/packages/flutter_chat_ui/lib/src/wave_animation.dart +++ b/packages/flutter_chat_ui/lib/src/wave_animation.dart @@ -12,7 +12,8 @@ class BarAnimation extends StatelessWidget { final double audioLevel; /// Creates a [BarAnimation] widget. - const BarAnimation({super.key, required this.color, required this.audioLevel}); + const BarAnimation( + {super.key, required this.color, required this.audioLevel,}); @override Widget build(BuildContext context) { diff --git a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart index 8b137891..e966fc2f 100644 --- a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart +++ b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart @@ -1 +1,67 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; + +class FlyerChatAudioMessage extends StatefulWidget { + final AudioMessage message; + final BorderRadiusGeometry? borderRadius; + final BoxConstraints? constraints; + + const FlyerChatAudioMessage({ + super.key, + required this.message, + this.borderRadius = const BorderRadius.all(Radius.circular(12)), + this.constraints = const BoxConstraints(maxWidth: 300, minHeight: 50), + }); + + @override + FlyerChatAudioMessageState createState() => FlyerChatAudioMessageState(); +} + +class FlyerChatAudioMessageState extends State { + final AudioPlayer _audioPlayer = AudioPlayer(); + bool _isPlaying = false; + + @override + void dispose() { + _audioPlayer.dispose(); + super.dispose(); + } + + void _togglePlayPause() async { + if (_isPlaying) { + await _audioPlayer.pause(); + } else { + await _audioPlayer.play(DeviceFileSource(widget.message.audioFile)); + } + setState(() { + _isPlaying = !_isPlaying; + }); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: widget.borderRadius ?? BorderRadius.zero, + child: Container( + constraints: widget.constraints, + color: Colors.grey[200], + child: Row( + children: [ + IconButton( + icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), + onPressed: _togglePlayPause, + ), + const Expanded( + child: Text( + 'Audio Message', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flyer_chat_audio_message/pubspec.yaml b/packages/flyer_chat_audio_message/pubspec.yaml index fdac7885..4b5119a5 100644 --- a/packages/flyer_chat_audio_message/pubspec.yaml +++ b/packages/flyer_chat_audio_message/pubspec.yaml @@ -10,8 +10,10 @@ environment: flutter: ">=3.16.0" dependencies: + audioplayers: ^6.1.0 flutter: sdk: flutter + flutter_chat_core: ^0.0.2 dev_dependencies: flutter_lints: ^4.0.0 diff --git a/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart b/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart index 24d6610b..67f0d33e 100644 --- a/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart +++ b/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart @@ -6,8 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:image/image.dart' show encodeJpg; import 'package:provider/provider.dart'; -import 'package:thumbhash/thumbhash.dart' - show rgbaToBmp, thumbHashToApproximateAspectRatio, thumbHashToRGBA; +import 'package:thumbhash/thumbhash.dart' show rgbaToBmp, thumbHashToApproximateAspectRatio, thumbHashToRGBA; import 'custom_network_image.dart'; import 'preload_image_provider.dart'; @@ -28,8 +27,7 @@ class FlyerChatImageMessage extends StatefulWidget { FlyerChatImageMessageState createState() => FlyerChatImageMessageState(); } -class FlyerChatImageMessageState extends State - with TickerProviderStateMixin { +class FlyerChatImageMessageState extends State with TickerProviderStateMixin { late ChatController _chatController; late CustomNetworkImage _customNetworkImage; late double _aspectRatio; @@ -42,8 +40,7 @@ class FlyerChatImageMessageState extends State if (widget.message.width != null && widget.message.height != null) { _aspectRatio = widget.message.width! / widget.message.height!; } else if (widget.message.thumbhash != null) { - final thumbhashBytes = - base64.decode(base64.normalize(widget.message.thumbhash!)); + final thumbhashBytes = base64.decode(base64.normalize(widget.message.thumbhash!)); _aspectRatio = thumbHashToApproximateAspectRatio(thumbhashBytes); @@ -84,10 +81,8 @@ class FlyerChatImageMessageState extends State @override Widget build(BuildContext context) { - final backgroundColor = - context.select((ChatTheme theme) => theme.backgroundColor); - final imagePlaceholderColor = - context.select((ChatTheme theme) => theme.imagePlaceholderColor); + final backgroundColor = context.select((ChatTheme theme) => theme.backgroundColor); + final imagePlaceholderColor = context.select((ChatTheme theme) => theme.imagePlaceholderColor); return ClipRRect( borderRadius: widget.borderRadius ?? BorderRadius.zero, @@ -118,8 +113,7 @@ class FlyerChatImageMessageState extends State child: CircularProgressIndicator( color: backgroundColor.withOpacity(0.5), value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! + ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); From d546a37f1097ef337a57c66482badb58f278d7e4 Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Sun, 10 Nov 2024 21:18:24 -0300 Subject: [PATCH 4/7] added audio player --- examples/flyer_chat/ios/Podfile.lock | 6 + examples/flyer_chat/lib/api.dart | 10 +- examples/flyer_chat/lib/create_message.dart | 104 +++++++++--------- examples/flyer_chat/lib/gemini.dart | 18 +-- .../flyer_chat/lib/hive_chat_controller.dart | 22 +--- examples/flyer_chat/lib/local.dart | 14 +++ packages/flutter_chat_ui/lib/src/chat.dart | 2 +- .../flutter_chat_ui/lib/src/chat_input.dart | 33 +++--- 8 files changed, 99 insertions(+), 110 deletions(-) diff --git a/examples/flyer_chat/ios/Podfile.lock b/examples/flyer_chat/ios/Podfile.lock index 39e0eacd..426e7443 100644 --- a/examples/flyer_chat/ios/Podfile.lock +++ b/examples/flyer_chat/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - audioplayers_darwin (0.0.1): + - Flutter - Flutter (1.0.0) - image_picker_ios (0.0.1): - Flutter @@ -13,6 +15,7 @@ PODS: - Flutter DEPENDENCIES: + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - Flutter (from `Flutter`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) @@ -21,6 +24,8 @@ DEPENDENCIES: - record_darwin (from `.symlinks/plugins/record_darwin/ios`) EXTERNAL SOURCES: + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/ios" Flutter: :path: Flutter image_picker_ios: @@ -35,6 +40,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/record_darwin/ios" SPEC CHECKSUMS: + audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 diff --git a/examples/flyer_chat/lib/api.dart b/examples/flyer_chat/lib/api.dart index 2b834433..863f4baa 100644 --- a/examples/flyer_chat/lib/api.dart +++ b/examples/flyer_chat/lib/api.dart @@ -58,10 +58,8 @@ class ApiState extends State { return Scaffold( body: Chat( builders: Builders( - textMessageBuilder: (context, message) => - FlyerChatTextMessage(message: message), - imageMessageBuilder: (context, message) => - FlyerChatImageMessage(message: message), + textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message), + imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message), ), chatController: _chatController, user: widget.author, @@ -145,7 +143,7 @@ class ApiState extends State { }); } - void _addItem(String? text) async { + Future _addItem(String? text) async { final message = await createMessage(widget.author, widget.dio, text: text); if (mounted) { @@ -159,8 +157,6 @@ class ApiState extends State { ); if (mounted) { - // Make sure to get the updated message - // (width and height might have been set by the image message widget) final possiblyUpdatedMessage = _chatController.messages.firstWhere( (element) => element.id == message.id, orElse: () => message, diff --git a/examples/flyer_chat/lib/create_message.dart b/examples/flyer_chat/lib/create_message.dart index a8651740..3d793557 100644 --- a/examples/flyer_chat/lib/create_message.dart +++ b/examples/flyer_chat/lib/create_message.dart @@ -5,67 +5,67 @@ import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_lorem/flutter_lorem.dart'; import 'package:uuid/uuid.dart'; +enum MessageType { text, image, audio } + Future createMessage( User author, Dio dio, { - bool? textOnly, + MessageType type = MessageType.text, String? text, + String filePath = 'audio.mp3', }) async { const uuid = Uuid(); - Message message; - if (Random().nextBool() || textOnly == true || text != null) { - message = TextMessage( - id: uuid.v4(), - author: author, - createdAt: DateTime.now().toUtc(), - text: text ?? lorem(paragraphs: 1, words: Random().nextInt(30) + 1), - ); - } else { - final orientation = ['portrait', 'square', 'wide'][Random().nextInt(3)]; - late double width, height; + switch (type) { + case MessageType.text: + return TextMessage( + id: uuid.v4(), + author: author, + createdAt: DateTime.now().toUtc(), + text: text ?? lorem(paragraphs: 1, words: Random().nextInt(30) + 1), + ); + case MessageType.image: + final orientation = ['portrait', 'square', 'wide'][Random().nextInt(3)]; + late double width, height; - if (orientation == 'portrait') { - width = 200; - height = 400; - } else if (orientation == 'square') { - width = 200; - height = 200; - } else { - width = 400; - height = 200; - } + if (orientation == 'portrait') { + width = 200; + height = 400; + } else if (orientation == 'square') { + width = 200; + height = 200; + } else { + width = 400; + height = 200; + } - final response = await dio.get( - 'https://whatever.diamanthq.dev/image?w=${width.toInt()}&h=${height.toInt()}&seed=${Random().nextInt(501)}', - options: Options( - headers: { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - 'Accept': '*/*', - }, - ), - ); + final response = await dio.get( + 'https://whatever.diamanthq.dev/image?w=${width.toInt()}&h=${height.toInt()}&seed=${Random().nextInt(501)}', + options: Options( + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + 'Accept': '*/*', + }, + ), + ); - message = ImageMessage( - id: uuid.v4(), - author: author, - createdAt: DateTime.now().toUtc(), - source: response.data['img'], - thumbhash: response.data['thumbhash'], - blurhash: response.data['blurhash'], - ); + return ImageMessage( + id: uuid.v4(), + author: author, + createdAt: DateTime.now().toUtc(), + source: response.data['img'], + thumbhash: response.data['thumbhash'], + blurhash: response.data['blurhash'], + ); + case MessageType.audio: + return AudioMessage( + id: uuid.v4(), + author: author, + createdAt: DateTime.now().toUtc(), + audioFile: filePath, + ); + default: + throw ArgumentError('Invalid message type: $type'); } - - // return ImageMessage( - // id: uuid.v4(), - // author: author, - // createdAt: DateTime.now().toUtc(), - // source: - // 'https://www.hdcarwallpapers.com/walls/audi_r8_spyder_v10_performance_rwd_2021_4k_8k-HD.jpg', - // thumbhash: '2gcODIKwdmg9eId1l4qTb2v4xw', - // blurhash: 'LPFFjU00^+IV~W4n%LRkROM|WBxu', - // ); - - return message; } diff --git a/examples/flyer_chat/lib/gemini.dart b/examples/flyer_chat/lib/gemini.dart index 95c12920..e555cc73 100644 --- a/examples/flyer_chat/lib/gemini.dart +++ b/examples/flyer_chat/lib/gemini.dart @@ -45,10 +45,7 @@ class GeminiState extends State { ); _chatSession = _model.startChat( - history: _chatController.messages - .whereType() - .map((message) => Content.text(message.text)) - .toList(), + history: _chatController.messages.whereType().map((message) => Content.text(message.text)).toList(), ); } @@ -65,10 +62,8 @@ class GeminiState extends State { return Scaffold( body: Chat( builders: Builders( - textMessageBuilder: (context, message) => - FlyerChatTextMessage(message: message), - imageMessageBuilder: (context, message) => - FlyerChatImageMessage(message: message), + textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message), + imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message), ), chatController: _chatController, crossCache: _crossCache, @@ -156,8 +151,7 @@ class GeminiState extends State { ); await _chatController.insert(_currentGeminiResponse!); } else { - final newUpdatedMessage = (_currentGeminiResponse as TextMessage) - .copyWith(text: accumulatedText); + final newUpdatedMessage = (_currentGeminiResponse as TextMessage).copyWith(text: accumulatedText); await _chatController.update( _currentGeminiResponse!, newUpdatedMessage, @@ -168,9 +162,7 @@ class GeminiState extends State { // as soon as message that is being generated reaches top of the viewport WidgetsBinding.instance.addPostFrameCallback((_) { if (!_scrollController.hasClients || !mounted) return; - if ((_scrollController.position.maxScrollExtent - - initialMaxScrollExtent) < - viewportDimension) { + if ((_scrollController.position.maxScrollExtent - initialMaxScrollExtent) < viewportDimension) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 250), diff --git a/examples/flyer_chat/lib/hive_chat_controller.dart b/examples/flyer_chat/lib/hive_chat_controller.dart index dec7c961..a41c336b 100644 --- a/examples/flyer_chat/lib/hive_chat_controller.dart +++ b/examples/flyer_chat/lib/hive_chat_controller.dart @@ -26,11 +26,7 @@ class HiveChatController implements ChatController { @override Future remove(Message message) async { - final index = _box - .getRange(0, _box.length) - .map((json) => Message.fromJson(json)) - .toList() - .indexOf(message); + final index = _box.getRange(0, _box.length).map((json) => Message.fromJson(json)).toList().indexOf(message); if (index > -1) { _box.write(() { @@ -44,11 +40,7 @@ class HiveChatController implements ChatController { Future update(Message oldMessage, Message newMessage) async { if (oldMessage == newMessage) return; - final index = _box - .getRange(0, _box.length) - .map((json) => Message.fromJson(json)) - .toList() - .indexOf(oldMessage); + final index = _box.getRange(0, _box.length).map((json) => Message.fromJson(json)).toList().indexOf(oldMessage); if (index > -1) { _box.write(() { @@ -67,10 +59,7 @@ class HiveChatController implements ChatController { } else { _box.write(() { _box.putAll( - messages - .map((message) => {message.id: message.toJson()}) - .toList() - .reduce((acc, map) => {...acc, ...map}), + messages.map((message) => {message.id: message.toJson()}).toList().reduce((acc, map) => {...acc, ...map}), ); _operationsController.add(ChatOperation.set()); }); @@ -79,10 +68,7 @@ class HiveChatController implements ChatController { @override List get messages { - return _box - .getRange(0, _box.length) - .map((json) => Message.fromJson(json)) - .toList(); + return _box.getRange(0, _box.length).map((json) => Message.fromJson(json)).toList(); } @override diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart index 81d4ed1f..17971f6a 100644 --- a/examples/flyer_chat/lib/local.dart +++ b/examples/flyer_chat/lib/local.dart @@ -45,6 +45,7 @@ class LocalState extends State { chatController: _chatController, user: widget.author, onMessageSend: _addItem, + onAudioSend: _addAudio, onMessageTap: _removeItem, onAttachmentTap: _handleAttachmentTap, ), @@ -102,4 +103,17 @@ class LocalState extends State { await _chatController.insert(imageMessage); } } + + Future _addAudio(String filePath) async { + final message = await createMessage( + widget.author, + widget.dio, + type: MessageType.audio, + filePath: filePath, + ); + + if (mounted) { + await _chatController.insert(message); + } + } } diff --git a/packages/flutter_chat_ui/lib/src/chat.dart b/packages/flutter_chat_ui/lib/src/chat.dart index 42884204..30a6f2b9 100644 --- a/packages/flutter_chat_ui/lib/src/chat.dart +++ b/packages/flutter_chat_ui/lib/src/chat.dart @@ -35,8 +35,8 @@ class Chat extends StatefulWidget { this.darkTheme, this.themeMode = ThemeMode.system, this.onMessageSend, - this.onMessageTap, this.onAudioSend, + this.onMessageTap, this.onAttachmentTap, }); diff --git a/packages/flutter_chat_ui/lib/src/chat_input.dart b/packages/flutter_chat_ui/lib/src/chat_input.dart index 162bd72b..448011db 100644 --- a/packages/flutter_chat_ui/lib/src/chat_input.dart +++ b/packages/flutter_chat_ui/lib/src/chat_input.dart @@ -98,8 +98,7 @@ class _ChatInputState extends State { @override Widget build(BuildContext context) { - final backgroundColor = - context.select((ChatTheme theme) => theme.backgroundColor); + final backgroundColor = context.select((ChatTheme theme) => theme.backgroundColor); final inputTheme = context.select((ChatTheme theme) => theme.inputTheme); final onAttachmentTap = context.read(); @@ -158,9 +157,7 @@ class _ChatInputState extends State { shape: BoxShape.circle, ), child: IconButton( - icon: value.text.isNotEmpty || _isRecording - ? widget.sendIcon! - : widget.audioIcon!, + icon: value.text.isNotEmpty || _isRecording ? widget.sendIcon! : widget.audioIcon!, color: inputTheme.hintStyle?.color, onPressed: () async { if (_isRecording) { @@ -186,8 +183,7 @@ class _ChatInputState extends State { } Widget _buildRecordingIndicator() { - final streamAmplitude = - _audioRecorder.onAmplitudeChanged(const Duration(seconds: 1)); + final streamAmplitude = _audioRecorder.onAmplitudeChanged(const Duration(seconds: 1)); return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -196,13 +192,13 @@ class _ChatInputState extends State { const SizedBox(width: 8), StreamBuilder( stream: Stream.periodic( - const Duration(seconds: 1), (count) => Duration(seconds: count),), + const Duration(seconds: 1), + (count) => Duration(seconds: count), + ), builder: (context, snapshot) { final duration = snapshot.data ?? Duration.zero; - final minutes = - duration.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = - duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); return Row( children: [ Text( @@ -240,12 +236,9 @@ class _ChatInputState extends State { } void _updateInputHeight() { - final renderBox = - _inputKey.currentContext?.findRenderObject() as RenderBox?; + final renderBox = _inputKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { - context - .read() - .updateHeight(renderBox.size.height); + context.read().updateHeight(renderBox.size.height); } } @@ -279,8 +272,10 @@ class _ChatInputState extends State { _recordedAudioPath = 'audio.m4a'; - await _audioRecorder.start(const RecordConfig(), - path: _recordedAudioPath!,); + await _audioRecorder.start( + const RecordConfig(), + path: _recordedAudioPath!, + ); } } catch (e) { debugPrint('Error during audio recording: $e'); From 190c540dc10f9dc69423b2586dc4bed1f31b9d5f Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Sun, 10 Nov 2024 21:40:18 -0300 Subject: [PATCH 5/7] working send file and send text --- examples/flyer_chat/lib/create_message.dart | 19 ++++++++++++------- examples/flyer_chat/lib/local.dart | 5 +++-- .../flutter_chat_ui/lib/src/chat_input.dart | 4 +++- .../lib/src/utils/typedefs.dart | 3 ++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/flyer_chat/lib/create_message.dart b/examples/flyer_chat/lib/create_message.dart index 3d793557..a2184b51 100644 --- a/examples/flyer_chat/lib/create_message.dart +++ b/examples/flyer_chat/lib/create_message.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:math'; import 'package:dio/dio.dart'; @@ -12,7 +13,7 @@ Future createMessage( Dio dio, { MessageType type = MessageType.text, String? text, - String filePath = 'audio.mp3', + File? file, }) async { const uuid = Uuid(); @@ -59,12 +60,16 @@ Future createMessage( blurhash: response.data['blurhash'], ); case MessageType.audio: - return AudioMessage( - id: uuid.v4(), - author: author, - createdAt: DateTime.now().toUtc(), - audioFile: filePath, - ); + if (file != null) { + return AudioMessage( + id: uuid.v4(), + author: author, + createdAt: DateTime.now().toUtc(), + audioFile: file.path, + ); + } else { + throw ArgumentError('File must be provided for audio messages'); + } default: throw ArgumentError('Invalid message type: $type'); } diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart index 17971f6a..065320c8 100644 --- a/examples/flyer_chat/lib/local.dart +++ b/examples/flyer_chat/lib/local.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:math'; import 'package:dio/dio.dart'; @@ -104,12 +105,12 @@ class LocalState extends State { } } - Future _addAudio(String filePath) async { + Future _addAudio(File file) async { final message = await createMessage( widget.author, widget.dio, type: MessageType.audio, - filePath: filePath, + file: file, ); if (mounted) { diff --git a/packages/flutter_chat_ui/lib/src/chat_input.dart b/packages/flutter_chat_ui/lib/src/chat_input.dart index 448011db..c4e038b0 100644 --- a/packages/flutter_chat_ui/lib/src/chat_input.dart +++ b/packages/flutter_chat_ui/lib/src/chat_input.dart @@ -1,4 +1,5 @@ import 'dart:developer' as developer; +import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -256,7 +257,8 @@ class _ChatInputState extends State { }); if (_recordedAudioPath!.isNotEmpty) { - context.read()?.call(_recordedAudioPath!); + final audioFile = File(_recordedAudioPath!); + context.read()?.call(audioFile); _resetAudioState(); } } diff --git a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart index 5478dc4c..c33ee7e4 100644 --- a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart +++ b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart @@ -1,8 +1,9 @@ +import 'dart:io'; import 'dart:ui'; import 'package:flutter_chat_core/flutter_chat_core.dart'; typedef OnMessageTapCallback = void Function(Message message); typedef OnMessageSendCallback = void Function(String text); -typedef OnAudioSendCallback = void Function(String filePath); +typedef OnAudioSendCallback = void Function(File file); typedef OnAttachmentTapCallback = VoidCallback; From 36d25f061b6fd1db146b0bbc2238924e4e588f4a Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Sun, 10 Nov 2024 21:45:47 -0300 Subject: [PATCH 6/7] improve audio player for example --- .../lib/src/flyer_chat_audio_message.dart | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart index e966fc2f..510ce1e3 100644 --- a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart +++ b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart @@ -1,6 +1,5 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_chat_core/flutter_chat_core.dart'; class FlyerChatAudioMessage extends StatefulWidget { @@ -22,6 +21,31 @@ class FlyerChatAudioMessage extends StatefulWidget { class FlyerChatAudioMessageState extends State { final AudioPlayer _audioPlayer = AudioPlayer(); bool _isPlaying = false; + Duration _currentPosition = Duration.zero; + Duration _totalDuration = Duration.zero; + + @override + void initState() { + super.initState(); + _audioPlayer.onDurationChanged.listen((duration) { + setState(() { + _totalDuration = duration; + }); + }); + + _audioPlayer.onPositionChanged.listen((position) { + setState(() { + _currentPosition = position; + }); + }); + + _audioPlayer.onPlayerComplete.listen((event) { + setState(() { + _isPlaying = false; + _currentPosition = Duration.zero; + }); + }); + } @override void dispose() { @@ -50,13 +74,28 @@ class FlyerChatAudioMessageState extends State { child: Row( children: [ IconButton( - icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), + icon: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + size: 32, + ), onPressed: _togglePlayPause, ), - const Expanded( - child: Text( - 'Audio Message', - overflow: TextOverflow.ellipsis, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Slider( + value: _currentPosition.inSeconds.toDouble(), + max: _totalDuration.inSeconds.toDouble(), + onChanged: (value) async { + final position = Duration(seconds: value.toInt()); + await _audioPlayer.seek(position); + setState(() { + _currentPosition = position; + }); + }, + ), + ], ), ), ], @@ -64,4 +103,10 @@ class FlyerChatAudioMessageState extends State { ), ); } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.remainder(60).toString().padLeft(1, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } } From 846d30e70cefa50d03f40e62167f170a191c75da Mon Sep 17 00:00:00 2001 From: lcsvcn <6011385+lcsvcn@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:52:51 -0300 Subject: [PATCH 7/7] add exception message --- .vscode/launch.json | 253 ++++++++++++++++++ .../flutter_chat_ui/lib/src/chat_input.dart | 4 +- .../lib/src/flyer_chat_audio_message.dart | 117 +++++--- 3 files changed, 334 insertions(+), 40 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..5d19f763 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,253 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "flutter_chat_ui", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat", + "cwd": "examples/flyer_chat", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat (profile mode)", + "cwd": "examples/flyer_chat", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat (release mode)", + "cwd": "examples/flyer_chat", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "cross_cache", + "cwd": "packages/cross_cache", + "request": "launch", + "type": "dart" + }, + { + "name": "cross_cache (profile mode)", + "cwd": "packages/cross_cache", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "cross_cache (release mode)", + "cwd": "packages/cross_cache", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flutter_chat_core", + "cwd": "packages/flutter_chat_core", + "request": "launch", + "type": "dart" + }, + { + "name": "flutter_chat_core (profile mode)", + "cwd": "packages/flutter_chat_core", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flutter_chat_core (release mode)", + "cwd": "packages/flutter_chat_core", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flutter_chat_ui", + "cwd": "packages/flutter_chat_ui", + "request": "launch", + "type": "dart" + }, + { + "name": "flutter_chat_ui (profile mode)", + "cwd": "packages/flutter_chat_ui", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flutter_chat_ui (release mode)", + "cwd": "packages/flutter_chat_ui", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_audio_message", + "cwd": "packages/flyer_chat_audio_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_audio_message (profile mode)", + "cwd": "packages/flyer_chat_audio_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_audio_message (release mode)", + "cwd": "packages/flyer_chat_audio_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_custom_message", + "cwd": "packages/flyer_chat_custom_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_custom_message (profile mode)", + "cwd": "packages/flyer_chat_custom_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_custom_message (release mode)", + "cwd": "packages/flyer_chat_custom_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_file_message", + "cwd": "packages/flyer_chat_file_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_file_message (profile mode)", + "cwd": "packages/flyer_chat_file_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_file_message (release mode)", + "cwd": "packages/flyer_chat_file_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_image_message", + "cwd": "packages/flyer_chat_image_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_image_message (profile mode)", + "cwd": "packages/flyer_chat_image_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_image_message (release mode)", + "cwd": "packages/flyer_chat_image_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_location_message", + "cwd": "packages/flyer_chat_location_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_location_message (profile mode)", + "cwd": "packages/flyer_chat_location_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_location_message (release mode)", + "cwd": "packages/flyer_chat_location_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_system_message", + "cwd": "packages/flyer_chat_system_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_system_message (profile mode)", + "cwd": "packages/flyer_chat_system_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_system_message (release mode)", + "cwd": "packages/flyer_chat_system_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_text_message", + "cwd": "packages/flyer_chat_text_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_text_message (profile mode)", + "cwd": "packages/flyer_chat_text_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_text_message (release mode)", + "cwd": "packages/flyer_chat_text_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "flyer_chat_video_message", + "cwd": "packages/flyer_chat_video_message", + "request": "launch", + "type": "dart" + }, + { + "name": "flyer_chat_video_message (profile mode)", + "cwd": "packages/flyer_chat_video_message", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "flyer_chat_video_message (release mode)", + "cwd": "packages/flyer_chat_video_message", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/packages/flutter_chat_ui/lib/src/chat_input.dart b/packages/flutter_chat_ui/lib/src/chat_input.dart index c4e038b0..68c8e4b4 100644 --- a/packages/flutter_chat_ui/lib/src/chat_input.dart +++ b/packages/flutter_chat_ui/lib/src/chat_input.dart @@ -272,11 +272,11 @@ class _ChatInputState extends State { _isRecording = true; }); - _recordedAudioPath = 'audio.m4a'; + _recordedAudioPath = 'audio.mp3'; await _audioRecorder.start( const RecordConfig(), - path: _recordedAudioPath!, + path: 'assets/${_recordedAudioPath!}', ); } } catch (e) { diff --git a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart index 510ce1e3..4925262c 100644 --- a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart +++ b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart @@ -1,12 +1,20 @@ +import 'dart:async'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; +/// A widget that displays an audio message with play/pause functionality. class FlyerChatAudioMessage extends StatefulWidget { + /// The audio message to be played. final AudioMessage message; + + /// The border radius of the audio message container. final BorderRadiusGeometry? borderRadius; + + /// The constraints for the audio message container. final BoxConstraints? constraints; + /// Creates a [FlyerChatAudioMessage] widget. const FlyerChatAudioMessage({ super.key, required this.message, @@ -19,49 +27,65 @@ class FlyerChatAudioMessage extends StatefulWidget { } class FlyerChatAudioMessageState extends State { - final AudioPlayer _audioPlayer = AudioPlayer(); - bool _isPlaying = false; - Duration _currentPosition = Duration.zero; - Duration _totalDuration = Duration.zero; + late final AudioPlayer _player; + PlayerState? _playerState; + Duration? _currentPosition; + Duration? _totalDuration; + + StreamSubscription? _durationSubscription; + StreamSubscription? _positionSubscription; + StreamSubscription? _playerCompleteSubscription; + StreamSubscription? _playerStateChangeSubscription; + + bool get _isPlaying => _playerState == PlayerState.playing; @override void initState() { super.initState(); - _audioPlayer.onDurationChanged.listen((duration) { - setState(() { - _totalDuration = duration; - }); - }); - - _audioPlayer.onPositionChanged.listen((position) { - setState(() { - _currentPosition = position; - }); - }); - - _audioPlayer.onPlayerComplete.listen((event) { - setState(() { - _isPlaying = false; - _currentPosition = Duration.zero; - }); - }); + _player = AudioPlayer(); + _player.setReleaseMode(ReleaseMode.stop); + _initStreams(); } @override void dispose() { - _audioPlayer.dispose(); + _durationSubscription?.cancel(); + _positionSubscription?.cancel(); + _playerCompleteSubscription?.cancel(); + _playerStateChangeSubscription?.cancel(); + _player.dispose(); super.dispose(); } - void _togglePlayPause() async { - if (_isPlaying) { - await _audioPlayer.pause(); - } else { - await _audioPlayer.play(DeviceFileSource(widget.message.audioFile)); + Future _togglePlayPause() async { + try { + if (_isPlaying) { + await _player.pause(); + } else { + await _player.setSource(AssetSource(widget.message.audioFile)); + await _player.resume(); + } + } on AudioPlayerException catch (e) { + _showErrorDialog('Audio Player Error', e.toString()); + } catch (e) { + _showErrorDialog('Error', 'An unexpected error occurred. $e'); } - setState(() { - _isPlaying = !_isPlaying; - }); + } + + void _showErrorDialog(String title, String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); } @override @@ -85,11 +109,11 @@ class FlyerChatAudioMessageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Slider( - value: _currentPosition.inSeconds.toDouble(), - max: _totalDuration.inSeconds.toDouble(), + value: _currentPosition?.inSeconds.toDouble() ?? 0.0, + max: _totalDuration?.inSeconds.toDouble() ?? 1.0, onChanged: (value) async { final position = Duration(seconds: value.toInt()); - await _audioPlayer.seek(position); + await _player.seek(position); setState(() { _currentPosition = position; }); @@ -104,9 +128,26 @@ class FlyerChatAudioMessageState extends State { ); } - String _formatDuration(Duration duration) { - final minutes = duration.inMinutes.remainder(60).toString().padLeft(1, '0'); - final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); - return '$minutes:$seconds'; + void _initStreams() { + _durationSubscription = _player.onDurationChanged.listen((duration) { + setState(() => _totalDuration = duration); + }); + + _positionSubscription = _player.onPositionChanged.listen((position) { + setState(() => _currentPosition = position); + }); + + _playerCompleteSubscription = _player.onPlayerComplete.listen((event) { + setState(() { + _playerState = PlayerState.stopped; + _currentPosition = Duration.zero; + }); + }); + + _playerStateChangeSubscription = _player.onPlayerStateChanged.listen((state) { + setState(() { + _playerState = state; + }); + }); } }