From a0d8c1a9b3c15d5ea3897f9cb9ad129e67b4c300 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 2 Aug 2025 01:53:02 +0800 Subject: [PATCH] :bug: Fixes call --- assets/i18n/en-US.json | 3 + lib/pods/call.dart | 38 ++++++++++ lib/pods/call.freezed.dart | 79 ++++++++++++++------- lib/pods/call.g.dart | 2 +- lib/screens/chat/call.dart | 9 ++- lib/widgets/chat/call_overlay.dart | 37 +++++----- lib/widgets/chat/call_participant_card.dart | 29 ++++++++ lib/widgets/content/cloud_files.dart | 1 - lib/widgets/content/markdown.dart | 7 +- 9 files changed, 153 insertions(+), 52 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index b73a8cf..52e41e2 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -728,5 +728,8 @@ "selectCamera": "Select Camera", "switchedTo": "Switched to {}", "connecting": "Connecting", + "reconnecting": "Reconnecting", + "disconnected": "Disconnected", + "connected": "Connected", "repliesLoadMore": "Load more replies" } diff --git a/lib/pods/call.dart b/lib/pods/call.dart index 78c3a51..650cf20 100644 --- a/lib/pods/call.dart +++ b/lib/pods/call.dart @@ -1,5 +1,8 @@ import 'dart:async'; import 'dart:developer'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:island/widgets/chat/call_button.dart'; import 'package:livekit_client/livekit_client.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -25,6 +28,7 @@ sealed class CallState with _$CallState { required bool isMicrophoneEnabled, required bool isCameraEnabled, required bool isScreenSharing, + required bool isSpeakerphone, @Default(Duration(seconds: 0)) Duration duration, String? error, }) = _CallState; @@ -62,6 +66,8 @@ class CallNotifier extends _$CallNotifier { List.unmodifiable(_participants); LocalParticipant? get localParticipant => _localParticipant; + Map participantsVolumes = {}; + Timer? _durationTimer; Room? get room => _room; @@ -74,6 +80,7 @@ class CallNotifier extends _$CallNotifier { isMicrophoneEnabled: true, isCameraEnabled: false, isScreenSharing: false, + isSpeakerphone: true, ); } @@ -264,6 +271,10 @@ class CallNotifier extends _$CallNotifier { _initRoomListeners(); _updateLiveParticipants(participants); + if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + Hardware.instance.setSpeakerphoneOn(true); + } + // Listen for connection updates _room!.addListener(() { state = state.copyWith( @@ -318,6 +329,12 @@ class CallNotifier extends _$CallNotifier { } } + Future toggleSpeakerphone() async { + state = state.copyWith(isSpeakerphone: !state.isSpeakerphone); + await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone); + state = state.copyWith(); + } + Future disconnect() async { if (_room != null) { await _room!.disconnect(); @@ -330,6 +347,26 @@ class CallNotifier extends _$CallNotifier { } } + void setParticipantVolume(CallParticipantLive live, double volume) { + if (participantsVolumes[live.remoteParticipant.sid] == null) { + participantsVolumes[live.remoteParticipant.sid] = 1; + } + Helper.setVolume( + volume, + live + .remoteParticipant + .audioTrackPublications + .first + .track! + .mediaStreamTrack, + ); + participantsVolumes[live.remoteParticipant.sid] = volume; + } + + double getParticipantVolume(CallParticipantLive live) { + return participantsVolumes[live.remoteParticipant.sid] ?? 1; + } + void dispose() { state = state.copyWith( error: null, @@ -343,5 +380,6 @@ class CallNotifier extends _$CallNotifier { _room?.dispose(); _durationTimer?.cancel(); _roomId = null; + participantsVolumes = {}; } } diff --git a/lib/pods/call.freezed.dart b/lib/pods/call.freezed.dart index eaa2ed0..4c9c65a 100644 --- a/lib/pods/call.freezed.dart +++ b/lib/pods/call.freezed.dart @@ -12,9 +12,9 @@ part of 'call.dart'; // dart format off T _$identity(T value) => value; /// @nodoc -mixin _$CallState { +mixin _$CallState implements DiagnosticableTreeMixin { - bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; Duration get duration; String? get error; + bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; String? get error; /// Create a copy of CallState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -22,19 +22,25 @@ mixin _$CallState { $CallStateCopyWith get copyWith => _$CallStateCopyWithImpl(this as CallState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'CallState')) + ..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error)); +} @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); } @override -int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error); +int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error); @override -String toString() { - return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)'; +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)'; } @@ -45,7 +51,7 @@ abstract mixin class $CallStateCopyWith<$Res> { factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl; @useResult $Res call({ - bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error + bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error }); @@ -62,12 +68,13 @@ class _$CallStateCopyWithImpl<$Res> /// Create a copy of CallState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) { return _then(_self.copyWith( isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable +as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as String?, @@ -152,10 +159,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _CallState() when $default != null: -return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);case _: +return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _: return orElse(); } @@ -173,10 +180,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error) $default,) {final _that = this; switch (_that) { case _CallState(): -return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);} +return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);} } /// A variant of `when` that fallback to returning `null` /// @@ -190,10 +197,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,) {final _that = this; switch (_that) { case _CallState() when $default != null: -return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);case _: +return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _: return null; } @@ -204,14 +211,15 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable /// @nodoc -class _CallState implements CallState { - const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.duration = const Duration(seconds: 0), this.error}); +class _CallState with DiagnosticableTreeMixin implements CallState { + const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, required this.isSpeakerphone, this.duration = const Duration(seconds: 0), this.error}); @override final bool isConnected; @override final bool isMicrophoneEnabled; @override final bool isCameraEnabled; @override final bool isScreenSharing; +@override final bool isSpeakerphone; @override@JsonKey() final Duration duration; @override final String? error; @@ -222,19 +230,25 @@ class _CallState implements CallState { _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'CallState')) + ..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error)); +} @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); } @override -int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error); +int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error); @override -String toString() { - return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)'; +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)'; } @@ -245,7 +259,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl; @override @useResult $Res call({ - bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error + bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error }); @@ -262,12 +276,13 @@ class __$CallStateCopyWithImpl<$Res> /// Create a copy of CallState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) { return _then(_CallState( isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable +as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as String?, @@ -278,7 +293,7 @@ as String?, } /// @nodoc -mixin _$CallParticipantLive { +mixin _$CallParticipantLive implements DiagnosticableTreeMixin { CallParticipant get participant; Participant get remoteParticipant; /// Create a copy of CallParticipantLive @@ -288,6 +303,12 @@ mixin _$CallParticipantLive { $CallParticipantLiveCopyWith get copyWith => _$CallParticipantLiveCopyWithImpl(this as CallParticipantLive, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'CallParticipantLive')) + ..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant)); +} @override bool operator ==(Object other) { @@ -299,7 +320,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; } @@ -475,7 +496,7 @@ return $default(_that.participant,_that.remoteParticipant);case _: /// @nodoc -class _CallParticipantLive extends CallParticipantLive { +class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMixin { const _CallParticipantLive({required this.participant, required this.remoteParticipant}): super._(); @@ -489,6 +510,12 @@ class _CallParticipantLive extends CallParticipantLive { _$CallParticipantLiveCopyWith<_CallParticipantLive> get copyWith => __$CallParticipantLiveCopyWithImpl<_CallParticipantLive>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'CallParticipantLive')) + ..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant)); +} @override bool operator ==(Object other) { @@ -500,7 +527,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; } diff --git a/lib/pods/call.g.dart b/lib/pods/call.g.dart index 9d1e3df..a3b23e9 100644 --- a/lib/pods/call.g.dart +++ b/lib/pods/call.g.dart @@ -6,7 +6,7 @@ part of 'call.dart'; // RiverpodGenerator // ************************************************************************** -String _$callNotifierHash() => r'a67ff053d69b2edbbb13c7c865f8adc3b77c4e86'; +String _$callNotifierHash() => r'333a1cd566a339644c83932e15dae03f1c5cc24b'; /// See also [CallNotifier]. @ProviderFor(CallNotifier) diff --git a/lib/screens/chat/call.dart b/lib/screens/chat/call.dart index ebe3349..c8874a4 100644 --- a/lib/screens/chat/call.dart +++ b/lib/screens/chat/call.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide ConnectionState; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -68,7 +68,12 @@ class CallScreen extends HookConsumerWidget { Text( callState.isConnected ? formatDuration(callState.duration) - : 'connecting', + : (switch (callNotifier.room?.connectionState) { + ConnectionState.connected => 'connected', + ConnectionState.connecting => 'connecting', + ConnectionState.reconnecting => 'reconnecting', + _ => 'disconnected', + }).tr(), style: const TextStyle(fontSize: 14), ), ], diff --git a/lib/widgets/chat/call_overlay.dart b/lib/widgets/chat/call_overlay.dart index 26eca83..c0a9db8 100644 --- a/lib/widgets/chat/call_overlay.dart +++ b/lib/widgets/chat/call_overlay.dart @@ -22,8 +22,10 @@ class CallControlsBar extends HookConsumerWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Wrap( + alignment: WrapAlignment.center, + runSpacing: 16, + spacing: 16, children: [ _buildCircularButtonWithDropdown( context: context, @@ -35,7 +37,6 @@ class CallControlsBar extends HookConsumerWidget { hasDropdown: true, deviceType: 'videoinput', ), - const Gap(16), _buildCircularButton( icon: callState.isScreenSharing @@ -44,7 +45,6 @@ class CallControlsBar extends HookConsumerWidget { onPressed: () => callNotifier.toggleScreenShare(), backgroundColor: const Color(0xFF424242), ), - const Gap(16), _buildCircularButtonWithDropdown( context: context, ref: ref, @@ -54,7 +54,14 @@ class CallControlsBar extends HookConsumerWidget { hasDropdown: true, deviceType: 'audioinput', ), - const Gap(16), + _buildCircularButton( + icon: + callState.isSpeakerphone + ? Symbols.mobile_speaker + : Symbols.ear_sound, + onPressed: () => callNotifier.toggleSpeakerphone(), + backgroundColor: const Color(0xFF424242), + ), _buildCircularButton( icon: Icons.call_end, onPressed: @@ -259,24 +266,14 @@ class CallControlsBar extends HookConsumerWidget { } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${'switchedTo'.tr()} ${device.label.isNotEmpty ? device.label : 'selectedDevice'.tr()}', - ), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${'failedToSwitchDevice'.tr()}: $e'), - backgroundColor: Colors.red, + showSnackBar( + 'switchedTo'.tr( + args: [device.label.isNotEmpty ? device.label : 'device'], ), ); } + } catch (err) { + showErrorAlert(err); } } } diff --git a/lib/widgets/chat/call_participant_card.dart b/lib/widgets/chat/call_participant_card.dart index 98d88e5..d4d6d8e 100644 --- a/lib/widgets/chat/call_participant_card.dart +++ b/lib/widgets/chat/call_participant_card.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -18,6 +19,10 @@ class CallParticipantCard extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final width = math.min(MediaQuery.of(context).size.width - 80, 360).toDouble(); + final callNotifier = ref.watch(callNotifierProvider.notifier); + + final volumeSliderValue = useState(callNotifier.getParticipantVolume(live)); + return PopupCard( elevation: 8, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), @@ -28,7 +33,31 @@ class CallParticipantCard extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Column( + spacing: 4, children: [ + Row( + children: [ + const Icon(Symbols.sound_detection_loud_sound, size: 16), + const Gap(8), + Expanded( + child: Slider( + value: volumeSliderValue.value, + onChanged: (value) { + volumeSliderValue.value = value; + }, + onChangeEnd: (value) { + callNotifier.setParticipantVolume(live, value); + }, + year2023: true, + padding: EdgeInsets.zero, + ), + ), + const Gap(8), + Text( + '${(volumeSliderValue.value * 100).toStringAsFixed(0)}%', + ), + ], + ), Row( children: [ const Icon(Symbols.wifi, size: 16), diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index cb26046..bdd7b8f 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -1,7 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/config.dart'; diff --git a/lib/widgets/content/markdown.dart b/lib/widgets/content/markdown.dart index cf12a80..4c1e697 100644 --- a/lib/widgets/content/markdown.dart +++ b/lib/widgets/content/markdown.dart @@ -81,7 +81,10 @@ class MarkdownTextContent extends HookConsumerWidget { if (url != null) { if (url.scheme == 'solian') { if (url.host == 'account') { - context.pushNamed('accountProfile', pathParameters: {'name': url.pathSegments[0]}); + context.pushNamed( + 'accountProfile', + pathParameters: {'name': url.pathSegments[0]}, + ); } return; } @@ -153,7 +156,7 @@ class MarkdownTextContent extends HookConsumerWidget { ), child: UniversalImage( uri: - '$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open', + '$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open', width: size, height: size, fit: BoxFit.cover,