diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fd35d..d54f72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.2.1 (2025-04-09) + +* feat: Adding more components for Agents. + ## 1.2.0 (2025-03-26) * Bump version for livekit sdk. diff --git a/example/lib/main.dart b/example/lib/main.dart index 2b7af20..46e4108 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:livekit_components/livekit_components.dart'; import 'package:logging/logging.dart'; import 'package:intl/intl.dart'; import 'package:responsive_builder/responsive_builder.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { final format = DateFormat('HH:mm:ss'); @@ -33,18 +34,53 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); +class MyHomePage extends StatefulWidget { + // + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} - final url = 'wss://livekit.example.com'; - final token = 'your_token_here'; +class _MyHomePageState extends State { + static const _storeKeyUri = 'uri'; + static const _storeKeyToken = 'token'; + + String _url = ''; + String _token = ''; + + void _readPrefs() async { + final prefs = await SharedPreferences.getInstance(); + _url = const bool.hasEnvironment('URL') + ? const String.fromEnvironment('URL') + : prefs.getString(_storeKeyUri) ?? 'your url here'; + _token = const bool.hasEnvironment('TOKEN') + ? const String.fromEnvironment('TOKEN') + : prefs.getString(_storeKeyToken) ?? 'your token here'; + } + + // Save URL and Token + Future _writePrefs(String url, String token) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_storeKeyUri, url); + await prefs.setString(_storeKeyToken, token); + } + + @override + void initState() { + super.initState(); + _readPrefs(); + } /// handle join button pressed, fetch connection details and connect to room. // ignore: unused_element - void _onJoinPressed(RoomContext roomCtx, String name, String roomName) async { + void _onJoinPressed(RoomContext roomCtx, String url, String token) async { if (kDebugMode) { - print('Joining room: name=$name, roomName=$roomName'); + print('Joining room: url=$url, token=$token'); } + await _writePrefs(url, token); try { await roomCtx.connect( url: url, @@ -61,9 +97,22 @@ class MyHomePage extends StatelessWidget { Widget build(BuildContext context) { return LivekitRoom( roomContext: RoomContext( - url: url, - token: token, enableAudioVisulizer: true, + onConnected: () { + if (kDebugMode) { + print('Connected to room'); + } + }, + onDisconnected: () { + if (kDebugMode) { + print('Disconnected from room'); + } + }, + onError: (error) { + if (kDebugMode) { + print('Error: $error'); + } + }, ), builder: (context, roomCtx) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); @@ -82,8 +131,8 @@ class MyHomePage extends StatelessWidget { /// show prejoin screen if not connected ? Prejoin( - token: token, - url: url, + token: _token, + url: _url, onJoinPressed: _onJoinPressed, ) : @@ -110,9 +159,19 @@ class MyHomePage extends StatelessWidget { ), ) : Expanded( - flex: 5, + flex: 6, child: Stack( children: [ + /* Expanded( + child: TranscriptionBuilder( + builder: + (context, roomCtx, transcriptions) { + return TranscriptionWidget( + transcriptions: transcriptions, + ); + }, + ), + ),*/ /// show participant loop ParticipantLoop( showAudioTracks: true, @@ -137,7 +196,10 @@ class MyHomePage extends StatelessWidget { identifier.isAudio && roomCtx .enableAudioVisulizer - ? const AudioVisualizerWidget() + ? const AudioVisualizerWidget( + backgroundColor: + LKColors.lkDarkBlue, + ) : IsSpeakingIndicator( builder: (context, isSpeaking) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index dfdae5b..066b9e9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,7 +2,7 @@ name: example description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -30,7 +30,7 @@ environment: dependencies: flutter: sdk: flutter - + livekit_components: path: .. @@ -40,9 +40,10 @@ dependencies: intl: ^0.19.0 logging: ^1.2.0 provider: ^6.1.2 - fluttertoast: ^8.2.8 + fluttertoast: ^8.2.12 responsive_builder: ^0.7.1 http: ^1.2.2 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: @@ -60,7 +61,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/lib/livekit_components.dart b/lib/livekit_components.dart index c69ba57..5d7e64a 100644 --- a/lib/livekit_components.dart +++ b/lib/livekit_components.dart @@ -21,6 +21,9 @@ export 'src/context/track_reference_context.dart'; export 'src/debug/logger.dart'; +export 'src/types/agent_state.dart'; +export 'src/types/transcription.dart'; + export 'src/ui/builder/camera_preview.dart'; export 'src/ui/builder/room/chat_toggle.dart'; export 'src/ui/builder/room/chat.dart'; @@ -35,6 +38,7 @@ export 'src/ui/builder/room/room_participants.dart'; export 'src/ui/builder/room/room.dart'; export 'src/ui/builder/room/screenshare_toggle.dart'; export 'src/ui/builder/room/room_active_recording_indicator.dart'; +export 'src/ui/builder/room/transcription.dart'; export 'src/ui/builder/participant/participant_track.dart'; export 'src/ui/builder/participant/participant_attributes.dart'; @@ -45,6 +49,7 @@ export 'src/ui/builder/participant/participant_name.dart'; export 'src/ui/builder/participant/participant_kind.dart'; export 'src/ui/builder/participant/participant_permissions.dart'; export 'src/ui/builder/participant/participant_transcription.dart'; +export 'src/ui/builder/participant/participant_selector.dart'; export 'src/ui/builder/track/connection_quality_indicator.dart'; export 'src/ui/builder/track/e2e_encryption_indicator.dart'; @@ -64,6 +69,7 @@ export 'src/ui/widgets/room/control_bar.dart'; export 'src/ui/widgets/room/disconnect_button.dart'; export 'src/ui/widgets/room/screenshare_toggle.dart'; export 'src/ui/widgets/room/clear_pin_button.dart'; +export 'src/ui/widgets/participant/transcription_widget.dart'; export 'src/ui/widgets/track/audio_visualizer_widget.dart'; export 'src/ui/widgets/track/focus_toggle.dart'; diff --git a/lib/src/context/media_device_context.dart b/lib/src/context/media_device_context.dart index 0bb4806..e8800ba 100644 --- a/lib/src/context/media_device_context.dart +++ b/lib/src/context/media_device_context.dart @@ -77,13 +77,13 @@ class MediaDeviceContext extends ChangeNotifier { selectedAudioInputDeviceId ??= Hardware.instance.selectedAudioInput?.deviceId ?? - _audioInputs?.first.deviceId; + _audioInputs?.firstOrNull?.deviceId; selectedVideoInputDeviceId ??= Hardware.instance.selectedVideoInput?.deviceId ?? - _videoInputs?.first.deviceId; + _videoInputs?.firstOrNull?.deviceId; selectedAudioOutputDeviceId ??= Hardware.instance.selectedAudioOutput?.deviceId ?? - _audioOutputs?.first.deviceId; + _audioOutputs?.firstOrNull?.deviceId; notifyListeners(); } diff --git a/lib/src/context/room_context.dart b/lib/src/context/room_context.dart index a80bf5f..ea64147 100644 --- a/lib/src/context/room_context.dart +++ b/lib/src/context/room_context.dart @@ -19,10 +19,12 @@ import 'package:flutter/material.dart' hide ConnectionState; import 'package:livekit_client/livekit_client.dart'; import 'package:provider/provider.dart'; +import 'package:livekit_components/src/context/transcription_context.dart'; import '../debug/logger.dart'; import 'chat_context.dart'; -class RoomContext extends ChangeNotifier with ChatContextMixin { +class RoomContext extends ChangeNotifier + with ChatContextMixin, TranscriptionContextMixin { /// Get the [RoomContext] from the [context]. /// this method must be called under the [LivekitRoom] widget. static RoomContext? of(BuildContext context) { @@ -62,6 +64,7 @@ class RoomContext extends ChangeNotifier with ChatContextMixin { ..on((event) { Debug.event('RoomContext: RoomConnectedEvent $roomName'); chatContextSetup(_listener, _room.localParticipant!); + transcriptionContextSetup(_listener); _connectionState = _room.connectionState; _connected = true; _connecting = false; @@ -76,6 +79,7 @@ class RoomContext extends ChangeNotifier with ChatContextMixin { Debug.event('RoomContext: RoomDisconnectedEvent $roomName'); _connectionState = _room.connectionState; chatContextSetup(null, null); + transcriptionContextSetup(null); _connected = false; _participants.clear(); onDisconnected?.call(); @@ -160,8 +164,8 @@ class RoomContext extends ChangeNotifier with ChatContextMixin { }) async { if (cameraOpened || microphoneOpened) { _fastConnectOptions = FastConnectOptions( - microphone: TrackOption(track: localAudioTrack!), - camera: TrackOption(track: localVideoTrack!), + microphone: TrackOption(track: localAudioTrack), + camera: TrackOption(track: localVideoTrack), ); await resetLocalTracks(); } diff --git a/lib/src/context/transcription_context.dart b/lib/src/context/transcription_context.dart new file mode 100644 index 0000000..4d6b237 --- /dev/null +++ b/lib/src/context/transcription_context.dart @@ -0,0 +1,50 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:livekit_client/livekit_client.dart'; + +import '../debug/logger.dart'; +import '../types/transcription.dart'; + +mixin TranscriptionContextMixin on ChangeNotifier { + final List _transcriptions = []; + List get transcriptions => _transcriptions; + EventsListener? _listener; + + void transcriptionContextSetup(EventsListener? listener) { + _listener = listener; + if (listener != null) { + _listener!.on((event) { + Debug.event('TranscriptionContext: TranscriptionEvent'); + for (var segment in event.segments) { + var transcription = _transcriptions + .firstWhereOrNull((t) => t.segment.id == segment.id); + if (transcription != null) { + transcription.segment = segment; + } else { + _transcriptions + .add(TranscriptionForParticipant(segment, event.participant)); + } + } + notifyListeners(); + }); + } else { + _listener = null; + _transcriptions.clear(); + } + } +} diff --git a/lib/src/types/agent_state.dart b/lib/src/types/agent_state.dart new file mode 100644 index 0000000..ed3f857 --- /dev/null +++ b/lib/src/types/agent_state.dart @@ -0,0 +1,15 @@ +/// Possible states for the agent participant +/// These states are set by the agent and sent via LiveKit metadata +enum AgentState { + initializing, // Agent is starting up + speaking, // Agent is speaking to the user + thinking, // Agent is processing user input + listening; // Agent is listening to user audio + + static AgentState fromString(String value) { + return AgentState.values.firstWhere( + (state) => state.name == value, + orElse: () => AgentState.initializing, + ); + } +} diff --git a/lib/src/types/track_identifier.dart b/lib/src/types/track_identifier.dart index 7ec8988..3ff4103 100644 --- a/lib/src/types/track_identifier.dart +++ b/lib/src/types/track_identifier.dart @@ -26,8 +26,17 @@ class TrackIdentifier { bool get isAudio => source == TrackSource.microphone || source == TrackSource.screenShareAudio; + bool get isVideo => source == TrackSource.camera || source == TrackSource.camera; + bool get isLocal => participant is LocalParticipant; + bool get hasTrack => track != null; + + bool get isAgent => kind == ParticipantKind.AGENT; + + ParticipantKind get kind => participant.kind; + + String get name => participant.name; } diff --git a/lib/src/types/transcription.dart b/lib/src/types/transcription.dart new file mode 100644 index 0000000..74e8be8 --- /dev/null +++ b/lib/src/types/transcription.dart @@ -0,0 +1,11 @@ +import 'package:livekit_client/livekit_client.dart' + show Participant, TranscriptionSegment; + +class TranscriptionForParticipant { + TranscriptionForParticipant( + this.segment, + this.participant, + ); + TranscriptionSegment segment; + final Participant participant; +} diff --git a/lib/src/ui/builder/participant/participant_selector.dart b/lib/src/ui/builder/participant/participant_selector.dart new file mode 100644 index 0000000..4cd59f4 --- /dev/null +++ b/lib/src/ui/builder/participant/participant_selector.dart @@ -0,0 +1,81 @@ +import 'package:flutter/widgets.dart'; + +import 'package:collection/collection.dart'; +import 'package:livekit_client/livekit_client.dart'; +import 'package:provider/provider.dart'; + +import 'package:livekit_components/src/types/track_identifier.dart'; +import '../../../context/room_context.dart'; +import '../../../debug/logger.dart'; +import 'participant_track.dart'; + +class ParticipantSelector extends StatelessWidget { + final bool Function(TrackIdentifier identifier) filter; + final Widget Function(BuildContext context, TrackIdentifier identifier) + builder; + const ParticipantSelector({ + required this.filter, + required this.builder, + super.key, + }); + + List> buildTracksMap( + bool audio, bool video, List participants) { + final List> trackMap = []; + int index = 0; + for (Participant participant in participants) { + Debug.log('=> participant ${participant.identity}, index: $index'); + index++; + var tracks = participant.trackPublications.values; + for (var track in tracks) { + if (track.kind == TrackType.AUDIO && !audio) { + continue; + } + if (track.kind == TrackType.VIDEO && !video) { + continue; + } + trackMap.add(MapEntry(TrackIdentifier(participant, track), track)); + + Debug.log( + '=> ${track.source.toString()} track ${track.sid} for ${participant.identity}'); + } + + if (!audio && !tracks.any((t) => t.kind == TrackType.VIDEO) || + !video && tracks.any((t) => t.kind == TrackType.AUDIO) || + tracks.isEmpty) { + trackMap.add(MapEntry(TrackIdentifier(participant), null)); + Debug.log('=> no tracks for ${participant.identity}'); + } + } + + return trackMap; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, roomCtx, child) { + Debug.log('> ParticipantSelector for ${roomCtx.roomName}'); + return Selector>( + selector: (context, participants) => roomCtx.participants, + shouldRebuild: (previous, next) => previous.length != next.length, + builder: (context, participants, child) { + var trackMap = buildTracksMap(true, true, participants); + var identifier = + trackMap.firstWhereOrNull((entry) => filter(entry.key))?.key; + + if (identifier == null) { + return const SizedBox(); + } + + return ParticipantTrack( + participant: identifier.participant, + track: identifier.track, + builder: (context) => builder(context, identifier), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/ui/builder/room/transcription.dart b/lib/src/ui/builder/room/transcription.dart new file mode 100644 index 0000000..7f20aa7 --- /dev/null +++ b/lib/src/ui/builder/room/transcription.dart @@ -0,0 +1,41 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../../../context/room_context.dart'; +import '../../../types/transcription.dart'; + +class TranscriptionBuilder extends StatelessWidget { + const TranscriptionBuilder({ + super.key, + required this.builder, + }); + + final Function(BuildContext context, RoomContext roomCtx, + List transcriptions) builder; + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, roomCtx, child) { + return builder( + context, + roomCtx, + roomCtx.transcriptions, + ); + }); + } +} diff --git a/lib/src/ui/prejoin/prejoin.dart b/lib/src/ui/prejoin/prejoin.dart index c60eaf0..daa2109 100644 --- a/lib/src/ui/prejoin/prejoin.dart +++ b/lib/src/ui/prejoin/prejoin.dart @@ -37,11 +37,11 @@ class Prejoin extends StatelessWidget { String url; void onTextTokenChanged(String token) async { - token = token; + this.token = token; } void onTextUrlChanged(String url) async { - url = url; + this.url = url; } void _handleJoinPressed(RoomContext roomCtx) async { @@ -89,11 +89,9 @@ class Prejoin extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - MicrophoneSelectButton( - showLabels: true, - ), + MicrophoneSelectButton(), CameraSelectButton( - showLabels: true, + showTitleWidget: true, ), ], )), diff --git a/lib/src/ui/widgets/camera_preview.dart b/lib/src/ui/widgets/camera_preview.dart index ae8943b..f528fd3 100644 --- a/lib/src/ui/widgets/camera_preview.dart +++ b/lib/src/ui/widgets/camera_preview.dart @@ -24,8 +24,10 @@ class CameraPreviewWidget extends StatelessWidget { const CameraPreviewWidget({ super.key, required this.track, + this.iconColor = LKColors.lkBlue, }); final LocalVideoTrack? track; + final Color iconColor; @override Widget build(BuildContext context) { return Container( @@ -39,7 +41,7 @@ class CameraPreviewWidget extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) => Icon( Icons.videocam_off_outlined, - color: LKColors.lkBlue, + color: iconColor, size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.33, diff --git a/lib/src/ui/widgets/participant/is_speaking_indicator.dart b/lib/src/ui/widgets/participant/is_speaking_indicator.dart index 00d7eb9..08769d9 100644 --- a/lib/src/ui/widgets/participant/is_speaking_indicator.dart +++ b/lib/src/ui/widgets/participant/is_speaking_indicator.dart @@ -21,10 +21,12 @@ class IsSpeakingIndicatorWidget extends StatelessWidget { Key? key, required this.isSpeaking, required this.child, + this.borderColor = LKColors.lkBlue, }) : super(key: key); final bool isSpeaking; final Widget child; + final Color borderColor; @override Widget build(BuildContext context) { @@ -33,7 +35,7 @@ class IsSpeakingIndicatorWidget extends StatelessWidget { border: isSpeaking ? Border.all( width: 3, - color: LKColors.lkBlue, + color: borderColor, ) : null, ), diff --git a/lib/src/ui/widgets/participant/participant_status_bar.dart b/lib/src/ui/widgets/participant/participant_status_bar.dart index 98bfda8..bd1afa6 100644 --- a/lib/src/ui/widgets/participant/participant_status_bar.dart +++ b/lib/src/ui/widgets/participant/participant_status_bar.dart @@ -49,7 +49,7 @@ class ParticipantStatusBar extends StatelessWidget { var isScreenShare = trackCtx?.isScreenShare ?? false; return Container( padding: const EdgeInsets.symmetric(horizontal: 6), - color: Colors.black.withOpacity(0.6), + color: Colors.black.withValues(alpha: 0.6), child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/src/ui/widgets/participant/transcription_widget.dart b/lib/src/ui/widgets/participant/transcription_widget.dart new file mode 100644 index 0000000..0beb1cf --- /dev/null +++ b/lib/src/ui/widgets/participant/transcription_widget.dart @@ -0,0 +1,82 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:chat_bubbles/chat_bubbles.dart'; +import 'package:livekit_client/livekit_client.dart'; + +import '../../../types/transcription.dart'; +import '../theme.dart'; + +class TranscriptionWidget extends StatelessWidget { + TranscriptionWidget({ + super.key, + required this.transcriptions, + this.backgroundColor = LKColors.lkDarkBlue, + this.textColor = Colors.white, + }); + + final Color backgroundColor; + final Color textColor; + final List transcriptions; + final ScrollController _scrollController = ScrollController(); + + List _buildMessages( + List transcriptions) { + List msgWidgets = []; + var sortedTranscriptions = transcriptions + ..sort((a, b) => + a.segment.firstReceivedTime.compareTo(b.segment.firstReceivedTime)); + for (var transcription in sortedTranscriptions) { + var participant = transcription.participant; + var segment = transcription.segment; + var isLocal = participant is LocalParticipant; + msgWidgets.add( + BubbleNormal( + text: segment.text + (segment.isFinal ? '' : '...'), + color: textColor, + tail: isLocal, + isSender: isLocal, + ), + ); + } + return msgWidgets; + } + + void scrollToBottom() { + _scrollController.animateTo(_scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + + @override + Widget build(BuildContext context) { + return Container( + color: backgroundColor, + padding: const EdgeInsets.all(1.0), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: _buildMessages(transcriptions), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/ui/widgets/room/camera_select_button.dart b/lib/src/ui/widgets/room/camera_select_button.dart index 6446b5d..8916b81 100644 --- a/lib/src/ui/widgets/room/camera_select_button.dart +++ b/lib/src/ui/widgets/room/camera_select_button.dart @@ -14,22 +14,44 @@ import 'package:flutter/material.dart'; +import 'package:livekit_components/src/ui/widgets/theme.dart'; import '../../builder/room/media_device_select_button.dart'; import 'media_device_select_button.dart'; class CameraSelectButton extends StatelessWidget { const CameraSelectButton({ super.key, - this.showLabels = false, + this.showTitleWidget = true, + this.color = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.iconColor = Colors.white, + this.foregroundColor = Colors.white, + this.selectedOverlayColor = LKColors.lkLightBlue, + this.titleWidget, }); - final bool showLabels; + final bool showTitleWidget; + final Color color; + final Color selectedColor; + final Color iconColor; + final Color foregroundColor; + final Color selectedOverlayColor; + final Widget? titleWidget; @override Widget build(BuildContext context) { return MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Camera', + titleWidget: titleWidget ?? + Text( + 'Camera', + style: TextStyle(fontSize: 14, color: iconColor), + ), + backgroundColor: color, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + iconColor: iconColor, + foregroundColor: foregroundColor, iconOn: Icons.videocam, iconOff: Icons.videocam_off, deviceList: deviceCtx.videoInputs ?? [], @@ -38,7 +60,7 @@ class CameraSelectButton extends StatelessWidget { onSelect: (device) => deviceCtx.selectVideoInput(device), onToggle: (enabled) => enabled ? deviceCtx.enableCamera() : deviceCtx.disableCamera(), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ); } diff --git a/lib/src/ui/widgets/room/camera_switch_button.dart b/lib/src/ui/widgets/room/camera_switch_button.dart index b86528f..139433a 100644 --- a/lib/src/ui/widgets/room/camera_switch_button.dart +++ b/lib/src/ui/widgets/room/camera_switch_button.dart @@ -22,19 +22,26 @@ class CameraSwitchButton extends StatelessWidget { this.currentPosition = CameraPosition.front, this.onToggle, this.disabled = false, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, }); final CameraPosition? currentPosition; final Function(CameraPosition position)? onToggle; final bool disabled; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; @override Widget build(BuildContext context) { return ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), + backgroundColor: + WidgetStateProperty.all(backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all(overlayColor), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20.0)))), padding: WidgetStateProperty.all( diff --git a/lib/src/ui/widgets/room/chat_toggle.dart b/lib/src/ui/widgets/room/chat_toggle.dart index c0eb3c5..bc9c26d 100644 --- a/lib/src/ui/widgets/room/chat_toggle.dart +++ b/lib/src/ui/widgets/room/chat_toggle.dart @@ -24,22 +24,33 @@ class ChatToggleWidget extends StatelessWidget { required this.isChatOpen, required this.toggleChat, this.showLabel = true, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.selectedOverlayColor = LKColors.lkLightBlue, }); final bool isChatOpen; final Function(bool enabled) toggleChat; final bool showLabel; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color selectedColor; + final Color selectedOverlayColor; @override Widget build(BuildContext context) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); return ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - isChatOpen ? LKColors.lkBlue : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.all(isChatOpen + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( - isChatOpen ? LKColors.lkLightBlue : Colors.grey), + isChatOpen ? selectedOverlayColor : backgroundColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -48,9 +59,9 @@ class ChatToggleWidget extends StatelessWidget { ), ), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => toggleChat(!isChatOpen), diff --git a/lib/src/ui/widgets/room/control_bar.dart b/lib/src/ui/widgets/room/control_bar.dart index 22934c9..06f0e60 100644 --- a/lib/src/ui/widgets/room/control_bar.dart +++ b/lib/src/ui/widgets/room/control_bar.dart @@ -23,6 +23,7 @@ import '../../builder/room/disconnect_button.dart'; import '../../builder/room/media_device_select_button.dart'; import '../../builder/room/screenshare_toggle.dart'; import '../../builder/room/speaker_switch.dart'; +import '../theme.dart'; import 'camera_switch_button.dart'; import 'chat_toggle.dart'; import 'disconnect_button.dart'; @@ -39,7 +40,12 @@ class ControlBar extends StatelessWidget { this.screenShare = true, this.leave = true, this.settings = true, - this.showLabels = false, + this.showTitleWidget = false, + this.backgroundColor = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.selectedOverlayColor = LKColors.lkLightBlue, + this.foregroundColor = Colors.white, + this.iconColor = Colors.white, }); final bool microphone; @@ -49,7 +55,13 @@ class ControlBar extends StatelessWidget { final bool screenShare; final bool leave; final bool settings; - final bool showLabels; + final bool showTitleWidget; + + final Color backgroundColor; + final Color selectedColor; + final Color selectedOverlayColor; + final Color foregroundColor; + final Color iconColor; bool get isMobile => lkPlatformIsMobile(); @@ -67,7 +79,15 @@ class ControlBar extends StatelessWidget { if (microphone) MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Microphone', + titleWidget: Text( + 'Microphone', + style: TextStyle(fontSize: 14), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + foregroundColor: foregroundColor, + iconColor: iconColor, iconOn: Icons.mic, iconOff: Icons.mic_off, deviceList: deviceCtx.audioInputs ?? [], @@ -77,14 +97,22 @@ class ControlBar extends StatelessWidget { onToggle: (enabled) => enabled ? deviceCtx.enableMicrophone() : deviceCtx.disableMicrophone(), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ), if (audioOutput && (lkPlatformIsDesktop() || lkPlatformIs(PlatformType.web))) MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Audio Output', + titleWidget: Text( + 'Audio Output', + style: TextStyle(fontSize: 14), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + foregroundColor: foregroundColor, + iconColor: iconColor, iconOn: Icons.volume_up, iconOff: Icons.volume_off, deviceList: deviceCtx.audioOutputs ?? [], @@ -93,13 +121,21 @@ class ControlBar extends StatelessWidget { toggleAvailable: false, defaultSelectable: true, onSelect: (device) => deviceCtx.selectAudioOutput(device), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ), if (camera) MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Camera', + titleWidget: Text( + 'Camera', + style: TextStyle(fontSize: 14), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + foregroundColor: foregroundColor, + iconColor: iconColor, iconOn: Icons.videocam, iconOff: Icons.videocam_off, deviceList: deviceCtx.videoInputs ?? [], @@ -109,7 +145,7 @@ class ControlBar extends StatelessWidget { onToggle: (enabled) => enabled ? deviceCtx.enableCamera() : deviceCtx.disableCamera(), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ), if (isMobile && microphone) @@ -135,7 +171,7 @@ class ControlBar extends StatelessWidget { roomCtx: roomCtx, deviceCtx: deviceCtx, screenShareEnabled: screenShareEnabled, - showLabel: showLabels, + showLabel: showTitleWidget, ), ), if (chat) @@ -143,7 +179,7 @@ class ControlBar extends StatelessWidget { builder: (context, roomCtx, isChatEnabled) => ChatToggleWidget( isChatOpen: roomCtx.isChatEnabled, toggleChat: (enabled) => roomCtx.toggleChat(enabled), - showLabel: showLabels, + showLabel: showTitleWidget, ), ), if (leave) @@ -151,7 +187,7 @@ class ControlBar extends StatelessWidget { builder: (context, roomCtx, connected) => DisconnectButtonWidget( roomCtx: roomCtx, connected: connected, - showLabel: showLabels, + showLabel: showTitleWidget, ), ), ], diff --git a/lib/src/ui/widgets/room/disconnect_button.dart b/lib/src/ui/widgets/room/disconnect_button.dart index a178d6d..6d9c524 100644 --- a/lib/src/ui/widgets/room/disconnect_button.dart +++ b/lib/src/ui/widgets/room/disconnect_button.dart @@ -27,6 +27,11 @@ class DisconnectButtonWidget extends StatelessWidget { this.onPressed, this.title = 'Leave', this.showLabel = true, + this.backgroundColor = Colors.grey, + this.selectedColor = Colors.red, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, + this.selectedOverlayColor = Colors.redAccent, }); final bool connected; @@ -34,6 +39,12 @@ class DisconnectButtonWidget extends StatelessWidget { final String title; final bool showLabel; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color selectedColor; + final Color selectedOverlayColor; + final Function()? onPressed; @override @@ -42,10 +53,10 @@ class DisconnectButtonWidget extends StatelessWidget { return ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all( - connected ? Colors.red : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: - WidgetStateProperty.all(connected ? Colors.redAccent : Colors.grey), + connected ? selectedColor : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + connected ? selectedOverlayColor : overlayColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -54,9 +65,9 @@ class DisconnectButtonWidget extends StatelessWidget { ), ), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => diff --git a/lib/src/ui/widgets/room/join_button.dart b/lib/src/ui/widgets/room/join_button.dart index 17706a3..2f1baf0 100644 --- a/lib/src/ui/widgets/room/join_button.dart +++ b/lib/src/ui/widgets/room/join_button.dart @@ -23,12 +23,23 @@ class JoinButtonWidget extends StatelessWidget { required this.roomCtx, required this.connected, this.onPressed, + this.backgroundColor = LKColors.lkBlue, + this.overlayColor = Colors.grey, + this.foregroundColor = Colors.white, + this.disabledColor = Colors.grey, + this.disabledOverlayColor = LKColors.lkLightBlue, }) : super(key: key); RoomContext roomCtx; bool connected; final Function()? onPressed; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color disabledColor; + final Color disabledOverlayColor; + @override Widget build(BuildContext context) { return ElevatedButton( @@ -38,9 +49,11 @@ class JoinButtonWidget extends StatelessWidget { ? roomCtx.connect() : null, style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(connected ? Colors.grey : LKColors.lkBlue), - foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.all( + connected ? disabledColor : backgroundColor), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + connected ? overlayColor : disabledOverlayColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -50,9 +63,9 @@ class JoinButtonWidget extends StatelessWidget { ), padding: WidgetStateProperty.all(const EdgeInsets.all(12)), ), - child: const Text( + child: Text( 'Join Room', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: foregroundColor, fontSize: 16), ), ); } diff --git a/lib/src/ui/widgets/room/media_device_select_button.dart b/lib/src/ui/widgets/room/media_device_select_button.dart index e69a20f..ca6baaa 100644 --- a/lib/src/ui/widgets/room/media_device_select_button.dart +++ b/lib/src/ui/widgets/room/media_device_select_button.dart @@ -17,29 +17,37 @@ import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; import 'package:responsive_builder/responsive_builder.dart'; -import '../theme.dart'; - class MediaDeviceSelectWidget extends StatelessWidget { const MediaDeviceSelectWidget({ super.key, required this.iconOn, required this.iconOff, required this.deviceList, - this.title, + required this.backgroundColor, + required this.foregroundColor, + required this.selectedColor, + required this.selectedOverlayColor, + required this.iconColor, + required this.titleWidget, this.onToggle, this.onSelect, required this.deviceIsOpened, this.selectedDeviceId, - this.showLabel = false, + this.showTitleWidget = false, this.defaultSelectable = false, this.toggleAvailable = true, }); final bool deviceIsOpened; - final String? title; + final Widget? titleWidget; final IconData iconOn; final IconData iconOff; - final bool showLabel; + final bool showTitleWidget; + final Color backgroundColor; + final Color iconColor; + final Color selectedColor; + final Color selectedOverlayColor; + final Color foregroundColor; final String? selectedDeviceId; final List deviceList; final Function(MediaDevice device)? onSelect; @@ -53,82 +61,88 @@ class MediaDeviceSelectWidget extends StatelessWidget { return Row(mainAxisSize: MainAxisSize.min, children: [ ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - deviceIsOpened ? LKColors.lkBlue : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.all(deviceIsOpened + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( - deviceIsOpened ? LKColors.lkLightBlue : Colors.grey), + deviceIsOpened ? selectedOverlayColor : backgroundColor), + shadowColor: WidgetStateProperty.all(Colors.transparent), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(20.0), bottomLeft: Radius.circular(20.0)))), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || - lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => onToggle?.call(!deviceIsOpened), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff), + Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff, + color: iconColor), const SizedBox(width: 2), - if (title != null && - (deviceScreenType != DeviceScreenType.mobile || showLabel)) - Text( - title!, - style: TextStyle(fontSize: 14), - ), + if (titleWidget != null && + (deviceScreenType != DeviceScreenType.mobile || + showTitleWidget)) + titleWidget!, ], ), ), const SizedBox(width: 0.2), - PopupMenuButton( - padding: const EdgeInsets.all(12), - icon: const Icon(Icons.arrow_drop_down), - offset: Offset( - 0, ((deviceList.isNotEmpty ? deviceList.length : 1) * -55.0)), - style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), - elevation: WidgetStateProperty.all(20), - shape: WidgetStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(20.0), - bottomRight: Radius.circular(20.0)))), + SizedBox( + height: 42, + child: PopupMenuButton( + icon: Icon( + Icons.arrow_drop_down, + color: iconColor, + ), + offset: Offset( + 0, ((deviceList.isNotEmpty ? deviceList.length : 1) * -55.0)), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(deviceIsOpened + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + deviceIsOpened ? selectedOverlayColor : backgroundColor), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0)))), + ), + enabled: deviceIsOpened || defaultSelectable, + itemBuilder: (BuildContext context) { + return [ + if (deviceList.isNotEmpty) + ...deviceList.map((device) { + return PopupMenuItem( + value: device, + child: ListTile( + selected: (device.deviceId == selectedDeviceId), + selectedColor: selectedColor, + leading: (device.deviceId == selectedDeviceId) + ? Icon( + Icons.check_box_outlined, + color: (device.deviceId == selectedDeviceId) + ? selectedColor + : backgroundColor, + ) + : Icon( + Icons.check_box_outline_blank, + color: backgroundColor, + ), + title: Text(device.label), + ), + onTap: () => onSelect?.call(device), + ); + }) + ]; + }, ), - enabled: deviceIsOpened || defaultSelectable, - itemBuilder: (BuildContext context) { - return [ - if (deviceList.isNotEmpty) - ...deviceList.map((device) { - return PopupMenuItem( - value: device, - child: ListTile( - selected: (device.deviceId == selectedDeviceId), - selectedColor: LKColors.lkBlue, - leading: (device.deviceId == selectedDeviceId) - ? Icon( - Icons.check_box_outlined, - color: (device.deviceId == selectedDeviceId) - ? LKColors.lkBlue - : Colors.white, - ) - : const Icon( - Icons.check_box_outline_blank, - color: Colors.white, - ), - title: Text(device.label), - ), - onTap: () => onSelect?.call(device), - ); - }) - ]; - }, ), ]); } diff --git a/lib/src/ui/widgets/room/microphone_select_button.dart b/lib/src/ui/widgets/room/microphone_select_button.dart index a03136d..f8458a6 100644 --- a/lib/src/ui/widgets/room/microphone_select_button.dart +++ b/lib/src/ui/widgets/room/microphone_select_button.dart @@ -15,21 +15,43 @@ import 'package:flutter/material.dart'; import '../../builder/room/media_device_select_button.dart'; +import '../theme.dart'; import 'media_device_select_button.dart'; class MicrophoneSelectButton extends StatelessWidget { const MicrophoneSelectButton({ super.key, - this.showLabels = false, + this.showtitleWidget = true, + this.titleWidget, + this.color = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.iconColor = Colors.white, + this.foregroundColor = Colors.white, + this.selectedOverlayColor = LKColors.lkLightBlue, }); - final bool showLabels; + final bool showtitleWidget; + final Widget? titleWidget; + final Color color; + final Color selectedColor; + final Color iconColor; + final Color foregroundColor; + final Color selectedOverlayColor; @override Widget build(BuildContext context) { return MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Microphone', + titleWidget: titleWidget ?? + Text( + 'Microphone', + style: TextStyle(fontSize: 14, color: iconColor), + ), + backgroundColor: color, + selectedColor: selectedColor, + foregroundColor: foregroundColor, + selectedOverlayColor: selectedOverlayColor, + iconColor: iconColor, iconOn: Icons.mic, iconOff: Icons.mic_off, deviceList: deviceCtx.audioInputs ?? [], @@ -39,7 +61,7 @@ class MicrophoneSelectButton extends StatelessWidget { onToggle: (enabled) => enabled ? deviceCtx.enableMicrophone() : deviceCtx.disableMicrophone(), - showLabel: showLabels, + showTitleWidget: showtitleWidget, ), ); } diff --git a/lib/src/ui/widgets/room/screenshare_toggle.dart b/lib/src/ui/widgets/room/screenshare_toggle.dart index 5c0cbc8..18b4809 100644 --- a/lib/src/ui/widgets/room/screenshare_toggle.dart +++ b/lib/src/ui/widgets/room/screenshare_toggle.dart @@ -28,12 +28,22 @@ class ScreenShareToggleWidget extends StatelessWidget { required this.deviceCtx, required this.screenShareEnabled, this.showLabel = false, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.selectedColor = LKColors.lkBlue, + this.overlayColor = Colors.grey, + this.selectedOverlayColor = LKColors.lkLightBlue, }); final bool screenShareEnabled; final bool showLabel; final RoomContext roomCtx; final MediaDeviceContext deviceCtx; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color selectedColor; + final Color selectedOverlayColor; @override Widget build(BuildContext context) { @@ -41,11 +51,11 @@ class ScreenShareToggleWidget extends StatelessWidget { return ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(screenShareEnabled - ? LKColors.lkBlue - : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( - screenShareEnabled ? LKColors.lkLightBlue : Colors.grey), + screenShareEnabled ? selectedOverlayColor : overlayColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -54,9 +64,9 @@ class ScreenShareToggleWidget extends StatelessWidget { ), ), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => screenShareEnabled diff --git a/lib/src/ui/widgets/room/speaker_switch_button.dart b/lib/src/ui/widgets/room/speaker_switch_button.dart index 92ef617..a35be40 100644 --- a/lib/src/ui/widgets/room/speaker_switch_button.dart +++ b/lib/src/ui/widgets/room/speaker_switch_button.dart @@ -20,19 +20,26 @@ class SpeakerSwitchButton extends StatelessWidget { this.isSpeakerOn = false, this.onToggle, this.disabled = false, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, }); final bool isSpeakerOn; final Function(bool speakerOn)? onToggle; final bool disabled; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; @override Widget build(BuildContext context) { return ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), + backgroundColor: + WidgetStateProperty.all(backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all(overlayColor), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20.0)))), padding: WidgetStateProperty.all(const EdgeInsets.all(12)), diff --git a/lib/src/ui/widgets/theme.dart b/lib/src/ui/widgets/theme.dart index 732dce7..4208ada 100644 --- a/lib/src/ui/widgets/theme.dart +++ b/lib/src/ui/widgets/theme.dart @@ -60,7 +60,7 @@ class LiveKitTheme { // backgroundColor: WidgetStateProperty.all(accentColor), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return accentColor.withOpacity(0.5); + return accentColor.withValues(alpha: 0.5); } return accentColor; }), @@ -75,13 +75,13 @@ class LiveKitTheme { if (states.contains(WidgetState.selected)) { return accentColor; } - return accentColor.withOpacity(0.3); + return accentColor.withValues(alpha: 0.3); }), thumbColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return Colors.white; } - return Colors.white.withOpacity(0.3); + return Colors.white.withValues(alpha: 0.3); }), ), dialogTheme: DialogTheme( @@ -103,7 +103,7 @@ class LiveKitTheme { color: LKColors.lkBlue, ), hintStyle: TextStyle( - color: LKColors.lkBlue.withOpacity(0.5), + color: LKColors.lkBlue.withValues(alpha: 0.5), ), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, diff --git a/lib/src/ui/widgets/track/audio_visualizer_widget.dart b/lib/src/ui/widgets/track/audio_visualizer_widget.dart index 2e61f00..05f25c3 100644 --- a/lib/src/ui/widgets/track/audio_visualizer_widget.dart +++ b/lib/src/ui/widgets/track/audio_visualizer_widget.dart @@ -4,7 +4,6 @@ import 'package:livekit_client/livekit_client.dart'; import 'package:provider/provider.dart'; import '../../../context/track_reference_context.dart'; -import '../theme.dart'; import 'no_track_widget.dart'; class AudioVisualizerWidgetOptions { @@ -35,9 +34,13 @@ class AudioVisualizerWidgetOptions { class AudioVisualizerWidget extends StatelessWidget { final AudioVisualizerWidgetOptions options; + final Widget noTrackWidget; + final Color backgroundColor; const AudioVisualizerWidget({ Key? key, + this.noTrackWidget = const NoTrackWidget(), + this.backgroundColor = Colors.transparent, this.options = const AudioVisualizerWidgetOptions(), }) : super(key: key); @@ -46,7 +49,7 @@ class AudioVisualizerWidget extends StatelessWidget { var trackCtx = Provider.of(context); if (trackCtx == null) { - return const NoTrackWidget(); + return noTrackWidget; } return Consumer( @@ -55,13 +58,14 @@ class AudioVisualizerWidget extends StatelessWidget { selector: (context, audioTrack) => trackCtx.audioTrack, builder: (BuildContext context, AudioTrack? audioTrack, Widget? child) { if (trackCtx.audioTrack == null) { - return const NoTrackWidget(); + return noTrackWidget; } return Container( - color: LKColors.lkDarkBlue, + color: backgroundColor, child: Center( child: SoundWaveformWidget( audioTrack: audioTrack, + participant: trackCtx.participant, options: options, ), ), @@ -75,10 +79,12 @@ class AudioVisualizerWidget extends StatelessWidget { class SoundWaveformWidget extends StatefulWidget { final AudioVisualizerWidgetOptions options; final AudioTrack? audioTrack; + final Participant participant; const SoundWaveformWidget({ super.key, this.audioTrack, + required this.participant, this.options = const AudioVisualizerWidgetOptions(), }); @@ -89,15 +95,28 @@ class SoundWaveformWidget extends StatefulWidget { class _SoundWaveformWidgetState extends State with TickerProviderStateMixin { late AnimationController controller; - List samples = [0, 0, 0, 0, 0, 0, 0]; + late List samples; AudioVisualizer? _visualizer; EventsListener? _listener; + EventsListener? _participantListener; void _startVisualizer(AudioTrack? track) async { if (track == null) { return; } - samples = List.filled(widget.options.barCount, 0); + + _participantListener ??= widget.participant.createListener(); + _participantListener?.on((e) { + if (mounted) { + setState(() { + samples = List.filled(widget.options.barCount, + widget.options.minHeight / widget.options.maxHeight); + }); + } + }); + + samples = List.filled(widget.options.barCount, + widget.options.minHeight / widget.options.maxHeight); _visualizer ??= createVisualizer(track, options: AudioVisualizerOptions( barCount: widget.options.barCount, @@ -120,6 +139,8 @@ class _SoundWaveformWidgetState extends State _visualizer = null; await _listener?.dispose(); _listener = null; + await _participantListener?.dispose(); + _participantListener = null; } @override @@ -148,44 +169,47 @@ class _SoundWaveformWidgetState extends State final count = widget.options.barCount; final minHeight = widget.options.minHeight; final maxHeight = widget.options.maxHeight; - return AnimatedBuilder( - animation: controller, - builder: (c, child) { - return Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - count, - (i) { - final heightPercent = - ((samples[i] - minHeight) / (maxHeight - minHeight)) - .clamp(0.0, 1.0); - final barOpacity = - (1.0 - widget.options.barMinOpacity) * heightPercent + - widget.options.barMinOpacity; - - return AnimatedContainer( - duration: Duration( - milliseconds: - widget.options.durationInMilliseconds ~/ count), - margin: i == (samples.length - 1) - ? EdgeInsets.zero - : EdgeInsets.only(right: widget.options.spacing), - height: samples[i] < minHeight - ? minHeight - : samples[i] > maxHeight - ? maxHeight - : samples[i], - width: widget.options.width, - decoration: BoxDecoration( - color: widget.options.color.withOpacity(barOpacity), - borderRadius: - BorderRadius.circular(widget.options.cornerRadius), - ), - ); - }, - ), - ); - }, + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => + AnimatedBuilder( + animation: controller, + builder: (c, child) { + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + count, + (i) { + final heightPercent = + ((samples[i] - minHeight) / (maxHeight - minHeight)) + .clamp(0.0, 1.0); + final barOpacity = + (1.0 - widget.options.barMinOpacity) * heightPercent + + widget.options.barMinOpacity; + + return AnimatedContainer( + duration: Duration( + milliseconds: + widget.options.durationInMilliseconds ~/ count), + margin: i == (samples.length - 1) + ? EdgeInsets.zero + : EdgeInsets.only(right: widget.options.spacing), + height: samples[i] < minHeight + ? minHeight + : samples[i] > maxHeight + ? maxHeight + : samples[i], + width: widget.options.width, + decoration: BoxDecoration( + color: widget.options.color.withValues(alpha: barOpacity), + borderRadius: + BorderRadius.circular(widget.options.cornerRadius), + ), + ); + }, + ), + ); + }, + ), ); } } diff --git a/lib/src/ui/widgets/track/no_track_widget.dart b/lib/src/ui/widgets/track/no_track_widget.dart index 097f5ed..79ff023 100644 --- a/lib/src/ui/widgets/track/no_track_widget.dart +++ b/lib/src/ui/widgets/track/no_track_widget.dart @@ -25,8 +25,13 @@ import '../theme.dart'; class NoTrackWidget extends StatelessWidget { const NoTrackWidget({ super.key, + this.iconColor = LKColors.lkBlue, + this.backgroundColor = LKColors.lkDarkBlue, }); + final Color iconColor; + final Color backgroundColor; + @override Widget build(BuildContext context) { var trackCtx = Provider.of(context); @@ -51,12 +56,12 @@ class NoTrackWidget extends StatelessWidget { } return Container( - color: LKColors.lkDarkBlue, + color: backgroundColor, child: Center( child: LayoutBuilder( builder: (context, constraints) => Icon( icon, - color: LKColors.lkBlue, + color: iconColor, size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.33, ), ), diff --git a/lib/src/ui/widgets/track/track_stats_widget.dart b/lib/src/ui/widgets/track/track_stats_widget.dart index 59c8af5..815f46c 100644 --- a/lib/src/ui/widgets/track/track_stats_widget.dart +++ b/lib/src/ui/widgets/track/track_stats_widget.dart @@ -26,7 +26,7 @@ class TrackStatsWidget extends StatelessWidget { children: [ trackCtx.showStatistics && stats.isNotEmpty ? Container( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8, diff --git a/pubspec.yaml b/pubspec.yaml index 9ddee23..c23967c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: livekit_components description: "Out-of-the-box flutter widgets for livekit client." -version: 1.2.0 +version: 1.2.1 homepage: https://github.com/livekit/components-flutter environment: @@ -12,11 +12,11 @@ dependencies: flutter: sdk: flutter flutter_background: ^1.3.0+1 - flutter_webrtc: ^0.12.12+hotfix.1 + flutter_webrtc: ^0.13.1+hotfix.1 google_fonts: ^6.2.1 http: ^1.2.2 intl: ^0.19.0 - livekit_client: ^2.4.1 + livekit_client: ^2.4.3 provider: ^6.1.2 responsive_builder: ^0.7.1