diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 000000000..3820a95c6
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/example/.metadata b/example/.metadata
index 706ff779c..2eaf67a03 100644
--- a/example/.metadata
+++ b/example/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: "603104015dd692ea3403755b55d07813d5cf8965"
+ revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: 603104015dd692ea3403755b55d07813d5cf8965
- base_revision: 603104015dd692ea3403755b55d07813d5cf8965
- - platform: android
- create_revision: 603104015dd692ea3403755b55d07813d5cf8965
- base_revision: 603104015dd692ea3403755b55d07813d5cf8965
+ create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
+ base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
+ - platform: macos
+ create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
+ base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
# User provided section
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 132ab7417..53933356e 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -8,7 +8,7 @@ plugins {
android {
namespace = "com.zebra.zeta.example"
compileSdk = flutter.compileSdkVersion
- ndkVersion = "25.1.8937393"
+ ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -24,7 +24,7 @@ android {
applicationId = "com.zebra.zeta.example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
- minSdk = 23
+ minSdkVersion flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 74a78b939..af1f02175 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -17,9 +17,9 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
@@ -41,5 +41,8 @@
+
+
+
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 9c5194d3a..d5ce57cba 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
\ No newline at end of file
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
\ No newline at end of file
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
index a00f9ea70..4f520718d 100644
--- a/example/android/settings.gradle
+++ b/example/android/settings.gradle
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "8.3.1" apply false
- id "org.jetbrains.kotlin.android" version "1.8.22" apply false
+ id "com.android.application" version "8.6.0" apply false
+ id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"
diff --git a/example/lib/home.dart b/example/lib/home.dart
index b4063d465..0dab3124a 100644
--- a/example/lib/home.dart
+++ b/example/lib/home.dart
@@ -24,6 +24,7 @@ import 'package:zeta_example/pages/components/global_header_example.dart';
import 'package:zeta_example/pages/components/filter_selection_example.dart';
import 'package:zeta_example/pages/components/list_example.dart';
import 'package:zeta_example/pages/components/list_item_example.dart';
+import 'package:zeta_example/pages/components/message_input_example.dart';
import 'package:zeta_example/pages/components/navigation_bar_example.dart';
import 'package:zeta_example/pages/components/navigation_rail_example.dart';
import 'package:zeta_example/pages/components/notification_list_example.dart';
@@ -32,7 +33,6 @@ import 'package:zeta_example/pages/components/radio_example.dart';
import 'package:zeta_example/pages/components/range_selector_example.dart';
import 'package:zeta_example/pages/components/screen_header_bar_example.dart';
import 'package:zeta_example/pages/components/select_input_example.dart';
-
import 'package:zeta_example/pages/components/search_bar_example.dart';
import 'package:zeta_example/pages/components/segmented_control_example.dart';
import 'package:zeta_example/pages/components/slider_example.dart';
@@ -97,6 +97,7 @@ final List components = [
Component(Label.name, (context) => const Label()),
Component(ListExample.name, (context) => const ListExample()),
Component(ListItemExample.name, (context) => const ListItemExample()),
+ Component(MessageInputExample.name, (context) => const MessageInputExample()),
Component(NavigationBarExample.name, (context) => const NavigationBarExample()),
Component(NavigationRailExample.name, (context) => const NavigationRailExample()),
Component(NotificationListItemExample.name, (context) => const NotificationListItemExample()),
diff --git a/example/lib/pages/components/message_input_example.dart b/example/lib/pages/components/message_input_example.dart
new file mode 100644
index 000000000..e4343be91
--- /dev/null
+++ b/example/lib/pages/components/message_input_example.dart
@@ -0,0 +1,84 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:zeta_example/widgets.dart';
+import 'package:zeta_flutter/zeta_flutter.dart';
+
+class MessageInputExample extends StatefulWidget {
+ static const String name = 'MessageInput';
+
+ const MessageInputExample({super.key});
+
+ @override
+ State createState() => _MessageInputExampleState();
+}
+
+class _MessageInputExampleState extends State {
+ late TextEditingController controller;
+ List messages = [];
+ List attachments = [];
+
+ @override
+ void initState() {
+ super.initState();
+ controller = TextEditingController();
+ messages = [for (var i = 0; i < 3; i++) 'Message $i'];
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ExampleScaffold(
+ name: MessageInputExample.name,
+ child: Column(
+ children: [
+ Expanded(
+ child: ListView.builder(
+ itemCount: messages.length,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Text(messages[index]),
+ );
+ },
+ ),
+ ),
+ ZetaMessageInput(
+ controller: controller,
+ maxVoiceMemoDuration: Duration(seconds: 10),
+ disabled: false,
+ allowsVoiceInput: true,
+ cameraTrailingButton: true,
+ hasActionMenu: true,
+ onSendLocation: (location) => setState(() => messages.add('Location: $location')),
+ onSendVoiceMemo: (file, bytes) => setState(() {
+ messages.add('Voice memo: ${file.path}');
+ }),
+ attachments: attachments,
+ onSendAttachments: (fileList) {
+ fileList.forEach((file) {
+ setState(() {
+ messages.add('File attachment: ${file.path}');
+ });
+ });
+ attachments.clear();
+ },
+ onSend: (message) {
+ if (message.isNotEmpty) {
+ setState(() {
+ messages.add(message);
+ });
+ print('Message sent: $message');
+ controller.clear();
+ }
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc
index 145c9eb8f..64e03628a 100644
--- a/example/linux/flutter/generated_plugin_registrant.cc
+++ b/example/linux/flutter/generated_plugin_registrant.cc
@@ -7,12 +7,16 @@
#include "generated_plugin_registrant.h"
#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);
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/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake
index ecad9e4f0..081622ccd 100644
--- a/example/linux/flutter/generated_plugins.cmake
+++ b/example/linux/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
+ file_selector_linux
record_linux
)
diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift
index 3ec2482bd..cecacbf56 100644
--- a/example/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -6,15 +6,23 @@ import FlutterMacOS
import Foundation
import audioplayers_darwin
+import file_picker
+import file_selector_macos
+import location
import path_provider_foundation
import record_macos
import shared_preferences_foundation
+import speech_to_text
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+ FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
+ LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+ SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}
diff --git a/example/macos/Podfile b/example/macos/Podfile
index 9ec46f8cd..95a1c558a 100644
--- a/example/macos/Podfile
+++ b/example/macos/Podfile
@@ -1,4 +1,4 @@
-platform :osx, '10.15'
+platform :osx, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -28,7 +28,6 @@ flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
- use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
end
diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock
index d395d6faf..8aa1e35cc 100644
--- a/example/macos/Podfile.lock
+++ b/example/macos/Podfile.lock
@@ -2,7 +2,16 @@ PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
+ - CwlCatchException (2.2.1):
+ - CwlCatchExceptionSupport (~> 2.2.1)
+ - CwlCatchExceptionSupport (2.2.1)
+ - file_picker (0.0.1):
+ - FlutterMacOS
+ - file_selector_macos (0.0.1):
+ - FlutterMacOS
- FlutterMacOS (1.0.0)
+ - location (0.0.1):
+ - FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -11,40 +20,67 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - speech_to_text (7.2.0):
+ - CwlCatchException
+ - Flutter
+ - FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`)
+ - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
+ - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
+ - location (from `Flutter/ephemeral/.symlinks/plugins/location/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
+ - speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
+SPEC REPOS:
+ trunk:
+ - CwlCatchException
+ - CwlCatchExceptionSupport
+
EXTERNAL SOURCES:
audioplayers_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin
+ file_picker:
+ :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
+ file_selector_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral
+ location:
+ :path: Flutter/ephemeral/.symlinks/plugins/location/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
record_macos:
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
+ speech_to_text:
+ :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
SPEC CHECKSUMS:
audioplayers_darwin: 4027b33a8f471d996c13f71cb77f0b1583b5d923
+ CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a
+ CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc
+ file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
+ file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
+ location: 7cdb0665bd6577d382b0a343acdadbcb7f964775
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
record_macos: 4440ca269ad3b870ebb1965297a365d558f0c520
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
+ speech_to_text: 87bf9298952e8d9073be1b6aade6d5758db5170c
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
-PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0
+PODFILE CHECKSUM: 849a35e86ba3f6dc9bc81afd485a3818b12a749b
COCOAPODS: 1.16.2
diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj
index 7ec3408c9..3ffa51b9f 100644
--- a/example/macos/Runner.xcodeproj/project.pbxproj
+++ b/example/macos/Runner.xcodeproj/project.pbxproj
@@ -21,15 +21,23 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
+ 22F494A5B8235393D2216CBD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DFD1E57507D81D51F5C956 /* Pods_Runner.framework */; };
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
- 4D99090773193CBDD11097DB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B3B47356FD6E66B2FD2113C /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
+ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
@@ -53,9 +61,13 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 0ADC580AF06725DD2349DD02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 2FFD83BC5BB3D408A567DF21 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
- 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10ED2044A3C60003C045 /* zeta_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = zeta_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
@@ -67,26 +79,39 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
- 3B3B47356FD6E66B2FD2113C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 775BFA427E635DE4CCD15022 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
- 89C2799490DE736CE6C6B909 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
- C8838B380443E31AB7DE4089 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ A9DFD1E57507D81D51F5C956 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ B77B24D6B05FA15CF19D12A8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 331C80D2294CF70F00263BE5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 4D99090773193CBDD11097DB /* Pods_Runner.framework in Frameworks */,
+ 22F494A5B8235393D2216CBD /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 331C80D6294CF71000263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
@@ -103,16 +128,18 @@
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
+ 331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
- 5084074E164045F316413879 /* Pods */,
+ B37A337AA32146B1306E042D /* Pods */,
);
sourceTree = "";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
- 33CC10ED2044A3C60003C045 /* example.app */,
+ 33CC10ED2044A3C60003C045 /* zeta_example.app */,
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "";
@@ -152,20 +179,21 @@
path = Runner;
sourceTree = "";
};
- 5084074E164045F316413879 /* Pods */ = {
+ B37A337AA32146B1306E042D /* Pods */ = {
isa = PBXGroup;
children = (
- C8838B380443E31AB7DE4089 /* Pods-Runner.debug.xcconfig */,
- 775BFA427E635DE4CCD15022 /* Pods-Runner.release.xcconfig */,
- 89C2799490DE736CE6C6B909 /* Pods-Runner.profile.xcconfig */,
+ 2FFD83BC5BB3D408A567DF21 /* Pods-Runner.debug.xcconfig */,
+ 0ADC580AF06725DD2349DD02 /* Pods-Runner.release.xcconfig */,
+ B77B24D6B05FA15CF19D12A8 /* Pods-Runner.profile.xcconfig */,
);
+ name = Pods;
path = Pods;
sourceTree = "";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
- 3B3B47356FD6E66B2FD2113C /* Pods_Runner.framework */,
+ A9DFD1E57507D81D51F5C956 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -173,17 +201,35 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
+ 331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C80D1294CF70F00263BE5 /* Sources */,
+ 331C80D2294CF70F00263BE5 /* Frameworks */,
+ 331C80D3294CF70F00263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
- 3BECC061C0ABEC8462CAC898 /* [CP] Check Pods Manifest.lock */,
+ AE2B29F0544898D70F318103 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
- BB66DD328FCDFBAE60FE60D4 /* [CP] Embed Pods Frameworks */,
+ E440B2AFF153D986F06C9B52 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -192,7 +238,7 @@
);
name = Runner;
productName = Runner;
- productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+ productReference = 33CC10ED2044A3C60003C045 /* zeta_example.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -201,10 +247,15 @@
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
+ BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
+ 331C80D4294CF70F00263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
@@ -235,12 +286,20 @@
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
+ 331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
+ 331C80D3294CF70F00263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -291,7 +350,7 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
- 3BECC061C0ABEC8462CAC898 /* [CP] Check Pods Manifest.lock */ = {
+ AE2B29F0544898D70F318103 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -313,7 +372,7 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- BB66DD328FCDFBAE60FE60D4 /* [CP] Embed Pods Frameworks */ = {
+ E440B2AFF153D986F06C9B52 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -333,6 +392,14 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 331C80D1294CF70F00263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -346,6 +413,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+ };
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
@@ -366,11 +438,54 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 331C80DB294CF71000263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.zebra.zeta.zetaExample.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/zeta_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/zeta_example";
+ };
+ name = Debug;
+ };
+ 331C80DC294CF71000263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.zebra.zeta.zetaExample.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/zeta_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/zeta_example";
+ };
+ name = Release;
+ };
+ 331C80DD294CF71000263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.zebra.zeta.zetaExample.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/zeta_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/zeta_example";
+ };
+ name = Profile;
+ };
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -394,9 +509,11 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -404,7 +521,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -444,6 +561,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -467,9 +585,11 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -483,7 +603,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -497,6 +617,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -520,9 +641,11 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -530,7 +653,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -596,6 +719,16 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C80DB294CF71000263BE5 /* Debug */,
+ 331C80DC294CF71000263BE5 /* Release */,
+ 331C80DD294CF71000263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 12076c83d..edd7f1194 100644
--- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -15,7 +15,7 @@
@@ -31,12 +31,23 @@
+
+
+
+
@@ -72,7 +83,7 @@
diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig
index 871a41aeb..861d14e26 100644
--- a/example/macos/Runner/Configs/AppInfo.xcconfig
+++ b/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -5,10 +5,10 @@
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
-PRODUCT_NAME = example
+PRODUCT_NAME = zeta_example
// The application's bundle identifier
-PRODUCT_BUNDLE_IDENTIFIER = com.zebra.zeta.example
+PRODUCT_BUNDLE_IDENTIFIER = com.zebra.zeta.zetaExample
// The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright © 2023 com.zebra.zeta. All rights reserved.
+PRODUCT_COPYRIGHT = Copyright © 2025 com.zebra.zeta. All rights reserved.
diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift
index 2722837ec..3cc05eb23 100644
--- a/example/macos/Runner/MainFlutterWindow.swift
+++ b/example/macos/Runner/MainFlutterWindow.swift
@@ -3,7 +3,7 @@ import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
- let flutterViewController = FlutterViewController.init()
+ let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements
index 90f6b188a..51bca823d 100644
--- a/example/macos/Runner/Release.entitlements
+++ b/example/macos/Runner/Release.entitlements
@@ -6,6 +6,6 @@
com.apple.security.device.audio-input
-
+
diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc
index c566a56a3..5e0768b78 100644
--- a/example/windows/flutter/generated_plugin_registrant.cc
+++ b/example/windows/flutter/generated_plugin_registrant.cc
@@ -7,11 +7,17 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
+ SpeechToTextWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("SpeechToTextWindows"));
}
diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake
index 045e16c1d..5388ac024 100644
--- a/example/windows/flutter/generated_plugins.cmake
+++ b/example/windows/flutter/generated_plugins.cmake
@@ -4,7 +4,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
+ file_selector_windows
record_windows
+ speech_to_text_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/action_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/action_button.dart
new file mode 100644
index 000000000..f53856342
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/action_button.dart
@@ -0,0 +1,71 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+
+/// Action button for the action menu in the message input
+class ActionButton extends ZetaStatelessWidget {
+ /// Creates an [ActionButton].
+ const ActionButton({
+ super.key,
+ required this.icon,
+ this.onPressed,
+ this.onLongPress,
+ this.color,
+ this.disabled,
+ this.size,
+ this.semanticLabel,
+ });
+
+ /// The icon to display.
+ final IconData icon;
+
+ /// The callback invoked when the button is pressed.
+ final VoidCallback? onPressed;
+
+ /// The callback invoked when the button is long pressed.
+ final VoidCallback? onLongPress;
+
+ /// The color of the icon. Defaults to mainDefault.
+ final Color? color;
+
+ /// Whether or not the button is disabled.
+ final bool? disabled;
+
+ /// A size override.
+ final double? size;
+
+ /// The semantic label on the icon button (used for screen readers).
+ final String? semanticLabel;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ return Semantics(
+ label: semanticLabel,
+ child: IconButton(
+ icon: Icon(
+ icon,
+ color: color ?? (onPressed == null || (disabled ?? false) ? colors.mainDisabled : colors.mainDefault),
+ size: size ?? spacing.xl_3,
+ ),
+ onPressed: (disabled ?? false) ? null : onPressed,
+ ),
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('icon', icon))
+ ..add(ObjectFlagProperty.has('callback', onPressed))
+ ..add(ColorProperty('color', color))
+ ..add(ObjectFlagProperty.has('onLongPressed', onLongPress))
+ ..add(DiagnosticsProperty('disabled', disabled))
+ ..add(StringProperty('semanticLabel', semanticLabel))
+ ..add(DoubleProperty('size', size));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/attachment_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/attachment_button.dart
new file mode 100644
index 000000000..80ddc78dc
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/attachment_button.dart
@@ -0,0 +1,96 @@
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// Attachment button for picking any type of file
+class AttachmentButton extends ZetaStatelessWidget {
+ /// Creates an [AttachmentButton].
+ const AttachmentButton({
+ super.key,
+ this.onAttach,
+ this.allowedExtensions,
+ this.allowMultiple = false,
+ this.maxSize,
+ });
+
+ /// Callback for when files are selected.
+ final ValueChanged>? onAttach;
+
+ /// Optional list of allowed file extensions (e.g., ['pdf', 'doc', 'docx']).
+ /// If null, all file types are allowed.
+ final List? allowedExtensions;
+
+ /// Whether to allow selecting multiple files at once.
+ final bool allowMultiple;
+
+ /// The maximum size of attachments in bytes.
+ final int? maxSize;
+
+ Future _pickFiles(BuildContext context) async {
+ // final scaffoldMessenger = ScaffoldMessenger.of(context);
+ try {
+ final FilePickerResult? result = await FilePicker.platform.pickFiles(
+ type: allowedExtensions != null ? FileType.custom : FileType.any,
+ allowedExtensions: allowedExtensions,
+ allowMultiple: allowMultiple,
+ );
+
+ if (result != null) {
+ var files = result.paths.where((String? path) => path != null).map((String? path) => File(path!)).toList();
+ // Filter by maxSize if specified
+ if (maxSize != null) {
+ files = files.where((file) {
+ try {
+ return file.lengthSync() <= maxSize!;
+ } catch (_) {
+ return false;
+ }
+ }).toList();
+ }
+ if (files.isNotEmpty && onAttach != null) {
+ onAttach!(files);
+ } else if (result.paths.isNotEmpty && files.isEmpty) {
+ if (context.mounted) {
+ _displayError(context, 'Selected file(s) exceed maximum size of ${maxSize! / 1000000}MB');
+ }
+ }
+ }
+ } catch (e) {
+ if (context.mounted) _displayError(context, e);
+ }
+ }
+
+ void _displayError(BuildContext context, Object error) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ ZetaSnackBar(
+ content: Text('Error picking files: $error'),
+ type: ZetaSnackBarType.error,
+ context: context,
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ActionButton(
+ icon: ZetaIcons.attachment,
+ onPressed: onAttach != null ? () => _pickFiles(context) : null,
+ semanticLabel: 'add an attachment',
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(ObjectFlagProperty>>.has('onAttach', onAttach))
+ ..add(IterableProperty('allowedExtensions', allowedExtensions))
+ ..add(FlagProperty('allowMultiple', value: allowMultiple, ifTrue: 'multiple files allowed'));
+ properties.add(IntProperty('maxSize', maxSize));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/image_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/image_button.dart
new file mode 100644
index 000000000..9590450d2
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/image_button.dart
@@ -0,0 +1,50 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// Camera button for capturing images
+class ImageButton extends ZetaStatelessWidget {
+ /// Creates a [ImageButton].
+ const ImageButton({
+ super.key,
+ this.onCapture,
+ this.disabled,
+ });
+
+ /// Callback for when an image is captured.
+ final ValueChanged? onCapture;
+
+ /// Wether or not the button is disabled.
+ final bool? disabled;
+
+ Future _pickImage(BuildContext context) async {
+ final ImagePicker picker = ImagePicker();
+ final XFile? image = await picker.pickImage(source: ImageSource.camera, imageQuality: 80);
+ if (image != null && onCapture != null) {
+ onCapture!(File(image.path));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ActionButton(
+ icon: ZetaIcons.camera,
+ onPressed: () => _pickImage(context),
+ disabled: disabled,
+ semanticLabel: 'capture an image',
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(ObjectFlagProperty>.has('onCapture', onCapture))
+ ..add(DiagnosticsProperty('disabled', disabled));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/location_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/location_button.dart
new file mode 100644
index 000000000..c6a497840
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/location_button.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:location/location.dart';
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// Location button for sending precise or approximate location
+class LocationButton extends ZetaStatelessWidget {
+ /// Creates a [LocationButton].
+ const LocationButton({
+ super.key,
+ this.onLocationCapture,
+ });
+
+ /// Callback for when a location is captured.
+ final ValueChanged? onLocationCapture;
+
+ Future _getLocation(BuildContext context) async {
+ final Location location = Location();
+ final LocationData locationData = await location.getLocation();
+
+ if (onLocationCapture != null) {
+ onLocationCapture?.call(locationData);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ActionButton(
+ icon: ZetaIcons.pin,
+ onPressed: onLocationCapture != null ? () => _getLocation(context) : null,
+ semanticLabel: 'send your location',
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(ObjectFlagProperty>.has('onLocationCapture', onLocationCapture));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/send_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/send_button.dart
new file mode 100644
index 000000000..ba3201f03
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/send_button.dart
@@ -0,0 +1,88 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// The send button used in the message input
+class SendButton extends ZetaStatelessWidget {
+ /// Creates a [SendButton].
+ const SendButton({
+ super.key,
+ required this.controller,
+ this.attachments,
+ this.onPressed,
+ this.onLongPress,
+ this.disabled,
+ });
+
+ /// The text editing controller of the connected text input.
+ final TextEditingController controller;
+
+ /// A list of attachment.
+ final List? attachments;
+
+ /// The callback invoked on press of the send button.
+ final void Function(String text, List attachments)? onPressed;
+
+ /// The callback invoked on long press of the send button.
+ final void Function(String text, List attachments)? onLongPress;
+
+ /// Wether or not the button is disabled.
+ final bool? disabled;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ return Stack(
+ children: [
+ ActionButton(
+ icon: ZetaIcons.send,
+ onPressed: onPressed != null ? () => onPressed!(controller.text, attachments ?? []) : null,
+ onLongPress: onLongPress != null ? () => onLongPress!(controller.text, attachments ?? []) : null,
+ disabled: disabled,
+ semanticLabel: 'send the message',
+ ),
+ if (attachments != null && attachments!.isNotEmpty)
+ Positioned(
+ right: 4,
+ top: 4,
+ child: Container(
+ width: spacing.large,
+ height: spacing.large,
+ decoration: BoxDecoration(
+ color: colors.surfacePrimarySubtle,
+ shape: BoxShape.circle,
+ ),
+ child: Center(
+ child: Text(
+ attachments!.length.toString(),
+ style: TextStyle(
+ color: colors.mainDefault,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('controller', controller))
+ ..add(IterableProperty('attachments', attachments))
+ ..add(ObjectFlagProperty attachments)?>.has('onPressed', onPressed))
+ ..add(ObjectFlagProperty attachments)?>.has('onLongPress', onLongPress))
+ ..add(DiagnosticsProperty('isDisabled', disabled));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/video_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/video_button.dart
new file mode 100644
index 000000000..56e15d486
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/video_button.dart
@@ -0,0 +1,43 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// Video button for capturing videos
+class VideoButton extends ZetaStatelessWidget {
+ /// Creates a [VideoButton].
+ const VideoButton({
+ super.key,
+ this.onCapture,
+ });
+
+ /// Callback for when a video is captured.
+ final ValueChanged? onCapture;
+
+ Future _pickVideo(BuildContext context) async {
+ final ImagePicker picker = ImagePicker();
+ final XFile? video = await picker.pickVideo(source: ImageSource.camera, maxDuration: const Duration(minutes: 5));
+ if (video != null && onCapture != null) {
+ onCapture!(File(video.path));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ActionButton(
+ icon: ZetaIcons.video,
+ onPressed: onCapture != null ? () => _pickVideo(context) : null,
+ semanticLabel: 'capture a video',
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(ObjectFlagProperty>.has('onCapture', onCapture));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/voice_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/voice_button.dart
new file mode 100644
index 000000000..d77895eb6
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/voice_button.dart
@@ -0,0 +1,132 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:speech_to_text/speech_to_text.dart' as stt;
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// A widget that provides voice input functionality.
+class VoiceButton extends ZetaStatefulWidget {
+ /// Creates a [VoiceButton].
+ const VoiceButton({
+ super.key,
+ required this.controller,
+ this.disabled,
+ });
+
+ /// The text input editing controller for the voice input.
+ final TextEditingController controller;
+
+ /// Whether or not the button is disabled.
+ final bool? disabled;
+
+ @override
+ VoiceButtonState createState() => VoiceButtonState();
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('controller', controller))
+ ..add(DiagnosticsProperty('disabled', disabled));
+ }
+}
+
+/// The state for the [VoiceButton].
+class VoiceButtonState extends State {
+ late stt.SpeechToText _speech;
+ bool _isListening = false;
+ String _recognizedText = '';
+ int _cursorPos = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _speech = stt.SpeechToText();
+ }
+
+ /// Handles the speech recognition result.
+ void onSpeechResult(String text) {
+ if (text.isEmpty) return;
+ final beforeText = widget.controller.text.substring(0, _cursorPos);
+ final afterText = widget.controller.text.substring(_cursorPos);
+ final addOneToCursorPos = beforeText.isNotEmpty || afterText.isNotEmpty;
+
+ if (_cursorPos != -1) {
+ widget.controller.text =
+ '${beforeText.isNotEmpty ? '${beforeText.trimRight()} ' : ''}$text${afterText.isNotEmpty ? ' ${afterText.trimLeft()}' : ''}';
+ } else {
+ widget.controller.text = widget.controller.text + text;
+ }
+
+ widget.controller.selection = TextSelection.fromPosition(
+ TextPosition(
+ offset: _cursorPos + text.length + (addOneToCursorPos ? 1 : 0),
+ ),
+ );
+ }
+
+ Future _startListening() async {
+ setState(() => _cursorPos = widget.controller.selection.baseOffset);
+
+ final bool available = await _speech.initialize(
+ onStatus: (status) => {
+ if (status == stt.SpeechToText.notListeningStatus)
+ {
+ setState(() => _isListening = false),
+ },
+ if (status == stt.SpeechToText.doneStatus) {onSpeechResult(_recognizedText)},
+ },
+ onError: (error) => {
+ setState(() => _isListening = false),
+ },
+ );
+
+ if (available) {
+ setState(() => _isListening = true);
+ setState(() => _recognizedText = '');
+ await _speech.listen(
+ pauseFor: const Duration(seconds: 3),
+ onResult: (result) {
+ setState(() {
+ _recognizedText = result.recognizedWords;
+ });
+ },
+ );
+ } else {
+ _displayErrorToast();
+ }
+ }
+
+ Future _stopListening() async {
+ setState(() => _isListening = false);
+ await _speech.stop();
+ }
+
+ void _displayErrorToast() {
+ ScaffoldMessenger.of(context).showSnackBar(
+ ZetaSnackBar(
+ type: ZetaSnackBarType.error,
+ context: context,
+ content: const Text(
+ 'Speech recognition not available; could be due to insufficient permissions',
+ ),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ return ActionButton(
+ icon: ZetaIcons.microphone,
+ onPressed: _isListening ? _stopListening : _startListening,
+ color: _isListening ? colors.mainPrimary : null,
+ disabled: widget.disabled,
+ semanticLabel: 'voice input',
+ size: spacing.xl_2,
+ );
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/actions/voice_memo_button.dart b/packages/zeta_flutter/lib/src/components/message_input/actions/voice_memo_button.dart
new file mode 100644
index 000000000..63057eb07
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/actions/voice_memo_button.dart
@@ -0,0 +1,88 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+import 'action_button.dart';
+
+/// Voice memo button for recording voice notes
+class VoiceMemoButton extends ZetaStatelessWidget {
+ /// Creates a [VoiceMemoButton].
+ const VoiceMemoButton({
+ super.key,
+ this.onSend,
+ this.maxDuration,
+ });
+
+ /// Callback for when a voice note is recorded.
+ final void Function(File file, Uint8List bytes)? onSend;
+
+ /// The maximum duration of voice memos.
+ final Duration? maxDuration;
+
+ Future _onClick(BuildContext context) async {
+ // Convert Uint8List to File for the callback
+ Future handleVoiceMemo(Uint8List audioData) async {
+ // Create a temporary file from the audio data
+ final directory = Directory.systemTemp;
+ final tempFile = File('${directory.path}/voice_memo_${DateTime.now().millisecondsSinceEpoch}.wav');
+ await tempFile.writeAsBytes(audioData);
+ onSend!(tempFile, audioData);
+ }
+
+ await showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (BuildContext context) {
+ return DraggableScrollableSheet(
+ initialChildSize: 0.3,
+ builder: (context, scrollController) {
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ color: Zeta.of(context).colors.mainInverse,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
+ ),
+ child: Column(
+ children: [
+ Expanded(
+ child: ZetaVoiceMemo(
+ maxRecordingDuration: maxDuration != null ? maxDuration! : const Duration(seconds: 120),
+ onSend: onSend != null
+ ? (audioData) {
+ unawaited(handleVoiceMemo(audioData));
+ Navigator.of(context).pop();
+ }
+ : null,
+ onDiscard: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ActionButton(
+ icon: ZetaIcons.audio,
+ onPressed: onSend != null ? () => _onClick(context) : null,
+ semanticLabel: 'record a voice memo',
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(ObjectFlagProperty.has('onSend', onSend));
+ properties.add(DiagnosticsProperty('maxDuration', maxDuration));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/components/document_attachment.dart b/packages/zeta_flutter/lib/src/components/message_input/components/document_attachment.dart
new file mode 100644
index 000000000..be419ce7b
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/components/document_attachment.dart
@@ -0,0 +1,59 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+import '../utils/file_type_checker.dart';
+
+/// Attachment item for displaying document files.
+class DocumentAttachment extends ZetaStatelessWidget {
+ /// Creates a [DocumentAttachment] widget.
+ const DocumentAttachment({
+ super.key,
+ required this.file,
+ });
+
+ /// The file to be displayed.
+ final File file;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ return Container(
+ decoration: BoxDecoration(
+ color: colors.mainLight,
+ border: Border.all(color: colors.borderDefault),
+ borderRadius: BorderRadius.circular(spacing.medium),
+ ),
+ padding: EdgeInsets.only(right: spacing.xl),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: EdgeInsets.only(left: spacing.small),
+ child: Text(file.path.split('/').last, maxLines: 1, overflow: TextOverflow.ellipsis),
+ ),
+ Row(
+ children: [
+ FileTypeChecker.getFileIcon(context, file),
+ SizedBox(width: spacing.minimum),
+ Flexible(
+ child: Text(file.path.split('.').last.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty('file', file));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/components/file_attachment.dart b/packages/zeta_flutter/lib/src/components/message_input/components/file_attachment.dart
new file mode 100644
index 000000000..021c0d2db
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/components/file_attachment.dart
@@ -0,0 +1,148 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import '../../../../zeta_flutter.dart';
+import '../utils/file_type_checker.dart';
+import 'document_attachment.dart';
+import 'image_attachment.dart';
+import 'video_attachment.dart';
+
+/// A widget that displays a file attachment in the Attachments Bar
+class FileAttachment extends ZetaStatelessWidget {
+ /// Creates a [FileAttachment] widget.
+ const FileAttachment({
+ super.key,
+ required this.file,
+ required this.index,
+ required this.onClose,
+ });
+
+ /// The file being displayed.
+ final File file;
+
+ /// The index of the file in the list.
+ final int index;
+
+ /// Callback function to be called when the attachment is closed.
+ final ValueChanged onClose;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ final size = spacing.xl_8 + spacing.medium;
+
+ final isImage = FileTypeChecker.isImage(file);
+ final isVideo = FileTypeChecker.isVideo(file);
+
+ Widget dialogImage = const SizedBox();
+ if (isImage) dialogImage = Image.file(file);
+ if (isVideo) {
+ dialogImage = FutureBuilder(
+ future: FileTypeChecker.getVideoThumbnail(file),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
+ return Stack(
+ children: [
+ Image(
+ image: snapshot.data!,
+ ),
+ Positioned.fill(
+ child: Center(
+ child: Container(
+ decoration: const BoxDecoration(
+ color: Color.fromARGB(128, 0, 0, 0),
+ shape: BoxShape.circle,
+ ),
+ padding: EdgeInsets.all(spacing.medium),
+ child: Icon(
+ ZetaIcons.video,
+ size: spacing.xl_11,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ } else if (snapshot.connectionState == ConnectionState.done && !snapshot.hasData) {
+ return SizedBox(
+ width: size,
+ height: size,
+ child: const Center(child: Icon(ZetaIcons.video)),
+ );
+ } else {
+ return SizedBox(
+ width: size,
+ height: size,
+ child: const Center(child: CircularProgressIndicator()),
+ );
+ }
+ },
+ );
+ }
+
+ return InkWell(
+ onTap: () async {
+ if (isImage || isVideo) {
+ await showDialog(
+ context: context,
+ builder: (_) => Dialog(
+ child: dialogImage,
+ ),
+ );
+ }
+ },
+ child: Container(
+ width: !isVideo && !isImage ? size * 3 : size,
+ height: size,
+ decoration: BoxDecoration(
+ color: colors.mainLight,
+ borderRadius: BorderRadius.circular(spacing.medium),
+ ),
+ child: Stack(
+ children: [
+ if (isVideo) VideoAttachment(file: file),
+ if (isImage) ImageAttachment(file: file),
+ if (!isVideo && !isImage) DocumentAttachment(file: file),
+ Positioned(
+ top: 1,
+ right: 1,
+ child: GestureDetector(
+ onTap: () async => {
+ onClose(index),
+ await SystemChannels.textInput.invokeMethod('TextInput.show'),
+ },
+ child: Container(
+ decoration: BoxDecoration(
+ color: colors.borderDefault,
+ shape: BoxShape.circle,
+ ),
+ padding: EdgeInsets.all(spacing.minimum),
+ child: Icon(
+ Icons.close,
+ size: spacing.large,
+ color: colors.mainDefault,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('file', file))
+ ..add(IntProperty('index', index))
+ ..add(ObjectFlagProperty>.has('onClose', onClose));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/components/image_attachment.dart b/packages/zeta_flutter/lib/src/components/message_input/components/image_attachment.dart
new file mode 100644
index 000000000..4fa3eec18
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/components/image_attachment.dart
@@ -0,0 +1,37 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+
+/// Attachment item for displaying image files.
+class ImageAttachment extends ZetaStatelessWidget {
+ /// Creates an [ImageAttachment] widget.
+ const ImageAttachment({super.key, required this.file});
+
+ /// The file to be displayed.
+ final File file;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+ final size = spacing.xl_8 + spacing.medium;
+
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(spacing.medium),
+ child: Image(
+ image: FileImage(file),
+ width: size,
+ height: size,
+ fit: BoxFit.cover,
+ ),
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty('file', file));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/components/video_attachment.dart b/packages/zeta_flutter/lib/src/components/message_input/components/video_attachment.dart
new file mode 100644
index 000000000..a8da74d72
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/components/video_attachment.dart
@@ -0,0 +1,60 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+import '../utils/file_type_checker.dart';
+
+/// Attachment item for displaying video files.
+class VideoAttachment extends ZetaStatelessWidget {
+ /// Creates a [VideoAttachment] widget.
+ const VideoAttachment({
+ super.key,
+ required this.file,
+ });
+
+ /// The file to be displayed.
+ final File file;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+ final size = spacing.xl_8 + spacing.medium;
+
+ return FutureBuilder?>(
+ future: FileTypeChecker.getVideoThumbnail(file),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: Image(
+ image: snapshot.data!,
+ width: size,
+ height: size,
+ fit: BoxFit.cover,
+ ),
+ );
+ } else if (snapshot.connectionState == ConnectionState.done && !snapshot.hasData) {
+ return SizedBox(
+ width: size,
+ height: size,
+ child: const Center(child: Icon(ZetaIcons.video)),
+ );
+ } else {
+ return SizedBox(
+ width: size,
+ height: size,
+ child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
+ );
+ }
+ },
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty('file', file));
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/message_input.dart b/packages/zeta_flutter/lib/src/components/message_input/message_input.dart
new file mode 100644
index 000000000..913d9bfde
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/message_input.dart
@@ -0,0 +1,409 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:location/location.dart';
+
+import '../../../zeta_flutter.dart';
+import 'actions/action_button.dart';
+import 'actions/attachment_button.dart';
+import 'actions/image_button.dart';
+import 'actions/location_button.dart';
+import 'actions/send_button.dart';
+import 'actions/video_button.dart';
+import 'actions/voice_button.dart';
+import 'actions/voice_memo_button.dart';
+import 'panels/actions_panel.dart';
+import 'panels/attachments_panel.dart';
+
+/// A customizable message input widget for user text entry and sending actions.
+///
+/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS-Zeta---Components?node-id=18271-19682&m=dev
+///
+/// Widgetbook:
+class ZetaMessageInput extends ZetaStatefulWidget {
+ /// Default message input
+ const ZetaMessageInput({
+ super.key,
+ this.controller,
+ this.onSend,
+ this.onLongPressSend,
+ this.onSendAttachments,
+ this.onSendVoiceMemo,
+ this.onSendLocation,
+ this.placeholder,
+ this.allowsVoiceInput,
+ this.cameraTrailingButton,
+ this.trailingButton,
+ this.attachmentAction = true,
+ this.pictureAction = true,
+ this.videoAction = true,
+ this.voiceMemoAction = true,
+ this.locationAction = true,
+ this.minLines,
+ this.maxLines,
+ this.hasActionMenu = false,
+ this.actions,
+ this.attachments,
+ this.allowedExtensions,
+ this.multiAttach = true,
+ this.disabled,
+ this.maxAttachmentSize = 1000000,
+ this.maxVoiceMemoDuration = const Duration(minutes: 1),
+ });
+
+ /// Comment input
+ const ZetaMessageInput.comment({
+ super.key,
+ this.controller,
+ this.onSend,
+ this.onLongPressSend,
+ this.onSendAttachments,
+ this.onSendVoiceMemo,
+ this.onSendLocation,
+ this.placeholder = 'Add a comment',
+ this.allowsVoiceInput,
+ this.cameraTrailingButton,
+ this.trailingButton,
+ this.attachmentAction = false,
+ this.pictureAction = false,
+ this.videoAction = false,
+ this.voiceMemoAction = false,
+ this.locationAction = false,
+ this.minLines = 1,
+ this.maxLines = 1,
+ this.hasActionMenu = false,
+ this.actions,
+ this.attachments,
+ this.allowedExtensions,
+ this.multiAttach = false,
+ this.disabled,
+ this.maxAttachmentSize = 1000000,
+ this.maxVoiceMemoDuration = const Duration(minutes: 1),
+ });
+
+ /// Message input with predefined actions
+ const ZetaMessageInput.actionMenu({
+ super.key,
+ this.controller,
+ this.onSend,
+ this.onLongPressSend,
+ this.onSendAttachments,
+ this.onSendVoiceMemo,
+ this.onSendLocation,
+ this.placeholder,
+ this.allowsVoiceInput,
+ this.cameraTrailingButton,
+ this.trailingButton,
+ this.attachmentAction = true,
+ this.pictureAction = true,
+ this.videoAction = true,
+ this.voiceMemoAction = true,
+ this.locationAction = true,
+ this.minLines = 1,
+ this.maxLines = 5,
+ this.hasActionMenu = true,
+ this.actions,
+ this.attachments,
+ this.allowedExtensions,
+ this.multiAttach = true,
+ this.disabled,
+ this.maxAttachmentSize = 1000000,
+ this.maxVoiceMemoDuration = const Duration(minutes: 1),
+ });
+
+ /// The text editing controller for the message input.
+ final TextEditingController? controller;
+
+ /// When there is no text in the input the send button will be greyed out.
+ /// However, the onSend and onLongPressSend callbacks will still be triggered.
+ final ValueChanged? onSend;
+
+ /// Callback for long press on the send button.
+ final ValueChanged? onLongPressSend;
+
+ /// Callback for sending attachments (images, videos, files, etc.).
+ final ValueChanged>? onSendAttachments;
+
+ /// Callback for sending voice memo.
+ final void Function(File file, Uint8List bytes)? onSendVoiceMemo;
+
+ /// Callback for sending location.
+ final ValueChanged? onSendLocation;
+
+ /// Placeholder text for the message input.
+ final String? placeholder;
+
+ /// Whether the message input should support voice input.
+ final bool? allowsVoiceInput;
+
+ /// Whether the camera button shows in place of a trailing button
+ /// Override trailingButton prop
+ final bool? cameraTrailingButton;
+
+ /// The trailing button to show when the text field and attachments are empty;
+ final Widget? trailingButton;
+
+ /// Whether to show the attachment button.
+ /// Defaults to true
+ final bool? attachmentAction;
+
+ /// Whether to show the picture button.
+ /// Defaults to true
+ final bool? pictureAction;
+
+ /// Whether to show the video button.
+ /// Defaults to true
+ final bool? videoAction;
+
+ /// Whether to show the voice button.
+ /// Defaults to true
+ final bool? voiceMemoAction;
+
+ /// Whether to show the location button.
+ /// Defaults to true
+ final bool? locationAction;
+
+ /// {@macro flutter.widgets.editableText.minLines}
+ /// Minimum number of lines for the text input.
+ final int? minLines;
+
+ /// {@macro flutter.widgets.editableText.maxLines}
+ /// Maximum number of lines for the text input.
+ final int? maxLines;
+
+ /// Whether to have the actions menu
+ /// Defaults to true
+ /// If true, it will use all the default action props
+ final bool? hasActionMenu;
+
+ /// The list of actions to display in the message input.
+ final List? actions;
+
+ /// The list of file attachments.
+ final List? attachments;
+
+ /// A list of allowed file extensions.
+ /// If null, it will accept any extension.
+ final List? allowedExtensions;
+
+ /// Whether or not the attachment button should let the user attach multiple files at once.
+ final bool? multiAttach;
+
+ /// Whether the entire message input is disabled.
+ /// Defaults to false.
+ final bool? disabled;
+
+ /// The maximum size of attachments in bytes.
+ /// Defaults to 1,000,000 bytes || 1MB
+ final int? maxAttachmentSize;
+
+ /// The maximum duration of voice memos.
+ final Duration? maxVoiceMemoDuration;
+
+ @override
+ State createState() => _MessageInputState();
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('controller', controller))
+ ..add(ObjectFlagProperty?>.has('onSend', onSend))
+ ..add(ObjectFlagProperty?>.has('onLongPressSend', onLongPressSend))
+ ..add(ObjectFlagProperty>?>.has('onSendAttachment', onSendAttachments))
+ ..add(StringProperty('placeholder', placeholder))
+ ..add(IntProperty('minLines', minLines))
+ ..add(IntProperty('maxLines', maxLines))
+ ..add(DiagnosticsProperty('voice', allowsVoiceInput))
+ ..add(DiagnosticsProperty('allowsCameraInput', cameraTrailingButton))
+ ..add(DiagnosticsProperty('usePresetActions', hasActionMenu))
+ ..add(IterableProperty('attachments', attachments))
+ ..add(DiagnosticsProperty('attachmentAction', attachmentAction))
+ ..add(DiagnosticsProperty('cameraAction', pictureAction))
+ ..add(DiagnosticsProperty('videoAction', videoAction))
+ ..add(DiagnosticsProperty('voiceAction', voiceMemoAction))
+ ..add(DiagnosticsProperty('locationAction', locationAction))
+ ..add(IterableProperty('allowedExtensions', allowedExtensions))
+ ..add(DiagnosticsProperty('multiAttach', multiAttach))
+ ..add(ObjectFlagProperty.has('onSendVoiceMemo', onSendVoiceMemo))
+ ..add(ObjectFlagProperty?>.has('onSendLocation', onSendLocation))
+ ..add(DiagnosticsProperty('disabled', disabled))
+ ..add(IntProperty('maxAttachmentSize', maxAttachmentSize))
+ ..add(DiagnosticsProperty('maxVoiceMemoDuration', maxVoiceMemoDuration));
+ }
+}
+
+class _MessageInputState extends State {
+ late TextEditingController _controller;
+ late bool _isMessageContentEmpty;
+ late bool _isActionMenuOpen;
+ final FocusNode _focusNode = FocusNode();
+ late List _attachments;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = widget.controller ?? TextEditingController();
+ _isMessageContentEmpty = _controller.text.isEmpty;
+ _attachments = widget.attachments ?? [];
+ _isActionMenuOpen = false;
+ }
+
+ @override
+ void dispose() {
+ if (widget.controller == null) {
+ _controller.dispose();
+ }
+ _focusNode.dispose();
+ super.dispose();
+ }
+
+ void _handleTextChange(String? text) {
+ if (text == null) return;
+
+ final isTextEmpty = text.trim().isEmpty && _attachments.isEmpty;
+ if (_isMessageContentEmpty != isTextEmpty) setState(() => _isMessageContentEmpty = isTextEmpty);
+ }
+
+ void _handleSend(String text, List attachments) {
+ widget.onSend!(text);
+ widget.onSendAttachments?.call(attachments);
+ }
+
+ void _handleLongPressSend(String text, List attachments) {
+ widget.onLongPressSend!(text);
+ widget.onSendAttachments?.call(attachments);
+ }
+
+ void _openActionsMenu() => setState(() => _isActionMenuOpen = !_isActionMenuOpen);
+ void _onCloseAttachment(int index) => setState(() => _attachments.removeAt(index));
+ void _onCapture(File file) => setState(() => _attachments.add(file));
+ void _onAttach(List files) => setState(() => _attachments.addAll(files));
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+ final verticalPadding = spacing.medium;
+ final horizontalPadding = spacing.large;
+
+ final showCameraButton = _isMessageContentEmpty && (widget.cameraTrailingButton ?? false);
+ final showTrailingButton =
+ _isMessageContentEmpty && widget.trailingButton != null && !(widget.cameraTrailingButton ?? false);
+ final showSendButton = !_isMessageContentEmpty || (!showCameraButton && !showTrailingButton);
+
+ return SafeArea(
+ child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () async {
+ /* This keeps the keyboard open when using the message input */
+ /* TODO: glitch where if the user taps the ZetaTextInput border it closes the keyboard */
+ await SystemChannels.textInput.invokeMethod('TextInput.show');
+ },
+ child: AnimatedSize(
+ duration: const Duration(milliseconds: 200),
+ child: Column(
+ children: [
+ if (_attachments.isNotEmpty)
+ AttachmentsPanel(
+ attachments: _attachments,
+ onCloseAttachment: _onCloseAttachment,
+ ),
+ Container(
+ padding: EdgeInsets.symmetric(
+ vertical: verticalPadding,
+ horizontal: horizontalPadding,
+ ),
+ decoration: BoxDecoration(
+ color: colors.surfaceDefault,
+ border: Border(
+ top: BorderSide(color: colors.borderDefault),
+ ),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ textBaseline: TextBaseline.ideographic,
+ children: [
+ if (widget.hasActionMenu ?? false)
+ ActionButton(
+ onPressed: _openActionsMenu,
+ icon: ZetaIcons.add,
+ disabled: widget.disabled,
+ semanticLabel: 'open action menu',
+ )
+ else
+ const Nothing(),
+ Expanded(
+ child: ZetaTextInput(
+ disabled: widget.disabled ?? false,
+ onChange: _handleTextChange,
+ focusNode: _focusNode,
+ minLines: widget.minLines,
+ maxLines: widget.maxLines,
+ controller: _controller,
+ rounded: context.rounded,
+ placeholder: widget.placeholder,
+ semanticLabel: widget.placeholder,
+ suffix: widget.allowsVoiceInput ?? false
+ ? VoiceButton(
+ controller: _controller,
+ disabled: widget.disabled,
+ )
+ : null,
+ ),
+ ),
+ if (showCameraButton)
+ ImageButton(
+ onCapture: _onCapture,
+ disabled: widget.disabled,
+ ),
+ if (showTrailingButton) widget.trailingButton!,
+ if (showSendButton)
+ SendButton(
+ controller: _controller,
+ attachments: _attachments,
+ onPressed: widget.onSend != null ? _handleSend : null,
+ onLongPress: widget.onLongPressSend != null ? _handleLongPressSend : null,
+ disabled: (_isMessageContentEmpty || widget.onSend == null) || (widget.disabled ?? false),
+ ),
+ ].gap(spacing.small),
+ ),
+ ),
+ SizedBox(
+ height: (_isActionMenuOpen && (widget.hasActionMenu ?? false)) ? null : 0,
+ child: ActionsPanel(
+ actions: [
+ if (widget.attachmentAction ?? false)
+ AttachmentButton(
+ allowMultiple: widget.multiAttach ?? false,
+ allowedExtensions: widget.allowedExtensions,
+ maxSize: widget.maxAttachmentSize,
+ onAttach: widget.onSendAttachments != null ? _onAttach : null,
+ ),
+ if (widget.pictureAction ?? false)
+ ImageButton(
+ onCapture: widget.onSendAttachments != null ? _onCapture : null,
+ ),
+ if (widget.videoAction ?? false)
+ VideoButton(
+ onCapture: widget.onSendAttachments != null ? _onCapture : null,
+ ),
+ if (widget.voiceMemoAction ?? false)
+ VoiceMemoButton(onSend: widget.onSendVoiceMemo, maxDuration: widget.maxVoiceMemoDuration),
+ if (widget.locationAction ?? false)
+ LocationButton(
+ onLocationCapture: widget.onSendLocation,
+ ),
+ if (widget.actions != null) ...widget.actions!,
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/panels/actions_panel.dart b/packages/zeta_flutter/lib/src/components/message_input/panels/actions_panel.dart
new file mode 100644
index 000000000..98d200a3d
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/panels/actions_panel.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+
+/// A widget that displays a horizontal action menu with given action buttons.
+class ActionsPanel extends ZetaStatelessWidget {
+ /// Creates an [ActionsPanel] with the given [actions].
+ const ActionsPanel({
+ super.key,
+ required this.actions,
+ });
+
+ /// The list of action buttons to display in the menu.
+ final List actions;
+
+ @override
+ Widget build(BuildContext context) {
+ final ZetaColors colors = Zeta.of(context).colors;
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ return ColoredBox(
+ color: colors.surfaceDefault,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: actions.map(
+ (action) {
+ return Padding(
+ padding: EdgeInsets.symmetric(
+ horizontal: spacing.small,
+ ),
+ child: action,
+ );
+ },
+ ).toList(),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/panels/attachments_panel.dart b/packages/zeta_flutter/lib/src/components/message_input/panels/attachments_panel.dart
new file mode 100644
index 000000000..8781b2394
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/panels/attachments_panel.dart
@@ -0,0 +1,64 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../../../zeta_flutter.dart';
+import '../components/file_attachment.dart';
+
+/// A bar that displays the list of attachments above the message input field.
+class AttachmentsPanel extends ZetaStatefulWidget {
+ /// Creates an [AttachmentsPanel] widget.
+ const AttachmentsPanel({
+ super.key,
+ required this.attachments,
+ required this.onCloseAttachment,
+ });
+
+ /// A list of files to display
+ final List attachments;
+
+ /// Callback function to be called when an attachment is closed.
+ final ValueChanged onCloseAttachment;
+
+ @override
+ State createState() => _AttachmentsPanelState();
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(IterableProperty('attachments', attachments))
+ ..add(ObjectFlagProperty>.has('onCloseAttachment', onCloseAttachment));
+ }
+}
+
+class _AttachmentsPanelState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final ZetaSpacing spacing = Zeta.of(context).spacing;
+
+ return Container(
+ width: double.infinity,
+ color: Colors.transparent,
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(
+ horizontal: spacing.large,
+ vertical: spacing.small,
+ ),
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ textDirection: TextDirection.rtl,
+ children: List.generate(
+ widget.attachments.length,
+ (int i) => FileAttachment(
+ file: widget.attachments[i],
+ index: i,
+ onClose: widget.onCloseAttachment,
+ ),
+ ).gap(spacing.medium),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/message_input/utils/file_type_checker.dart b/packages/zeta_flutter/lib/src/components/message_input/utils/file_type_checker.dart
new file mode 100644
index 000000000..80e291321
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/components/message_input/utils/file_type_checker.dart
@@ -0,0 +1,157 @@
+// not needed internal file
+// ignore_for_file: public_member_api_docs
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:get_thumbnail_video/index.dart';
+import 'package:get_thumbnail_video/video_thumbnail.dart';
+
+import '../../../../zeta_flutter.dart';
+import '../../../utils/file_attachment_icons.dart';
+// import 'package:mime/mime.dart';
+
+/// File type enumeration
+enum FileType {
+ image,
+ video,
+ document,
+ audio,
+ other,
+}
+
+/// A utility class for checking file types.
+class FileTypeChecker {
+ static const List _imageExtensions = [
+ 'jpg',
+ 'jpeg',
+ 'png',
+ 'gif',
+ 'bmp',
+ 'webp',
+ 'tiff',
+ 'svg',
+ 'ico',
+ 'heic',
+ 'heif',
+ ];
+
+ static const List _videoExtensions = [
+ 'mp4',
+ 'avi',
+ 'mov',
+ 'wmv',
+ 'flv',
+ 'webm',
+ 'mkv',
+ '3gp',
+ 'm4v',
+ 'f4v',
+ 'asf',
+ 'rm',
+ 'rmvb',
+ ];
+
+ static const List _documentExtensions = [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'ppt',
+ 'pptx',
+ 'txt',
+ 'rtf',
+ 'odt',
+ 'ods',
+ 'odp',
+ 'csv',
+ ];
+
+ static const List _audioExtensions = [
+ 'mp3',
+ 'wav',
+ 'flac',
+ 'aac',
+ 'ogg',
+ 'wma',
+ 'm4a',
+ 'aiff',
+ 'au',
+ ];
+
+ /// Check file type using extension
+ static FileType getFileTypeByExtension(File file) {
+ final extension = file.path.split('.').last.toLowerCase();
+
+ if (_imageExtensions.contains(extension)) return FileType.image;
+ if (_videoExtensions.contains(extension)) return FileType.video;
+ if (_documentExtensions.contains(extension)) return FileType.document;
+ if (_audioExtensions.contains(extension)) return FileType.audio;
+
+ return FileType.other;
+ }
+
+ /// Check file type using MIME type
+ // static FileType getFileTypeByMime(File file) {
+ // final mimeType = lookupMimeType(file.path);
+
+ // if (mimeType == null) return FileType.other;
+
+ // if (mimeType.startsWith('image/')) return FileType.image;
+ // if (mimeType.startsWith('video/')) return FileType.video;
+ // if (mimeType.startsWith('audio/')) return FileType.audio;
+ // if (mimeType.startsWith('application/') || mimeType.startsWith('text/')) {
+ // return FileType.document;
+ // }
+
+ // return FileType.other;
+ // }
+
+ /// Convenience methods
+ /// returns true if the file has an image extension (.png, .jpg, etc.)
+ static bool isImage(File file) => getFileTypeByExtension(file) == FileType.image;
+
+ /// returns true if the file has an video extension (.mp4, .avi, etc.)
+ static bool isVideo(File file) => getFileTypeByExtension(file) == FileType.video;
+
+ /// returns true if the file has a document extension (.pdf, .docx, etc.)
+ static bool isDocument(File file) => getFileTypeByExtension(file) == FileType.document;
+
+ /// returns true if the file has an audio extension (.mp3, .wav, etc.)
+ static bool isAudio(File file) => getFileTypeByExtension(file) == FileType.audio;
+
+ /// Generate a thumbnail from a video file
+ static Future getVideoThumbnail(File videoFile) async {
+ try {
+ final uint8list = await VideoThumbnail.thumbnailData(
+ video: videoFile.path,
+ imageFormat: ImageFormat.JPEG,
+ maxWidth: 780,
+ quality: 75,
+ );
+
+ return MemoryImage(uint8list);
+ } catch (e) {
+ // Return a default video icon if thumbnail generation fails
+ // throw Exception('Failed to generate thumbnail: $e');
+ return null;
+ }
+ }
+
+ /// Get the appropriate icon for a file based on its extension
+ static Widget getFileIcon(BuildContext context, File file) {
+ final spacing = Zeta.of(context).spacing;
+
+ final extension = file.path.split('.').last.toLowerCase();
+
+ final icon = getFileExtensionIcon(extension);
+
+ if (icon != null) {
+ return icon.paddingStart(spacing.small);
+ }
+
+ // Default file icon
+ return const ZetaIcon(ZetaIcons.attachment);
+ }
+}
diff --git a/packages/zeta_flutter/lib/src/components/text_input/internal_text_input.dart b/packages/zeta_flutter/lib/src/components/text_input/internal_text_input.dart
index b612dd4bc..4cb8b8b29 100644
--- a/packages/zeta_flutter/lib/src/components/text_input/internal_text_input.dart
+++ b/packages/zeta_flutter/lib/src/components/text_input/internal_text_input.dart
@@ -39,6 +39,8 @@ class InternalTextInput extends ZetaStatefulWidget {
this.borderRadius,
this.textInputAction,
this.constrained = false,
+ this.minLines,
+ this.maxLines,
}) : requirementLevel = requirementLevel ?? ZetaFormFieldRequirement.none,
assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'),
assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.');
@@ -139,6 +141,14 @@ class InternalTextInput extends ZetaStatefulWidget {
/// Determines if the prefix and suffix should be constrained.
final bool constrained;
+ /// The minimum number of lines the input can display.
+ /// Defaults to 1.
+ final int? minLines;
+
+ /// The maximum number of lines the input can display.
+ /// Defaults to 1.
+ final int? maxLines;
+
@override
State createState() => InternalTextInputState();
@override
@@ -167,7 +177,9 @@ class InternalTextInput extends ZetaStatefulWidget {
..add(DiagnosticsProperty('borderRadius', borderRadius))
..add(StringProperty('semanticLabel', semanticLabel))
..add(EnumProperty('textInputAction', textInputAction))
- ..add(DiagnosticsProperty('constrained', constrained));
+ ..add(DiagnosticsProperty('constrained', constrained))
+ ..add(IntProperty('minLines', minLines))
+ ..add(IntProperty('maxLines', maxLines));
}
}
@@ -353,6 +365,8 @@ class InternalTextInputState extends State {
cursorErrorColor: _colors.mainNegative,
obscureText: widget.obscureText,
focusNode: widget.focusNode,
+ maxLines: widget.maxLines ?? 1,
+ minLines: widget.minLines,
decoration: InputDecoration(
isDense: true,
contentPadding: _contentPadding,
diff --git a/packages/zeta_flutter/lib/src/components/text_input/text_input.dart b/packages/zeta_flutter/lib/src/components/text_input/text_input.dart
index 202147382..9923d6368 100644
--- a/packages/zeta_flutter/lib/src/components/text_input/text_input.dart
+++ b/packages/zeta_flutter/lib/src/components/text_input/text_input.dart
@@ -48,6 +48,8 @@ class ZetaTextInput extends ZetaTextFormField {
this.keyboardType,
this.focusNode,
this.semanticLabel,
+ this.minLines,
+ this.maxLines,
}) : assert(initialValue == null || controller == null, 'Only one of initial value and controller can be accepted.'),
assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'),
assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'),
@@ -77,6 +79,8 @@ class ZetaTextInput extends ZetaTextFormField {
keyboardType: keyboardType,
focusNode: focusNode,
semanticLabel: semanticLabel,
+ minLines: minLines,
+ maxLines: maxLines,
);
},
);
@@ -150,6 +154,14 @@ class ZetaTextInput extends ZetaTextFormField {
/// {@macro zeta-widget-semantic-label}
final String? semanticLabel;
+ /// The minimum number of lines the input can display.
+ /// Defaults to 1.
+ final int? minLines;
+
+ /// The maximum number of lines the input can display.
+ /// Defaults to 1.
+ final int? maxLines;
+
@override
FormFieldState createState() => ZetaTextFormFieldState();
@@ -176,6 +188,8 @@ class ZetaTextInput extends ZetaTextFormField {
..add(DiagnosticsProperty('obscureText', obscureText))
..add(DiagnosticsProperty('keyboardType', keyboardType))
..add(DiagnosticsProperty('focusNode', focusNode))
- ..add(StringProperty('semanticLabel', semanticLabel));
+ ..add(StringProperty('semanticLabel', semanticLabel))
+ ..add(IntProperty('minLines', minLines))
+ ..add(IntProperty('maxLines', maxLines));
}
}
diff --git a/packages/zeta_flutter/lib/src/components/voice_memo/state/playback_state.dart b/packages/zeta_flutter/lib/src/components/voice_memo/state/playback_state.dart
index c593d75f1..dad135992 100644
--- a/packages/zeta_flutter/lib/src/components/voice_memo/state/playback_state.dart
+++ b/packages/zeta_flutter/lib/src/components/voice_memo/state/playback_state.dart
@@ -22,14 +22,14 @@ enum FileFetchMode {
/// State class for managing audio playback in the [ZetaVoiceMemo] and [ZetaAudioVisualizer].
class PlaybackState extends ChangeNotifier {
/// Constructs a [PlaybackState].
- PlaybackState({String? assetPath, String? deviceFilePath, String? url}) {
- unawaited(_init(assetPath, deviceFilePath, url));
+ PlaybackState({String? assetPath, String? deviceFilePath, String? url, Uint8List? bytes}) {
+ unawaited(_init(assetPath, deviceFilePath, url, bytes));
_audioPlayer.onPlayerComplete.listen((_) => resetPlayback());
_positionSubscription = _audioPlayer.onPositionChanged.listen(_onPositionChanged);
}
- Future _init(String? assetPath, String? deviceFilePath, String? url) async {
- await loadAudio(assetPath: assetPath, deviceFilePath: deviceFilePath, url: url);
+ Future _init(String? assetPath, String? deviceFilePath, String? url, Uint8List? bytes) async {
+ await loadAudio(assetPath: assetPath, deviceFilePath: deviceFilePath, url: url, bytes: bytes);
await resetPlayback();
}
@@ -98,6 +98,7 @@ class PlaybackState extends ChangeNotifier {
String? assetPath,
String? url,
String? deviceFilePath,
+ Uint8List? bytes,
List? audioChunks,
RecordConfig? recordConfig,
}) async {
@@ -107,6 +108,8 @@ class PlaybackState extends ChangeNotifier {
_localFile = await handleFile(url, FileFetchMode.url);
} else if (deviceFilePath != null) {
_localFile = Uri.file(deviceFilePath);
+ } else if (bytes != null) {
+ _audioChunks = bytes;
} else if (audioChunks != null && audioChunks.isNotEmpty && recordConfig != null) {
_audioChunks = Uint8List.fromList([
...generatePCMWavHeader(audioChunks, recordConfig),
diff --git a/packages/zeta_flutter/lib/src/components/voice_memo/ui/audio_visualizer.dart b/packages/zeta_flutter/lib/src/components/voice_memo/ui/audio_visualizer.dart
index 574951619..335a4bff0 100644
--- a/packages/zeta_flutter/lib/src/components/voice_memo/ui/audio_visualizer.dart
+++ b/packages/zeta_flutter/lib/src/components/voice_memo/ui/audio_visualizer.dart
@@ -26,6 +26,7 @@ class ZetaAudioVisualizer extends ZetaStatefulWidget {
this.onPause,
this.onPlay,
this.errorMessage = 'Audio cannot be played',
+ this.bytes,
});
/// The path of a local audio asset to visualize.
@@ -74,6 +75,9 @@ class ZetaAudioVisualizer extends ZetaStatefulWidget {
/// Error message to display when audio can not be played.
final String errorMessage;
+ /// A stream of audio bytes to visualize.
+ final Uint8List? bytes;
+
@override
State createState() => _ZetaAudioVisualizerState();
@@ -91,7 +95,8 @@ class ZetaAudioVisualizer extends ZetaStatefulWidget {
..add(ColorProperty('foregroundColor', foregroundColor))
..add(ColorProperty('tertiaryColor', tertiaryColor))
..add(ColorProperty('playButtonColor', playButtonColor))
- ..add(StringProperty('errorMessage', errorMessage));
+ ..add(StringProperty('errorMessage', errorMessage))
+ ..add(ObjectFlagProperty('bytes', bytes));
}
}
@@ -229,6 +234,7 @@ class _ZetaAudioVisualizerState extends State {
assetPath: widget.assetPath,
deviceFilePath: widget.deviceFilePath,
url: widget.url,
+ bytes: widget.bytes,
),
child: Consumer(
builder: (context, state, _) => _buildVisualizer(context, state),
diff --git a/packages/zeta_flutter/lib/src/components/voice_memo/ui/voice_memo.dart b/packages/zeta_flutter/lib/src/components/voice_memo/ui/voice_memo.dart
index 18ab26c34..078005c83 100644
--- a/packages/zeta_flutter/lib/src/components/voice_memo/ui/voice_memo.dart
+++ b/packages/zeta_flutter/lib/src/components/voice_memo/ui/voice_memo.dart
@@ -21,6 +21,7 @@ class ZetaVoiceMemo extends ZetaStatefulWidget {
this.recordingLabel = 'Recording message...',
this.maxLimitLabel = 'Recording message {timer} seconds left...',
this.sendMessageLabel = 'Send message?',
+ this.sendActionIcon = ZetaIcons.send,
this.playingLabel = 'Playing...',
this.recordingNotAllowedLabel = 'Recording not allowed.',
this.onDiscard,
@@ -61,6 +62,11 @@ class ZetaVoiceMemo extends ZetaStatefulWidget {
/// {@macro zeta-widget-translations}
final String sendMessageLabel;
+ /// The icon to show on the trailing action
+ ///
+ /// {@macro zeta-widget-translations}
+ final IconData sendActionIcon;
+
/// The label shown when the voice memo is playing.
///
/// Defaults to 'Playing...'
@@ -128,7 +134,8 @@ class ZetaVoiceMemo extends ZetaStatefulWidget {
..add(StringProperty('recordingNotAllowedLabel', recordingNotAllowedLabel))
..add(ObjectFlagProperty.has('onSend', onSend))
..add(DiagnosticsProperty('recordConfig', recordConfig))
- ..add(DoubleProperty('loudnessMultiplier', loudnessMultiplier));
+ ..add(DoubleProperty('loudnessMultiplier', loudnessMultiplier))
+ ..add(DiagnosticsProperty('sendActionIcon', sendActionIcon));
}
}
@@ -239,7 +246,7 @@ class _ZetaVoiceMemoState extends State {
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
- icon: Icon(ZetaIcons.send, size: zeta.spacing.xl_6),
+ icon: Icon(widget.sendActionIcon, size: zeta.spacing.xl_6),
padding: EdgeInsets.all(zeta.spacing.minimum),
color: zeta.colors.mainPrimary,
onPressed: (state.canRecord &&
diff --git a/packages/zeta_flutter/lib/src/utils/file_attachment_icons.dart b/packages/zeta_flutter/lib/src/utils/file_attachment_icons.dart
new file mode 100644
index 000000000..c022d4ba8
--- /dev/null
+++ b/packages/zeta_flutter/lib/src/utils/file_attachment_icons.dart
@@ -0,0 +1,101 @@
+import 'package:flutter/material.dart';
+
+import '../../zeta_flutter.dart';
+
+/// Returns a ZetaIcon based on the file extension.
+/// Returns null if no matching icon is found.
+ZetaIcon? getFileExtensionIcon(String extension) {
+ final ext = extension.toLowerCase();
+ final iconData = _extensionIconMap[ext];
+ return iconData != null ? ZetaIcon(iconData) : null;
+}
+
+const Map _extensionIconMap = {
+ 'aac': ZetaIcons.aac_attach,
+ 'ai': ZetaIcons.ai_attach,
+ 'asp': ZetaIcons.asp_attach,
+ '3ds': ZetaIcons.attach_3ds,
+ 'au': ZetaIcons.au_attach,
+ 'avi': ZetaIcons.avi_attach,
+ 'bin': ZetaIcons.bin_attach,
+ 'bmp': ZetaIcons.bmp_attach,
+ 'com': ZetaIcons.com_attach,
+ 'css': ZetaIcons.css_attach,
+ 'csv': ZetaIcons.csv_attach,
+ 'dat': ZetaIcons.dat_attach,
+ 'dbf': ZetaIcons.dbf_attach,
+ 'dll': ZetaIcons.dll_attach,
+ 'doc': ZetaIcons.doc_attach,
+ 'docx': ZetaIcons.docx_attach,
+ 'dwg': ZetaIcons.dwg_attach,
+ 'eml': ZetaIcons.eml_attach,
+ 'eps': ZetaIcons.eps_attach,
+ 'exe': ZetaIcons.exe_attach,
+ 'ext': ZetaIcons.ext_attach,
+ 'fla': ZetaIcons.fla_attach,
+ 'fodg': ZetaIcons.fodg_attach,
+ 'fodp': ZetaIcons.fodp_attach,
+ 'fods': ZetaIcons.fods_attach,
+ 'gif': ZetaIcons.gif_attach,
+ 'htm': ZetaIcons.htm_attach,
+ 'html': ZetaIcons.html_attach,
+ 'ico': ZetaIcons.ico_attach,
+ 'ini': ZetaIcons.ini_attach,
+ 'iso': ZetaIcons.iso_attach,
+ 'jar': ZetaIcons.jar_attach,
+ 'jpeg': ZetaIcons.jpeg_attach,
+ 'jpg': ZetaIcons.jpg_attach,
+ 'js': ZetaIcons.js_attach,
+ 'json': ZetaIcons.json_attach,
+ 'key': ZetaIcons.key_attach,
+ 'mdb': ZetaIcons.mdb_attach,
+ 'mkv': ZetaIcons.mkv_attach,
+ 'mov': ZetaIcons.mov_attach,
+ 'mp3': ZetaIcons.mp3_attach,
+ 'mp4': ZetaIcons.mp4_attach,
+ 'mpeg': ZetaIcons.mpeg_attach,
+ 'mpg': ZetaIcons.mpg_attach,
+ 'mpga': ZetaIcons.mpga_attach,
+ 'nfo': ZetaIcons.nfo_attach,
+ 'obj': ZetaIcons.obj_attach,
+ 'odf': ZetaIcons.odf_attach,
+ 'odp': ZetaIcons.odp_attach,
+ 'ods': ZetaIcons.ods_attach,
+ 'odt': ZetaIcons.odt_attach,
+ 'oog': ZetaIcons.oog_attach,
+ 'otf': ZetaIcons.otf_attach,
+ 'ott': ZetaIcons.ott_attach,
+ 'pdf': ZetaIcons.pdf_attach,
+ 'pkg': ZetaIcons.pkg_attach,
+ 'png': ZetaIcons.png_attach,
+ 'pps': ZetaIcons.pps_attach,
+ 'ppt': ZetaIcons.ppt_attach,
+ 'pptx': ZetaIcons.pptx_attach,
+ 'psd': ZetaIcons.psd_attach,
+ 'rar': ZetaIcons.rar_attach,
+ 'rtf': ZetaIcons.rtf_attach,
+ 'sdc': ZetaIcons.sdc_attach,
+ 'sdd': ZetaIcons.sdd_attach,
+ 'svg': ZetaIcons.svg_attach,
+ 'swf': ZetaIcons.swf_attach,
+ 'tif': ZetaIcons.tif_attach,
+ 'tiff': ZetaIcons.tiff_attach,
+ 'ttf': ZetaIcons.ttf_attach,
+ 'txt': ZetaIcons.txt_attach,
+ 'vcf': ZetaIcons.vcf_attach,
+ 'video': ZetaIcons.video_attach,
+ 'wav': ZetaIcons.wav_attach,
+ 'webm': ZetaIcons.webm_attach,
+ 'wmv': ZetaIcons.wmv_attach,
+ 'woff': ZetaIcons.woff_attach,
+ 'xlff': ZetaIcons.xlff_attach,
+ 'xliff': ZetaIcons.xliff_attach,
+ 'xls': ZetaIcons.xls_attach,
+ 'xlsb': ZetaIcons.xlsb_attach,
+ 'xlsm': ZetaIcons.xlsm_attach,
+ 'xlsx': ZetaIcons.xlsx_attach,
+ 'xml': ZetaIcons.xml_attach,
+ 'xsd': ZetaIcons.xsd_attach,
+ 'xslt': ZetaIcons.xslt_attach,
+ 'zip': ZetaIcons.zip_attach,
+};
diff --git a/packages/zeta_flutter/lib/zeta_components.dart b/packages/zeta_flutter/lib/zeta_components.dart
index 03d02bc93..ea8c80520 100644
--- a/packages/zeta_flutter/lib/zeta_components.dart
+++ b/packages/zeta_flutter/lib/zeta_components.dart
@@ -40,6 +40,7 @@ export 'src/components/in_page_banner/in_page_banner.dart';
export 'src/components/list_item/dropdown_list_item.dart';
export 'src/components/list_item/list_item.dart';
export 'src/components/list_item/notification_list_item.dart';
+export 'src/components/message_input/message_input.dart';
export 'src/components/navigation bar/navigation_bar.dart' hide NavigationItem;
export 'src/components/navigation_rail/navigation_rail.dart';
export 'src/components/pagination/pagination.dart';
diff --git a/packages/zeta_flutter/pubspec.yaml b/packages/zeta_flutter/pubspec.yaml
index 8fa258d11..2cce78571 100644
--- a/packages/zeta_flutter/pubspec.yaml
+++ b/packages/zeta_flutter/pubspec.yaml
@@ -14,14 +14,19 @@ resolution: workspace
dependencies:
audioplayers: ^6.5.1
collection: ^1.19.1
+ file_picker: ^10.3.3
flutter:
sdk: flutter
flutter_slidable: ^4.0.1
+ get_thumbnail_video: ^0.7.2
http: ^1.5.0
+ image_picker: ^1.2.0
intl: ^0.20.2
+ location: ^8.0.1
mask_text_input_formatter: ^2.9.0
provider: ^6.1.5+1
record: ^6.1.1
+ speech_to_text: ^7.3.0
zeta_flutter_theme: ^1.2.2
zeta_flutter_utils: ^1.1.3
zeta_icons: ^1.1.5
diff --git a/packages/zeta_flutter/test/src/components/message_input/assets/zebra.png b/packages/zeta_flutter/test/src/components/message_input/assets/zebra.png
new file mode 100644
index 000000000..fbc70e145
Binary files /dev/null and b/packages/zeta_flutter/test/src/components/message_input/assets/zebra.png differ
diff --git a/packages/zeta_flutter/test/src/components/message_input/message_input_test.dart b/packages/zeta_flutter/test/src/components/message_input/message_input_test.dart
new file mode 100644
index 000000000..95a690398
--- /dev/null
+++ b/packages/zeta_flutter/test/src/components/message_input/message_input_test.dart
@@ -0,0 +1,514 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:zeta_flutter/src/components/message_input/actions/action_button.dart';
+import 'package:zeta_flutter/src/components/message_input/actions/image_button.dart';
+import 'package:zeta_flutter/src/components/message_input/actions/send_button.dart';
+import 'package:zeta_flutter/src/components/message_input/actions/voice_button.dart';
+import 'package:zeta_flutter/src/components/message_input/panels/attachments_panel.dart';
+import 'package:zeta_flutter/zeta_flutter.dart';
+
+import '../../../test_utils/test_utils.dart';
+
+void main() {
+ group('Accessibility Tests', () {
+ testWidgets('Input has correct semantics', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(body: ZetaMessageInput(placeholder: 'Type a message', onSend: (_) {})),
+ ),
+ );
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+ final semantics = tester.getSemantics(textField);
+ // Check that the semantics label contains the placeholder
+ expect(semantics.label, contains('Type a message'));
+ });
+
+ testWidgets('Image button has correct semantics', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(body: ZetaMessageInput(cameraTrailingButton: true, onSend: (_) {})),
+ ),
+ );
+ final imageButton = find.byType(ImageButton);
+ expect(imageButton, findsOneWidget);
+ final semantics = tester.getSemantics(imageButton);
+ expect(semantics.label, contains('capture an image'));
+ });
+
+ testWidgets('Action menu button has correct semantics', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(body: ZetaMessageInput.actionMenu(onSend: (_) {})),
+ ),
+ );
+ final actionMenuButton = find.widgetWithIcon(ActionButton, ZetaIcons.add);
+ expect(actionMenuButton, findsOneWidget);
+ final semantics = tester.getSemantics(actionMenuButton);
+ expect(semantics.label, contains('open action menu'));
+ });
+
+ testWidgets('voice button has correct semantics', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ZetaMessageInput(
+ onSend: (_) {},
+ allowsVoiceInput: true,
+ ),
+ ),
+ ),
+ );
+ final voiceButton = find.byType(VoiceButton);
+ expect(voiceButton, findsOneWidget);
+ final semantics = tester.getSemantics(voiceButton);
+ expect(semantics.label, contains('voice input'));
+ });
+ });
+
+ group('Content Tests', () {
+ testWidgets('Attachments bar opens when attachments are added', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ final attachments = [File('./assets/zebra.png')];
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ZetaMessageInput(
+ controller: controller,
+ attachments: attachments,
+ onSend: (_) {},
+ ),
+ ),
+ ),
+ );
+ // AttachmentsPanel should be present
+ expect(find.byType(AttachmentsPanel), findsOneWidget);
+ });
+
+ testWidgets('Attachments bar closes when all attachments are removed', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ final attachments = [File('./assets/zebra.png')];
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: StatefulBuilder(
+ builder: (context, setState) {
+ return ZetaMessageInput(
+ controller: controller,
+ attachments: attachments,
+ onSend: (_) {},
+ onSendAttachments: (files) {
+ setState(attachments.clear);
+ },
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ // Initially, AttachmentsPanel should be present
+ expect(find.byType(AttachmentsPanel), findsOneWidget);
+
+ // Click send button
+ final sendButton = find.byType(SendButton);
+ expect(sendButton, findsOneWidget);
+ tester.widget(sendButton).onPressed?.call('', []);
+ await tester.pump();
+
+ // AttachmentsPanel should not be present
+ expect(find.byType(AttachmentsPanel), findsNothing);
+ });
+
+ testWidgets('entire component can be disabled', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ disabled: true,
+ allowsVoiceInput: true,
+ cameraTrailingButton: true,
+ hasActionMenu: true,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ // Input should be disabled
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+ expect(tester.widget(textField).enabled, isFalse);
+
+ // Action menu button should be disabled
+ final actionMenuButton = find.widgetWithIcon(ActionButton, ZetaIcons.add);
+ expect(actionMenuButton, findsOneWidget);
+ final actionMenuIconButton = find.descendant(
+ of: actionMenuButton,
+ matching: find.byType(IconButton),
+ );
+ expect(actionMenuIconButton, findsOneWidget);
+ expect(tester.widget(actionMenuIconButton).onPressed, isNull);
+
+ // Main bar camera button should be disabled
+ final imageButton = find.byType(ImageButton);
+ expect(imageButton, findsOneWidget);
+ final imageButtonIconButton = find.descendant(
+ of: imageButton,
+ matching: find.byType(IconButton),
+ );
+ expect(imageButtonIconButton, findsOneWidget);
+ expect(tester.widget(imageButtonIconButton).onPressed, isNull);
+ });
+
+ testWidgets('input accepts newline characters when maxLines > 1', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ minLines: 1,
+ maxLines: 5,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ // Enter multiline text
+ const multilineText = 'Line 1\nLine 2\nLine 3';
+ await tester.enterText(textField, multilineText);
+ await tester.pumpAndSettle();
+ expect(controller.text, multilineText);
+ });
+ testWidgets('input does not accept newline characters when maxLines = 1', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ // Enter multiline text
+ const multilineText = 'Line 1\nLine 2\nLine 3';
+ const singleText = 'Line 1Line 2Line 3';
+ await tester.enterText(textField, multilineText);
+ await tester.pumpAndSettle();
+ expect(controller.text, singleText);
+ });
+
+ testWidgets('placeholder is displayed when input is empty', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ const placeholderText = 'Type your message...';
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ placeholder: placeholderText,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ // Find the placeholder text
+ expect(find.text(placeholderText), findsOneWidget);
+ });
+
+ testWidgets('controller and input are empty with initial state', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ attachments: const [],
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ // Controller is empty
+ expect(controller.text, '');
+
+ // Find the TextField and verify it's empty
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+ expect(tester.widget(textField).controller?.text ?? '', '');
+ });
+
+ /* TODO: Implement maxLength for message input */
+ // testWidgets('input accepts up to maxLength characters', (WidgetTester tester) async {
+ // final controller = TextEditingController();
+ // await tester.pumpWidget(MaterialApp(
+ // home: Scaffold(
+ // body: ZetaMessageInput(
+ // controller: controller,
+ // maxLength: 10,
+ // ),
+ // ),
+ // ));
+
+ // final inputFinder = find.byType(TextField);
+ // expect(inputFinder, findsOneWidget);
+
+ // // Enter exactly maxLength characters
+ // await tester.enterText(inputFinder, '1234567890');
+ // await tester.pump();
+ // expect(controller.text, '1234567890');
+ // expect(controller.text.length, 10);
+ // });
+
+ // testWidgets('additional characters beyond maxLength are ignored', (WidgetTester tester) async {
+ // final controller = TextEditingController();
+ // await tester.pumpWidget(MaterialApp(
+ // home: Scaffold(
+ // body: ZetaMessageInput(
+ // controller: controller,
+ // maxLength: 10,
+ // ),
+ // ),
+ // ));
+
+ // final inputFinder = find.byType(TextField);
+ // expect(inputFinder, findsOneWidget);
+
+ // // Enter more than maxLength characters
+ // await tester.enterText(inputFinder, '123456789012345');
+ // await tester.pump();
+ // expect(controller.text, '1234567890');
+ // expect(controller.text.length, 10);
+ // });
+
+ testWidgets('send button onPressed is null with initialState', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ // Find the send button and verify it's disabled
+ final sendButton = find.byType(SendButton);
+ expect(sendButton, findsOneWidget);
+ expect(tester.widget(sendButton).onPressed, isNull);
+ });
+ });
+
+ group('Dimensions Tests', () {});
+
+ group('Styling Tests', () {
+ testWidgets('send button is colors.mainDisabled with initialState', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ final context = getBuildContext(tester, ZetaMessageInput);
+ final colors = Zeta.of(context).colors;
+
+ final sendButton = find.byType(SendButton);
+ expect(sendButton, findsOneWidget);
+
+ // Check color is mainDisabled
+ final icon = tester.widget(find.descendant(of: sendButton, matching: find.byType(Icon)));
+ expect(icon.color, colors.mainDisabled);
+ });
+ });
+
+ group('Interaction Tests', () {
+ testWidgets('clicking send with text invokes onSend callback with correct message', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ String? sentMessage;
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ onSend: (msg) => sentMessage = msg,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ await tester.enterText(textField, 'Hello world');
+ await tester.pump();
+
+ final sendButton = find.byType(SendButton);
+ await tester.tap(sendButton);
+ await tester.pump();
+
+ expect(sentMessage, 'Hello world');
+ });
+
+ testWidgets('send button becomes default color when input has valid non-whitespace text and onSend is provided',
+ (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(
+ controller: controller,
+ onSend: (value) {},
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ final context = getBuildContext(tester, ZetaMessageInput);
+ final colors = Zeta.of(context).colors;
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ await tester.enterText(textField, 'Hello');
+ await tester.pump();
+
+ final sendButton = find.byType(SendButton);
+ expect(sendButton, findsOneWidget);
+
+ // Check color is mainDefault
+ final icon = tester.widget(find.descendant(of: sendButton, matching: find.byType(Icon)));
+ expect(icon.color, colors.mainDefault);
+ });
+
+ testWidgets('send button remains as disabled color for whitespace-only input', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(controller: controller),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ final context = getBuildContext(tester, ZetaMessageInput);
+ final colors = Zeta.of(context).colors;
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ await tester.enterText(textField, ' ');
+ await tester.pump();
+
+ final sendButton = find.byType(SendButton);
+ expect(sendButton, findsOneWidget);
+
+ // Check color is mainDisabled
+ final icon = tester.widget(find.descendant(of: sendButton, matching: find.byType(Icon)));
+ expect(icon.color, colors.mainDisabled);
+ });
+ testWidgets('typing a character updates the input value', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(controller: controller),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ await tester.enterText(textField, 'A');
+ await tester.pump();
+ expect(controller.text, 'A');
+ });
+
+ testWidgets('typing multiple characters updates accordingly', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(controller: controller),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ await tester.enterText(textField, 'Hello, world!');
+ await tester.pump();
+ expect(controller.text, 'Hello, world!');
+ });
+
+ testWidgets('pasting text into the field updates the input value', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(controller: controller),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ // Simulate pasting by setting the controller text directly
+ controller.text = 'Pasted text';
+ await tester.pump();
+ expect(controller.text, 'Pasted text');
+ });
+
+ testWidgets('input gains focus on tap/click', (WidgetTester tester) async {
+ final controller = TextEditingController();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: TestApp(
+ home: ZetaMessageInput(controller: controller),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget);
+
+ // Tap the input field
+ await tester.tap(textField);
+ await tester.pumpAndSettle();
+
+ // The input should have focus
+ final textFieldWidget = tester.widget(textField);
+ expect(textFieldWidget.focusNode?.hasFocus ?? false, isTrue);
+ });
+ });
+
+ group('Performance Tests', () {});
+}
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_continuous.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_continuous.png
index f460d30ef..ca1b29eb5 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_continuous.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_continuous.png differ
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_disabled.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_disabled.png
index ed0589834..63709b763 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_disabled.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_disabled.png differ
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_stepped.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_stepped.png
index 601ee3a13..52d909e19 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_stepped.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_rounded_stepped.png differ
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_continuous.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_continuous.png
index 46728ce83..6ad1ea1df 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_continuous.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_continuous.png differ
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_disabled.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_disabled.png
index 2d3c5321b..cb9bd0aba 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_disabled.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_disabled.png differ
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_stepped.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_stepped.png
index b06ee8709..7fbc18590 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_stepped.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_selector_sharp_stepped.png differ
diff --git a/packages/zeta_flutter/test/src/components/range_selector/golden/range_slider_sharp_stepped.png b/packages/zeta_flutter/test/src/components/range_selector/golden/range_slider_sharp_stepped.png
index e89d4a047..40c223c98 100644
Binary files a/packages/zeta_flutter/test/src/components/range_selector/golden/range_slider_sharp_stepped.png and b/packages/zeta_flutter/test/src/components/range_selector/golden/range_slider_sharp_stepped.png differ
diff --git a/pubspec.yaml b/pubspec.yaml
index b8ef27241..1923527e9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -6,6 +6,7 @@ environment:
dev_dependencies:
melos: ^6.3.3
+ analyzer: any
workspace:
- packages/zeta_flutter
- packages/zeta_flutter_theme
diff --git a/widgetbook/lib/main.directories.g.dart b/widgetbook/lib/main.directories.g.dart
index 68d2b09f2..8587ff389 100644
--- a/widgetbook/lib/main.directories.g.dart
+++ b/widgetbook/lib/main.directories.g.dart
@@ -52,6 +52,8 @@ import 'package:zeta_widgetbook/src/components/in_page_banner.widgetbook.dart'
as _zeta_widgetbook_src_components_in_page_banner_widgetbook;
import 'package:zeta_widgetbook/src/components/list_item_widgetbook.dart'
as _zeta_widgetbook_src_components_list_item_widgetbook;
+import 'package:zeta_widgetbook/src/components/message_input.widgetbook.dart'
+ as _zeta_widgetbook_src_components_message_input_widgetbook;
import 'package:zeta_widgetbook/src/components/navigation_bar.widgetbook.dart'
as _zeta_widgetbook_src_components_navigation_bar_widgetbook;
import 'package:zeta_widgetbook/src/components/navigation_rail.widgetbook.dart'
@@ -635,6 +637,20 @@ final directories = <_widgetbook.WidgetbookNode>[
),
],
),
+ _widgetbook.WidgetbookFolder(
+ name: 'MessageInput',
+ children: [
+ _widgetbook.WidgetbookLeafComponent(
+ name: 'MessageInput',
+ useCase: _widgetbook.WidgetbookUseCase(
+ name: 'Message Input',
+ builder: _zeta_widgetbook_src_components_message_input_widgetbook.defaultMessageInput,
+ designLink:
+ 'https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=18271-19682',
+ ),
+ )
+ ],
+ ),
_widgetbook.WidgetbookFolder(
name: 'Navigation Bar',
children: [
diff --git a/widgetbook/lib/src/components/message_input.widgetbook.dart b/widgetbook/lib/src/components/message_input.widgetbook.dart
new file mode 100644
index 000000000..6e2fca45c
--- /dev/null
+++ b/widgetbook/lib/src/components/message_input.widgetbook.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+import 'package:widgetbook/widgetbook.dart';
+import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
+import 'package:zeta_flutter/zeta_flutter.dart';
+import 'package:zeta_widgetbook/main.dart';
+import 'package:zeta_widgetbook/src/utils/utils.dart';
+
+@widgetbook.UseCase(
+ name: 'Message Input',
+ type: ZetaMessageInput,
+ path: '$componentsPath/ZetaMessageInput',
+ designLink: 'https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=18271-19682',
+)
+Widget defaultMessageInput(BuildContext context) {
+ final controller = TextEditingController();
+
+ final String? placeholder = context.knobs.stringOrNull(label: 'Placeholder');
+ final bool allowsVoiceInput = context.knobs.boolean(label: 'Allows Voice Input', initialValue: true);
+ final range = context.knobs.range(label: 'Range of lines', initialValue: const RangeValues(1, 10));
+ final minLines = range.start.round();
+ final maxLines = range.end.round();
+ final bool disabled = context.knobs.boolean(label: 'Disabled', initialValue: false);
+ final bool hasActionMenu = context.knobs.boolean(label: 'Has Action Menu', initialValue: true);
+ final bool cameraTrailingButton = context.knobs.boolean(label: 'Camera Trailing Button', initialValue: false);
+
+ return StatefulBuilder(
+ builder: (context, setState) => ZetaMessageInput.actionMenu(
+ controller: controller,
+ placeholder: placeholder,
+ allowsVoiceInput: allowsVoiceInput,
+ minLines: minLines,
+ maxLines: maxLines,
+ disabled: disabled,
+ hasActionMenu: hasActionMenu,
+ cameraTrailingButton: cameraTrailingButton,
+ attachments: [],
+ onSend: (message) {},
+ onSendAttachments: (value) {},
+ ));
+}
diff --git a/widgetbook/linux/flutter/generated_plugin_registrant.cc b/widgetbook/linux/flutter/generated_plugin_registrant.cc
index 6867a3e36..7b54566d7 100644
--- a/widgetbook/linux/flutter/generated_plugin_registrant.cc
+++ b/widgetbook/linux/flutter/generated_plugin_registrant.cc
@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
#include
@@ -14,6 +15,9 @@ 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);
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/widgetbook/linux/flutter/generated_plugins.cmake b/widgetbook/linux/flutter/generated_plugins.cmake
index 81097248a..daf934bf2 100644
--- a/widgetbook/linux/flutter/generated_plugins.cmake
+++ b/widgetbook/linux/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
+ file_selector_linux
record_linux
url_launcher_linux
)
diff --git a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift
index 50dda46b8..3d835ba2f 100644
--- a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -6,15 +6,23 @@ import FlutterMacOS
import Foundation
import audioplayers_darwin
+import file_picker
+import file_selector_macos
+import location
import path_provider_foundation
import record_macos
import shared_preferences_foundation
+import speech_to_text
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+ FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
+ LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+ SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
diff --git a/widgetbook/macos/Podfile b/widgetbook/macos/Podfile
index b52666a10..dbccf89c9 100644
--- a/widgetbook/macos/Podfile
+++ b/widgetbook/macos/Podfile
@@ -1,4 +1,4 @@
-platform :osx, '10.15'
+platform :osx, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/widgetbook/macos/Podfile.lock b/widgetbook/macos/Podfile.lock
deleted file mode 100644
index 15f5ba121..000000000
--- a/widgetbook/macos/Podfile.lock
+++ /dev/null
@@ -1,49 +0,0 @@
-PODS:
- - audioplayers_darwin (0.0.1):
- - Flutter
- - FlutterMacOS
- - FlutterMacOS (1.0.0)
- - path_provider_foundation (0.0.1):
- - Flutter
- - FlutterMacOS
- - record_macos (1.1.0):
- - FlutterMacOS
- - shared_preferences_foundation (0.0.1):
- - Flutter
- - FlutterMacOS
- - url_launcher_macos (0.0.1):
- - FlutterMacOS
-
-DEPENDENCIES:
- - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`)
- - FlutterMacOS (from `Flutter/ephemeral`)
- - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
- - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
-
-EXTERNAL SOURCES:
- audioplayers_darwin:
- :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin
- FlutterMacOS:
- :path: Flutter/ephemeral
- path_provider_foundation:
- :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
- record_macos:
- :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
- shared_preferences_foundation:
- :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
- url_launcher_macos:
- :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
-
-SPEC CHECKSUMS:
- audioplayers_darwin: 4027b33a8f471d996c13f71cb77f0b1583b5d923
- FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
- path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
- record_macos: 4440ca269ad3b870ebb1965297a365d558f0c520
- shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
- url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
-
-PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
-
-COCOAPODS: 1.16.2
diff --git a/widgetbook/macos/Runner.xcodeproj/project.pbxproj b/widgetbook/macos/Runner.xcodeproj/project.pbxproj
index 49ad312e1..adc940971 100644
--- a/widgetbook/macos/Runner.xcodeproj/project.pbxproj
+++ b/widgetbook/macos/Runner.xcodeproj/project.pbxproj
@@ -557,7 +557,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -639,7 +639,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -689,7 +689,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
diff --git a/widgetbook/windows/flutter/generated_plugin_registrant.cc b/widgetbook/windows/flutter/generated_plugin_registrant.cc
index 67b645c2c..e7776798e 100644
--- a/widgetbook/windows/flutter/generated_plugin_registrant.cc
+++ b/widgetbook/windows/flutter/generated_plugin_registrant.cc
@@ -7,14 +7,20 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
+ SpeechToTextWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("SpeechToTextWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/widgetbook/windows/flutter/generated_plugins.cmake b/widgetbook/windows/flutter/generated_plugins.cmake
index f071d09be..357973d31 100644
--- a/widgetbook/windows/flutter/generated_plugins.cmake
+++ b/widgetbook/windows/flutter/generated_plugins.cmake
@@ -4,7 +4,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
+ file_selector_windows
record_windows
+ speech_to_text_windows
url_launcher_windows
)