feat(videocall): integrate videocall feature with architecture refactor
Merges videocall stash, resolves conflicts with installed_apps routes, and refactors UI to match current legacy patterns: - Replace themePortProvider with context.sfColors and Theme.of(context) - Replace showTopSnackbar with feedback dialogs - Replace hardcoded colors with theme-aware colorScheme - Wrap test login button in kDebugMode - Rename error enum values to be more descriptive
This commit is contained in:
@@ -225,6 +225,11 @@ void configureAppRouter() {
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'videocall',
|
||||
name: 'videocall',
|
||||
pageBuilder: const VideocallBuilder().buildPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -14,3 +14,4 @@ export 'src/features/call_history/call_history_builder.dart';
|
||||
export 'src/features/background_image/background_image_builder.dart';
|
||||
export 'src/features/installed_apps/installed_apps_builder.dart';
|
||||
export 'src/features/app_usage_schedules/app_usage_schedules_builder.dart';
|
||||
export 'src/features/videocall/videocall_builder.dart';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
abstract class VideocallSignalingDatasource {
|
||||
Future<void> initiateCall({
|
||||
required String deviceId,
|
||||
required String appAccount,
|
||||
required String roomNumber,
|
||||
});
|
||||
|
||||
Future<void> cancelCall({required String deviceId});
|
||||
|
||||
Future<void> refuseCall({
|
||||
required String deviceId,
|
||||
required String roomNumber,
|
||||
});
|
||||
|
||||
Future<int> getRoomParticipantCount({required String roomNumber});
|
||||
|
||||
Future<void> reportParticipantCount({
|
||||
required String roomNumber,
|
||||
required int count,
|
||||
required int type,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
import 'videocall_signaling_datasource.dart';
|
||||
|
||||
class VideocallSignalingDatasourceImpl implements VideocallSignalingDatasource {
|
||||
VideocallSignalingDatasourceImpl(this._repository);
|
||||
|
||||
final SaveFamilyRepository _repository;
|
||||
|
||||
@override
|
||||
Future<void> initiateCall({
|
||||
required String deviceId,
|
||||
required String appAccount,
|
||||
required String roomNumber,
|
||||
}) async {
|
||||
// TODO: Implement when backend API spec is available
|
||||
// await _repository.post('/devices/$deviceId/videocall/initiate', body: {...});
|
||||
throw UnimplementedError(
|
||||
'Backend signaling API not yet available. Waiting for endpoint spec.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelCall({required String deviceId}) async {
|
||||
// TODO: Implement when backend API spec is available
|
||||
throw UnimplementedError(
|
||||
'Backend signaling API not yet available. Waiting for endpoint spec.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refuseCall({
|
||||
required String deviceId,
|
||||
required String roomNumber,
|
||||
}) async {
|
||||
// TODO: Implement when backend API spec is available
|
||||
throw UnimplementedError(
|
||||
'Backend signaling API not yet available. Waiting for endpoint spec.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getRoomParticipantCount({required String roomNumber}) async {
|
||||
// TODO: Implement when backend API spec is available
|
||||
throw UnimplementedError(
|
||||
'Backend signaling API not yet available. Waiting for endpoint spec.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportParticipantCount({
|
||||
required String roomNumber,
|
||||
required int count,
|
||||
required int type,
|
||||
}) async {
|
||||
// TODO: Implement when backend API spec is available
|
||||
throw UnimplementedError(
|
||||
'Backend signaling API not yet available. Waiting for endpoint spec.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import '../../domain/repositories/videocall_signaling_repository.dart';
|
||||
import '../datasources/videocall_signaling_datasource.dart';
|
||||
|
||||
class VideocallSignalingRepositoryImpl implements VideocallSignalingRepository {
|
||||
VideocallSignalingRepositoryImpl(this._datasource);
|
||||
|
||||
final VideocallSignalingDatasource _datasource;
|
||||
|
||||
@override
|
||||
Future<void> initiateCall({
|
||||
required String deviceId,
|
||||
required String appAccount,
|
||||
required String roomNumber,
|
||||
}) {
|
||||
return _datasource.initiateCall(
|
||||
deviceId: deviceId,
|
||||
appAccount: appAccount,
|
||||
roomNumber: roomNumber,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelCall({required String deviceId}) {
|
||||
return _datasource.cancelCall(deviceId: deviceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refuseCall({
|
||||
required String deviceId,
|
||||
required String roomNumber,
|
||||
}) {
|
||||
return _datasource.refuseCall(deviceId: deviceId, roomNumber: roomNumber);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getRoomParticipantCount({required String roomNumber}) {
|
||||
return _datasource.getRoomParticipantCount(roomNumber: roomNumber);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportParticipantCount({
|
||||
required String roomNumber,
|
||||
required int count,
|
||||
required int type,
|
||||
}) {
|
||||
return _datasource.reportParticipantCount(
|
||||
roomNumber: roomNumber,
|
||||
count: count,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
enum VideocallErrorEvent {
|
||||
sdkInitialization,
|
||||
authentication,
|
||||
callStart,
|
||||
callAnswer,
|
||||
network,
|
||||
cameraPermission,
|
||||
microphonePermission,
|
||||
generic,
|
||||
}
|
||||
|
||||
enum VideocallSuccessEvent {
|
||||
connected,
|
||||
callEnded,
|
||||
}
|
||||
|
||||
enum VideocallScreenMode {
|
||||
idle,
|
||||
outgoing,
|
||||
incoming,
|
||||
inCall,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:videocall_sdk/videocall_sdk.dart';
|
||||
|
||||
part 'videocall_participant.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class VideocallParticipant with _$VideocallParticipant {
|
||||
const factory VideocallParticipant({
|
||||
required String participantId,
|
||||
required String userId,
|
||||
@Default(false) bool isAudio,
|
||||
@Default(false) bool isVideo,
|
||||
@Default(false) bool isSelf,
|
||||
JCMediaDeviceVideoCanvas? videoCanvas,
|
||||
}) = _VideocallParticipant;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'videocall_participant.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$VideocallParticipant {
|
||||
|
||||
String get participantId; String get userId; bool get isAudio; bool get isVideo; bool get isSelf; JCMediaDeviceVideoCanvas? get videoCanvas;
|
||||
/// Create a copy of VideocallParticipant
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$VideocallParticipantCopyWith<VideocallParticipant> get copyWith => _$VideocallParticipantCopyWithImpl<VideocallParticipant>(this as VideocallParticipant, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is VideocallParticipant&&(identical(other.participantId, participantId) || other.participantId == participantId)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.isAudio, isAudio) || other.isAudio == isAudio)&&(identical(other.isVideo, isVideo) || other.isVideo == isVideo)&&(identical(other.isSelf, isSelf) || other.isSelf == isSelf)&&(identical(other.videoCanvas, videoCanvas) || other.videoCanvas == videoCanvas));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,participantId,userId,isAudio,isVideo,isSelf,videoCanvas);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideocallParticipant(participantId: $participantId, userId: $userId, isAudio: $isAudio, isVideo: $isVideo, isSelf: $isSelf, videoCanvas: $videoCanvas)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $VideocallParticipantCopyWith<$Res> {
|
||||
factory $VideocallParticipantCopyWith(VideocallParticipant value, $Res Function(VideocallParticipant) _then) = _$VideocallParticipantCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String participantId, String userId, bool isAudio, bool isVideo, bool isSelf, JCMediaDeviceVideoCanvas? videoCanvas
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$VideocallParticipantCopyWithImpl<$Res>
|
||||
implements $VideocallParticipantCopyWith<$Res> {
|
||||
_$VideocallParticipantCopyWithImpl(this._self, this._then);
|
||||
|
||||
final VideocallParticipant _self;
|
||||
final $Res Function(VideocallParticipant) _then;
|
||||
|
||||
/// Create a copy of VideocallParticipant
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? participantId = null,Object? userId = null,Object? isAudio = null,Object? isVideo = null,Object? isSelf = null,Object? videoCanvas = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
participantId: null == participantId ? _self.participantId : participantId // ignore: cast_nullable_to_non_nullable
|
||||
as String,userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isAudio: null == isAudio ? _self.isAudio : isAudio // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isVideo: null == isVideo ? _self.isVideo : isVideo // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSelf: null == isSelf ? _self.isSelf : isSelf // ignore: cast_nullable_to_non_nullable
|
||||
as bool,videoCanvas: freezed == videoCanvas ? _self.videoCanvas : videoCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [VideocallParticipant].
|
||||
extension VideocallParticipantPatterns on VideocallParticipant {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _VideocallParticipant value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallParticipant() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _VideocallParticipant value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallParticipant():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _VideocallParticipant value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallParticipant() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String participantId, String userId, bool isAudio, bool isVideo, bool isSelf, JCMediaDeviceVideoCanvas? videoCanvas)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallParticipant() when $default != null:
|
||||
return $default(_that.participantId,_that.userId,_that.isAudio,_that.isVideo,_that.isSelf,_that.videoCanvas);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String participantId, String userId, bool isAudio, bool isVideo, bool isSelf, JCMediaDeviceVideoCanvas? videoCanvas) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallParticipant():
|
||||
return $default(_that.participantId,_that.userId,_that.isAudio,_that.isVideo,_that.isSelf,_that.videoCanvas);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String participantId, String userId, bool isAudio, bool isVideo, bool isSelf, JCMediaDeviceVideoCanvas? videoCanvas)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallParticipant() when $default != null:
|
||||
return $default(_that.participantId,_that.userId,_that.isAudio,_that.isVideo,_that.isSelf,_that.videoCanvas);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _VideocallParticipant implements VideocallParticipant {
|
||||
const _VideocallParticipant({required this.participantId, required this.userId, this.isAudio = false, this.isVideo = false, this.isSelf = false, this.videoCanvas});
|
||||
|
||||
|
||||
@override final String participantId;
|
||||
@override final String userId;
|
||||
@override@JsonKey() final bool isAudio;
|
||||
@override@JsonKey() final bool isVideo;
|
||||
@override@JsonKey() final bool isSelf;
|
||||
@override final JCMediaDeviceVideoCanvas? videoCanvas;
|
||||
|
||||
/// Create a copy of VideocallParticipant
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$VideocallParticipantCopyWith<_VideocallParticipant> get copyWith => __$VideocallParticipantCopyWithImpl<_VideocallParticipant>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideocallParticipant&&(identical(other.participantId, participantId) || other.participantId == participantId)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.isAudio, isAudio) || other.isAudio == isAudio)&&(identical(other.isVideo, isVideo) || other.isVideo == isVideo)&&(identical(other.isSelf, isSelf) || other.isSelf == isSelf)&&(identical(other.videoCanvas, videoCanvas) || other.videoCanvas == videoCanvas));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,participantId,userId,isAudio,isVideo,isSelf,videoCanvas);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideocallParticipant(participantId: $participantId, userId: $userId, isAudio: $isAudio, isVideo: $isVideo, isSelf: $isSelf, videoCanvas: $videoCanvas)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$VideocallParticipantCopyWith<$Res> implements $VideocallParticipantCopyWith<$Res> {
|
||||
factory _$VideocallParticipantCopyWith(_VideocallParticipant value, $Res Function(_VideocallParticipant) _then) = __$VideocallParticipantCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String participantId, String userId, bool isAudio, bool isVideo, bool isSelf, JCMediaDeviceVideoCanvas? videoCanvas
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$VideocallParticipantCopyWithImpl<$Res>
|
||||
implements _$VideocallParticipantCopyWith<$Res> {
|
||||
__$VideocallParticipantCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _VideocallParticipant _self;
|
||||
final $Res Function(_VideocallParticipant) _then;
|
||||
|
||||
/// Create a copy of VideocallParticipant
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? participantId = null,Object? userId = null,Object? isAudio = null,Object? isVideo = null,Object? isSelf = null,Object? videoCanvas = freezed,}) {
|
||||
return _then(_VideocallParticipant(
|
||||
participantId: null == participantId ? _self.participantId : participantId // ignore: cast_nullable_to_non_nullable
|
||||
as String,userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isAudio: null == isAudio ? _self.isAudio : isAudio // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isVideo: null == isVideo ? _self.isVideo : isVideo // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSelf: null == isSelf ? _self.isSelf : isSelf // ignore: cast_nullable_to_non_nullable
|
||||
as bool,videoCanvas: freezed == videoCanvas ? _self.videoCanvas : videoCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,22 @@
|
||||
abstract class VideocallSignalingRepository {
|
||||
Future<void> initiateCall({
|
||||
required String deviceId,
|
||||
required String appAccount,
|
||||
required String roomNumber,
|
||||
});
|
||||
|
||||
Future<void> cancelCall({required String deviceId});
|
||||
|
||||
Future<void> refuseCall({
|
||||
required String deviceId,
|
||||
required String roomNumber,
|
||||
});
|
||||
|
||||
Future<int> getRoomParticipantCount({required String roomNumber});
|
||||
|
||||
Future<void> reportParticipantCount({
|
||||
required String roomNumber,
|
||||
required int count,
|
||||
required int type,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:videocall_sdk/videocall_sdk.dart';
|
||||
|
||||
import '../../domain/entities/videocall_error.dart';
|
||||
import '../../domain/entities/videocall_participant.dart';
|
||||
import 'group_call_view_state.dart';
|
||||
|
||||
final groupCallViewModelProvider =
|
||||
NotifierProvider.autoDispose<GroupCallViewModel, GroupCallViewState>(
|
||||
GroupCallViewModel.new,
|
||||
);
|
||||
|
||||
class GroupCallViewModel extends Notifier<GroupCallViewState> {
|
||||
late final VideocallChannelService _channelService;
|
||||
late final VideocallDeviceService _deviceService;
|
||||
|
||||
StreamSubscription<({bool result, int reason, String channelId})>? _joinSub;
|
||||
StreamSubscription<({int reason, String channelId})>? _leaveSub;
|
||||
StreamSubscription<JCMediaChannelParticipant>? _participantJoinSub;
|
||||
StreamSubscription<JCMediaChannelParticipant>? _participantLeftSub;
|
||||
StreamSubscription<
|
||||
({
|
||||
JCMediaChannelParticipant participant,
|
||||
ChannelChangeParam changeParam,
|
||||
})>?
|
||||
_participantUpdateSub;
|
||||
|
||||
@override
|
||||
GroupCallViewState build() {
|
||||
_channelService = GetIt.I<VideocallChannelService>();
|
||||
_deviceService = GetIt.I<VideocallDeviceService>();
|
||||
|
||||
ref.onDispose(_disposeSubscriptions);
|
||||
_subscribeToStreams();
|
||||
|
||||
return const GroupCallViewState();
|
||||
}
|
||||
|
||||
void _subscribeToStreams() {
|
||||
_joinSub = _channelService.joinStream.listen(_onJoin);
|
||||
_leaveSub = _channelService.leaveStream.listen(_onLeave);
|
||||
_participantJoinSub =
|
||||
_channelService.participantJoinStream.listen(_onParticipantJoin);
|
||||
_participantLeftSub =
|
||||
_channelService.participantLeftStream.listen(_onParticipantLeft);
|
||||
_participantUpdateSub =
|
||||
_channelService.participantUpdateStream.listen(_onParticipantUpdate);
|
||||
}
|
||||
|
||||
// -- Channel actions --
|
||||
|
||||
Future<void> joinChannel(String channelId) async {
|
||||
await _channelService.enableUploadAudioStream(true);
|
||||
await _channelService.enableUploadVideoStream(true);
|
||||
|
||||
final ok = await _channelService.join(channelId);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (!ok) {
|
||||
state = state.copyWith(errorEvent: VideocallErrorEvent.callStart);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(channelId: channelId);
|
||||
}
|
||||
|
||||
Future<void> leaveChannel() async {
|
||||
await _channelService.leave();
|
||||
}
|
||||
|
||||
Future<void> stopChannel() async {
|
||||
await _channelService.stop();
|
||||
}
|
||||
|
||||
// -- Media controls --
|
||||
|
||||
Future<void> toggleMic() async {
|
||||
final newValue = !state.isMicEnabled;
|
||||
await _channelService.enableUploadAudioStream(newValue);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isMicEnabled: newValue);
|
||||
}
|
||||
|
||||
Future<void> toggleCamera() async {
|
||||
final newValue = !state.isCameraEnabled;
|
||||
await _channelService.enableUploadVideoStream(newValue);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isCameraEnabled: newValue);
|
||||
}
|
||||
|
||||
Future<void> switchCamera() async {
|
||||
await _deviceService.switchCamera();
|
||||
}
|
||||
|
||||
// -- Stream callbacks --
|
||||
|
||||
Future<void> _onJoin(
|
||||
({bool result, int reason, String channelId}) event) async {
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (!event.result) {
|
||||
state = state.copyWith(errorEvent: VideocallErrorEvent.callStart);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(isJoined: true, channelId: event.channelId);
|
||||
|
||||
// Start local video
|
||||
final selfParticipant = await _channelService.getSelfParticipant();
|
||||
if (selfParticipant == null || !ref.mounted) return;
|
||||
|
||||
final localCanvas = await selfParticipant.startVideo(
|
||||
JCMediaDevice.RENDER_FULL_CONTENT,
|
||||
JCMediaChannel.PICTURESIZE_LARGE,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(localCanvas: localCanvas);
|
||||
}
|
||||
|
||||
void _onLeave(({int reason, String channelId}) event) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isJoined: false,
|
||||
participants: [],
|
||||
localCanvas: null,
|
||||
successEvent: VideocallSuccessEvent.callEnded,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onParticipantJoin(
|
||||
JCMediaChannelParticipant participant) async {
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final isSelf = await participant.isSelf();
|
||||
final userId = await participant.getUserId();
|
||||
final isAudio = await participant.isAudio();
|
||||
final isVideo = await participant.isVideo();
|
||||
|
||||
JCMediaDeviceVideoCanvas? canvas;
|
||||
if (isVideo) {
|
||||
canvas = await participant.startVideo(
|
||||
JCMediaDevice.RENDER_FULL_CONTENT,
|
||||
JCMediaChannel.PICTURESIZE_LARGE,
|
||||
);
|
||||
await _channelService.requestVideo(
|
||||
participant, JCMediaChannel.PICTURESIZE_LARGE);
|
||||
}
|
||||
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final newParticipant = VideocallParticipant(
|
||||
participantId: participant.participantId,
|
||||
userId: userId,
|
||||
isAudio: isAudio,
|
||||
isVideo: isVideo,
|
||||
isSelf: isSelf,
|
||||
videoCanvas: canvas,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
participants: [...state.participants, newParticipant],
|
||||
);
|
||||
}
|
||||
|
||||
void _onParticipantLeft(JCMediaChannelParticipant participant) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
participants: state.participants
|
||||
.where((p) => p.participantId != participant.participantId)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _onParticipantUpdate(
|
||||
({
|
||||
JCMediaChannelParticipant participant,
|
||||
ChannelChangeParam changeParam,
|
||||
}) event) {
|
||||
if (!ref.mounted) return;
|
||||
// Update participant in list if needed
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(errorEvent: null);
|
||||
}
|
||||
|
||||
void _disposeSubscriptions() {
|
||||
_joinSub?.cancel();
|
||||
_leaveSub?.cancel();
|
||||
_participantJoinSub?.cancel();
|
||||
_participantLeftSub?.cancel();
|
||||
_participantUpdateSub?.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:videocall_sdk/videocall_sdk.dart';
|
||||
|
||||
import '../../domain/entities/videocall_error.dart';
|
||||
import '../../domain/entities/videocall_participant.dart';
|
||||
|
||||
part 'group_call_view_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class GroupCallViewState with _$GroupCallViewState {
|
||||
const factory GroupCallViewState({
|
||||
@Default('') String channelId,
|
||||
@Default(false) bool isJoined,
|
||||
@Default(true) bool isMicEnabled,
|
||||
@Default(true) bool isCameraEnabled,
|
||||
@Default([]) List<VideocallParticipant> participants,
|
||||
JCMediaDeviceVideoCanvas? localCanvas,
|
||||
VideocallErrorEvent? errorEvent,
|
||||
VideocallSuccessEvent? successEvent,
|
||||
}) = _GroupCallViewState;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'group_call_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$GroupCallViewState {
|
||||
|
||||
String get channelId; bool get isJoined; bool get isMicEnabled; bool get isCameraEnabled; List<VideocallParticipant> get participants; JCMediaDeviceVideoCanvas? get localCanvas; VideocallErrorEvent? get errorEvent; VideocallSuccessEvent? get successEvent;
|
||||
/// Create a copy of GroupCallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$GroupCallViewStateCopyWith<GroupCallViewState> get copyWith => _$GroupCallViewStateCopyWithImpl<GroupCallViewState>(this as GroupCallViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is GroupCallViewState&&(identical(other.channelId, channelId) || other.channelId == channelId)&&(identical(other.isJoined, isJoined) || other.isJoined == isJoined)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&const DeepCollectionEquality().equals(other.participants, participants)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,channelId,isJoined,isMicEnabled,isCameraEnabled,const DeepCollectionEquality().hash(participants),localCanvas,errorEvent,successEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GroupCallViewState(channelId: $channelId, isJoined: $isJoined, isMicEnabled: $isMicEnabled, isCameraEnabled: $isCameraEnabled, participants: $participants, localCanvas: $localCanvas, errorEvent: $errorEvent, successEvent: $successEvent)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $GroupCallViewStateCopyWith<$Res> {
|
||||
factory $GroupCallViewStateCopyWith(GroupCallViewState value, $Res Function(GroupCallViewState) _then) = _$GroupCallViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String channelId, bool isJoined, bool isMicEnabled, bool isCameraEnabled, List<VideocallParticipant> participants, JCMediaDeviceVideoCanvas? localCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$GroupCallViewStateCopyWithImpl<$Res>
|
||||
implements $GroupCallViewStateCopyWith<$Res> {
|
||||
_$GroupCallViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final GroupCallViewState _self;
|
||||
final $Res Function(GroupCallViewState) _then;
|
||||
|
||||
/// Create a copy of GroupCallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? channelId = null,Object? isJoined = null,Object? isMicEnabled = null,Object? isCameraEnabled = null,Object? participants = null,Object? localCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
channelId: null == channelId ? _self.channelId : channelId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isJoined: null == isJoined ? _self.isJoined : isJoined // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isMicEnabled: null == isMicEnabled ? _self.isMicEnabled : isMicEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,participants: null == participants ? _self.participants : participants // ignore: cast_nullable_to_non_nullable
|
||||
as List<VideocallParticipant>,localCanvas: freezed == localCanvas ? _self.localCanvas : localCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallSuccessEvent?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [GroupCallViewState].
|
||||
extension GroupCallViewStatePatterns on GroupCallViewState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _GroupCallViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _GroupCallViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _GroupCallViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _GroupCallViewState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _GroupCallViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _GroupCallViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String channelId, bool isJoined, bool isMicEnabled, bool isCameraEnabled, List<VideocallParticipant> participants, JCMediaDeviceVideoCanvas? localCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _GroupCallViewState() when $default != null:
|
||||
return $default(_that.channelId,_that.isJoined,_that.isMicEnabled,_that.isCameraEnabled,_that.participants,_that.localCanvas,_that.errorEvent,_that.successEvent);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String channelId, bool isJoined, bool isMicEnabled, bool isCameraEnabled, List<VideocallParticipant> participants, JCMediaDeviceVideoCanvas? localCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _GroupCallViewState():
|
||||
return $default(_that.channelId,_that.isJoined,_that.isMicEnabled,_that.isCameraEnabled,_that.participants,_that.localCanvas,_that.errorEvent,_that.successEvent);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String channelId, bool isJoined, bool isMicEnabled, bool isCameraEnabled, List<VideocallParticipant> participants, JCMediaDeviceVideoCanvas? localCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _GroupCallViewState() when $default != null:
|
||||
return $default(_that.channelId,_that.isJoined,_that.isMicEnabled,_that.isCameraEnabled,_that.participants,_that.localCanvas,_that.errorEvent,_that.successEvent);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _GroupCallViewState implements GroupCallViewState {
|
||||
const _GroupCallViewState({this.channelId = '', this.isJoined = false, this.isMicEnabled = true, this.isCameraEnabled = true, final List<VideocallParticipant> participants = const [], this.localCanvas, this.errorEvent, this.successEvent}): _participants = participants;
|
||||
|
||||
|
||||
@override@JsonKey() final String channelId;
|
||||
@override@JsonKey() final bool isJoined;
|
||||
@override@JsonKey() final bool isMicEnabled;
|
||||
@override@JsonKey() final bool isCameraEnabled;
|
||||
final List<VideocallParticipant> _participants;
|
||||
@override@JsonKey() List<VideocallParticipant> get participants {
|
||||
if (_participants is EqualUnmodifiableListView) return _participants;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_participants);
|
||||
}
|
||||
|
||||
@override final JCMediaDeviceVideoCanvas? localCanvas;
|
||||
@override final VideocallErrorEvent? errorEvent;
|
||||
@override final VideocallSuccessEvent? successEvent;
|
||||
|
||||
/// Create a copy of GroupCallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$GroupCallViewStateCopyWith<_GroupCallViewState> get copyWith => __$GroupCallViewStateCopyWithImpl<_GroupCallViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GroupCallViewState&&(identical(other.channelId, channelId) || other.channelId == channelId)&&(identical(other.isJoined, isJoined) || other.isJoined == isJoined)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&const DeepCollectionEquality().equals(other._participants, _participants)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,channelId,isJoined,isMicEnabled,isCameraEnabled,const DeepCollectionEquality().hash(_participants),localCanvas,errorEvent,successEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GroupCallViewState(channelId: $channelId, isJoined: $isJoined, isMicEnabled: $isMicEnabled, isCameraEnabled: $isCameraEnabled, participants: $participants, localCanvas: $localCanvas, errorEvent: $errorEvent, successEvent: $successEvent)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$GroupCallViewStateCopyWith<$Res> implements $GroupCallViewStateCopyWith<$Res> {
|
||||
factory _$GroupCallViewStateCopyWith(_GroupCallViewState value, $Res Function(_GroupCallViewState) _then) = __$GroupCallViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String channelId, bool isJoined, bool isMicEnabled, bool isCameraEnabled, List<VideocallParticipant> participants, JCMediaDeviceVideoCanvas? localCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$GroupCallViewStateCopyWithImpl<$Res>
|
||||
implements _$GroupCallViewStateCopyWith<$Res> {
|
||||
__$GroupCallViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _GroupCallViewState _self;
|
||||
final $Res Function(_GroupCallViewState) _then;
|
||||
|
||||
/// Create a copy of GroupCallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? channelId = null,Object? isJoined = null,Object? isMicEnabled = null,Object? isCameraEnabled = null,Object? participants = null,Object? localCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) {
|
||||
return _then(_GroupCallViewState(
|
||||
channelId: null == channelId ? _self.channelId : channelId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isJoined: null == isJoined ? _self.isJoined : isJoined // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isMicEnabled: null == isMicEnabled ? _self.isMicEnabled : isMicEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,participants: null == participants ? _self._participants : participants // ignore: cast_nullable_to_non_nullable
|
||||
as List<VideocallParticipant>,localCanvas: freezed == localCanvas ? _self.localCanvas : localCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallSuccessEvent?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,270 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:videocall_sdk/videocall_sdk.dart';
|
||||
|
||||
import '../../domain/entities/videocall_error.dart';
|
||||
import 'videocall_view_state.dart';
|
||||
|
||||
final videocallViewModelProvider =
|
||||
NotifierProvider.autoDispose<VideocallViewModel, VideocallViewState>(
|
||||
VideocallViewModel.new,
|
||||
);
|
||||
|
||||
class VideocallViewModel extends Notifier<VideocallViewState> {
|
||||
late final VideocallSdkManager _manager;
|
||||
late final VideocallCallService _callService;
|
||||
late final VideocallDeviceService _deviceService;
|
||||
late final VideocallClient _client;
|
||||
|
||||
StreamSubscription<VideocallItem>? _callAddSub;
|
||||
StreamSubscription<VideocallItem>? _callUpdateSub;
|
||||
StreamSubscription<({int reason, String description})>? _callRemoveSub;
|
||||
StreamSubscription<VideocallItem>? _missedCallSub;
|
||||
StreamSubscription<VideocallClientState>? _clientStateSub;
|
||||
|
||||
@override
|
||||
VideocallViewState build() {
|
||||
_manager = GetIt.I<VideocallSdkManager>();
|
||||
_callService = GetIt.I<VideocallCallService>();
|
||||
_deviceService = GetIt.I<VideocallDeviceService>();
|
||||
_client = GetIt.I<VideocallClient>();
|
||||
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
final deviceId = device?.identificator ?? '';
|
||||
|
||||
ref.onDispose(_disposeSubscriptions);
|
||||
Future.microtask(_initSdk);
|
||||
|
||||
return VideocallViewState(deviceId: deviceId);
|
||||
}
|
||||
|
||||
Future<void> _initSdk() async {
|
||||
if (_manager.isInitialized) {
|
||||
_subscribeToStreams();
|
||||
state = state.copyWith(isSdkReady: true);
|
||||
return;
|
||||
}
|
||||
|
||||
final ok = await _manager.initialize();
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (!ok) {
|
||||
state = state.copyWith(errorEvent: VideocallErrorEvent.sdkInitialization);
|
||||
return;
|
||||
}
|
||||
|
||||
_subscribeToStreams();
|
||||
state = state.copyWith(isSdkReady: true);
|
||||
}
|
||||
|
||||
void _subscribeToStreams() {
|
||||
_callAddSub = _callService.callItemAddStream.listen(_onCallItemAdd);
|
||||
_callUpdateSub =
|
||||
_callService.callItemUpdateStream.listen(_onCallItemUpdate);
|
||||
_callRemoveSub =
|
||||
_callService.callItemRemoveStream.listen(_onCallItemRemove);
|
||||
_missedCallSub = _callService.missedCallStream.listen(_onMissedCall);
|
||||
_clientStateSub = _client.stateStream.listen(_onClientStateChange);
|
||||
}
|
||||
|
||||
// -- Auth --
|
||||
|
||||
Future<bool> login({
|
||||
required String userId,
|
||||
required String password,
|
||||
}) async {
|
||||
final ok = await _client.login(userId: userId, password: password);
|
||||
if (!ref.mounted) return false;
|
||||
if (!ok) {
|
||||
state = state.copyWith(errorEvent: VideocallErrorEvent.authentication);
|
||||
return false;
|
||||
}
|
||||
state = state.copyWith(localUserId: userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -- Call actions --
|
||||
|
||||
Future<void> startCall(String remoteUserId) async {
|
||||
state = state.copyWith(
|
||||
remoteUserId: remoteUserId,
|
||||
screenMode: VideocallScreenMode.outgoing,
|
||||
errorEvent: null,
|
||||
);
|
||||
|
||||
final ok = await _callService.startCall(
|
||||
userId: remoteUserId,
|
||||
isVideo: true,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (!ok) {
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.idle,
|
||||
errorEvent: VideocallErrorEvent.callStart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> answerCall() async {
|
||||
final ok = await _callService.answerCall(isVideo: true);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (!ok) {
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.idle,
|
||||
errorEvent: VideocallErrorEvent.callAnswer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> hangUp() async {
|
||||
await _callService.hangUp();
|
||||
if (!ref.mounted) return;
|
||||
await _stopVideoViews();
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.idle,
|
||||
currentCall: null,
|
||||
successEvent: VideocallSuccessEvent.callEnded,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> rejectCall() async {
|
||||
await _callService.hangUp();
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.idle,
|
||||
currentCall: null,
|
||||
);
|
||||
}
|
||||
|
||||
// -- Media controls --
|
||||
|
||||
Future<void> toggleMic() async {
|
||||
final item = await _callService.getActiveCallItem();
|
||||
if (item == null) return;
|
||||
final newValue = !state.isMicEnabled;
|
||||
await _callService.muteMicrophone(item, !newValue);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isMicEnabled: newValue);
|
||||
}
|
||||
|
||||
Future<void> toggleSpeaker() async {
|
||||
final newValue = !state.isSpeakerEnabled;
|
||||
await _deviceService.enableSpeaker(newValue);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isSpeakerEnabled: newValue);
|
||||
}
|
||||
|
||||
Future<void> switchCamera() async {
|
||||
await _deviceService.switchCamera();
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isFrontCamera: !state.isFrontCamera);
|
||||
}
|
||||
|
||||
// -- Video views --
|
||||
|
||||
Future<void> _startVideoViews() async {
|
||||
final localCanvas = await _callService.startLocalVideo();
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(localCanvas: localCanvas);
|
||||
|
||||
final remoteCanvas = await _callService.startRemoteVideo();
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
remoteCanvas: remoteCanvas,
|
||||
isRemoteVideoAvailable: remoteCanvas != null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _stopVideoViews() async {
|
||||
await _callService.stopLocalVideo();
|
||||
await _callService.stopRemoteVideo();
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
localCanvas: null,
|
||||
remoteCanvas: null,
|
||||
isRemoteVideoAvailable: false,
|
||||
);
|
||||
}
|
||||
|
||||
// -- Stream callbacks --
|
||||
|
||||
void _onCallItemAdd(VideocallItem item) {
|
||||
if (!ref.mounted) return;
|
||||
if (item.direction == CallDirection.incoming) {
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.incoming,
|
||||
currentCall: item,
|
||||
remoteUserId: item.userId,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.outgoing,
|
||||
currentCall: item,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onCallItemUpdate(VideocallItem item) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(currentCall: item);
|
||||
|
||||
if (item.state == VideocallState.talking &&
|
||||
state.screenMode != VideocallScreenMode.inCall) {
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.inCall,
|
||||
successEvent: VideocallSuccessEvent.connected,
|
||||
);
|
||||
_startVideoViews();
|
||||
}
|
||||
}
|
||||
|
||||
void _onCallItemRemove(({int reason, String description}) event) {
|
||||
if (!ref.mounted) return;
|
||||
_stopVideoViews();
|
||||
state = state.copyWith(
|
||||
screenMode: VideocallScreenMode.idle,
|
||||
currentCall: null,
|
||||
localCanvas: null,
|
||||
remoteCanvas: null,
|
||||
isRemoteVideoAvailable: false,
|
||||
successEvent: VideocallSuccessEvent.callEnded,
|
||||
);
|
||||
}
|
||||
|
||||
void _onMissedCall(VideocallItem item) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
errorEvent: VideocallErrorEvent.callStart,
|
||||
);
|
||||
}
|
||||
|
||||
void _onClientStateChange(VideocallClientState clientState) {
|
||||
if (!ref.mounted) return;
|
||||
if (clientState == VideocallClientState.loggedIn) {
|
||||
state = state.copyWith(isSdkReady: true);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(errorEvent: null);
|
||||
}
|
||||
|
||||
void clearSuccess() {
|
||||
state = state.copyWith(successEvent: null);
|
||||
}
|
||||
|
||||
void _disposeSubscriptions() {
|
||||
_callAddSub?.cancel();
|
||||
_callUpdateSub?.cancel();
|
||||
_callRemoveSub?.cancel();
|
||||
_missedCallSub?.cancel();
|
||||
_clientStateSub?.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:videocall_sdk/videocall_sdk.dart';
|
||||
|
||||
import '../../domain/entities/videocall_error.dart';
|
||||
|
||||
part 'videocall_view_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class VideocallViewState with _$VideocallViewState {
|
||||
const factory VideocallViewState({
|
||||
@Default('') String deviceId,
|
||||
@Default('') String localUserId,
|
||||
@Default('') String remoteUserId,
|
||||
@Default(VideocallScreenMode.idle) VideocallScreenMode screenMode,
|
||||
@Default(false) bool isSdkReady,
|
||||
@Default(true) bool isMicEnabled,
|
||||
@Default(true) bool isSpeakerEnabled,
|
||||
@Default(true) bool isFrontCamera,
|
||||
@Default(false) bool isRemoteVideoAvailable,
|
||||
VideocallItem? currentCall,
|
||||
JCMediaDeviceVideoCanvas? localCanvas,
|
||||
JCMediaDeviceVideoCanvas? remoteCanvas,
|
||||
VideocallErrorEvent? errorEvent,
|
||||
VideocallSuccessEvent? successEvent,
|
||||
}) = _VideocallViewState;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'videocall_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$VideocallViewState {
|
||||
|
||||
String get deviceId; String get localUserId; String get remoteUserId; VideocallScreenMode get screenMode; bool get isSdkReady; bool get isMicEnabled; bool get isSpeakerEnabled; bool get isFrontCamera; bool get isRemoteVideoAvailable; VideocallItem? get currentCall; JCMediaDeviceVideoCanvas? get localCanvas; JCMediaDeviceVideoCanvas? get remoteCanvas; VideocallErrorEvent? get errorEvent; VideocallSuccessEvent? get successEvent;
|
||||
/// Create a copy of VideocallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$VideocallViewStateCopyWith<VideocallViewState> get copyWith => _$VideocallViewStateCopyWithImpl<VideocallViewState>(this as VideocallViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is VideocallViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.localUserId, localUserId) || other.localUserId == localUserId)&&(identical(other.remoteUserId, remoteUserId) || other.remoteUserId == remoteUserId)&&(identical(other.screenMode, screenMode) || other.screenMode == screenMode)&&(identical(other.isSdkReady, isSdkReady) || other.isSdkReady == isSdkReady)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isSpeakerEnabled, isSpeakerEnabled) || other.isSpeakerEnabled == isSpeakerEnabled)&&(identical(other.isFrontCamera, isFrontCamera) || other.isFrontCamera == isFrontCamera)&&(identical(other.isRemoteVideoAvailable, isRemoteVideoAvailable) || other.isRemoteVideoAvailable == isRemoteVideoAvailable)&&(identical(other.currentCall, currentCall) || other.currentCall == currentCall)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.remoteCanvas, remoteCanvas) || other.remoteCanvas == remoteCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,deviceId,localUserId,remoteUserId,screenMode,isSdkReady,isMicEnabled,isSpeakerEnabled,isFrontCamera,isRemoteVideoAvailable,currentCall,localCanvas,remoteCanvas,errorEvent,successEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideocallViewState(deviceId: $deviceId, localUserId: $localUserId, remoteUserId: $remoteUserId, screenMode: $screenMode, isSdkReady: $isSdkReady, isMicEnabled: $isMicEnabled, isSpeakerEnabled: $isSpeakerEnabled, isFrontCamera: $isFrontCamera, isRemoteVideoAvailable: $isRemoteVideoAvailable, currentCall: $currentCall, localCanvas: $localCanvas, remoteCanvas: $remoteCanvas, errorEvent: $errorEvent, successEvent: $successEvent)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $VideocallViewStateCopyWith<$Res> {
|
||||
factory $VideocallViewStateCopyWith(VideocallViewState value, $Res Function(VideocallViewState) _then) = _$VideocallViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$VideocallViewStateCopyWithImpl<$Res>
|
||||
implements $VideocallViewStateCopyWith<$Res> {
|
||||
_$VideocallViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final VideocallViewState _self;
|
||||
final $Res Function(VideocallViewState) _then;
|
||||
|
||||
/// Create a copy of VideocallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? localUserId = null,Object? remoteUserId = null,Object? screenMode = null,Object? isSdkReady = null,Object? isMicEnabled = null,Object? isSpeakerEnabled = null,Object? isFrontCamera = null,Object? isRemoteVideoAvailable = null,Object? currentCall = freezed,Object? localCanvas = freezed,Object? remoteCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,localUserId: null == localUserId ? _self.localUserId : localUserId // ignore: cast_nullable_to_non_nullable
|
||||
as String,remoteUserId: null == remoteUserId ? _self.remoteUserId : remoteUserId // ignore: cast_nullable_to_non_nullable
|
||||
as String,screenMode: null == screenMode ? _self.screenMode : screenMode // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallScreenMode,isSdkReady: null == isSdkReady ? _self.isSdkReady : isSdkReady // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isMicEnabled: null == isMicEnabled ? _self.isMicEnabled : isMicEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSpeakerEnabled: null == isSpeakerEnabled ? _self.isSpeakerEnabled : isSpeakerEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isFrontCamera: null == isFrontCamera ? _self.isFrontCamera : isFrontCamera // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isRemoteVideoAvailable: null == isRemoteVideoAvailable ? _self.isRemoteVideoAvailable : isRemoteVideoAvailable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,currentCall: freezed == currentCall ? _self.currentCall : currentCall // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallItem?,localCanvas: freezed == localCanvas ? _self.localCanvas : localCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,remoteCanvas: freezed == remoteCanvas ? _self.remoteCanvas : remoteCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallSuccessEvent?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [VideocallViewState].
|
||||
extension VideocallViewStatePatterns on VideocallViewState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _VideocallViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _VideocallViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallViewState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _VideocallViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallViewState() when $default != null:
|
||||
return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallViewState():
|
||||
return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallViewState() when $default != null:
|
||||
return $default(_that.deviceId,_that.localUserId,_that.remoteUserId,_that.screenMode,_that.isSdkReady,_that.isMicEnabled,_that.isSpeakerEnabled,_that.isFrontCamera,_that.isRemoteVideoAvailable,_that.currentCall,_that.localCanvas,_that.remoteCanvas,_that.errorEvent,_that.successEvent);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _VideocallViewState implements VideocallViewState {
|
||||
const _VideocallViewState({this.deviceId = '', this.localUserId = '', this.remoteUserId = '', this.screenMode = VideocallScreenMode.idle, this.isSdkReady = false, this.isMicEnabled = true, this.isSpeakerEnabled = true, this.isFrontCamera = true, this.isRemoteVideoAvailable = false, this.currentCall, this.localCanvas, this.remoteCanvas, this.errorEvent, this.successEvent});
|
||||
|
||||
|
||||
@override@JsonKey() final String deviceId;
|
||||
@override@JsonKey() final String localUserId;
|
||||
@override@JsonKey() final String remoteUserId;
|
||||
@override@JsonKey() final VideocallScreenMode screenMode;
|
||||
@override@JsonKey() final bool isSdkReady;
|
||||
@override@JsonKey() final bool isMicEnabled;
|
||||
@override@JsonKey() final bool isSpeakerEnabled;
|
||||
@override@JsonKey() final bool isFrontCamera;
|
||||
@override@JsonKey() final bool isRemoteVideoAvailable;
|
||||
@override final VideocallItem? currentCall;
|
||||
@override final JCMediaDeviceVideoCanvas? localCanvas;
|
||||
@override final JCMediaDeviceVideoCanvas? remoteCanvas;
|
||||
@override final VideocallErrorEvent? errorEvent;
|
||||
@override final VideocallSuccessEvent? successEvent;
|
||||
|
||||
/// Create a copy of VideocallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$VideocallViewStateCopyWith<_VideocallViewState> get copyWith => __$VideocallViewStateCopyWithImpl<_VideocallViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideocallViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.localUserId, localUserId) || other.localUserId == localUserId)&&(identical(other.remoteUserId, remoteUserId) || other.remoteUserId == remoteUserId)&&(identical(other.screenMode, screenMode) || other.screenMode == screenMode)&&(identical(other.isSdkReady, isSdkReady) || other.isSdkReady == isSdkReady)&&(identical(other.isMicEnabled, isMicEnabled) || other.isMicEnabled == isMicEnabled)&&(identical(other.isSpeakerEnabled, isSpeakerEnabled) || other.isSpeakerEnabled == isSpeakerEnabled)&&(identical(other.isFrontCamera, isFrontCamera) || other.isFrontCamera == isFrontCamera)&&(identical(other.isRemoteVideoAvailable, isRemoteVideoAvailable) || other.isRemoteVideoAvailable == isRemoteVideoAvailable)&&(identical(other.currentCall, currentCall) || other.currentCall == currentCall)&&(identical(other.localCanvas, localCanvas) || other.localCanvas == localCanvas)&&(identical(other.remoteCanvas, remoteCanvas) || other.remoteCanvas == remoteCanvas)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,deviceId,localUserId,remoteUserId,screenMode,isSdkReady,isMicEnabled,isSpeakerEnabled,isFrontCamera,isRemoteVideoAvailable,currentCall,localCanvas,remoteCanvas,errorEvent,successEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideocallViewState(deviceId: $deviceId, localUserId: $localUserId, remoteUserId: $remoteUserId, screenMode: $screenMode, isSdkReady: $isSdkReady, isMicEnabled: $isMicEnabled, isSpeakerEnabled: $isSpeakerEnabled, isFrontCamera: $isFrontCamera, isRemoteVideoAvailable: $isRemoteVideoAvailable, currentCall: $currentCall, localCanvas: $localCanvas, remoteCanvas: $remoteCanvas, errorEvent: $errorEvent, successEvent: $successEvent)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$VideocallViewStateCopyWith<$Res> implements $VideocallViewStateCopyWith<$Res> {
|
||||
factory _$VideocallViewStateCopyWith(_VideocallViewState value, $Res Function(_VideocallViewState) _then) = __$VideocallViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String deviceId, String localUserId, String remoteUserId, VideocallScreenMode screenMode, bool isSdkReady, bool isMicEnabled, bool isSpeakerEnabled, bool isFrontCamera, bool isRemoteVideoAvailable, VideocallItem? currentCall, JCMediaDeviceVideoCanvas? localCanvas, JCMediaDeviceVideoCanvas? remoteCanvas, VideocallErrorEvent? errorEvent, VideocallSuccessEvent? successEvent
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$VideocallViewStateCopyWithImpl<$Res>
|
||||
implements _$VideocallViewStateCopyWith<$Res> {
|
||||
__$VideocallViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _VideocallViewState _self;
|
||||
final $Res Function(_VideocallViewState) _then;
|
||||
|
||||
/// Create a copy of VideocallViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? localUserId = null,Object? remoteUserId = null,Object? screenMode = null,Object? isSdkReady = null,Object? isMicEnabled = null,Object? isSpeakerEnabled = null,Object? isFrontCamera = null,Object? isRemoteVideoAvailable = null,Object? currentCall = freezed,Object? localCanvas = freezed,Object? remoteCanvas = freezed,Object? errorEvent = freezed,Object? successEvent = freezed,}) {
|
||||
return _then(_VideocallViewState(
|
||||
deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,localUserId: null == localUserId ? _self.localUserId : localUserId // ignore: cast_nullable_to_non_nullable
|
||||
as String,remoteUserId: null == remoteUserId ? _self.remoteUserId : remoteUserId // ignore: cast_nullable_to_non_nullable
|
||||
as String,screenMode: null == screenMode ? _self.screenMode : screenMode // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallScreenMode,isSdkReady: null == isSdkReady ? _self.isSdkReady : isSdkReady // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isMicEnabled: null == isMicEnabled ? _self.isMicEnabled : isMicEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSpeakerEnabled: null == isSpeakerEnabled ? _self.isSpeakerEnabled : isSpeakerEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isFrontCamera: null == isFrontCamera ? _self.isFrontCamera : isFrontCamera // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isRemoteVideoAvailable: null == isRemoteVideoAvailable ? _self.isRemoteVideoAvailable : isRemoteVideoAvailable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,currentCall: freezed == currentCall ? _self.currentCall : currentCall // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallItem?,localCanvas: freezed == localCanvas ? _self.localCanvas : localCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,remoteCanvas: freezed == remoteCanvas ? _self.remoteCanvas : remoteCanvas // ignore: cast_nullable_to_non_nullable
|
||||
as JCMediaDeviceVideoCanvas?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallSuccessEvent?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,311 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
|
||||
import '../domain/entities/videocall_error.dart';
|
||||
import 'state/videocall_view_model.dart';
|
||||
import 'state/videocall_view_state.dart';
|
||||
import 'widgets/call_controls_widget.dart';
|
||||
import 'widgets/call_status_indicator.dart';
|
||||
import 'widgets/incoming_call_overlay.dart';
|
||||
import 'widgets/video_view_widget.dart';
|
||||
|
||||
class VideocallScreen extends ConsumerStatefulWidget {
|
||||
const VideocallScreen({super.key, required this.navigationContract});
|
||||
|
||||
final NavigationContract navigationContract;
|
||||
|
||||
@override
|
||||
ConsumerState<VideocallScreen> createState() => _VideocallScreenState();
|
||||
}
|
||||
|
||||
class _VideocallScreenState extends ConsumerState<VideocallScreen> {
|
||||
final _userIdController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_userIdController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(videocallViewModelProvider);
|
||||
final vm = ref.read(videocallViewModelProvider.notifier);
|
||||
|
||||
ref.listen(
|
||||
videocallViewModelProvider.select((s) => s.errorEvent),
|
||||
(_, next) {
|
||||
if (next == null) return;
|
||||
final key = switch (next) {
|
||||
VideocallErrorEvent.sdkInitialization ||
|
||||
VideocallErrorEvent.authentication ||
|
||||
VideocallErrorEvent.callStart ||
|
||||
VideocallErrorEvent.callAnswer ||
|
||||
VideocallErrorEvent.cameraPermission ||
|
||||
VideocallErrorEvent.microphonePermission ||
|
||||
VideocallErrorEvent.network ||
|
||||
VideocallErrorEvent.generic =>
|
||||
I18n.errorGeneric,
|
||||
};
|
||||
showErrorDialog(context, key);
|
||||
vm.clearError();
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
videocallViewModelProvider.select((s) => s.successEvent),
|
||||
(_, next) {
|
||||
if (next == null) return;
|
||||
if (next == VideocallSuccessEvent.callEnded) {
|
||||
showInfoDialog(context, I18n.videoCall);
|
||||
}
|
||||
vm.clearSuccess();
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: SafeArea(
|
||||
child: switch (state.screenMode) {
|
||||
VideocallScreenMode.idle => _IdleView(
|
||||
state: state,
|
||||
controller: _userIdController,
|
||||
vm: vm,
|
||||
),
|
||||
VideocallScreenMode.outgoing => _OutgoingView(state: state, vm: vm),
|
||||
VideocallScreenMode.incoming => _IncomingView(state: state, vm: vm),
|
||||
VideocallScreenMode.inCall => _InCallView(state: state, vm: vm),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IdleView extends StatelessWidget {
|
||||
const _IdleView({
|
||||
required this.state,
|
||||
required this.controller,
|
||||
required this.vm,
|
||||
});
|
||||
|
||||
final VideocallViewState state;
|
||||
final TextEditingController controller;
|
||||
final VideocallViewModel vm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (!state.isSdkReady) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: colorScheme.onSurface),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Inicializando SDK...',
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.videocam, color: colorScheme.onSurface, size: 64),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Videollamada',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: controller,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'User ID del destinatario',
|
||||
hintStyle: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.onSurface.withValues(alpha: 0.24)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: context.sfColors.legacyPrimary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final userId = controller.text.trim();
|
||||
if (userId.isEmpty) return;
|
||||
vm.startCall(userId);
|
||||
},
|
||||
icon: const Icon(Icons.videocam),
|
||||
label: const Text('Iniciar videollamada'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.sfColors.legacyPrimary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (kDebugMode && state.localUserId.isEmpty)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await vm.login(userId: 'p_test1', password: 'test123');
|
||||
},
|
||||
child: Text(
|
||||
'Login como p_test1 (testing)',
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)),
|
||||
),
|
||||
),
|
||||
if (state.localUserId.isNotEmpty)
|
||||
Text(
|
||||
'Logged in as: ${state.localUserId}',
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38), fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OutgoingView extends StatelessWidget {
|
||||
const _OutgoingView({required this.state, required this.vm});
|
||||
|
||||
final VideocallViewState state;
|
||||
final VideocallViewModel vm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: CallStatusIndicator(
|
||||
screenMode: state.screenMode,
|
||||
remoteUserId: state.remoteUserId,
|
||||
onCancel: vm.hangUp,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IncomingView extends StatelessWidget {
|
||||
const _IncomingView({required this.state, required this.vm});
|
||||
|
||||
final VideocallViewState state;
|
||||
final VideocallViewModel vm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IncomingCallOverlay(
|
||||
callerUserId: state.remoteUserId,
|
||||
isVideo: state.currentCall?.isVideo ?? true,
|
||||
onAccept: vm.answerCall,
|
||||
onReject: vm.rejectCall,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InCallView extends StatelessWidget {
|
||||
const _InCallView({required this.state, required this.vm});
|
||||
|
||||
final VideocallViewState state;
|
||||
final VideocallViewModel vm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (state.remoteCanvas != null)
|
||||
VideoViewWidget(canvas: state.remoteCanvas!)
|
||||
else
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.person, color: colorScheme.onSurface.withValues(alpha: 0.24), size: 96),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Esperando video remoto...',
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.38)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (state.localCanvas != null)
|
||||
Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.onSurface.withValues(alpha: 0.24), width: 1),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: VideoViewWidget(
|
||||
canvas: state.localCanvas!,
|
||||
width: 120,
|
||||
height: 160,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
colorScheme.surface.withValues(alpha: 0.8),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: CallControlsWidget(
|
||||
isMicEnabled: state.isMicEnabled,
|
||||
isSpeakerEnabled: state.isSpeakerEnabled,
|
||||
isFrontCamera: state.isFrontCamera,
|
||||
onToggleMic: vm.toggleMic,
|
||||
onToggleSpeaker: vm.toggleSpeaker,
|
||||
onSwitchCamera: vm.switchCamera,
|
||||
onHangUp: vm.hangUp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CallControlsWidget extends StatelessWidget {
|
||||
const CallControlsWidget({
|
||||
super.key,
|
||||
required this.isMicEnabled,
|
||||
required this.isSpeakerEnabled,
|
||||
required this.isFrontCamera,
|
||||
required this.onToggleMic,
|
||||
required this.onToggleSpeaker,
|
||||
required this.onSwitchCamera,
|
||||
required this.onHangUp,
|
||||
});
|
||||
|
||||
final bool isMicEnabled;
|
||||
final bool isSpeakerEnabled;
|
||||
final bool isFrontCamera;
|
||||
final VoidCallback onToggleMic;
|
||||
final VoidCallback onToggleSpeaker;
|
||||
final VoidCallback onSwitchCamera;
|
||||
final VoidCallback onHangUp;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_ControlButton(
|
||||
icon: isMicEnabled ? Icons.mic : Icons.mic_off,
|
||||
label: isMicEnabled ? 'Mic On' : 'Mic Off',
|
||||
isActive: isMicEnabled,
|
||||
onPressed: onToggleMic,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: isSpeakerEnabled
|
||||
? Icons.volume_up
|
||||
: Icons.volume_off,
|
||||
label: isSpeakerEnabled ? 'Speaker' : 'Earpiece',
|
||||
isActive: isSpeakerEnabled,
|
||||
onPressed: onToggleSpeaker,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: Icons.cameraswitch,
|
||||
label: isFrontCamera ? 'Front' : 'Back',
|
||||
isActive: true,
|
||||
onPressed: onSwitchCamera,
|
||||
),
|
||||
_ControlButton(
|
||||
icon: Icons.call_end,
|
||||
label: 'Hang Up',
|
||||
isActive: false,
|
||||
isDestructive: true,
|
||||
onPressed: onHangUp,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ControlButton extends StatelessWidget {
|
||||
const _ControlButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isActive,
|
||||
this.isDestructive = false,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool isActive;
|
||||
final bool isDestructive;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final backgroundColor = isDestructive
|
||||
? colorScheme.error
|
||||
: isActive
|
||||
? colorScheme.onSurface.withValues(alpha: 0.2)
|
||||
: colorScheme.onSurface.withValues(alpha: 0.4);
|
||||
|
||||
final iconColor = isDestructive
|
||||
? Colors.white
|
||||
: isActive
|
||||
? colorScheme.onSurface
|
||||
: colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Material(
|
||||
color: backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Icon(icon, color: iconColor, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 11),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../domain/entities/videocall_error.dart';
|
||||
|
||||
class CallStatusIndicator extends StatelessWidget {
|
||||
const CallStatusIndicator({
|
||||
super.key,
|
||||
required this.screenMode,
|
||||
required this.remoteUserId,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
final VideocallScreenMode screenMode;
|
||||
final String remoteUserId;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (text, showCancel) = switch (screenMode) {
|
||||
VideocallScreenMode.outgoing => ('Llamando a $remoteUserId...', true),
|
||||
VideocallScreenMode.incoming => ('$remoteUserId te está llamando', false),
|
||||
_ => ('', false),
|
||||
};
|
||||
|
||||
if (text.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: colorScheme.onSurface),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (showCancel) ...[
|
||||
const SizedBox(height: 48),
|
||||
Material(
|
||||
color: colorScheme.error,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: onCancel,
|
||||
customBorder: const CircleBorder(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Icon(Icons.call_end, color: Colors.white, size: 32),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IncomingCallOverlay extends StatelessWidget {
|
||||
const IncomingCallOverlay({
|
||||
super.key,
|
||||
required this.callerUserId,
|
||||
required this.isVideo,
|
||||
required this.onAccept,
|
||||
required this.onReject,
|
||||
});
|
||||
|
||||
final String callerUserId;
|
||||
final bool isVideo;
|
||||
final VoidCallback onAccept;
|
||||
final VoidCallback onReject;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
color: colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Icon(
|
||||
isVideo ? Icons.videocam : Icons.call,
|
||||
color: colorScheme.onSurface,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
isVideo ? 'Videollamada entrante' : 'Llamada entrante',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
callerUserId,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 64),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_ActionButton(
|
||||
icon: Icons.call_end,
|
||||
color: colorScheme.error,
|
||||
label: 'Rechazar',
|
||||
onPressed: onReject,
|
||||
),
|
||||
_ActionButton(
|
||||
icon: isVideo ? Icons.videocam : Icons.call,
|
||||
color: Colors.green,
|
||||
label: 'Aceptar',
|
||||
onPressed: onAccept,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
const _ActionButton({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Material(
|
||||
color: color,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Icon(icon, color: Colors.white, size: 36),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7))),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../domain/entities/videocall_participant.dart';
|
||||
import 'participant_tile_widget.dart';
|
||||
|
||||
class ParticipantGridWidget extends StatelessWidget {
|
||||
const ParticipantGridWidget({
|
||||
super.key,
|
||||
required this.participants,
|
||||
});
|
||||
|
||||
final List<VideocallParticipant> participants;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (participants.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Esperando participantes...',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final crossAxisCount = participants.length <= 2 ? 1 : 2;
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: 3 / 4,
|
||||
),
|
||||
itemCount: participants.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ParticipantTileWidget(participant: participants[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../domain/entities/videocall_participant.dart';
|
||||
import 'video_view_widget.dart';
|
||||
|
||||
class ParticipantTileWidget extends StatelessWidget {
|
||||
const ParticipantTileWidget({
|
||||
super.key,
|
||||
required this.participant,
|
||||
});
|
||||
|
||||
final VideocallParticipant participant;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (participant.videoCanvas != null && participant.isVideo)
|
||||
VideoViewWidget(canvas: participant.videoCanvas!)
|
||||
else
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.38),
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface.withValues(alpha: 0.54),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!participant.isAudio)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(Icons.mic_off, color: colorScheme.error, size: 14),
|
||||
),
|
||||
Text(
|
||||
participant.isSelf ? 'Tú' : participant.userId,
|
||||
style: TextStyle(color: colorScheme.onSurface, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:videocall_sdk/videocall_sdk.dart';
|
||||
|
||||
class VideoViewWidget extends StatefulWidget {
|
||||
const VideoViewWidget({
|
||||
super.key,
|
||||
required this.canvas,
|
||||
this.width = double.infinity,
|
||||
this.height = double.infinity,
|
||||
this.fit = BoxFit.cover,
|
||||
});
|
||||
|
||||
final JCMediaDeviceVideoCanvas canvas;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
State<VideoViewWidget> createState() => _VideoViewWidgetState();
|
||||
}
|
||||
|
||||
class _VideoViewWidgetState extends State<VideoViewWidget> {
|
||||
Widget? _videoWidget;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initVideoView();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(VideoViewWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.canvas.videoCanvasId != widget.canvas.videoCanvasId) {
|
||||
_initVideoView();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initVideoView() async {
|
||||
final Widget videoWidget;
|
||||
if (Platform.isIOS) {
|
||||
videoWidget = await widget.canvas.getIOSVideoView(
|
||||
(viewId) {},
|
||||
widget.width,
|
||||
widget.height,
|
||||
);
|
||||
} else {
|
||||
videoWidget = await widget.canvas.getAndroidVideoView();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _videoWidget = videoWidget);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: _videoWidget ??
|
||||
const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
import '../data/datasources/videocall_signaling_datasource.dart';
|
||||
import '../data/datasources/videocall_signaling_datasource_impl.dart';
|
||||
|
||||
final videocallSignalingDatasourceProvider =
|
||||
Provider<VideocallSignalingDatasource>((ref) {
|
||||
final repository = GetIt.I<SaveFamilyRepository>();
|
||||
return VideocallSignalingDatasourceImpl(repository);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../data/repositories/videocall_signaling_repository_impl.dart';
|
||||
import '../domain/repositories/videocall_signaling_repository.dart';
|
||||
import 'videocall_signaling_datasource_provider.dart';
|
||||
|
||||
final videocallSignalingRepositoryProvider =
|
||||
Provider<VideocallSignalingRepository>((ref) {
|
||||
final datasource = ref.read(videocallSignalingDatasourceProvider);
|
||||
return VideocallSignalingRepositoryImpl(datasource);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
|
||||
import 'presentation/videocall_screen.dart';
|
||||
|
||||
class VideocallBuilder {
|
||||
const VideocallBuilder();
|
||||
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
|
||||
|
||||
return MaterialPage<void>(
|
||||
key: state.pageKey,
|
||||
child: VideocallScreen(navigationContract: navigationContract),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ dependencies:
|
||||
path: ../../packages/legacy_ui
|
||||
sf_shared:
|
||||
path: ../../../../packages/sf_shared
|
||||
videocall_sdk:
|
||||
path: ../../../../packages/videocall_sdk
|
||||
#dependencies go here
|
||||
flutter_svg: ^2.2.2
|
||||
get_it: ^9.0.5
|
||||
|
||||
@@ -76,6 +76,7 @@ class AppRoutes {
|
||||
static const doNotDisturb = '$deviceManagement/do_not_disturb';
|
||||
static const callHistory = '$deviceManagement/call_history';
|
||||
static const backgroundImage = '$deviceManagement/background_image';
|
||||
static const videocall = '$deviceManagement/videocall';
|
||||
|
||||
static const legacyLogin = '$legacy/login';
|
||||
static const legacySignup = '$legacy/signup';
|
||||
|
||||
Reference in New Issue
Block a user