🐛 Fixes call

This commit is contained in:
2025-08-02 01:53:02 +08:00
parent 26135d2116
commit a0d8c1a9b3
9 changed files with 153 additions and 52 deletions

View File

@@ -728,5 +728,8 @@
"selectCamera": "Select Camera", "selectCamera": "Select Camera",
"switchedTo": "Switched to {}", "switchedTo": "Switched to {}",
"connecting": "Connecting", "connecting": "Connecting",
"reconnecting": "Reconnecting",
"disconnected": "Disconnected",
"connected": "Connected",
"repliesLoadMore": "Load more replies" "repliesLoadMore": "Load more replies"
} }

View File

@@ -1,5 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; 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:island/widgets/chat/call_button.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -25,6 +28,7 @@ sealed class CallState with _$CallState {
required bool isMicrophoneEnabled, required bool isMicrophoneEnabled,
required bool isCameraEnabled, required bool isCameraEnabled,
required bool isScreenSharing, required bool isScreenSharing,
required bool isSpeakerphone,
@Default(Duration(seconds: 0)) Duration duration, @Default(Duration(seconds: 0)) Duration duration,
String? error, String? error,
}) = _CallState; }) = _CallState;
@@ -62,6 +66,8 @@ class CallNotifier extends _$CallNotifier {
List.unmodifiable(_participants); List.unmodifiable(_participants);
LocalParticipant? get localParticipant => _localParticipant; LocalParticipant? get localParticipant => _localParticipant;
Map<String, double> participantsVolumes = {};
Timer? _durationTimer; Timer? _durationTimer;
Room? get room => _room; Room? get room => _room;
@@ -74,6 +80,7 @@ class CallNotifier extends _$CallNotifier {
isMicrophoneEnabled: true, isMicrophoneEnabled: true,
isCameraEnabled: false, isCameraEnabled: false,
isScreenSharing: false, isScreenSharing: false,
isSpeakerphone: true,
); );
} }
@@ -264,6 +271,10 @@ class CallNotifier extends _$CallNotifier {
_initRoomListeners(); _initRoomListeners();
_updateLiveParticipants(participants); _updateLiveParticipants(participants);
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
Hardware.instance.setSpeakerphoneOn(true);
}
// Listen for connection updates // Listen for connection updates
_room!.addListener(() { _room!.addListener(() {
state = state.copyWith( state = state.copyWith(
@@ -318,6 +329,12 @@ class CallNotifier extends _$CallNotifier {
} }
} }
Future<void> toggleSpeakerphone() async {
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
state = state.copyWith();
}
Future<void> disconnect() async { Future<void> disconnect() async {
if (_room != null) { if (_room != null) {
await _room!.disconnect(); 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() { void dispose() {
state = state.copyWith( state = state.copyWith(
error: null, error: null,
@@ -343,5 +380,6 @@ class CallNotifier extends _$CallNotifier {
_room?.dispose(); _room?.dispose();
_durationTimer?.cancel(); _durationTimer?.cancel();
_roomId = null; _roomId = null;
participantsVolumes = {};
} }
} }

View File

@@ -12,9 +12,9 @@ part of 'call.dart';
// dart format off // dart format off
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
/// @nodoc /// @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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -22,19 +22,25 @@ mixin _$CallState {
$CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>(this as CallState, _$identity); $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>(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 @override
bool operator ==(Object other) { 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 @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 @override
String toString() { String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)'; 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; factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable 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,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,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,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 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 Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?, as String?,
@@ -152,10 +159,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(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) { switch (_that) {
case _CallState() when $default != null: 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(); return orElse();
} }
@@ -173,10 +180,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _CallState(): 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` /// 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 extends Object?>(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _CallState() when $default != null: 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; return null;
} }
@@ -204,14 +211,15 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// @nodoc /// @nodoc
class _CallState implements CallState { class _CallState with DiagnosticableTreeMixin implements CallState {
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.duration = const Duration(seconds: 0), this.error}); 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 isConnected;
@override final bool isMicrophoneEnabled; @override final bool isMicrophoneEnabled;
@override final bool isCameraEnabled; @override final bool isCameraEnabled;
@override final bool isScreenSharing; @override final bool isScreenSharing;
@override final bool isSpeakerphone;
@override@JsonKey() final Duration duration; @override@JsonKey() final Duration duration;
@override final String? error; @override final String? error;
@@ -222,19 +230,25 @@ class _CallState implements CallState {
_$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallState>(this, _$identity); _$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 @override
bool operator ==(Object other) { 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 @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 @override
String toString() { String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)'; 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; factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_CallState(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable 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,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,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,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 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 Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?, as String?,
@@ -278,7 +293,7 @@ as String?,
} }
/// @nodoc /// @nodoc
mixin _$CallParticipantLive { mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
CallParticipant get participant; Participant get remoteParticipant; CallParticipant get participant; Participant get remoteParticipant;
/// Create a copy of CallParticipantLive /// Create a copy of CallParticipantLive
@@ -288,6 +303,12 @@ mixin _$CallParticipantLive {
$CallParticipantLiveCopyWith<CallParticipantLive> get copyWith => _$CallParticipantLiveCopyWithImpl<CallParticipantLive>(this as CallParticipantLive, _$identity); $CallParticipantLiveCopyWith<CallParticipantLive> get copyWith => _$CallParticipantLiveCopyWithImpl<CallParticipantLive>(this as CallParticipantLive, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallParticipantLive'))
..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant));
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -299,7 +320,7 @@ bool operator ==(Object other) {
int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); int get hashCode => Object.hash(runtimeType,participant,remoteParticipant);
@override @override
String toString() { String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)';
} }
@@ -475,7 +496,7 @@ return $default(_that.participant,_that.remoteParticipant);case _:
/// @nodoc /// @nodoc
class _CallParticipantLive extends CallParticipantLive { class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMixin {
const _CallParticipantLive({required this.participant, required this.remoteParticipant}): super._(); 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); _$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 @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -500,7 +527,7 @@ bool operator ==(Object other) {
int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); int get hashCode => Object.hash(runtimeType,participant,remoteParticipant);
@override @override
String toString() { String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)';
} }

View File

@@ -6,7 +6,7 @@ part of 'call.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$callNotifierHash() => r'a67ff053d69b2edbbb13c7c865f8adc3b77c4e86'; String _$callNotifierHash() => r'333a1cd566a339644c83932e15dae03f1c5cc24b';
/// See also [CallNotifier]. /// See also [CallNotifier].
@ProviderFor(CallNotifier) @ProviderFor(CallNotifier)

View File

@@ -1,7 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'package:easy_localization/easy_localization.dart'; 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:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -68,7 +68,12 @@ class CallScreen extends HookConsumerWidget {
Text( Text(
callState.isConnected callState.isConnected
? formatDuration(callState.duration) ? formatDuration(callState.duration)
: 'connecting', : (switch (callNotifier.room?.connectionState) {
ConnectionState.connected => 'connected',
ConnectionState.connecting => 'connecting',
ConnectionState.reconnecting => 'reconnecting',
_ => 'disconnected',
}).tr(),
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
], ],

View File

@@ -22,8 +22,10 @@ class CallControlsBar extends HookConsumerWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row( child: Wrap(
mainAxisAlignment: MainAxisAlignment.center, alignment: WrapAlignment.center,
runSpacing: 16,
spacing: 16,
children: [ children: [
_buildCircularButtonWithDropdown( _buildCircularButtonWithDropdown(
context: context, context: context,
@@ -35,7 +37,6 @@ class CallControlsBar extends HookConsumerWidget {
hasDropdown: true, hasDropdown: true,
deviceType: 'videoinput', deviceType: 'videoinput',
), ),
const Gap(16),
_buildCircularButton( _buildCircularButton(
icon: icon:
callState.isScreenSharing callState.isScreenSharing
@@ -44,7 +45,6 @@ class CallControlsBar extends HookConsumerWidget {
onPressed: () => callNotifier.toggleScreenShare(), onPressed: () => callNotifier.toggleScreenShare(),
backgroundColor: const Color(0xFF424242), backgroundColor: const Color(0xFF424242),
), ),
const Gap(16),
_buildCircularButtonWithDropdown( _buildCircularButtonWithDropdown(
context: context, context: context,
ref: ref, ref: ref,
@@ -54,7 +54,14 @@ class CallControlsBar extends HookConsumerWidget {
hasDropdown: true, hasDropdown: true,
deviceType: 'audioinput', deviceType: 'audioinput',
), ),
const Gap(16), _buildCircularButton(
icon:
callState.isSpeakerphone
? Symbols.mobile_speaker
: Symbols.ear_sound,
onPressed: () => callNotifier.toggleSpeakerphone(),
backgroundColor: const Color(0xFF424242),
),
_buildCircularButton( _buildCircularButton(
icon: Icons.call_end, icon: Icons.call_end,
onPressed: onPressed:
@@ -259,24 +266,14 @@ class CallControlsBar extends HookConsumerWidget {
} }
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showSnackBar(
SnackBar( 'switchedTo'.tr(
content: Text( args: [device.label.isNotEmpty ? device.label : 'device'],
'${'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,
), ),
); );
} }
} catch (err) {
showErrorAlert(err);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -18,6 +19,10 @@ class CallParticipantCard extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final width = final width =
math.min(MediaQuery.of(context).size.width - 80, 360).toDouble(); math.min(MediaQuery.of(context).size.width - 80, 360).toDouble();
final callNotifier = ref.watch(callNotifierProvider.notifier);
final volumeSliderValue = useState(callNotifier.getParticipantVolume(live));
return PopupCard( return PopupCard(
elevation: 8, elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
@@ -28,7 +33,31 @@ class CallParticipantCard extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Column( Column(
spacing: 4,
children: [ 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( Row(
children: [ children: [
const Icon(Symbols.wifi, size: 16), const Icon(Symbols.wifi, size: 16),

View File

@@ -1,7 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';

View File

@@ -81,7 +81,10 @@ class MarkdownTextContent extends HookConsumerWidget {
if (url != null) { if (url != null) {
if (url.scheme == 'solian') { if (url.scheme == 'solian') {
if (url.host == 'account') { if (url.host == 'account') {
context.pushNamed('accountProfile', pathParameters: {'name': url.pathSegments[0]}); context.pushNamed(
'accountProfile',
pathParameters: {'name': url.pathSegments[0]},
);
} }
return; return;
} }
@@ -153,7 +156,7 @@ class MarkdownTextContent extends HookConsumerWidget {
), ),
child: UniversalImage( child: UniversalImage(
uri: uri:
'$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open', '$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open',
width: size, width: size,
height: size, height: size,
fit: BoxFit.cover, fit: BoxFit.cover,