💄 Better call UI
This commit is contained in:
@@ -16,6 +16,8 @@ import 'package:island/talker.dart';
|
|||||||
part 'call.g.dart';
|
part 'call.g.dart';
|
||||||
part 'call.freezed.dart';
|
part 'call.freezed.dart';
|
||||||
|
|
||||||
|
enum ViewMode { grid, stage }
|
||||||
|
|
||||||
String formatDuration(Duration duration) {
|
String formatDuration(Duration duration) {
|
||||||
String negativeSign = duration.isNegative ? '-' : '';
|
String negativeSign = duration.isNegative ? '-' : '';
|
||||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
@@ -33,6 +35,7 @@ sealed class CallState with _$CallState {
|
|||||||
required bool isScreenSharing,
|
required bool isScreenSharing,
|
||||||
required bool isSpeakerphone,
|
required bool isSpeakerphone,
|
||||||
@Default(Duration(seconds: 0)) Duration duration,
|
@Default(Duration(seconds: 0)) Duration duration,
|
||||||
|
@Default(ViewMode.grid) ViewMode viewMode,
|
||||||
String? error,
|
String? error,
|
||||||
}) = _CallState;
|
}) = _CallState;
|
||||||
}
|
}
|
||||||
@@ -84,6 +87,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
isCameraEnabled: false,
|
isCameraEnabled: false,
|
||||||
isScreenSharing: false,
|
isScreenSharing: false,
|
||||||
isSpeakerphone: true,
|
isSpeakerphone: true,
|
||||||
|
viewMode: ViewMode.grid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +262,8 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
duration: Duration(
|
duration: Duration(
|
||||||
milliseconds:
|
milliseconds:
|
||||||
(DateTime.now().millisecondsSinceEpoch -
|
(DateTime.now().millisecondsSinceEpoch -
|
||||||
(ongoingCall?.createdAt.millisecondsSinceEpoch ??
|
(ongoingCall?.createdAt.millisecondsSinceEpoch ??
|
||||||
DateTime.now().millisecondsSinceEpoch)),
|
DateTime.now().millisecondsSinceEpoch)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -418,6 +422,14 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
|
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleViewMode() {
|
||||||
|
state = state.copyWith(
|
||||||
|
viewMode: state.viewMode == ViewMode.grid
|
||||||
|
? ViewMode.stage
|
||||||
|
: ViewMode.grid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$CallState implements DiagnosticableTreeMixin {
|
mixin _$CallState implements DiagnosticableTreeMixin {
|
||||||
|
|
||||||
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; String? get error;
|
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; ViewMode get viewMode; 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)
|
||||||
@@ -26,21 +26,21 @@ $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>
|
|||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
properties
|
properties
|
||||||
..add(DiagnosticsProperty('type', 'CallState'))
|
..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));
|
..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('viewMode', viewMode))..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.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(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.viewMode, viewMode) || other.viewMode == viewMode)&&(identical(other.error, error) || other.error == error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error);
|
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,viewMode,error);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||||
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)';
|
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, viewMode: $viewMode, error: $error)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -51,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, bool isSpeakerphone, Duration duration, String? error
|
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ 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? isSpeakerphone = 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? viewMode = 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
|
||||||
@@ -76,7 +76,8 @@ as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCam
|
|||||||
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,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,viewMode: null == viewMode ? _self.viewMode : viewMode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ViewMode,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -159,10 +160,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@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;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, 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.isSpeakerphone,_that.duration,_that.error);case _:
|
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.viewMode,_that.error);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -180,10 +181,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, bool isSpeakerphone, 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, ViewMode viewMode, 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.isSpeakerphone,_that.duration,_that.error);}
|
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.viewMode,_that.error);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -197,10 +198,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, bool isSpeakerphone, 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, ViewMode viewMode, 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.isSpeakerphone,_that.duration,_that.error);case _:
|
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.viewMode,_that.error);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -212,7 +213,7 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
|
|||||||
|
|
||||||
|
|
||||||
class _CallState with DiagnosticableTreeMixin implements CallState {
|
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});
|
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, required this.isSpeakerphone, this.duration = const Duration(seconds: 0), this.viewMode = ViewMode.grid, this.error});
|
||||||
|
|
||||||
|
|
||||||
@override final bool isConnected;
|
@override final bool isConnected;
|
||||||
@@ -221,6 +222,7 @@ class _CallState with DiagnosticableTreeMixin implements CallState {
|
|||||||
@override final bool isScreenSharing;
|
@override final bool isScreenSharing;
|
||||||
@override final bool isSpeakerphone;
|
@override final bool isSpeakerphone;
|
||||||
@override@JsonKey() final Duration duration;
|
@override@JsonKey() final Duration duration;
|
||||||
|
@override@JsonKey() final ViewMode viewMode;
|
||||||
@override final String? error;
|
@override final String? error;
|
||||||
|
|
||||||
/// Create a copy of CallState
|
/// Create a copy of CallState
|
||||||
@@ -234,21 +236,21 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt
|
|||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
properties
|
properties
|
||||||
..add(DiagnosticsProperty('type', 'CallState'))
|
..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));
|
..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('viewMode', viewMode))..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.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(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.viewMode, viewMode) || other.viewMode == viewMode)&&(identical(other.error, error) || other.error == error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error);
|
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,viewMode,error);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||||
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)';
|
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, viewMode: $viewMode, error: $error)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -259,7 +261,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, bool isSpeakerphone, Duration duration, String? error
|
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -276,7 +278,7 @@ 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? isSpeakerphone = 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? viewMode = 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
|
||||||
@@ -284,7 +286,8 @@ as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCam
|
|||||||
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,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,viewMode: null == viewMode ? _self.viewMode : viewMode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ViewMode,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class CallNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$callNotifierHash() => r'ef4e3e9c9d411cf9dce1ceb456a3b866b2c87db3';
|
String _$callNotifierHash() => r'40bd884d3918b8e817329589c921774ab3c62ea2';
|
||||||
|
|
||||||
abstract class _$CallNotifier extends $Notifier<CallState> {
|
abstract class _$CallNotifier extends $Notifier<CallState> {
|
||||||
CallState build();
|
CallState build();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ final class MessagesNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'284625cff963ff26375d50d7202a33184e810fcb';
|
String _$messagesNotifierHash() => r'c7e2cd7f5b8673af88f5076814393dbfbd0d43c5';
|
||||||
|
|
||||||
final class MessagesNotifierFamily extends $Family
|
final class MessagesNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
callState.isConnected
|
callState.isConnected
|
||||||
? formatDuration(callState.duration)
|
? formatDuration(callState.duration)
|
||||||
: (switch (callNotifier.room?.connectionState) {
|
: (switch (callNotifier.room?.connectionState) {
|
||||||
ConnectionState.connected => 'connected',
|
ConnectionState.connected => 'connected',
|
||||||
ConnectionState.connecting => 'connecting',
|
ConnectionState.connecting => 'connecting',
|
||||||
ConnectionState.reconnecting => 'reconnecting',
|
ConnectionState.reconnecting => 'reconnecting',
|
||||||
_ => 'disconnected',
|
_ => 'disconnected',
|
||||||
}).tr(),
|
}).tr(),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -92,40 +92,40 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body:
|
body: callState.error != null
|
||||||
callState.error != null
|
? Center(
|
||||||
? Center(
|
child: ConstrainedBox(
|
||||||
child: ConstrainedBox(
|
constraints: const BoxConstraints(maxWidth: 320),
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.error_outline, size: 48),
|
||||||
const Icon(Symbols.error_outline, size: 48),
|
const Gap(4),
|
||||||
const Gap(4),
|
Text(
|
||||||
Text(
|
callState.error!,
|
||||||
callState.error!,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
style: const TextStyle(color: Color(0xFF757575)),
|
||||||
style: const TextStyle(color: Color(0xFF757575)),
|
),
|
||||||
),
|
const Gap(8),
|
||||||
const Gap(8),
|
TextButton(
|
||||||
TextButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
callNotifier.disconnect();
|
||||||
callNotifier.disconnect();
|
callNotifier.dispose();
|
||||||
callNotifier.dispose();
|
callNotifier.joinRoom(room);
|
||||||
callNotifier.joinRoom(room);
|
},
|
||||||
},
|
child: Text('retry').tr(),
|
||||||
child: Text('retry').tr(),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(child: CallContent()),
|
|
||||||
CallControlsBar(),
|
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: double.infinity),
|
||||||
|
Expanded(child: CallContent()),
|
||||||
|
CallControlsBar(popOnLeaves: true),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,89 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
class CallStageView extends HookConsumerWidget {
|
||||||
|
final List<CallParticipantLive> participants;
|
||||||
|
final double? outerMaxHeight;
|
||||||
|
|
||||||
|
const CallStageView({
|
||||||
|
super.key,
|
||||||
|
required this.participants,
|
||||||
|
this.outerMaxHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final focusedIndex = useState<int>(0);
|
||||||
|
|
||||||
|
final focusedParticipant = participants[focusedIndex.value];
|
||||||
|
final otherParticipants = participants
|
||||||
|
.where((p) => p != focusedParticipant)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Focused participant (takes most space)
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Calculate dynamic width based on available space
|
||||||
|
final maxWidth = constraints.maxWidth * 0.8;
|
||||||
|
final maxHeight = (outerMaxHeight ?? constraints.maxHeight) * 0.6;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: CallParticipantTile(
|
||||||
|
live: focusedParticipant,
|
||||||
|
allTiles: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Horizontal list of other participants
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
for (final participant in otherParticipants)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTapDown: (_) {
|
||||||
|
final newIndex = participants.indexOf(participant);
|
||||||
|
focusedIndex.value = newIndex;
|
||||||
|
},
|
||||||
|
child: CallParticipantTile(
|
||||||
|
live: participant,
|
||||||
|
radius: 32,
|
||||||
|
allTiles: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CallContent extends HookConsumerWidget {
|
class CallContent extends HookConsumerWidget {
|
||||||
const CallContent({super.key});
|
final double? outerMaxHeight;
|
||||||
|
const CallContent({super.key, this.outerMaxHeight});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -34,7 +111,7 @@ class CallContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (allAudioOnly) {
|
if (allAudioOnly) {
|
||||||
// Audio-only: show avatars in a compact row
|
// Audio-only: show avatars in a compact row with animated containers
|
||||||
return Center(
|
return Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@@ -45,36 +122,49 @@ class CallContent extends HookConsumerWidget {
|
|||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final live in participants)
|
for (final live in participants)
|
||||||
SpeakingRippleAvatar(
|
Padding(
|
||||||
live: live,
|
padding: const EdgeInsets.all(8),
|
||||||
size: 72,
|
child: SpeakingRippleAvatar(live: live, size: 72),
|
||||||
).padding(horizontal: 4),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show all participants in a responsive grid
|
if (callState.viewMode == ViewMode.stage) {
|
||||||
return LayoutBuilder(
|
// Stage: allow user to select a participant to focus, show others below
|
||||||
builder: (context, constraints) {
|
return CallStageView(
|
||||||
// Calculate width for responsive 2-column layout
|
participants: participants,
|
||||||
final itemWidth = (constraints.maxWidth / 2) - 16;
|
outerMaxHeight: outerMaxHeight,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Grid: show all participants in a responsive grid
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Calculate width for responsive 2-column layout
|
||||||
|
final itemWidth = (constraints.maxWidth / 2) - 16;
|
||||||
|
|
||||||
return Wrap(
|
return SingleChildScrollView(
|
||||||
alignment: WrapAlignment.center,
|
child: Wrap(
|
||||||
runAlignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
spacing: 8,
|
runAlignment: WrapAlignment.center,
|
||||||
runSpacing: 8,
|
spacing: 8,
|
||||||
children: [
|
runSpacing: 8,
|
||||||
for (final participant in participants)
|
children: [
|
||||||
SizedBox(
|
for (final participant in participants)
|
||||||
width: itemWidth,
|
SizedBox(
|
||||||
child: CallParticipantTile(live: participant),
|
width: itemWidth,
|
||||||
),
|
child: CallParticipantTile(
|
||||||
],
|
live: participant,
|
||||||
);
|
allTiles: true,
|
||||||
},
|
),
|
||||||
);
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import 'package:livekit_client/livekit_client.dart';
|
|||||||
|
|
||||||
class CallControlsBar extends HookConsumerWidget {
|
class CallControlsBar extends HookConsumerWidget {
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
const CallControlsBar({super.key, this.isCompact = false});
|
final bool popOnLeaves;
|
||||||
|
const CallControlsBar({
|
||||||
|
super.key,
|
||||||
|
this.isCompact = false,
|
||||||
|
this.popOnLeaves = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -41,91 +46,97 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
_buildCircularButtonWithDropdown(
|
_buildCircularButtonWithDropdown(
|
||||||
context: context,
|
context: context,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
icon:
|
icon: callState.isCameraEnabled
|
||||||
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
? Symbols.videocam
|
||||||
|
: Symbols.videocam_off,
|
||||||
onPressed: () => callNotifier.toggleCamera(),
|
onPressed: () => callNotifier.toggleCamera(),
|
||||||
backgroundColor: const Color(0xFF424242),
|
backgroundColor: const Color(0xFF424242),
|
||||||
hasDropdown: true,
|
hasDropdown: true,
|
||||||
deviceType: 'videoinput',
|
deviceType: 'videoinput',
|
||||||
),
|
),
|
||||||
_buildCircularButton(
|
_buildCircularButton(
|
||||||
icon:
|
icon: callState.isScreenSharing
|
||||||
callState.isScreenSharing
|
? Symbols.stop_screen_share
|
||||||
? Icons.stop_screen_share
|
: Symbols.screen_share,
|
||||||
: Icons.screen_share,
|
|
||||||
onPressed: () => callNotifier.toggleScreenShare(context),
|
onPressed: () => callNotifier.toggleScreenShare(context),
|
||||||
backgroundColor: const Color(0xFF424242),
|
backgroundColor: const Color(0xFF424242),
|
||||||
),
|
),
|
||||||
_buildCircularButtonWithDropdown(
|
_buildCircularButtonWithDropdown(
|
||||||
context: context,
|
context: context,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
icon: callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
icon: callState.isMicrophoneEnabled ? Symbols.mic : Symbols.mic_off,
|
||||||
onPressed: () => callNotifier.toggleMicrophone(),
|
onPressed: () => callNotifier.toggleMicrophone(),
|
||||||
backgroundColor: const Color(0xFF424242),
|
backgroundColor: const Color(0xFF424242),
|
||||||
hasDropdown: true,
|
hasDropdown: true,
|
||||||
deviceType: 'audioinput',
|
deviceType: 'audioinput',
|
||||||
),
|
),
|
||||||
_buildCircularButton(
|
_buildCircularButton(
|
||||||
icon:
|
icon: callState.isSpeakerphone
|
||||||
callState.isSpeakerphone
|
? Symbols.mobile_speaker
|
||||||
? Symbols.mobile_speaker
|
: Symbols.ear_sound,
|
||||||
: Symbols.ear_sound,
|
|
||||||
onPressed: () => callNotifier.toggleSpeakerphone(),
|
onPressed: () => callNotifier.toggleSpeakerphone(),
|
||||||
backgroundColor: const Color(0xFF424242),
|
backgroundColor: const Color(0xFF424242),
|
||||||
),
|
),
|
||||||
|
_buildCircularButton(
|
||||||
|
icon: callState.viewMode == ViewMode.grid
|
||||||
|
? Symbols.grid_view
|
||||||
|
: Symbols.view_list,
|
||||||
|
onPressed: () => callNotifier.toggleViewMode(),
|
||||||
|
backgroundColor: const Color(0xFF424242),
|
||||||
|
),
|
||||||
_buildCircularButton(
|
_buildCircularButton(
|
||||||
icon: Icons.call_end,
|
icon: Icons.call_end,
|
||||||
onPressed:
|
onPressed: () => showModalBottomSheet(
|
||||||
() => showModalBottomSheet(
|
context: context,
|
||||||
context: context,
|
isScrollControlled: true,
|
||||||
isScrollControlled: true,
|
useRootNavigator: true,
|
||||||
useRootNavigator: true,
|
builder: (innerContext) => Column(
|
||||||
builder:
|
mainAxisSize: MainAxisSize.min,
|
||||||
(innerContext) => Column(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
const Gap(24),
|
||||||
children: [
|
ListTile(
|
||||||
const Gap(24),
|
leading: const Icon(Symbols.logout, fill: 1),
|
||||||
ListTile(
|
title: Text('callLeave').tr(),
|
||||||
leading: const Icon(Symbols.logout, fill: 1),
|
onTap: () {
|
||||||
title: Text('callLeave').tr(),
|
callNotifier.disconnect();
|
||||||
onTap: () {
|
if (popOnLeaves) {
|
||||||
callNotifier.disconnect();
|
if (Navigator.of(context).canPop()) {
|
||||||
if (Navigator.of(context).canPop()) {
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
}
|
||||||
}
|
Navigator.of(innerContext).pop();
|
||||||
Navigator.of(innerContext).pop();
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.call_end, fill: 1),
|
leading: const Icon(Symbols.call_end, fill: 1),
|
||||||
iconColor: Colors.red,
|
iconColor: Colors.red,
|
||||||
title: Text('callEnd').tr(),
|
title: Text('callEnd').tr(),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
try {
|
try {
|
||||||
showLoadingModal(context);
|
showLoadingModal(context);
|
||||||
await apiClient.delete(
|
await apiClient.delete(
|
||||||
'/sphere/chat/realtime/${callNotifier.roomId}',
|
'/sphere/chat/realtime/${callNotifier.roomId}',
|
||||||
);
|
);
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
if (context.mounted) {
|
if (context.mounted && popOnLeaves) {
|
||||||
if (Navigator.of(context).canPop()) {
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) hideLoadingModal(context);
|
if (context.mounted) hideLoadingModal(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xFFE53E3E),
|
backgroundColor: const Color(0xFFE53E3E),
|
||||||
iconColor: Colors.white,
|
iconColor: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -185,12 +196,11 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: isCompact ? 0 : -4,
|
right: isCompact ? 0 : -4,
|
||||||
child: Material(
|
child: Material(
|
||||||
color:
|
color: Colors
|
||||||
Colors
|
.transparent, // Make Material transparent to show underlying color
|
||||||
.transparent, // Make Material transparent to show underlying color
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap:
|
onTap: () =>
|
||||||
() => _showDeviceSelectionDialog(context, ref, deviceType),
|
_showDeviceSelectionDialog(context, ref, deviceType),
|
||||||
borderRadius: BorderRadius.circular((isCompact ? 16 : 24) / 2),
|
borderRadius: BorderRadius.circular((isCompact ? 16 : 24) / 2),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: isCompact ? 16 : 24,
|
width: isCompact ? 16 : 24,
|
||||||
@@ -232,10 +242,9 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext dialogContext) {
|
builder: (BuildContext dialogContext) {
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText:
|
titleText: deviceType == 'videoinput'
|
||||||
deviceType == 'videoinput'
|
? 'selectCamera'.tr()
|
||||||
? 'selectCamera'.tr()
|
: 'selectMicrophone'.tr(),
|
||||||
: 'selectMicrophone'.tr(),
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: devices.length,
|
itemCount: devices.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -434,30 +443,23 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
) {
|
) {
|
||||||
final lastSpeaker =
|
final lastSpeaker =
|
||||||
callNotifier.participants
|
callNotifier.participants
|
||||||
.where(
|
.where((element) => element.remoteParticipant.lastSpokeAt != null)
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
.isEmpty
|
||||||
)
|
? callNotifier.participants.firstOrNull
|
||||||
.isEmpty
|
: callNotifier.participants
|
||||||
? callNotifier.participants.firstOrNull
|
.where((element) => element.remoteParticipant.lastSpokeAt != null)
|
||||||
: callNotifier.participants
|
.fold(
|
||||||
.where(
|
callNotifier.participants.firstOrNull,
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(value, element) =>
|
||||||
)
|
element.remoteParticipant.lastSpokeAt != null &&
|
||||||
.fold(
|
(value?.remoteParticipant.lastSpokeAt == null ||
|
||||||
callNotifier.participants.firstOrNull,
|
element.remoteParticipant.lastSpokeAt!.compareTo(
|
||||||
(value, element) =>
|
value!.remoteParticipant.lastSpokeAt!,
|
||||||
element.remoteParticipant.lastSpokeAt != null &&
|
) >
|
||||||
(value?.remoteParticipant.lastSpokeAt == null ||
|
0)
|
||||||
element.remoteParticipant.lastSpokeAt!
|
? element
|
||||||
.compareTo(
|
: value,
|
||||||
value!
|
);
|
||||||
.remoteParticipant
|
|
||||||
.lastSpokeAt!,
|
|
||||||
) >
|
|
||||||
0)
|
|
||||||
? element
|
|
||||||
: value,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastSpeaker == null) {
|
if (lastSpeaker == null) {
|
||||||
return const SizedBox.shrink(key: ValueKey('active_waiting'));
|
return const SizedBox.shrink(key: ValueKey('active_waiting'));
|
||||||
@@ -488,16 +490,15 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
openColor: Theme.of(context).scaffoldBackgroundColor,
|
openColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
middleColor: Theme.of(context).scaffoldBackgroundColor,
|
middleColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
openBuilder: (context, action) => CallScreen(room: room),
|
openBuilder: (context, action) => CallScreen(room: room),
|
||||||
closedBuilder:
|
closedBuilder: (context, openContainer) => IconButton(
|
||||||
(context, openContainer) => IconButton(
|
visualDensity: const VisualDensity(
|
||||||
visualDensity: const VisualDensity(
|
horizontal: -4,
|
||||||
horizontal: -4,
|
vertical: -4,
|
||||||
vertical: -4,
|
),
|
||||||
),
|
icon: const Icon(Icons.fullscreen),
|
||||||
icon: const Icon(Icons.fullscreen),
|
onPressed: openContainer,
|
||||||
onPressed: openContainer,
|
tooltip: 'Full Screen',
|
||||||
tooltip: 'Full Screen',
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
@@ -512,10 +513,10 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
).padding(horizontal: 12, vertical: 8),
|
).padding(horizontal: 12, vertical: 8),
|
||||||
// Video Preview
|
// Video Preview
|
||||||
Container(
|
Container(
|
||||||
height: 200,
|
height: 320,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
child: const CallContent(),
|
child: const CallContent(outerMaxHeight: 320),
|
||||||
),
|
),
|
||||||
const CallControlsBar(
|
const CallControlsBar(
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
@@ -540,11 +541,10 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child:
|
child: SpeakingRippleAvatar(
|
||||||
SpeakingRippleAvatar(
|
live: lastSpeaker,
|
||||||
live: lastSpeaker,
|
size: 36,
|
||||||
size: 36,
|
).center(),
|
||||||
).center(),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -83,24 +83,21 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: const BoxDecoration(shape: BoxShape.circle),
|
decoration: const BoxDecoration(shape: BoxShape.circle),
|
||||||
child: account.when(
|
child: account.when(
|
||||||
data:
|
data: (value) => CallParticipantGestureDetector(
|
||||||
(value) => CallParticipantGestureDetector(
|
participant: live,
|
||||||
participant: live,
|
child: ProfilePictureWidget(
|
||||||
child: ProfilePictureWidget(
|
file: value.profile.picture,
|
||||||
file: value.profile.picture,
|
radius: size / 2,
|
||||||
radius: size / 2,
|
),
|
||||||
),
|
),
|
||||||
),
|
error: (_, _) => CircleAvatar(
|
||||||
error:
|
radius: size / 2,
|
||||||
(_, _) => CircleAvatar(
|
child: const Icon(Symbols.question_mark),
|
||||||
radius: size / 2,
|
),
|
||||||
child: const Icon(Symbols.person_remove),
|
loading: () => CircleAvatar(
|
||||||
),
|
radius: size / 2,
|
||||||
loading:
|
child: CircularProgressIndicator(),
|
||||||
() => CircleAvatar(
|
),
|
||||||
radius: size / 2,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (live.remoteParticipant.isMuted)
|
if (live.remoteParticipant.isMuted)
|
||||||
@@ -130,12 +127,20 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
|
|
||||||
class CallParticipantTile extends HookConsumerWidget {
|
class CallParticipantTile extends HookConsumerWidget {
|
||||||
final CallParticipantLive live;
|
final CallParticipantLive live;
|
||||||
|
final bool allTiles;
|
||||||
|
final double radius;
|
||||||
|
|
||||||
const CallParticipantTile({super.key, required this.live});
|
const CallParticipantTile({
|
||||||
|
super.key,
|
||||||
|
required this.live,
|
||||||
|
this.allTiles = false,
|
||||||
|
this.radius = 48,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final userInfo = ref.watch(accountProvider(live.participant.name));
|
final userInfo = ref.watch(accountProvider(live.participant.name));
|
||||||
|
final account = ref.watch(accountProvider(live.participant.identity));
|
||||||
|
|
||||||
final hasVideo =
|
final hasVideo =
|
||||||
live.hasVideo &&
|
live.hasVideo &&
|
||||||
@@ -143,7 +148,7 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
|
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
if (hasVideo) {
|
if (hasVideo || allTiles) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
@@ -166,12 +171,11 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color:
|
color: isSpeaking
|
||||||
isSpeaking
|
? Colors.green.withOpacity(
|
||||||
? Colors.green.withOpacity(
|
0.5 + 0.5 * audioLevel.clamp(0.0, 1.0),
|
||||||
0.5 + 0.5 * audioLevel.clamp(0.0, 1.0),
|
)
|
||||||
)
|
: Theme.of(context).colorScheme.outlineVariant,
|
||||||
: Theme.of(context).colorScheme.outlineVariant,
|
|
||||||
width: isSpeaking ? 4 : 1,
|
width: isSpeaking ? 4 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -182,14 +186,37 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
VideoTrackRenderer(
|
if (hasVideo)
|
||||||
live.remoteParticipant.trackPublications.values
|
VideoTrackRenderer(
|
||||||
.where((track) => track.kind == TrackType.VIDEO)
|
live.remoteParticipant.trackPublications.values
|
||||||
.first
|
.where(
|
||||||
.track
|
(track) => track.kind == TrackType.VIDEO,
|
||||||
as VideoTrack,
|
)
|
||||||
renderMode: VideoRenderMode.platformView,
|
.first
|
||||||
),
|
.track
|
||||||
|
as VideoTrack,
|
||||||
|
renderMode: VideoRenderMode.platformView,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Center(
|
||||||
|
child: account.when(
|
||||||
|
data: (value) => CallParticipantGestureDetector(
|
||||||
|
participant: live,
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
file: value.profile.picture,
|
||||||
|
radius: radius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (_, _) => CircleAvatar(
|
||||||
|
radius: radius,
|
||||||
|
child: const Icon(Symbols.question_mark),
|
||||||
|
),
|
||||||
|
loading: () => CircleAvatar(
|
||||||
|
radius: radius,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 8,
|
left: 8,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class RepliesNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$repliesNotifierHash() => r'2fa51bc3b8cc640e68fa316f61d00f8a0a3740ed';
|
String _$repliesNotifierHash() => r'fcaea9b502b1d713a8084da022a03e86d67acc1a';
|
||||||
|
|
||||||
final class RepliesNotifierFamily extends $Family
|
final class RepliesNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
Reference in New Issue
Block a user