diff --git a/ios/Podfile b/ios/Podfile index c4cb422..4929876 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -41,5 +41,9 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + # Workaround for https://github.com/flutter/flutter/issues/64502 + config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES' # <= this line + end end end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 137c6de..35617c5 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,6 +26,16 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSCalendarsUsageDescription + Grant access to Calander help us to shows Solar Calander with your own events. + NSCameraUsageDescription + Grant access to Camera will allow Solian take photo or video for your post. + NSMicrophoneUsageDescription + Grant access to Microphone will allow Solian record audio for your post. + NSPhotoLibraryAddUsageDescription + Grant access to Photo Library will allow Solian download photo to album for you. + NSPhotoLibraryUsageDescription + Grant access to Photo Library will allow Solian upload photo or video for your post. UIApplicationSupportsIndirectInputEvents UIBackgroundModes @@ -33,11 +43,14 @@ fetch audio remote-notification + voip UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main + UIStatusBarHidden + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -51,17 +64,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSCalendarsUsageDescription - Grant access to Calander help us to shows Solar Calander with your own events. - NSCameraUsageDescription - Grant access to Camera will allow Solian take photo or video for your post. - NSMicrophoneUsageDescription - Grant access to Microphone will allow Solian record audio for your post. - NSPhotoLibraryAddUsageDescription - Grant access to Photo Library will allow Solian download photo to album for you. - NSPhotoLibraryUsageDescription - Grant access to Photo Library will allow Solian upload photo or video for your post. - UIStatusBarHidden - diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 836f56d..551f2c8 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -85,4 +85,5 @@ class DefaultFirebaseOptions { storageBucket: 'solian-0x001.firebasestorage.app', measurementId: 'G-JD1YEG9D6F', ); -} + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d4353d8..f364c11 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; import 'package:island/screens/auth/tabs.dart'; import 'package:island/services/notify.dart'; +import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:relative_time/relative_time.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -28,11 +29,21 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + log( + "[SplashScreen] Keeping the flash screen to loading other resources...", + ); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); } - await EasyLocalization.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + try { + await EasyLocalization.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + log("[SplashScreen] Firebase is ready!"); + } catch (err) { + showErrorAlert(err); + } final prefs = await SharedPreferences.getInstance(); @@ -43,6 +54,7 @@ void main() async { appWindow.size = initialSize; appWindow.alignment = Alignment.center; appWindow.show(); + log("[SplashScreen] Desktop window is ready!"); }); } @@ -52,10 +64,12 @@ void main() async { if (imagePickerImplementation is ImagePickerAndroid) { imagePickerImplementation.useAndroidPhotoPicker = true; } + log("[SplashScreen] Android image picker is ready!"); } if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { FlutterNativeSplash.remove(); + log("[SplashScreen] Now hiding the splash screen..."); } runApp( diff --git a/lib/models/chat.dart b/lib/models/chat.dart index ac5bd4a..d6733ae 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -146,12 +146,27 @@ sealed class ChatRealtimeJoinResponse with _$ChatRealtimeJoinResponse { required String callId, required String roomName, required bool isAdmin, + required List participants, }) = _ChatRealtimeJoinResponse; factory ChatRealtimeJoinResponse.fromJson(Map json) => _$ChatRealtimeJoinResponseFromJson(json); } +@freezed +sealed class CallParticipant with _$CallParticipant { + const factory CallParticipant({ + required String identity, + required String name, + required DateTime joinedAt, + required String? accountId, + required SnChatMember? profile, + }) = _CallParticipant; + + factory CallParticipant.fromJson(Map json) => + _$CallParticipantFromJson(json); +} + @freezed sealed class SnRealtimeCall with _$SnRealtimeCall { const factory SnRealtimeCall({ diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index 7df3096..5eb124b 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -1342,7 +1342,7 @@ as DateTime, /// @nodoc mixin _$ChatRealtimeJoinResponse { - String get provider; String get endpoint; String get token; String get callId; String get roomName; bool get isAdmin; + String get provider; String get endpoint; String get token; String get callId; String get roomName; bool get isAdmin; List get participants; /// Create a copy of ChatRealtimeJoinResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1355,16 +1355,16 @@ $ChatRealtimeJoinResponseCopyWith get copyWith => _$Ch @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatRealtimeJoinResponse&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.endpoint, endpoint) || other.endpoint == endpoint)&&(identical(other.token, token) || other.token == token)&&(identical(other.callId, callId) || other.callId == callId)&&(identical(other.roomName, roomName) || other.roomName == roomName)&&(identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatRealtimeJoinResponse&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.endpoint, endpoint) || other.endpoint == endpoint)&&(identical(other.token, token) || other.token == token)&&(identical(other.callId, callId) || other.callId == callId)&&(identical(other.roomName, roomName) || other.roomName == roomName)&&(identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)&&const DeepCollectionEquality().equals(other.participants, participants)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,provider,endpoint,token,callId,roomName,isAdmin); +int get hashCode => Object.hash(runtimeType,provider,endpoint,token,callId,roomName,isAdmin,const DeepCollectionEquality().hash(participants)); @override String toString() { - return 'ChatRealtimeJoinResponse(provider: $provider, endpoint: $endpoint, token: $token, callId: $callId, roomName: $roomName, isAdmin: $isAdmin)'; + return 'ChatRealtimeJoinResponse(provider: $provider, endpoint: $endpoint, token: $token, callId: $callId, roomName: $roomName, isAdmin: $isAdmin, participants: $participants)'; } @@ -1375,7 +1375,7 @@ abstract mixin class $ChatRealtimeJoinResponseCopyWith<$Res> { factory $ChatRealtimeJoinResponseCopyWith(ChatRealtimeJoinResponse value, $Res Function(ChatRealtimeJoinResponse) _then) = _$ChatRealtimeJoinResponseCopyWithImpl; @useResult $Res call({ - String provider, String endpoint, String token, String callId, String roomName, bool isAdmin + String provider, String endpoint, String token, String callId, String roomName, bool isAdmin, List participants }); @@ -1392,7 +1392,7 @@ class _$ChatRealtimeJoinResponseCopyWithImpl<$Res> /// Create a copy of ChatRealtimeJoinResponse /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? provider = null,Object? endpoint = null,Object? token = null,Object? callId = null,Object? roomName = null,Object? isAdmin = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? provider = null,Object? endpoint = null,Object? token = null,Object? callId = null,Object? roomName = null,Object? isAdmin = null,Object? participants = null,}) { return _then(_self.copyWith( provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable as String,endpoint: null == endpoint ? _self.endpoint : endpoint // ignore: cast_nullable_to_non_nullable @@ -1400,7 +1400,8 @@ as String,token: null == token ? _self.token : token // ignore: cast_nullable_to as String,callId: null == callId ? _self.callId : callId // ignore: cast_nullable_to_non_nullable as String,roomName: null == roomName ? _self.roomName : roomName // ignore: cast_nullable_to_non_nullable as String,isAdmin: null == isAdmin ? _self.isAdmin : isAdmin // ignore: cast_nullable_to_non_nullable -as bool, +as bool,participants: null == participants ? _self.participants : participants // ignore: cast_nullable_to_non_nullable +as List, )); } @@ -1411,7 +1412,7 @@ as bool, @JsonSerializable() class _ChatRealtimeJoinResponse implements ChatRealtimeJoinResponse { - const _ChatRealtimeJoinResponse({required this.provider, required this.endpoint, required this.token, required this.callId, required this.roomName, required this.isAdmin}); + const _ChatRealtimeJoinResponse({required this.provider, required this.endpoint, required this.token, required this.callId, required this.roomName, required this.isAdmin, required final List participants}): _participants = participants; factory _ChatRealtimeJoinResponse.fromJson(Map json) => _$ChatRealtimeJoinResponseFromJson(json); @override final String provider; @@ -1420,6 +1421,13 @@ class _ChatRealtimeJoinResponse implements ChatRealtimeJoinResponse { @override final String callId; @override final String roomName; @override final bool isAdmin; + final List _participants; +@override List get participants { + if (_participants is EqualUnmodifiableListView) return _participants; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_participants); +} + /// Create a copy of ChatRealtimeJoinResponse /// with the given fields replaced by the non-null parameter values. @@ -1434,16 +1442,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatRealtimeJoinResponse&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.endpoint, endpoint) || other.endpoint == endpoint)&&(identical(other.token, token) || other.token == token)&&(identical(other.callId, callId) || other.callId == callId)&&(identical(other.roomName, roomName) || other.roomName == roomName)&&(identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatRealtimeJoinResponse&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.endpoint, endpoint) || other.endpoint == endpoint)&&(identical(other.token, token) || other.token == token)&&(identical(other.callId, callId) || other.callId == callId)&&(identical(other.roomName, roomName) || other.roomName == roomName)&&(identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)&&const DeepCollectionEquality().equals(other._participants, _participants)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,provider,endpoint,token,callId,roomName,isAdmin); +int get hashCode => Object.hash(runtimeType,provider,endpoint,token,callId,roomName,isAdmin,const DeepCollectionEquality().hash(_participants)); @override String toString() { - return 'ChatRealtimeJoinResponse(provider: $provider, endpoint: $endpoint, token: $token, callId: $callId, roomName: $roomName, isAdmin: $isAdmin)'; + return 'ChatRealtimeJoinResponse(provider: $provider, endpoint: $endpoint, token: $token, callId: $callId, roomName: $roomName, isAdmin: $isAdmin, participants: $participants)'; } @@ -1454,7 +1462,7 @@ abstract mixin class _$ChatRealtimeJoinResponseCopyWith<$Res> implements $ChatRe factory _$ChatRealtimeJoinResponseCopyWith(_ChatRealtimeJoinResponse value, $Res Function(_ChatRealtimeJoinResponse) _then) = __$ChatRealtimeJoinResponseCopyWithImpl; @override @useResult $Res call({ - String provider, String endpoint, String token, String callId, String roomName, bool isAdmin + String provider, String endpoint, String token, String callId, String roomName, bool isAdmin, List participants }); @@ -1471,7 +1479,7 @@ class __$ChatRealtimeJoinResponseCopyWithImpl<$Res> /// Create a copy of ChatRealtimeJoinResponse /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? provider = null,Object? endpoint = null,Object? token = null,Object? callId = null,Object? roomName = null,Object? isAdmin = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? provider = null,Object? endpoint = null,Object? token = null,Object? callId = null,Object? roomName = null,Object? isAdmin = null,Object? participants = null,}) { return _then(_ChatRealtimeJoinResponse( provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable as String,endpoint: null == endpoint ? _self.endpoint : endpoint // ignore: cast_nullable_to_non_nullable @@ -1479,7 +1487,8 @@ as String,token: null == token ? _self.token : token // ignore: cast_nullable_to as String,callId: null == callId ? _self.callId : callId // ignore: cast_nullable_to_non_nullable as String,roomName: null == roomName ? _self.roomName : roomName // ignore: cast_nullable_to_non_nullable as String,isAdmin: null == isAdmin ? _self.isAdmin : isAdmin // ignore: cast_nullable_to_non_nullable -as bool, +as bool,participants: null == participants ? _self._participants : participants // ignore: cast_nullable_to_non_nullable +as List, )); } @@ -1487,6 +1496,175 @@ as bool, } +/// @nodoc +mixin _$CallParticipant { + + String get identity; String get name; DateTime get joinedAt; String? get accountId; SnChatMember? get profile; +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CallParticipantCopyWith get copyWith => _$CallParticipantCopyWithImpl(this as CallParticipant, _$identity); + + /// Serializes this CallParticipant to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.profile, profile) || other.profile == profile)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile); + +@override +String toString() { + return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)'; +} + + +} + +/// @nodoc +abstract mixin class $CallParticipantCopyWith<$Res> { + factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl; +@useResult +$Res call({ + String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile +}); + + +$SnChatMemberCopyWith<$Res>? get profile; + +} +/// @nodoc +class _$CallParticipantCopyWithImpl<$Res> + implements $CallParticipantCopyWith<$Res> { + _$CallParticipantCopyWithImpl(this._self, this._then); + + final CallParticipant _self; + final $Res Function(CallParticipant) _then; + +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,Object? accountId = freezed,Object? profile = freezed,}) { + return _then(_self.copyWith( +identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable +as DateTime,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String?,profile: freezed == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable +as SnChatMember?, + )); +} +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnChatMemberCopyWith<$Res>? get profile { + if (_self.profile == null) { + return null; + } + + return $SnChatMemberCopyWith<$Res>(_self.profile!, (value) { + return _then(_self.copyWith(profile: value)); + }); +} +} + + +/// @nodoc +@JsonSerializable() + +class _CallParticipant implements CallParticipant { + const _CallParticipant({required this.identity, required this.name, required this.joinedAt, required this.accountId, required this.profile}); + factory _CallParticipant.fromJson(Map json) => _$CallParticipantFromJson(json); + +@override final String identity; +@override final String name; +@override final DateTime joinedAt; +@override final String? accountId; +@override final SnChatMember? profile; + +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CallParticipantCopyWith<_CallParticipant> get copyWith => __$CallParticipantCopyWithImpl<_CallParticipant>(this, _$identity); + +@override +Map toJson() { + return _$CallParticipantToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.profile, profile) || other.profile == profile)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile); + +@override +String toString() { + return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)'; +} + + +} + +/// @nodoc +abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipantCopyWith<$Res> { + factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl; +@override @useResult +$Res call({ + String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile +}); + + +@override $SnChatMemberCopyWith<$Res>? get profile; + +} +/// @nodoc +class __$CallParticipantCopyWithImpl<$Res> + implements _$CallParticipantCopyWith<$Res> { + __$CallParticipantCopyWithImpl(this._self, this._then); + + final _CallParticipant _self; + final $Res Function(_CallParticipant) _then; + +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,Object? accountId = freezed,Object? profile = freezed,}) { + return _then(_CallParticipant( +identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable +as DateTime,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String?,profile: freezed == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable +as SnChatMember?, + )); +} + +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnChatMemberCopyWith<$Res>? get profile { + if (_self.profile == null) { + return null; + } + + return $SnChatMemberCopyWith<$Res>(_self.profile!, (value) { + return _then(_self.copyWith(profile: value)); + }); +} +} + + /// @nodoc mixin _$SnRealtimeCall { diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index b8ef225..859177f 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -249,6 +249,10 @@ _ChatRealtimeJoinResponse _$ChatRealtimeJoinResponseFromJson( callId: json['call_id'] as String, roomName: json['room_name'] as String, isAdmin: json['is_admin'] as bool, + participants: + (json['participants'] as List) + .map((e) => CallParticipant.fromJson(e as Map)) + .toList(), ); Map _$ChatRealtimeJoinResponseToJson( @@ -260,8 +264,30 @@ Map _$ChatRealtimeJoinResponseToJson( 'call_id': instance.callId, 'room_name': instance.roomName, 'is_admin': instance.isAdmin, + 'participants': instance.participants.map((e) => e.toJson()).toList(), }; +_CallParticipant _$CallParticipantFromJson(Map json) => + _CallParticipant( + identity: json['identity'] as String, + name: json['name'] as String, + joinedAt: DateTime.parse(json['joined_at'] as String), + accountId: json['account_id'] as String?, + profile: + json['profile'] == null + ? null + : SnChatMember.fromJson(json['profile'] as Map), + ); + +Map _$CallParticipantToJson(_CallParticipant instance) => + { + 'identity': instance.identity, + 'name': instance.name, + 'joined_at': instance.joinedAt.toIso8601String(), + 'account_id': instance.accountId, + 'profile': instance.profile?.toJson(), + }; + _SnRealtimeCall _$SnRealtimeCallFromJson(Map json) => _SnRealtimeCall( id: json['id'] as String, diff --git a/lib/pods/call.dart b/lib/pods/call.dart index b8ba3df..7b0b364 100644 --- a/lib/pods/call.dart +++ b/lib/pods/call.dart @@ -1,7 +1,12 @@ +import 'package:island/pods/userinfo.dart'; +import 'package:island/screens/chat/chat.dart'; import 'package:livekit_client/livekit_client.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:island/pods/network.dart'; +import 'package:island/models/chat.dart'; +import 'package:island/pods/websocket.dart'; part 'call.g.dart'; part 'call.freezed.dart'; @@ -9,43 +14,244 @@ part 'call.freezed.dart'; @freezed sealed class CallState with _$CallState { const factory CallState({ - required bool isMuted, required bool isConnected, + required bool isMicrophoneEnabled, + required bool isCameraEnabled, + required bool isScreenSharing, String? error, }) = _CallState; } +@freezed +sealed class CallParticipantLive with _$CallParticipantLive { + const CallParticipantLive._(); + + const factory CallParticipantLive({ + required CallParticipant participant, + required Participant remoteParticipant, + }) = _CallParticipantLive; + + bool get isSpeaking => remoteParticipant.isSpeaking; + bool get isMuted => remoteParticipant.isMuted; + bool get isScreenSharing => remoteParticipant.isScreenShareEnabled(); + bool get isScreenSharingWithAudio => + remoteParticipant.isScreenShareAudioEnabled(); + + bool get hasVideo => remoteParticipant.hasVideo; + bool get hasAudio => remoteParticipant.hasAudio; +} + @riverpod class CallNotifier extends _$CallNotifier { Room? _room; LocalParticipant? _localParticipant; - LocalAudioTrack? _localAudioTrack; + List _participants = []; + final Map _participantInfoByIdentity = {}; + StreamSubscription? _wsSubscription; + EventsListener? _roomListener; + + List get participants => + List.unmodifiable(_participants); + LocalParticipant? get localParticipant => _localParticipant; @override CallState build() { - return const CallState(isMuted: false, isConnected: false); + // Subscribe to websocket updates + _subscribeToParticipantsUpdate(); + return const CallState( + isConnected: false, + isMicrophoneEnabled: true, + isCameraEnabled: false, + isScreenSharing: false, + ); } + void _subscribeToParticipantsUpdate() { + // Only subscribe once + if (_wsSubscription != null) return; + final ws = ref.read(websocketProvider); + _wsSubscription = ws.dataStream.listen((packet) { + if (packet.type == 'call.participants.update' && packet.data != null) { + final participantsData = packet.data!["participants"]; + if (participantsData is List) { + final parsed = + participantsData + .map( + (e) => + CallParticipant.fromJson(Map.from(e)), + ) + .toList(); + _updateLiveParticipants(parsed); + } + } + }); + } + + void _initRoomListeners() { + if (_room == null) return; + _roomListener?.dispose(); + _roomListener = _room!.createListener(); + _room!.addListener(_onRoomChange); + _roomListener! + ..on((e) { + _refreshLiveParticipants(); + }) + ..on((e) { + _participants = []; + state = state.copyWith(); + }); + } + + void _onRoomChange() { + _refreshLiveParticipants(); + } + + void _refreshLiveParticipants() { + if (_room == null) return; + final remoteParticipants = _room!.remoteParticipants; + _participants = []; + // Add local participant first if available + if (_localParticipant != null) { + final localInfo = _buildParticipant(); + _participants.add( + CallParticipantLive( + participant: localInfo, + remoteParticipant: _localParticipant!, + ), + ); + } + // Add remote participants + _participants.addAll( + remoteParticipants.values.map((remote) { + final match = + _participantInfoByIdentity[remote.identity] ?? + CallParticipant( + identity: remote.identity, + name: remote.identity, + joinedAt: DateTime.now(), + accountId: null, + profile: null, + ); + return CallParticipantLive( + participant: match, + remoteParticipant: remote, + ); + }), + ); + state = state.copyWith(); + } + + /// Builds the CallParticipant object for the local participant. + /// Optionally, pass [participants] if you want to prioritize info from the latest list. + CallParticipant _buildParticipant({List? participants}) { + if (_localParticipant == null) { + throw StateError('No local participant available'); + } + // Prefer info from the latest participants list if available + if (participants != null) { + final idx = participants.indexWhere( + (p) => p.identity == _localParticipant!.identity, + ); + if (idx != -1) return participants[idx]; + } + + final userInfo = ref.read(userInfoProvider); + final roomIdentity = ref.read(chatroomIdentityProvider(_roomId)); + // Otherwise, use info from the identity map or fallback to minimal + return _participantInfoByIdentity[_localParticipant!.identity] ?? + CallParticipant( + identity: _localParticipant!.identity, + name: _localParticipant!.identity, + joinedAt: DateTime.now(), + accountId: userInfo.value?.id, + profile: roomIdentity.value, + ); + } + + void _updateLiveParticipants(List participants) { + // Update the info map for lookup + for (final p in participants) { + _participantInfoByIdentity[p.identity] = p; + } + if (_room == null) { + // Can't build live objects, just store empty + _participants = []; + state = state.copyWith(); + return; + } + final remoteParticipants = _room!.remoteParticipants; + final remotes = remoteParticipants.values.toList(); + _participants = []; + // Add local participant if present in the list + if (_localParticipant != null) { + final localInfo = _buildParticipant(participants: participants); + _participants.add( + CallParticipantLive( + participant: localInfo, + remoteParticipant: _localParticipant!, + ), + ); + } + // Add remote participants + _participants.addAll( + participants.map((p) { + RemoteParticipant? remote; + for (final r in remotes) { + if (r.identity == p.identity) { + remote = r; + break; + } + } + if (_localParticipant != null && + p.identity == _localParticipant!.identity) { + return null; // Already added local + } + return remote != null + ? CallParticipantLive(participant: p, remoteParticipant: remote) + : null; + }).whereType(), + ); + state = state.copyWith(); + } + + String? _roomId; + Future joinRoom(String roomId) async { + _roomId = roomId; try { final apiClient = ref.read(apiClientProvider); final response = await apiClient.get('/chat/realtime/$roomId/join'); if (response.statusCode == 200 && response.data != null) { final data = response.data; - final String endpoint = data['endpoint']; - final String token = data['token']; + // Parse join response + final joinResponse = ChatRealtimeJoinResponse.fromJson(data); + final participants = joinResponse.participants; + final String endpoint = joinResponse.endpoint; + final String token = joinResponse.token; // Connect to LiveKit _room = Room(); - await _room!.connect(endpoint, token); + + await _room!.connect( + endpoint, + token, + connectOptions: ConnectOptions(autoSubscribe: true), + roomOptions: RoomOptions(adaptiveStream: true, dynacast: true), + fastConnectOptions: FastConnectOptions( + microphone: TrackOption(enabled: true), + ), + ); _localParticipant = _room!.localParticipant; - // Create local audio track and publish - _localAudioTrack = await LocalAudioTrack.create(); - await _localParticipant!.publishAudioTrack(_localAudioTrack!); + + _initRoomListeners(); + _updateLiveParticipants(participants); // Listen for connection updates _room!.addListener(() { state = state.copyWith( isConnected: _room!.connectionState == ConnectionState.connected, + isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(), + isCameraEnabled: _localParticipant!.isCameraEnabled(), + isScreenSharing: _localParticipant!.isScreenShareEnabled(), ); }); state = state.copyWith(isConnected: true); @@ -57,27 +263,55 @@ class CallNotifier extends _$CallNotifier { } } - void toggleMute() { - final newMuted = !state.isMuted; - state = state.copyWith(isMuted: newMuted); - if (_localAudioTrack != null) { - if (newMuted) { - _localAudioTrack!.mute(); + Future toggleMicrophone() async { + if (_localParticipant != null) { + const autostop = true; + final target = !_localParticipant!.isMicrophoneEnabled(); + state = state.copyWith(isMicrophoneEnabled: target); + if (target) { + await _localParticipant!.audioTrackPublications.firstOrNull?.unmute( + stopOnMute: autostop, + ); } else { - _localAudioTrack!.unmute(); + await _localParticipant!.audioTrackPublications.firstOrNull?.mute( + stopOnMute: autostop, + ); } } } + Future toggleCamera() async { + if (_localParticipant != null) { + final target = !_localParticipant!.isCameraEnabled(); + state = state.copyWith(isCameraEnabled: target); + await _localParticipant!.setCameraEnabled(target); + } + } + + Future toggleScreenShare() async { + if (_localParticipant != null) { + final target = !_localParticipant!.isScreenShareEnabled(); + state = state.copyWith(isScreenSharing: target); + await _localParticipant!.setScreenShareEnabled(target); + } + } + Future disconnect() async { if (_room != null) { await _room!.disconnect(); - state = state.copyWith(isConnected: false); + state = state.copyWith( + isConnected: false, + isMicrophoneEnabled: false, + isCameraEnabled: false, + isScreenSharing: false, + ); } } void dispose() { - _localAudioTrack?.dispose(); + _wsSubscription?.cancel(); + _roomListener?.dispose(); + _room?.removeListener(_onRoomChange); _room?.dispose(); } } diff --git a/lib/pods/call.freezed.dart b/lib/pods/call.freezed.dart index 7e237ea..db900db 100644 --- a/lib/pods/call.freezed.dart +++ b/lib/pods/call.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$CallState { - bool get isMuted; bool get isConnected; String? get error; + bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; String? get error; /// Create a copy of CallState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,16 +26,16 @@ $CallStateCopyWith get copyWith => _$CallStateCopyWithImpl @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isMuted, isMuted) || other.isMuted == isMuted)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.error, error) || other.error == error)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.error, error) || other.error == error)); } @override -int get hashCode => Object.hash(runtimeType,isMuted,isConnected,error); +int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error); @override String toString() { - return 'CallState(isMuted: $isMuted, isConnected: $isConnected, error: $error)'; + return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, error: $error)'; } @@ -46,7 +46,7 @@ abstract mixin class $CallStateCopyWith<$Res> { factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl; @useResult $Res call({ - bool isMuted, bool isConnected, String? error + bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, String? error }); @@ -63,10 +63,12 @@ class _$CallStateCopyWithImpl<$Res> /// Create a copy of CallState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isMuted = null,Object? isConnected = null,Object? error = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? error = freezed,}) { return _then(_self.copyWith( -isMuted: null == isMuted ? _self.isMuted : isMuted // ignore: cast_nullable_to_non_nullable -as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable +as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable +as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as String?, )); @@ -79,11 +81,13 @@ as String?, class _CallState implements CallState { - const _CallState({required this.isMuted, required this.isConnected, this.error}); + const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.error}); -@override final bool isMuted; @override final bool isConnected; +@override final bool isMicrophoneEnabled; +@override final bool isCameraEnabled; +@override final bool isScreenSharing; @override final String? error; /// Create a copy of CallState @@ -96,16 +100,16 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isMuted, isMuted) || other.isMuted == isMuted)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.error, error) || other.error == error)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.error, error) || other.error == error)); } @override -int get hashCode => Object.hash(runtimeType,isMuted,isConnected,error); +int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error); @override String toString() { - return 'CallState(isMuted: $isMuted, isConnected: $isConnected, error: $error)'; + return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, error: $error)'; } @@ -116,7 +120,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl; @override @useResult $Res call({ - bool isMuted, bool isConnected, String? error + bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, String? error }); @@ -133,10 +137,12 @@ class __$CallStateCopyWithImpl<$Res> /// Create a copy of CallState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isMuted = null,Object? isConnected = null,Object? error = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? error = freezed,}) { return _then(_CallState( -isMuted: null == isMuted ? _self.isMuted : isMuted // ignore: cast_nullable_to_non_nullable -as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable +as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable +as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as String?, )); @@ -145,4 +151,152 @@ as String?, } +/// @nodoc +mixin _$CallParticipantLive { + + CallParticipant get participant; Participant get remoteParticipant; +/// Create a copy of CallParticipantLive +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CallParticipantLiveCopyWith get copyWith => _$CallParticipantLiveCopyWithImpl(this as CallParticipantLive, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipantLive&&(identical(other.participant, participant) || other.participant == participant)&&(identical(other.remoteParticipant, remoteParticipant) || other.remoteParticipant == remoteParticipant)); +} + + +@override +int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); + +@override +String toString() { + return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; +} + + +} + +/// @nodoc +abstract mixin class $CallParticipantLiveCopyWith<$Res> { + factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl; +@useResult +$Res call({ + CallParticipant participant, Participant remoteParticipant +}); + + +$CallParticipantCopyWith<$Res> get participant; + +} +/// @nodoc +class _$CallParticipantLiveCopyWithImpl<$Res> + implements $CallParticipantLiveCopyWith<$Res> { + _$CallParticipantLiveCopyWithImpl(this._self, this._then); + + final CallParticipantLive _self; + final $Res Function(CallParticipantLive) _then; + +/// Create a copy of CallParticipantLive +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? participant = null,Object? remoteParticipant = null,}) { + return _then(_self.copyWith( +participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable +as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable +as Participant, + )); +} +/// Create a copy of CallParticipantLive +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CallParticipantCopyWith<$Res> get participant { + + return $CallParticipantCopyWith<$Res>(_self.participant, (value) { + return _then(_self.copyWith(participant: value)); + }); +} +} + + +/// @nodoc + + +class _CallParticipantLive extends CallParticipantLive { + const _CallParticipantLive({required this.participant, required this.remoteParticipant}): super._(); + + +@override final CallParticipant participant; +@override final Participant remoteParticipant; + +/// Create a copy of CallParticipantLive +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CallParticipantLiveCopyWith<_CallParticipantLive> get copyWith => __$CallParticipantLiveCopyWithImpl<_CallParticipantLive>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipantLive&&(identical(other.participant, participant) || other.participant == participant)&&(identical(other.remoteParticipant, remoteParticipant) || other.remoteParticipant == remoteParticipant)); +} + + +@override +int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); + +@override +String toString() { + return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; +} + + +} + +/// @nodoc +abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallParticipantLiveCopyWith<$Res> { + factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl; +@override @useResult +$Res call({ + CallParticipant participant, Participant remoteParticipant +}); + + +@override $CallParticipantCopyWith<$Res> get participant; + +} +/// @nodoc +class __$CallParticipantLiveCopyWithImpl<$Res> + implements _$CallParticipantLiveCopyWith<$Res> { + __$CallParticipantLiveCopyWithImpl(this._self, this._then); + + final _CallParticipantLive _self; + final $Res Function(_CallParticipantLive) _then; + +/// Create a copy of CallParticipantLive +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? participant = null,Object? remoteParticipant = null,}) { + return _then(_CallParticipantLive( +participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable +as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable +as Participant, + )); +} + +/// Create a copy of CallParticipantLive +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CallParticipantCopyWith<$Res> get participant { + + return $CallParticipantCopyWith<$Res>(_self.participant, (value) { + return _then(_self.copyWith(participant: value)); + }); +} +} + // dart format on diff --git a/lib/pods/call.g.dart b/lib/pods/call.g.dart index 2c2c3c0..d5b41e4 100644 --- a/lib/pods/call.g.dart +++ b/lib/pods/call.g.dart @@ -6,7 +6,7 @@ part of 'call.dart'; // RiverpodGenerator // ************************************************************************** -String _$callNotifierHash() => r'c39e8d88673113bde0b14eb16cd9d86fa549e42c'; +String _$callNotifierHash() => r'5512070f943d98e999d97549c73e4d5f6e7b3ddd'; /// See also [CallNotifier]. @ProviderFor(CallNotifier) diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 4ff5bbd..ec5ca83 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -62,8 +62,8 @@ class AccountProfileRoute extends _i27.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => - AccountProfileRouteArgs(name: pathParams.getString('name')), + orElse: + () => AccountProfileRouteArgs(name: pathParams.getString('name')), ); return _i1.AccountProfileScreen(key: args.key, name: args.name); }, @@ -81,6 +81,16 @@ class AccountProfileRouteArgs { String toString() { return 'AccountProfileRouteArgs{key: $key, name: $name}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AccountProfileRouteArgs) return false; + return key == other.key && name == other.name; + } + + @override + int get hashCode => key.hashCode ^ name.hashCode; } /// generated route for @@ -120,6 +130,16 @@ class AccountRouteArgs { String toString() { return 'AccountRouteArgs{key: $key, isAside: $isAside}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AccountRouteArgs) return false; + return key == other.key && isAside == other.isAside; + } + + @override + int get hashCode => key.hashCode ^ isAside.hashCode; } /// generated route for @@ -193,6 +213,16 @@ class CallRouteArgs { String toString() { return 'CallRouteArgs{key: $key, roomId: $roomId}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! CallRouteArgs) return false; + return key == other.key && roomId == other.roomId; + } + + @override + int get hashCode => key.hashCode ^ roomId.hashCode; } /// generated route for @@ -234,6 +264,16 @@ class ChatDetailRouteArgs { String toString() { return 'ChatDetailRouteArgs{key: $key, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ChatDetailRouteArgs) return false; + return key == other.key && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ id.hashCode; } /// generated route for @@ -273,6 +313,16 @@ class ChatListRouteArgs { String toString() { return 'ChatListRouteArgs{key: $key, isAside: $isAside}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ChatListRouteArgs) return false; + return key == other.key && isAside == other.isAside; + } + + @override + int get hashCode => key.hashCode ^ isAside.hashCode; } /// generated route for @@ -314,6 +364,16 @@ class ChatRoomRouteArgs { String toString() { return 'ChatRoomRouteArgs{key: $key, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ChatRoomRouteArgs) return false; + return key == other.key && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ id.hashCode; } /// generated route for @@ -385,6 +445,16 @@ class CreatorHubRouteArgs { String toString() { return 'CreatorHubRouteArgs{key: $key, isAside: $isAside}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! CreatorHubRouteArgs) return false; + return key == other.key && isAside == other.isAside; + } + + @override + int get hashCode => key.hashCode ^ isAside.hashCode; } /// generated route for @@ -439,6 +509,16 @@ class EditChatRouteArgs { String toString() { return 'EditChatRouteArgs{key: $key, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EditChatRouteArgs) return false; + return key == other.key && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ id.hashCode; } /// generated route for @@ -480,6 +560,16 @@ class EditPublisherRouteArgs { String toString() { return 'EditPublisherRouteArgs{key: $key, name: $name}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EditPublisherRouteArgs) return false; + return key == other.key && name == other.name; + } + + @override + int get hashCode => key.hashCode ^ name.hashCode; } /// generated route for @@ -521,6 +611,16 @@ class EditRealmRouteArgs { String toString() { return 'EditRealmRouteArgs{key: $key, slug: $slug}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EditRealmRouteArgs) return false; + return key == other.key && slug == other.slug; + } + + @override + int get hashCode => key.hashCode ^ slug.hashCode; } /// generated route for @@ -550,10 +650,11 @@ class EditStickerPacksRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => EditStickerPacksRouteArgs( - pubName: pathParams.getString('name'), - packId: pathParams.optString('packId'), - ), + orElse: + () => EditStickerPacksRouteArgs( + pubName: pathParams.getString('name'), + packId: pathParams.optString('packId'), + ), ); return _i12.EditStickerPacksScreen( key: args.key, @@ -581,6 +682,18 @@ class EditStickerPacksRouteArgs { String toString() { return 'EditStickerPacksRouteArgs{key: $key, pubName: $pubName, packId: $packId}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EditStickerPacksRouteArgs) return false; + return key == other.key && + pubName == other.pubName && + packId == other.packId; + } + + @override + int get hashCode => key.hashCode ^ pubName.hashCode ^ packId.hashCode; } /// generated route for @@ -605,10 +718,11 @@ class EditStickersRoute extends _i27.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => EditStickersRouteArgs( - packId: pathParams.getString('packId'), - id: pathParams.optString('id'), - ), + orElse: + () => EditStickersRouteArgs( + packId: pathParams.getString('packId'), + id: pathParams.optString('id'), + ), ); return _i13.EditStickersScreen( key: args.key, @@ -636,6 +750,16 @@ class EditStickersRouteArgs { String toString() { return 'EditStickersRouteArgs{key: $key, packId: $packId, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EditStickersRouteArgs) return false; + return key == other.key && packId == other.packId && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ packId.hashCode ^ id.hashCode; } /// generated route for @@ -659,8 +783,8 @@ class EventCalanderRoute extends _i27.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => - EventCalanderRouteArgs(name: pathParams.getString('name')), + orElse: + () => EventCalanderRouteArgs(name: pathParams.getString('name')), ); return _i14.EventCalanderScreen(key: args.key, name: args.name); }, @@ -678,6 +802,16 @@ class EventCalanderRouteArgs { String toString() { return 'EventCalanderRouteArgs{key: $key, name: $name}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EventCalanderRouteArgs) return false; + return key == other.key && name == other.name; + } + + @override + int get hashCode => key.hashCode ^ name.hashCode; } /// generated route for @@ -717,6 +851,16 @@ class ExploreRouteArgs { String toString() { return 'ExploreRouteArgs{key: $key, isAside: $isAside}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ExploreRouteArgs) return false; + return key == other.key && isAside == other.isAside; + } + + @override + int get hashCode => key.hashCode ^ isAside.hashCode; } /// generated route for @@ -821,8 +965,9 @@ class NewStickerPacksRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => - NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), + orElse: + () => + NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), ); return _i12.NewStickerPacksScreen(key: args.key, pubName: args.pubName); }, @@ -840,6 +985,16 @@ class NewStickerPacksRouteArgs { String toString() { return 'NewStickerPacksRouteArgs{key: $key, pubName: $pubName}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! NewStickerPacksRouteArgs) return false; + return key == other.key && pubName == other.pubName; + } + + @override + int get hashCode => key.hashCode ^ pubName.hashCode; } /// generated route for @@ -863,8 +1018,8 @@ class NewStickersRoute extends _i27.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => - NewStickersRouteArgs(packId: pathParams.getString('packId')), + orElse: + () => NewStickersRouteArgs(packId: pathParams.getString('packId')), ); return _i13.NewStickersScreen(key: args.key, packId: args.packId); }, @@ -882,6 +1037,16 @@ class NewStickersRouteArgs { String toString() { return 'NewStickersRouteArgs{key: $key, packId: $packId}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! NewStickersRouteArgs) return false; + return key == other.key && packId == other.packId; + } + + @override + int get hashCode => key.hashCode ^ packId.hashCode; } /// generated route for @@ -940,6 +1105,16 @@ class PostComposeRouteArgs { String toString() { return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PostComposeRouteArgs) return false; + return key == other.key && originalPost == other.originalPost; + } + + @override + int get hashCode => key.hashCode ^ originalPost.hashCode; } /// generated route for @@ -981,6 +1156,16 @@ class PostDetailRouteArgs { String toString() { return 'PostDetailRouteArgs{key: $key, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PostDetailRouteArgs) return false; + return key == other.key && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ id.hashCode; } /// generated route for @@ -1022,6 +1207,16 @@ class PostEditRouteArgs { String toString() { return 'PostEditRouteArgs{key: $key, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PostEditRouteArgs) return false; + return key == other.key && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ id.hashCode; } /// generated route for @@ -1046,8 +1241,8 @@ class PublisherProfileRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => - PublisherProfileRouteArgs(name: pathParams.getString('name')), + orElse: + () => PublisherProfileRouteArgs(name: pathParams.getString('name')), ); return _i20.PublisherProfileScreen(key: args.key, name: args.name); }, @@ -1065,6 +1260,16 @@ class PublisherProfileRouteArgs { String toString() { return 'PublisherProfileRouteArgs{key: $key, name: $name}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PublisherProfileRouteArgs) return false; + return key == other.key && name == other.name; + } + + @override + int get hashCode => key.hashCode ^ name.hashCode; } /// generated route for @@ -1106,6 +1311,16 @@ class RealmDetailRouteArgs { String toString() { return 'RealmDetailRouteArgs{key: $key, slug: $slug}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! RealmDetailRouteArgs) return false; + return key == other.key && slug == other.slug; + } + + @override + int get hashCode => key.hashCode ^ slug.hashCode; } /// generated route for @@ -1179,10 +1394,11 @@ class StickerPackDetailRoute builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => StickerPackDetailRouteArgs( - pubName: pathParams.getString('name'), - id: pathParams.getString('packId'), - ), + orElse: + () => StickerPackDetailRouteArgs( + pubName: pathParams.getString('name'), + id: pathParams.getString('packId'), + ), ); return _i13.StickerPackDetailScreen( key: args.key, @@ -1210,6 +1426,16 @@ class StickerPackDetailRouteArgs { String toString() { return 'StickerPackDetailRouteArgs{key: $key, pubName: $pubName, id: $id}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! StickerPackDetailRouteArgs) return false; + return key == other.key && pubName == other.pubName && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ pubName.hashCode ^ id.hashCode; } /// generated route for @@ -1251,6 +1477,16 @@ class StickersRouteArgs { String toString() { return 'StickersRouteArgs{key: $key, pubName: $pubName}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! StickersRouteArgs) return false; + return key == other.key && pubName == other.pubName; + } + + @override + int get hashCode => key.hashCode ^ pubName.hashCode; } /// generated route for @@ -1300,6 +1536,16 @@ class TabsNavigationWidgetArgs { String toString() { return 'TabsNavigationWidgetArgs{key: $key, child: $child, router: $router}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! TabsNavigationWidgetArgs) return false; + return key == other.key && child == other.child && router == other.router; + } + + @override + int get hashCode => key.hashCode ^ child.hashCode ^ router.hashCode; } /// generated route for diff --git a/lib/screens/chat/call.dart b/lib/screens/chat/call.dart index a131dc5..86c62e0 100644 --- a/lib/screens/chat/call.dart +++ b/lib/screens/chat/call.dart @@ -1,8 +1,18 @@ import 'package:auto_route/annotations.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/call.dart'; +import 'package:island/pods/userinfo.dart'; +import 'package:island/screens/chat/chat.dart'; +import 'package:island/services/responsive.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/chat/call_button.dart'; +import 'package:island/widgets/chat/call_participant_tile.dart'; +import 'package:livekit_client/livekit_client.dart'; +import 'package:styled_widget/styled_widget.dart'; @RoutePage() class CallScreen extends HookConsumerWidget { @@ -11,6 +21,9 @@ class CallScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final ongoingCall = ref.watch(ongoingCallProvider(roomId)); + final userInfo = ref.watch(userInfoProvider); + final chatRoom = ref.watch(chatroomProvider(roomId)); final callState = ref.watch(callNotifierProvider); final callNotifier = ref.read(callNotifierProvider.notifier); @@ -19,25 +32,327 @@ class CallScreen extends HookConsumerWidget { return null; }, []); - return Scaffold( - appBar: AppBar(title: const Text('Audio Call')), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + final actionButtonStyle = ButtonStyle( + minimumSize: const MaterialStatePropertyAll(Size(24, 24)), + ); + + final viewMode = useState('grid'); + + return AppScaffold( + appBar: AppBar( + leading: PageBackButton( + onWillPop: () { + callNotifier.disconnect().then((_) { + callNotifier.dispose(); + }); + }, + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (callState.error != null) - Text(callState.error!, style: const TextStyle(color: Colors.red)), - IconButton( - icon: Icon(callState.isMuted ? Icons.mic_off : Icons.mic), - onPressed: callNotifier.toggleMute, + Text( + chatRoom.whenOrNull()?.name ?? 'loading'.tr(), + style: const TextStyle(fontSize: 16), + ), + Text( + callState.isConnected + ? Duration( + milliseconds: + (DateTime.now().millisecondsSinceEpoch - + (ongoingCall + .value + ?.createdAt + .millisecondsSinceEpoch ?? + 0)), + ).toString() + : 'Connecting', + style: const TextStyle(fontSize: 14), ), - if (callState.isConnected) - const Text('Connected') - else - const CircularProgressIndicator(), ], ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: Icon(Icons.grid_view), + tooltip: 'Grid View', + onPressed: () => viewMode.value = 'grid', + color: + viewMode.value == 'grid' + ? Theme.of(context).colorScheme.primary + : null, + ), + IconButton( + icon: Icon(Icons.view_agenda), + tooltip: 'Stage View', + onPressed: () => viewMode.value = 'stage', + color: + viewMode.value == 'stage' + ? Theme.of(context).colorScheme.primary + : null, + ), + ], + ), + const Gap(8), + ], ), + body: + callState.error != null + ? Center( + child: Text( + callState.error!, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.red), + ), + ) + : Column( + children: [ + Card( + margin: const EdgeInsets.only(left: 12, right: 12, top: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Builder( + builder: (context) { + if (callNotifier.localParticipant == null) { + return CircularProgressIndicator().center(); + } + return SizedBox( + width: 40, + height: 40, + child: + SpeakingRippleAvatar( + isSpeaking: + callNotifier + .localParticipant! + .isSpeaking, + audioLevel: + callNotifier + .localParticipant! + .audioLevel, + pictureId: + userInfo.value?.profile.pictureId, + size: 36, + ).center(), + ); + }, + ), + ], + ), + ), + IconButton( + icon: Icon( + callState.isMicrophoneEnabled + ? Icons.mic + : Icons.mic_off, + ), + onPressed: () { + callNotifier.toggleMicrophone(); + }, + style: actionButtonStyle, + ), + IconButton( + icon: Icon( + callState.isCameraEnabled + ? Icons.videocam + : Icons.videocam_off, + ), + onPressed: () { + callNotifier.toggleCamera(); + }, + style: actionButtonStyle, + ), + IconButton( + icon: Icon( + callState.isScreenSharing + ? Icons.stop_screen_share + : Icons.screen_share, + ), + onPressed: () { + callNotifier.toggleScreenShare(); + }, + style: actionButtonStyle, + ), + ], + ).padding(all: 16), + ), + Expanded( + child: Builder( + builder: (context) { + if (!callState.isConnected) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (callNotifier.participants.isEmpty) { + return const Center( + child: Text('No participants in call'), + ); + } + final participants = callNotifier.participants; + final allAudioOnly = participants.every( + (p) => + !(p.hasVideo && + p.remoteParticipant.trackPublications.values + .any( + (pub) => + pub.track != null && + pub.kind == TrackType.VIDEO, + )), + ); + if (allAudioOnly) { + // Audio-only: show avatars in a compact row + return Center( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, + children: [ + for (final live in participants) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: SpeakingRippleAvatar( + isSpeaking: live.isSpeaking, + audioLevel: + live.remoteParticipant.audioLevel, + pictureId: + live + .participant + .profile + ?.account + .profile + .pictureId, + size: 72, + ), + ), + ], + ), + ), + ); + } + if (viewMode.value == 'stage') { + // Stage view: show main speaker(s) large, others in row + final mainSpeakers = + participants + .where( + (p) => p + .remoteParticipant + .trackPublications + .values + .any( + (pub) => + pub.track != null && + pub.kind == TrackType.VIDEO, + ), + ) + .toList(); + if (mainSpeakers.isEmpty && participants.isNotEmpty) { + mainSpeakers.add(participants.first); + } + final others = + participants + .where((p) => !mainSpeakers.contains(p)) + .toList(); + return Column( + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final speaker in mainSpeakers) + Expanded( + child: + AspectRatio( + aspectRatio: 16 / 9, + child: Card( + margin: EdgeInsets.zero, + child: ClipRRect( + borderRadius: + BorderRadius.circular(8), + child: Column( + children: [ + CallParticipantTile( + live: speaker, + ), + ], + ), + ), + ), + ).center(), + ), + ], + ).padding(horizontal: 12), + ), + if (others.isNotEmpty) + SizedBox( + height: 100, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (final other in others) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: CallParticipantTile( + live: other, + ), + ), + ], + ), + ), + ], + ); + } + // Default: grid view + return GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + isWidestScreen(context) + ? 4 + : isWiderScreen(context) + ? 3 + : 2, + childAspectRatio: 16 / 9, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: participants.length, + itemBuilder: (context, idx) { + final live = participants[idx]; + return AspectRatio( + aspectRatio: 16 / 9, + child: Card( + margin: EdgeInsets.zero, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Column( + children: [CallParticipantTile(live: live)], + ), + ), + ), + ).center(); + }, + ); + }, + ), + ), + ], + ), ); } } diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index d9b57f5..dbcec03 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -244,6 +244,7 @@ class ChatListScreen extends HookConsumerWidget { Tab( child: Text( 'chatTabAll'.tr(), + textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).appBarTheme.foregroundColor!, ), @@ -252,6 +253,7 @@ class ChatListScreen extends HookConsumerWidget { Tab( child: Text( 'chatTabDirect'.tr(), + textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).appBarTheme.foregroundColor!, ), @@ -260,6 +262,7 @@ class ChatListScreen extends HookConsumerWidget { Tab( child: Text( 'chatTabGroup'.tr(), + textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).appBarTheme.foregroundColor!, ), diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 73fa9ea..3965e9b 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -179,12 +179,14 @@ class AppScaffold extends StatelessWidget { class PageBackButton extends StatelessWidget { final List? shadows; - const PageBackButton({super.key, this.shadows}); + final VoidCallback? onWillPop; + const PageBackButton({super.key, this.shadows, this.onWillPop}); @override Widget build(BuildContext context) { return IconButton( onPressed: () { + onWillPop?.call(); context.router.maybePop(); }, icon: Icon( diff --git a/lib/widgets/chat/call_participant_tile.dart b/lib/widgets/chat/call_participant_tile.dart new file mode 100644 index 0000000..87cb63e --- /dev/null +++ b/lib/widgets/chat/call_participant_tile.dart @@ -0,0 +1,114 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:island/pods/call.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:livekit_client/livekit_client.dart'; + +class SpeakingRippleAvatar extends StatelessWidget { + final bool isSpeaking; + final double audioLevel; + final String? pictureId; + final double size; + + const SpeakingRippleAvatar({ + super.key, + required this.isSpeaking, + required this.audioLevel, + required this.pictureId, + this.size = 96, + }); + + @override + Widget build(BuildContext context) { + final avatarRadius = size / 2; + final clampedLevel = audioLevel.clamp(0.0, 1.0); + final rippleRadius = avatarRadius + clampedLevel * (size * 0.333); + return TweenAnimationBuilder( + tween: Tween( + begin: avatarRadius, + end: isSpeaking ? rippleRadius : avatarRadius, + ), + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + builder: (context, animatedRadius, child) { + return Stack( + alignment: Alignment.center, + children: [ + if (isSpeaking) + Container( + width: animatedRadius * 2, + height: animatedRadius * 2, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel), + ), + ), + Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration(shape: BoxShape.circle), + child: ProfilePictureWidget(fileId: pictureId, radius: size / 2), + ), + ], + ); + }, + ); + } +} + +class CallParticipantTile extends StatelessWidget { + final CallParticipantLive live; + + const CallParticipantTile({super.key, required this.live}); + + @override + Widget build(BuildContext context) { + final hasVideo = + live.hasVideo && + live.remoteParticipant.trackPublications.values + .where((pub) => pub.track != null && pub.kind == TrackType.VIDEO) + .isNotEmpty; + final audioLevel = live.remoteParticipant.audioLevel; + + if (hasVideo) { + return Stack( + fit: StackFit.loose, + children: [ + Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: AspectRatio( + aspectRatio: 16 / 9, + child: VideoTrackRenderer( + live.remoteParticipant.trackPublications.values + .where((track) => track.kind == TrackType.VIDEO) + .first + .track + as VideoTrack, + renderMode: VideoRenderMode.platformView, + ), + ), + ), + Positioned( + left: 8, + right: 8, + bottom: 8, + child: Text( + live.participant.profile?.account.nick ?? + '${'unknown'.tr()}\'s video', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ], + ); + } else { + return SpeakingRippleAvatar( + isSpeaking: live.isSpeaking, + audioLevel: audioLevel, + pictureId: live.participant.profile?.account.profile.pictureId, + size: 84, + ); + } + } +} diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 98eb9be..a25f670 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -66,7 +66,7 @@ @@ -83,7 +83,7 @@ diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index f8c8b25..5cdcb7b 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,6 +8,8 @@ com.apple.security.device.audio-input + com.apple.security.device.camera + com.apple.security.files.downloads.read-write com.apple.security.files.user-selected.read-only diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..986d2a4 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,17 @@ com.apple.security.app-sandbox + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + diff --git a/pubspec.lock b/pubspec.lock index d3a99fa..f60d709 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,18 +77,18 @@ packages: dependency: "direct main" description: name: auto_route - sha256: "89bc5d17d8c575399891194b8cd02b39f52a8512c730052f17ebe443cdcb9109" + sha256: eae18fcd3e3762eb6074a3560c0f411d1e36bd9f8d3eed9c15ed1c577e8d1815 url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "10.1.0" auto_route_generator: dependency: "direct dev" description: name: auto_route_generator - sha256: "8e622d26dc6be4bf496d47969e3e9ba555c3abcf2290da6abfa43cbd4f57fa52" + sha256: "9e3846fcbeacba5c362557328dd8c8fbc953b6a0cbc3395365e8d8f92eea29c4" url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "10.1.0" avatar_stack: dependency: "direct main" description: @@ -1225,10 +1225,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: d45b6c36c3effa8cb51b1afb8698107d5ff1f88fa4631428f34a8a01abc295d7 + sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b" url: "https://pub.dev" source: hosted - version: "4.2815.0" + version: "4.2815.1" media_kit: dependency: "direct main" description: